diff --git a/redisinsight/ui/src/pages/home/HomePage.tsx b/redisinsight/ui/src/pages/home/HomePage.tsx index a4c8fc529b..75faed434e 100644 --- a/redisinsight/ui/src/pages/home/HomePage.tsx +++ b/redisinsight/ui/src/pages/home/HomePage.tsx @@ -2,8 +2,9 @@ import { EuiPage, EuiPageBody, EuiResizableContainer, EuiResizeObserver } from ' import React, { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import cx from 'classnames' +import DatabasePanel from 'uiSrc/pages/home/components/database-panel' import { clusterSelector, resetDataRedisCluster, resetInstancesRedisCluster, } from 'uiSrc/slices/instances/cluster' -import { setTitle } from 'uiSrc/utils' +import { Nullable, setTitle } from 'uiSrc/utils' import { PageHeader } from 'uiSrc/components' import { BrowserStorageItem } from 'uiSrc/constants' import { resetKeys } from 'uiSrc/slices/browser/keys' @@ -25,19 +26,22 @@ import { fetchContentAction as fetchCreateRedisButtonsAction } from 'uiSrc/slice import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' -import AddDatabaseContainer, { AddDbType } from './components/AddDatabases/AddDatabasesContainer' -import DatabasesList from './components/DatabasesListComponent/DatabasesListWrapper' -import WelcomeComponent from './components/WelcomeComponent/WelcomeComponent' -import HomeHeader from './components/HomeHeader' +import { AddDbType } from 'uiSrc/pages/home/constants' +import DatabasesList from './components/databases-list-component' +import WelcomeComponent from './components/welcome-component' +import HomeHeader from './components/home-header' import './styles.scss' import styles from './styles.module.scss' +enum RightPanelName { + AddDatabase = 'add', + EditDatabase = 'edit' +} + const HomePage = () => { const [width, setWidth] = useState(0) - const [addDialogIsOpen, setAddDialogIsOpen] = useState(false) - const [editDialogIsOpen, setEditDialogIsOpen] = useState(false) - const [dialogIsOpen, setDialogIsOpen] = useState(false) + const [openRightPanel, setOpenRightPanel] = useState>(null) const [welcomeIsShow, setWelcomeIsShow] = useState( !localStorageService.get(BrowserStorageItem.instancesCount) ) @@ -86,8 +90,7 @@ const HomePage = () => { useEffect(() => { if (isChangedInstance) { - setAddDialogIsOpen(!isChangedInstance) - setEditDialogIsOpen(!isChangedInstance) + setOpenRightPanel(null) dispatch(setEditedInstance(null)) // send page view after adding database from welcome page sendPageViewTelemetry({ @@ -107,29 +110,25 @@ const HomePage = () => { useEffect(() => { if (clusterCredentials || cloudCredentials || sentinelInstance) { - setAddDialogIsOpen(true) + setOpenRightPanel(RightPanelName.AddDatabase) } }, [clusterCredentials, cloudCredentials, sentinelInstance]) useEffect(() => { if (action === UrlHandlingActions.Connect) { - setAddDialogIsOpen(true) + setOpenRightPanel(RightPanelName.AddDatabase) } }, [action, dbConnection]) useEffect(() => { - const isDialogOpen = !!instances.length && (addDialogIsOpen || editDialogIsOpen) - const instancesCashCount = JSON.parse( localStorageService.get(BrowserStorageItem.instancesCount) ?? '0' ) - const isShowWelcome = !instances.length && !addDialogIsOpen && !editDialogIsOpen && !instancesCashCount - - setDialogIsOpen(isDialogOpen) + const isShowWelcome = !instances.length && !openRightPanel && !instancesCashCount setWelcomeIsShow(isShowWelcome) - }, [addDialogIsOpen, editDialogIsOpen, instances, loading]) + }, [openRightPanel, instances, loading]) useEffect(() => { if (editedInstance) { @@ -152,7 +151,7 @@ const HomePage = () => { const closeEditDialog = () => { dispatch(setEditedInstance(null)) - setEditDialogIsOpen(false) + setOpenRightPanel(null) sendEventTelemetry({ event: TelemetryEvent.CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED, @@ -166,9 +165,8 @@ const HomePage = () => { dispatch(resetDataRedisCluster()) dispatch(resetDataSentinel()) - setAddDialogIsOpen(false) + setOpenRightPanel(null) dispatch(setEditedInstance(null)) - setEditDialogIsOpen(false) if (action === UrlHandlingActions.Connect) { dispatch(setUrlHandlingInitialState()) @@ -181,22 +179,23 @@ const HomePage = () => { const handleAddInstance = (addDbType = AddDbType.manual) => { initialDbTypeRef.current = addDbType - setAddDialogIsOpen(true) + setOpenRightPanel(RightPanelName.AddDatabase) dispatch(setEditedInstance(null)) - setEditDialogIsOpen(false) } const handleEditInstance = (editedInstance: Instance) => { if (editedInstance) { dispatch(fetchEditedInstanceAction(editedInstance)) - setEditDialogIsOpen(true) - setAddDialogIsOpen(false) + setOpenRightPanel(RightPanelName.EditDatabase) } } const handleDeleteInstances = (instances: Instance[]) => { - if (instances.find((instance) => instance.id === editedInstance?.id)) { + if ( + instances.find((instance) => instance.id === editedInstance?.id) + && openRightPanel === RightPanelName.EditDatabase + ) { dispatch(setEditedInstance(null)) - setEditDialogIsOpen(false) + setOpenRightPanel(null) } instances.forEach((instance) => { @@ -227,7 +226,7 @@ const HomePage = () => { onAddInstance={handleAddInstance} direction="row" /> - {dialogIsOpen ? ( + {openRightPanel && instances.length ? (
{(EuiResizablePanel, EuiResizableButton) => ( @@ -242,7 +241,7 @@ const HomePage = () => {
{ scrollable={false} initialSize={38} className={cx({ - [styles.contentActive]: editDialogIsOpen, + [styles.contentActive]: openRightPanel === RightPanelName.EditDatabase, })} id="form" paddingSize="none" style={{ minWidth: '494px' }} > - {editDialogIsOpen && ( - - )} - - {addDialogIsOpen && ( - )} @@ -297,14 +294,14 @@ const HomePage = () => { ) : ( <> - {addDialogIsOpen && ( - () -const mockedEditedInstance: Instance = { - name: 'name', - host: 'host', - port: 123, - timeout: 10_000, - id: '123', - modules: [], - tls: true, - caCert: { id: 'zxc' }, - clientCert: { id: 'zxc' }, -} - -const mockedValues = { - newCaCert: '', - tls: true, - newCaCertName: '', - selectedCaCertName: '', - tlsClientAuthRequired: false, - verifyServerTlsCert: true, - newTlsCertPairName: '', - selectedTlsClientCertId: '', - newTlsClientCert: '', - newTlsClientKey: '', -} - -jest.mock('./InstanceForm/InstanceForm', () => ({ - __esModule: true, - namedExport: jest.fn(), - default: jest.fn(), -})) - -jest.mock('uiSrc/telemetry', () => ({ - ...jest.requireActual('uiSrc/telemetry'), - sendEventTelemetry: jest.fn(), -})) - -jest.mock('uiSrc/slices/instances/instances', () => ({ - ...jest.requireActual('uiSrc/slices/instances/instances'), - updateInstanceAction: () => jest.fn, - testInstanceStandaloneAction: () => jest.fn, - instancesSelector: jest.fn().mockReturnValue({ loadingChanging: false }), -})) - -jest.mock('uiSrc/slices/instances/clientCerts', () => ({ - clientCertsSelector: () => jest.fn().mockReturnValue({ data: [] }), - fetchClientCerts: jest.fn, -})) - -jest.mock('uiSrc/slices/instances/caCerts', () => ({ - caCertsSelector: () => jest.fn().mockReturnValue({ data: [] }), - fetchCaCerts: () => jest.fn, -})) - -jest.mock('uiSrc/slices/instances/sentinel', () => ({ - sentinelSelector: () => jest.fn().mockReturnValue({ loading: false }), - fetchMastersSentinelAction: () => jest.fn, -})) - -let store: typeof mockedStore -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -const MockInstanceForm = (props: InstanceProps) => ( -
- - - - -
-) - -describe('InstanceFormWrapper', () => { - beforeAll(() => { - InstanceForm.mockImplementation(MockInstanceForm) - }) - it('should render', () => { - expect( - render( - - ) - ).toBeTruthy() - }) - - it('should send prop timeout / 1_000 (in seconds)', () => { - expect( - render( - - ) - ).toBeTruthy() - - expect(InstanceForm).toHaveBeenCalledWith( - expect.objectContaining({ - formFields: expect.objectContaining({ - timeout: toString(mockedEditedInstance?.timeout / 1_000), - }), - }), - {}, - ) - }) - - it('should call onClose', () => { - const onClose = jest.fn() - render( - - ) - fireEvent.click(screen.getByTestId('close-btn')) - expect(onClose).toBeCalled() - }) - - it('should submit with editMode', () => { - const component = render( - - ) - fireEvent.click(screen.getByTestId('submit-form-btn')) - expect(component).toBeTruthy() - }) - - it('should call onHostNamePaste', () => { - const component = render( - - ) - fireEvent.click(screen.getByTestId('paste-hostName-btn')) - expect(component).toBeTruthy() - }) - - it('should call proper telemetry events after click test connection', () => { - const sendEventTelemetryMock = jest.fn() - - sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) - - render( - - ) - fireEvent.click(screen.getByTestId('btn-test-connection')) - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.CONFIG_DATABASES_TEST_CONNECTION_CLICKED, - }) - sendEventTelemetry.mockRestore() - }) - - it('should call proper actions onSubmit with url handling', () => { - render( - - ) - fireEvent.click(screen.getByTestId('submit-form-btn')) - expect(store.getActions()).toEqual([ - defaultInstanceChanging() - ]) - }) -}) diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx deleted file mode 100644 index 9c71b881e7..0000000000 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx +++ /dev/null @@ -1,650 +0,0 @@ -/* eslint-disable no-nested-ternary */ -import { ConnectionString } from 'connection-string' -import { isUndefined, pick, toNumber, toString, omit } from 'lodash' -import React, { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router' - -import { - checkConnectToInstanceAction, - createInstanceStandaloneAction, - instancesSelector, - testInstanceStandaloneAction, - updateInstanceAction, - cloneInstanceAction, -} from 'uiSrc/slices/instances/instances' -import { fetchMastersSentinelAction, sentinelSelector, } from 'uiSrc/slices/instances/sentinel' -import { Nullable, removeEmpty, getFormUpdates, transformQueryParamsObject } from 'uiSrc/utils' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { caCertsSelector, fetchCaCerts } from 'uiSrc/slices/instances/caCerts' -import { ConnectionType, Instance, InstanceType, } from 'uiSrc/slices/interfaces' -import { DbType, Pages, REDIS_URI_SCHEMES } from 'uiSrc/constants' -import { clientCertsSelector, fetchClientCerts, } from 'uiSrc/slices/instances/clientCerts' -import { appInfoSelector } from 'uiSrc/slices/app/info' - -import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' -import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' -import { getRedirectionPage } from 'uiSrc/utils/routing' -import InstanceForm from './InstanceForm' -import { DbConnectionInfo } from './InstanceForm/interfaces' -import { ADD_NEW, ADD_NEW_CA_CERT, DEFAULT_TIMEOUT, NO_CA_CERT, SshPassType } from './InstanceForm/constants' - -export interface Props { - width: number - isResizablePanel?: boolean - instanceType: InstanceType - editMode: boolean - urlHandlingAction?: Nullable - initialValues?: Nullable> - editedInstance: Nullable - onClose?: () => void - onDbEdited?: () => void - onAliasEdited?: (value: string) => void -} - -export enum SubmitBtnText { - AddDatabase = 'Add Redis Database', - EditDatabase = 'Apply changes', - ConnectToSentinel = 'Discover database', - CloneDatabase = 'Clone Database' -} - -export enum LoadingDatabaseText { - AddDatabase = 'Adding database...', - EditDatabase = 'Editing database...', -} - -export enum TitleDatabaseText { - AddDatabase = 'Add Redis Database', - EditDatabase = 'Edit Redis Database', -} - -const getInitialValues = (editedInstance?: Nullable>) => ({ - // undefined - to show default value, empty string - for existing db - host: editedInstance?.host ?? (editedInstance ? '' : undefined), - port: editedInstance?.port?.toString() ?? (editedInstance ? '' : undefined), - name: editedInstance?.name ?? (editedInstance ? '' : undefined), - username: editedInstance?.username ?? '', - password: editedInstance?.password ?? '', - timeout: editedInstance?.timeout - ? toString(editedInstance?.timeout / 1_000) - : (editedInstance ? '' : undefined), - tls: !!editedInstance?.tls ?? false, - ssh: !!editedInstance?.ssh ?? false, - servername: editedInstance?.tlsServername, - sshPassType: editedInstance?.sshOptions - ? (editedInstance.sshOptions.privateKey ? SshPassType.PrivateKey : SshPassType.Password) - : SshPassType.Password -}) - -const InstanceFormWrapper = (props: Props) => { - const { - editMode, - width, - instanceType, - isResizablePanel = false, - onClose, - onDbEdited, - onAliasEdited, - editedInstance, - urlHandlingAction, - initialValues: initialValuesProp - } = props - const [initialValues, setInitialValues] = useState(getInitialValues(editedInstance || initialValuesProp)) - const [isCloneMode, setIsCloneMode] = useState(false) - - const { host, port, name, username, password, timeout, tls, ssh, sshPassType, servername } = initialValues - - const { loadingChanging: loadingStandalone } = useSelector(instancesSelector) - const { loading: loadingSentinel } = useSelector(sentinelSelector) - const { data: caCertificates } = useSelector(caCertsSelector) - const { data: certificates } = useSelector(clientCertsSelector) - const { server } = useSelector(appInfoSelector) - const { properties: urlHandlingProperties } = useSelector(appRedirectionSelector) - - const tlsClientAuthRequired = !!editedInstance?.clientCert?.id ?? false - const selectedTlsClientCertId = editedInstance?.clientCert?.id ?? ADD_NEW - const verifyServerTlsCert = editedInstance?.verifyServerCert ?? false - const selectedCaCertName = editedInstance?.caCert?.id ?? NO_CA_CERT - const sentinelMasterUsername = editedInstance?.sentinelMaster?.username ?? '' - const sentinelMasterPassword = editedInstance?.sentinelMaster?.password ?? '' - - const connectionType = editedInstance?.connectionType ?? DbType.STANDALONE - const masterName = editedInstance?.sentinelMaster?.name - - const history = useHistory() - const dispatch = useDispatch() - - useEffect(() => { - dispatch(fetchCaCerts()) - dispatch(fetchClientCerts()) - }, []) - - useEffect(() => { - (editedInstance || initialValuesProp) && setInitialValues({ - ...initialValues, - ...getInitialValues(editedInstance || initialValuesProp) - }) - setIsCloneMode(false) - }, [editedInstance, initialValuesProp]) - - const onMastersSentinelFetched = () => { - history.push(Pages.sentinelDatabases) - } - - const handleSuccessConnectWithRedirect = (id: string) => { - const { redirect } = urlHandlingProperties - dispatch(setUrlHandlingInitialState()) - - dispatch(checkConnectToInstanceAction(id, (id) => { - if (redirect) { - const pageToRedirect = getRedirectionPage(redirect, id) - - if (pageToRedirect) { - history.push(pageToRedirect) - } - } - })) - } - - const handleSubmitDatabase = (payload: any) => { - if (isCloneMode && connectionType === ConnectionType.Sentinel) { - dispatch(createInstanceStandaloneAction(payload)) - return - } - - if (instanceType === InstanceType.Sentinel) { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED - }) - - delete payload.name - delete payload.db - dispatch(fetchMastersSentinelAction(payload, onMastersSentinelFetched)) - } else { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_MANUALLY_SUBMITTED - }) - - if (urlHandlingAction === UrlHandlingActions.Connect) { - const cloudDetails = transformQueryParamsObject( - pick( - urlHandlingProperties, - ['cloudId', 'subscriptionType', 'planMemoryLimit', 'memoryLimitMeasurementUnit', 'free'] - ) - ) - - const db = { ...payload } - if (cloudDetails?.cloudId) { - db.cloudDetails = cloudDetails - } - - dispatch(createInstanceStandaloneAction(db, undefined, handleSuccessConnectWithRedirect)) - return - } - - dispatch( - createInstanceStandaloneAction(payload, onMastersSentinelFetched) - ) - } - } - const handleEditDatabase = (payload: any) => { - dispatch(updateInstanceAction(payload, onDbEdited)) - } - - const handleCloneDatabase = (payload: any) => { - dispatch(cloneInstanceAction(payload)) - } - - const handleUpdateEditingName = (name: string) => { - const requiredFields = [ - 'id', - 'host', - 'port', - 'username', - 'password', - 'tls', - 'sentinelMaster', - ] - const database = pick(editedInstance, ...requiredFields) - dispatch(updateInstanceAction({ ...database, name })) - } - - const handleTestConnectionDatabase = (values: DbConnectionInfo) => { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_TEST_CONNECTION_CLICKED - }) - const { - name, - host, - port, - username, - password, - db, - compressor, - timeout, - sentinelMasterName, - sentinelMasterUsername, - sentinelMasterPassword, - newCaCert, - tls, - sni, - servername, - newCaCertName, - selectedCaCertName, - tlsClientAuthRequired, - verifyServerTlsCert, - newTlsCertPairName, - selectedTlsClientCertId, - newTlsClientCert, - newTlsClientKey, - } = values - - const tlsSettings = { - useTls: tls, - servername: (sni && servername) || undefined, - verifyServerCert: verifyServerTlsCert, - caCert: - !tls || selectedCaCertName === NO_CA_CERT - ? undefined - : selectedCaCertName === ADD_NEW_CA_CERT - ? { - new: { - name: newCaCertName, - certificate: newCaCert, - }, - } - : { - name: selectedCaCertName, - }, - clientAuth: tls && tlsClientAuthRequired, - clientCert: !tls - ? undefined - : typeof selectedTlsClientCertId === 'string' - && tlsClientAuthRequired - && selectedTlsClientCertId !== ADD_NEW - ? { id: selectedTlsClientCertId } - : selectedTlsClientCertId === ADD_NEW && tlsClientAuthRequired - ? { - new: { - name: newTlsCertPairName, - certificate: newTlsClientCert, - key: newTlsClientKey, - }, - } - : undefined, - } - - const database: any = { - name, - host, - port: +port, - db: +(db || 0), - username, - password, - compressor, - timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT), - } - - // add tls & ssh for database (modifies database object) - applyTlSDatabase(database, tlsSettings) - applySSHDatabase(database, values) - - if (isCloneMode && connectionType === ConnectionType.Sentinel) { - database.sentinelMaster = { - name: sentinelMasterName, - username: sentinelMasterUsername, - password: sentinelMasterPassword, - } - } - - if (editMode && editedInstance) { - dispatch(testInstanceStandaloneAction({ - ...getFormUpdates(database, editedInstance), - id: editedInstance.id, - })) - } else { - dispatch(testInstanceStandaloneAction(removeEmpty(database))) - } - } - - const autoFillFormDetails = (content: string): boolean => { - try { - const details = new ConnectionString(content) - - /* If a protocol exists, it should be a redis protocol */ - if (details.protocol && !REDIS_URI_SCHEMES.includes(details.protocol)) return false - /* - * Auto fill logic: - * 1) If the port is parsed, we are sure that the user has indeed copied a connection string. - * '172.18.0.2:12000' => {host: '172,18.0.2', port: 12000} - * 'redis-12000.cluster.local:12000' => {host: 'redis-12000.cluster.local', port: 12000} - * 'lorem ipsum' => {host: undefined, port: undefined} - * 2) If the port is `undefined` but a redis URI scheme is present as protocol, we follow - * the "Scheme semantics" as mentioned in the official URI schemes. - * i) redis:// - https://www.iana.org/assignments/uri-schemes/prov/redis - * ii) rediss:// - https://www.iana.org/assignments/uri-schemes/prov/rediss - */ - if ( - details.port !== undefined - || REDIS_URI_SCHEMES.includes(details.protocol || '') - ) { - setInitialValues({ - name: details.host || name || 'localhost:6379', - host: details.hostname || host || 'localhost', - port: `${details.port || port || 9443}`, - username: details.user || '', - password: details.password, - tls: details.protocol === 'rediss', - ssh: false, - sshPassType: SshPassType.Password - } as any) - /* - * auto fill was successfull so return true - */ - return true - } - } catch (err) { - /* The pasted content is not a connection URI so ignore. */ - return false - } - return false - } - - const applyTlSDatabase = (database: any, tlsSettings: any) => { - const { useTls, verifyServerCert, servername, caCert, clientAuth, clientCert } = tlsSettings - if (!useTls) return - - database.tls = useTls - database.tlsServername = servername - database.verifyServerCert = !!verifyServerCert - - if (!isUndefined(caCert?.new)) { - database.caCert = { - name: caCert?.new.name, - certificate: caCert?.new.certificate, - } - } - - if (!isUndefined(caCert?.name)) { - database.caCert = { id: caCert?.name } - } - - if (clientAuth) { - if (!isUndefined(clientCert.new)) { - database.clientCert = { - name: clientCert.new.name, - certificate: clientCert.new.certificate, - key: clientCert.new.key, - } - } - - if (!isUndefined(clientCert.id)) { - database.clientCert = { id: clientCert.id } - } - } - } - - const applySSHDatabase = (database: any, values: DbConnectionInfo) => { - const { - ssh, - sshPassType, - sshHost, - sshPort, - sshPassword, - sshUsername, - sshPassphrase, - sshPrivateKey, - } = values - - if (ssh) { - database.ssh = true - database.sshOptions = { - host: sshHost, - port: +sshPort, - username: sshUsername, - } - - if (sshPassType === SshPassType.Password) { - database.sshOptions.password = sshPassword - database.sshOptions.passphrase = null - database.sshOptions.privateKey = null - } - - if (sshPassType === SshPassType.PrivateKey) { - database.sshOptions.password = null - database.sshOptions.passphrase = sshPassphrase - database.sshOptions.privateKey = sshPrivateKey - } - } - } - - const editDatabase = (tlsSettings: any, values: DbConnectionInfo, isCloneMode: boolean) => { - const { - name, - host, - port, - db, - username, - password, - timeout, - compressor, - sentinelMasterUsername, - sentinelMasterPassword, - } = values - - const database: any = { - id: editedInstance?.id, - name, - host, - port: +port, - db: +(db || 0), - username, - password, - compressor, - timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT), - } - - // add tls & ssh for database (modifies database object) - applyTlSDatabase(database, tlsSettings) - applySSHDatabase(database, values) - - if (connectionType === ConnectionType.Sentinel) { - database.sentinelMaster = {} - database.sentinelMaster.name = masterName - database.sentinelMaster.username = sentinelMasterUsername - database.sentinelMaster.password = sentinelMasterPassword - } - - const payload = getFormUpdates(database, omit(editedInstance, ['id'])) - if (isCloneMode) { - handleCloneDatabase(payload) - } else { - handleEditDatabase(payload) - } - } - - const addDatabase = (tlsSettings: any, values: DbConnectionInfo) => { - const { - name, - host, - port, - username, - password, - timeout, - db, - compressor, - sentinelMasterName, - sentinelMasterUsername, - sentinelMasterPassword, - } = values - const database: any = { - name, - host, - port: +port, - db: +(db || 0), - compressor, - username, - password, - timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT), - } - - // add tls & ssh for database (modifies database object) - applyTlSDatabase(database, tlsSettings) - applySSHDatabase(database, values) - - if (isCloneMode && connectionType === ConnectionType.Sentinel) { - database.sentinelMaster = { - name: sentinelMasterName, - username: sentinelMasterUsername, - password: sentinelMasterPassword, - } - } - - handleSubmitDatabase(removeEmpty(database)) - } - - const handleConnectionFormSubmit = (values: DbConnectionInfo) => { - const { - newCaCert, - tls, - sni, - servername, - newCaCertName, - selectedCaCertName, - tlsClientAuthRequired, - verifyServerTlsCert, - newTlsCertPairName, - selectedTlsClientCertId, - newTlsClientCert, - newTlsClientKey, - } = values - - const tlsSettings = { - useTls: tls, - servername: (sni && servername) || undefined, - verifyServerCert: verifyServerTlsCert, - caCert: - !tls || selectedCaCertName === NO_CA_CERT - ? undefined - : selectedCaCertName === ADD_NEW_CA_CERT - ? { - new: { - name: newCaCertName, - certificate: newCaCert, - }, - } - : { - name: selectedCaCertName, - }, - clientAuth: tls && tlsClientAuthRequired, - clientCert: !tls - ? undefined - : typeof selectedTlsClientCertId === 'string' - && tlsClientAuthRequired - && selectedTlsClientCertId !== ADD_NEW - ? { id: selectedTlsClientCertId } - : selectedTlsClientCertId === ADD_NEW && tlsClientAuthRequired - ? { - new: { - name: newTlsCertPairName, - certificate: newTlsClientCert, - key: newTlsClientKey, - }, - } - : undefined, - } - - if (editMode) { - editDatabase(tlsSettings, values, isCloneMode) - } else { - addDatabase(tlsSettings, values) - } - } - - const handleOnClose = () => { - dispatch(setUrlHandlingInitialState()) - - if (isCloneMode) { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, - eventData: { - databaseId: editedInstance?.id, - } - }) - } - onClose?.() - } - - const connectionFormData = { - ...editedInstance, - name, - host, - port, - tls, - username, - password, - timeout, - connectionType, - tlsClientAuthRequired, - certificates, - selectedTlsClientCertId, - caCertificates, - verifyServerTlsCert, - selectedCaCertName, - sentinelMasterUsername, - sentinelMasterPassword, - ssh, - sshPassType, - servername, - } - - const getSubmitButtonText = () => { - if (instanceType === InstanceType.Sentinel) { - return SubmitBtnText.ConnectToSentinel - } - if (isCloneMode) { - return SubmitBtnText.CloneDatabase - } - if (editMode) { - return SubmitBtnText.EditDatabase - } - return SubmitBtnText.AddDatabase - } - - return ( -
- -
- ) -} - -export default InstanceFormWrapper diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/index.ts b/redisinsight/ui/src/pages/home/components/AddInstanceForm/index.ts deleted file mode 100644 index df91655e04..0000000000 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import InstanceFormWrapper from './InstanceFormWrapper' - -export default InstanceFormWrapper diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.spec.tsx rename to redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.tsx b/redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.tsx similarity index 95% rename from redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.tsx rename to redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.tsx index 5436af2f92..71f07cd700 100644 --- a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.tsx @@ -8,7 +8,7 @@ import { useResizableFormField } from 'uiSrc/services' import { resetErrors } from 'uiSrc/slices/app/notifications' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import CloudConnectionForm from './CloudConnectionForm/CloudConnectionForm' +import CloudConnectionForm from './cloud-connection-form' export interface Props { width: number diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionForm/CloudConnectionForm.spec.tsx b/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionForm/CloudConnectionForm.spec.tsx rename to redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionForm/CloudConnectionForm.tsx b/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionForm/CloudConnectionForm.tsx rename to redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.tsx diff --git a/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/index.ts b/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/index.ts new file mode 100644 index 0000000000..0c05734c8c --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/index.ts @@ -0,0 +1,3 @@ +import CloudConnectionForm from './CloudConnectionForm' + +export default CloudConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/cloud-connection/index.ts b/redisinsight/ui/src/pages/home/components/cloud-connection/index.ts new file mode 100644 index 0000000000..90a70f2f19 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cloud-connection/index.ts @@ -0,0 +1,3 @@ +import CloudConnectionFormWrapper from './CloudConnectionFormWrapper' + +export default CloudConnectionFormWrapper diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/styles.module.scss b/redisinsight/ui/src/pages/home/components/cloud-connection/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/CloudConnection/styles.module.scss rename to redisinsight/ui/src/pages/home/components/cloud-connection/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.spec.tsx similarity index 93% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.spec.tsx rename to redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.spec.tsx index a0569dca26..6d080282b9 100644 --- a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.spec.tsx @@ -1,14 +1,15 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import ClusterConnectionForm, { Props as ClusterConnectionFormProps } from + './cluster-connection-form/ClusterConnectionForm' import ClusterConnectionFormWrapper, { Props, } from './ClusterConnectionFormWrapper' -import ClusterConnectionForm, { Props as ClusterConnectionFormProps } from './ClusterConnectionForm/ClusterConnectionForm' const mockedProps = mock() -jest.mock('./ClusterConnectionForm/ClusterConnectionForm', () => ({ +jest.mock('./cluster-connection-form/ClusterConnectionForm', () => ({ __esModule: true, namedExport: jest.fn(), default: jest.fn(), diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.tsx similarity index 52% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.tsx rename to redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.tsx index 2f2d590be5..38379a24b7 100644 --- a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { ConnectionString } from 'connection-string' import { useHistory } from 'react-router-dom' import { clusterSelector, fetchInstancesRedisCluster, } from 'uiSrc/slices/instances/cluster' -import { REDIS_URI_SCHEMES, Pages } from 'uiSrc/constants' +import { Pages } from 'uiSrc/constants' import { useResizableFormField } from 'uiSrc/services' import { resetErrors } from 'uiSrc/slices/app/notifications' -import { ICredentialsRedisCluster } from 'uiSrc/slices/interfaces' +import { ICredentialsRedisCluster, InstanceType } from 'uiSrc/slices/interfaces' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { autoFillFormDetails } from 'uiSrc/pages/home/utils' -import ClusterConnectionForm from './ClusterConnectionForm/ClusterConnectionForm' +import ClusterConnectionForm from './cluster-connection-form/ClusterConnectionForm' export interface Props { width: number; @@ -70,44 +70,9 @@ const ClusterConnectionFormWrapper = ({ onClose, width }: Props) => { history.push(Pages.redisEnterpriseAutodiscovery) } - const autoFillFormDetails = (content: string): boolean => { - try { - const details = new ConnectionString(content) - - /* If a protocol exists, it should be a redis protocol */ - if (details.protocol && !REDIS_URI_SCHEMES.includes(details.protocol)) return false - /* - * Auto fill logic: - * 1) If the port is parsed, we are sure that the user has indeed copied a connection string. - * '172.18.0.2:12000' => {host: '172,18.0.2', port: 12000} - * 'redis-12000.cluster.local:12000' => {host: 'redis-12000.cluster.local', port: 12000} - * 'lorem ipsum' => {host: undefined, port: undefined} - * 2) If the port is `undefined` but a redis URI scheme is present as protocol, we follow - * the "Scheme semantics" as mentioned in the official URI schemes. - * i) redis:// - https://www.iana.org/assignments/uri-schemes/prov/redis - * ii) rediss:// - https://www.iana.org/assignments/uri-schemes/prov/rediss - */ - if ( - details.port !== undefined - || REDIS_URI_SCHEMES.includes(details.protocol || '') - ) { - setInitialValues({ - host: details.hostname || initialValues.host || 'localhost', - port: `${details.port || initialValues.port || 9443}`, - username: details.user || '', - password: details.password || '', - }) - /* - * auto fill was successfull so return true - */ - return true - } - } catch (err) { - /* The pasted content is not a connection URI so ignore. */ - return false - } - return false - } + const handlePostHostName = (content: string) => ( + autoFillFormDetails(content, initialValues, setInitialValues, InstanceType.RedisEnterpriseCluster) + ) return (
@@ -117,7 +82,7 @@ const ClusterConnectionFormWrapper = ({ onClose, width }: Props) => { username={credentials?.username ?? ''} password={credentials?.password ?? ''} initialValues={initialValues} - onHostNamePaste={autoFillFormDetails} + onHostNamePaste={handlePostHostName} flexGroupClassName={flexGroupClassName} flexItemClassName={flexItemClassName} onClose={onClose} diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.spec.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.spec.tsx rename to redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.tsx rename to redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx diff --git a/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/index.ts b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/index.ts new file mode 100644 index 0000000000..44ec2d4262 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/index.ts @@ -0,0 +1,3 @@ +import ClusterConnectionForm from './ClusterConnectionForm' + +export default ClusterConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/cluster-connection/index.ts b/redisinsight/ui/src/pages/home/components/cluster-connection/index.ts new file mode 100644 index 0000000000..405d374c5f --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/index.ts @@ -0,0 +1,3 @@ +import ClusterConnectionFormWrapper from './ClusterConnectionFormWrapper' + +export default ClusterConnectionFormWrapper diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/styles.module.scss b/redisinsight/ui/src/pages/home/components/cluster-connection/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/styles.module.scss rename to redisinsight/ui/src/pages/home/components/cluster-connection/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/types.ts b/redisinsight/ui/src/pages/home/components/cluster-connection/types.ts similarity index 100% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/types.ts rename to redisinsight/ui/src/pages/home/components/cluster-connection/types.ts diff --git a/redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.spec.tsx b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.spec.tsx similarity index 61% rename from redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.spec.tsx rename to redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.spec.tsx index 87cc8a70db..889a36bd66 100644 --- a/redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.spec.tsx @@ -22,25 +22,6 @@ describe('DatabaseAlias', () => { expect(render()).toBeTruthy() }) - it('should call onApplyChanges on edit alias', () => { - const onApply = jest.fn() - render() - - fireEvent.click(screen.getByTestId('edit-alias-btn')) - fireEvent.change(screen.getByTestId('alias-input'), { target: { value: 'alias' } }) - fireEvent.submit(screen.getByTestId('alias-input')) - - expect(onApply).toHaveBeenCalledWith('alias', expect.anything(), expect.anything()) - }) - - it('should call onOpen', () => { - const onOpen = jest.fn() - render() - - fireEvent.click(screen.getByTestId('connect-to-db-btn')) - expect(onOpen).toHaveBeenCalled() - }) - it('should not render part of content in edit mode', () => { render() @@ -48,14 +29,6 @@ describe('DatabaseAlias', () => { expect(screen.queryByTestId('db-alias')).toHaveTextContent('alias') }) - it('should call onCloneBack in clone mode', () => { - const onCloneBack = jest.fn() - render() - - fireEvent.click(screen.getByTestId('back-btn')) - expect(onCloneBack).toHaveBeenCalled() - }) - it('should render icon for redis-stack', () => { render() diff --git a/redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.tsx b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx similarity index 74% rename from redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.tsx rename to redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx index 207270b79a..52a726911f 100644 --- a/redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.tsx +++ b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx @@ -11,12 +11,15 @@ import { EuiToolTip, } from '@elastic/eui' import cx from 'classnames' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { toNumber } from 'lodash' +import { useHistory } from 'react-router' + +import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module' import { BuildType } from 'uiSrc/constants/env' import { appInfoSelector } from 'uiSrc/slices/app/info' import { Nullable, getDbIndex } from 'uiSrc/utils' -import { Theme } from 'uiSrc/constants' +import { PageNames, Pages, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' import RediStackDarkMin from 'uiSrc/assets/img/modules/redistack/RediStackDark-min.svg' @@ -24,29 +27,52 @@ import RediStackLightMin from 'uiSrc/assets/img/modules/redistack/RediStackLight import RediStackLightLogo from 'uiSrc/assets/img/modules/redistack/RedisStackLogoLight.svg' import RediStackDarkLogo from 'uiSrc/assets/img/modules/redistack/RedisStackLogoDark.svg' +import { getRedisModulesSummary, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { + changeInstanceAliasAction, + checkConnectToInstanceAction, + setConnectedInstanceId +} from 'uiSrc/slices/instances/instances' +import { resetKeys } from 'uiSrc/slices/browser/keys' +import { appContextSelector, setAppContextInitialState } from 'uiSrc/slices/app/context' import styles from './styles.module.scss' export interface Props { alias: string - database?: Nullable - onOpen: () => void - onClone: () => void - onCloneBack: () => void + database?: Nullable isLoading: boolean - onApplyChanges: (value: string, onSuccess?: () => void, onFail?: () => void) => void + onAliasEdited?: (value: string) => void isRediStack?: boolean isCloneMode: boolean + id?: string + provider?: string + setIsCloneMode: (value: boolean) => void + modules: AdditionalRedisModule[] } const DatabaseAlias = (props: Props) => { - const { alias, database, onOpen, onClone, onCloneBack, onApplyChanges, isLoading, isRediStack, isCloneMode } = props + const { + alias, + database, + id, + provider, + onAliasEdited, + isLoading, + isRediStack, + isCloneMode, + setIsCloneMode, + modules, + } = props const { server } = useSelector(appInfoSelector) + const { contextInstanceId, lastPage } = useSelector(appContextSelector) const [isEditing, setIsEditing] = useState(false) const [value, setValue] = useState(alias) const { theme } = useContext(ThemeContext) + const history = useHistory() + const dispatch = useDispatch() useEffect(() => { setValue(alias) @@ -60,21 +86,68 @@ const DatabaseAlias = (props: Props) => { isEditing && setValue(value) } + const connectToInstance = () => { + if (contextInstanceId && contextInstanceId !== id) { + dispatch(resetKeys()) + dispatch(setAppContextInitialState()) + } + dispatch(setConnectedInstanceId(id ?? '')) + + if (lastPage === PageNames.workbench && contextInstanceId === id) { + history.push(Pages.workbench(id)) + return + } + history.push(Pages.browser(id ?? '')) + } + const handleOpen = (event: any) => { event.stopPropagation() event.preventDefault() - onOpen() + const modulesSummary = getRedisModulesSummary(modules) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE_BUTTON_CLICKED, + eventData: { + databaseId: id, + provider, + ...modulesSummary, + } + }) + dispatch(checkConnectToInstanceAction(id, connectToInstance)) + // onOpen() } const handleClone = (e: React.MouseEvent) => { e.stopPropagation() e.preventDefault() - onClone() + setIsCloneMode(true) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_REQUESTED, + eventData: { + databaseId: id + } + }) } const handleApplyChanges = () => { setIsEditing(false) - onApplyChanges(value, () => {}, () => setValue(alias)) + dispatch(changeInstanceAliasAction( + id, + value, + () => { + onAliasEdited?.(value) + }, + () => setValue(alias) + )) + } + + const handleCloneBack = () => { + setIsCloneMode(false) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, + eventData: { + databaseId: id + } + }) } const handleDeclineChanges = (event?: React.MouseEvent) => { @@ -89,7 +162,7 @@ const DatabaseAlias = (props: Props) => { {isCloneMode && ( () -describe('AddDatabasesContainer', () => { +describe('DatabasePanel', () => { it('should render', () => { expect( - render() + render() ).toBeTruthy() }) it('should render instance types after click on auto discover', () => { - render() + render() fireEvent.click(screen.getByTestId('add-auto')) expect(screen.getByTestId('db-types')).toBeInTheDocument() }) diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx b/redisinsight/ui/src/pages/home/components/database-panel/DatabasePanel.tsx similarity index 79% rename from redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx rename to redisinsight/ui/src/pages/home/components/database-panel/DatabasePanel.tsx index 4d06fd40c1..f0b650a409 100644 --- a/redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx +++ b/redisinsight/ui/src/pages/home/components/database-panel/DatabasePanel.tsx @@ -20,10 +20,12 @@ import { sentinelSelector, resetDataSentinel } from 'uiSrc/slices/instances/sent import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' -import InstanceConnections from './InstanceConnections/InstanceConnections' -import InstanceFormWrapper from '../AddInstanceForm/InstanceFormWrapper' -import ClusterConnectionFormWrapper from '../ClusterConnection/ClusterConnectionFormWrapper' -import CloudConnectionFormWrapper from '../CloudConnection/CloudConnectionFormWrapper' +import { AddDbType } from 'uiSrc/pages/home/constants' +import ClusterConnectionFormWrapper from 'uiSrc/pages/home/components/cluster-connection' +import CloudConnectionFormWrapper from 'uiSrc/pages/home/components/cloud-connection' +import SentinelConnectionWrapper from 'uiSrc/pages/home/components/sentinel-connection' +import ManualConnectionWrapper from 'uiSrc/pages/home/components/manual-connection' +import InstanceConnections from 'uiSrc/pages/home/components/database-panel/instance-connections' import styles from './styles.module.scss' @@ -41,12 +43,7 @@ export interface Props { initConnectionType?: AddDbType } -export enum AddDbType { - manual, - auto, -} - -const AddDatabasesContainer = React.memo((props: Props) => { +const DatabasePanel = React.memo((props: Props) => { const { editMode, isResizablePanel, @@ -92,6 +89,12 @@ const AddDatabasesContainer = React.memo((props: Props) => { } }, [action, dbConnection]) + useEffect(() => { + if (editMode) { + setConnectionType(AddDbType.manual) + } + }, [editMode]) + useEffect(() => // ComponentWillUnmount () => { @@ -183,15 +186,12 @@ const AddDatabasesContainer = React.memo((props: Props) => { const Form = () => ( <> {connectionType === AddDbType.manual && ( - + )} {connectionType === AddDbType.auto && ( <> {typeSelected === InstanceType.Sentinel && ( - + )} {typeSelected === InstanceType.RedisEnterpriseCluster && ( @@ -208,29 +208,29 @@ const AddDatabasesContainer = React.memo((props: Props) => { <>
{!isFullWidth && onClose && ( - - - + + + )} {!editMode && ( - <> - -

Discover and Add Redis Databases

-
- - {connectionType === AddDbType.auto && } - + <> + +

Discover and Add Redis Databases

+
+ + {connectionType === AddDbType.auto && } + )} {Form()}
@@ -239,4 +239,4 @@ const AddDatabasesContainer = React.memo((props: Props) => { ) }) -export default AddDatabasesContainer +export default DatabasePanel diff --git a/redisinsight/ui/src/pages/home/components/database-panel/index.ts b/redisinsight/ui/src/pages/home/components/database-panel/index.ts new file mode 100644 index 0000000000..18eecda936 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/database-panel/index.ts @@ -0,0 +1,3 @@ +import DatabasePanel from './DatabasePanel' + +export default DatabasePanel diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.spec.tsx b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.spec.tsx rename to redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.tsx b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.tsx similarity index 98% rename from redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.tsx rename to redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.tsx index dc7e73e4d0..a5703dcf5d 100644 --- a/redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.tsx +++ b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.tsx @@ -13,8 +13,7 @@ import LightActiveManualSvg from 'uiSrc/assets/img/light_theme/active_manual.svg import LightNotActiveManualSvg from 'uiSrc/assets/img/light_theme/n_active_manual.svg' import LightActiveAutoSvg from 'uiSrc/assets/img/light_theme/active_auto.svg' import LightNotActiveAutoSvg from 'uiSrc/assets/img/light_theme/n_active_auto.svg' - -import { AddDbType } from '../AddDatabasesContainer' +import { AddDbType } from 'uiSrc/pages/home/constants' import styles from '../styles.module.scss' diff --git a/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/index.ts b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/index.ts new file mode 100644 index 0000000000..162026205d --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/index.ts @@ -0,0 +1,3 @@ +import InstanceConnections from './InstanceConnections' + +export default InstanceConnections diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/styles.module.scss b/redisinsight/ui/src/pages/home/components/database-panel/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/AddDatabases/styles.module.scss rename to redisinsight/ui/src/pages/home/components/database-panel/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.spec.tsx similarity index 97% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.spec.tsx index 9cffb5a5a7..c1f6d74d86 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.spec.tsx @@ -11,11 +11,11 @@ import { RootState, store } from 'uiSrc/slices/store' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { errorHandlers } from 'uiSrc/mocks/res/responseComposition' import DatabasesListWrapper, { Props } from './DatabasesListWrapper' -import DatabasesList, { Props as DatabasesListProps } from './DatabasesList/DatabasesList' +import DatabasesList, { Props as DatabasesListProps } from './databases-list/DatabasesList' const mockedProps = mock() -jest.mock('./DatabasesList/DatabasesList', () => ({ +jest.mock('./databases-list/DatabasesList', () => ({ __esModule: true, namedExport: jest.fn(), default: jest.fn(), diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.tsx similarity index 99% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.tsx index 15342e7be9..4021271d0e 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.tsx @@ -40,7 +40,7 @@ import RediStackLightLogo from 'uiSrc/assets/img/modules/redistack/RedisStackLog import RediStackDarkLogo from 'uiSrc/assets/img/modules/redistack/RedisStackLogoDark.svg' import { ReactComponent as CloudLinkIcon } from 'uiSrc/assets/img/oauth/cloud_link.svg' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' -import DatabasesList from './DatabasesList/DatabasesList' +import DatabasesList from './databases-list' import styles from './styles.module.scss' diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/DatabasesList.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/DatabasesList.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/DatabasesList.spec.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/DatabasesList.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/DatabasesList.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/DatabasesList.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/DatabasesList.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/DatabasesList.tsx diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/action-bar/ActionBar.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/action-bar/ActionBar.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/action-bar/ActionBar.spec.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/action-bar/ActionBar.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/action-bar/ActionBar.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/action-bar/ActionBar.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/action-bar/ActionBar.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/action-bar/ActionBar.tsx diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/action-bar/styles.module.scss b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/action-bar/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/action-bar/styles.module.scss rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/action-bar/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/delete-action/DeleteAction.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/delete-action/DeleteAction.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/delete-action/DeleteAction.spec.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/delete-action/DeleteAction.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/delete-action/DeleteAction.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/delete-action/DeleteAction.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/delete-action/DeleteAction.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/delete-action/DeleteAction.tsx diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/export-action/ExportAction.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/export-action/ExportAction.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/export-action/ExportAction.spec.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/export-action/ExportAction.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/export-action/ExportAction.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/export-action/ExportAction.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/export-action/ExportAction.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/export-action/ExportAction.tsx diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/index.ts b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/index.ts similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/index.ts rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/index.ts diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/styles.module.scss b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/components/styles.module.scss rename to redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/components/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/index.ts b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/index.ts new file mode 100644 index 0000000000..06b6eca03c --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/databases-list-component/databases-list/index.ts @@ -0,0 +1,3 @@ +import DatabasesList from './DatabasesList' + +export default DatabasesList diff --git a/redisinsight/ui/src/pages/home/components/databases-list-component/index.ts b/redisinsight/ui/src/pages/home/components/databases-list-component/index.ts new file mode 100644 index 0000000000..5685d6dfb3 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/databases-list-component/index.ts @@ -0,0 +1,3 @@ +import DatabasesListWrapper from './DatabasesListWrapper' + +export default DatabasesListWrapper diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss b/redisinsight/ui/src/pages/home/components/databases-list-component/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss rename to redisinsight/ui/src/pages/home/components/databases-list-component/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/DatabaseForm.tsx b/redisinsight/ui/src/pages/home/components/form/DatabaseForm.tsx similarity index 86% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/DatabaseForm.tsx rename to redisinsight/ui/src/pages/home/components/form/DatabaseForm.tsx index 9ed4aa02a0..fe839b983c 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/DatabaseForm.tsx +++ b/redisinsight/ui/src/pages/home/components/form/DatabaseForm.tsx @@ -14,20 +14,32 @@ import { import { BuildType } from 'uiSrc/constants/env' import { SECURITY_FIELD } from 'uiSrc/constants' import { appInfoSelector } from 'uiSrc/slices/app/info' -import { handlePasteHostName, MAX_PORT_NUMBER, MAX_TIMEOUT_NUMBER, selectOnFocus, validateField, validatePortNumber, validateTimeoutNumber } from 'uiSrc/utils' -import { ConnectionType, InstanceType } from 'uiSrc/slices/interfaces' -import { DbConnectionInfo } from '../interfaces' +import { + handlePasteHostName, + MAX_PORT_NUMBER, + MAX_TIMEOUT_NUMBER, + selectOnFocus, + validateField, + validatePortNumber, + validateTimeoutNumber, +} from 'uiSrc/utils' +import { DbConnectionInfo, IPasswordType } from 'uiSrc/pages/home/interfaces' + +interface IShowFields { + alias: boolean + host: boolean + port: boolean + timeout: boolean +} export interface Props { flexGroupClassName?: string flexItemClassName?: string formik: FormikProps - isEditMode: boolean - isCloneMode: boolean onHostNamePaste: (content: string) => boolean - instanceType: InstanceType - connectionType?: ConnectionType - isFromCloud: boolean + showFields: IShowFields + autoFocus?: boolean + passwordType?: IPasswordType } const DatabaseForm = (props: Props) => { @@ -35,12 +47,10 @@ const DatabaseForm = (props: Props) => { flexGroupClassName = '', flexItemClassName = '', formik, - isEditMode, - isCloneMode, onHostNamePaste, - instanceType, - connectionType, - isFromCloud, + autoFocus = false, + showFields, + passwordType = IPasswordType.Password, } = props const { server } = useSelector(appInfoSelector) @@ -84,11 +94,11 @@ const DatabaseForm = (props: Props) => { return ( <> - {(!isEditMode || isCloneMode) && !isFromCloud && ( + {showFields.host && ( { )} - {server?.buildType !== BuildType.RedisStack && !isFromCloud && ( + {server?.buildType !== BuildType.RedisStack && showFields.port && ( { )} - {( - (!isEditMode || isCloneMode) - && instanceType !== InstanceType.Sentinel - && connectionType !== ConnectionType.Sentinel - ) && ( + {showFields.alias && ( @@ -179,7 +185,7 @@ const DatabaseForm = (props: Props) => { { - {connectionType !== ConnectionType.Sentinel && instanceType !== InstanceType.Sentinel && ( + {showFields.timeout && ( () + +jest.mock('./manual-connection-form/ManualConnectionForm', () => ({ + __esModule: true, + namedExport: jest.fn(), + default: jest.fn(), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockManualConnectionFrom = (props: ManualConnectionFromProps) => ( +
+ + + + +
+) + +describe('ManualConnectionWrapper', () => { + beforeAll(() => { + ManualConnectionFrom.mockImplementation(mockManualConnectionFrom) + }) + it('should render', () => { + expect( + render() + ).toBeTruthy() + }) + + it('should call onHostNamePaste', () => { + const component = render() + fireEvent.click(screen.getByTestId('onHostNamePaste-btn')) + expect(component).toBeTruthy() + }) + + it('should call onClose', () => { + const onClose = jest.fn() + render() + fireEvent.click(screen.getByTestId('onClose-btn')) + expect(onClose).toBeCalled() + }) + + it('should have add database submit button', () => { + render() + expect(screen.getByTestId('btn-submit')).toHaveTextContent(SubmitBtnText.AddDatabase) + }) + + it('should have edit database submit button', () => { + render() + expect(screen.getByTestId('btn-submit')).toHaveTextContent(SubmitBtnText.EditDatabase) + }) + + it('should have edit database submit button', () => { + render() + act(() => { + fireEvent.click(screen.getByTestId('onClone-btn')) + }) + expect(screen.getByTestId('btn-submit')).toHaveTextContent(SubmitBtnText.CloneDatabase) + }) + + it('should call proper telemetry event on Add database', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + sendEventTelemetry.mockRestore() + render() + act(() => { + fireEvent.click(screen.getByTestId('btn-submit')) + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_MANUALLY_SUBMITTED, + }) + }) + + it('should call proper telemetry event on Clone database', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + sendEventTelemetry.mockRestore() + render() + act(() => { + fireEvent.click(screen.getByTestId('onClone-btn')) + }) + act(() => { + fireEvent.click(screen.getByTestId('onClose-btn')) + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, + eventData: { databaseId: undefined } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx b/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx new file mode 100644 index 0000000000..2efff9a03d --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx @@ -0,0 +1,262 @@ +import { pick, toNumber, omit } from 'lodash' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router' + +import { + checkConnectToInstanceAction, + createInstanceStandaloneAction, + instancesSelector, + testInstanceStandaloneAction, + updateInstanceAction, + cloneInstanceAction, +} from 'uiSrc/slices/instances/instances' +import { Nullable, removeEmpty, getFormUpdates, transformQueryParamsObject, getDiffKeysOfObjectValues } from 'uiSrc/utils' +import { BuildType } from 'uiSrc/constants/env' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { fetchCaCerts } from 'uiSrc/slices/instances/caCerts' +import { ConnectionType, Instance, InstanceType } from 'uiSrc/slices/interfaces' +import { DbType, Pages } from 'uiSrc/constants' +import { fetchClientCerts, } from 'uiSrc/slices/instances/clientCerts' +import { appInfoSelector } from 'uiSrc/slices/app/info' +import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' +import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' +import { getRedirectionPage } from 'uiSrc/utils/routing' +import { DbConnectionInfo } from 'uiSrc/pages/home/interfaces' +import { applyTlSDatabase, applySSHDatabase, autoFillFormDetails, getTlsSettings, getFormValues } from 'uiSrc/pages/home/utils' +import { + DEFAULT_TIMEOUT, + SubmitBtnText, +} from 'uiSrc/pages/home/constants' +import ManualConnectionForm from './manual-connection-form' + +export interface Props { + width: number + editMode: boolean + urlHandlingAction?: Nullable + initialValues?: Nullable> + editedInstance: Nullable + onClose?: () => void + onDbEdited?: () => void + onAliasEdited?: (value: string) => void +} + +const ManualConnectionWrapper = (props: Props) => { + const { + editMode, + width, + onClose, + onDbEdited, + onAliasEdited, + editedInstance, + urlHandlingAction, + initialValues: initialValuesProp + } = props + const [formFields, setFormFields] = useState(getFormValues(editedInstance || initialValuesProp)) + + const [isCloneMode, setIsCloneMode] = useState(false) + + const { loadingChanging: loadingStandalone } = useSelector(instancesSelector) + const { server } = useSelector(appInfoSelector) + const { properties: urlHandlingProperties } = useSelector(appRedirectionSelector) + + const connectionType = editedInstance?.connectionType ?? DbType.STANDALONE + + const history = useHistory() + const dispatch = useDispatch() + + useEffect(() => { + dispatch(fetchCaCerts()) + dispatch(fetchClientCerts()) + }, []) + + useEffect(() => { + setFormFields(getFormValues(editedInstance || initialValuesProp)) + setIsCloneMode(false) + }, [editedInstance, initialValuesProp]) + + const onMastersSentinelFetched = () => { + history.push(Pages.sentinelDatabases) + } + + const handleSuccessConnectWithRedirect = (id: string) => { + const { redirect } = urlHandlingProperties + dispatch(setUrlHandlingInitialState()) + + dispatch(checkConnectToInstanceAction(id, (id) => { + if (redirect) { + const pageToRedirect = getRedirectionPage(redirect, id) + + if (pageToRedirect) { + history.push(pageToRedirect) + } + } + })) + } + + const handleAddDatabase = (payload: any) => { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_MANUALLY_SUBMITTED + }) + + if (urlHandlingAction === UrlHandlingActions.Connect) { + const cloudDetails = transformQueryParamsObject( + pick( + urlHandlingProperties, + ['cloudId', 'subscriptionType', 'planMemoryLimit', 'memoryLimitMeasurementUnit', 'free'] + ) + ) + + const db = { ...payload } + if (cloudDetails?.cloudId) { + db.cloudDetails = cloudDetails + } + + dispatch(createInstanceStandaloneAction(db, undefined, handleSuccessConnectWithRedirect)) + return + } + + dispatch(createInstanceStandaloneAction(payload, onMastersSentinelFetched)) + } + const handleEditDatabase = (payload: any) => { + dispatch(updateInstanceAction(payload, onDbEdited)) + } + + const handleCloneDatabase = (payload: any) => { + dispatch(cloneInstanceAction(payload)) + } + + const handleTestConnectionDatabase = (values: DbConnectionInfo) => { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_TEST_CONNECTION_CLICKED + }) + const payload = preparePayload(values) + + dispatch(testInstanceStandaloneAction(payload)) + } + + const handleConnectionFormSubmit = (values: DbConnectionInfo) => { + if (isCloneMode) { + const diffKeys = getDiffKeysOfObjectValues(formFields, values) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED, + eventData: { + fieldsModified: diffKeys + } + }) + } + const payload = preparePayload(values) + + if (isCloneMode) { + handleCloneDatabase(payload) + return + } + if (editMode) { + handleEditDatabase(payload) + return + } + + handleAddDatabase(payload) + } + + const preparePayload = (values: any) => { + const tlsSettings = getTlsSettings(values) + + const { + name, + host, + port, + db, + username, + password, + timeout, + compressor, + sentinelMasterName, + sentinelMasterUsername, + sentinelMasterPassword, + } = values + + const database: any = { + id: editedInstance?.id, + name, + host, + port: +port, + db: +(db || 0), + username, + password, + compressor, + timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT), + } + + // add tls & ssh for database (modifies database object) + applyTlSDatabase(database, tlsSettings) + applySSHDatabase(database, values) + + if (connectionType === ConnectionType.Sentinel) { + database.sentinelMaster = { + name: sentinelMasterName, + username: sentinelMasterUsername, + password: sentinelMasterPassword, + } + } + + if (editMode) { + database.id = editedInstance?.id + + return getFormUpdates(database, omit(editedInstance, ['id'])) + } + + return removeEmpty(database) + } + + const handleOnClose = () => { + dispatch(setUrlHandlingInitialState()) + + if (isCloneMode) { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, + eventData: { + databaseId: editedInstance?.id, + } + }) + } + onClose?.() + } + + const getSubmitButtonText = () => { + if (isCloneMode) { + return SubmitBtnText.CloneDatabase + } + if (editMode) { + return SubmitBtnText.EditDatabase + } + return SubmitBtnText.AddDatabase + } + + const handlePostHostName = (content: string): boolean => ( + autoFillFormDetails(content, formFields, setFormFields, InstanceType.Standalone) + ) + + return ( +
+ +
+ ) +} + +export default ManualConnectionWrapper diff --git a/redisinsight/ui/src/pages/home/components/manual-connection/index.ts b/redisinsight/ui/src/pages/home/components/manual-connection/index.ts new file mode 100644 index 0000000000..28d4f3af8c --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/manual-connection/index.ts @@ -0,0 +1,3 @@ +import ManualConnectionWrapper from './ManualConnectionWrapper' + +export default ManualConnectionWrapper diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.tsx similarity index 53% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx rename to redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.tsx index 27c019c9cf..89f1c9fb93 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx +++ b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.tsx @@ -9,138 +9,77 @@ import { keys, } from '@elastic/eui' import { FormikErrors, useFormik } from 'formik' -import { isEmpty, pick, toString } from 'lodash' +import { isEmpty, pick } from 'lodash' import React, { useEffect, useRef, useState } from 'react' import ReactDOM from 'react-dom' import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router' -import { PageNames, Pages } from 'uiSrc/constants' import validationErrors from 'uiSrc/constants/validationErrors' -import DatabaseAlias from 'uiSrc/pages/home/components/DatabaseAlias' +import DatabaseAlias from 'uiSrc/pages/home/components/database-alias' import { useResizableFormField } from 'uiSrc/services' -import { appContextSelector, setAppContextInitialState } from 'uiSrc/slices/app/context' -import { resetKeys } from 'uiSrc/slices/browser/keys' -import { - changeInstanceAliasAction, - checkConnectToInstanceAction, - resetInstanceUpdateAction, - setConnectedInstanceId, -} from 'uiSrc/slices/instances/instances' -import { ConnectionType, InstanceType, } from 'uiSrc/slices/interfaces' -import { getRedisModulesSummary, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { Nullable, getDiffKeysOfObjectValues, isRediStack } from 'uiSrc/utils' +import { resetInstanceUpdateAction } from 'uiSrc/slices/instances/instances' +import { ConnectionType } from 'uiSrc/slices/interfaces' +import { isRediStack } from 'uiSrc/utils' import { BuildType } from 'uiSrc/constants/env' import { appRedirectionSelector } from 'uiSrc/slices/app/url-handling' import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' import { - ADD_NEW_CA_CERT, - NO_CA_CERT, - ADD_NEW, fieldDisplayNames, - SshPassType, - DEFAULT_TIMEOUT, - NONE, -} from './constants' - -import { DbConnectionInfo, ISubmitButton } from './interfaces' + SubmitBtnText, +} from 'uiSrc/pages/home/constants' +import { getFormErrors, getSubmitButtonContent } from 'uiSrc/pages/home/utils' +import { DbConnectionInfo, ISubmitButton } from 'uiSrc/pages/home/interfaces' import { DbIndex, DbInfo, - MessageSentinel, MessageStandalone, TlsDetails, DatabaseForm, - DbCompressor -} from './form-components' + DbCompressor, + SSHDetails, +} from 'uiSrc/pages/home/components/form' import { DbInfoSentinel, PrimaryGroupSentinel, SentinelHostPort, SentinelMasterDatabase, -} from './form-components/sentinel' -import SSHDetails from './form-components/SSHDetails' -import { LoadingDatabaseText, SubmitBtnText, TitleDatabaseText, } from '../InstanceFormWrapper' +} from 'uiSrc/pages/home/components/form/sentinel' +import { caCertsSelector } from 'uiSrc/slices/instances/caCerts' +import { clientCertsSelector } from 'uiSrc/slices/instances/clientCerts' export interface Props { width: number - isResizablePanel?: boolean formFields: DbConnectionInfo submitButtonText?: SubmitBtnText - titleText?: TitleDatabaseText loading: boolean buildType?: BuildType - instanceType: InstanceType - loadingMsg: LoadingDatabaseText isEditMode: boolean isCloneMode: boolean setIsCloneMode: (value: boolean) => void - initialValues: DbConnectionInfo onSubmit: (values: DbConnectionInfo) => void onTestConnection: (values: DbConnectionInfo) => void - updateEditingName: (name: string) => void onHostNamePaste: (content: string) => boolean onClose?: () => void onAliasEdited?: (value: string) => void - setErrorMsgRef?: (database: HTMLDivElement | null) => void - urlHandlingAction?: Nullable } -const getInitFieldsDisplayNames = ({ host, port, name, instanceType }: any) => { - if (!host || !port) { - if (!name && instanceType !== InstanceType.Sentinel) { - return pick(fieldDisplayNames, ['host', 'port', 'name']) - } - return pick(fieldDisplayNames, ['host', 'port']) +const getInitFieldsDisplayNames = ({ host, port, name }: any) => { + if (!host || !port || !name) { + return pick(fieldDisplayNames, ['host', 'port', 'name']) } return {} } -const getDefaultHost = () => '127.0.0.1' -const getDefaultPort = (instanceType: InstanceType) => (instanceType === InstanceType.Sentinel ? '26379' : '6379') - -const AddStandaloneForm = (props: Props) => { +const ManualConnectionForm = (props: Props) => { const { - formFields: { - id, - host, - name, - port, - tls, - db = null, - compressor = NONE, - nameFromProvider, - sentinelMaster, - connectionType, - nodes = null, - tlsClientAuthRequired, - certificates, - selectedTlsClientCertId = '', - verifyServerTlsCert, - caCertificates, - selectedCaCertName, - username, - password, - timeout, - modules, - sentinelMasterPassword, - sentinelMasterUsername, - servername, - provider, - ssh, - sshPassType = SshPassType.Password, - sshOptions, - version, - }, - initialValues: initialValuesProp, + formFields, width, onClose, onSubmit, onTestConnection, onHostNamePaste, submitButtonText, - instanceType, buildType, loading, isEditMode, @@ -149,60 +88,29 @@ const AddStandaloneForm = (props: Props) => { onAliasEdited, } = props - const { contextInstanceId, lastPage } = useSelector(appContextSelector) - const { action } = useSelector(appRedirectionSelector) - - const prepareInitialValues = () => ({ - host: host ?? getDefaultHost(), - port: port ? port.toString() : getDefaultPort(instanceType), - timeout: timeout ? timeout.toString() : toString(DEFAULT_TIMEOUT / 1_000), - name: name ?? `${getDefaultHost()}:${getDefaultPort(instanceType)}`, - username, - password, - tls, - db, - compressor, + const { + id, + host, + name, + port, + db = null, + nameFromProvider, + sentinelMaster, + connectionType, + nodes = null, modules, - showDb: !!db, - showCompressor: compressor !== NONE, - sni: !!servername, - servername, - newCaCert: '', - newCaCertName: '', - selectedCaCertName, - tlsClientAuthRequired, - verifyServerTlsCert, - newTlsCertPairName: '', - selectedTlsClientCertId, - newTlsClientCert: '', - newTlsClientKey: '', - sentinelMasterName: sentinelMaster?.name || '', - sentinelMasterUsername, - sentinelMasterPassword, - ssh, - sshPassType, - sshHost: sshOptions?.host ?? '', - sshPort: sshOptions?.port ?? 22, - sshUsername: sshOptions?.username ?? '', - sshPassword: sshOptions?.password ?? '', - sshPrivateKey: sshOptions?.privateKey ?? '', - sshPassphrase: sshOptions?.passphrase ?? '' - }) + provider, + version, + } = formFields - const [initialValues, setInitialValues] = useState(prepareInitialValues()) + const { action } = useSelector(appRedirectionSelector) + const { data: caCertificates } = useSelector(caCertsSelector) + const { data: certificates } = useSelector(clientCertsSelector) const [errors, setErrors] = useState>( - getInitFieldsDisplayNames({ host, port, name, instanceType }) + getInitFieldsDisplayNames({ host, port, name }) ) - useEffect(() => { - const values = prepareInitialValues() - - setInitialValues(values) - formik.setValues(values) - }, [initialValuesProp, isCloneMode]) - - const history = useHistory() const dispatch = useDispatch() const formRef = useRef(null) @@ -211,84 +119,14 @@ const AddStandaloneForm = (props: Props) => { const isFromCloud = action === UrlHandlingActions.Connect const validate = (values: DbConnectionInfo) => { - const errs: FormikErrors = {} - - if (!values.host) { - errs.host = fieldDisplayNames.host - } - if (!values.port) { - errs.port = fieldDisplayNames.port - } - - if (!values.name && instanceType !== InstanceType.Sentinel) { - errs.name = fieldDisplayNames.name - } - - if ( - values.tls - && values.verifyServerTlsCert - && values.selectedCaCertName === NO_CA_CERT - ) { - errs.selectedCaCertName = fieldDisplayNames.selectedCaCertName - } - - if ( - values.tls - && values.selectedCaCertName === ADD_NEW_CA_CERT - && values.newCaCertName === '' - ) { - errs.newCaCertName = fieldDisplayNames.newCaCertName - } - - if ( - values.tls - && values.selectedCaCertName === ADD_NEW_CA_CERT - && values.newCaCert === '' - ) { - errs.newCaCert = fieldDisplayNames.newCaCert - } - - if ( - values.tls - && values.sni - && values.servername === '' - ) { - errs.servername = fieldDisplayNames.servername - } - - if ( - values.tls - && values.tlsClientAuthRequired - && values.selectedTlsClientCertId === ADD_NEW - ) { - if (values.newTlsCertPairName === '') { - errs.newTlsCertPairName = fieldDisplayNames.newTlsCertPairName - } - if (values.newTlsClientCert === '') { - errs.newTlsClientCert = fieldDisplayNames.newTlsClientCert - } - if (values.newTlsClientKey === '') { - errs.newTlsClientKey = fieldDisplayNames.newTlsClientKey - } - } + const errs = getFormErrors(values) if (isCloneMode && connectionType === ConnectionType.Sentinel && !values.sentinelMasterName) { errs.sentinelMasterName = fieldDisplayNames.sentinelMasterName } - if (values.ssh) { - if (!values.sshHost) { - errs.sshHost = fieldDisplayNames.sshHost - } - if (!values.sshPort) { - errs.sshPort = fieldDisplayNames.sshPort - } - if (!values.sshUsername) { - errs.sshUsername = fieldDisplayNames.sshUsername - } - if (values.sshPassType === SshPassType.PrivateKey && !values.sshPrivateKey) { - errs.sshPrivateKey = fieldDisplayNames.sshPrivateKey - } + if (!values.name) { + errs.name = fieldDisplayNames.name } setErrors(errs) @@ -296,19 +134,10 @@ const AddStandaloneForm = (props: Props) => { } const formik = useFormik({ - initialValues, + initialValues: formFields, validate, enableReinitialize: true, onSubmit: (values: any) => { - if (isCloneMode) { - const diffKeys = getDiffKeysOfObjectValues(formik.initialValues, values) - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED, - eventData: { - fieldsModified: diffKeys - } - }) - } onSubmit(values) }, }) @@ -326,7 +155,7 @@ const AddStandaloneForm = (props: Props) => { } useEffect(() => - // componentWillUnmount + // componentWillUnmount () => { if (isEditMode) { dispatch(resetInstanceUpdateAction()) @@ -334,88 +163,14 @@ const AddStandaloneForm = (props: Props) => { }, []) - const handleCheckConnectToInstance = () => { - const modulesSummary = getRedisModulesSummary(modules) - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE_BUTTON_CLICKED, - eventData: { - databaseId: id, - provider, - ...modulesSummary, - } - }) - dispatch(checkConnectToInstanceAction(id, connectToInstance)) - } - - const handleCloneDatabase = () => { - setIsCloneMode(true) - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_REQUESTED, - eventData: { - databaseId: id - } - }) - } - - const handleBackCloneDatabase = () => { - setIsCloneMode(false) - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, - eventData: { - databaseId: id - } - }) - } + useEffect(() => { + formik.resetForm() + }, [isCloneMode]) const handleTestConnectionDatabase = () => { onTestConnection(formik.values) } - const handleChangeDatabaseAlias = ( - value: string, - onSuccess?: () => void, - onFail?: () => void - ) => { - dispatch(changeInstanceAliasAction( - id, - value, - () => { - onAliasEdited?.(value) - onSuccess?.() - }, - onFail - )) - } - - const connectToInstance = () => { - if (contextInstanceId && contextInstanceId !== id) { - dispatch(resetKeys()) - dispatch(setAppContextInitialState()) - } - dispatch(setConnectedInstanceId(id ?? '')) - - if (lastPage === PageNames.workbench && contextInstanceId === id) { - history.push(Pages.workbench(id)) - return - } - history.push(Pages.browser(id)) - } - - const getSubmitButtonContent = (submitIsDisabled?: boolean) => { - const maxErrorsCount = 5 - const errorsArr = Object.values(errors).map((err) => [ - err, -
, - ]) - - if (errorsArr.length > maxErrorsCount) { - errorsArr.splice(maxErrorsCount, errorsArr.length, ['...']) - } - return submitIsDisabled ? ( - {errorsArr} - ) : null - } - const SubmitButton = ({ text = '', onClick, @@ -429,7 +184,7 @@ const AddStandaloneForm = (props: Props) => { ? validationErrors.REQUIRED_TITLE(Object.keys(errors).length) : null } - content={getSubmitButtonContent(submitIsDisabled)} + content={getSubmitButtonContent(errors, submitIsDisabled)} > { responsive={false} > - {instanceType !== InstanceType.Sentinel && ( - + - - Test Connection - - - )} + Test Connection + +
@@ -520,26 +273,21 @@ const AddStandaloneForm = (props: Props) => { alias={name} database={db} isLoading={loading} - onOpen={handleCheckConnectToInstance} - onClone={handleCloneDatabase} - onCloneBack={handleBackCloneDatabase} - onApplyChanges={handleChangeDatabaseAlias} + id={id} + provider={provider} + modules={modules} + setIsCloneMode={setIsCloneMode} + onAliasEdited={onAliasEdited} />
)}
- {!isEditMode && instanceType === InstanceType.Standalone && !isFromCloud && ( + {!isEditMode && !isFromCloud && ( <>
)} - {!isEditMode && instanceType === InstanceType.Sentinel && ( - <> - -
- - )} {!isEditMode && !isFromCloud && ( { formik={formik} flexItemClassName={flexItemClassName} flexGroupClassName={flexGroupClassName} - isCloneMode={isCloneMode} - isEditMode={isEditMode} - connectionType={connectionType} - instanceType={instanceType} onHostNamePaste={onHostNamePaste} + showFields={{ host: true, alias: true, port: true, timeout: true }} + /> + + - {instanceType !== InstanceType.Sentinel && ( - - )} - {instanceType !== InstanceType.Sentinel && ( - - )} { certificates={certificates} caCertificates={caCertificates} /> - {instanceType !== InstanceType.Sentinel && buildType !== BuildType.RedisStack && ( + {buildType !== BuildType.RedisStack && ( { formik={formik} flexItemClassName={flexItemClassName} flexGroupClassName={flexGroupClassName} - isCloneMode={isCloneMode} - isEditMode={isEditMode} - isFromCloud={isFromCloud} - connectionType={connectionType} - instanceType={instanceType} + showFields={{ + alias: !isEditMode || isCloneMode, + host: (!isEditMode || isCloneMode) && !isFromCloud, + port: !isFromCloud, + timeout: true, + }} + autoFocus={!isCloneMode && isEditMode} onHostNamePaste={onHostNamePaste} /> {isCloneMode && ( @@ -690,14 +433,10 @@ const AddStandaloneForm = (props: Props) => { formik={formik} flexItemClassName={flexItemClassName} flexGroupClassName={flexGroupClassName} - isCloneMode={isCloneMode} - isEditMode={isEditMode} - connectionType={connectionType} - instanceType={instanceType} + showFields={{ host: false, port: true, alias: false, timeout: false }} onHostNamePaste={onHostNamePaste} /> - { formik={formik} flexItemClassName={flexItemClassName} flexGroupClassName={flexGroupClassName} - isCloneMode={isCloneMode} - isEditMode={isEditMode} - connectionType={connectionType} - instanceType={instanceType} + showFields={{ host: true, port: true, alias: false, timeout: false }} onHostNamePaste={onHostNamePaste} /> @@ -780,4 +516,4 @@ const AddStandaloneForm = (props: Props) => { ) } -export default AddStandaloneForm +export default ManualConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionFrom.spec.tsx similarity index 90% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx rename to redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionFrom.spec.tsx index cbd472a542..e003c23422 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionFrom.spec.tsx @@ -1,13 +1,14 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils' -import { ConnectionType, InstanceType } from 'uiSrc/slices/interfaces' +import { ConnectionType } from 'uiSrc/slices/interfaces' import { BuildType } from 'uiSrc/constants/env' import { appRedirectionSelector } from 'uiSrc/slices/app/url-handling' import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' -import InstanceForm, { Props } from './InstanceForm' -import { ADD_NEW_CA_CERT, SshPassType } from './constants' -import { DbConnectionInfo } from './interfaces' +import { ADD_NEW_CA_CERT, SshPassType } from 'uiSrc/pages/home/constants' +import { DbConnectionInfo } from 'uiSrc/pages/home/interfaces' + +import ManualConnectionForm, { Props } from './ManualConnectionForm' const BTN_SUBMIT = 'btn-submit' const NEW_CA_CERT = 'new-ca-cert' @@ -23,8 +24,6 @@ const formFields = { host: 'localhost', port: '6379', name: 'lala', - caCertificates: [], - certificates: [], } jest.mock('uiSrc/slices/instances/instances', () => ({ @@ -43,7 +42,7 @@ describe('InstanceForm', () => { it('should render', () => { expect( render( - + ) ).toBeTruthy() }) @@ -51,7 +50,7 @@ describe('InstanceForm', () => { it('should render with ConnectionType.Sentinel', () => { expect( render( - { it('should render with ConnectionType.Cluster', () => { expect( render( - { it('should render tooltip with nodes', () => { expect( render( - { it('should render DatabaseForm', () => { expect( render( - { render(
- { const handleSubmit = jest.fn() render(
- { render(
- { const handleTestConnection = jest.fn() render(
- { }) expect(handleTestConnection).toBeCalledWith( expect.objectContaining({ - showDb: true, + showDb: ['on'], }) ) await act(() => { @@ -272,7 +271,7 @@ describe('InstanceForm', () => { expect(handleSubmit).toBeCalledWith( expect.objectContaining({ - showDb: true, + showDb: ['on'], }) ) }) @@ -282,7 +281,7 @@ describe('InstanceForm', () => { const handleTestConnection = jest.fn() render(
- { }) expect(handleTestConnection).toBeCalledWith( expect.objectContaining({ - showDb: true, + showDb: ['on'], db: '12' }) ) @@ -321,7 +320,7 @@ describe('InstanceForm', () => { expect(handleSubmit).toBeCalledWith( expect.objectContaining({ - showDb: true, + showDb: ['on'], db: '12' }) ) @@ -332,7 +331,7 @@ describe('InstanceForm', () => { const handleTestConnection = jest.fn() render(
- { }) expect(handleTestConnection).toBeCalledWith( expect.objectContaining({ - sni: true, + sni: ['on'], servername: formFields.host }) ) @@ -366,7 +365,7 @@ describe('InstanceForm', () => { expect(handleSubmit).toBeCalledWith( expect.objectContaining({ - sni: true, + sni: ['on'], servername: formFields.host }) ) @@ -377,7 +376,7 @@ describe('InstanceForm', () => { const handleTestConnection = jest.fn() render(
- { }) expect(handleTestConnection).toBeCalledWith( expect.objectContaining({ - sni: true, + sni: ['on'], servername: '12' }) ) @@ -417,7 +416,7 @@ describe('InstanceForm', () => { expect(handleSubmit).toBeCalledWith( expect.objectContaining({ - sni: true, + sni: ['on'], servername: '12' }) ) @@ -428,7 +427,7 @@ describe('InstanceForm', () => { const handleTestConnection = jest.fn() render(
- { const handleTestConnection = jest.fn() const { queryByText } = render(
- { const handleTestConnection = jest.fn() render(
- { const handleTestConnection = jest.fn() render(
- { const handleSubmit = jest.fn() const { container } = render(
- { it('should render clone mode btn', () => { render( - { describe('should render proper fields with Clone mode', () => { it('should render proper fields for standalone db', () => { render( - { it('should render proper fields for sentinel db', () => { render( - { it('should render selected logical database with proper db index', () => { render( - @@ -779,7 +779,7 @@ describe('InstanceForm', () => { it('should render proper database alias', () => { render( - { expect(screen.getByTestId('db-alias')).toHaveTextContent('Clone ') }) - it('should render proper default values for standalone', () => { - render( - - ) - expect(screen.getByTestId('host')).toHaveValue('127.0.0.1') - expect(screen.getByTestId('port')).toHaveValue('6379') - expect(screen.getByTestId('name')).toHaveValue('127.0.0.1:6379') - }) - - it('should render proper default values for sentinel', () => { - render( - - ) - expect(screen.getByTestId('host')).toHaveValue('127.0.0.1') - expect(screen.getByTestId('port')).toHaveValue('26379') - }) + // it('should render proper default values for standalone', () => { + // render( + // + // ) + // expect(screen.getByTestId('host')).toHaveValue('127.0.0.1') + // expect(screen.getByTestId('port')).toHaveValue('6379') + // expect(screen.getByTestId('name')).toHaveValue('127.0.0.1:6379') + // }) }) it('should change Use SSH checkbox', async () => { const handleSubmit = jest.fn() render(
- {
) - fireEvent.click(screen.getByTestId('use-ssh')) + act(() => { + fireEvent.click(screen.getByTestId('use-ssh')) + }) expect(screen.getByTestId('use-ssh')).toBeChecked() }) @@ -841,7 +831,7 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() render(
- { const handleSubmit = jest.fn() render(
- {
) - fireEvent.click(screen.getByTestId('use-ssh')) + act(() => { + fireEvent.click(screen.getByTestId('use-ssh')) + }) expect(screen.getByTestId('sshHost')).toBeInTheDocument() expect(screen.getByTestId('sshPort')).toBeInTheDocument() - expect(screen.getByTestId('sshPort')).toHaveValue('22') expect(screen.getByTestId('sshPassword')).toBeInTheDocument() expect(screen.queryByTestId('sshPrivateKey')).not.toBeInTheDocument() expect(screen.queryByTestId('sshPassphrase')).not.toBeInTheDocument() @@ -888,7 +880,7 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() const { container } = render(
- { expect(screen.getByTestId('sshHost')).toBeInTheDocument() expect(screen.getByTestId('sshPort')).toBeInTheDocument() - expect(screen.getByTestId('sshPort')).toHaveValue('22') expect(screen.queryByTestId('sshPassword')).not.toBeInTheDocument() expect(screen.getByTestId('sshPrivateKey')).toBeInTheDocument() expect(screen.getByTestId('sshPassphrase')).toBeInTheDocument() @@ -921,11 +912,12 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() render(
- @@ -956,6 +948,15 @@ describe('InstanceForm', () => { ) }) + expect(screen.getByTestId(BTN_SUBMIT)).toBeDisabled() + + await act(() => { + fireEvent.change( + screen.getByTestId('sshPort'), + { target: { value: '22' } } + ) + }) + expect(screen.getByTestId(BTN_SUBMIT)).not.toBeDisabled() }) @@ -963,11 +964,12 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() const { container } = render(
- @@ -990,6 +992,10 @@ describe('InstanceForm', () => { screen.getByTestId('sshHost'), { target: { value: 'localhost' } } ) + fireEvent.change( + screen.getByTestId('sshPort'), + { target: { value: '22' } } + ) fireEvent.change( screen.getByTestId('sshUsername'), { target: { value: 'username' } } @@ -1012,11 +1018,12 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() render(
- @@ -1067,7 +1074,7 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() const { container } = render(
- { it('should render password input with 10_000 length limit', () => { render( - @@ -1140,14 +1147,14 @@ describe('InstanceForm', () => { it('should render security fields with proper attributes', () => { render( - @@ -1167,13 +1174,13 @@ describe('InstanceForm', () => { it('should render ssh password with proper attributes', () => { render( - @@ -1189,9 +1196,14 @@ describe('InstanceForm', () => { it('should render ssh password input with 10_000 length limit', () => { render( - ) @@ -1201,7 +1213,7 @@ describe('InstanceForm', () => { describe('timeout', () => { it('should render timeout input with 7 length limit and 1_000_000 value', () => { render( - @@ -1220,7 +1232,7 @@ describe('InstanceForm', () => { it('should put only numbers', () => { render( - @@ -1242,7 +1254,7 @@ describe('InstanceForm', () => { })) const { queryByTestId } = render( - @@ -1255,4 +1267,22 @@ describe('InstanceForm', () => { expect(queryByTestId('db-info-host')).toBeInTheDocument() }) }) + + it('should call submit on press Enter', async () => { + const handleSubmit = jest.fn() + render( +
+ +
+ ) + + await act(() => { + fireEvent.keyDown(screen.getByTestId('form'), { key: 'Enter', code: 13, charCode: 13 }) + }) + expect(handleSubmit).toBeCalled() + }) }) diff --git a/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/index.ts b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/index.ts new file mode 100644 index 0000000000..600d1f3024 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/index.ts @@ -0,0 +1,3 @@ +import ManualConnectionForm from './ManualConnectionForm' + +export default ManualConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.spec.tsx b/redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.spec.tsx rename to redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.tsx b/redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.tsx rename to redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.tsx diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/index.ts b/redisinsight/ui/src/pages/home/components/search-databases-list/index.ts similarity index 100% rename from redisinsight/ui/src/pages/home/components/SearchDatabasesList/index.ts rename to redisinsight/ui/src/pages/home/components/search-databases-list/index.ts diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/styles.module.scss b/redisinsight/ui/src/pages/home/components/search-databases-list/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/SearchDatabasesList/styles.module.scss rename to redisinsight/ui/src/pages/home/components/search-databases-list/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.spec.tsx new file mode 100644 index 0000000000..b37d75b206 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.spec.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import SentinelConnectionForm, { Props as SentinelConnectionFormProps } from + 'uiSrc/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import SentinelConnectionWrapper, { + Props, +} from './SentinelConnectionWrapper' + +const mockedProps = mock() + +jest.mock('./sentinel-connection-form/SentinelConnectionForm', () => ({ + __esModule: true, + namedExport: jest.fn(), + default: jest.fn(), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockSentinelConnectionForm = (props: SentinelConnectionFormProps) => ( +
+ + + +
+) + +describe('SentinelConnectionWrapper', () => { + beforeAll(() => { + SentinelConnectionForm.mockImplementation(mockSentinelConnectionForm) + }) + it('should render', () => { + expect( + render() + ).toBeTruthy() + }) + + it('should call onHostNamePaste', () => { + const component = render() + fireEvent.click(screen.getByTestId('onHostNamePaste-btn')) + expect(component).toBeTruthy() + }) + + it('should call onClose', () => { + const onClose = jest.fn() + render() + fireEvent.click(screen.getByTestId('onClose-btn')) + expect(onClose).toBeCalled() + }) + + it('Should call proper telemetry event', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() + + fireEvent.click(screen.getByTestId('onSubmit-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED, + }) + + sendEventTelemetry.mockRestore() + }) +}) diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.tsx b/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.tsx new file mode 100644 index 0000000000..a462959230 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router' + +import { fetchMastersSentinelAction, sentinelSelector, } from 'uiSrc/slices/instances/sentinel' +import { removeEmpty } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { caCertsSelector, fetchCaCerts } from 'uiSrc/slices/instances/caCerts' +import { Pages } from 'uiSrc/constants' +import { clientCertsSelector, fetchClientCerts, } from 'uiSrc/slices/instances/clientCerts' + +import { DbConnectionInfo } from 'uiSrc/pages/home/interfaces' +import { applyTlSDatabase, autoFillFormDetails, getTlsSettings } from 'uiSrc/pages/home/utils' +import { ADD_NEW, NO_CA_CERT } from 'uiSrc/pages/home/constants' +import { InstanceType } from 'uiSrc/slices/interfaces' +import SentinelConnectionForm from './sentinel-connection-form' + +export interface Props { + width: number + onClose?: () => void +} +const DEFAULT_SENTINEL_HOST = '127.0.0.1' +const DEFAULT_SENTINEL_PORT = '26379' + +const INITIAL_VALUES = { + host: DEFAULT_SENTINEL_HOST, + port: DEFAULT_SENTINEL_PORT, + username: '', + password: '', + tls: false, + tlsClientAuthRequired: false, + selectedTlsClientCertId: ADD_NEW, + verifyServerTlsCert: false, + selectedCaCertName: NO_CA_CERT, + +} + +const SentinelConnectionWrapper = (props: Props) => { + const { + width, + onClose, + } = props + const [initialValues, setInitialValues] = useState(INITIAL_VALUES) + + const { loading } = useSelector(sentinelSelector) + const { data: caCertificates } = useSelector(caCertsSelector) + const { data: certificates } = useSelector(clientCertsSelector) + + const history = useHistory() + const dispatch = useDispatch() + + useEffect(() => { + dispatch(fetchCaCerts()) + dispatch(fetchClientCerts()) + }, []) + + const onMastersSentinelFetched = () => { + history.push(Pages.sentinelDatabases) + } + + const handleSubmitDatabase = (payload: any) => { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED + }) + + dispatch(fetchMastersSentinelAction(payload, onMastersSentinelFetched)) + } + + const addDatabase = (tlsSettings: any, values: DbConnectionInfo) => { + const { + host, + port, + username, + password, + } = values + const database: any = { + host, + port: +port, + username, + password, + } + + // add tls for database + applyTlSDatabase(database, tlsSettings) + handleSubmitDatabase(removeEmpty(database)) + } + + const handleConnectionFormSubmit = (values: DbConnectionInfo) => { + const tlsSettings = getTlsSettings(values) + + addDatabase(tlsSettings, values) + } + + const handlePostHostName = (content: string): boolean => ( + autoFillFormDetails(content, initialValues, setInitialValues, InstanceType.Sentinel) + ) + + return ( +
+ +
+ ) +} + +export default SentinelConnectionWrapper diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/index.ts b/redisinsight/ui/src/pages/home/components/sentinel-connection/index.ts new file mode 100644 index 0000000000..0947d0226b --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/index.ts @@ -0,0 +1,3 @@ +import SentinelConnectionWrapper from './SentinelConnectionWrapper' + +export default SentinelConnectionWrapper diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.spec.tsx b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.spec.tsx new file mode 100644 index 0000000000..53958ab3d4 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.spec.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import SentinelConnectionForm, { Props } from './SentinelConnectionForm' + +const mockedProps = mock() + +const mockValues = { + host: 'host', + port: '123' +} + +describe('SentinelConnectionForm', () => { + it('should render', () => { + expect( + render() + ).toBeTruthy() + }) + + it('should call submit form on press Enter', async () => { + const mockSubmit = jest.fn() + render() + + await act(() => { + fireEvent.keyDown(screen.getByTestId('form'), { key: 'Enter', code: 13, charCode: 13 }) + }) + + expect(mockSubmit).toBeCalled() + }) + + it('should render Footer', async () => { + render( +
+ +
+ ) + + expect(screen.getByTestId('btn-submit')).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.tsx b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.tsx new file mode 100644 index 0000000000..bf4dff6350 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.tsx @@ -0,0 +1,192 @@ +import { + EuiButton, + EuiForm, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + keys, +} from '@elastic/eui' +import { FormikErrors, useFormik } from 'formik' +import { isEmpty, pick } from 'lodash' +import React, { useRef, useState } from 'react' +import ReactDOM from 'react-dom' + +import validationErrors from 'uiSrc/constants/validationErrors' +import { useResizableFormField } from 'uiSrc/services' +import { + fieldDisplayNames, +} from 'uiSrc/pages/home/constants' +import { getFormErrors, getSubmitButtonContent } from 'uiSrc/pages/home/utils' +import { DbConnectionInfo, ISubmitButton, IPasswordType } from 'uiSrc/pages/home/interfaces' +import { + MessageSentinel, + TlsDetails, + DatabaseForm, +} from 'uiSrc/pages/home/components/form' + +export interface Props { + width: number + loading: boolean + initialValues: DbConnectionInfo + certificates: { id: string; name: string }[], + caCertificates: { id: string; name: string }[], + onSubmit: (values: DbConnectionInfo) => void + onHostNamePaste: (content: string) => boolean + onClose?: () => void +} + +const getInitFieldsDisplayNames = ({ host, port }: any) => { + if (!host || !port) { + return pick(fieldDisplayNames, ['host', 'port']) + } + return {} +} + +const SentinelConnectionForm = (props: Props) => { + const { + initialValues = {}, + width, + onClose, + onSubmit, + onHostNamePaste, + loading, + certificates, + caCertificates, + } = props + + const [errors, setErrors] = useState>( + getInitFieldsDisplayNames(initialValues) + ) + + const formRef = useRef(null) + + const submitIsDisable = () => !isEmpty(errors) + + const validate = (values: DbConnectionInfo) => { + const errs = getFormErrors(values) + setErrors(errs) + return errs + } + + const formik = useFormik({ + initialValues, + validate, + enableReinitialize: true, + onSubmit: (values: any) => { + onSubmit(values) + }, + }) + + const [flexGroupClassName, flexItemClassName] = useResizableFormField( + formRef, + width + ) + + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.key === keys.ENTER && !submitIsDisable()) { + // event. + formik.submitForm() + } + } + + const SubmitButton = ({ + onClick, + submitIsDisabled, + }: ISubmitButton) => ( + + + Discover Database + + + ) + + const Footer = () => { + const footerEl = document.getElementById('footerDatabaseForm') + + if (footerEl) { + return ReactDOM.createPortal( + + + + + {onClose && ( + + Cancel + + )} + + + + , + footerEl + ) + } + return null + } + + return ( +
+
+ +
+ + + + +
+
+
+ ) +} + +export default SentinelConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/index.ts b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/index.ts new file mode 100644 index 0000000000..e598e90aae --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/index.ts @@ -0,0 +1,3 @@ +import SentinelConnectionForm from './SentinelConnectionForm' + +export default SentinelConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/styles.module.scss b/redisinsight/ui/src/pages/home/components/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/styles.module.scss rename to redisinsight/ui/src/pages/home/components/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.spec.tsx b/redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.spec.tsx similarity index 97% rename from redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.spec.tsx rename to redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.spec.tsx index b5d92c3fbc..3f8301c8c8 100644 --- a/redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.spec.tsx @@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash' import { render, screen, fireEvent, mockedStore, cleanup } from 'uiSrc/utils/test-utils' import { contentSelector } from 'uiSrc/slices/content/create-redis-buttons' import { MOCKED_CREATE_REDIS_BTN_CONTENT } from 'uiSrc/mocks/content/content' -import { AddDbType } from 'uiSrc/pages/home/components/AddDatabases/AddDatabasesContainer' +import { AddDbType } from 'uiSrc/pages/home/constants' import { setSocialDialogState } from 'uiSrc/slices/oauth/cloud' import { OAuthSocialSource } from 'uiSrc/slices/interfaces' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' diff --git a/redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.tsx b/redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.tsx similarity index 97% rename from redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.tsx rename to redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.tsx index b6649dea00..9d61200e3c 100644 --- a/redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.tsx +++ b/redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.tsx @@ -10,14 +10,14 @@ import { ThemeContext } from 'uiSrc/contexts/themeContext' import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' import darkLogo from 'uiSrc/assets/img/dark_logo.svg' import lightLogo from 'uiSrc/assets/img/light_logo.svg' -import { AddDbType } from 'uiSrc/pages/home/components/AddDatabases/AddDatabasesContainer' import { ReactComponent as CloudStars } from 'uiSrc/assets/img/oauth/stars.svg' import { ReactComponent as CloudIcon } from 'uiSrc/assets/img/oauth/cloud.svg' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import { contentSelector } from 'uiSrc/slices/content/create-redis-buttons' import { getContentByFeature } from 'uiSrc/utils/content' -import { HELP_LINKS, IHelpGuide } from 'uiSrc/pages/home/constants/help-links' +import { AddDbType, HELP_LINKS, IHelpGuide } from 'uiSrc/pages/home/constants' + import { ContentCreateRedis } from 'uiSrc/slices/interfaces/content' import { FeatureFlagComponent, @@ -34,7 +34,7 @@ export interface Props { onAddInstance: (addDbType?: AddDbType) => void } -const Welcome = ({ onAddInstance }: Props) => { +const WelcomeComponent = ({ onAddInstance }: Props) => { const featureFlags = useSelector(appFeatureFlagsFeaturesSelector) const { loading, data } = useSelector(contentSelector) @@ -293,4 +293,4 @@ const Welcome = ({ onAddInstance }: Props) => { ) } -export default Welcome +export default WelcomeComponent diff --git a/redisinsight/ui/src/pages/home/components/welcome-component/index.ts b/redisinsight/ui/src/pages/home/components/welcome-component/index.ts new file mode 100644 index 0000000000..b198b75e9b --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/welcome-component/index.ts @@ -0,0 +1,3 @@ +import WelcomeComponent from './WelcomeComponent' + +export default WelcomeComponent diff --git a/redisinsight/ui/src/pages/home/components/WelcomeComponent/styles.module.scss b/redisinsight/ui/src/pages/home/components/welcome-component/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/WelcomeComponent/styles.module.scss rename to redisinsight/ui/src/pages/home/components/welcome-component/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/constants/database.ts b/redisinsight/ui/src/pages/home/constants/database.ts new file mode 100644 index 0000000000..edd8e9585c --- /dev/null +++ b/redisinsight/ui/src/pages/home/constants/database.ts @@ -0,0 +1,4 @@ +export enum AddDbType { + manual, + auto, +} diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/constants.ts b/redisinsight/ui/src/pages/home/constants/form.ts similarity index 76% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/constants.ts rename to redisinsight/ui/src/pages/home/constants/form.ts index 057ce40220..5229a0a649 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/constants.ts +++ b/redisinsight/ui/src/pages/home/constants/form.ts @@ -2,6 +2,9 @@ export const ADD_NEW_CA_CERT = 'ADD_NEW_CA_CERT' export const NO_CA_CERT = 'NO_CA_CERT' export const ADD_NEW = 'ADD_NEW' export const NONE = 'NONE' +export const DEFAULT_HOST = '127.0.0.1' +export const DEFAULT_PORT = '6379' +export const DEFAULT_ALIAS = `${DEFAULT_HOST}:${DEFAULT_PORT}` export enum SshPassType { Password = 'password', @@ -29,3 +32,9 @@ export const fieldDisplayNames = { const DEFAULT_TIMEOUT_ENV = process.env.CONNECTIONS_TIMEOUT_DEFAULT || '30000' // 30 sec export const DEFAULT_TIMEOUT = parseInt(DEFAULT_TIMEOUT_ENV, 10) + +export enum SubmitBtnText { + AddDatabase = 'Add Redis Database', + EditDatabase = 'Apply Changes', + CloneDatabase = 'Clone Database' +} diff --git a/redisinsight/ui/src/pages/home/constants/index.ts b/redisinsight/ui/src/pages/home/constants/index.ts new file mode 100644 index 0000000000..59ec3920ec --- /dev/null +++ b/redisinsight/ui/src/pages/home/constants/index.ts @@ -0,0 +1,3 @@ +export * from './form' +export * from './help-links' +export * from './database' diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/interfaces.ts b/redisinsight/ui/src/pages/home/interfaces/form.ts similarity index 84% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/interfaces.ts rename to redisinsight/ui/src/pages/home/interfaces/form.ts index a52a19322a..909f19846e 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/interfaces.ts +++ b/redisinsight/ui/src/pages/home/interfaces/form.ts @@ -1,7 +1,8 @@ import { Instance } from 'uiSrc/slices/interfaces' -import { ADD_NEW_CA_CERT, NO_CA_CERT } from './constants' +import { ADD_NEW_CA_CERT, NO_CA_CERT } from 'uiSrc/pages/home/constants' export interface DbConnectionInfo extends Instance { + id?: string port: string tlsClientAuthRequired?: boolean certificates?: { id: number; name: string }[] @@ -26,8 +27,8 @@ export interface DbConnectionInfo extends Instance { sentinelMasterName?: string ssh?: boolean sshPassType?: string - sshHost: string - sshPort: string + sshHost?: string + sshPort?: string sshUsername?: string sshPassword?: string | true sshPrivateKey?: string | true @@ -39,3 +40,8 @@ export interface ISubmitButton { text?: string submitIsDisabled?: boolean } + +export enum IPasswordType { + Password = 'password', + Dual = 'dual', +} diff --git a/redisinsight/ui/src/pages/home/interfaces/index.ts b/redisinsight/ui/src/pages/home/interfaces/index.ts new file mode 100644 index 0000000000..54151771d5 --- /dev/null +++ b/redisinsight/ui/src/pages/home/interfaces/index.ts @@ -0,0 +1 @@ +export * from './form' diff --git a/redisinsight/ui/src/pages/home/utils/form.tsx b/redisinsight/ui/src/pages/home/utils/form.tsx new file mode 100644 index 0000000000..ddb493db44 --- /dev/null +++ b/redisinsight/ui/src/pages/home/utils/form.tsx @@ -0,0 +1,329 @@ +import { ConnectionString } from 'connection-string' +import { isUndefined, toString } from 'lodash' +import React from 'react' +import { FormikErrors } from 'formik' +import { REDIS_URI_SCHEMES } from 'uiSrc/constants' +import { InstanceType } from 'uiSrc/slices/interfaces' +import { + ADD_NEW, + ADD_NEW_CA_CERT, DEFAULT_ALIAS, + DEFAULT_HOST, DEFAULT_PORT, DEFAULT_TIMEOUT, + fieldDisplayNames, + NO_CA_CERT, NONE, + SshPassType +} from 'uiSrc/pages/home/constants' +import { DbConnectionInfo } from 'uiSrc/pages/home/interfaces' +import { Nullable } from 'uiSrc/utils' + +export const getTlsSettings = (values: DbConnectionInfo) => ({ + useTls: values.tls, + servername: (values.sni && values.servername) || undefined, + verifyServerCert: values.verifyServerTlsCert, + caCert: + !values.tls || values.selectedCaCertName === NO_CA_CERT + ? undefined + : values.selectedCaCertName === ADD_NEW_CA_CERT + ? { + new: { + name: values.newCaCertName, + certificate: values.newCaCert, + }, + } + : { + name: values.selectedCaCertName, + }, + clientAuth: values.tls && values.tlsClientAuthRequired, + clientCert: !values.tls + ? undefined + : typeof values.selectedTlsClientCertId === 'string' + && values.tlsClientAuthRequired + && values.selectedTlsClientCertId !== ADD_NEW + ? { id: values.selectedTlsClientCertId } + : values.selectedTlsClientCertId === ADD_NEW && values.tlsClientAuthRequired + ? { + new: { + name: values.newTlsCertPairName, + certificate: values.newTlsClientCert, + key: values.newTlsClientKey, + }, + } + : undefined, +}) + +export const applyTlSDatabase = (database: any, tlsSettings: any) => { + const { useTls, verifyServerCert, servername, caCert, clientAuth, clientCert } = tlsSettings + if (!useTls) return + + database.tls = useTls + database.tlsServername = servername + database.verifyServerCert = !!verifyServerCert + + if (!isUndefined(caCert?.new)) { + database.caCert = { + name: caCert?.new.name, + certificate: caCert?.new.certificate, + } + } + + if (!isUndefined(caCert?.name)) { + database.caCert = { id: caCert?.name } + } + + if (clientAuth) { + if (!isUndefined(clientCert.new)) { + database.clientCert = { + name: clientCert.new.name, + certificate: clientCert.new.certificate, + key: clientCert.new.key, + } + } + + if (!isUndefined(clientCert.id)) { + database.clientCert = { id: clientCert.id } + } + } +} + +export const applySSHDatabase = (database: any, values: DbConnectionInfo) => { + const { + ssh, + sshPassType, + sshHost, + sshPort, + sshPassword, + sshUsername, + sshPassphrase, + sshPrivateKey, + } = values + + if (ssh) { + database.ssh = true + database.sshOptions = { + host: sshHost, + port: +sshPort, + username: sshUsername, + } + + if (sshPassType === SshPassType.Password) { + database.sshOptions.password = sshPassword + database.sshOptions.passphrase = null + database.sshOptions.privateKey = null + } + + if (sshPassType === SshPassType.PrivateKey) { + database.sshOptions.password = null + database.sshOptions.passphrase = sshPassphrase + database.sshOptions.privateKey = sshPrivateKey + } + } +} + +export const getFormErrors = (values: DbConnectionInfo) => { + const errs: FormikErrors = {} + + if (!values.host) { + errs.host = fieldDisplayNames.host + } + if (!values.port) { + errs.port = fieldDisplayNames.port + } + + if ( + values.tls + && values.verifyServerTlsCert + && values.selectedCaCertName === NO_CA_CERT + ) { + errs.selectedCaCertName = fieldDisplayNames.selectedCaCertName + } + + if ( + values.tls + && values.selectedCaCertName === ADD_NEW_CA_CERT + && values.newCaCertName === '' + ) { + errs.newCaCertName = fieldDisplayNames.newCaCertName + } + + if ( + values.tls + && values.selectedCaCertName === ADD_NEW_CA_CERT + && values.newCaCert === '' + ) { + errs.newCaCert = fieldDisplayNames.newCaCert + } + + if ( + values.tls + && values.sni + && values.servername === '' + ) { + errs.servername = fieldDisplayNames.servername + } + + if ( + values.tls + && values.tlsClientAuthRequired + && values.selectedTlsClientCertId === ADD_NEW + ) { + if (values.newTlsCertPairName === '') { + errs.newTlsCertPairName = fieldDisplayNames.newTlsCertPairName + } + if (values.newTlsClientCert === '') { + errs.newTlsClientCert = fieldDisplayNames.newTlsClientCert + } + if (values.newTlsClientKey === '') { + errs.newTlsClientKey = fieldDisplayNames.newTlsClientKey + } + } + + if (values.ssh) { + if (!values.sshHost) { + errs.sshHost = fieldDisplayNames.sshHost + } + if (!values.sshPort) { + errs.sshPort = fieldDisplayNames.sshPort + } + if (!values.sshUsername) { + errs.sshUsername = fieldDisplayNames.sshUsername + } + if (values.sshPassType === SshPassType.PrivateKey && !values.sshPrivateKey) { + errs.sshPrivateKey = fieldDisplayNames.sshPrivateKey + } + } + + return errs +} + +export const autoFillFormDetails = ( + content: string, + initialValues: any, + setInitialValues: (data: any) => void, + instanceType: InstanceType +): boolean => { + try { + const details = new ConnectionString(content) + + /* If a protocol exists, it should be a redis protocol */ + if (details.protocol && !REDIS_URI_SCHEMES.includes(details.protocol)) return false + /* + * Auto fill logic: + * 1) If the port is parsed, we are sure that the user has indeed copied a connection string. + * '172.18.0.2:12000' => {host: '172,18.0.2', port: 12000} + * 'redis-12000.cluster.local:12000' => {host: 'redis-12000.cluster.local', port: 12000} + * 'lorem ipsum' => {host: undefined, port: undefined} + * 2) If the port is `undefined` but a redis URI scheme is present as protocol, we follow + * the "Scheme semantics" as mentioned in the official URI schemes. + * i) redis:// - https://www.iana.org/assignments/uri-schemes/prov/redis + * ii) rediss:// - https://www.iana.org/assignments/uri-schemes/prov/rediss + */ + if ( + details.port !== undefined + || REDIS_URI_SCHEMES.includes(details.protocol || '') + ) { + const getUpdatedInitialValues = () => { + switch (instanceType) { + case InstanceType.RedisEnterpriseCluster: { + return ({ + host: details.hostname || initialValues.host || 'localhost', + port: `${details.port || initialValues.port || 9443}`, + username: details.user || '', + password: details.password || '', + }) + } + + case InstanceType.Sentinel: { + return ({ + host: details.hostname || initialValues.host || 'localhost', + port: `${details.port || initialValues.port || 9443}`, + username: details.user || '', + password: details.password, + tls: details.protocol === 'rediss', + }) + } + + case InstanceType.Standalone: { + return ({ + name: details.host || initialValues.name || 'localhost:6379', + host: details.hostname || initialValues.host || 'localhost', + port: `${details.port || initialValues.port || 9443}`, + username: details.user || '', + password: details.password, + tls: details.protocol === 'rediss', + ssh: false, + sshPassType: SshPassType.Password + }) + } + default: { + return {} + } + } + } + setInitialValues(getFormValues(getUpdatedInitialValues())) + /* + * autofill was successfull so return true + */ + return true + } + } catch (err) { + /* The pasted content is not a connection URI so ignore. */ + return false + } + return false +} + +export const getSubmitButtonContent = (errors: FormikErrors, submitIsDisabled?: boolean) => { + const maxErrorsCount = 5 + const errorsArr = Object.values(errors).map((err) => [ + err, +
, + ]) + + if (errorsArr.length > maxErrorsCount) { + errorsArr.splice(maxErrorsCount, errorsArr.length, ['...']) + } + return submitIsDisabled ? ( + {errorsArr} + ) : null +} + +export const getFormValues = (instance?: Nullable>) => ({ + ...instance, + host: instance?.host ?? (instance ? '' : DEFAULT_HOST), + port: instance?.port?.toString() ?? (instance ? '' : DEFAULT_PORT), + timeout: instance?.timeout + ? toString(instance?.timeout / 1_000) + : toString(DEFAULT_TIMEOUT / 1_000), + name: instance?.name ?? (instance ? '' : DEFAULT_ALIAS), + username: instance?.username ?? '', + password: instance?.password ?? '', + tls: instance?.tls ?? false, + db: instance?.db, + compressor: instance?.compressor ?? NONE, + modules: instance?.modules, + showDb: !!instance?.db, + showCompressor: instance && instance.compressor && instance.compressor !== NONE, + sni: !!instance?.servername, + servername: instance?.servername, + newCaCert: '', + newCaCertName: '', + selectedCaCertName: instance?.caCert?.id ?? NO_CA_CERT, + tlsClientAuthRequired: instance?.clientCert?.id ?? false, + verifyServerTlsCert: instance?.verifyServerCert ?? false, + newTlsCertPairName: '', + selectedTlsClientCertId: instance?.clientCert?.id ?? ADD_NEW, + newTlsClientCert: '', + newTlsClientKey: '', + sentinelMasterName: instance?.sentinelMaster?.name || '', + sentinelMasterUsername: instance?.sentinelMaster?.username, + sentinelMasterPassword: instance?.sentinelMaster?.password, + ssh: instance?.ssh ?? false, + sshPassType: instance?.sshOptions + ? (instance.sshOptions.privateKey ? SshPassType.PrivateKey : SshPassType.Password) + : SshPassType.Password, + sshHost: instance?.sshOptions?.host ?? '', + sshPort: instance?.sshOptions?.port ?? 22, + sshUsername: instance?.sshOptions?.username ?? '', + sshPassword: instance?.sshOptions?.password ?? '', + sshPrivateKey: instance?.sshOptions?.privateKey ?? '', + sshPassphrase: instance?.sshOptions?.passphrase ?? '' +}) diff --git a/redisinsight/ui/src/pages/home/utils/index.ts b/redisinsight/ui/src/pages/home/utils/index.ts new file mode 100644 index 0000000000..54151771d5 --- /dev/null +++ b/redisinsight/ui/src/pages/home/utils/index.ts @@ -0,0 +1 @@ +export * from './form' diff --git a/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx b/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx index dea2c66625..c68c2a7cec 100644 --- a/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx +++ b/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx @@ -11,14 +11,14 @@ import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { setConnectedInstanceId } from 'uiSrc/slices/instances/instances' import { appInfoSelector } from 'uiSrc/slices/app/info' import { Instance } from 'uiSrc/slices/interfaces' -import AddDatabaseContainer from 'uiSrc/pages/home/components/AddDatabases/AddDatabasesContainer' import { ContentCreateRedis } from 'uiSrc/slices/interfaces/content' import PromoLink from 'uiSrc/components/promo-link/PromoLink' import { getPathToResource } from 'uiSrc/services/resourcesService' -import { HELP_LINKS } from 'uiSrc/pages/home/constants/help-links' +import { HELP_LINKS } from 'uiSrc/pages/home/constants' import { sendEventTelemetry } from 'uiSrc/telemetry' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { contentSelector } from 'uiSrc/slices/content/create-redis-buttons' +import DatabasePanel from 'uiSrc/pages/home/components/database-panel/DatabasePanel' import './styles.scss' import styles from './styles.module.scss' @@ -119,7 +119,7 @@ const EditConnection = () => { )}
- void) { +export function updateInstanceAction({ id, ...payload }: Partial, onSuccess?: () => void) { return async (dispatch: AppDispatch) => { dispatch(defaultInstanceChanging()) diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 3180a32641..a52c6f83c5 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -15,7 +15,7 @@ import { CreateSentinelDatabaseDto } from 'apiSrc/modules/redis-sentinel/dto/cre import { CreateSentinelDatabaseResponse } from 'apiSrc/modules/redis-sentinel/dto/create.sentinel.database.response' import { RedisNodeInfoResponse } from 'apiSrc/modules/database/dto/redis-info.dto' -export interface Instance extends DatabaseInstanceResponse { +export interface Instance extends Partial { host: string port: number nameFromProvider?: Nullable diff --git a/tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts b/tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts index c2123af937..2771935808 100644 --- a/tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts @@ -49,7 +49,7 @@ test('Verify that user can add several fields and values during Stream key creat const streamData = { 'string': Common.generateWord(20), 'array': `[${Common.generateWord(20)}, ${chance.integer()}]`, 'integer': `${chance.integer()}`, 'json': '{\'test\': \'test\'}', 'null': 'null', 'boolean': 'true' }; const scrollSelector = Selector('.eui-yScroll').nth(-1); - // Open Add New Stream Key Form + // Open Add New Stream Key form await browserPage.commonAddNewKey(keyName); await t.click(browserPage.streamOption); // Verify that user can see Entity ID filled by * by default on add Stream key form