Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#RIVS-299 - Add the changes from the main project #224

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"Key Name": "Key Name",
" will be deleted.": " will be deleted.",
"Delete": "Delete",
"will be deleted from Redis for VS Code.": "will be deleted from Redis for VS Code.",
"will be removed from Redis for VS Code.": "will be removed from Redis for VS Code.",
"Key Size": "Key Size",
"Key Size: ": "Key Size: ",
"Length": "Length",
Expand Down Expand Up @@ -295,9 +295,6 @@
"Host:": "Host:",
"Database Index:": "Database Index:",
"Modules:": "Modules:",
"Select Logical Database": "Select Logical Database",
"Database Index": "Database Index",
"Enter Database Index": "Enter Database Index",
"No decompression": "No decompression",
"Enable automatic data decompression": "Enable automatic data decompression",
"Decompression format": "Decompression format",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"download:backend": "tsc ./scripts/downloadBackend.ts && node ./scripts/downloadBackend.js",
"dev": "vite dev",
"dev:key": "cross-env RI_DATA_ROUTE=main/key vite dev",
"dev:database": "cross-env RI_DATA_ROUTE=main/add_database vite dev",
"dev:sidebar": "cross-env RI_DATA_ROUTE=sidebar vite dev",
"l10n:collect": "npx @vscode/l10n-dev export -o ./l10n ./src",
"watch": "tsc -watch -p ./",
Expand Down Expand Up @@ -286,7 +287,7 @@
"react-inlinesvg": "^4.1.1",
"react-monaco-editor": "^0.55.0",
"react-router-dom": "^6.17.0",
"react-select": "^5.8.0",
"react-select": "^5.8.3",
"react-spinners": "^0.13.8",
"react-virtualized": "^9.22.5",
"react-virtualized-auto-sizer": "^1.0.20",
Expand Down
103 changes: 67 additions & 36 deletions src/webviews/src/components/database-form/TlsDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,61 @@ import React, { ChangeEvent, useId } from 'react'
import cx from 'classnames'
import { FormikProps } from 'formik'
import * as l10n from '@vscode/l10n'
import { VSCodeDivider } from '@vscode/webview-ui-toolkit/react'
import { CheckboxChangeEvent } from 'rc-checkbox'
import { find } from 'lodash'
import { MenuListProps } from 'react-select'

import { validateCertName, validateField } from 'uiSrc/utils'
import { sendEventTelemetry, TelemetryEvent, validateCertName, validateField } from 'uiSrc/utils'
import {
ADD_NEW,
ADD_NEW_CA_CERT,
ADD_NEW_CA_CERT_LABEL,
ADD_NEW_LABEL,
ApiEndpoints,
NO_CA_CERT,
NO_CA_CERT_LABEL,
} from 'uiSrc/constants'
import { DbConnectionInfo } from 'uiSrc/interfaces'
import { Checkbox, InputText, Select, TextArea } from 'uiSrc/ui'
import { DbConnectionInfo, RedisString } from 'uiSrc/interfaces'
import { Checkbox, InputText, TextArea } from 'uiSrc/ui'
import { removeCertAction } from 'uiSrc/store'
import { SuperSelectRemovableOption, SuperSelect, SuperSelectOption } from 'uiSrc/components'
import styles from './styles.module.scss'

const suffix = '_tls_details'

export interface Props {
formik: FormikProps<DbConnectionInfo>
caCertificates?: { id: string, name: string }[]
certificates?: { id: string, name: string }[]
}

const TlsDetails = (props: Props) => {
const { formik, caCertificates, certificates } = props
const id = useId()

const optionsCertsCA = [
{
value: NO_CA_CERT,
label: NO_CA_CERT_LABEL,
},
{
value: ADD_NEW_CA_CERT,
label: ADD_NEW_CA_CERT_LABEL,
},
const handleDeleteCaCert = (id: RedisString, onSuccess?: () => void) => {
removeCertAction(id, ApiEndpoints.CA_CERTIFICATES, () => {
onSuccess?.()
handleClickDeleteCert('CA')
})
}

const handleDeleteClientCert = (id: RedisString, onSuccess?: () => void) => {
removeCertAction(id, ApiEndpoints.CLIENT_CERTIFICATES, () => {
onSuccess?.()
handleClickDeleteCert('Client')
})
}

const handleClickDeleteCert = (certificateType: 'Client' | 'CA') => {
sendEventTelemetry({
event: TelemetryEvent.CONFIG_DATABASES_CERTIFICATE_REMOVED,
eventData: {
certificateType,
},
})
}

const optionsCertsCA: SuperSelectOption[] = [
NO_CA_CERT,
ADD_NEW_CA_CERT,
]

caCertificates?.forEach((cert) => {
Expand All @@ -45,12 +66,7 @@ const TlsDetails = (props: Props) => {
})
})

const optionsCertsClient = [
{
value: ADD_NEW,
label: ADD_NEW_LABEL,
},
]
const optionsCertsClient: SuperSelectOption[] = [ADD_NEW]

certificates?.forEach((cert) => {
optionsCertsClient.push({
Expand Down Expand Up @@ -130,25 +146,30 @@ const TlsDetails = (props: Props) => {
<div className="w-[100px]">
{`${l10n.t('CA Certificate')}${formik.values.verifyServerTlsCert ? '*' : ''}`}
</div>
<Select
// name="selectedCaCertName"
// placeholder="Select CA certificate"
<SuperSelect
containerClassName="database-form-select w-[256px]"
itemClassName="database-form-select__option"
idSelected={formik.values.selectedCaCertName ?? NO_CA_CERT}
selectedOption={find(optionsCertsCA, { value: formik.values.selectedCaCertName }) as SuperSelectOption ?? NO_CA_CERT}
options={optionsCertsCA}
onChange={(value) => {
components={{ MenuList: (props: MenuListProps<SuperSelectOption, false>) => (
<SuperSelectRemovableOption
{...props}
suffix={suffix}
countDefaultOptions={2}
onDeleteOption={handleDeleteCaCert}
/>
) }}
onChange={(option) => {
formik.setFieldValue(
'selectedCaCertName',
value || NO_CA_CERT,
option?.value || NO_CA_CERT?.value,
)
}}
testid="select-ca-cert"
/>
</div>

{formik.values.tls
&& formik.values.selectedCaCertName === ADD_NEW_CA_CERT && (
&& formik.values.selectedCaCertName === ADD_NEW_CA_CERT.value && (
<div className="mb-3">
<InputText
name="newCaCertName"
Expand All @@ -170,7 +191,7 @@ const TlsDetails = (props: Props) => {
</div>

{formik.values.tls
&& formik.values.selectedCaCertName === ADD_NEW_CA_CERT && (
&& formik.values.selectedCaCertName === ADD_NEW_CA_CERT.value && (
<div>
<TextArea
name="newCaCert"
Expand Down Expand Up @@ -203,13 +224,23 @@ const TlsDetails = (props: Props) => {
<div>
<div className="flex items-center pb-3">
<div className="w-[100px]">{l10n.t('Client Certificate*')}</div>
<Select
<SuperSelect
containerClassName="database-form-select w-[256px]"
itemClassName="database-form-select__option"
selectedOption={find(optionsCertsClient, { value: formik.values.selectedTlsClientCertId }) as SuperSelectOption ?? ADD_NEW}
options={optionsCertsClient}
idSelected={formik.values.selectedTlsClientCertId ?? ADD_NEW}
onChange={(value) => {
formik.setFieldValue('selectedTlsClientCertId', value)
components={{ MenuList: (props: MenuListProps<SuperSelectOption, false>) => (
<SuperSelectRemovableOption
{...props}
countDefaultOptions={1}
suffix={suffix}
onDeleteOption={handleDeleteClientCert}
/>
) }}
onChange={(option) => {
formik.setFieldValue(
'selectedTlsClientCertId',
option?.value || ADD_NEW?.value,
)
}}
testid="select-cert"
/>
Expand Down
4 changes: 4 additions & 0 deletions src/webviews/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export { FieldMessage } from './field-message/FieldMessage'
export { NoDatabases } from './no-databases/NoDatabases'
export { MonacoLanguages } from './monaco-languages/MonacoLanguages'
export { UploadFile } from './upload-file/UploadFile'
export { SuperSelect } from './super-select/SuperSelect'
export { SuperSelectRemovableOption } from './super-select/components/removable-option/RemovableOption'
export * from './database-form'
export * from './consents-option'
export * from './consents-privacy'

export type { SuperSelectOption } from './super-select/SuperSelect'
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const PopoverDelete = (props: Props) => {
<Popup
key={item}
ref={ref}
nested={false}
closeOnEscape
closeOnDocumentClick
repositionOnResize
Expand Down
43 changes: 43 additions & 0 deletions src/webviews/src/components/super-select/SuperSelect.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react'
import { instance, mock } from 'ts-mockito'

import { render, constants, fireEvent, waitFor } from 'testSrc/helpers'
import { SuperSelect, Props } from './SuperSelect'

const mockedProps = mock<Props>()
const testIdMock = 'my-select-component'

describe('SuperSelect', () => {
it('should render', async () => {
expect(render(
<SuperSelect {...instance(mockedProps)} options={constants.SUPER_SELECT_OPTIONS} />),
).toBeTruthy()
})

it('should call onChange when the first option is selected', async () => {
const mockedOnChange = vi.fn()
const mockedLabel = constants.SUPER_SELECT_OPTIONS?.[0].label ?? ''
const mockedValue = constants.SUPER_SELECT_OPTIONS?.[0].value ?? ''

const { getByText, queryByTestId } = render(<SuperSelect
options={constants.SUPER_SELECT_OPTIONS}
onChange={mockedOnChange}
testid={testIdMock}
/>)

const mySelectComponent = queryByTestId('my-select-component')

expect(mySelectComponent).toBeDefined()
expect(mySelectComponent).not.toBeNull()
expect(mockedOnChange).toHaveBeenCalledTimes(0)

fireEvent.keyDown(mySelectComponent?.firstChild!, { key: 'ArrowDown' })
await waitFor(() => getByText(mockedLabel))
fireEvent.click(getByText(mockedLabel))

expect(mockedOnChange).toHaveBeenCalledTimes(1)
expect(mockedOnChange).toHaveBeenCalledWith(
{ label: mockedLabel, value: mockedValue },
{ action: 'select-option', name: undefined, option: undefined })
})
})
51 changes: 51 additions & 0 deletions src/webviews/src/components/super-select/SuperSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { FC } from 'react'
import cx from 'classnames'
import Select, { Props as SelectProps } from 'react-select'

import { Maybe } from 'uiSrc/interfaces'
import styles from './styles.module.scss'

export interface SuperSelectOption {
value: string
label: string
testid?: string
}

export interface Props extends SelectProps<SuperSelectOption, false> {
selectedOption?: Maybe<SuperSelectOption>
containerClassName?: string
itemClassName?: string
testid?: string
}

const SuperSelect: FC<Props> = (props) => {
const {
selectedOption,
containerClassName,
testid,
} = props

return (
<div className={cx(styles.container, containerClassName)} data-testid={testid}>
<Select<SuperSelectOption>
{...props}
closeMenuOnSelect
closeMenuOnScroll
isSearchable={false}
isMulti={false}
value={selectedOption}
classNames={{
container: () => styles.selectContainer,
control: () => styles.control,
option: ({ isSelected }) => cx(styles.option, { [styles.optionSelected]: isSelected }),
singleValue: () => styles.singleValue,
menu: () => styles.menu,
indicatorsContainer: () => styles.indicatorsContainer,
indicatorSeparator: () => 'hidden',
}}
/>
</div>
)
}

export { SuperSelect }
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'
import { instance, mock } from 'ts-mockito'

import { render, constants } from 'testSrc/helpers'
import { SuperSelectRemovableOption, Props } from './RemovableOption'

const mockedProps = mock<Props>()

describe('SuperSelectRemovableOption', () => {
it('should render', async () => {
expect(render(<SuperSelectRemovableOption {...instance(mockedProps)} options={constants.SUPER_SELECT_OPTIONS} />)).toBeTruthy()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { Children, useCallback, useState } from 'react'
import { MenuListProps } from 'react-select'
import cx from 'classnames'
import * as l10n from '@vscode/l10n'

import { PopoverDelete, SuperSelectOption } from 'uiSrc/components'
import { Maybe, RedisString } from 'uiSrc/interfaces'

import styles from '../../styles.module.scss'

export interface Props extends MenuListProps<SuperSelectOption, false> {
suffix: string
countDefaultOptions: number
onDeleteOption: (id: RedisString, onSuccess: () => void) => void
}

const SuperSelectRemovableOption = (props: Props) => {
const { suffix, countDefaultOptions = 0, options, children, getValue, selectOption, onDeleteOption } = props

const [activeOptionId, setActiveOptionId] = useState<Maybe<string>>(undefined)

const showPopover = useCallback((id = '') => {
setActiveOptionId(`${id}${suffix}`)
}, [])

const handleRemoveOption = (id: RedisString) => {
onDeleteOption(id, () => {
const selectedValue = getValue()?.[0]
setActiveOptionId(undefined)

// reset selected option if removed value is selected
if (selectedValue?.value === id) {
selectOption(options?.[0] as SuperSelectOption)
} else {
selectOption(selectedValue)
}
})
}

return (
<div>
{Children.map(children, (child, i) => (
<div key={(options[i] as SuperSelectOption).value} className={cx(styles.option, 'flex justify-between items-center relative')}>
{child}
{i + 1 > countDefaultOptions && <PopoverDelete
header={`${(options[i] as SuperSelectOption).label}`}
text={l10n.t('will be removed from Redis for VS Code.')}
item={(options[i] as SuperSelectOption).value}
suffix={suffix}
triggerClassName='absolute right-2.5'
position='right center'
deleting={activeOptionId}
showPopover={showPopover}
handleDeleteItem={handleRemoveOption}
testid={`delete-option-${(options[i] as SuperSelectOption).value}`}
/>}
</div>
))}
</div>
)
}

export { SuperSelectRemovableOption }
Loading
Loading