Skip to content

Commit

Permalink
Merge pull request #224 from RedisInsight/feature/RIVS-299_Add_featur…
Browse files Browse the repository at this point in the history
…e_from_RI

#RIVS-299 - Add the changes from the main project
  • Loading branch information
vlad-dargel authored Dec 17, 2024
2 parents 31c9472 + 9330dea commit 0736188
Show file tree
Hide file tree
Showing 39 changed files with 769 additions and 92 deletions.
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 @@ -287,7 +288,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,7 +10,11 @@ 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 { AutoRefresh } from './auto-refresh/AutoRefresh'
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

0 comments on commit 0736188

Please sign in to comment.