Skip to content

Commit 3c30161

Browse files
Merge pull request #2602 from RedisInsight/fe/feature/RI-4813_not_duplicate_cloud_database
#RI-4813 - [FE] Do not duplicate Cloud databases
2 parents b1a36bc + abd5e58 commit 3c30161

File tree

9 files changed

+238
-40
lines changed

9 files changed

+238
-40
lines changed

redisinsight/ui/src/components/notifications/success-messages.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,9 @@ export default {
222222
REMOVED_CAPI_KEY: (name: string) => ({
223223
title: 'API Key has been removed',
224224
message: `${formatNameShort(name)} has been removed from RedisInsight.`
225-
})
225+
}),
226+
DATABASE_ALREADY_EXISTS: () => ({
227+
title: 'Database already exists',
228+
message: 'No new database connections have been added.',
229+
}),
226230
}

redisinsight/ui/src/constants/customErrorCodes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ export enum CustomErrorCodes {
3030
CloudSubscriptionUnableToDetermine = 11_111,
3131
CloudTaskNotFound = 11_112,
3232
CloudJobNotFound = 11_113,
33+
34+
// General database errors [11200, 11299]
35+
DatabaseAlreadyExists = 11_200,
3336
}

redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { instance, mock } from 'ts-mockito'
33
import { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils'
44
import { ConnectionType, InstanceType } from 'uiSrc/slices/interfaces'
55
import { BuildType } from 'uiSrc/constants/env'
6+
import { appRedirectionSelector } from 'uiSrc/slices/app/url-handling'
7+
import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'
68
import InstanceForm, { Props } from './InstanceForm'
79
import { ADD_NEW_CA_CERT } from './constants'
810
import { DbConnectionInfo } from './interfaces'
@@ -32,6 +34,11 @@ jest.mock('uiSrc/slices/instances/instances', () => ({
3234
setConnectedInstanceId: jest.fn,
3335
}))
3436

37+
jest.mock('uiSrc/slices/app/url-handling', () => ({
38+
...jest.requireActual('uiSrc/slices/app/url-handling'),
39+
appRedirectionSelector: jest.fn().mockReturnValue(() => ({ action: null })),
40+
}))
41+
3542
describe('InstanceForm', () => {
3643
it('should render', () => {
3744
expect(
@@ -1178,4 +1185,25 @@ describe('InstanceForm', () => {
11781185
expect(screen.getByTestId('timeout')).toHaveAttribute('value', '112')
11791186
})
11801187
})
1188+
1189+
describe('cloud', () => {
1190+
it('some fields should be readonly if instance data source from cloud', () => {
1191+
(appRedirectionSelector as jest.Mock).mockImplementation(() => ({
1192+
action: UrlHandlingActions.Connect,
1193+
}))
1194+
1195+
const { queryByTestId } = render(
1196+
<InstanceForm
1197+
{...instance(mockedProps)}
1198+
formFields={formFields}
1199+
/>
1200+
)
1201+
1202+
expect(queryByTestId('connection-type')).not.toBeInTheDocument()
1203+
expect(queryByTestId('host')).not.toBeInTheDocument()
1204+
expect(queryByTestId('port')).not.toBeInTheDocument()
1205+
expect(queryByTestId('db-info-port')).toBeInTheDocument()
1206+
expect(queryByTestId('db-info-host')).toBeInTheDocument()
1207+
})
1208+
})
11811209
})

redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import {
88
EuiToolTip,
99
keys,
1010
} from '@elastic/eui'
11-
1211
import { FormikErrors, useFormik } from 'formik'
1312
import { isEmpty, pick, toString } from 'lodash'
1413
import React, { useEffect, useRef, useState } from 'react'
1514
import ReactDOM from 'react-dom'
1615
import { useDispatch, useSelector } from 'react-redux'
1716
import { useHistory } from 'react-router'
17+
1818
import { PageNames, Pages } from 'uiSrc/constants'
1919
import validationErrors from 'uiSrc/constants/validationErrors'
2020
import DatabaseAlias from 'uiSrc/pages/home/components/DatabaseAlias'
@@ -27,11 +27,12 @@ import {
2727
resetInstanceUpdateAction,
2828
setConnectedInstanceId,
2929
} from 'uiSrc/slices/instances/instances'
30-
3130
import { ConnectionType, InstanceType, } from 'uiSrc/slices/interfaces'
3231
import { getRedisModulesSummary, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
33-
import { getDiffKeysOfObjectValues, isRediStack } from 'uiSrc/utils'
32+
import { Nullable, getDiffKeysOfObjectValues, isRediStack } from 'uiSrc/utils'
3433
import { BuildType } from 'uiSrc/constants/env'
34+
import { appRedirectionSelector } from 'uiSrc/slices/app/url-handling'
35+
import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'
3536

3637
import {
3738
ADD_NEW_CA_CERT,
@@ -83,6 +84,7 @@ export interface Props {
8384
onClose?: () => void
8485
onAliasEdited?: (value: string) => void
8586
setErrorMsgRef?: (database: HTMLDivElement | null) => void
87+
urlHandlingAction?: Nullable<UrlHandlingActions>
8688
}
8789

8890
const getInitFieldsDisplayNames = ({ host, port, name, instanceType }: any) => {
@@ -148,6 +150,7 @@ const AddStandaloneForm = (props: Props) => {
148150
} = props
149151

150152
const { contextInstanceId, lastPage } = useSelector(appContextSelector)
153+
const { action } = useSelector(appRedirectionSelector)
151154

152155
const prepareInitialValues = () => ({
153156
host: host ?? getDefaultHost(),
@@ -205,6 +208,7 @@ const AddStandaloneForm = (props: Props) => {
205208
const formRef = useRef<HTMLDivElement>(null)
206209

207210
const submitIsDisable = () => !isEmpty(errors)
211+
const isFromCloud = action === UrlHandlingActions.Connect
208212

209213
const validate = (values: DbConnectionInfo) => {
210214
const errs: FormikErrors<DbConnectionInfo> = {}
@@ -524,7 +528,7 @@ const AddStandaloneForm = (props: Props) => {
524528
</div>
525529
)}
526530
<div className="getStartedForm" ref={formRef}>
527-
{!isEditMode && instanceType === InstanceType.Standalone && (
531+
{!isEditMode && instanceType === InstanceType.Standalone && !isFromCloud && (
528532
<>
529533
<MessageStandalone />
530534
<br />
@@ -536,7 +540,7 @@ const AddStandaloneForm = (props: Props) => {
536540
<br />
537541
</>
538542
)}
539-
{!isEditMode && (
543+
{!isEditMode && !isFromCloud && (
540544
<EuiForm
541545
component="form"
542546
onSubmit={formik.handleSubmit}
@@ -583,7 +587,7 @@ const AddStandaloneForm = (props: Props) => {
583587
)}
584588
</EuiForm>
585589
)}
586-
{(isEditMode || isCloneMode) && connectionType !== ConnectionType.Sentinel && (
590+
{(isEditMode || isCloneMode || isFromCloud) && connectionType !== ConnectionType.Sentinel && (
587591
<>
588592
{!isCloneMode && (
589593
<DbInfo
@@ -594,6 +598,7 @@ const AddStandaloneForm = (props: Props) => {
594598
modules={modules}
595599
nameFromProvider={nameFromProvider}
596600
nodes={nodes}
601+
isFromCloud={isFromCloud}
597602
/>
598603
)}
599604
<EuiForm
@@ -608,6 +613,7 @@ const AddStandaloneForm = (props: Props) => {
608613
flexGroupClassName={flexGroupClassName}
609614
isCloneMode={isCloneMode}
610615
isEditMode={isEditMode}
616+
isFromCloud={isFromCloud}
611617
connectionType={connectionType}
612618
instanceType={instanceType}
613619
onHostNamePaste={onHostNamePaste}

redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/DatabaseForm.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface Props {
2626
onHostNamePaste: (content: string) => boolean
2727
instanceType: InstanceType
2828
connectionType?: ConnectionType
29+
isFromCloud: boolean
2930
}
3031

3132
const DatabaseForm = (props: Props) => {
@@ -37,7 +38,8 @@ const DatabaseForm = (props: Props) => {
3738
isCloneMode,
3839
onHostNamePaste,
3940
instanceType,
40-
connectionType
41+
connectionType,
42+
isFromCloud,
4143
} = props
4244

4345
const { server } = useSelector(appInfoSelector)
@@ -81,7 +83,7 @@ const DatabaseForm = (props: Props) => {
8183
return (
8284
<>
8385
<EuiFlexGroup className={flexGroupClassName}>
84-
{(!isEditMode || isCloneMode) && (
86+
{(!isEditMode || isCloneMode) && !isFromCloud && (
8587
<EuiFlexItem className={flexItemClassName}>
8688
<EuiFormRow label="Host*">
8789
<EuiFieldText
@@ -106,7 +108,7 @@ const DatabaseForm = (props: Props) => {
106108
</EuiFormRow>
107109
</EuiFlexItem>
108110
)}
109-
{server?.buildType !== BuildType.RedisStack && (
111+
{server?.buildType !== BuildType.RedisStack && !isFromCloud && (
110112
<EuiFlexItem className={flexItemClassName}>
111113
<EuiFormRow label="Port*" helpText="Should not exceed 65535.">
112114
<EuiFieldNumber

redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/DbInfo.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ export interface Props {
2121
port: string
2222
db: Nullable<number>
2323
modules: AdditionalRedisModule[]
24+
isFromCloud: boolean
2425
}
2526

2627
const DbInfo = (props: Props) => {
27-
const { connectionType, nameFromProvider, nodes = null, host, port, db, modules } = props
28+
const { connectionType, nameFromProvider, nodes = null, host, port, db, modules, isFromCloud } = props
2829

2930
const { server } = useSelector(appInfoSelector)
3031

@@ -59,16 +60,18 @@ const DbInfo = (props: Props) => {
5960

6061
return (
6162
<EuiListGroup className={styles.dbInfoGroup} flush>
62-
<EuiListGroupItem
63-
label={(
64-
<EuiText color="subdued" size="s">
65-
Connection Type:
66-
<EuiTextColor color="default" className={styles.dbInfoListValue} data-testid="connection-type">
67-
{capitalize(connectionType)}
68-
</EuiTextColor>
69-
</EuiText>
70-
)}
71-
/>
63+
{!isFromCloud && (
64+
<EuiListGroupItem
65+
label={(
66+
<EuiText color="subdued" size="s">
67+
Connection Type:
68+
<EuiTextColor color="default" className={styles.dbInfoListValue} data-testid="connection-type">
69+
{capitalize(connectionType)}
70+
</EuiTextColor>
71+
</EuiText>
72+
)}
73+
/>
74+
)}
7275

7376
{nameFromProvider && (
7477
<EuiListGroupItem
@@ -88,19 +91,19 @@ const DbInfo = (props: Props) => {
8891
{!!nodes?.length && <AppendEndpoints />}
8992
<EuiText color="subdued" size="s">
9093
Host:
91-
<EuiTextColor color="default" className={styles.dbInfoListValue}>
94+
<EuiTextColor color="default" className={styles.dbInfoListValue} data-testid="db-info-host">
9295
{host}
9396
</EuiTextColor>
9497
</EuiText>
9598
</>
9699
)}
97100
/>
98-
{server?.buildType === BuildType.RedisStack && (
101+
{(server?.buildType === BuildType.RedisStack || isFromCloud) && (
99102
<EuiListGroupItem
100103
label={(
101104
<EuiText color="subdued" size="s">
102105
Port:
103-
<EuiTextColor color="default" className={styles.dbInfoListValue}>
106+
<EuiTextColor color="default" className={styles.dbInfoListValue} data-testid="db-info-port">
104107
{port}
105108
</EuiTextColor>
106109
</EuiText>

redisinsight/ui/src/slices/app/url-handling.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const {
3131
setUrlHandlingInitialState,
3232
setFromUrl,
3333
setUrlDbConnection,
34-
setUrlProperties
34+
setUrlProperties,
3535
} = appUrlHandlingSlice.actions
3636

3737
export const appRedirectionSelector = (state: RootState) => state.app.urlHandling

redisinsight/ui/src/slices/instances/instances.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { first, isNull, map, find, orderBy } from 'lodash'
1+
import { first, isNull, map, find, orderBy, get } from 'lodash'
22
import { createSlice } from '@reduxjs/toolkit'
33
import axios, { AxiosError, CancelTokenSource } from 'axios'
44

55
import ApiErrors from 'uiSrc/constants/apiErrors'
66
import { apiService, localStorageService, sessionStorageService } from 'uiSrc/services'
7-
import { ApiEndpoints, BrowserStorageItem } from 'uiSrc/constants'
7+
import { ApiEndpoints, BrowserStorageItem, CustomErrorCodes } from 'uiSrc/constants'
88
import { setAppContextInitialState } from 'uiSrc/slices/app/context'
99
import successMessages from 'uiSrc/components/notifications/success-messages'
1010
import { checkRediStack, getApiErrorMessage, isStatusSuccessful, Nullable } from 'uiSrc/utils'
@@ -362,8 +362,20 @@ export function createInstanceStandaloneAction(
362362
onSuccess?.(data.id)
363363
}
364364
} catch (_error) {
365-
const error: AxiosError = _error
365+
const error = _error as AxiosError
366366
const errorMessage = getApiErrorMessage(error)
367+
const errorCode = get(error, 'response.data.errorCode', 0) as CustomErrorCodes
368+
369+
if (errorCode === CustomErrorCodes.DatabaseAlreadyExists) {
370+
const databaseId = get(error, 'response.data.resource.databaseId', '')
371+
372+
dispatch(autoCreateAndConnectToInstanceActionSuccess(
373+
databaseId,
374+
successMessages.DATABASE_ALREADY_EXISTS(),
375+
onSuccess,
376+
))
377+
return
378+
}
367379

368380
dispatch(defaultInstanceChangingFailure(errorMessage))
369381

@@ -389,23 +401,50 @@ export function autoCreateAndConnectToInstanceAction(
389401
const { status, data } = await apiService.post(`${ApiEndpoints.DATABASES}`, payload)
390402

391403
if (isStatusSuccessful(status)) {
392-
dispatch(setAppContextInitialState())
393-
dispatch(setConnectedInstanceId(data?.id ?? ''))
394-
395-
dispatch(checkConnectToInstanceAction(data.id, (id) => {
396-
setTimeout(() => {
397-
dispatch(removeInfiniteNotification(InfiniteMessagesIds.autoCreateDb))
398-
onSuccess?.(id)
399-
}, HIDE_CREATING_DB_DELAY_MS)
400-
}))
404+
dispatch(autoCreateAndConnectToInstanceActionSuccess(
405+
data?.id,
406+
successMessages.ADDED_NEW_INSTANCE(data?.name),
407+
onSuccess,
408+
))
401409
}
402410
} catch (error) {
411+
const errorCode = get(error, 'response.data.errorCode', 0) as CustomErrorCodes
412+
413+
if (errorCode === CustomErrorCodes.DatabaseAlreadyExists) {
414+
const databaseId = get(error, 'response.data.resource.databaseId', '')
415+
416+
dispatch(autoCreateAndConnectToInstanceActionSuccess(
417+
databaseId,
418+
successMessages.DATABASE_ALREADY_EXISTS(),
419+
onSuccess,
420+
))
421+
return
422+
}
403423
dispatch(addErrorNotification(error as AxiosError))
404424
dispatch(removeInfiniteNotification(InfiniteMessagesIds.autoCreateDb))
405425
}
406426
}
407427
}
408428

429+
function autoCreateAndConnectToInstanceActionSuccess(
430+
id: string,
431+
message: any,
432+
onSuccess?: (id: string) => void,
433+
) {
434+
return async (dispatch: AppDispatch) => {
435+
dispatch(setAppContextInitialState())
436+
dispatch(setConnectedInstanceId(id ?? ''))
437+
438+
dispatch(checkConnectToInstanceAction(id, (id) => {
439+
setTimeout(() => {
440+
dispatch(removeInfiniteNotification(InfiniteMessagesIds.autoCreateDb))
441+
dispatch(addMessageNotification(message))
442+
onSuccess?.(id)
443+
}, HIDE_CREATING_DB_DELAY_MS)
444+
}))
445+
}
446+
}
447+
409448
// Asynchronous thunk action
410449
export function updateInstanceAction({ id, ...payload }: Instance, onSuccess?: () => void) {
411450
return async (dispatch: AppDispatch) => {

0 commit comments

Comments
 (0)