diff --git a/redisinsight/api/src/modules/rdi/dto/update.rdi.dto.ts b/redisinsight/api/src/modules/rdi/dto/update.rdi.dto.ts index b41c36b706..6e52c73787 100644 --- a/redisinsight/api/src/modules/rdi/dto/update.rdi.dto.ts +++ b/redisinsight/api/src/modules/rdi/dto/update.rdi.dto.ts @@ -2,5 +2,5 @@ import { OmitType, PartialType } from '@nestjs/swagger'; import { Rdi } from 'src/modules/rdi/models'; export class UpdateRdiDto extends PartialType(OmitType(Rdi, [ - 'id', 'lastConnection', + 'id', 'lastConnection', 'url', 'version', ] as const)) {} diff --git a/redisinsight/api/src/modules/rdi/rdi.service.ts b/redisinsight/api/src/modules/rdi/rdi.service.ts index e308a76f80..85d281e495 100644 --- a/redisinsight/api/src/modules/rdi/rdi.service.ts +++ b/redisinsight/api/src/modules/rdi/rdi.service.ts @@ -13,12 +13,19 @@ import { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider import { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory'; import { SessionMetadata } from 'src/common/models'; import { wrapRdiPipelineError } from 'src/modules/rdi/exceptions'; +import { isUndefined, omitBy } from 'lodash'; +import { deepMerge } from 'src/common/utils'; import { RdiAnalytics } from './rdi.analytics'; @Injectable() export class RdiService { private logger = new Logger('RdiService'); + static connectionFields: string[] = [ + 'username', + 'password', + ]; + constructor( private readonly repository: RdiRepository, private readonly analytics: RdiAnalytics, @@ -26,6 +33,10 @@ export class RdiService { private readonly rdiClientFactory: RdiClientFactory, ) {} + static isConnectionAffected(dto: UpdateRdiDto) { + return Object.keys(omitBy(dto, isUndefined)).some((field) => this.connectionFields.includes(field)); + } + async list(): Promise { return await this.repository.list(); } @@ -41,17 +52,20 @@ export class RdiService { } async update(rdiClientMetadata: RdiClientMetadata, dto: UpdateRdiDto): Promise { - // TODO update dto to get only updated fields - const model = classToClass(Rdi, dto); - - await this.rdiClientProvider.delete(rdiClientMetadata); + const oldRdiInstance = await this.get(rdiClientMetadata.id); + const newRdiInstance = await deepMerge(oldRdiInstance, dto); try { - await this.rdiClientFactory.createClient(rdiClientMetadata, model); + if (RdiService.isConnectionAffected(dto)) { + await this.rdiClientFactory.createClient(rdiClientMetadata, newRdiInstance); + await this.rdiClientProvider.deleteManyByRdiId(rdiClientMetadata.id); + } + + return await this.repository.update(rdiClientMetadata.id, newRdiInstance); } catch (error) { + this.logger.error(`Failed to update rdi instance ${rdiClientMetadata.id}`, error); throw wrapRdiPipelineError(error); } - return await this.repository.update(rdiClientMetadata.id, model); } async create(sessionMetadata: SessionMetadata, dto: CreateRdiDto): Promise { diff --git a/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts b/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts index 64eab7d36a..f041581e7b 100644 --- a/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts +++ b/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts @@ -70,7 +70,7 @@ export class LocalRdiRepository extends RdiRepository { /** * @inheritDoc */ - public async update(id: string, rdi: Partial): Promise { + public async update(id: string, rdi: Rdi): Promise { const oldEntity = await this.modelEncryptor.decryptEntity((await this.repository.findOneBy({ id })), true); const newEntity = classToClass(RdiEntity, rdi); diff --git a/redisinsight/api/src/modules/rdi/repository/rdi.repository.ts b/redisinsight/api/src/modules/rdi/repository/rdi.repository.ts index 3e00546868..d0e1254545 100644 --- a/redisinsight/api/src/modules/rdi/repository/rdi.repository.ts +++ b/redisinsight/api/src/modules/rdi/repository/rdi.repository.ts @@ -31,7 +31,7 @@ export abstract class RdiRepository { /** * Delete RDI by id - * @param id + * @param ids */ abstract delete(ids: string[]): Promise; } diff --git a/redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx b/redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx index 0d4294c9fb..486433ac02 100644 --- a/redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx +++ b/redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx @@ -178,7 +178,7 @@ describe('RdiPage', () => { expect(screen.getByTestId('connection-form-password-input')).toHaveValue('') }) - it('should call edit instance when editInstance is provided', async () => { + it('should call edit instance with proper data when editInstance is provided', async () => { render() fireEvent.click(screen.getByTestId('edit-instance-1')) @@ -196,16 +196,10 @@ describe('RdiPage', () => { }) expect(editInstanceAction).toBeCalledWith( + '1', { - id: '1', - lastConnection: new Date('1/1/2024'), name: 'name', password: 'password2', - url: 'redis-12345.c253.us-central1-1.gce.cloud.redislabs.com:12345', - username: 'user', - version: '1.2', - visible: true, - error: '' }, expect.any(Function) ) diff --git a/redisinsight/ui/src/pages/rdi/home/RdiPage.tsx b/redisinsight/ui/src/pages/rdi/home/RdiPage.tsx index 1b73e02cd6..17ddade340 100644 --- a/redisinsight/ui/src/pages/rdi/home/RdiPage.tsx +++ b/redisinsight/ui/src/pages/rdi/home/RdiPage.tsx @@ -17,7 +17,7 @@ import { sendPageViewTelemetry } from 'uiSrc/telemetry' import HomePageTemplate from 'uiSrc/templates/home-page-template' -import { setTitle } from 'uiSrc/utils' +import { getFormUpdates, setTitle } from 'uiSrc/utils' import EmptyMessage from './empty-message/EmptyMessage' import ConnectionForm from './connection-form/ConnectionForm' import RdiHeader from './header/RdiHeader' @@ -47,14 +47,15 @@ const RdiPage = () => { setWidth(innerWidth) } - const handleAddInstance = (instance: Partial) => { + const handleFormSubmit = (instance: Partial) => { const onSuccess = () => { setIsConnectionFormOpen(false) setEditInstance(null) } if (editInstance) { - dispatch(editInstanceAction({ ...editInstance, ...instance }, onSuccess)) + const payload = getFormUpdates(instance, editInstance) + dispatch(editInstanceAction(editInstance.id, payload, onSuccess)) } else { dispatch(createInstanceAction({ ...instance }, onSuccess)) } @@ -153,7 +154,7 @@ const RdiPage = () => { > {isConnectionFormOpen && ( { }) }) - it('should disable test connection button when form is invalid', async () => { + // TODO update when add test connection endpoint + it.skip('should disable test connection button when form is invalid', async () => { render() await waitFor(() => { @@ -91,7 +92,7 @@ describe('ConnectionForm', () => { expect(tooltip).toBeInTheDocument() }) - it('should show validation tooltip when test connection button is disabled', async () => { + it.skip('should show validation tooltip when test connection button is disabled', async () => { render() fireEvent.mouseOver(screen.getByTestId('connection-form-test-button')) @@ -107,7 +108,7 @@ describe('ConnectionForm', () => { expect(screen.getByTestId('connection-form-add-button')).toBeDisabled() }) - it('should disable test connection button when isLoading = true', async () => { + it.skip('should disable test connection button when isLoading = true', async () => { render() expect(screen.getByTestId('connection-form-test-button')).toBeDisabled() diff --git a/redisinsight/ui/src/pages/rdi/home/connection-form/ConnectionForm.tsx b/redisinsight/ui/src/pages/rdi/home/connection-form/ConnectionForm.tsx index 799abb5f71..c5a63c5a90 100644 --- a/redisinsight/ui/src/pages/rdi/home/connection-form/ConnectionForm.tsx +++ b/redisinsight/ui/src/pages/rdi/home/connection-form/ConnectionForm.tsx @@ -11,7 +11,6 @@ import { EuiToolTip } from '@elastic/eui' import { Field, FieldInputProps, FieldMetaProps, Form, Formik, FormikErrors, FormikHelpers } from 'formik' -import { omit } from 'lodash' import React, { useEffect, useState } from 'react' import cx from 'classnames' @@ -29,7 +28,7 @@ export interface ConnectionFormValues { } export interface Props { - onAddInstance: (instance: Partial) => void + onSubmit: (instance: Partial) => void onCancel: () => void editInstance: RdiInstance | null isLoading: boolean @@ -69,18 +68,15 @@ const UrlTooltip = () => ( ) -const ConnectionForm = ({ onAddInstance, onCancel, editInstance, isLoading }: Props) => { +const ConnectionForm = (props: Props) => { + const { onSubmit, onCancel, editInstance, isLoading } = props + const [initialFormValues, setInitialFormValues] = useState(getInitialValues(editInstance)) - const [passwordChanged, setPasswordChanged] = useState(false) useEffect(() => { setInitialFormValues(getInitialValues(editInstance)) }, [editInstance]) - const onSubmit = (formValues: ConnectionFormValues) => { - onAddInstance({ ...omit(formValues, !passwordChanged ? 'password' : '') }) - } - const validate = (values: ConnectionFormValues) => { const errors: FormikErrors = {} @@ -167,20 +163,18 @@ const ConnectionForm = ({ onAddInstance, onCancel, editInstance, isLoading }: Pr meta: FieldMetaProps }) => ( { - if (field.value === SECURITY_FIELD && !meta.touched) { - form.setFieldValue('password', '') - } - - setPasswordChanged(true) - }} - /> + data-testid="connection-form-password-input" + className={styles.passwordField} + fullWidth + placeholder="Enter Password" + maxLength={500} + {...field} + onFocus={() => { + if (field.value === SECURITY_FIELD && !meta.touched) { + form.setFieldValue('password', '') + } + }} + /> )} @@ -188,39 +182,39 @@ const ConnectionForm = ({ onAddInstance, onCancel, editInstance, isLoading }: Pr
- - - Test Connection - - + {/* */} + {/* */} + {/* Test Connection */} + {/* */} + {/* */} - Cancel - + Cancel + - - Add Instance - - + + {editInstance ? 'Apply Changes' : 'Add Instance'} + + diff --git a/redisinsight/ui/src/slices/rdi/instances.ts b/redisinsight/ui/src/slices/rdi/instances.ts index 9611cdee06..a169d2d740 100644 --- a/redisinsight/ui/src/slices/rdi/instances.ts +++ b/redisinsight/ui/src/slices/rdi/instances.ts @@ -209,7 +209,8 @@ export function createInstanceAction(payload: Partial, onSuccess?: // Asynchronous thunk action export function editInstanceAction( - { id, ...payload }: Partial, + id: string, + payload: Partial, onSuccess?: (data: RdiInstanceResponse) => void ) { return async (dispatch: AppDispatch) => {