diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 240c7771a1..87ea32a37f 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -19,7 +19,7 @@ display: flex; flex-direction: column; } - + .sbdocs-wrapper div:has(>div>.toc-wrapper){ width:14rem; } diff --git a/redisinsight/ui/src/components/auto-discover/EmptyState.tsx b/redisinsight/ui/src/components/auto-discover/EmptyState.tsx new file mode 100644 index 0000000000..0ed72d4310 --- /dev/null +++ b/redisinsight/ui/src/components/auto-discover/EmptyState.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { Col, FlexItem } from 'uiSrc/components/base/layout/flex' +import { Text } from 'uiSrc/components/base/text' + +import type { EmptyStateProps } from './EmptyState.types' + +export const EmptyState = ({ message }: EmptyStateProps) => ( + + + {message} + + +) + diff --git a/redisinsight/ui/src/components/auto-discover/EmptyState.types.ts b/redisinsight/ui/src/components/auto-discover/EmptyState.types.ts new file mode 100644 index 0000000000..3b2cad4393 --- /dev/null +++ b/redisinsight/ui/src/components/auto-discover/EmptyState.types.ts @@ -0,0 +1,4 @@ +export interface EmptyStateProps { + message: string +} + diff --git a/redisinsight/ui/src/components/auto-discover/index.ts b/redisinsight/ui/src/components/auto-discover/index.ts index 3618342256..fc2df97dfb 100644 --- a/redisinsight/ui/src/components/auto-discover/index.ts +++ b/redisinsight/ui/src/components/auto-discover/index.ts @@ -1,110 +1,5 @@ -import React from 'react' -import styled from 'styled-components' -import { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox' -import { Text, Title } from 'uiSrc/components/base/text' -import { Theme } from 'uiSrc/components/base/theme/types' -import { Col, FlexItem } from 'uiSrc/components/base/layout/flex' -import { FormField } from 'uiSrc/components/base/forms/FormField' -import { IconButton } from 'uiSrc/components/base/forms/buttons' -import { CopyIcon } from 'uiSrc/components/base/icons' +export * from './styles' -export const PageTitle = styled(Title).attrs({ - size: 'L', -})` - padding-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space050}; -` -export const PageSubTitle = styled(Text).attrs({ - size: 'S', - component: 'span', -})` - padding-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space100}; -` -export const SearchContainer = styled(FlexItem)` - max-width: 100%; - padding-top: ${({ theme }: { theme: Theme }) => theme.core.space.space150}; -` -export const SearchForm = styled(FormField)` - width: 266px; -` -export const Footer = styled(FlexItem).attrs<{ - grow?: boolean | number - padding?: React.ComponentProps['padding'] -}>(({ grow, padding }) => ({ - grow: grow ?? false, - padding: padding ?? 6, -}))` - border-top: 1px solid - ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral400}; -` - -export const DatabaseContainer = styled(Col)` - position: relative; - padding: ${({ theme }: { theme: Theme }) => - `${theme.core.space.space250} ${theme.core.space.space200} 0 ${theme.core.space.space200}`}; - @media only screen and (min-width: 768px) { - padding: ${({ theme }: { theme: Theme }) => - `${theme.core.space.space400} ${theme.core.space.space200} 0 ${theme.core.space.space400}`}; - max-width: calc(100vw - 95px); - } -` - -export const DatabaseWrapper = styled.div` - height: auto; - scrollbar-width: thin; - //overflow: auto; - padding: 1px 1px 75px; - position: relative; - background-color: ${({ theme }: { theme: Theme }) => - theme.semantic.color.background.neutral100}; - flex-grow: 1; - overflow: hidden; -` -export const SelectAllCheckbox = styled(Checkbox)` - & svg { - margin: 0 !important; - } -` -export const CellText = styled(Text).attrs({ - size: 'M', - component: 'span', -})` - max-width: 100%; - display: inline-block; - width: auto; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -` - -export const CopyPublicEndpointText = styled(CellText)` - vertical-align: top; -` - -export const StatusColumnText = styled(CellText)` - text-transform: capitalize; -` -export const CopyBtn = styled(IconButton).attrs({ - icon: CopyIcon, - size: 'L', -})` - margin-left: 15px; - opacity: 0; - height: 0; - transition: opacity 0.25s ease-in-out; -` - -export const CopyTextContainer = styled.div` - height: 24px; - line-height: 24px; - width: auto; - max-width: 100%; - padding-right: 34px; - position: relative; - * { - } - - &:hover ${CopyBtn} { - opacity: 1; - height: auto; - } -` +export { Header } from './Header' +export { EmptyState } from './EmptyState' +export type { EmptyStateProps } from './EmptyState.types' diff --git a/redisinsight/ui/src/components/auto-discover/styles.ts b/redisinsight/ui/src/components/auto-discover/styles.ts new file mode 100644 index 0000000000..56733bc1f8 --- /dev/null +++ b/redisinsight/ui/src/components/auto-discover/styles.ts @@ -0,0 +1,107 @@ +import styled from 'styled-components' +import { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox' +import { Text, Title } from 'uiSrc/components/base/text' +import { Theme } from 'uiSrc/components/base/theme/types' +import { Col, FlexItem } from 'uiSrc/components/base/layout/flex' +import { FormField } from 'uiSrc/components/base/forms/FormField' +import { IconButton } from 'uiSrc/components/base/forms/buttons' +import { CopyIcon } from 'uiSrc/components/base/icons' + +export const PageTitle = styled(Title).attrs({ + size: 'L', +})` + padding-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space050}; +` +export const PageSubTitle = styled(Text).attrs({ + size: 'S', + component: 'span', +})` + padding-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space100}; +` +export const SearchContainer = styled(FlexItem)` + max-width: 100%; + padding-top: ${({ theme }: { theme: Theme }) => theme.core.space.space150}; +` +export const SearchForm = styled(FormField)` + width: 266px; +` +export const Footer = styled(FlexItem).attrs<{ + grow?: boolean | number + padding?: React.ComponentProps['padding'] +}>(({ grow, padding }) => ({ + grow: grow ?? false, + padding: padding ?? 6, +}))` + border-top: 1px solid + ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral400}; +` + +export const DatabaseContainer = styled(Col)` + position: relative; + padding: ${({ theme }: { theme: Theme }) => + `${theme.core.space.space250} ${theme.core.space.space200} 0 ${theme.core.space.space200}`}; + @media only screen and (min-width: 768px) { + padding: ${({ theme }: { theme: Theme }) => + `${theme.core.space.space400} ${theme.core.space.space200} 0 ${theme.core.space.space400}`}; + max-width: calc(100vw - 95px); + } +` + +export const DatabaseWrapper = styled.div` + height: auto; + scrollbar-width: thin; + padding: ${({ theme }: { theme: Theme }) => theme.core.space.space010}; + position: relative; + background-color: ${({ theme }: { theme: Theme }) => + theme.semantic.color.background.neutral100}; + overflow: hidden; +` +export const SelectAllCheckbox = styled(Checkbox)` + & svg { + margin: 0 !important; + } +` +export const CellText = styled(Text).attrs({ + size: 'M', + component: 'span', +})` + max-width: 100%; + display: inline-block; + width: auto; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +` + +export const CopyPublicEndpointText = styled(CellText)` + vertical-align: top; +` + +export const StatusColumnText = styled(CellText)` + text-transform: capitalize; +` +export const CopyBtn = styled(IconButton).attrs({ + icon: CopyIcon, + size: 'L', +})` + margin-left: 15px; + opacity: 0; + height: 0; + transition: opacity 0.25s ease-in-out; +` + +export const CopyTextContainer = styled.div` + height: 24px; + line-height: 24px; + width: auto; + max-width: 100%; + padding-right: 34px; + position: relative; + * { + } + + &:hover ${CopyBtn} { + opacity: 1; + height: auto; + } +` diff --git a/redisinsight/ui/src/components/base/display/loader/Loader.tsx b/redisinsight/ui/src/components/base/display/loader/Loader.tsx index 05062edcf0..34062774a5 100644 --- a/redisinsight/ui/src/components/base/display/loader/Loader.tsx +++ b/redisinsight/ui/src/components/base/display/loader/Loader.tsx @@ -1,13 +1,11 @@ import React, { ComponentProps } from 'react' - import { Loader as RedisLoader } from '@redis-ui/components' -import { useTheme, theme } from '@redis-ui/styles' - -type Space = typeof theme.core.space +import { useTheme } from '@redis-ui/styles' +import { Theme } from 'uiSrc/components/base/theme/types' export type RedisLoaderProps = ComponentProps -const convertSizeToPx = (tShirtSize: string, space: Space) => { +const convertSizeToPx = (tShirtSize: string, space: Theme['core']['space']) => { switch (tShirtSize.toLowerCase()) { case 's': return space.space050 diff --git a/redisinsight/ui/src/components/base/layout/list/list.styles.ts b/redisinsight/ui/src/components/base/layout/list/list.styles.ts index 92fd2fb069..fc0e63d7d7 100644 --- a/redisinsight/ui/src/components/base/layout/list/list.styles.ts +++ b/redisinsight/ui/src/components/base/layout/list/list.styles.ts @@ -12,6 +12,7 @@ import { import { AllIconsType } from 'uiSrc/components/base/icons/RiIcon' import { IconProps } from 'uiSrc/components/base/icons' +import { Theme } from 'uiSrc/components/base/theme/types' export const ListClassNames = { listItem: 'RI-list-group-item', @@ -25,9 +26,6 @@ export const ListClassNames = { export const MAX_FORM_WIDTH = 400 -export const GAP_SIZES = ['none', 's', 'm'] as const -export type ListGroupGapSize = (typeof GAP_SIZES)[number] - export type ListGroupProps = HTMLAttributes & { className?: string /** @@ -40,7 +38,7 @@ export type ListGroupProps = HTMLAttributes & { * Spacing between list items * @default s */ - gap?: ListGroupGapSize + gap?: keyof typeof listStyles.gap /** * Sets the max-width of the page. @@ -55,14 +53,12 @@ export type ListGroupProps = HTMLAttributes & { export const listStyles = { gap: { - none: css` - gap: 0; - `, + none: 'gap: 0;', s: css` - gap: var(--gap-s); + gap: ${({ theme }: { theme: Theme }) => theme.core.space.space100}; `, m: css` - gap: var(--gap-m); + gap: ${({ theme }: { theme: Theme }) => theme.core.space.space150}; `, }, flush: css` @@ -78,7 +74,7 @@ export const listStyles = { export const StyledGroup = styled.ul< Omit & { - $gap?: ListGroupGapSize + $gap?: keyof typeof listStyles.gap $flush?: boolean } >` diff --git a/redisinsight/ui/src/components/base/tooltip/HoverContent.tsx b/redisinsight/ui/src/components/base/tooltip/HoverContent.tsx index a722cd5833..014e1506a0 100644 --- a/redisinsight/ui/src/components/base/tooltip/HoverContent.tsx +++ b/redisinsight/ui/src/components/base/tooltip/HoverContent.tsx @@ -8,9 +8,11 @@ interface RiTooltipContentProps { content: React.ReactNode } -export const HoverContent = ({ title, content }: RiTooltipContentProps) => ( - - {title && {title}} - {content} - -) +export const HoverContent = ({ title, content }: RiTooltipContentProps) => { + return ( + + {typeof title === 'string' ? {title} : title} + {content} + + ) +} diff --git a/redisinsight/ui/src/components/base/tooltip/RiTooltip.spec.tsx b/redisinsight/ui/src/components/base/tooltip/RiTooltip.spec.tsx index 75c4b9bc76..a15c101c00 100644 --- a/redisinsight/ui/src/components/base/tooltip/RiTooltip.spec.tsx +++ b/redisinsight/ui/src/components/base/tooltip/RiTooltip.spec.tsx @@ -2,6 +2,7 @@ import React from 'react' import { fireEvent, screen, act } from '@testing-library/react' import { render, waitForRiTooltipVisible } from 'uiSrc/utils/test-utils' import { RiTooltip, RiTooltipProps } from './RITooltip' +import { HoverContent } from './HoverContent' const TestButton = () => ( + , + ) - fireEvent.click(screen.getByTestId(CLOSE_BUTTON)) - expect(screen.queryByTestId(CLOSE_BUTTON)).toBeNull() + expect(getByTestId('complex-content')).toBeInTheDocument() + expect(getByText('Title')).toBeInTheDocument() + expect(getByText('Description text')).toBeInTheDocument() + expect(getByText('Action')).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/components/message-bar/MessageBar.tsx b/redisinsight/ui/src/components/message-bar/MessageBar.tsx index 4244eb4d9c..8064db5bac 100644 --- a/redisinsight/ui/src/components/message-bar/MessageBar.tsx +++ b/redisinsight/ui/src/components/message-bar/MessageBar.tsx @@ -1,39 +1,41 @@ -import React, { useEffect, useState } from 'react' - -import { FlexItem } from 'uiSrc/components/base/layout/flex' -import { CancelSlimIcon } from 'uiSrc/components/base/icons' -import { IconButton } from 'uiSrc/components/base/forms/buttons' -import styles from './styles.module.scss' -import { Container, ContainerWrapper } from './MessageBar.styles' +import React, { useEffect } from 'react' +import { riToast, RiToaster } from 'uiSrc/components/base/display/toast' +import { ONE_HOUR } from 'uiSrc/components/notifications/constants' export interface Props { children?: React.ReactElement opened: boolean + variant?: typeof riToast.Variant.Success | typeof riToast.Variant.Attention } -const MessageBar = ({ children, opened }: Props) => { - const [isOpen, setIsOpen] = useState(false) +export const MessageBar = ({ + children, + opened, + variant = riToast.Variant.Success, +}: Props) => { useEffect(() => { - setIsOpen(opened) - }, [opened]) + if (!opened) { + return + } + + riToast( + { + message: children, + }, + { + variant, + containerId: 'autodiscovery-message-bar', + }, + ) + }, [opened, variant]) - if (!isOpen) { - return null - } return ( - - - {children} - - setIsOpen(false)} - data-testid="close-button" - /> - - - + ) } diff --git a/redisinsight/ui/src/mocks/factories/cloud/RedisCloudAccount.factory.ts b/redisinsight/ui/src/mocks/factories/cloud/RedisCloudAccount.factory.ts new file mode 100644 index 0000000000..20a2615b8d --- /dev/null +++ b/redisinsight/ui/src/mocks/factories/cloud/RedisCloudAccount.factory.ts @@ -0,0 +1,14 @@ +import { Factory } from 'fishery' +import { faker } from '@faker-js/faker' +import { RedisCloudAccount } from 'uiSrc/slices/interfaces' + +export const RedisCloudAccountFactory = Factory.define( + () => ({ + accountId: faker.number.int({ min: 100000, max: 999999 }), + accountName: faker.person.fullName(), + ownerName: + faker.helpers.maybe(() => faker.company.name(), { probability: 0.7 }) ?? + null, + ownerEmail: faker.internet.email(), + }), +) diff --git a/redisinsight/ui/src/mocks/factories/cloud/RedisCloudInstance.factory.ts b/redisinsight/ui/src/mocks/factories/cloud/RedisCloudInstance.factory.ts new file mode 100644 index 0000000000..3a8f0a85cd --- /dev/null +++ b/redisinsight/ui/src/mocks/factories/cloud/RedisCloudInstance.factory.ts @@ -0,0 +1,200 @@ +import { Factory } from 'fishery' +import { faker } from '@faker-js/faker' +import { + InstanceRedisCloud, + InstanceRedisClusterStatus, + AddRedisDatabaseStatus, + RedisCloudSubscriptionType, + RedisDefaultModules, + AddRedisClusterDatabaseOptions, +} from 'uiSrc/slices/interfaces' + +export const RedisCloudInstanceFactory = Factory.define( + () => { + const host = `${faker.word.noun()}-${faker.number.int({ min: 1000, max: 99999 })}.cloud.redis.example.com` + const port = faker.number.int({ min: 10000, max: 65535 }) + const uid = faker.number.int({ min: 1000, max: 999999 }) + const databaseId = faker.number.int({ min: 1, max: 999999 }) + const subscriptionId = faker.number.int({ min: 100000, max: 99999999 }) + const subscriptionName = `${faker.word.noun()}-subscription-${faker.number.int({ min: 100, max: 999 })}` + + // Pick a random unique subset of modules + const allModules = Object.values(RedisDefaultModules) + const randomModules = faker.helpers.arrayElements( + allModules, + faker.number.int({ min: 0, max: allModules.length }), + ) as RedisDefaultModules[] + const modules = Array.from(randomModules) as RedisDefaultModules[] + + return { + accessKey: faker.string.alphanumeric(16), + secretKey: faker.string.alphanumeric(24), + credentials: null, + account: null, + host, + port, + uid, + name: `${faker.word.noun()}-${faker.number.int({ min: 1000, max: 99999 })}`, + id: faker.number.int({ min: 1, max: 999999 }), + dnsName: host, + address: `${host}:${port}`, + status: faker.helpers.arrayElement([ + InstanceRedisClusterStatus.Active, + InstanceRedisClusterStatus.Pending, + InstanceRedisClusterStatus.ImportPending, + ]), + modules, + tls: faker.datatype.boolean(), + options: (() => { + const persistenceChoices = [ + 'aof-every-1-second', + 'aof-every-write', + 'snapshot-every-1-hour', + 'snapshot-every-6-hours', + 'snapshot-every-12-hours', + 'none', + ] as const + + const generators: Record any> = { + [AddRedisClusterDatabaseOptions.ActiveActive]: () => + faker.datatype.boolean(), + [AddRedisClusterDatabaseOptions.Backup]: () => + faker.helpers.arrayElement([ + 'snapshot-every-1-hour', + 'snapshot-every-6-hours', + 'snapshot-every-12-hours', + true, + false, + ]), + [AddRedisClusterDatabaseOptions.Clustering]: () => + faker.datatype.boolean(), + // Present in instanceMock examples; not in enum mapping used by UI renderer, but safe to include + enabledDataPersistence: () => faker.datatype.boolean(), + [AddRedisClusterDatabaseOptions.PersistencePolicy]: () => + faker.helpers.arrayElement([ + ...persistenceChoices, + ] as unknown as string[]), + [AddRedisClusterDatabaseOptions.Flash]: () => + faker.datatype.boolean(), + [AddRedisClusterDatabaseOptions.Replication]: () => + faker.datatype.boolean(), + [AddRedisClusterDatabaseOptions.ReplicaDestination]: () => + faker.datatype.boolean(), + [AddRedisClusterDatabaseOptions.ReplicaSource]: () => + faker.datatype.boolean(), + } + + const keys = Object.keys(generators) + const subsetSize = faker.number.int({ min: 0, max: keys.length }) + const selected = faker.helpers.arrayElements(keys, subsetSize) + + return selected.reduce((acc: any, key: string) => { + acc[key] = generators[key]() + return acc + }, {}) + })(), + message: undefined, + publicEndpoint: `${host}:${port}`, + databaseId, + databaseIdAdded: undefined, + subscriptionId, + subscriptionType: faker.helpers.enumValue(RedisCloudSubscriptionType), + subscriptionName, + subscriptionIdAdded: undefined, + statusAdded: faker.helpers.enumValue(AddRedisDatabaseStatus), + messageAdded: + faker.helpers.maybe(() => faker.lorem.sentence()) ?? undefined, + databaseDetails: undefined, + free: faker.datatype.boolean(), + } + }, +) + +// Predictable variants ("traits") for stories/tests +export const RedisCloudInstanceFactorySuccess = + RedisCloudInstanceFactory.params({ + statusAdded: AddRedisDatabaseStatus.Success, + messageAdded: 'Added successfully', + }) + +export const RedisCloudInstanceFactoryFail = RedisCloudInstanceFactory.params({ + statusAdded: AddRedisDatabaseStatus.Fail, + messageAdded: 'Failed to add database', +}) + +export const RedisCloudInstanceFactoryFixed = RedisCloudInstanceFactory.params({ + subscriptionType: RedisCloudSubscriptionType.Fixed, +}) + +export const RedisCloudInstanceFactoryFlexible = + RedisCloudInstanceFactory.params({ + subscriptionType: RedisCloudSubscriptionType.Flexible, + }) + +export const RedisCloudInstanceFactoryActive = RedisCloudInstanceFactory.params( + { status: InstanceRedisClusterStatus.Active }, +) + +export const RedisCloudInstanceFactoryPending = + RedisCloudInstanceFactory.params({ + status: InstanceRedisClusterStatus.Pending, + }) + +export const RedisCloudInstanceFactoryWithoutModules = + RedisCloudInstanceFactory.params({ modules: [] }) + +export const RedisCloudInstanceFactoryWithModules = ( + modules: RedisDefaultModules[], +) => RedisCloudInstanceFactory.params({ modules }) + +export const RedisCloudInstanceFactoryFree = RedisCloudInstanceFactory.params({ + free: true, +}) + +export const RedisCloudInstanceFactoryPaid = RedisCloudInstanceFactory.params({ + free: false, +}) + +// Option-focused traits +export const RedisCloudInstanceFactoryOptionsNone = + RedisCloudInstanceFactory.params({ + options: { + [AddRedisClusterDatabaseOptions.ActiveActive]: false, + [AddRedisClusterDatabaseOptions.Backup]: false, + [AddRedisClusterDatabaseOptions.Clustering]: false, + enabledDataPersistence: false, + [AddRedisClusterDatabaseOptions.PersistencePolicy]: 'none', + [AddRedisClusterDatabaseOptions.Flash]: false, + [AddRedisClusterDatabaseOptions.Replication]: false, + [AddRedisClusterDatabaseOptions.ReplicaDestination]: false, + [AddRedisClusterDatabaseOptions.ReplicaSource]: false, + }, + }) + +export const RedisCloudInstanceFactoryOptionsFull = + RedisCloudInstanceFactory.params({ + options: { + [AddRedisClusterDatabaseOptions.ActiveActive]: true, + // Use a concrete backup schedule value similar to instanceMock examples + [AddRedisClusterDatabaseOptions.Backup]: 'snapshot-every-12-hours', + [AddRedisClusterDatabaseOptions.Clustering]: true, + enabledDataPersistence: true, + [AddRedisClusterDatabaseOptions.PersistencePolicy]: 'aof-every-1-second', + [AddRedisClusterDatabaseOptions.Flash]: true, + [AddRedisClusterDatabaseOptions.Replication]: true, + [AddRedisClusterDatabaseOptions.ReplicaDestination]: false, + [AddRedisClusterDatabaseOptions.ReplicaSource]: false, + }, + }) + +export const RedisCloudInstanceFactoryOptionsBackupSchedule = ( + schedule: + | 'snapshot-every-1-hour' + | 'snapshot-every-6-hours' + | 'snapshot-every-12-hours', +) => + RedisCloudInstanceFactory.params({ + options: { + [AddRedisClusterDatabaseOptions.Backup]: schedule, + }, + }) diff --git a/redisinsight/ui/src/mocks/factories/cloud/RedisCloudSubscription.factory.ts b/redisinsight/ui/src/mocks/factories/cloud/RedisCloudSubscription.factory.ts new file mode 100644 index 0000000000..491b60b916 --- /dev/null +++ b/redisinsight/ui/src/mocks/factories/cloud/RedisCloudSubscription.factory.ts @@ -0,0 +1,32 @@ +import { Factory } from 'fishery' +import { faker } from '@faker-js/faker' +import { + RedisCloudSubscription, + RedisCloudSubscriptionStatus, + RedisCloudSubscriptionType, +} from 'uiSrc/slices/interfaces' + +const PROVIDERS = ['aws', 'google', 'azure'] as const +const REGIONS = [ + 'us-east-1', + 'us-west-2', + 'eu-west-1', + 'eu-central-1', + 'ap-southeast-1', +] as const + +export const RedisCloudSubscriptionFactory = Factory.define(() => { + const region = faker.helpers.arrayElement([...REGIONS]) + const provider = faker.helpers.arrayElement([...PROVIDERS]) + + return { + id: faker.number.int({ min: 100000, max: 99999999 }), + name: `${faker.word.noun()}-${faker.number.int({ min: 1000, max: 99999 })}.${region}.cloud`, + type: faker.helpers.enumValue(RedisCloudSubscriptionType), + numberOfDatabases: faker.number.int({ min: 0, max: 20 }), + provider, + region, + status: faker.helpers.enumValue(RedisCloudSubscriptionStatus), + free: faker.datatype.boolean(), + } +}) diff --git a/redisinsight/ui/src/mocks/factories/cluster/RedisClusterInstance.factory.ts b/redisinsight/ui/src/mocks/factories/cluster/RedisClusterInstance.factory.ts new file mode 100644 index 0000000000..691b4bd86b --- /dev/null +++ b/redisinsight/ui/src/mocks/factories/cluster/RedisClusterInstance.factory.ts @@ -0,0 +1,67 @@ +import { Factory } from 'fishery' +import { faker } from '@faker-js/faker' +import { + AddRedisDatabaseStatus, + InstanceRedisCluster, + InstanceRedisClusterStatus, + RedisDefaultModules, +} from 'uiSrc/slices/interfaces' + +export const RedisClusterInstanceFactory = Factory.define( + () => { + const host = faker.internet.ip() + const port = faker.number.int({ min: 6379, max: 65535 }) + const uid = faker.number.int({ min: 1, max: 999999 }) + const dnsName = `redis-${faker.number.int({ min: 1000, max: 99999 })}.cluster.local` + + // Pick a random unique subset of modules + const allModules = Object.values(RedisDefaultModules) + const randomModules = faker.helpers.arrayElements( + allModules, + faker.number.int({ min: 0, max: Math.min(3, allModules.length) }), + ) as RedisDefaultModules[] + const modules = Array.from(new Set(randomModules)) as RedisDefaultModules[] + + return { + host, + port, + uid, + name: `redis-db-${faker.number.int({ min: 1, max: 999 })}`, + id: faker.number.int({ min: 1, max: 999999 }), + dnsName, + address: `${host}:${port}`, + status: faker.helpers.arrayElement([ + InstanceRedisClusterStatus.Active, + InstanceRedisClusterStatus.Pending, + InstanceRedisClusterStatus.CreationFailed, + ]), + modules, + tls: faker.datatype.boolean(), + options: {}, + message: faker.datatype.boolean() ? faker.lorem.sentence() : undefined, + uidAdded: undefined, + statusAdded: undefined, + messageAdded: undefined, + databaseDetails: undefined, + } + }, +) + +export const RedisClusterInstanceAddedFactory = RedisClusterInstanceFactory.afterBuild( + (instance) => { + const statusAdded = faker.helpers.arrayElement([ + AddRedisDatabaseStatus.Success, + AddRedisDatabaseStatus.Fail, + ]) + + return { + ...instance, + uidAdded: faker.number.int({ min: 1, max: 999999 }), + statusAdded, + messageAdded: + statusAdded === AddRedisDatabaseStatus.Success + ? 'Successfully added' + : faker.lorem.sentence(), + } + }, +) diff --git a/redisinsight/ui/src/mocks/factories/sentinel/SentinelMaster.factory.ts b/redisinsight/ui/src/mocks/factories/sentinel/SentinelMaster.factory.ts new file mode 100644 index 0000000000..221bb60f23 --- /dev/null +++ b/redisinsight/ui/src/mocks/factories/sentinel/SentinelMaster.factory.ts @@ -0,0 +1,32 @@ +import { Factory } from 'fishery' +import { faker } from '@faker-js/faker' +import { + AddRedisDatabaseStatus, + ModifiedSentinelMaster, +} from 'uiSrc/slices/interfaces' + +export const SentinelMasterFactory = Factory.define( + () => { + const name = `mymaster${faker.number.int({ min: 1, max: 999 })}` + const host = faker.internet.ip() + const port = faker.number.int({ min: 6379, max: 65535 }).toString() + + return { + id: faker.string.uuid(), + name, + alias: name, + host, + port, + username: faker.datatype.boolean() ? faker.internet.userName() : '', + password: faker.datatype.boolean() ? faker.internet.password() : '', + db: faker.number.int({ min: 0, max: 15 }), + numberOfSlaves: faker.number.int({ min: 0, max: 5 }), + status: faker.helpers.arrayElement([ + AddRedisDatabaseStatus.Success, + AddRedisDatabaseStatus.Fail, + ]), + message: faker.datatype.boolean() ? faker.lorem.sentence() : '', + loading: false, + } + }, +) diff --git a/redisinsight/ui/src/packages/redisearch/src/components/TableResult/TableResult.tsx b/redisinsight/ui/src/packages/redisearch/src/components/TableResult/TableResult.tsx index 1fc1c55a64..d6e4339f45 100644 --- a/redisinsight/ui/src/packages/redisearch/src/components/TableResult/TableResult.tsx +++ b/redisinsight/ui/src/packages/redisearch/src/components/TableResult/TableResult.tsx @@ -4,7 +4,8 @@ import cx from 'classnames' import { flatten, isArray, isEmpty, map, uniq } from 'lodash' import styled from 'styled-components' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' +import { handleCopy as handleCopyUtil } from 'uiSrc/utils' +import { Table, ColumnDef } from 'uiSrc/components/base/layout/table' import { ColorText } from 'uiSrc/components/base/text/ColorText' import { IconButton } from 'uiSrc/components/base/forms/buttons' import { CopyIcon } from 'uiSrc/components/base/icons' @@ -36,7 +37,7 @@ const noResultsMessage = 'No results found.' const TableResult = React.memo((props: Props) => { const { result, query, matched, cursorId } = props - const [columns, setColumns] = useState[]>([]) + const [columns, setColumns] = useState[]>([]) const checkShouldParsedHTML = (query: string) => { const command = query.toUpperCase() @@ -50,7 +51,7 @@ const TableResult = React.memo((props: Props) => { event.preventDefault() event.stopPropagation() - navigator.clipboard.writeText(text) + handleCopyUtil(text) } useEffect(() => { @@ -62,7 +63,7 @@ const TableResult = React.memo((props: Props) => { const uniqColumns = uniq(flatten(map(result, (doc) => Object.keys(doc)))) ?? [] - const newColumns: ColumnDefinition[] = uniqColumns.map( + const newColumns: ColumnDef[] = uniqColumns.map( (title: string = ' ') => ({ header: title, id: title, diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/alert.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/alert.tsx new file mode 100644 index 0000000000..825d46642a --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/alert.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces' + +import { AlertCell } from '../components/AlertCell/AlertCell' + +export const ALERT_COLUMN_ID = 'alert' as const + +export const alertColumn = (): ColumnDef => { + return { + id: ALERT_COLUMN_ID, + accessorKey: ALERT_COLUMN_ID, + header: '', + enableResizing: false, + enableSorting: false, + size: 50, + cell: ({ + row: { + original: { status, numberOfDatabases }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/database.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/database.tsx new file mode 100644 index 0000000000..51ece7c18f --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/database.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { DatabaseCell } from '../components/DatabaseCell/DatabaseCell' + +export const DATABASE_COLUMN_ID = 'name' as const + +export const databaseColumn = (): ColumnDef => { + return { + header: 'Database', + id: DATABASE_COLUMN_ID, + accessorKey: DATABASE_COLUMN_ID, + enableSorting: true, + maxSize: 150, + cell: ({ + row: { + original: { name }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/databaseResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/databaseResult.tsx new file mode 100644 index 0000000000..647ff6c896 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/databaseResult.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { DatabaseCell } from '../components/DatabaseCell/DatabaseCell' + +export const DATABASE_RESULT_COLUMN_ID = 'name' as const + +export const databaseResultColumn = (): ColumnDef => { + return { + header: 'Database', + id: DATABASE_RESULT_COLUMN_ID, + accessorKey: DATABASE_RESULT_COLUMN_ID, + enableSorting: true, + maxSize: 120, + cell: ({ + row: { + original: { name }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/endpoint.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/endpoint.tsx new file mode 100644 index 0000000000..e619d0d7a7 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/endpoint.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { EndpointCell } from '../components/EndpointCell/EndpointCell' + +export const ENDPOINT_COLUMN_ID = 'publicEndpoint' as const + +export const endpointColumn = (): ColumnDef => { + return { + header: 'Endpoint', + id: ENDPOINT_COLUMN_ID, + accessorKey: ENDPOINT_COLUMN_ID, + enableSorting: true, + minSize: 200, + cell: ({ + row: { + original: { publicEndpoint }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/endpointResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/endpointResult.tsx new file mode 100644 index 0000000000..49fa65b2ce --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/endpointResult.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { EndpointCell } from '../components/EndpointCell/EndpointCell' + +export const ENDPOINT_RESULT_COLUMN_ID = 'publicEndpoint' as const + +export const endpointResultColumn = (): ColumnDef => { + return { + header: 'Endpoint', + id: ENDPOINT_RESULT_COLUMN_ID, + accessorKey: ENDPOINT_RESULT_COLUMN_ID, + enableSorting: true, + minSize: 250, + maxSize: 310, + cell: ({ + row: { + original: { publicEndpoint }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/id.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/id.tsx new file mode 100644 index 0000000000..3fa5158c22 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/id.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces' + +import { CellText } from 'uiSrc/components/auto-discover' + +export const ID_COLUMN_ID = 'id' as const + +export const idColumn = (): ColumnDef => { + return { + id: ID_COLUMN_ID, + accessorKey: ID_COLUMN_ID, + header: 'Id', + enableSorting: true, + size: 80, + cell: ({ + row: { + original: { id }, + }, + }) => {id}, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/messageResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/messageResult.tsx new file mode 100644 index 0000000000..80a4a50699 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/messageResult.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { MessageResultCell } from '../components/MessageResultCell/MessageResultCell' + +export const MESSAGE_RESULT_COLUMN_ID = 'messageAdded' as const + +export const messageResultColumn = (): ColumnDef => { + return { + header: 'Result', + id: MESSAGE_RESULT_COLUMN_ID, + accessorKey: MESSAGE_RESULT_COLUMN_ID, + enableSorting: true, + minSize: 110, + cell: ({ + row: { + original: { statusAdded, messageAdded }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/modules.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/modules.tsx new file mode 100644 index 0000000000..04876c6e75 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/modules.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { DatabaseListModules } from 'uiSrc/components' + +export const MODULES_COLUMN_ID = 'modules' as const + +export const modulesColumn = (): ColumnDef => { + return { + header: 'Capabilities', + id: MODULES_COLUMN_ID, + accessorKey: MODULES_COLUMN_ID, + enableSorting: true, + maxSize: 120, + cell: function Modules({ row: { original: instance } }) { + return ( + ({ name }))} + /> + ) + }, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/modulesResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/modulesResult.tsx new file mode 100644 index 0000000000..bcd1aae426 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/modulesResult.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { DatabaseListModules } from 'uiSrc/components' + +export const MODULES_RESULT_COLUMN_ID = 'modules' as const + +export const modulesResultColumn = (): ColumnDef => { + return { + header: 'Capabilities', + id: MODULES_RESULT_COLUMN_ID, + accessorKey: MODULES_RESULT_COLUMN_ID, + enableSorting: true, + maxSize: 150, + cell: function Modules({ row: { original: instance } }) { + return ( + ({ name }))} + /> + ) + }, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/numberOfDbs.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/numberOfDbs.tsx new file mode 100644 index 0000000000..99431541ca --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/numberOfDbs.tsx @@ -0,0 +1,27 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces' + +import { CellText } from 'uiSrc/components/auto-discover' +import { isNumber } from 'lodash' + +export const NUMBER_OF_DBS_COLUMN_ID = 'numberOfDatabases' as const + +export const numberOfDbsColumn = (): ColumnDef => { + return { + id: NUMBER_OF_DBS_COLUMN_ID, + accessorKey: NUMBER_OF_DBS_COLUMN_ID, + header: '# databases', + enableSorting: true, + cell: ({ + row: { + original: { numberOfDatabases }, + }, + }) => ( + + {isNumber(numberOfDatabases) ? numberOfDatabases : '-'} + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/options.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/options.tsx new file mode 100644 index 0000000000..da83fcc150 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/options.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { DatabaseListOptions } from 'uiSrc/components' +import { parseInstanceOptionsCloud } from 'uiSrc/utils' + +export const OPTIONS_COLUMN_ID = 'options' as const + +export const optionsColumn = ( + instances: InstanceRedisCloud[], +): ColumnDef => { + return { + header: 'Options', + id: OPTIONS_COLUMN_ID, + accessorKey: OPTIONS_COLUMN_ID, + enableSorting: true, + maxSize: 120, + cell: ({ row: { original: instance } }) => { + const options = parseInstanceOptionsCloud( + instance.databaseId, + instances || [], + ) + return + }, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/optionsResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/optionsResult.tsx new file mode 100644 index 0000000000..5cd18c71b0 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/optionsResult.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' +import { DatabaseListOptions } from 'uiSrc/components' +import { parseInstanceOptionsCloud } from 'uiSrc/utils' + +export const OPTIONS_RESULT_COLUMN_ID = 'options' as const + +export const optionsResultColumn = ( + instancesForOptions: InstanceRedisCloud[], +): ColumnDef => { + return { + header: 'Options', + id: OPTIONS_RESULT_COLUMN_ID, + accessorKey: OPTIONS_RESULT_COLUMN_ID, + enableSorting: true, + maxSize: 180, + cell: function Options({ row: { original: instance } }) { + const options = parseInstanceOptionsCloud( + instance.databaseId, + instancesForOptions, + ) + return + }, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/provider.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/provider.tsx new file mode 100644 index 0000000000..b8c0baebc9 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/provider.tsx @@ -0,0 +1,22 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces' + +import { CellText } from 'uiSrc/components/auto-discover' + +export const PROVIDER_COLUMN_ID = 'provider' as const + +export const providerColumn = (): ColumnDef => { + return { + id: PROVIDER_COLUMN_ID, + accessorKey: PROVIDER_COLUMN_ID, + header: 'Cloud provider', + enableSorting: true, + cell: ({ + row: { + original: { provider }, + }, + }) => {provider ?? '-'}, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/region.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/region.tsx new file mode 100644 index 0000000000..f8551d6c44 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/region.tsx @@ -0,0 +1,22 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces' + +import { CellText } from 'uiSrc/components/auto-discover' + +export const REGION_COLUMN_ID = 'region' as const + +export const regionColumn = (): ColumnDef => { + return { + id: REGION_COLUMN_ID, + accessorKey: REGION_COLUMN_ID, + header: 'Region', + enableSorting: true, + cell: ({ + row: { + original: { region }, + }, + }) => {region ?? '-'}, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/selection.ts b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/selection.ts new file mode 100644 index 0000000000..fe5577490e --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/selection.ts @@ -0,0 +1,6 @@ +import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces' +import { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils' + +export const selectionColumn = () => { + return getSelectionColumn() +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/status.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/status.tsx new file mode 100644 index 0000000000..64599dad1e --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/status.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { + type RedisCloudSubscription, + RedisCloudSubscriptionStatusText, +} from 'uiSrc/slices/interfaces' + +import { CellText } from 'uiSrc/components/auto-discover' + +export const STATUS_COLUMN_ID = 'status' as const + +export const statusColumn = (): ColumnDef => { + return { + id: STATUS_COLUMN_ID, + accessorKey: STATUS_COLUMN_ID, + header: 'Status', + enableSorting: true, + cell: ({ + row: { + original: { status }, + }, + }) => ( + {RedisCloudSubscriptionStatusText[status] ?? '-'} + ), + } +} +/* + { + header: 'Status', + id: 'status', + accessorKey: 'status', + enableSorting: true, + maxSize: 100, + cell: ({ + row: { + original: { status }, + }, + }) => {status}, + } + */ diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/statusDb.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/statusDb.tsx new file mode 100644 index 0000000000..45e176e981 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/statusDb.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { StatusColumnText } from 'uiSrc/components/auto-discover' + +export const STATUS_DB_COLUMN_ID = 'status' as const + +export const statusDbColumn = (): ColumnDef => { + return { + header: 'Status', + id: STATUS_DB_COLUMN_ID, + accessorKey: STATUS_DB_COLUMN_ID, + enableSorting: true, + maxSize: 100, + cell: ({ + row: { + original: { status }, + }, + }) => {status}, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/statusDbResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/statusDbResult.tsx new file mode 100644 index 0000000000..8d83fa1eeb --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/statusDbResult.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { CellText } from 'uiSrc/components/auto-discover' + +export const STATUS_DB_RESULT_COLUMN_ID = 'status' as const + +export const statusDbResultColumn = (): ColumnDef => { + return { + header: 'Status', + id: STATUS_DB_RESULT_COLUMN_ID, + accessorKey: STATUS_DB_RESULT_COLUMN_ID, + enableSorting: true, + size: 80, + cell: ({ + row: { + original: { status }, + }, + }) => {status}, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscription.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscription.tsx new file mode 100644 index 0000000000..758fb51419 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscription.tsx @@ -0,0 +1,22 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces' + +import { SubscriptionCell } from '../components/SubscriptionCell/SubscriptionCell' + +export const SUBSCRIPTION_COLUMN_ID = 'name' as const + +export const subscriptionColumn = (): ColumnDef => { + return { + id: SUBSCRIPTION_COLUMN_ID, + accessorKey: SUBSCRIPTION_COLUMN_ID, + header: 'Subscription', + enableSorting: true, + cell: ({ + row: { + original: { name }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionDb.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionDb.tsx new file mode 100644 index 0000000000..aa11d92430 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionDb.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { SubscriptionCell } from '../components/SubscriptionCell/SubscriptionCell' + +export const SUBSCRIPTION_DB_COLUMN_ID = 'subscriptionName' as const + +export const subscriptionDbColumn = (): ColumnDef => { + return { + header: 'Subscription', + id: SUBSCRIPTION_DB_COLUMN_ID, + accessorKey: SUBSCRIPTION_DB_COLUMN_ID, + enableSorting: true, + minSize: 200, + cell: ({ + row: { + original: { subscriptionName: name }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionDbResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionDbResult.tsx new file mode 100644 index 0000000000..1fe2b1049a --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionDbResult.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +import { SubscriptionCell } from '../components/SubscriptionCell/SubscriptionCell' + +export const SUBSCRIPTION_DB_RESULT_COLUMN_ID = 'subscriptionName' as const + +export const subscriptionDbResultColumn = (): ColumnDef => { + return { + header: 'Subscription', + id: SUBSCRIPTION_DB_RESULT_COLUMN_ID, + accessorKey: SUBSCRIPTION_DB_RESULT_COLUMN_ID, + enableSorting: true, + maxSize: 270, + cell: ({ + row: { + original: { subscriptionName: name }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionId.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionId.tsx new file mode 100644 index 0000000000..d01d6b3e7c --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionId.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' +import { CellText } from 'uiSrc/components/auto-discover' + +export const SUBSCRIPTION_ID_COLUMN_ID = 'subscriptionId' as const + +export const subscriptionIdColumn = (): ColumnDef => { + return { + header: 'Subscription ID', + id: SUBSCRIPTION_ID_COLUMN_ID, + accessorKey: SUBSCRIPTION_ID_COLUMN_ID, + enableSorting: true, + maxSize: 120, + cell: ({ + row: { + original: { subscriptionId }, + }, + }) => ( + + {subscriptionId} + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionIdResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionIdResult.tsx new file mode 100644 index 0000000000..8bbef82602 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionIdResult.tsx @@ -0,0 +1,14 @@ +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +export const SUBSCRIPTION_ID_RESULT_COLUMN_ID = 'subscriptionId' as const + +export const subscriptionIdResultColumn = (): ColumnDef => { + return { + header: 'Subscription ID', + id: SUBSCRIPTION_ID_RESULT_COLUMN_ID, + accessorKey: SUBSCRIPTION_ID_RESULT_COLUMN_ID, + enableSorting: true, + maxSize: 150, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionType.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionType.tsx new file mode 100644 index 0000000000..3811e7d196 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionType.tsx @@ -0,0 +1,30 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { + type InstanceRedisCloud, + RedisCloudSubscriptionTypeText, +} from 'uiSrc/slices/interfaces' + +import { CellText } from 'uiSrc/components/auto-discover' + +export const SUBSCRIPTION_TYPE_COLUMN_ID = 'subscriptionType' as const + +export const subscriptionTypeColumn = (): ColumnDef => { + return { + header: 'Type', + id: SUBSCRIPTION_TYPE_COLUMN_ID, + accessorKey: SUBSCRIPTION_TYPE_COLUMN_ID, + enableSorting: true, + maxSize: 100, + cell: ({ + row: { + original: { subscriptionType }, + }, + }) => ( + + {RedisCloudSubscriptionTypeText[subscriptionType!] ?? '-'} + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionTypeResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionTypeResult.tsx new file mode 100644 index 0000000000..e0a823dfea --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionTypeResult.tsx @@ -0,0 +1,23 @@ +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { + type InstanceRedisCloud, + RedisCloudSubscriptionTypeText, +} from 'uiSrc/slices/interfaces' + +export const SUBSCRIPTION_TYPE_RESULT_COLUMN_ID = 'subscriptionType' as const + +export const subscriptionTypeResultColumn = + (): ColumnDef => { + return { + header: 'Type', + id: SUBSCRIPTION_TYPE_RESULT_COLUMN_ID, + accessorKey: SUBSCRIPTION_TYPE_RESULT_COLUMN_ID, + enableSorting: true, + size: 95, + cell: ({ + row: { + original: { subscriptionType }, + }, + }) => RedisCloudSubscriptionTypeText[subscriptionType!] ?? '-', + } + } diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/type.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/type.tsx new file mode 100644 index 0000000000..c555f06255 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/type.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { + type RedisCloudSubscription, + RedisCloudSubscriptionTypeText, +} from 'uiSrc/slices/interfaces' + +import { CellText } from 'uiSrc/components/auto-discover' + +export const TYPE_COLUMN_ID = 'type' as const + +export const typeColumn = (): ColumnDef => { + return { + id: TYPE_COLUMN_ID, + accessorKey: TYPE_COLUMN_ID, + header: 'Type', + enableSorting: true, + cell: ({ + row: { + original: { type }, + }, + }) => {RedisCloudSubscriptionTypeText[type] ?? '-'}, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.spec.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.spec.tsx new file mode 100644 index 0000000000..5fd1d5e1f4 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.spec.tsx @@ -0,0 +1,56 @@ +import React from 'react' + +import { render, screen } from 'uiSrc/utils/test-utils' +import { RedisCloudSubscriptionStatus } from 'uiSrc/slices/interfaces' + +import { AlertCell } from './AlertCell' + +describe('AlertCell', () => { + it('should render success icon when subscription is active and has databases', () => { + render( + , + ) + + const icon = screen.getByRole('img', { hidden: true }) + expect(icon).toBeInTheDocument() + }) + + it('should render warning icon when subscription is not active', () => { + render( + , + ) + + const alertIcon = screen.getByLabelText('subscription alert') + expect(alertIcon).toBeInTheDocument() + }) + + it('should render warning icon when subscription has no databases', () => { + render( + , + ) + + const alertIcon = screen.getByLabelText('subscription alert') + expect(alertIcon).toBeInTheDocument() + }) + + it('should render warning icon when subscription is not active and has no databases', () => { + render( + , + ) + + const alertIcon = screen.getByLabelText('subscription alert') + expect(alertIcon).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.tsx new file mode 100644 index 0000000000..771eff5358 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.tsx @@ -0,0 +1,40 @@ +import React from 'react' + +import { RiTooltip } from 'uiSrc/components' +import { RedisCloudSubscriptionStatus } from 'uiSrc/slices/interfaces' +import { RiIcon } from 'uiSrc/components/base/icons' +import { CellText } from 'uiSrc/components/auto-discover' +import { AlertStatusContent } from 'uiSrc/pages/autodiscover-cloud/components/AlertStatusContent' +import styles from 'uiSrc/pages/autodiscover-cloud/redis-cloud-subscriptions/styles.module.scss' + +import { AlertCellProps } from './AlertCell.types' + +export const AlertCell = ({ status, numberOfDatabases }: AlertCellProps) => { + const isUnavailable = + status !== RedisCloudSubscriptionStatus.Active || numberOfDatabases === 0 + + if (isUnavailable) { + return ( + + This subscription is not available for one of the following reasons: + + } + content={} + position="right" + className={styles.tooltipStatus} + > + + + ) + } + + return +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.types.ts new file mode 100644 index 0000000000..442ce6bd61 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.types.ts @@ -0,0 +1,7 @@ +import { RedisCloudSubscription } from 'uiSrc/slices/interfaces' + +export interface AlertCellProps { + status: RedisCloudSubscription['status'] + numberOfDatabases: RedisCloudSubscription['numberOfDatabases'] +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.spec.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.spec.tsx new file mode 100644 index 0000000000..dfb5848e11 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.spec.tsx @@ -0,0 +1,48 @@ +import React from 'react' + +import { render, screen } from 'uiSrc/utils/test-utils' + +import { DatabaseCell } from './DatabaseCell' + +describe('DatabaseCell', () => { + it('should render database name', () => { + const name = 'test-database' + render() + + expect(screen.getByText(name)).toBeInTheDocument() + }) + + it('should render with testid', () => { + const name = 'my-db' + render() + + expect(screen.getByTestId(`db_name_${name}`)).toBeInTheDocument() + }) + + it('should truncate long names to 200 characters', () => { + const longName = 'a'.repeat(300) + render() + + const displayedText = screen.getByText('a'.repeat(200)) + expect(displayedText).toBeInTheDocument() + }) + + it('should replace spaces in names', () => { + const nameWithSpaces = 'my database name' + render() + + // replaceSpaces replaces spaces with nbsp + const element = screen.getByTestId(`db_name_${nameWithSpaces}`) + expect(element).toBeInTheDocument() + }) + + it('should apply custom className', () => { + const name = 'test-db' + const customClass = 'custom-class' + const { container } = render() + + const element = container.querySelector(`.${customClass}`) + expect(element).toBeInTheDocument() + }) +}) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.tsx new file mode 100644 index 0000000000..22a97fd60f --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.tsx @@ -0,0 +1,30 @@ +import React from 'react' + +import { formatLongName, replaceSpaces } from 'uiSrc/utils' +import { RiTooltip } from 'uiSrc/components' +import { CellText } from 'uiSrc/components/auto-discover' +import styles from 'uiSrc/pages/autodiscover-cloud/redis-cloud-databases/styles.module.scss' + +import { DatabaseCellProps } from './DatabaseCell.types' + +export const DatabaseCell = ({ name, className }: DatabaseCellProps) => { + const cellContent = replaceSpaces(name.substring(0, 200)) + + return ( +
+ + {cellContent} + +
+ ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.types.ts new file mode 100644 index 0000000000..67f897e36d --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.types.ts @@ -0,0 +1,5 @@ +export interface DatabaseCellProps { + name: string + className?: string +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.spec.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.spec.tsx new file mode 100644 index 0000000000..276c88eb89 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react' + +import { render, screen } from 'uiSrc/utils/test-utils' + +import { EndpointCell } from './EndpointCell' + +describe('EndpointCell', () => { + it('should render endpoint text', () => { + const endpoint = 'redis-12345.c1.us-east-1.ec2.cloud.redislabs.com:12345' + render() + + expect(screen.getByText(endpoint)).toBeInTheDocument() + }) + + it('should render copy button', () => { + const endpoint = 'redis-12345.c1.us-east-1.ec2.cloud.redislabs.com:12345' + render() + + const copyButton = screen.getByLabelText('Copy public endpoint') + expect(copyButton).toBeInTheDocument() + }) + + it('should render both endpoint text and copy button together', () => { + const endpoint = 'test-endpoint:6379' + render() + + expect(screen.getByText(endpoint)).toBeInTheDocument() + expect(screen.getByLabelText('Copy public endpoint')).toBeInTheDocument() + }) + + it('should render "-" when publicEndpoint is undefined', () => { + render() + + expect(screen.getByText('-')).toBeInTheDocument() + expect( + screen.queryByLabelText('Copy public endpoint'), + ).not.toBeInTheDocument() + }) + + it('should render "-" when publicEndpoint is empty string', () => { + render() + + expect(screen.getByText('-')).toBeInTheDocument() + expect( + screen.queryByLabelText('Copy public endpoint'), + ).not.toBeInTheDocument() + }) +}) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.tsx new file mode 100644 index 0000000000..0fdfb06cd8 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.tsx @@ -0,0 +1,41 @@ +import React from 'react' + +import { + CellText, + CopyBtn, + CopyPublicEndpointText, + CopyTextContainer, +} from 'uiSrc/components/auto-discover' +import { RiTooltip } from 'uiSrc/components' +import { formatLongName, handleCopy } from 'uiSrc/utils' + +import { EndpointCellProps } from './EndpointCell.types' + +export const EndpointCell = ({ publicEndpoint }: EndpointCellProps) => { + if (!publicEndpoint) { + return - + } + + return ( + + + {publicEndpoint} + + + + handleCopy(publicEndpoint)} + /> + + + ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.types.ts new file mode 100644 index 0000000000..8bdc9a6db0 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.types.ts @@ -0,0 +1,4 @@ +export interface EndpointCellProps { + publicEndpoint?: string +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.spec.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.spec.tsx new file mode 100644 index 0000000000..fd30f9e39f --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.spec.tsx @@ -0,0 +1,66 @@ +import React from 'react' + +import { render, screen } from 'uiSrc/utils/test-utils' +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +import { MessageResultCell } from './MessageResultCell' + +describe('MessageResultCell', () => { + it('should render success message when status is success', () => { + const message = 'Database added successfully' + render( + , + ) + + expect(screen.getByText(message)).toBeInTheDocument() + }) + + it('should render error icon and text when status is not success', () => { + const message = 'Failed to add database' + render( + , + ) + + expect(screen.getByText('Error')).toBeInTheDocument() + }) + + it('should not render success message when status is fail', () => { + const message = 'Failed to add database' + render( + , + ) + + expect(screen.queryByText(message)).not.toBeInTheDocument() + }) + + it('should render dash when statusAdded is undefined', () => { + render( + , + ) + + expect(screen.getByText('-')).toBeInTheDocument() + }) + + it('should handle missing messageAdded gracefully', () => { + const { container } = render( + , + ) + + const cellText = container.querySelector('.RI-text') + expect(cellText).toBeInTheDocument() + expect(cellText?.textContent).toBe('') + }) +}) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.tsx new file mode 100644 index 0000000000..88a5aca0e2 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.tsx @@ -0,0 +1,45 @@ +import React from 'react' + +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' +import { CellText } from 'uiSrc/components/auto-discover' +import { RiTooltip } from 'uiSrc/components' +import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { ColorText } from 'uiSrc/components/base/text' +import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' + +import { MessageResultCellProps } from './MessageResultCell.types' + +export const MessageResultCell = ({ + statusAdded, + messageAdded = '', +}: MessageResultCellProps) => { + if (!statusAdded) { + return - + } + + if (statusAdded === AddRedisDatabaseStatus.Success) { + return {messageAdded} + } + + return ( + + + + + + + + + Error + + + + + ) +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.types.ts new file mode 100644 index 0000000000..eaf513266f --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.types.ts @@ -0,0 +1,7 @@ +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +export interface MessageResultCellProps { + statusAdded?: AddRedisDatabaseStatus + messageAdded?: string +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.spec.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.spec.tsx new file mode 100644 index 0000000000..8cf26f374b --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.spec.tsx @@ -0,0 +1,43 @@ +import React from 'react' + +import { render, screen } from 'uiSrc/utils/test-utils' + +import { SubscriptionCell } from './SubscriptionCell' + +describe('SubscriptionCell', () => { + it('should render subscription name', () => { + const name = 'test-subscription' + render() + + expect(screen.getByText(name)).toBeInTheDocument() + }) + + it('should truncate long names to 200 characters', () => { + const longName = 'a'.repeat(300) + render() + + const displayedText = screen.getByText('a'.repeat(200)) + expect(displayedText).toBeInTheDocument() + }) + + it('should replace spaces in names', () => { + const nameWithSpaces = 'my subscription name' + render() + + // replaceSpaces replaces spaces with nbsp + const element = screen.getByRole('presentation') + expect(element).toBeInTheDocument() + }) + + it('should apply custom className', () => { + const name = 'test-sub' + const customClass = 'custom-class' + const { container } = render( + , + ) + + const element = container.querySelector(`.${customClass}`) + expect(element).toBeInTheDocument() + }) +}) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.tsx new file mode 100644 index 0000000000..65fb023131 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +import { formatLongName, replaceSpaces } from 'uiSrc/utils' +import { RiTooltip } from 'uiSrc/components' +import { CellText } from 'uiSrc/components/auto-discover' +import styles from 'uiSrc/pages/autodiscover-cloud/redis-cloud-databases/styles.module.scss' + +import { SubscriptionCellProps } from './SubscriptionCell.types' + +export const SubscriptionCell = ({ + name, + className, +}: SubscriptionCellProps) => { + const cellContent = replaceSpaces(name.substring(0, 200)) + + return ( +
+ + {cellContent} + +
+ ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.types.ts new file mode 100644 index 0000000000..5ead84d086 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.types.ts @@ -0,0 +1,5 @@ +export interface SubscriptionCellProps { + name: string + className?: string +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/index.ts b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/index.ts new file mode 100644 index 0000000000..f3cc04b22c --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/index.ts @@ -0,0 +1,6 @@ +export * from './AlertCell/AlertCell' +export * from './DatabaseCell/DatabaseCell' +export * from './EndpointCell/EndpointCell' +export * from './MessageResultCell/MessageResultCell' +export * from './SubscriptionCell/SubscriptionCell' + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/index.ts b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/index.ts new file mode 100644 index 0000000000..d484219c41 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/index.ts @@ -0,0 +1,26 @@ +export * from './columns/alert' +export * from './columns/database' +export * from './columns/databaseResult' +export * from './columns/endpoint' +export * from './columns/endpointResult' +export * from './columns/id' +export * from './columns/messageResult' +export * from './columns/modules' +export * from './columns/modulesResult' +export * from './columns/numberOfDbs' +export * from './columns/options' +export * from './columns/optionsResult' +export * from './columns/provider' +export * from './columns/region' +export * from './columns/selection' +export * from './columns/status' +export * from './columns/statusDb' +export * from './columns/statusDbResult' +export * from './columns/subscription' +export * from './columns/subscriptionDb' +export * from './columns/subscriptionDbResult' +export * from './columns/subscriptionId' +export * from './columns/subscriptionIdResult' +export * from './columns/subscriptionType' +export * from './columns/subscriptionTypeResult' +export * from './columns/type' diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/components/AlertStatusContent.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/components/AlertStatusContent.tsx new file mode 100644 index 0000000000..f9deed4948 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/components/AlertStatusContent.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { + AlertStatusDot, + AlertStatusList, + AlertStatusListItem, +} from 'uiSrc/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.styles' + +export const AlertStatusContent = () => ( + + } + /> + } + /> + } + /> + +) diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.spec.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.spec.tsx index ecd45c9773..474d154e2b 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.spec.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.spec.tsx @@ -1,9 +1,6 @@ import React from 'react' -import { instance, mock } from 'ts-mockito' import { render } from 'uiSrc/utils/test-utils' -import RedisCloudDatabasesResult, { Props } from './RedisCloudDatabasesResult' - -const mockedProps = mock() +import RedisCloudDatabasesResult from './RedisCloudDatabasesResult' describe('RedisCloudDatabasesResult', () => { it('should render', () => { @@ -18,8 +15,10 @@ describe('RedisCloudDatabasesResult', () => { expect( render( , ), ).toBeTruthy() diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.stories.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.stories.tsx new file mode 100644 index 0000000000..43b3cd1836 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.stories.tsx @@ -0,0 +1,83 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import RedisCloudDatabasesResult from './RedisCloudDatabasesResult' +import { + RedisCloudInstanceFactory, + RedisCloudInstanceFactorySuccess, + RedisCloudInstanceFactoryFail, + RedisCloudInstanceFactoryWithModules, + RedisCloudInstanceFactoryOptionsFull, +} from 'uiSrc/mocks/factories/cloud/RedisCloudInstance.factory' +import { colFactory } from './utils/colFactory' +import { RedisDefaultModules } from 'uiSrc/slices/interfaces' + +const meta: Meta = { + component: RedisCloudDatabasesResult, + args: { + instances: [], + columns: [], + onView: () => {}, + onBack: () => {}, + }, +} + +export default meta + +type Story = StoryObj + +export const Empty: Story = {} + +const mixedInstances = RedisCloudInstanceFactory.buildList(10) +const mixedColumns = colFactory(mixedInstances, mixedInstances) +export const MixedResults: Story = { + args: { + instances: mixedInstances, + columns: mixedColumns, + }, +} + +const successInstances = RedisCloudInstanceFactorySuccess.buildList(8) +const successColumns = colFactory(successInstances, successInstances) +export const AllSuccess: Story = { + args: { + instances: successInstances, + columns: successColumns, + }, +} + +const failInstances = RedisCloudInstanceFactoryFail.buildList(8) +const failColumns = colFactory(failInstances, failInstances) +export const AllFailed: Story = { + args: { + instances: failInstances, + columns: failColumns, + }, +} + +const withModulesInstances = RedisCloudInstanceFactoryWithModules([ + RedisDefaultModules.Search, + RedisDefaultModules.ReJSON, + RedisDefaultModules.TimeSeries, +]).buildList(8) +const withModulesColumns = colFactory( + withModulesInstances, + withModulesInstances, +) +export const WithModules: Story = { + args: { + instances: withModulesInstances, + columns: withModulesColumns, + }, +} + +const withOptionsInstances = RedisCloudInstanceFactoryOptionsFull.buildList(8) +const withOptionsColumns = colFactory( + withOptionsInstances, + withOptionsInstances, +) +export const WithOptions: Story = { + args: { + instances: withOptionsInstances, + columns: withOptionsColumns, + }, +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.tsx index 911d6c2cca..ed390c78a9 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.tsx @@ -1,31 +1,27 @@ -import React, { useState, useEffect } from 'react' -import { useSelector } from 'react-redux' -import { - InstanceRedisCloud, - AddRedisDatabaseStatus, -} from 'uiSrc/slices/interfaces' -import { cloudSelector } from 'uiSrc/slices/instances/cloud' +import React, { useEffect, useState } from 'react' +import type { InstanceRedisCloud } from 'uiSrc/slices/interfaces' +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' import MessageBar from 'uiSrc/components/message-bar/MessageBar' +import { riToast } from 'uiSrc/components/base/display/toast' import { AutodiscoveryPageTemplate } from 'uiSrc/templates' -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' -import { - PrimaryButton, - SecondaryButton, -} from 'uiSrc/components/base/forms/buttons' -import { SearchInput } from 'uiSrc/components/base/inputs' -import { Text } from 'uiSrc/components/base/text' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' +import { Row } from 'uiSrc/components/base/layout/flex' +import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import { Table } from 'uiSrc/components/base/layout/table' import { Spacer } from 'uiSrc/components/base/layout' import { + DatabaseContainer, DatabaseWrapper, + EmptyState, Footer, - PageTitle, - SearchForm, + Header, } from 'uiSrc/components/auto-discover' +import { SummaryText } from './components' export interface Props { - columns: ColumnDefinition[] + instances: InstanceRedisCloud[] + columns: ColumnDef[] onView: () => void onBack: () => void } @@ -33,12 +29,15 @@ export interface Props { const loadingMsg = 'loading...' const notFoundMsg = 'Not found' -const RedisCloudDatabaseListResult = ({ columns, onBack, onView }: Props) => { +const RedisCloudDatabaseListResult = ({ + instances, + columns, + onBack, + onView, +}: Props) => { const [items, setItems] = useState([]) const [message, setMessage] = useState(loadingMsg) - const { dataAdded: instances } = useSelector(cloudSelector) - useEffect(() => setItems(instances), [instances]) const countSuccessAdded = instances.filter( @@ -67,40 +66,15 @@ const RedisCloudDatabaseListResult = ({ columns, onBack, onView }: Props) => { setItems(itemsTemp) } - const SummaryText = () => ( - - Summary: - {countSuccessAdded ? ( - - Successfully added {countSuccessAdded} database(s) - {countFailAdded ? '. ' : '.'} - - ) : null} - {countFailAdded ? ( - Failed to add {countFailAdded} database(s). - ) : null} - - ) - return ( -
- - Redis Enterprise Databases Added - - - - - - - - - + +
+ { desc: false, }, ]} + paginationEnabled={items.length > 10} + stripedRows + emptyState={() => } /> - {!items.length && {message}} - - + + - +
- - + - Back to adding databases - - View Databases diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx index c76c61d67d..8146bc9ce8 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx @@ -1,265 +1,17 @@ -import React, { useEffect } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' - -import { Pages } from 'uiSrc/constants' -import { - cloudSelector, - resetDataRedisCloud, - resetLoadedRedisCloud, -} from 'uiSrc/slices/instances/cloud' -import { - InstanceRedisCloud, - AddRedisDatabaseStatus, - LoadedCloud, - RedisCloudSubscriptionTypeText, -} from 'uiSrc/slices/interfaces' -import { - formatLongName, - parseInstanceOptionsCloud, - replaceSpaces, - setTitle, -} from 'uiSrc/utils' -import { - DatabaseListModules, - DatabaseListOptions, - RiTooltip, -} from 'uiSrc/components' -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' -import { IconButton } from 'uiSrc/components/base/forms/buttons' -import { CopyIcon } from 'uiSrc/components/base/icons' -import { ColorText } from 'uiSrc/components/base/text' -import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' -import { ColumnDefinition } from 'uiSrc/components/base/layout/table' +import React from 'react' import RedisCloudDatabasesResult from './RedisCloudDatabasesResult' - -import styles from './styles.module.scss' -import { - CellText, - CopyPublicEndpointText, - CopyTextContainer, -} from 'uiSrc/components/auto-discover' +import { useCloudDatabasesResultConfig } from './hooks/useCloudDatabasesResultConfig' const RedisCloudDatabasesResultPage = () => { - const dispatch = useDispatch() - const history = useHistory() - - const { data: instancesForOptions, dataAdded: instances } = - useSelector(cloudSelector) - - setTitle('Redis Enterprise Databases Added') - - useEffect(() => { - if (!instances.length) { - history.push(Pages.home) - } - }, []) - - const handleClose = () => { - dispatch(resetDataRedisCloud()) - history.push(Pages.home) - } - - const handleBackAdditing = () => { - dispatch(resetLoadedRedisCloud(LoadedCloud.InstancesAdded)) - history.push(Pages.home) - } - - const handleCopy = (text = '') => { - navigator.clipboard.writeText(text) - } - - const columns: ColumnDefinition[] = [ - { - header: 'Database', - id: 'name', - accessorKey: 'name', - enableSorting: true, - size: 195, - cell: function InstanceCell({ - row: { - original: { name }, - }, - }) { - const cellContent = replaceSpaces(name.substring(0, 200)) - return ( -
- - {cellContent} - -
- ) - }, - }, - { - header: 'Subscription ID', - id: 'subscriptionId', - accessorKey: 'subscriptionId', - enableSorting: true, - size: 170, - }, - { - header: 'Subscription', - id: 'subscriptionName', - accessorKey: 'subscriptionName', - enableSorting: true, - size: 300, - cell: function SubscriptionCell({ - row: { - original: { subscriptionName: name }, - }, - }) { - const cellContent = replaceSpaces(name.substring(0, 200)) - return ( -
- - {cellContent} - -
- ) - }, - }, - { - header: 'Type', - id: 'subscriptionType', - accessorKey: 'subscriptionType', - enableSorting: true, - size: 95, - cell: ({ - row: { - original: { subscriptionType }, - }, - }) => RedisCloudSubscriptionTypeText[subscriptionType!] ?? '-', - }, - { - header: 'Status', - id: 'status', - accessorKey: 'status', - enableSorting: true, - size: 95, - cell: ({ - row: { - original: { status }, - }, - }) => {status}, - }, - { - header: 'Endpoint', - id: 'publicEndpoint', - accessorKey: 'publicEndpoint', - enableSorting: true, - size: 310, - cell: function PublicEndpoint({ - row: { - original: { publicEndpoint }, - }, - }) { - const text = publicEndpoint - return ( - - {text} - - handleCopy(text)} - /> - - - ) - }, - }, - { - header: 'Capabilities', - id: 'modules', - accessorKey: 'modules', - enableSorting: true, - size: 200, - cell: function Modules({ row: { original: instance } }) { - return ( - ({ name }))} - /> - ) - }, - }, - { - header: 'Options', - id: 'options', - accessorKey: 'options', - enableSorting: true, - size: 180, - cell: function Opitions({ row: { original: instance } }) { - const options = parseInstanceOptionsCloud( - instance.databaseId, - instancesForOptions, - ) - return - }, - }, - { - header: 'Result', - id: 'messageAdded', - accessorKey: 'messageAdded', - enableSorting: true, - size: 110, - cell: function Message({ - row: { - original: { statusAdded, messageAdded }, - }, - }) { - return ( - <> - {statusAdded === AddRedisDatabaseStatus.Success ? ( - {messageAdded} - ) : ( - - - - - - - - - Error - - - - - )} - - ) - }, - }, - ] + const { instances, columns, handleClose, handleBackAdding } = + useCloudDatabasesResultConfig() return ( ) } diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/SummaryText/SummaryText.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/SummaryText/SummaryText.tsx new file mode 100644 index 0000000000..e9510ca4c3 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/SummaryText/SummaryText.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +import { ColorText, Text } from 'uiSrc/components/base/text' +import type { SummaryTextProps } from './SummaryText.types' + +export const SummaryText = ({ countSuccessAdded, countFailAdded }: SummaryTextProps) => ( + + Summary: {' '} + {countSuccessAdded ? ( + + Successfully added {countSuccessAdded} database(s) + {countFailAdded ? '. ' : '.'} + + ) : null} + {countFailAdded ? ( + Failed to add {countFailAdded} database(s). + ) : null} + +) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/SummaryText/SummaryText.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/SummaryText/SummaryText.types.ts new file mode 100644 index 0000000000..c8e0e17d13 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/SummaryText/SummaryText.types.ts @@ -0,0 +1,5 @@ +export interface SummaryTextProps { + countSuccessAdded: number + countFailAdded: number +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/index.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/index.ts new file mode 100644 index 0000000000..908fb4b4c0 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/index.ts @@ -0,0 +1,2 @@ +export { SummaryText } from './SummaryText/SummaryText' + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/hooks/useCloudDatabasesResultConfig.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/hooks/useCloudDatabasesResultConfig.ts new file mode 100644 index 0000000000..0da2d1c8b7 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/hooks/useCloudDatabasesResultConfig.ts @@ -0,0 +1,52 @@ +import { useCallback, useEffect, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { Pages } from 'uiSrc/constants' +import { + cloudSelector, + resetDataRedisCloud, + resetLoadedRedisCloud, +} from 'uiSrc/slices/instances/cloud' +import { LoadedCloud } from 'uiSrc/slices/interfaces' +import { setTitle } from 'uiSrc/utils' +import { colFactory } from '../utils/colFactory' +import { UseCloudDatabasesResultConfigReturn } from './useCloudDatabasesResultConfig.types' + +export const useCloudDatabasesResultConfig = + (): UseCloudDatabasesResultConfigReturn => { + const dispatch = useDispatch() + const history = useHistory() + + const { data: instancesForOptions, dataAdded: instances } = + useSelector(cloudSelector) + + useEffect(() => { + if (!instances.length) { + history.push(Pages.home) + } + setTitle('Redis Enterprise Databases Added') + }, [instances.length, history]) + + const handleClose = useCallback(() => { + dispatch(resetDataRedisCloud()) + history.push(Pages.home) + }, [dispatch, history]) + + const handleBackAdding = useCallback(() => { + dispatch(resetLoadedRedisCloud(LoadedCloud.InstancesAdded)) + history.push(Pages.home) + }, [dispatch, history]) + + const columns = useMemo( + () => colFactory(instances || [], instancesForOptions || []), + [instances, instancesForOptions], + ) + + return { + instances, + columns, + handleClose, + handleBackAdding, + } + } diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/hooks/useCloudDatabasesResultConfig.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/hooks/useCloudDatabasesResultConfig.types.ts new file mode 100644 index 0000000000..a25ae5381d --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/hooks/useCloudDatabasesResultConfig.types.ts @@ -0,0 +1,10 @@ +import { ColumnDef } from 'uiSrc/components/base/layout/table' +import { InstanceRedisCloud } from 'uiSrc/slices/interfaces' + +export interface UseCloudDatabasesResultConfigReturn { + instances: InstanceRedisCloud[] + columns: ColumnDef[] + handleClose: () => void + handleBackAdding: () => void +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/utils/colFactory.spec.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/utils/colFactory.spec.ts new file mode 100644 index 0000000000..e6786cbb38 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/utils/colFactory.spec.ts @@ -0,0 +1,155 @@ +import { + RedisDefaultModules, + AddRedisClusterDatabaseOptions, + type InstanceRedisCloud, +} from 'uiSrc/slices/interfaces' +import { colFactory } from './colFactory' +import { + DATABASE_RESULT_COLUMN_ID, + SUBSCRIPTION_ID_RESULT_COLUMN_ID, + SUBSCRIPTION_DB_RESULT_COLUMN_ID, + SUBSCRIPTION_TYPE_RESULT_COLUMN_ID, + STATUS_DB_RESULT_COLUMN_ID, + ENDPOINT_RESULT_COLUMN_ID, + MODULES_RESULT_COLUMN_ID, + OPTIONS_RESULT_COLUMN_ID, + MESSAGE_RESULT_COLUMN_ID, +} from '../../column-definitions' +import { RedisCloudInstanceFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudInstance.factory' + +describe('colFactory', () => { + it('should return base columns without modules and options when instances array is empty', () => { + const instances: InstanceRedisCloud[] = [] + + const columns = colFactory(instances, instances) + + expect(columns).toHaveLength(7) + expect(columns.map((col) => col.id)).toEqual([ + DATABASE_RESULT_COLUMN_ID, + SUBSCRIPTION_ID_RESULT_COLUMN_ID, + SUBSCRIPTION_DB_RESULT_COLUMN_ID, + SUBSCRIPTION_TYPE_RESULT_COLUMN_ID, + STATUS_DB_RESULT_COLUMN_ID, + ENDPOINT_RESULT_COLUMN_ID, + MESSAGE_RESULT_COLUMN_ID, + ]) + }) + + it('should return base columns without modules and options when instances have no modules or options', () => { + const instances = RedisCloudInstanceFactory.buildList(1, { + modules: [], + options: undefined, + }) + + const columns = colFactory(instances, instances) + + expect(columns).toHaveLength(7) + expect(columns.map((col) => col.id)).toEqual([ + DATABASE_RESULT_COLUMN_ID, + SUBSCRIPTION_ID_RESULT_COLUMN_ID, + SUBSCRIPTION_DB_RESULT_COLUMN_ID, + SUBSCRIPTION_TYPE_RESULT_COLUMN_ID, + STATUS_DB_RESULT_COLUMN_ID, + ENDPOINT_RESULT_COLUMN_ID, + MESSAGE_RESULT_COLUMN_ID, + ]) + }) + + it('should include modules column when at least one instance has modules', () => { + const instances = [ + RedisCloudInstanceFactory.build({ modules: [], options: undefined }), + RedisCloudInstanceFactory.build({ + modules: [RedisDefaultModules.ReJSON], + options: undefined, + }), + ] + + const columns = colFactory(instances, instances) + + expect(columns).toHaveLength(8) + expect(columns.map((col) => col.id)).toContain(MODULES_RESULT_COLUMN_ID) + expect(columns[6].id).toBe(MODULES_RESULT_COLUMN_ID) + expect(columns.map((col) => col.id)).not.toContain(OPTIONS_RESULT_COLUMN_ID) + }) + + it('should include options column when at least one instance has options with truthy values', () => { + const instances = [ + RedisCloudInstanceFactory.build({ modules: [], options: {} }), + RedisCloudInstanceFactory.build({ + modules: [], + options: { + [AddRedisClusterDatabaseOptions.Backup]: true, + [AddRedisClusterDatabaseOptions.Clustering]: false, + }, + }), + ] + + const columns = colFactory(instances, instances) + + expect(columns).toHaveLength(8) + expect(columns.map((col) => col.id)).toContain(OPTIONS_RESULT_COLUMN_ID) + expect(columns[6].id).toBe(OPTIONS_RESULT_COLUMN_ID) + expect(columns.map((col) => col.id)).not.toContain(MODULES_RESULT_COLUMN_ID) + }) + it('should include both modules and options columns when instances have both', () => { + const instances = RedisCloudInstanceFactory.buildList(1, { + modules: [RedisDefaultModules.ReJSON], + options: { + [AddRedisClusterDatabaseOptions.Backup]: true, + }, + }) + + const columns = colFactory(instances, instances) + + expect(columns).toHaveLength(9) + expect(columns.map((col) => col.id)).toEqual([ + DATABASE_RESULT_COLUMN_ID, + SUBSCRIPTION_ID_RESULT_COLUMN_ID, + SUBSCRIPTION_DB_RESULT_COLUMN_ID, + SUBSCRIPTION_TYPE_RESULT_COLUMN_ID, + STATUS_DB_RESULT_COLUMN_ID, + ENDPOINT_RESULT_COLUMN_ID, + MODULES_RESULT_COLUMN_ID, + OPTIONS_RESULT_COLUMN_ID, + MESSAGE_RESULT_COLUMN_ID, + ]) + }) + + it('should always have message column as the last column', () => { + const instancesWithModules = RedisCloudInstanceFactory.buildList(1, { + modules: [RedisDefaultModules.ReJSON], + options: undefined, + }) + + const columnsWithModules = colFactory( + instancesWithModules, + instancesWithModules, + ) + expect(columnsWithModules[columnsWithModules.length - 1].id).toBe( + MESSAGE_RESULT_COLUMN_ID, + ) + + const instancesWithOptions = RedisCloudInstanceFactory.buildList(1, { + modules: [], + options: { [AddRedisClusterDatabaseOptions.Backup]: true }, + }) + + const columnsWithOptions = colFactory( + instancesWithOptions, + instancesWithOptions, + ) + expect(columnsWithOptions[columnsWithOptions.length - 1].id).toBe( + MESSAGE_RESULT_COLUMN_ID, + ) + + const instancesWithBoth = RedisCloudInstanceFactory.buildList(1, { + modules: [RedisDefaultModules.ReJSON], + options: { [AddRedisClusterDatabaseOptions.Backup]: true }, + }) + + const columnsWithBoth = colFactory(instancesWithBoth, instancesWithBoth) + expect(columnsWithBoth[columnsWithBoth.length - 1].id).toBe( + MESSAGE_RESULT_COLUMN_ID, + ) + }) +}) diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/utils/colFactory.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/utils/colFactory.ts new file mode 100644 index 0000000000..13676e2eb2 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/utils/colFactory.ts @@ -0,0 +1,49 @@ +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' +import { + databaseResultColumn, + subscriptionIdResultColumn, + subscriptionDbResultColumn, + subscriptionTypeResultColumn, + statusDbResultColumn, + endpointResultColumn, + modulesResultColumn, + optionsResultColumn, + messageResultColumn, +} from '../../column-definitions' + +export const colFactory = ( + instances: InstanceRedisCloud[] = [], + instancesForOptions: InstanceRedisCloud[] = [], +): ColumnDef[] => { + const shouldShowCapabilities = instances.some( + (instance) => instance.modules?.length, + ) + const shouldShowOptions = instances.some( + (instance) => + instance.options && + Object.values(instance.options).filter(Boolean).length, + ) + + const columns: ColumnDef[] = [ + databaseResultColumn(), + subscriptionIdResultColumn(), + subscriptionDbResultColumn(), + subscriptionTypeResultColumn(), + statusDbResultColumn(), + endpointResultColumn(), + ] + + if (shouldShowCapabilities) { + columns.push(modulesResultColumn()) + } + + if (shouldShowOptions) { + columns.push(optionsResultColumn(instancesForOptions)) + } + + columns.push(messageResultColumn()) + + return columns +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.spec.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.spec.tsx index 167eeb6e67..7e6511cd0a 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.spec.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.spec.tsx @@ -1,9 +1,6 @@ import React from 'react' -import { instance, mock } from 'ts-mockito' import { render } from 'uiSrc/utils/test-utils' -import RedisCloudDatabases, { Props } from './RedisCloudDatabases' - -const mockedProps = mock() +import RedisCloudDatabases from './RedisCloudDatabases' describe('RedisCloudDatabases', () => { it('should render', () => { @@ -19,8 +16,13 @@ describe('RedisCloudDatabases', () => { render( , ), ).toBeTruthy() diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.stories.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.stories.tsx new file mode 100644 index 0000000000..289339f3e7 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.stories.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react' +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' + +import RedisCloudDatabases from './' +import { colFactory } from '../utils/colFactory' + +import { RowSelectionState } from 'uiSrc/components/base/layout/table' +import { InstanceRedisCloud } from 'uiSrc/slices/interfaces' +import { RedisCloudInstanceFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudInstance.factory' + +const emptyColumns = colFactory([]) + +const meta: Meta = { + component: RedisCloudDatabases, + args: { + columns: emptyColumns, + instances: [], + selection: [], + loading: true, + onSubmit: () => {}, + }, +} + +export default meta + +type Story = StoryObj + +export const Empty: Story = {} + +const RenderStory = () => { + const instancesMock: InstanceRedisCloud[] = + RedisCloudInstanceFactory.buildList(6) + const columns = colFactory(instancesMock) + const [selection, setSelection] = useState([]) + + const handleSelectionChange = (currentSelected: RowSelectionState) => { + const newSelection = instancesMock.filter((item) => { + const { id } = item + if (!id) { + return false + } + return currentSelected[id] + }) + setSelection(newSelection) + } + + return ( + + ) +} + +export const WithDatabases: Story = { + render: () => , +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.tsx index 8ada4c33af..b5de1cd00a 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.tsx @@ -1,14 +1,11 @@ -import React, { useState, useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { map, pick } from 'lodash' -import { useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' -import { cloudSelector } from 'uiSrc/slices/instances/cloud' import { InstanceRedisCloud } from 'uiSrc/slices/interfaces' import validationErrors from 'uiSrc/constants/validationErrors' import { AutodiscoveryPageTemplate } from 'uiSrc/templates' -import { Row } from 'uiSrc/components/base/layout/flex' +import { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex' import { InfoIcon } from 'uiSrc/components/base/icons' import { DestructiveButton, @@ -16,24 +13,27 @@ import { SecondaryButton, } from 'uiSrc/components/base/forms/buttons' import { RiPopover, RiTooltip } from 'uiSrc/components/base' -import { Pages } from 'uiSrc/constants' -import { SearchInput } from 'uiSrc/components/base/inputs' import { Text } from 'uiSrc/components/base/text' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' +import { + ColumnDef, + RowSelectionState, + Table, +} from 'uiSrc/components/base/layout/table' import styles from '../styles.module.scss' import { Spacer } from 'uiSrc/components/base/layout' import { + DatabaseContainer, DatabaseWrapper, Footer, - PageSubTitle, - PageTitle, - SearchContainer, - SearchForm, + Header, } from 'uiSrc/components/auto-discover' export interface Props { - columns: ColumnDefinition[] + columns: ColumnDef[] + instances: InstanceRedisCloud[] selection: InstanceRedisCloud[] + loading: boolean + onSelectionChange: (currentSelected: RowSelectionState) => void onClose: () => void onBack: () => void onSubmit: ( @@ -51,11 +51,14 @@ interface IPopoverProps { const loadingMsg = 'loading...' const notFoundMsg = 'Not found' const noResultsMessage = - 'Your Redis Enterprise Сloud has no databases available' + 'Your Redis Enterprise Cloud has no databases available' const RedisCloudDatabasesPage = ({ columns, selection, + instances, + loading, + onSelectionChange, onClose, onBack, onSubmit, @@ -64,27 +67,15 @@ const RedisCloudDatabasesPage = ({ const [message, setMessage] = useState(loadingMsg) const [isPopoverOpen, setIsPopoverOpen] = useState(false) - const history = useHistory() - - const { loading, data: instances } = useSelector(cloudSelector) - useEffect(() => { if (instances !== null) { setItems(instances) } - }, [instances]) - - useEffect(() => { - if (instances === null) { - history.push(Pages.home) - } - }, []) - useEffect(() => { - if (instances?.length === 0) { + if (instances?.length === 0 && !loading) { setMessage(noResultsMessage) } - }, [instances]) + }, [instances, loading]) const handleSubmit = () => { onSubmit( @@ -181,30 +172,23 @@ const RedisCloudDatabasesPage = ({ return ( -
- Redis Cloud Databases - - - - These are {items.length > 1 ? 'databases ' : 'database '} + +
1 ? 'databases ' : 'database '} in your Redis Cloud. Select the - {items.length > 1 ? ' databases ' : ' database '} that you want to - add. - - - - - - - - + ${items.length > 1 ? ' databases ' : ' database '} that you want to + add.`} + /> +
`${row.databaseId}`} columns={columns} data={items} defaultSorting={[ @@ -213,20 +197,27 @@ const RedisCloudDatabasesPage = ({ desc: false, }, ]} + paginationEnabled={items.length > 10} + stripedRows + pageSizes={[5, 10, 25, 50, 100]} + emptyState={() => ( + + + {message} + + + )} /> - {!items.length && {message}} + {!items.length && ( + + {message} + + )} - +
- - - Back to adding databases - - + + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx index d3f425986e..a441fef4c1 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx @@ -1,6 +1,9 @@ import React from 'react' +import { InstanceRedisCloud } from 'uiSrc/slices/interfaces' import RedisCloudDatabases from './RedisCloudDatabases' -import { useCloudDatabasesConfig } from './useCloudDatabasesConfig' +import { useCloudDatabasesConfig } from './hooks/useCloudDatabasesConfig' + +const EMPTY_INSTANCES: InstanceRedisCloud[] = [] const RedisCloudDatabasesPage = () => { const { @@ -9,6 +12,8 @@ const RedisCloudDatabasesPage = () => { handleClose, handleBackAdding, handleAddInstances, + handleSelectionChange, + instances, } = useCloudDatabasesConfig() return ( @@ -18,6 +23,9 @@ const RedisCloudDatabasesPage = () => { onBack={handleBackAdding} onSubmit={handleAddInstances} columns={columns} + instances={instances || EMPTY_INSTANCES} + loading={false} + onSelectionChange={handleSelectionChange} /> ) } diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.test.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.test.ts new file mode 100644 index 0000000000..94cd4c31f1 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.test.ts @@ -0,0 +1,172 @@ +import { cloneDeep } from 'lodash' + +import { + mockStore, + initialStateDefault, + renderHook, + act, +} from 'uiSrc/utils/test-utils' +import { LoadedCloud, OAuthSocialAction } from 'uiSrc/slices/interfaces' +import { + resetDataRedisCloud, + resetLoadedRedisCloud, +} from 'uiSrc/slices/instances/cloud' + +import { useCloudDatabasesConfig } from './useCloudDatabasesConfig' + +describe('useCloudDatabasesConfig', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return correct initial state with columns', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.data = [ + { databaseId: 1, name: 'db1', subscriptionId: 1, free: false } as any, + { databaseId: 2, name: 'db2', subscriptionId: 2, free: false } as any, + ] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudDatabasesConfig(), { store }) + + expect(result.current.columns).toHaveLength(9) + expect(result.current.columns[0].id).toBe('row-selection') + expect(result.current.columns[1].id).toBe('name') + expect(result.current.selection).toEqual([]) + expect(result.current.instances).toHaveLength(2) + expect(result.current.loading).toBe(false) + expect(typeof result.current.handleClose).toBe('function') + expect(typeof result.current.handleBackAdding).toBe('function') + expect(typeof result.current.handleAddInstances).toBe('function') + expect(typeof result.current.handleSelectionChange).toBe('function') + }) + + it('should return columns without selection when instances array is empty', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.data = [] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudDatabasesConfig(), { store }) + + expect(result.current.columns).toHaveLength(8) + expect(result.current.columns[0].id).toBe('name') + }) + + describe('handleSelectionChange', () => { + it('should update selection based on current selected state', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.data = [ + { databaseId: 1, name: 'db1', subscriptionId: 1, free: false } as any, + { databaseId: 2, name: 'db2', subscriptionId: 2, free: false } as any, + ] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudDatabasesConfig(), { store }) + + act(() => { + result.current.handleSelectionChange({ 1: true, 2: false }) + }) + + expect(result.current.selection).toEqual([ + { databaseId: 1, name: 'db1', subscriptionId: 1, free: false }, + ]) + }) + + it('should filter out items without databaseId', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.data = [ + { databaseId: 1, name: 'db1' } as any, + { databaseId: null, name: 'db2' } as any, + ] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudDatabasesConfig(), { store }) + + act(() => { + result.current.handleSelectionChange({ 1: true }) + }) + + expect(result.current.selection).toEqual([{ databaseId: 1, name: 'db1' }]) + }) + }) + + describe('handleClose', () => { + it('should dispatch reset data action', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.data = [] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudDatabasesConfig(), { store }) + + act(() => { + result.current.handleClose() + }) + + const actions = store.getActions() + expect(actions).toContainEqual(resetDataRedisCloud()) + }) + }) + + describe('handleBackAdding', () => { + it('should dispatch reset loaded state action', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.data = [] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudDatabasesConfig(), { store }) + + act(() => { + result.current.handleBackAdding() + }) + + const actions = store.getActions() + expect(actions).toContainEqual( + resetLoadedRedisCloud(LoadedCloud.Instances), + ) + }) + }) + + describe('handleAddInstances', () => { + it('should dispatch create instances action', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.data = [] + state.connections.cloud.credentials = { + accessKey: 'test-key', + secretKey: 'test-secret', + } + const store = mockStore(state) + + const { result } = renderHook(() => useCloudDatabasesConfig(), { store }) + + const databases = [ + { databaseId: 1, subscriptionId: 1, free: false }, + { databaseId: 2, subscriptionId: 2, free: false }, + ] + + act(() => { + result.current.handleAddInstances(databases) + }) + + const actions = store.getActions() + const createAction = actions.find( + (action) => action.type === 'cloud/createInstancesRedisCloud', + ) + expect(createAction).toBeDefined() + }) + }) + + describe('SSO Flow handling', () => { + it('should dispatch reset data when userOAuthProfile is null in Import flow', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.data = [] + state.connections.cloud.ssoFlow = OAuthSocialAction.Import + state.oauth.cloud.user.data = null + const store = mockStore(state) + + renderHook(() => useCloudDatabasesConfig(), { store }) + + const actions = store.getActions() + expect(actions).toContainEqual(resetDataRedisCloud()) + }) + }) +}) diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.ts new file mode 100644 index 0000000000..e9098573b7 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.ts @@ -0,0 +1,139 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { + addInstancesRedisCloud, + cloudSelector, + fetchSubscriptionsRedisCloud, + resetDataRedisCloud, + resetLoadedRedisCloud, +} from 'uiSrc/slices/instances/cloud' +import { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud' +import { setTitle } from 'uiSrc/utils' +import { Pages } from 'uiSrc/constants' +import { + InstanceRedisCloud, + LoadedCloud, + OAuthSocialAction, +} from 'uiSrc/slices/interfaces' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { RowSelectionState } from 'uiSrc/components/base/layout/table' + +import { UseCloudDatabasesConfigReturn } from './useCloudDatabasesConfig.types' +import { colFactory } from '../utils/colFactory' + +export const useCloudDatabasesConfig = (): UseCloudDatabasesConfigReturn => { + const dispatch = useDispatch() + const history = useHistory() + + const { + ssoFlow, + credentials, + loading, + data: instances, + dataAdded: instancesAdded, + } = useSelector(cloudSelector) + const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector) + + const currentAccountIdRef = useRef(userOAuthProfile?.id) + const ssoFlowRef = useRef(ssoFlow) + + const [selection, setSelection] = useState([]) + + const handleSelectionChange = useCallback( + (currentSelected: RowSelectionState) => { + const newSelection = instances?.filter((item) => { + const { databaseId } = item + if (!databaseId) { + return false + } + return currentSelected[databaseId] + }) + setSelection(newSelection || []) + }, + [instances], + ) + + useEffect(() => { + if (instances === null) { + history.push(Pages.home) + } + setTitle('Redis Cloud Databases') + + dispatch(resetLoadedRedisCloud(LoadedCloud.Instances)) + }, [instances]) + + useEffect(() => { + if (ssoFlowRef.current !== OAuthSocialAction.Import) return + + if (!userOAuthProfile) { + dispatch(resetDataRedisCloud()) + history.push(Pages.home) + return + } + + if (currentAccountIdRef.current !== userOAuthProfile?.id) { + dispatch( + fetchSubscriptionsRedisCloud(null, true, () => { + history.push(Pages.redisCloudSubscriptions) + }), + ) + } + }, [userOAuthProfile]) + + useEffect(() => { + if (instancesAdded.length) { + history.push(Pages.redisCloudDatabasesResult) + } + }, [instancesAdded]) + + const sendCancelEvent = useCallback(() => { + sendEventTelemetry({ + event: + TelemetryEvent.CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_CANCELLED, + }) + }, []) + + const handleClose = useCallback(() => { + sendCancelEvent() + dispatch(resetDataRedisCloud()) + history.push(Pages.home) + }, [dispatch, history, sendCancelEvent]) + + const handleBackAdding = useCallback(() => { + sendCancelEvent() + dispatch(resetLoadedRedisCloud(LoadedCloud.Instances)) + history.push(Pages.home) + }, [dispatch, history, sendCancelEvent]) + + const handleAddInstances = useCallback( + ( + databases: Pick< + InstanceRedisCloud, + 'subscriptionId' | 'databaseId' | 'free' + >[], + ) => { + dispatch( + addInstancesRedisCloud( + { databases, credentials }, + ssoFlow === OAuthSocialAction.Import, + ), + ) + }, + [dispatch, credentials, ssoFlow], + ) + + const columns = useMemo(() => colFactory(instances || []), [instances]) + + return { + columns, + selection, + instances, + loading, + handleClose, + handleBackAdding, + handleAddInstances, + handleSelectionChange, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.types.ts new file mode 100644 index 0000000000..85dcfb39f2 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.types.ts @@ -0,0 +1,16 @@ +import { InstanceRedisCloud } from 'uiSrc/slices/interfaces' +import { ColumnDef, RowSelectionState } from 'uiSrc/components/base/layout/table' + +export interface UseCloudDatabasesConfigReturn { + columns: ColumnDef[] + selection: InstanceRedisCloud[] + instances: InstanceRedisCloud[] | null + loading: boolean + handleClose: () => void + handleBackAdding: () => void + handleAddInstances: ( + databases: Pick[] + ) => void + handleSelectionChange: (currentSelected: RowSelectionState) => void +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/useCloudDatabasesConfig.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/useCloudDatabasesConfig.tsx deleted file mode 100644 index 7abb9553a9..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/useCloudDatabasesConfig.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' -import { - addInstancesRedisCloud, - cloudSelector, - fetchSubscriptionsRedisCloud, - resetDataRedisCloud, - resetLoadedRedisCloud, -} from 'uiSrc/slices/instances/cloud' -import { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud' -import React, { useEffect, useRef, useState } from 'react' -import { - formatLongName, - parseInstanceOptionsCloud, - replaceSpaces, - setTitle, -} from 'uiSrc/utils' -import { Pages } from 'uiSrc/constants' -import { - InstanceRedisCloud, - LoadedCloud, - OAuthSocialAction, - RedisCloudSubscriptionTypeText, -} from 'uiSrc/slices/interfaces' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { ColumnDefinition } from 'uiSrc/components/base/layout/table' -import { - DatabaseListModules, - DatabaseListOptions, - RiTooltip, -} from 'uiSrc/components' -import styles from 'uiSrc/pages/autodiscover-cloud/redis-cloud-databases/styles.module.scss' -import { IconButton } from 'uiSrc/components/base/forms/buttons' -import { CopyIcon } from 'uiSrc/components/base/icons' -import { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils' -import { - CellText, - CopyPublicEndpointText, - CopyTextContainer, -} from 'uiSrc/components/auto-discover' - -export const useCloudDatabasesConfig = () => { - const dispatch = useDispatch() - const history = useHistory() - - const { - ssoFlow, - credentials, - data: instances, - dataAdded: instancesAdded, - } = useSelector(cloudSelector) - const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector) - const currentAccountIdRef = useRef(userOAuthProfile?.id) - const ssoFlowRef = useRef(ssoFlow) - const [selection, setSelection] = useState([]) - const onSelectionChange = (selected: InstanceRedisCloud) => - setSelection((previous) => { - const isSelected = previous.some( - (item) => item.databaseId === selected.databaseId, - ) - if (isSelected) { - return previous.filter( - (item) => item.databaseId !== selected.databaseId, - ) - } - return [...previous, selected] - }) - setTitle('Redis Cloud Databases') - - useEffect(() => { - if (instances === null) { - history.push(Pages.home) - } - - dispatch(resetLoadedRedisCloud(LoadedCloud.Instances)) - }, []) - - useEffect(() => { - if (ssoFlowRef.current !== OAuthSocialAction.Import) return - - if (!userOAuthProfile) { - dispatch(resetDataRedisCloud()) - history.push(Pages.home) - return - } - - if (currentAccountIdRef.current !== userOAuthProfile?.id) { - dispatch( - fetchSubscriptionsRedisCloud(null, true, () => { - history.push(Pages.redisCloudSubscriptions) - }), - ) - } - }, [userOAuthProfile]) - - useEffect(() => { - if (instancesAdded.length) { - history.push(Pages.redisCloudDatabasesResult) - } - }, [instancesAdded]) - - const sendCancelEvent = () => { - sendEventTelemetry({ - event: - TelemetryEvent.CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_CANCELLED, - }) - } - - const handleClose = () => { - sendCancelEvent() - dispatch(resetDataRedisCloud()) - history.push(Pages.home) - } - - const handleBackAdding = () => { - sendCancelEvent() - dispatch(resetLoadedRedisCloud(LoadedCloud.Instances)) - history.push(Pages.home) - } - - const handleAddInstances = ( - databases: Pick< - InstanceRedisCloud, - 'subscriptionId' | 'databaseId' | 'free' - >[], - ) => { - dispatch( - addInstancesRedisCloud( - { databases, credentials }, - ssoFlow === OAuthSocialAction.Import, - ), - ) - } - - const handleCopy = (text = '') => { - navigator.clipboard.writeText(text) - } - - const columns: ColumnDefinition[] = [ - getSelectionColumn({ - setSelection, - onSelectionChange, - }), - { - header: 'Database', - id: 'name', - accessorKey: 'name', - enableSorting: true, - size: 195, - cell: ({ - row: { - original: { name }, - }, - }) => { - const cellContent = replaceSpaces(name.substring(0, 200)) - return ( -
- - {cellContent} - -
- ) - }, - }, - { - header: 'Subscription ID', - id: 'subscriptionId', - accessorKey: 'subscriptionId', - enableSorting: true, - size: 170, - cell: ({ - row: { - original: { subscriptionId }, - }, - }) => ( - - {subscriptionId} - - ), - }, - { - header: 'Subscription', - id: 'subscriptionName', - accessorKey: 'subscriptionName', - enableSorting: true, - size: 300, - cell: ({ - row: { - original: { subscriptionName: name }, - }, - }) => { - const cellContent = replaceSpaces(name.substring(0, 200)) - return ( -
- - {cellContent} - -
- ) - }, - }, - { - header: 'Type', - id: 'subscriptionType', - accessorKey: 'subscriptionType', - enableSorting: true, - size: 95, - cell: ({ - row: { - original: { subscriptionType }, - }, - }) => ( - - {RedisCloudSubscriptionTypeText[subscriptionType!] ?? '-'} - - ), - }, - { - header: 'Status', - id: 'status', - accessorKey: 'status', - enableSorting: true, - size: 110, - cell: ({ - row: { - original: { status }, - }, - }) => {status}, - }, - { - header: 'Endpoint', - id: 'publicEndpoint', - accessorKey: 'publicEndpoint', - enableSorting: true, - size: 310, - cell: ({ - row: { - original: { publicEndpoint }, - }, - }) => { - const text = publicEndpoint - return ( - - {text} - - handleCopy(text)} - /> - - - ) - }, - }, - { - header: 'Capabilities', - id: 'modules', - accessorKey: 'modules', - enableSorting: true, - size: 200, - cell: function Modules({ row: { original: instance } }) { - return ( - ({ name }))} - /> - ) - }, - }, - { - header: 'Options', - id: 'options', - accessorKey: 'options', - enableSorting: true, - size: 180, - cell: ({ row: { original: instance } }) => { - const options = parseInstanceOptionsCloud( - instance.databaseId, - instances || [], - ) - return - }, - }, - ] - - return { - columns, - selection, - handleClose, - handleBackAdding, - handleAddInstances, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/utils/colFactory.spec.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/utils/colFactory.spec.ts new file mode 100644 index 0000000000..e9657562c1 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/utils/colFactory.spec.ts @@ -0,0 +1,66 @@ +import { InstanceRedisCloud } from 'uiSrc/slices/interfaces' +import { colFactory } from './colFactory' + +describe('colFactory', () => { + it('should return columns without selection column when instances array is empty', () => { + const instances: InstanceRedisCloud[] = [] + + const columns = colFactory(instances) + + expect(columns).toHaveLength(8) + expect(columns[0].id).toBe('name') + }) + + it('should return columns with selection column when instances array has items', () => { + const instances: InstanceRedisCloud[] = [ + { + databaseId: 1, + name: 'test-db', + subscriptionId: 1, + subscriptionType: 1, + free: false, + } as any, + ] + + const columns = colFactory(instances) + + expect(columns).toHaveLength(9) + expect(columns[0].id).toBe('row-selection') + expect(columns[1].id).toBe('name') + }) + + it('should include all required column definitions in correct order', () => { + const instances: InstanceRedisCloud[] = [ + { databaseId: 1, name: 'test-db' } as any, + ] + + const columns = colFactory(instances) + + const columnIds = columns.map((col) => col.id) + + expect(columnIds).toEqual([ + 'row-selection', + 'name', + 'subscriptionId', + 'subscriptionName', + 'subscriptionType', + 'status', + 'publicEndpoint', + 'modules', + 'options', + ]) + }) + + it('should pass instances to optionsColumn', () => { + const instances: InstanceRedisCloud[] = [ + { databaseId: 1, name: 'test-db' } as any, + { databaseId: 2, name: 'test-db-2' } as any, + ] + + const columns = colFactory(instances) + + // The options column should be the last one + const optionsColumn = columns[columns.length - 1] + expect(optionsColumn.id).toBe('options') + }) +}) diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/utils/colFactory.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/utils/colFactory.ts new file mode 100644 index 0000000000..036f7356c1 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/utils/colFactory.ts @@ -0,0 +1,33 @@ +import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils' +import { + databaseColumn, + endpointColumn, + modulesColumn, + optionsColumn, + statusDbColumn, + subscriptionDbColumn, + subscriptionIdColumn, + subscriptionTypeColumn, +} from '../../column-definitions' + +export const colFactory = (instances: InstanceRedisCloud[]): ColumnDef[] => { + const columns: ColumnDef[] = [ + databaseColumn(), + subscriptionIdColumn(), + subscriptionDbColumn(), + subscriptionTypeColumn(), + statusDbColumn(), + endpointColumn(), + modulesColumn(), + optionsColumn(instances), + ] + + if (instances.length) { + return [getSelectionColumn(), ...columns] + } + + return columns +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.stories.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.stories.tsx new file mode 100644 index 0000000000..e4dec407d1 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.stories.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react' +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' + +import RedisCloudSubscriptions from './RedisCloudSubscriptions' +import { colFactory } from '../utils/colFactory' +import { + RedisCloudAccount, + RedisCloudSubscription, +} from 'uiSrc/slices/interfaces' +import { RowSelectionState } from 'uiSrc/components/base/layout/table' +import { RedisCloudSubscriptionFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudSubscription.factory' +import { RedisCloudAccountFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudAccount.factory' + +const subscriptionsMock: RedisCloudSubscription[] = + RedisCloudSubscriptionFactory.buildList(3) +const subscriptions100: RedisCloudSubscription[] = + RedisCloudSubscriptionFactory.buildList(100) + +const emptyColumns = colFactory([]) + +const accountMock = RedisCloudAccountFactory.build() +const meta: Meta = { + component: RedisCloudSubscriptions, + args: { + columns: emptyColumns, + subscriptions: [], + selection: [], + account: null, + loading: false, + onSubmit: () => {}, + }, +} + +export default meta + +type Story = StoryObj + +export const Empty: Story = {} + +const RenderStory = ({ + account, + columns, + subscriptions, +}: { + account: RedisCloudAccount + columns: ReturnType + subscriptions: RedisCloudSubscription[] +}) => { + const [selection, setSelection] = useState([]) + + const handleSelectionChange = (currentSelected: RowSelectionState) => { + const newSelection = subscriptions.filter((item) => { + const { id } = item + if (!id) { + return false + } + return currentSelected[id] + }) + setSelection(newSelection) + } + + return ( + + ) +} + +export const WithSubscription: Story = { + render: () => ( + + ), +} + +export const With100Subscriptions: Story = { + render: () => ( + + ), +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.styles.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.styles.ts index 805ff8fb6b..d75d3bec80 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.styles.ts +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.styles.ts @@ -1,37 +1,5 @@ import styled from 'styled-components' -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' -import { Theme } from 'uiSrc/components/base/theme/types' import { Group, Item } from 'uiSrc/components/base/layout/list' -import { ColorText } from 'uiSrc/components/base/text' - -export const AccountItem = styled(FlexItem).attrs({ - grow: false, - direction: 'row', - padding: 3, -})` - align-items: center; -` - -export const AccountItemTitle = styled(ColorText).attrs({ - size: 'XS', - color: 'secondary', -})` - text-wrap: nowrap; -` -export const AccountWrapper = styled(Row).attrs({ - justify: 'start', - gap: 'xxl', - align: 'center', -})` - width: 100%; - border-radius: 0.8rem; - min-height: 44px; - padding-left: ${({ theme }: { theme: Theme }) => theme.core.space.space150}; - border: 1px solid - ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral500}; - background-color: ${({ theme }: { theme: Theme }) => - theme.semantic.color.background.neutral500}; -` export const AlertStatusDot = styled.span` &::before { diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx index 28ba35616c..c6e240eb0d 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx @@ -1,43 +1,35 @@ -import React, { useState, useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { map } from 'lodash' import { - InstanceRedisCloud, - RedisCloudAccount, - RedisCloudSubscription, + type InstanceRedisCloud, + type RedisCloudAccount, + type RedisCloudSubscription, RedisCloudSubscriptionStatus, } from 'uiSrc/slices/interfaces' -import { Maybe, Nullable } from 'uiSrc/utils' -import { LoadingContent, Spacer } from 'uiSrc/components/base/layout' +import { type Maybe, type Nullable } from 'uiSrc/utils' +import { Spacer } from 'uiSrc/components/base/layout' import MessageBar from 'uiSrc/components/message-bar/MessageBar' -import validationErrors from 'uiSrc/constants/validationErrors' +import { riToast } from 'uiSrc/components/base/display/toast' import { AutodiscoveryPageTemplate } from 'uiSrc/templates' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' - -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' -import { - DestructiveButton, - PrimaryButton, - SecondaryButton, -} from 'uiSrc/components/base/forms/buttons' -import { InfoIcon } from 'uiSrc/components/base/icons' -import { SearchInput } from 'uiSrc/components/base/inputs' -import { ColorText, Text } from 'uiSrc/components/base/text' -import { RiPopover, RiTooltip } from 'uiSrc/components/base' -import styles from '../styles.module.scss' import { - AccountItem, - AccountItemTitle, - AccountWrapper, -} from './RedisCloudSubscriptions.styles' + type ColumnDef, + type RowSelectionState, + Table, +} from 'uiSrc/components/base/layout/table' + +import { Row } from 'uiSrc/components/base/layout/flex' import { + DatabaseContainer, DatabaseWrapper, + EmptyState, Footer, - PageTitle, - SearchForm, + Header, } from 'uiSrc/components/auto-discover' +import { canSelectRow } from '../utils/canSelectRow' +import { Account, CancelButton, SubmitButton, SummaryText } from '../components' export interface Props { - columns: ColumnDefinition[] + columns: ColumnDef[] subscriptions: Nullable selection: Nullable loading: boolean @@ -50,10 +42,7 @@ export interface Props { Pick >[], ) => void -} - -interface IPopoverProps { - isPopoverOpen: boolean + onSelectionChange: (state: RowSelectionState) => void } const loadingMsg = 'loading...' @@ -69,6 +58,7 @@ const RedisCloudSubscriptions = ({ onClose, onBack, onSubmit, + onSelectionChange, }: Props) => { // const subscriptions = []; const [items, setItems] = useState(subscriptions || []) @@ -125,140 +115,27 @@ const RedisCloudSubscriptions = ({ setItems(itemsTemp) } - const CancelButton = ({ isPopoverOpen: popoverIsOpen }: IPopoverProps) => ( - - Cancel - - } - > - - Your changes have not been saved. Do you want to proceed to - the list of databases? - -
-
- - Proceed - -
-
- ) - - const SubmitButton = ({ isDisabled }: { isDisabled: boolean }) => ( - {validationErrors.NO_SUBSCRIPTIONS_CLOUD} - ) : null - } - > - - Show databases - - - ) - - const SummaryText = () => ( - - Summary: - {countStatusActive ? ( - - Successfully discovered database(s) in {countStatusActive} -   - {countStatusActive > 1 ? 'subscriptions' : 'subscription'} - .  - - ) : null} - - {countStatusFailed ? ( - - Failed to discover database(s) in {countStatusFailed} -   - {countStatusFailed > 1 ? 'subscriptions.' : ' subscription.'} - - ) : null} - - ) - - const Account = () => ( - <> - - Account ID:  - - - - Name:  - - - - Owner Name:  - - - - Owner Email:  - - - - ) return ( -
- Redis Cloud Subscriptions - - - - - - - - -
- + +
+ - - - - + {account && ( + <> + + + + )}
`${row.id}`} columns={columns} data={items} defaultSorting={[ @@ -267,31 +144,40 @@ const RedisCloudSubscriptions = ({ desc: false, }, ]} - paginationEnabled + paginationEnabled={items.length > 10} stripedRows - pageSizes={[5, 10, 25, 50, 100]} + emptyState={() => } /> - {!items.length && ( - {message} - )} - 0}> - + 0} + variant={ + !!countStatusFailed + ? riToast.Variant.Attention + : riToast.Variant.Success + } + > + - +
- - - Back to adding databases - - - - + + + +
@@ -299,24 +185,4 @@ const RedisCloudSubscriptions = ({ ) } -const AccountValue = ({ - value, - ...rest -}: { - value?: Nullable -}) => { - if (!value) { - return ( -
- -
- ) - } - return ( - - {value} - - ) -} - export default RedisCloudSubscriptions diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx index dd24120e4e..1d8657c9b9 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx @@ -1,6 +1,7 @@ import React from 'react' + import RedisCloudSubscriptions from './RedisCloudSubscriptions/RedisCloudSubscriptions' -import { useCloudSubscriptionConfig } from './useCloudSubscriptionConfig' +import { useCloudSubscriptionConfig } from './hooks/useCloudSubscriptionConfig' const RedisCloudSubscriptionsPage = () => { const { @@ -14,6 +15,7 @@ const RedisCloudSubscriptionsPage = () => { handleClose, handleBackAdding, handleLoadInstances, + handleSelectionChange, } = useCloudSubscriptionConfig() return ( @@ -27,6 +29,7 @@ const RedisCloudSubscriptionsPage = () => { onClose={handleClose} onBack={handleBackAdding} onSubmit={handleLoadInstances} + onSelectionChange={handleSelectionChange} /> ) } diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.style.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.style.ts new file mode 100644 index 0000000000..70d76dede1 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.style.ts @@ -0,0 +1,46 @@ +import styled from 'styled-components' +import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { type Theme } from 'uiSrc/components/base/theme/types' +import { ColorText } from 'uiSrc/components/base/text' + +export const AccountWrapper = styled(Row).attrs({ + justify: 'start', + gap: 'l', + align: 'center', +})` + align-self: stretch; + width: 100%; + border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space100}; + min-height: 44px; + padding: ${({ theme }: { theme: Theme }) => theme.core.space.space200}; + background-color: ${({ theme }: { theme: Theme }) => + theme.semantic.color.background.neutral200}; +` + +export const AccountItem = styled(FlexItem).attrs({ + grow: false, + direction: 'row', +})` + align-items: center; + gap: ${({ theme }: { theme: Theme }) => theme.core.space.space100}; + &:not(:last-child):after { + content: ''; + margin-left: ${({ theme }: { theme: Theme }) => theme.core.space.space100}; + border-right: 1px solid + ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral400}; + height: ${({ theme }: { theme: Theme }) => theme.core.space.space200}; + } +` + +export const AccountItemTitle = styled(ColorText).attrs({ + size: 'M', +})` + color: ${({ theme }: { theme: Theme }) => + theme.components.typography.colors.secondary}; + white-space: nowrap; +` + +export const LoadingWrapper = styled.div` + width: 80px; + height: 15px; +` diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.tsx new file mode 100644 index 0000000000..cfc8ca40ae --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import { LoadingContent } from 'uiSrc/components/base/layout' +import { ColorText } from 'uiSrc/components/base/text' + +import * as S from './Account.style' +import { type AccountProps, type AccountValueProps } from './Account.types' + +const AccountValue = ({ value, ...rest }: AccountValueProps) => { + if (!value) { + return ( + + + + ) + } + + return ( + + {value} + + ) +} + +export const Account = ({ + account: { accountId, accountName, ownerEmail, ownerName }, +}: AccountProps) => ( + + {accountId && ( + + Account ID: + + + )} + {accountName && ( + + Name: + + + )} + {ownerName && ( + + Owner Name: + + + )} + {ownerEmail && ( + + Owner Email: + + + )} + +) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.types.ts new file mode 100644 index 0000000000..4bc5b66984 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.types.ts @@ -0,0 +1,12 @@ +import { type RedisCloudAccount } from 'uiSrc/slices/interfaces' +import { type Nullable } from 'uiSrc/utils' + +export interface AccountProps { + account: RedisCloudAccount +} + +export interface AccountValueProps { + value?: Nullable + 'data-testid'?: string +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/CancelButton/CancelButton.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/CancelButton/CancelButton.tsx new file mode 100644 index 0000000000..4b3a782e8c --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/CancelButton/CancelButton.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { SecondaryButton, DestructiveButton } from 'uiSrc/components/base/forms/buttons' +import { Text } from 'uiSrc/components/base/text' +import { RiPopover } from 'uiSrc/components/base' + +import styles from '../../styles.module.scss' +import { type CancelButtonProps } from './CancelButton.types' + +export const CancelButton = ({ + isPopoverOpen, + onClose, + onShowPopover, + onClosePopover, +}: CancelButtonProps) => ( + + Cancel + + } + > + + Your changes have not been saved. Do you want to proceed to + the list of databases? + +
+
+ + Proceed + +
+
+) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/CancelButton/CancelButton.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/CancelButton/CancelButton.types.ts new file mode 100644 index 0000000000..b1649bb5d0 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/CancelButton/CancelButton.types.ts @@ -0,0 +1,7 @@ +export interface CancelButtonProps { + isPopoverOpen: boolean + onClose: () => void + onShowPopover: () => void + onClosePopover: () => void +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SubmitButton/SubmitButton.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SubmitButton/SubmitButton.tsx new file mode 100644 index 0000000000..88b741961a --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SubmitButton/SubmitButton.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' +import { RiTooltip } from 'uiSrc/components/base' +import validationErrors from 'uiSrc/constants/validationErrors' + +import { type SubmitButtonProps } from './SubmitButton.types' + +export const SubmitButton = ({ + isDisabled, + loading, + onClick, +}: SubmitButtonProps) => ( + {validationErrors.NO_SUBSCRIPTIONS_CLOUD} + ) : null + } + > + + Show databases + + +) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SubmitButton/SubmitButton.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SubmitButton/SubmitButton.types.ts new file mode 100644 index 0000000000..190d1a0b78 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SubmitButton/SubmitButton.types.ts @@ -0,0 +1,6 @@ +export interface SubmitButtonProps { + isDisabled: boolean + loading: boolean + onClick: () => void +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SummaryText/SummaryText.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SummaryText/SummaryText.tsx new file mode 100644 index 0000000000..f71e44ea54 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SummaryText/SummaryText.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { ColorText, Text } from 'uiSrc/components/base/text' + +import { type SummaryTextProps } from './SummaryText.types' + +export const SummaryText = ({ + countStatusActive, + countStatusFailed, +}: SummaryTextProps) => ( + + Summary: + {countStatusActive ? ( + + Successfully discovered database(s) in {countStatusActive} +   + {countStatusActive > 1 ? 'subscriptions' : 'subscription'} + .  + + ) : null} + + {countStatusFailed ? ( + + Failed to discover database(s) in {countStatusFailed} +   + {countStatusFailed > 1 ? 'subscriptions.' : ' subscription.'} + + ) : null} + +) + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SummaryText/SummaryText.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SummaryText/SummaryText.types.ts new file mode 100644 index 0000000000..abbc551c6e --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SummaryText/SummaryText.types.ts @@ -0,0 +1,5 @@ +export interface SummaryTextProps { + countStatusActive: number + countStatusFailed: number +} + diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/index.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/index.ts new file mode 100644 index 0000000000..9d91ef01f0 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/index.ts @@ -0,0 +1,4 @@ +export { Account } from './Account/Account' +export { CancelButton } from './CancelButton/CancelButton' +export { SubmitButton } from './SubmitButton/SubmitButton' +export { SummaryText } from './SummaryText/SummaryText' diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.test.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.test.ts new file mode 100644 index 0000000000..9657357626 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.test.ts @@ -0,0 +1,205 @@ +import { cloneDeep } from 'lodash' + +import { + act, + initialStateDefault, + mockStore, + renderHook, +} from 'uiSrc/utils/test-utils' +import { + OAuthSocialAction, + RedisCloudSubscriptionStatus, + RedisCloudSubscriptionType, +} from 'uiSrc/slices/interfaces' +import { + resetDataRedisCloud, + resetLoadedRedisCloud, +} from 'uiSrc/slices/instances/cloud' + +import { useCloudSubscriptionConfig } from './useCloudSubscriptionConfig' + +describe('useCloudSubscriptionConfig', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return correct initial state with subscriptions', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [ + { + id: 1, + name: 'sub1', + status: RedisCloudSubscriptionStatus.Active, + numberOfDatabases: 5, + } as any, + ] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + expect(result.current.columns).toHaveLength(9) + expect(result.current.columns[0].id).toBe('row-selection') + expect(result.current.subscriptions).toHaveLength(1) + expect(result.current.selection).toEqual([]) + expect(result.current.loading).toBe(false) + }) + + it('should redirect to home when subscriptions is null', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = null + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + // The redirect happens in useEffect, check that it would be triggered + // expect(mockPush).toHaveBeenCalledWith(Pages.home) + expect(result.current.subscriptions).toBeNull() + }) + + it('should handle selection changes correctly', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [ + { + id: 1, + name: 'sub1', + status: RedisCloudSubscriptionStatus.Active, + numberOfDatabases: 5, + } as any, + { + id: 2, + name: 'sub2', + status: RedisCloudSubscriptionStatus.Active, + numberOfDatabases: 3, + } as any, + ] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + act(() => { + result.current.handleSelectionChange({ 1: true, 2: false }) + }) + + expect(result.current.selection).toHaveLength(1) + expect(result.current.selection[0].id).toBe(1) + }) + + it('should dispatch resetDataRedisCloud on close', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [{ id: 1 } as any] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + act(() => { + result.current.handleClose() + }) + + const actions = store.getActions() + expect(actions.map((a) => a.type)).toContain(resetDataRedisCloud.type) + }) + + it('should dispatch resetLoadedRedisCloud on back', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [{ id: 1 } as any] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + act(() => { + result.current.handleBackAdding() + }) + + const actions = store.getActions() + expect(actions.map((a) => a.type)).toContain(resetLoadedRedisCloud.type) + }) + + it('should call handleLoadInstances with correct parameters', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [{ id: 1 } as any] + state.connections.cloud.credentials = { + accessKey: 'test-key', + secretKey: 'test-secret', + } + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + const subscriptionsToLoad = [ + { + subscriptionId: 1, + subscriptionType: RedisCloudSubscriptionType.Flexible, + free: false, + }, + ] + + // Just check that the handler exists and can be called without error + expect(result.current.handleLoadInstances).toBeDefined() + expect(() => { + act(() => { + result.current.handleLoadInstances(subscriptionsToLoad) + }) + }).not.toThrow() + }) + + it('should check instances loaded state', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [{ id: 1 } as any] + state.connections.cloud.loaded.instances = true + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + // The navigation happens in useEffect based on instancesLoaded + expect(result.current).toBeDefined() + }) + + it('should handle SSO flow correctly', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [{ id: 1 } as any] + state.connections.cloud.ssoFlow = OAuthSocialAction.Import + state.oauth.cloud.user.data = { id: 123 } + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { + store, + }) + + // Check that hook returns expected values for SSO flow + expect(result.current.subscriptions).toHaveLength(1) + }) + + it('should filter out items without id when handling selection', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [ + { id: 1, name: 'sub1' } as any, + { id: undefined, name: 'sub2' } as any, + { id: 3, name: 'sub3' } as any, + ] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + act(() => { + result.current.handleSelectionChange({ + 1: true, + undefined: true, + 3: true, + }) + }) + + expect(result.current.selection).toHaveLength(2) + expect(result.current.selection.map((s) => s.id)).toEqual([1, 3]) + }) + + it('should return 7 columns when subscriptions array is empty', () => { + const state = cloneDeep(initialStateDefault) + state.connections.cloud.subscriptions = [] + const store = mockStore(state) + + const { result } = renderHook(() => useCloudSubscriptionConfig(), { store }) + + expect(result.current.columns).toHaveLength(7) + expect(result.current.columns[0].id).toBe('id') + }) +}) diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.ts new file mode 100644 index 0000000000..c9965afc55 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.ts @@ -0,0 +1,144 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { + InstanceRedisCloud, + LoadedCloud, + OAuthSocialAction, + RedisCloudSubscription, +} from 'uiSrc/slices/interfaces' +import { RowSelectionState } from 'uiSrc/components/base/layout/table' +import { + cloudSelector, + fetchInstancesRedisCloud, + fetchSubscriptionsRedisCloud, + resetDataRedisCloud, + resetLoadedRedisCloud, +} from 'uiSrc/slices/instances/cloud' +import { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud' +import { Maybe, setTitle } from 'uiSrc/utils' +import { Pages } from 'uiSrc/constants' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +import { colFactory } from '../utils/colFactory' +import { UseCloudSubscriptionConfigReturn } from './useCloudSubscriptionConfig.types' + +export const useCloudSubscriptionConfig = + (): UseCloudSubscriptionConfigReturn => { + const dispatch = useDispatch() + const history = useHistory() + + const { + ssoFlow, + credentials, + subscriptions, + loading, + error: subscriptionsError, + loaded: { instances: instancesLoaded }, + account: { error: accountError, data: account }, + } = useSelector(cloudSelector) + const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector) + const currentAccountIdRef = useRef(userOAuthProfile?.id) + const ssoFlowRef = useRef(ssoFlow) + + const [selection, setSelection] = useState([]) + + useEffect(() => { + if (subscriptions === null) { + history.push(Pages.home) + } else { + setTitle('Redis Cloud Subscriptions') + } + }, []) + + useEffect(() => { + if (ssoFlowRef.current !== OAuthSocialAction.Import) return + + if (!userOAuthProfile) { + history.push(Pages.home) + return + } + + if (currentAccountIdRef.current !== userOAuthProfile?.id) { + dispatch(fetchSubscriptionsRedisCloud(null, true)) + currentAccountIdRef.current = userOAuthProfile?.id + } + }, [userOAuthProfile]) + + useEffect(() => { + if (instancesLoaded) { + history.push(Pages.redisCloudDatabases) + } + }, [instancesLoaded]) + + const sendCancelEvent = useCallback(() => { + sendEventTelemetry({ + event: + TelemetryEvent.CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_CANCELLED, + }) + }, []) + + const handleClose = useCallback(() => { + sendCancelEvent() + dispatch(resetDataRedisCloud()) + history.push(Pages.home) + }, [dispatch, history, sendCancelEvent]) + + const handleBackAdding = useCallback(() => { + sendCancelEvent() + dispatch(resetLoadedRedisCloud(LoadedCloud.Subscriptions)) + history.push(Pages.home) + }, [dispatch, history, sendCancelEvent]) + + const handleLoadInstances = useCallback( + ( + subscriptions: Maybe< + Pick< + InstanceRedisCloud, + 'subscriptionId' | 'subscriptionType' | 'free' + > + >[], + ) => { + dispatch( + fetchInstancesRedisCloud( + { subscriptions, credentials }, + ssoFlow === OAuthSocialAction.Import, + ), + ) + }, + [dispatch, credentials, ssoFlow], + ) + + const handleSelectionChange = useCallback( + (currentSelected: RowSelectionState) => { + const newSelection = subscriptions?.filter(({ id }) => { + if (!id) { + return false + } + return currentSelected[id] + }) + setSelection(newSelection || []) + }, + [subscriptions], + ) + + const columns = useMemo( + () => colFactory(subscriptions || []), + [subscriptions], + ) + + return { + columns, + selection, + loading, + account, + subscriptions, + subscriptionsError, + accountError, + handleClose, + handleBackAdding, + handleLoadInstances, + handleSelectionChange, + } + } diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.types.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.types.ts new file mode 100644 index 0000000000..aacdc77f26 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.types.ts @@ -0,0 +1,28 @@ +import { + ColumnDef, + RowSelectionState, +} from 'uiSrc/components/base/layout/table' +import { + InstanceRedisCloud, + RedisCloudAccount, + RedisCloudSubscription, +} from 'uiSrc/slices/interfaces' +import { Maybe } from 'uiSrc/utils' + +export interface UseCloudSubscriptionConfigReturn { + columns: ColumnDef[] + selection: RedisCloudSubscription[] + loading: boolean + account: RedisCloudAccount | null + subscriptions: RedisCloudSubscription[] | null + subscriptionsError: string + accountError: string + handleClose: () => void + handleBackAdding: () => void + handleLoadInstances: ( + subscriptions: Maybe< + Pick + >[], + ) => void + handleSelectionChange: (currentSelected: RowSelectionState) => void +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/useCloudSubscriptionConfig.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/useCloudSubscriptionConfig.tsx deleted file mode 100644 index 85c771b0c0..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/useCloudSubscriptionConfig.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' -import { isNumber } from 'lodash' - -import { - InstanceRedisCloud, - LoadedCloud, - OAuthSocialAction, - RedisCloudSubscription, - RedisCloudSubscriptionStatus, - RedisCloudSubscriptionStatusText, - RedisCloudSubscriptionTypeText, -} from 'uiSrc/slices/interfaces' -import { - ColumnDefinition, - RowDefinition, -} from 'uiSrc/components/base/layout/table' -import { - cloudSelector, - fetchInstancesRedisCloud, - fetchSubscriptionsRedisCloud, - resetDataRedisCloud, - resetLoadedRedisCloud, -} from 'uiSrc/slices/instances/cloud' -import { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud' -import { formatLongName, Maybe, replaceSpaces, setTitle } from 'uiSrc/utils' -import { Pages } from 'uiSrc/constants' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import styles from 'uiSrc/pages/autodiscover-cloud/redis-cloud-subscriptions/styles.module.scss' -import { - AlertStatusDot, - AlertStatusList, - AlertStatusListItem, -} from 'uiSrc/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.styles' -import { RiTooltip } from 'uiSrc/components' -import { IconButton } from 'uiSrc/components/base/forms/buttons' -import { ToastDangerIcon } from 'uiSrc/components/base/icons' -import { Text } from 'uiSrc/components/base/text' -import { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils' - -function canSelectRow(row: RowDefinition) { - return ( - row.original.status === RedisCloudSubscriptionStatus.Active && - row.original.numberOfDatabases !== 0 && - row.getCanSelect() - ) -} - -export const useCloudSubscriptionConfig = () => { - const dispatch = useDispatch() - const history = useHistory() - - const { - ssoFlow, - credentials, - subscriptions, - loading, - error: subscriptionsError, - loaded: { instances: instancesLoaded }, - account: { error: accountError, data: account }, - } = useSelector(cloudSelector) - const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector) - const currentAccountIdRef = useRef(userOAuthProfile?.id) - const ssoFlowRef = useRef(ssoFlow) - - setTitle('Redis Cloud Subscriptions') - - useEffect(() => { - if (subscriptions === null) { - history.push(Pages.home) - } - }, []) - - useEffect(() => { - if (ssoFlowRef.current !== OAuthSocialAction.Import) return - - if (!userOAuthProfile) { - history.push(Pages.home) - return - } - - if (currentAccountIdRef.current !== userOAuthProfile?.id) { - dispatch(fetchSubscriptionsRedisCloud(null, true)) - currentAccountIdRef.current = userOAuthProfile?.id - } - }, [userOAuthProfile]) - - useEffect(() => { - if (instancesLoaded) { - history.push(Pages.redisCloudDatabases) - } - }, [instancesLoaded]) - - const sendCancelEvent = () => { - sendEventTelemetry({ - event: - TelemetryEvent.CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_CANCELLED, - }) - } - - const handleClose = () => { - sendCancelEvent() - dispatch(resetDataRedisCloud()) - history.push(Pages.home) - } - - const handleBackAdding = () => { - sendCancelEvent() - dispatch(resetLoadedRedisCloud(LoadedCloud.Subscriptions)) - history.push(Pages.home) - } - - const handleLoadInstances = ( - subscriptions: Maybe< - Pick - >[], - ) => { - dispatch( - fetchInstancesRedisCloud( - { subscriptions, credentials }, - ssoFlow === OAuthSocialAction.Import, - ), - ) - } - - const AlertStatusContent = () => ( - - } - /> - } - /> - } - /> - - ) - const [selection, setSelection] = useState([]) - const onSelectionChange = (selected: RedisCloudSubscription) => - setSelection((previous) => { - const canSelect = - selected.status === RedisCloudSubscriptionStatus.Active && - selected.numberOfDatabases !== 0 - - if (!canSelect) { - return previous - } - - const isSelected = previous.some( - (item) => item.id === selected.id && item.type === selected.type, - ) - if (isSelected) { - return previous.filter( - (item) => !(item.id === selected.id && item.type === selected.type), - ) - } - return [...previous, selected] - }) - - const columns: ColumnDefinition[] = [ - getSelectionColumn({ - setSelection, - onSelectionChange, - canSelectRow, - }), - { - id: 'alert', - accessorKey: 'alert', - header: '', - enableResizing: false, - enableSorting: false, - size: 50, - cell: ({ - row: { - original: { status, numberOfDatabases }, - }, - }) => - status !== RedisCloudSubscriptionStatus.Active || - numberOfDatabases === 0 ? ( - - This subscription is not available for one of the following - reasons: - - } - content={} - position="right" - className={styles.tooltipStatus} - > - - - ) : null, - }, - { - id: 'id', - accessorKey: 'id', - header: 'Id', - enableSorting: true, - size: 80, - cell: ({ - row: { - original: { id }, - }, - }) => {id}, - }, - { - id: 'name', - accessorKey: 'name', - header: 'Subscription', - enableSorting: true, - cell: function InstanceCell({ - row: { - original: { name }, - }, - }) { - const cellContent = replaceSpaces(name.substring(0, 200)) - return ( -
- - {cellContent} - -
- ) - }, - }, - { - id: 'type', - accessorKey: 'type', - header: 'Type', - enableSorting: true, - cell: ({ - row: { - original: { type }, - }, - }) => RedisCloudSubscriptionTypeText[type] ?? '-', - }, - { - id: 'provider', - accessorKey: 'provider', - header: 'Cloud provider', - enableSorting: true, - cell: ({ - row: { - original: { provider }, - }, - }) => provider ?? '-', - }, - { - id: 'region', - accessorKey: 'region', - header: 'Region', - enableSorting: true, - cell: ({ - row: { - original: { region }, - }, - }) => region ?? '-', - }, - { - id: 'numberOfDatabases', - accessorKey: 'numberOfDatabases', - header: '# databases', - enableSorting: true, - cell: ({ - row: { - original: { numberOfDatabases }, - }, - }) => (isNumber(numberOfDatabases) ? numberOfDatabases : '-'), - }, - { - id: 'status', - accessorKey: 'status', - header: 'Status', - enableSorting: true, - cell: ({ - row: { - original: { status }, - }, - }) => RedisCloudSubscriptionStatusText[status] ?? '-', - }, - ] - - return { - columns, - selection, - loading, - account, - subscriptions, - subscriptionsError, - accountError, - handleClose, - handleBackAdding, - handleLoadInstances, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/canSelectRow.test.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/canSelectRow.test.ts new file mode 100644 index 0000000000..c489127bc1 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/canSelectRow.test.ts @@ -0,0 +1,55 @@ +import { + RedisCloudSubscription, + RedisCloudSubscriptionStatus, +} from 'uiSrc/slices/interfaces' +import { canSelectRow } from './canSelectRow' + +describe('canSelectRow', () => { + it('should return true when subscription is active and has databases', () => { + const row = { + original: { + id: 1, + status: RedisCloudSubscriptionStatus.Active, + numberOfDatabases: 5, + } as RedisCloudSubscription, + } as any + + expect(canSelectRow(row)).toBe(true) + }) + + it('should return false when subscription is not active', () => { + const row = { + original: { + id: 1, + status: RedisCloudSubscriptionStatus.Deleting, + numberOfDatabases: 5, + } as RedisCloudSubscription, + } as any + + expect(canSelectRow(row)).toBe(false) + }) + + it('should return false when subscription has no databases', () => { + const row = { + original: { + id: 1, + status: RedisCloudSubscriptionStatus.Active, + numberOfDatabases: 0, + } as RedisCloudSubscription, + } as any + + expect(canSelectRow(row)).toBe(false) + }) + + it('should return false when subscription is not active and has no databases', () => { + const row = { + original: { + id: 1, + status: RedisCloudSubscriptionStatus.Error, + numberOfDatabases: 0, + } as RedisCloudSubscription, + } as any + + expect(canSelectRow(row)).toBe(false) + }) +}) diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/canSelectRow.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/canSelectRow.ts new file mode 100644 index 0000000000..c1894ccc0d --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/canSelectRow.ts @@ -0,0 +1,14 @@ +import { Row } from 'uiSrc/components/base/layout/table' +import { + RedisCloudSubscription, + RedisCloudSubscriptionStatus, +} from 'uiSrc/slices/interfaces' + +export function canSelectRow({ + original, +}: Row): boolean { + return ( + original.status === RedisCloudSubscriptionStatus.Active && + original.numberOfDatabases !== 0 + ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/colFactory.spec.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/colFactory.spec.ts new file mode 100644 index 0000000000..f9577195e1 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/colFactory.spec.ts @@ -0,0 +1,47 @@ +import { RedisCloudSubscription } from 'uiSrc/slices/interfaces' +import { colFactory } from './colFactory' + +describe('colFactory', () => { + it('should return columns without selection column when items array is empty', () => { + const items: RedisCloudSubscription[] = [] + const columns = colFactory(items) + + expect(columns).toHaveLength(7) + expect(columns[0].id).toBe('id') + expect(columns[1].id).toBe('name') + }) + + it('should return columns with selection column when items array has data', () => { + const items: RedisCloudSubscription[] = [ + { + id: 1, + name: 'test-subscription', + } as any, + ] + const columns = colFactory(items) + + expect(columns).toHaveLength(9) + expect(columns[0].id).toBe('row-selection') + expect(columns[1].id).toBe('alert') + expect(columns[2].id).toBe('id') + }) + + it('should include all required column definitions in correct order', () => { + const items: RedisCloudSubscription[] = [{ id: 1, name: 'test-sub' } as any] + const columns = colFactory(items) + + const columnIds = columns.map((col) => col.id) + + expect(columnIds).toEqual([ + 'row-selection', + 'alert', + 'id', + 'name', + 'type', + 'provider', + 'region', + 'numberOfDatabases', + 'status', + ]) + }) +}) diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/colFactory.ts b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/colFactory.ts new file mode 100644 index 0000000000..247e4df3aa --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/colFactory.ts @@ -0,0 +1,33 @@ +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces' + +import { + alertColumn, + idColumn, + numberOfDbsColumn, + providerColumn, + regionColumn, + selectionColumn, + statusColumn, + subscriptionColumn, + typeColumn, +} from 'uiSrc/pages/autodiscover-cloud/column-definitions' + +export const colFactory = ( + items: RedisCloudSubscription[], +): ColumnDef[] => { + const cols: ColumnDef[] = [ + idColumn(), + subscriptionColumn(), + typeColumn(), + providerColumn(), + regionColumn(), + numberOfDbsColumn(), + statusColumn(), + ] + if (items.length > 0) { + cols.unshift(alertColumn()) + cols.unshift(selectionColumn()) + } + return cols +} diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/utils.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/utils.tsx index e189d132be..75bc1d440b 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/utils.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/utils.tsx @@ -15,6 +15,7 @@ export const getSelectionColumn = ({ }: Props = {}): ColumnDef => { return { id, + maxSize: size, size, isHeaderCustom: true, header: ({ table }) => ( diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/SentinelDatabasesResult.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/SentinelDatabasesResult.tsx index ef18c4a7a6..fc5371fd38 100644 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/SentinelDatabasesResult.tsx +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/SentinelDatabasesResult.tsx @@ -2,23 +2,25 @@ import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { sentinelSelector } from 'uiSrc/slices/instances/sentinel' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' +import { type ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' import MessageBar from 'uiSrc/components/message-bar/MessageBar' +import { riToast } from 'uiSrc/components/base/display/toast' import { AutodiscoveryPageTemplate } from 'uiSrc/templates' -import { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { Col, Row } from 'uiSrc/components/base/layout/flex' import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' import { Text } from 'uiSrc/components/base/text' -import { ColumnDef, Table } from 'uiSrc/components/base/layout/table' +import { type ColumnDef, Table } from 'uiSrc/components/base/layout/table' import { DatabaseContainer, DatabaseWrapper, + EmptyState, Footer, } from 'uiSrc/components/auto-discover' import { Spacer } from 'uiSrc/components/base/layout' import { Header } from 'uiSrc/components/auto-discover/Header' -import { SummaryText } from './Summary' +import { SummaryText } from './components/Summary' export interface Props { countSuccessAdded: number @@ -96,25 +98,21 @@ const SentinelDatabasesResult = ({ rowSelectionMode={undefined} columns={columns} data={items} - defaultSorting={[ - { - id: 'message', - desc: false, - }, - ]} - emptyState={() => ( -
- - {message} - - - )} + defaultSorting={[{ id: 'message', desc: false }]} + emptyState={() => } stripedRows paginationEnabled={items?.length > 10} /> )} - + ( +}: SummaryTextProps) => ( Summary:  diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/components/SummaryTextProps.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/components/SummaryTextProps.types.ts new file mode 100644 index 0000000000..872a561da4 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/components/SummaryTextProps.types.ts @@ -0,0 +1,4 @@ +export type SummaryTextProps = { + countSuccessAdded: number + countFailAdded: number +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/AddressColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/AddressColumn.tsx deleted file mode 100644 index 68f93cafad..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/AddressColumn.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react' -import { - CopyTextContainer, - CopyPublicEndpointText, - CopyBtn, -} from 'uiSrc/components/auto-discover' -import { RiTooltip } from 'uiSrc/components' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -const handleCopy = (text = '') => { - navigator.clipboard.writeText(text) -} - -export const AddressColumn = (): ColumnDef => { - return { - header: 'Address', - id: 'host', - accessorKey: 'host', - enableSorting: true, - cell: ({ - row: { - original: { host, port }, - }, - }) => { - const text = `${host}:${port}` - return ( - - - {text} - - - handleCopy(text)} - tabIndex={-1} - /> - - - ) - }, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/AliasColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/AliasColumn.tsx deleted file mode 100644 index 62e1438717..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/AliasColumn.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react' -import { CellText } from 'uiSrc/components/auto-discover' -import { InputFieldSentinel } from 'uiSrc/components' -import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { - ModifiedSentinelMaster, - AddRedisDatabaseStatus, -} from 'uiSrc/slices/interfaces' - -export const AliasColumn = ( - handleChangedInput: (name: string, value: string) => void, - errorNotAuth: ( - error?: string | object | null, - status?: AddRedisDatabaseStatus, - ) => boolean, -): ColumnDef => { - return { - header: 'Database Alias*', - id: 'alias', - accessorKey: 'alias', - enableSorting: true, - cell: ({ - row: { - original: { id, alias, error, loading = false, status }, - }, - }) => { - if (errorNotAuth(error, status)) { - return {alias} - } - return ( - - ) - }, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/DbColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/DbColumn.tsx deleted file mode 100644 index b211d3d027..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/DbColumn.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react' -import { InputFieldSentinel } from 'uiSrc/components' -import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { - ModifiedSentinelMaster, - AddRedisDatabaseStatus, -} from 'uiSrc/slices/interfaces' -import { ApiStatusCode } from 'uiSrc/constants' - -export const DbColumn = ( - handleChangedInput: (name: string, value: string) => void, -): ColumnDef => { - return { - header: 'Database Index', - id: 'db', - accessorKey: 'db', - size: 140, - cell: ({ - row: { - original: { db, id, loading = false, status, error }, - }, - }) => { - if (status === AddRedisDatabaseStatus.Success) { - return db || not assigned - } - const isDBInvalid = - typeof error === 'object' && - error !== null && - 'statusCode' in error && - error.statusCode === ApiStatusCode.BadRequest - return ( -
- -
- ) - }, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/NumberOfReplicasColumn.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/NumberOfReplicasColumn.ts deleted file mode 100644 index dd911758e9..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/NumberOfReplicasColumn.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -export const NumberOfReplicasColumn = (): ColumnDef => { - return { - header: '# of replicas', - id: 'numberOfSlaves', - accessorKey: 'numberOfSlaves', - enableSorting: true, - maxSize: 120, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/PasswordColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/PasswordColumn.tsx deleted file mode 100644 index f9901b1af2..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/PasswordColumn.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react' -import { InputFieldSentinel } from 'uiSrc/components' -import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { - ModifiedSentinelMaster, - AddRedisDatabaseStatus, -} from 'uiSrc/slices/interfaces' - -export const PasswordColumn = ( - handleChangedInput: (name: string, value: string) => void, - isInvalid: boolean, - errorNotAuth: ( - error?: string | object | null, - status?: AddRedisDatabaseStatus, - ) => boolean, -): ColumnDef => { - return { - header: 'Password', - id: 'password', - accessorKey: 'password', - cell: ({ - row: { - original: { password, id, error, loading = false, status }, - }, - }) => { - if ( - errorNotAuth(error, status) || - status === AddRedisDatabaseStatus.Success - ) { - return password ? '************' : not assigned - } - return ( -
- -
- ) - }, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/PrimaryGroupColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/PrimaryGroupColumn.tsx deleted file mode 100644 index 9f24910dd3..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/PrimaryGroupColumn.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { CellText } from 'uiSrc/components/auto-discover' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -export const PrimaryGroupColumn = (): ColumnDef => { - return { - header: 'Primary Group', - id: 'name', - accessorKey: 'name', - enableSorting: true, - maxSize: 200, - cell: ({ - row: { - original: { name }, - }, - }) => {name}, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/ResultColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/ResultColumn.tsx deleted file mode 100644 index 5aa6454271..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/ResultColumn.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react' - -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' -import { Loader } from 'uiSrc/components/base/display' -import { - AddRedisDatabaseStatus, - ModifiedSentinelMaster, -} from 'uiSrc/slices/interfaces' -import { CellText } from 'uiSrc/components/auto-discover' -import { RiTooltip } from 'uiSrc/components' -import { ColorText } from 'uiSrc/components/base/text' -import { Spacer } from 'uiSrc/components/base/layout' -import { InfoIcon, RiIcon } from 'uiSrc/components/base/icons' -import { ColumnDef } from 'uiSrc/components/base/layout/table' -import { ApiStatusCode } from 'uiSrc/constants' -import { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors' -import validationErrors from 'uiSrc/constants/validationErrors' -import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' - -const addError = ( - { name, error, alias, loading }: ModifiedSentinelMaster, - onAddInstance: (name: string) => void = () => {}, -) => { - const isDisabled = !alias - if ( - typeof error === 'object' && - error !== null && - 'statusCode' in error && - error?.statusCode !== ApiStatusCode.Unauthorized && - !ApiEncryptionErrors.includes(error?.name || '') && - error?.statusCode !== ApiStatusCode.BadRequest - ) { - return '' - } - return ( - - Database Alias : null} - > - onAddInstance(name)} - icon={isDisabled ? InfoIcon : undefined} - > - Add Primary Group - - - - ) -} - -export const ResultColumn = ( - addActions?: boolean, - onAddInstance?: (name: string) => void, -): ColumnDef => { - return { - header: 'Result', - id: 'message', - accessorKey: 'message', - enableSorting: true, - minSize: addActions ? 250 : 110, - cell: ({ - row: { - original: { status, message, name, error, alias, loading = false }, - }, - }) => { - return ( - - {loading && } - {!loading && status === AddRedisDatabaseStatus.Success && ( - {message} - )} - {!loading && status !== AddRedisDatabaseStatus.Success && ( - - - - Error - - - - - - )} - {addActions && - addError({ name, error, alias, loading }, onAddInstance)} - - ) - }, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/UsernameColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/UsernameColumn.tsx deleted file mode 100644 index f835aa11d2..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/UsernameColumn.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import { InputFieldSentinel } from 'uiSrc/components' -import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { - ModifiedSentinelMaster, - AddRedisDatabaseStatus, -} from 'uiSrc/slices/interfaces' - -export const UsernameColumn = ( - handleChangedInput: (name: string, value: string) => void, - isInvalid: boolean, - errorNotAuth: ( - error?: string | object | null, - status?: AddRedisDatabaseStatus, - ) => boolean, -): ColumnDef => { - return { - header: 'Username', - id: 'username', - accessorKey: 'username', - cell: ({ - row: { - original: { username, id, loading = false, error, status }, - }, - }) => { - if ( - errorNotAuth(error, status) || - status === AddRedisDatabaseStatus.Success - ) { - return username || Default - } - return ( -
- -
- ) - }, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/address.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/address.tsx new file mode 100644 index 0000000000..7e1d679b2c --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/address.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { AddressCell } from '../components' + +export const addressColumn = (): ColumnDef => { + return { + header: 'Address', + id: 'host', + accessorKey: 'host', + enableSorting: true, + cell: ({ + row: { + original: { host, port }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/alias.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/alias.tsx new file mode 100644 index 0000000000..3a24df4f68 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/alias.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { + ModifiedSentinelMaster, + AddRedisDatabaseStatus, +} from 'uiSrc/slices/interfaces' + +import { AliasCell } from '../components' + +export const aliasColumn = ( + handleChangedInput: (name: string, value: string) => void, + errorNotAuth: ( + error?: string | object | null, + status?: AddRedisDatabaseStatus, + ) => boolean, +): ColumnDef => { + return { + header: 'Database Alias*', + id: 'alias', + accessorKey: 'alias', + enableSorting: true, + cell: ({ + row: { + original: { id, alias, error, loading = false, status }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/db.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/db.tsx new file mode 100644 index 0000000000..10014502e0 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/db.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { DbCell } from '../components' + +export const dbColumn = ( + handleChangedInput: (name: string, value: string) => void, +): ColumnDef => { + return { + header: 'Database Index', + id: 'db', + accessorKey: 'db', + size: 140, + cell: ({ + row: { + original: { db, id, loading = false, status, error }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/numberOfReplicas.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/numberOfReplicas.ts new file mode 100644 index 0000000000..f1d9280560 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/numberOfReplicas.ts @@ -0,0 +1,12 @@ +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +export const numberOfReplicasColumn = (): ColumnDef => { + return { + header: '# of replicas', + id: 'numberOfSlaves', + accessorKey: 'numberOfSlaves', + enableSorting: true, + maxSize: 120, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/password.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/password.tsx new file mode 100644 index 0000000000..1cad5a7484 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/password.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { + ModifiedSentinelMaster, + AddRedisDatabaseStatus, +} from 'uiSrc/slices/interfaces' + +import { PasswordCell } from '../components' + +export const passwordColumn = ( + handleChangedInput: (name: string, value: string) => void, + isInvalid: boolean, + errorNotAuth: ( + error?: string | object | null, + status?: AddRedisDatabaseStatus, + ) => boolean, +): ColumnDef => { + return { + header: 'Password', + id: 'password', + accessorKey: 'password', + cell: ({ + row: { + original: { password, id, error, loading = false, status }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/primaryGroup.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/primaryGroup.tsx new file mode 100644 index 0000000000..9ff7c7bd2e --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/primaryGroup.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { PrimaryGroupCell } from '../components' + +export const primaryGroupColumn = (): ColumnDef => { + return { + header: 'Primary Group', + id: 'name', + accessorKey: 'name', + enableSorting: true, + maxSize: 200, + cell: ({ + row: { + original: { name }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/result.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/result.tsx new file mode 100644 index 0000000000..9d5ad03cda --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/result.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { ResultCell } from '../components' + +export const resultColumn = ( + addActions?: boolean, + onAddInstance?: (name: string) => void, +): ColumnDef => { + return { + header: 'Result', + id: 'message', + accessorKey: 'message', + enableSorting: true, + minSize: addActions ? 250 : 110, + cell: ({ + row: { + original: { status, message, name, error, alias, loading = false }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/username.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/username.tsx new file mode 100644 index 0000000000..822165a415 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/username.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { + ModifiedSentinelMaster, + AddRedisDatabaseStatus, +} from 'uiSrc/slices/interfaces' + +import { UsernameCell } from '../components' + +export const usernameColumn = ( + handleChangedInput: (name: string, value: string) => void, + isInvalid: boolean, + errorNotAuth: ( + error?: string | object | null, + status?: AddRedisDatabaseStatus, + ) => boolean, +): ColumnDef => { + return { + header: 'Username', + id: 'username', + accessorKey: 'username', + cell: ({ + row: { + original: { username, id, loading = false, error, status }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddErrorButton/AddErrorButton.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddErrorButton/AddErrorButton.tsx new file mode 100644 index 0000000000..e1aab4ee33 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddErrorButton/AddErrorButton.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { FlexItem } from 'uiSrc/components/base/layout/flex' +import { RiTooltip } from 'uiSrc/components' +import { ApiStatusCode } from 'uiSrc/constants' +import { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors' +import validationErrors from 'uiSrc/constants/validationErrors' +import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' +import { InfoIcon } from 'uiSrc/components/base/icons' + +import type { AddErrorButtonProps } from './AddErrorButton.types' + +export const AddErrorButton = ({ + name, + error, + alias, + loading = false, + onAddInstance = () => {}, +}: AddErrorButtonProps) => { + const isDisabled = !alias + if ( + typeof error === 'object' && + error !== null && + 'statusCode' in error && + error?.statusCode !== ApiStatusCode.Unauthorized && + !ApiEncryptionErrors.includes(error?.name || '') && + error?.statusCode !== ApiStatusCode.BadRequest + ) { + return null + } + return ( + + Database Alias : null} + > + onAddInstance(name)} + icon={isDisabled ? InfoIcon : undefined} + > + Add Primary Group + + + + ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddErrorButton/AddErrorButton.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddErrorButton/AddErrorButton.types.ts new file mode 100644 index 0000000000..4085d48e4f --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddErrorButton/AddErrorButton.types.ts @@ -0,0 +1,13 @@ +interface ErrorWithStatusCode { + statusCode?: number + name?: string + [key: string]: any +} + +export interface AddErrorButtonProps { + name: string + error?: string | ErrorWithStatusCode | null + alias?: string + loading?: boolean + onAddInstance?: (name: string) => void +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddressCell/AddressCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddressCell/AddressCell.tsx new file mode 100644 index 0000000000..b16712e404 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddressCell/AddressCell.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { + CopyTextContainer, + CopyPublicEndpointText, + CopyBtn, +} from 'uiSrc/components/auto-discover' +import { RiTooltip } from 'uiSrc/components' +import { handleCopy } from 'uiSrc/utils' + +import type { AddressCellProps } from './AddressCell.types' + +export const AddressCell = ({ host = '', port = '' }: AddressCellProps) => { + if (!host || !port) { + return null + } + + const text = `${host}:${port}` + return ( + + + {text} + + + handleCopy(text)} + tabIndex={-1} + /> + + + ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddressCell/AddressCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddressCell/AddressCell.types.ts new file mode 100644 index 0000000000..9b94eba763 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddressCell/AddressCell.types.ts @@ -0,0 +1,4 @@ +export interface AddressCellProps { + host?: string + port?: string +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AliasCell/AliasCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AliasCell/AliasCell.tsx new file mode 100644 index 0000000000..cb0da36e65 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AliasCell/AliasCell.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { CellText } from 'uiSrc/components/auto-discover' +import { InputFieldSentinel } from 'uiSrc/components' +import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' + +import type { AliasCellProps } from './AliasCell.types' + +export const AliasCell = ({ + id = '', + alias, + error, + loading = false, + status, + handleChangedInput, + errorNotAuth, +}: AliasCellProps) => { + if (errorNotAuth(error, status)) { + return {alias} + } + return ( + + ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AliasCell/AliasCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AliasCell/AliasCell.types.ts new file mode 100644 index 0000000000..56058007a1 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AliasCell/AliasCell.types.ts @@ -0,0 +1,14 @@ +import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +export interface AliasCellProps { + id?: string + alias?: string + error?: string | object | null + loading?: boolean + status?: AddRedisDatabaseStatus + handleChangedInput: (name: string, value: string) => void + errorNotAuth: ( + error?: string | object | null, + status?: AddRedisDatabaseStatus, + ) => boolean +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/DbCell/DbCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/DbCell/DbCell.tsx new file mode 100644 index 0000000000..e0b0b75993 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/DbCell/DbCell.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { InputFieldSentinel } from 'uiSrc/components' +import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' +import { ApiStatusCode } from 'uiSrc/constants' +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +import type { DbCellProps } from './DbCell.types' + +export const DbCell = ({ + db, + id = '', + loading = false, + status, + error, + handleChangedInput, +}: DbCellProps) => { + if (status === AddRedisDatabaseStatus.Success) { + return db !== undefined ? {db} : not assigned + } + const isDBInvalid = + typeof error === 'object' && + error !== null && + 'statusCode' in error && + error.statusCode === ApiStatusCode.BadRequest + return ( +
+ +
+ ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/DbCell/DbCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/DbCell/DbCell.types.ts new file mode 100644 index 0000000000..c57554b97a --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/DbCell/DbCell.types.ts @@ -0,0 +1,10 @@ +import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +export interface DbCellProps { + db?: number + id?: string + loading?: boolean + status?: AddRedisDatabaseStatus + error?: string | object | null + handleChangedInput: (name: string, value: string) => void +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PasswordCell/PasswordCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PasswordCell/PasswordCell.tsx new file mode 100644 index 0000000000..548b3b8009 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PasswordCell/PasswordCell.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { InputFieldSentinel } from 'uiSrc/components' +import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +import type { PasswordCellProps } from './PasswordCell.types' + +export const PasswordCell = ({ + password = '', + id = '', + error, + loading = false, + status, + handleChangedInput, + isInvalid, + errorNotAuth, +}: PasswordCellProps) => { + if ( + errorNotAuth(error, status) || + status === AddRedisDatabaseStatus.Success + ) { + return password ? ************ : not assigned + } + return ( +
+ +
+ ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PasswordCell/PasswordCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PasswordCell/PasswordCell.types.ts new file mode 100644 index 0000000000..e9725654ad --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PasswordCell/PasswordCell.types.ts @@ -0,0 +1,15 @@ +import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +export interface PasswordCellProps { + password?: string + id?: string + error?: string | object | null + loading?: boolean + status?: AddRedisDatabaseStatus + handleChangedInput: (name: string, value: string) => void + isInvalid: boolean + errorNotAuth: ( + error?: string | object | null, + status?: AddRedisDatabaseStatus, + ) => boolean +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.tsx new file mode 100644 index 0000000000..3ef0a2c646 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { CellText } from 'uiSrc/components/auto-discover' + +import type { PrimaryGroupCellProps } from './PrimaryGroupCell.types' + +export const PrimaryGroupCell = ({ name }: PrimaryGroupCellProps) => ( + {name} +) diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.types.ts new file mode 100644 index 0000000000..9bb7481055 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.types.ts @@ -0,0 +1,3 @@ +export interface PrimaryGroupCellProps { + name: string +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/ResultCell/ResultCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/ResultCell/ResultCell.tsx new file mode 100644 index 0000000000..49cb5a01ed --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/ResultCell/ResultCell.tsx @@ -0,0 +1,58 @@ +import React from 'react' + +import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { Loader } from 'uiSrc/components/base/display' +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' +import { CellText } from 'uiSrc/components/auto-discover' +import { RiTooltip } from 'uiSrc/components' +import { ColorText } from 'uiSrc/components/base/text' +import { Spacer } from 'uiSrc/components/base/layout' +import { RiIcon } from 'uiSrc/components/base/icons' + +import { AddErrorButton } from '../AddErrorButton/AddErrorButton' +import type { ResultCellProps } from './ResultCell.types' + +export const ResultCell = ({ + status, + message = '', + name, + error, + alias, + loading = false, + addActions, + onAddInstance, +}: ResultCellProps) => { + return ( + + {loading && } + {!loading && status === AddRedisDatabaseStatus.Success && ( + {message} + )} + {!loading && status !== AddRedisDatabaseStatus.Success && ( + + + + Error + + + + + + )} + {addActions && onAddInstance && ( + + )} + + ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/ResultCell/ResultCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/ResultCell/ResultCell.types.ts new file mode 100644 index 0000000000..74cc5137bb --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/ResultCell/ResultCell.types.ts @@ -0,0 +1,12 @@ +import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +export interface ResultCellProps { + status?: AddRedisDatabaseStatus + message?: string + name: string + error?: string | object | null + alias?: string + loading?: boolean + addActions?: boolean + onAddInstance?: (name: string) => void +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/UsernameCell/UsernameCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/UsernameCell/UsernameCell.tsx new file mode 100644 index 0000000000..d55050dc31 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/UsernameCell/UsernameCell.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { InputFieldSentinel } from 'uiSrc/components' +import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +import type { UsernameCellProps } from './UsernameCell.types' + +export const UsernameCell = ({ + username, + id, + loading = false, + error, + status, + handleChangedInput, + isInvalid, + errorNotAuth, +}: UsernameCellProps) => { + if ( + errorNotAuth(error, status) || + status === AddRedisDatabaseStatus.Success + ) { + return username ? {username} : Default + } + return ( +
+ +
+ ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/UsernameCell/UsernameCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/UsernameCell/UsernameCell.types.ts new file mode 100644 index 0000000000..df3ac8b10a --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/UsernameCell/UsernameCell.types.ts @@ -0,0 +1,15 @@ +import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' + +export interface UsernameCellProps { + username?: string + id?: string + loading?: boolean + error?: string | object | null + status?: AddRedisDatabaseStatus + handleChangedInput: (name: string, value: string) => void + isInvalid: boolean + errorNotAuth: ( + error?: string | object | null, + status?: AddRedisDatabaseStatus, + ) => boolean +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/index.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/index.ts new file mode 100644 index 0000000000..28fca22c2b --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/index.ts @@ -0,0 +1,8 @@ +export { AddErrorButton } from './AddErrorButton/AddErrorButton' +export { AddressCell } from './AddressCell/AddressCell' +export { AliasCell } from './AliasCell/AliasCell' +export { DbCell } from './DbCell/DbCell' +export { PasswordCell } from './PasswordCell/PasswordCell' +export { PrimaryGroupCell } from './PrimaryGroupCell/PrimaryGroupCell' +export { ResultCell } from './ResultCell/ResultCell' +export { UsernameCell } from './UsernameCell/UsernameCell' diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/index.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/index.ts index eef0057052..f832586b91 100644 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/index.ts +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/index.ts @@ -1,8 +1,8 @@ -export * from './ResultColumn' -export * from './PrimaryGroupColumn' -export * from './AliasColumn' -export * from './AddressColumn' -export * from './NumberOfReplicasColumn' -export * from './UsernameColumn' -export * from './PasswordColumn' -export * from './DbColumn' +export * from './columns/result' +export * from './columns/primaryGroup' +export * from './columns/alias' +export * from './columns/address' +export * from './columns/numberOfReplicas' +export * from './columns/username' +export * from './columns/password' +export * from './columns/db' diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/useSentinelDatabasesResultConfig.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/useSentinelDatabasesResultConfig.tsx index a533e7aca4..6739bf7367 100644 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/useSentinelDatabasesResultConfig.tsx +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/useSentinelDatabasesResultConfig.tsx @@ -19,14 +19,14 @@ import { removeEmpty, setTitle } from 'uiSrc/utils' import { pick } from 'lodash' import { ColumnDef } from 'uiSrc/components/base/layout/table' import { - AliasColumn, - DbColumn, - AddressColumn, - NumberOfReplicasColumn, - PasswordColumn, - PrimaryGroupColumn, - ResultColumn, - UsernameColumn, + aliasColumn, + dbColumn, + addressColumn, + numberOfReplicasColumn, + passwordColumn, + primaryGroupColumn, + resultColumn, + usernameColumn, } from './components/column-definitions' // Define an interface for the error object @@ -55,14 +55,14 @@ export const colFactory = ( itemsLength: number, ) => { const cols: ColumnDef[] = [ - ResultColumn(countSuccessAdded !== itemsLength, handleAddInstance), - PrimaryGroupColumn(), - AliasColumn(handleChangedInput, errorNotAuth), - AddressColumn(), - NumberOfReplicasColumn(), - UsernameColumn(handleChangedInput, isInvalid, errorNotAuth), - PasswordColumn(handleChangedInput, isInvalid, errorNotAuth), - DbColumn(handleChangedInput), + resultColumn(countSuccessAdded !== itemsLength, handleAddInstance), + primaryGroupColumn(), + aliasColumn(handleChangedInput, errorNotAuth), + addressColumn(), + numberOfReplicasColumn(), + usernameColumn(handleChangedInput, isInvalid, errorNotAuth), + passwordColumn(handleChangedInput, isInvalid, errorNotAuth), + dbColumn(handleChangedInput), ] return cols diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.stories.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.stories.tsx index 41c79be34e..8c7561348e 100644 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.stories.tsx +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.stories.tsx @@ -1,77 +1,53 @@ import React, { useState } from 'react' import type { Meta, StoryObj } from '@storybook/react-vite' -import { action } from 'storybook/actions' import { expect, fn, screen } from 'storybook/test' +import { SentinelMasterFactory } from 'uiSrc/mocks/factories/sentinel/SentinelMaster.factory' import SentinelDatabases from './SentinelDatabases' -import { - AddRedisDatabaseStatus, - ModifiedSentinelMaster, -} from 'uiSrc/slices/interfaces' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' import { RowSelectionState } from '@redis-ui/table' import { colFactory, getRowId } from '../../useSentinelDatabasesConfig' +const emptyColumnsMock = colFactory([], () => {}) + const meta: Meta = { component: SentinelDatabases, args: { + selection: [], + columns: emptyColumnsMock, + masters: [], onBack: fn(), onClose: fn(), onSubmit: fn(), + onSelectionChange: fn(), }, } export default meta type Story = StoryObj -let mastersMock: ModifiedSentinelMaster[] = [ - { - name: 'mymaster', - status: AddRedisDatabaseStatus.Fail, - message: '', - host: '192.168.0.19', - port: '6379', - numberOfSlaves: 0, - // nodes: [], - id: '1', - alias: 'mymaster', - username: '', - password: '', - db: 1, - }, - { - name: 'mymaster2', - status: AddRedisDatabaseStatus.Success, - message: '', - host: '192.168.0.18', - port: '6380', - numberOfSlaves: 0, - // nodes: [], - id: '2', - alias: 'mymaster2', - username: '', - password: '', - db: 1, - }, - { - name: 'mymaster3', - status: AddRedisDatabaseStatus.Fail, - message: '', - host: '192.168.0.18', - port: '6380', - numberOfSlaves: 0, - alias: 'mymaster3', - username: 'default', - password: 'abcde', - db: 1, - }, -] -let columnsMock = colFactory(mastersMock, () => {}) - const DefaultRender = () => { + const mastersMock: ModifiedSentinelMaster[] = [ + SentinelMasterFactory.build({ + id: '1', + name: 'mymaster', + alias: 'mymaster', + }), + SentinelMasterFactory.build({ + name: 'mymaster2', + alias: 'mymaster2', + }), + SentinelMasterFactory.build({ + name: 'mymaster3', + alias: 'mymaster3', + }), + ] + let columnsMock = colFactory(mastersMock, () => {}) const [rowSelection, setSelection] = useState({}) const selection = Object.keys(rowSelection) .map((key) => mastersMock.find((master) => getRowId(master) === key)) .filter((item): item is ModifiedSentinelMaster => Boolean(item)) + return ( {}) + export const Empty: Story = { - args: { - selection: [], - columns: emptyColumnsMock, - masters: [], - onClose: action('onClose'), - onBack: action('onBack'), - onSubmit: action('onSubmit'), - onSelectionChange: action('on selection change'), - }, play: async ({ canvas }) => { await expect( canvas.getByText('Your Redis Sentinel has no primary groups available'), diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.tsx index d7e1872b3f..642ec3f086 100644 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.tsx +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.tsx @@ -2,35 +2,26 @@ import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { sentinelSelector } from 'uiSrc/slices/instances/sentinel' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' -import validationErrors from 'uiSrc/constants/validationErrors' +import { type ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' import { AutodiscoveryPageTemplate } from 'uiSrc/templates' -import { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { Row } from 'uiSrc/components/base/layout/flex' import { - DestructiveButton, - PrimaryButton, - SecondaryButton, -} from 'uiSrc/components/base/forms/buttons' -import { RiIcon } from 'uiSrc/components/base/icons' -import { Text } from 'uiSrc/components/base/text' -import { RiPopover, RiTooltip } from 'uiSrc/components/base' -import { - ColumnDef, - RowSelectionState, + type ColumnDef, + type RowSelectionState, Table, } from 'uiSrc/components/base/layout/table' +import { Spacer } from 'uiSrc/components/base/layout' import { DatabaseContainer, DatabaseWrapper, + EmptyState, Footer, + Header, } from 'uiSrc/components/auto-discover' -import { Spacer } from 'uiSrc/components/base/layout' -import { Header } from 'uiSrc/components/auto-discover/Header' import { getRowId } from '../../useSentinelDatabasesConfig' - -import styles from '../../../styles.module.scss' +import { CancelButton, SubmitButton, NoMastersMessage } from './components' export interface Props { columns: ColumnDef[] @@ -42,10 +33,6 @@ export interface Props { onSubmit: (databases: ModifiedSentinelMaster[]) => void } -interface IPopoverProps { - isPopoverOpen: boolean -} - const loadingMsg = 'loading...' const notMastersMsg = 'Your Redis Sentinel has no primary groups available.' const notFoundMsg = 'Not found.' @@ -113,74 +100,6 @@ const SentinelDatabases = ({ setItems(itemsTemp) } - const CancelButton = ({ isPopoverOpen: popoverIsOpen }: IPopoverProps) => ( - - Cancel - - } - > - - Your changes have not been saved. Do you want to proceed to - the list of databases? - -
-
- - Proceed - -
-
- ) - - const SubmitButton = ({ onClick }: { onClick: () => void }) => { - let title: string | null = null - let content: string | null = null - const emptyAliases = selection.filter(({ alias }) => !alias) - - if (selection.length < 1) { - title = validationErrors.SELECT_AT_LEAST_ONE('primary group') - content = validationErrors.NO_PRIMARY_GROUPS_SENTINEL - } - - if (emptyAliases.length !== 0) { - title = validationErrors.REQUIRED_TITLE(emptyAliases.length) - content = 'Database Alias' - } - const TooltipIcon = () => ( - {content}}> - - - ) - - return ( - - Add Primary Group - - ) - } - return ( @@ -214,27 +133,28 @@ const SentinelDatabases = ({ desc: false, }, ]} + paginationEnabled={items.length > 10} stripedRows - emptyState={() => ( -
- - {message} - - - )} + emptyState={() => } /> - {!masters.length && ( - - {notMastersMsg} - - )} + {!masters.length && }
- - + +
diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/CancelButton.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/CancelButton.tsx new file mode 100644 index 0000000000..5d2cbe43f7 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/CancelButton.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { + SecondaryButton, + DestructiveButton, +} from 'uiSrc/components/base/forms/buttons' +import { Text } from 'uiSrc/components/base/text' +import { RiPopover } from 'uiSrc/components/base' + +import { type CancelButtonProps } from './CancelButton.types' +import styles from './styles.module.scss' + +export const CancelButton = ({ + isPopoverOpen, + onClose, + onShowPopover, + onClosePopover, +}: CancelButtonProps) => ( + + Cancel + + } + > + + Your changes have not been saved. Do you want to proceed to the + list of databases? + +
+
+ + Proceed + +
+
+) diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/CancelButton.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/CancelButton.types.ts new file mode 100644 index 0000000000..b1649bb5d0 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/CancelButton.types.ts @@ -0,0 +1,7 @@ +export interface CancelButtonProps { + isPopoverOpen: boolean + onClose: () => void + onShowPopover: () => void + onClosePopover: () => void +} + diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/styles.module.scss b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/styles.module.scss new file mode 100644 index 0000000000..329e027887 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/styles.module.scss @@ -0,0 +1,4 @@ +.panelCancelBtn { + max-width: 350px !important; + margin-left: -10px; +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/NoMastersMessage/NoMastersMessage.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/NoMastersMessage/NoMastersMessage.tsx new file mode 100644 index 0000000000..a725cff5d4 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/NoMastersMessage/NoMastersMessage.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +import { Col } from 'uiSrc/components/base/layout/flex' +import { Text } from 'uiSrc/components/base/text' + +import { type NoMastersMessageProps } from './NoMastersMessage.types' + +export const NoMastersMessage = ({ message }: NoMastersMessageProps) => ( +
+ {message} + +) diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/NoMastersMessage/NoMastersMessage.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/NoMastersMessage/NoMastersMessage.types.ts new file mode 100644 index 0000000000..10af6ef1e3 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/NoMastersMessage/NoMastersMessage.types.ts @@ -0,0 +1,4 @@ +export interface NoMastersMessageProps { + message: string +} + diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/SubmitButton/SubmitButton.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/SubmitButton/SubmitButton.tsx new file mode 100644 index 0000000000..8318cf667d --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/SubmitButton/SubmitButton.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' +import { RiIcon } from 'uiSrc/components/base/icons' +import { RiTooltip } from 'uiSrc/components/base' +import validationErrors from 'uiSrc/constants/validationErrors' + +import { type SubmitButtonProps } from './SubmitButton.types' + +const TooltipIcon = ({ title, content }: { title: string | null; content: string | null }) => ( + {content}}> + + +) + +export const SubmitButton = ({ + selection, + loading, + onClick, + isDisabled, +}: SubmitButtonProps) => { + let title: string | null = null + let content: string | null = null + const emptyAliases = selection.filter(({ alias }) => !alias) + + if (selection.length < 1) { + title = validationErrors.SELECT_AT_LEAST_ONE('primary group') + content = validationErrors.NO_PRIMARY_GROUPS_SENTINEL + } + + if (emptyAliases.length !== 0) { + title = validationErrors.REQUIRED_TITLE(emptyAliases.length) + content = 'Database Alias' + } + + return ( + : undefined} + data-testid="btn-add-primary-group" + > + Add Primary Group + + ) +} + diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/SubmitButton/SubmitButton.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/SubmitButton/SubmitButton.types.ts new file mode 100644 index 0000000000..61c2e5e854 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/SubmitButton/SubmitButton.types.ts @@ -0,0 +1,9 @@ +import { type ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +export interface SubmitButtonProps { + selection: ModifiedSentinelMaster[] + loading: boolean + onClick: () => void + isDisabled: boolean +} + diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/index.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/index.ts new file mode 100644 index 0000000000..a05ed0359e --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/index.ts @@ -0,0 +1,4 @@ +export { CancelButton } from './CancelButton/CancelButton' +export { SubmitButton } from './SubmitButton/SubmitButton' +export { NoMastersMessage } from './NoMastersMessage/NoMastersMessage' + diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/AddressColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/AddressColumn.tsx deleted file mode 100644 index 40f96aefce..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/AddressColumn.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react' -import { - CopyTextContainer, - CopyPublicEndpointText, - CopyBtn, -} from 'uiSrc/components/auto-discover' -import { RiTooltip } from 'uiSrc/components' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -const handleCopy = (text = '') => { - return navigator.clipboard.writeText(text) -} - -export const AddressColumn = (): ColumnDef => { - return { - header: 'Address', - id: 'host', - accessorKey: 'host', - enableSorting: true, - cell: ({ - row: { - original: { host, port }, - }, - }) => { - const text = `${host}:${port}` - return ( - - {text} - - handleCopy(text)} - tabIndex={-1} - /> - - - ) - }, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/AliasColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/AliasColumn.tsx deleted file mode 100644 index d99cfd257e..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/AliasColumn.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' -import { InputFieldSentinel } from 'uiSrc/components' -import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -export const AliasColumn = ( - handleChangedInput: (name: string, value: string) => void, -): ColumnDef => { - return { - header: 'Database Alias*', - id: 'alias', - accessorKey: 'alias', - enableSorting: true, - size: 200, - cell: ({ - row: { - original: { id, alias, name }, - }, - }) => ( -
- -
- ), - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/DbIndexColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/DbIndexColumn.tsx deleted file mode 100644 index 815bc98376..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/DbIndexColumn.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import { InputFieldSentinel, RiTooltip } from 'uiSrc/components' -import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' -import { RiIcon } from 'uiSrc/components/base/icons' - -export const DbIndexColumn = ( - handleChangedInput: (name: string, value: string) => void, -): ColumnDef => { - return { - header: 'Database Index', - id: 'db', - accessorKey: 'db', - size: 140, - cell: ({ - row: { - original: { db = 0, id }, - }, - }) => ( -
- - - - } - /> -
- ), - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/NumberOfReplicasColumn.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/NumberOfReplicasColumn.ts deleted file mode 100644 index cab6858056..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/NumberOfReplicasColumn.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -export const NumberOfReplicasColumn = (): ColumnDef => { - return { - header: '# of replicas', - id: 'numberOfSlaves', - accessorKey: 'numberOfSlaves', - enableSorting: true, - size: 120, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/PasswordColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/PasswordColumn.tsx deleted file mode 100644 index 8c9514c83c..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/PasswordColumn.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import { InputFieldSentinel } from 'uiSrc/components' -import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -export const PasswordColumn = ( - handleChangedInput: (name: string, value: string) => void, -): ColumnDef => { - return { - header: 'Password', - id: 'password', - accessorKey: 'password', - cell: ({ - row: { - original: { password, id }, - }, - }) => ( -
- -
- ), - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/PrimaryGroupColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/PrimaryGroupColumn.tsx deleted file mode 100644 index 6a589c8e6e..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/PrimaryGroupColumn.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { CellText } from 'uiSrc/components/auto-discover' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -export const PrimaryGroupColumn = (): ColumnDef => { - return { - header: 'Primary Group', - id: 'name', - accessorKey: 'name', - enableSorting: true, - size: 200, - cell: ({ - row: { - original: { name }, - }, - }) => {name}, - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/UsernameColumn.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/UsernameColumn.tsx deleted file mode 100644 index ea00a18d04..0000000000 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/UsernameColumn.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import { InputFieldSentinel } from 'uiSrc/components' -import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' -import { type ColumnDef } from 'uiSrc/components/base/layout/table' -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' - -export const UsernameColumn = ( - handleChangedInput: (name: string, value: string) => void, -): ColumnDef => { - return { - header: 'Username', - id: 'username', - accessorKey: 'username', - cell: ({ - row: { - original: { username, id }, - }, - }) => ( -
- -
- ), - } -} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/address.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/address.tsx new file mode 100644 index 0000000000..7e1d679b2c --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/address.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { AddressCell } from '../components' + +export const addressColumn = (): ColumnDef => { + return { + header: 'Address', + id: 'host', + accessorKey: 'host', + enableSorting: true, + cell: ({ + row: { + original: { host, port }, + }, + }) => , + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/alias.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/alias.tsx new file mode 100644 index 0000000000..b3bb5e4f4b --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/alias.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { AliasCell } from '../components' + +export const aliasColumn = ( + handleChangedInput: (name: string, value: string) => void, +): ColumnDef => { + return { + header: 'Database Alias*', + id: 'alias', + accessorKey: 'alias', + enableSorting: true, + size: 200, + cell: ({ + row: { + original: { id, alias, name }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/dbIndex.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/dbIndex.tsx new file mode 100644 index 0000000000..3c2c98aba5 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/dbIndex.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { DbIndexCell } from '../components' + +export const dbIndexColumn = ( + handleChangedInput: (name: string, value: string) => void, +): ColumnDef => { + return { + header: 'Database Index', + id: 'db', + accessorKey: 'db', + size: 140, + cell: ({ + row: { + original: { db = 0, id }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/numberOfReplicas.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/numberOfReplicas.ts new file mode 100644 index 0000000000..eadb9cd56b --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/numberOfReplicas.ts @@ -0,0 +1,12 @@ +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +export const numberOfReplicasColumn = (): ColumnDef => { + return { + header: '# of replicas', + id: 'numberOfSlaves', + accessorKey: 'numberOfSlaves', + enableSorting: true, + size: 120, + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/password.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/password.tsx new file mode 100644 index 0000000000..fa29823ef9 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/password.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { PasswordCell } from '../components' + +export const passwordColumn = ( + handleChangedInput: (name: string, value: string) => void, +): ColumnDef => { + return { + header: 'Password', + id: 'password', + accessorKey: 'password', + cell: ({ + row: { + original: { password, id }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/primaryGroup.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/primaryGroup.tsx new file mode 100644 index 0000000000..e9636a5c80 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/primaryGroup.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { PrimaryGroupCell } from '../components' + +export const primaryGroupColumn = (): ColumnDef => { + return { + header: 'Primary Group', + id: 'name', + accessorKey: 'name', + enableSorting: true, + size: 200, + cell: ({ + row: { + original: { name }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/SelectionColumn.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/selection.ts similarity index 54% rename from redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/SelectionColumn.ts rename to redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/selection.ts index 8d18cf02f0..f1797d5ab7 100644 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/SelectionColumn.ts +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/selection.ts @@ -1,6 +1,6 @@ -import { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' import { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils' -export const SelectionColumn = () => { +export const selectionColumn = () => { return getSelectionColumn() } diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/username.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/username.tsx new file mode 100644 index 0000000000..3c2d0b5f8d --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/username.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import type { ColumnDef } from 'uiSrc/components/base/layout/table' +import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces' + +import { UsernameCell } from '../components' + +export const usernameColumn = ( + handleChangedInput: (name: string, value: string) => void, +): ColumnDef => { + return { + header: 'Username', + id: 'username', + accessorKey: 'username', + cell: ({ + row: { + original: { username, id }, + }, + }) => ( + + ), + } +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AddressCell/AddressCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AddressCell/AddressCell.tsx new file mode 100644 index 0000000000..8c9437cc84 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AddressCell/AddressCell.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { + CopyTextContainer, + CopyPublicEndpointText, + CopyBtn, +} from 'uiSrc/components/auto-discover' +import { RiTooltip } from 'uiSrc/components' +import { handleCopy } from 'uiSrc/utils' + +import type { AddressCellProps } from './AddressCell.types' + +export const AddressCell = ({ host, port }: AddressCellProps) => { + if (!host || !port) { + return null + } + + const text = `${host}:${port}` + return ( + + {text} + + handleCopy(text)} + tabIndex={-1} + /> + + + ) +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AddressCell/AddressCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AddressCell/AddressCell.types.ts new file mode 100644 index 0000000000..9b94eba763 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AddressCell/AddressCell.types.ts @@ -0,0 +1,4 @@ +export interface AddressCellProps { + host?: string + port?: string +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AliasCell/AliasCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AliasCell/AliasCell.tsx new file mode 100644 index 0000000000..e3a17f6337 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AliasCell/AliasCell.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { InputFieldSentinel } from 'uiSrc/components' +import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' + +import type { AliasCellProps } from './AliasCell.types' + +export const AliasCell = ({ id, alias, name, handleChangedInput }: AliasCellProps) => ( +
+ +
+) diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AliasCell/AliasCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AliasCell/AliasCell.types.ts new file mode 100644 index 0000000000..63318fb22f --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AliasCell/AliasCell.types.ts @@ -0,0 +1,6 @@ +export interface AliasCellProps { + id?: string + alias?: string + name?: string + handleChangedInput: (name: string, value: string) => void +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/DbIndexCell/DbIndexCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/DbIndexCell/DbIndexCell.tsx new file mode 100644 index 0000000000..c739ea81ab --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/DbIndexCell/DbIndexCell.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { InputFieldSentinel, RiTooltip } from 'uiSrc/components' +import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' +import { RiIcon } from 'uiSrc/components/base/icons' + +import type { DbIndexCellProps } from './DbIndexCell.types' + +export const DbIndexCell = ({ db = 0, id, handleChangedInput }: DbIndexCellProps) => ( +
+ + + + } + /> +
+) diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/DbIndexCell/DbIndexCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/DbIndexCell/DbIndexCell.types.ts new file mode 100644 index 0000000000..2cf5192a04 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/DbIndexCell/DbIndexCell.types.ts @@ -0,0 +1,5 @@ +export interface DbIndexCellProps { + db: number + id: string + handleChangedInput: (name: string, value: string) => void +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PasswordCell/PasswordCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PasswordCell/PasswordCell.tsx new file mode 100644 index 0000000000..72281b3383 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PasswordCell/PasswordCell.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { InputFieldSentinel } from 'uiSrc/components' +import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' + +import type { PasswordCellProps } from './PasswordCell.types' + +export const PasswordCell = ({ password, id, handleChangedInput }: PasswordCellProps) => ( +
+ +
+) diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PasswordCell/PasswordCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PasswordCell/PasswordCell.types.ts new file mode 100644 index 0000000000..61db7ff7e8 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PasswordCell/PasswordCell.types.ts @@ -0,0 +1,5 @@ +export interface PasswordCellProps { + password: string + id: string + handleChangedInput: (name: string, value: string) => void +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.tsx new file mode 100644 index 0000000000..3ef0a2c646 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { CellText } from 'uiSrc/components/auto-discover' + +import type { PrimaryGroupCellProps } from './PrimaryGroupCell.types' + +export const PrimaryGroupCell = ({ name }: PrimaryGroupCellProps) => ( + {name} +) diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.types.ts new file mode 100644 index 0000000000..9bb7481055 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.types.ts @@ -0,0 +1,3 @@ +export interface PrimaryGroupCellProps { + name: string +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/UsernameCell/UsernameCell.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/UsernameCell/UsernameCell.tsx new file mode 100644 index 0000000000..86e9faae11 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/UsernameCell/UsernameCell.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { InputFieldSentinel } from 'uiSrc/components' +import { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel' + +import type { UsernameCellProps } from './UsernameCell.types' + +export const UsernameCell = ({ username, id, handleChangedInput }: UsernameCellProps) => ( +
+ +
+) diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/UsernameCell/UsernameCell.types.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/UsernameCell/UsernameCell.types.ts new file mode 100644 index 0000000000..f7d9f65014 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/UsernameCell/UsernameCell.types.ts @@ -0,0 +1,5 @@ +export interface UsernameCellProps { + username: string + id: string + handleChangedInput: (name: string, value: string) => void +} diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/index.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/index.ts new file mode 100644 index 0000000000..345db77581 --- /dev/null +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/index.ts @@ -0,0 +1,6 @@ +export { AddressCell } from './AddressCell/AddressCell' +export { AliasCell } from './AliasCell/AliasCell' +export { DbIndexCell } from './DbIndexCell/DbIndexCell' +export { PasswordCell } from './PasswordCell/PasswordCell' +export { PrimaryGroupCell } from './PrimaryGroupCell/PrimaryGroupCell' +export { UsernameCell } from './UsernameCell/UsernameCell' diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/index.ts b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/index.ts index 2268197763..7962da0f32 100644 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/index.ts +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/index.ts @@ -1,8 +1,8 @@ -export * from './PrimaryGroupColumn' -export * from './AliasColumn' -export * from './AddressColumn' -export * from './NumberOfReplicasColumn' -export * from './UsernameColumn' -export * from './PasswordColumn' -export * from './DbIndexColumn' -export * from './SelectionColumn' +export * from './columns/primaryGroup' +export * from './columns/alias' +export * from './columns/address' +export * from './columns/numberOfReplicas' +export * from './columns/username' +export * from './columns/password' +export * from './columns/dbIndex' +export * from './columns/selection' diff --git a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/useSentinelDatabasesConfig.tsx b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/useSentinelDatabasesConfig.tsx index c0be6759e6..f69847971f 100644 --- a/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/useSentinelDatabasesConfig.tsx +++ b/redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/useSentinelDatabasesConfig.tsx @@ -20,14 +20,14 @@ import { RowSelectionState, } from 'uiSrc/components/base/layout/table' import { - PrimaryGroupColumn, - AliasColumn, - AddressColumn, - NumberOfReplicasColumn, - UsernameColumn, - PasswordColumn, - DbIndexColumn, - SelectionColumn, + primaryGroupColumn, + aliasColumn, + addressColumn, + numberOfReplicasColumn, + usernameColumn, + passwordColumn, + dbIndexColumn, + selectionColumn, } from './components/column-definitions' const updateSelection = ( @@ -51,16 +51,16 @@ export const colFactory = ( handleChangedInput: (name: string, value: string) => void, ) => { const cols: ColumnDef[] = [ - PrimaryGroupColumn(), - AliasColumn(handleChangedInput), - AddressColumn(), - NumberOfReplicasColumn(), - UsernameColumn(handleChangedInput), - PasswordColumn(handleChangedInput), - DbIndexColumn(handleChangedInput), + primaryGroupColumn(), + aliasColumn(handleChangedInput), + addressColumn(), + numberOfReplicasColumn(), + usernameColumn(handleChangedInput), + passwordColumn(handleChangedInput), + dbIndexColumn(handleChangedInput), ] if (items.length > 0) { - cols.unshift(SelectionColumn()) + cols.unshift(selectionColumn()) } return cols } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.tsx index f4853b6bfb..b57e9016fe 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.tsx @@ -3,6 +3,12 @@ import { isNull } from 'lodash' import React, { useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux' +import { + handleCopy as handleCopyUtil, + formatLongName, + isEqualBuffers, + stringToBuffer, +} from 'uiSrc/utils' import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' import { TEXT_UNPRINTABLE_CHARACTERS } from 'uiSrc/constants' import { AddCommonFieldsFormConfig } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' @@ -19,7 +25,6 @@ import { sendEventTelemetry, TelemetryEvent, } from 'uiSrc/telemetry' -import { formatLongName, isEqualBuffers, stringToBuffer } from 'uiSrc/utils' import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' import { IconButton } from 'uiSrc/components/base/forms/buttons' @@ -123,7 +128,7 @@ const KeyDetailsHeaderName = ({ onEditKey }: Props) => { keyInputIsEditing: boolean, keyNameInputRef: React.RefObject, ) => { - navigator.clipboard.writeText(text) + handleCopyUtil(text) if (keyInputIsEditing) { keyNameInputRef?.current?.focus() diff --git a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.tsx b/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.tsx index ee49d40117..2cd7d5c4ec 100644 --- a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.tsx +++ b/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.tsx @@ -1,10 +1,9 @@ -import { EuiIcon } from '@elastic/eui' import cx from 'classnames' import { map } from 'lodash' import React, { useState } from 'react' import { LoadingContent } from 'uiSrc/components/base/layout' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' +import { Table, ColumnDef } from 'uiSrc/components/base/layout/table' import { formatBytes, Nullable } from 'uiSrc/utils' import { rgb } from 'uiSrc/utils/colors' import { numberWithSpaces } from 'uiSrc/utils/numbers' @@ -21,7 +20,7 @@ const ClusterNodesTable = ({ nodes: Nullable loading: boolean }) => { - const [sort, setSort] = useState({ + const [sort, setSort] = useState({ field: 'host', direction: 'asc', }) @@ -41,7 +40,7 @@ const ClusterNodesTable = ({ ) - const columns: ColumnDefinition[] = [ + const columns: ColumnDef[] = [ { header: `${nodes?.length} Primary nodes`, id: 'host', diff --git a/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.stories.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.stories.tsx new file mode 100644 index 0000000000..47fcf1c65e --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import ClusterConnectionForm from './' + +const meta: Meta = { + component: ClusterConnectionForm, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = {} diff --git a/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx index f73a905815..2569e17f45 100644 --- a/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx @@ -1,12 +1,13 @@ import React, { useEffect, useState } from 'react' import ReactDOM from 'react-dom' import { isEmpty } from 'lodash' -import { FormikErrors, useFormik } from 'formik' +import { FormikErrors, useFormik } from 'formik' import * as keys from 'uiSrc/constants/keys' import { MAX_PORT_NUMBER, validateField } from 'uiSrc/utils/validations' import { handlePasteHostName } from 'uiSrc/utils' import validationErrors from 'uiSrc/constants/validationErrors' + import { ICredentialsRedisCluster } from 'uiSrc/slices/interfaces' import { MessageEnterpriceSoftware } from 'uiSrc/pages/home/components/form/Messages' @@ -59,7 +60,7 @@ const fieldDisplayNames: Values = { host: 'Cluster Host', port: 'Cluster Port', username: 'Admin Username', - // deepcode ignore NoHardcodedPasswords: + // deepcode ignore NoHardcodedPasswords: password: 'Admin Password', } diff --git a/redisinsight/ui/src/pages/home/components/database-panel-dialog/DatabasePanelDialog.stories.tsx b/redisinsight/ui/src/pages/home/components/database-panel-dialog/DatabasePanelDialog.stories.tsx new file mode 100644 index 0000000000..1be4e48431 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/database-panel-dialog/DatabasePanelDialog.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { action } from 'storybook/actions' + +import { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory' +import { ConnectionType } from 'uiSrc/slices/interfaces' +import DatabasePanelDialog from './index' + +const meta = { + component: DatabasePanelDialog, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + editMode: false, + editedInstance: null, + onClose: action('onClose'), + }, +} + +const mockInstance = DBInstanceFactory.build({ + id: '13bd1fb0-0af6-4433-b138-99eba801f3fe', + host: '127.0.0.1', + port: 6666, + name: '127.0.0.1:6666', + connectionType: ConnectionType.Standalone, + provider: 'REDIS_STACK', + lastConnection: new Date('2025-10-17T06:29:06.536Z'), + modules: [ + { name: 'timeseries', version: 11202, semanticVersion: '1.12.2' }, + { name: 'search', version: 21005, semanticVersion: '2.10.5' }, + { name: 'ReJSON', version: 20803, semanticVersion: '2.8.3' }, + { name: 'bf', version: 20802, semanticVersion: '2.8.2' }, + { name: 'redisgears_2', version: 20020, semanticVersion: '2.0.20' }, + ], + version: '7.4.0', +}) + +export const EditModeTrue: Story = { + args: { + editMode: true, + editedInstance: mockInstance, + onClose: action('onClose'), + }, +} diff --git a/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx b/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx index 0dfd5c762c..906601e1bb 100644 --- a/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx @@ -256,6 +256,7 @@ const ManualConnectionWrapper = (props: Props) => { if ( values.selectedCaCertName === ADD_NEW_CA_CERT && values.newCaCertName !== '' && + editedInstance && values.newCaCertName === editedInstance.caCert?.name ) { updatedValues.caCert = database.caCert diff --git a/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.stories.tsx b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.stories.tsx new file mode 100644 index 0000000000..f0b77a6d54 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' + +import { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory' +import { NO_CA_CERT } from 'uiSrc/pages/home/constants' +import ManualConnectionForm from './' + +const conn = DBInstanceFactory.build() +const meta: Meta = { + component: ManualConnectionForm, + args: { + formFields: { + ...conn, + port: conn.port.toString(), + selectedCaCertName: NO_CA_CERT, + } as any, + loading: false, + isEditMode: false, + isCloneMode: false, + setIsCloneMode: fn(), + }, +} + +export default meta + +type Story = StoryObj + +export const AddConnection: Story = {} +export const CloneConnection: Story = { + args: { + ...meta.args, + isCloneMode: true, + }, +} +export const EditConnection: Story = { + args: { + ...meta.args, + isEditMode: true, + }, +} + +export const EditWithNodesConnection: Story = { + args: { + ...meta.args, + formFields: { + ...conn, + port: conn.port.toString(), + selectedCaCertName: NO_CA_CERT, + nodes: [ + { + host: '127.0.0.1', + port: 6666, + }, + { + host: '127.0.0.1', + port: 7777, + }, + ], + } as any, + isEditMode: true, + }, +} diff --git a/redisinsight/ui/src/pages/home/components/styles.module.scss b/redisinsight/ui/src/pages/home/components/styles.module.scss index 0e8380a603..9f174ea9ea 100644 --- a/redisinsight/ui/src/pages/home/components/styles.module.scss +++ b/redisinsight/ui/src/pages/home/components/styles.module.scss @@ -23,9 +23,6 @@ } .anchorEndpoints { - position: absolute; - right: 12px; - top: 5px; pointer-events: auto !important; svg { diff --git a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.stories.tsx b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.stories.tsx new file mode 100644 index 0000000000..36ba6a181a --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' + +import { RedisClusterInstanceFactory } from 'uiSrc/mocks/factories/cluster/RedisClusterInstance.factory' +import RedisClusterDatabases from './RedisClusterDatabases' +import { colFactory } from './useClusterDatabasesConfig' + +const emptyInstances: [] = [] +const mockInstances = RedisClusterInstanceFactory.buildList(5) +const mockManyInstances = RedisClusterInstanceFactory.buildList(15) + +const [emptyColumns] = colFactory(emptyInstances) +const [columnsWithData] = colFactory(mockInstances) +const [columnsWithManyData] = colFactory(mockManyInstances) + +const meta: Meta = { + component: RedisClusterDatabases, + args: { + columns: emptyColumns, + instances: emptyInstances, + loading: false, + onClose: fn(), + onBack: fn(), + onSubmit: fn(), + }, +} + +export default meta + +type Story = StoryObj + +export const Empty: Story = {} + +export const WithDatabases: Story = { + args: { + instances: mockInstances, + columns: columnsWithData, + }, +} + +export const WithManyDatabases: Story = { + args: { + instances: mockManyInstances, + columns: columnsWithManyData, + }, +} + +export const Loading: Story = { + args: { + instances: emptyInstances, + columns: emptyColumns, + loading: true, + }, +} diff --git a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.tsx b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.tsx index ec497e81d4..12510e8389 100644 --- a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.tsx +++ b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.tsx @@ -1,37 +1,35 @@ import React, { useEffect, useState } from 'react' -import cx from 'classnames' -import { map } from 'lodash' -import { useSelector } from 'react-redux' -import { SearchInput } from 'uiSrc/components/base/inputs' -import { Maybe } from 'uiSrc/utils' -import { RiPopover, RiTooltip } from 'uiSrc/components/base' -import { InstanceRedisCluster } from 'uiSrc/slices/interfaces' -import { clusterSelector } from 'uiSrc/slices/instances/cluster' +import type { Maybe } from 'uiSrc/utils' +import { RiTooltip } from 'uiSrc/components/base' +import type { InstanceRedisCluster } from 'uiSrc/slices/interfaces' import validationErrors from 'uiSrc/constants/validationErrors' import { AutodiscoveryPageTemplate } from 'uiSrc/templates' -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { Row } from 'uiSrc/components/base/layout/flex' import { InfoIcon } from 'uiSrc/components/base/icons' +import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' import { - DestructiveButton, - PrimaryButton, - SecondaryButton, -} from 'uiSrc/components/base/forms/buttons' -import { FormField } from 'uiSrc/components/base/forms/FormField' -import { Title } from 'uiSrc/components/base/text/Title' -import { Text } from 'uiSrc/components/base/text' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' -import styles from './styles.module.scss' + type ColumnDef, + type RowSelectionState, + Table, +} from 'uiSrc/components/base/layout/table' +import { + DatabaseContainer, + DatabaseWrapper, + EmptyState, + Footer, + Header, +} from 'uiSrc/components/auto-discover' +import { Spacer } from 'uiSrc/components/base/layout' +import { CancelButton } from './components' interface Props { - columns: ColumnDefinition[] + columns: ColumnDef[] onClose: () => void onBack: () => void onSubmit: (uids: Maybe[]) => void -} - -interface IPopoverProps { - isPopoverOpen: boolean + instances: InstanceRedisCluster[] + loading: boolean } const loadingMsg = 'loading...' @@ -39,19 +37,32 @@ const notFoundMsg = 'Not found' const noResultsMessage = 'Your Redis Enterprise Cluster has no databases available.' +function getSubtitle(items: InstanceRedisCluster[]) { + if (!items.length) { + return null + } + + return `These are the ${items.length > 1 ? 'databases ' : 'database '} +in your Redis Enterprise Cluster. Select the +${items.length > 1 ? ' databases ' : ' database '} that you want +to add.` +} + +const hasSelection = (selection: RowSelectionState) => + Object.values(selection).some(Boolean) const RedisClusterDatabases = ({ columns, onClose, onBack, onSubmit, + instances, + loading, }: Props) => { const [items, setItems] = useState([]) const [message, setMessage] = useState(loadingMsg) const [isPopoverOpen, setIsPopoverOpen] = useState(false) - const [selection, setSelection] = useState([]) - - const { data: instances, loading } = useSelector(clusterSelector) + const [selection, setSelection] = useState({}) useEffect(() => { if (instances !== null) { @@ -66,7 +77,11 @@ const RedisClusterDatabases = ({ }, [instances]) const handleSubmit = () => { - onSubmit(map(selection, 'uid')) + // Map rowSelection state to the selected items list using uid as row id + const selected = Object.entries(selection) + .filter(([_uid, isSelected]) => Boolean(isSelected)) + .map(([uid]) => Number(uid)) + onSubmit(selected) } const showPopover = () => { @@ -77,17 +92,10 @@ const RedisClusterDatabases = ({ setIsPopoverOpen(false) } - const isSubmitDisabled = () => selection.length < 1 - - const selectionValue = { - onSelectionChange: (selected: InstanceRedisCluster) => - setSelection((previous) => { - const isSelected = previous.some((item) => item.uid === selected.uid) - if (isSelected) { - return previous.filter((item) => item.uid !== selected.uid) - } - return [...previous, selected] - }), + const isSubmitDisabled = () => !hasSelection(selection) + + const onSelectionChange = (selection: RowSelectionState) => { + setSelection(selection) } const onQueryChange = (term: string) => { @@ -106,140 +114,66 @@ const RedisClusterDatabases = ({ setItems(itemsTemp) } - const CancelButton = ({ isPopoverOpen: popoverIsOpen }: IPopoverProps) => ( - - Cancel - - } - > - - Your changes have not been saved. Do you want to proceed to - the list of databases? - -
-
- - Proceed - -
-
- ) - return ( -
- - Auto-Discover Redis Enterprise Databases - - - - {!!items.length && ( - - These are the {items.length > 1 ? 'databases ' : 'database '} - in your Redis Enterprise Cluster. Select the - {items.length > 1 ? ' databases ' : ' database '} that you want - to add. - - )} - - - - - - - -
-
+ +
+ +
`${row.uid}`} + onRowSelectionChange={onSelectionChange} + defaultSorting={[{ id: 'name', desc: false }]} + paginationEnabled={items.length > 10} + stripedRows + emptyState={() => } + /> + + +
+ + - {!items.length && ( - {message} - )} - - - - - {validationErrors.NO_DBS_SELECTED} + ) : null + } > - Back to adding databases - - - - - {validationErrors.NO_DBS_SELECTED} - - ) : null - } + - - Add selected Databases - - - + Add selected Databases + + - +
) } diff --git a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.spec.tsx b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.spec.tsx index eaa97c77c1..47beec647f 100644 --- a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.spec.tsx +++ b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.spec.tsx @@ -57,7 +57,7 @@ describe('RedisClusterDatabasesPage', () => { it('should render', () => { ;(clusterSelector as jest.Mock).mockReturnValueOnce({ data: [], - dataAdded: [{}], + dataAdded: [], }) expect(render()).toBeTruthy() diff --git a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx index 1e6e129a1a..2f2090c8b6 100644 --- a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx +++ b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx @@ -1,215 +1,24 @@ import React from 'react' -import { useHistory } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' - -import { Pages } from 'uiSrc/constants' -import { - addInstancesRedisCluster, - clusterSelector, - resetDataRedisCluster, - resetInstancesRedisCluster, -} from 'uiSrc/slices/instances/cluster' -import { - formatLongName, - Maybe, - parseInstanceOptionsCluster, - setTitle, -} from 'uiSrc/utils' -import { - AddRedisDatabaseStatus, - InstanceRedisCluster, -} from 'uiSrc/slices/interfaces' -import { - DatabaseListModules, - DatabaseListOptions, - RiTooltip, -} from 'uiSrc/components' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' -import { IconButton } from 'uiSrc/components/base/forms/buttons' -import { CopyIcon } from 'uiSrc/components/base/icons' -import { ColorText, Text } from 'uiSrc/components/base/text' -import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' -import { ColumnDefinition } from 'uiSrc/components/base/layout/table' import RedisClusterDatabases from './RedisClusterDatabases' import RedisClusterDatabasesResult from './RedisClusterDatabasesResult' - -import styles from './styles.module.scss' +import { useClusterDatabasesConfig } from './useClusterDatabasesConfig' const RedisClusterDatabasesPage = () => { - const dispatch = useDispatch() - const history = useHistory() - const { - credentials, - data: instances, - dataAdded: instancesAdded, - } = useSelector(clusterSelector) - setTitle('Auto-Discover Redis Enterprise Databases') - - const sendCancelEvent = () => { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_CANCELLED, - }) - } - - const handleClose = (sendEvent = true) => { - sendEvent && sendCancelEvent() - dispatch(resetDataRedisCluster()) - history.push(Pages.home) - } - - const handleBackAdding = (sendEvent = true) => { - sendEvent && sendCancelEvent() - dispatch(resetInstancesRedisCluster()) - history.push(Pages.home) - } - - const handleAddInstances = (uids: Maybe[]) => { - dispatch(addInstancesRedisCluster({ uids, credentials })) - } - - const handleCopy = (text = '') => { - navigator.clipboard.writeText(text) - } - - const columns: ColumnDefinition[] = [ - { - header: 'Database', - id: 'name', - accessorKey: 'name', - enableSorting: true, - cell: ({ - row: { - original: { name }, - }, - }) => { - const cellContent = name - .substring(0, 200) - .replace(/\s\s/g, '\u00a0\u00a0') - return ( -
- - {cellContent} - -
- ) - }, - }, - { - header: 'Status', - id: 'status', - accessorKey: 'status', - enableSorting: true, - }, - { - header: 'Endpoint', - id: 'dnsName', - accessorKey: 'dnsName', - enableSorting: true, - cell: ({ - row: { - original: { dnsName, port }, - }, - }) => { - const text = `${dnsName}:${port}` - return ( - !!dnsName && ( -
- {text} - - handleCopy(text)} - /> - -
- ) - ) - }, - }, - { - header: 'Capabilities', - id: 'modules', - accessorKey: 'modules', - enableSorting: true, - cell: function Modules({ row: { original: instance } }) { - return ( - ({ name }))} - /> - ) - }, - }, - { - header: 'Options', - id: 'options', - accessorKey: 'options', - enableSorting: true, - cell: ({ row: { original: instance } }) => { - const options = parseInstanceOptionsCluster( - instance?.uid, - instances || [], - ) - return - }, - }, - ] - - const messageColumn: ColumnDefinition = { - header: 'Result', - id: 'messageAdded', - accessorKey: 'messageAdded', - enableSorting: true, - cell: function Message({ - row: { - original: { statusAdded, messageAdded }, - }, - }) { - return ( - <> - {statusAdded === AddRedisDatabaseStatus.Success ? ( - {messageAdded} - ) : ( - - - - - - - - - Error - - - - - )} - - ) - }, - } - - const columnsResult: ColumnDefinition[] = [...columns] - columnsResult.push(messageColumn) + columns, + columnsResult, + instancesAdded, + instances, + loading, + handleClose, + handleBackAdding, + handleAddInstances, + } = useClusterDatabasesConfig() if (instancesAdded.length) { return ( { return ( () +import RedisClusterDatabasesResult from './RedisClusterDatabasesResult' describe('RedisClusterDatabasesResult', () => { it('should render', () => { @@ -20,8 +15,10 @@ describe('RedisClusterDatabasesResult', () => { expect( render( , ), ).toBeTruthy() diff --git a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.stories.tsx b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.stories.tsx new file mode 100644 index 0000000000..343e7482ca --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.stories.tsx @@ -0,0 +1,61 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' + +import { RedisClusterInstanceAddedFactory } from 'uiSrc/mocks/factories/cluster/RedisClusterInstance.factory' +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' +import RedisClusterDatabasesResult from './RedisClusterDatabasesResult' +import { colFactory } from './useClusterDatabasesConfig' + +const mockInstancesSuccess = RedisClusterInstanceAddedFactory.buildList(3, { + statusAdded: AddRedisDatabaseStatus.Success, +}) +const mockInstancesFailed = RedisClusterInstanceAddedFactory.buildList(2, { + statusAdded: AddRedisDatabaseStatus.Fail, +}) +const mockInstancesMixed = [ + ...RedisClusterInstanceAddedFactory.buildList(3, { + statusAdded: AddRedisDatabaseStatus.Success, + }), + ...RedisClusterInstanceAddedFactory.buildList(2, { + statusAdded: AddRedisDatabaseStatus.Fail, + }), +] + +const [, colMock] = colFactory(mockInstancesSuccess) + +const meta: Meta = { + component: RedisClusterDatabasesResult, + args: { + columns: colMock, + instances: [], + onBack: fn(), + onView: fn(), + }, +} + +export default meta + +type Story = StoryObj + +export const Empty: Story = {} + +export const AllSuccess: Story = { + args: { + instances: mockInstancesSuccess, + columns: colMock, + }, +} + +export const AllFailed: Story = { + args: { + instances: mockInstancesFailed, + columns: colMock, + }, +} + +export const Mixed: Story = { + args: { + instances: mockInstancesMixed, + columns: colMock, + }, +} diff --git a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.tsx b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.tsx index b037fd209a..4c2f0b7c78 100644 --- a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.tsx +++ b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.tsx @@ -1,30 +1,28 @@ -import React, { useState, useEffect } from 'react' -import cx from 'classnames' -import { useSelector } from 'react-redux' +import React, { useEffect, useState } from 'react' -import { - AddRedisDatabaseStatus, - InstanceRedisCluster, -} from 'uiSrc/slices/interfaces' +import type { InstanceRedisCluster } from 'uiSrc/slices/interfaces' +import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' import { setTitle } from 'uiSrc/utils' -import { clusterSelector } from 'uiSrc/slices/instances/cluster' import MessageBar from 'uiSrc/components/message-bar/MessageBar' +import { riToast } from 'uiSrc/components/base/display/toast' import { AutodiscoveryPageTemplate } from 'uiSrc/templates' -import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { Row } from 'uiSrc/components/base/layout/flex' +import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' +import { type ColumnDef, Table } from 'uiSrc/components/base/layout/table' import { - PrimaryButton, - SecondaryButton, -} from 'uiSrc/components/base/forms/buttons' -import { SearchInput } from 'uiSrc/components/base/inputs' -import { FormField } from 'uiSrc/components/base/forms/FormField' -import { Title } from 'uiSrc/components/base/text/Title' -import { Text } from 'uiSrc/components/base/text' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' -import styles from './styles.module.scss' + DatabaseContainer, + DatabaseWrapper, + EmptyState, + Footer, + Header, +} from 'uiSrc/components/auto-discover' +import { Spacer } from 'uiSrc/components/base/layout' +import { SummaryText } from './components' export interface Props { - columns: ColumnDefinition[] + columns: ColumnDef[] + instances: InstanceRedisCluster[] onView: (sendEvent?: boolean) => void onBack: (sendEvent?: boolean) => void } @@ -32,15 +30,22 @@ export interface Props { const loadingMsg = 'loading...' const notFoundMsg = 'Not found' -const RedisClusterDatabasesResult = ({ columns, onBack, onView }: Props) => { +const RedisClusterDatabasesResult = ({ + columns, + instances, + onBack, + onView, +}: Props) => { const [items, setItems] = useState([]) const [message, setMessage] = useState(loadingMsg) - const { dataAdded: instances } = useSelector(clusterSelector) - - setTitle('Redis Enterprise Databases Added') + useEffect(() => { + setTitle('Redis Enterprise Databases Added') + }, []) - useEffect(() => setItems(instances), [instances]) + useEffect(() => { + setItems(instances) + }, [instances]) const countSuccessAdded = instances.filter( ({ statusAdded }) => statusAdded === AddRedisDatabaseStatus.Success, @@ -65,75 +70,49 @@ const RedisClusterDatabasesResult = ({ columns, onBack, onView }: Props) => { setItems(itemsTemp) } - const SummaryText = () => ( - - Summary: - {countSuccessAdded ? ( - - Successfully added {countSuccessAdded} database(s) - {countFailAdded ? '. ' : '.'} - - ) : null} - {countFailAdded ? ( - Failed to add {countFailAdded} database(s). - ) : null} - - ) - return ( -
- + <DatabaseContainer justify="start"> + <Header + title={` Redis Enterprise - {countSuccessAdded + countFailAdded > 1 - ? ' Databases ' - : ' Database '} + ${ + countSuccessAdded + countFailAdded > 1 + ? ' Databases ' + : ' Database ' + } Added - - - - - - - - - - - - - -
-
+ `} + onBack={onBack} + onQueryChange={onQueryChange} + /> + + + + +
10} + stripedRows + emptyState={() => } /> - {!items.length && ( - {message} - )} - - - - - onBack(false)} - className="btn-cancel btn-back" - data-testid="btn-back-to-adding" - > - Back to adding databases - + + +
+ onView(false)} @@ -142,7 +121,7 @@ const RedisClusterDatabasesResult = ({ columns, onBack, onView }: Props) => { View Databases - +
) } diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/capabilities.tsx b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/capabilities.tsx new file mode 100644 index 0000000000..83e54b864d --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/capabilities.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { DatabaseListModules } from 'uiSrc/components' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces' + +export const capabilitiesColumn = (): ColumnDef => { + return { + header: 'Capabilities', + id: 'modules', + accessorKey: 'modules', + enableSorting: true, + maxSize: 150, + cell: function Modules({ row: { original: instance } }) { + return ( + ({ name }))} + /> + ) + }, + } +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/database.tsx b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/database.tsx new file mode 100644 index 0000000000..b97aa06e84 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/database.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces' + +import { DatabaseCell } from '../components/DatabaseCell' + +export const databaseColumn = (): ColumnDef => { + return { + header: 'Database', + id: 'name', + accessorKey: 'name', + minSize: 180, + enableSorting: true, + cell: ({ + row: { + original: { name }, + }, + }) => , + } +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/endpoint.tsx b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/endpoint.tsx new file mode 100644 index 0000000000..f10423d8fa --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/endpoint.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces' + +import { EndpointCell } from '../components/EndpointCell' + +export const endpointColumn = (): ColumnDef => { + return { + header: 'Endpoint', + id: 'dnsName', + accessorKey: 'dnsName', + enableSorting: true, + cell: ({ + row: { + original: { dnsName, port }, + }, + }) => , + } +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/options.tsx b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/options.tsx new file mode 100644 index 0000000000..3038af80b5 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/options.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { DatabaseListOptions } from 'uiSrc/components' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces' +import { parseInstanceOptionsCluster } from 'uiSrc/utils' + +export const optionsColumn = ( + instances: InstanceRedisCluster[], +): ColumnDef => { + return { + header: 'Options', + id: 'options', + accessorKey: 'options', + enableSorting: true, + maxSize: 180, + cell: ({ row: { original: instance } }) => { + const options = parseInstanceOptionsCluster(instance?.uid, instances) + return + }, + } +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/result.tsx b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/result.tsx new file mode 100644 index 0000000000..4396156fd1 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/result.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces' + +import { ResultCell } from '../components/ResultCell' + +export const resultColumn = (): ColumnDef => { + return { + header: 'Result', + id: 'messageAdded', + accessorKey: 'messageAdded', + enableSorting: true, + cell: ({ + row: { + original: { statusAdded, messageAdded }, + }, + }) => , + } +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/selection.ts b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/selection.ts new file mode 100644 index 0000000000..febd7e8fdd --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/selection.ts @@ -0,0 +1,7 @@ +import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces' +import { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils' + +export const selectionColumn = () => { + return getSelectionColumn() +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/status.ts b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/status.ts new file mode 100644 index 0000000000..1c42ba95a6 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/status.ts @@ -0,0 +1,13 @@ +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces' + +export const statusColumn = (): ColumnDef => { + return { + header: 'Status', + id: 'status', + accessorKey: 'status', + enableSorting: true, + size: 100, + } +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/DatabaseCell.tsx b/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/DatabaseCell.tsx new file mode 100644 index 0000000000..e2e989921c --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/DatabaseCell.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { RiTooltip } from 'uiSrc/components' +import { formatLongName } from 'uiSrc/utils' +import { CellText } from 'uiSrc/components/auto-discover' + +import styles from '../../styles.module.scss' + +export interface DatabaseCellProps { + name: string +} + +export const DatabaseCell = ({ name }: DatabaseCellProps) => { + const cellContent = (name || '') + .substring(0, 200) + .replace(/\s\s/g, '\u00a0\u00a0') + + return ( +
+ + {cellContent} + +
+ ) +} diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/EndpointCell.tsx b/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/EndpointCell.tsx new file mode 100644 index 0000000000..a08ce0b2e0 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/EndpointCell.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { RiTooltip } from 'uiSrc/components' +import { formatLongName, handleCopy } from 'uiSrc/utils' +import { + CopyBtn, + CopyPublicEndpointText, + CopyTextContainer, +} from 'uiSrc/components/auto-discover' + +export interface EndpointCellProps { + dnsName: string + port: number +} + +export const EndpointCell = ({ dnsName, port }: EndpointCellProps) => { + if (!dnsName) { + return null + } + const text = `${dnsName}:${port}` + + return ( + + + {text} + + + + handleCopy(text)} + /> + + + ) +} diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/ResultCell.tsx b/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/ResultCell.tsx new file mode 100644 index 0000000000..6a3a9d0c6f --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/ResultCell.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { type AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces' +import { ColorText, Text } from 'uiSrc/components/base/text' +import { RiTooltip } from 'uiSrc/components' +import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' +import { RiIcon } from 'uiSrc/components/base/icons' + +export interface ResultCellProps { + statusAdded: AddRedisDatabaseStatus | undefined + messageAdded: string | undefined +} + +export const ResultCell = ({ statusAdded, messageAdded }: ResultCellProps) => { + if (statusAdded === 'success') { + return {messageAdded} + } + + return ( + + + + + + + + + Error + + + + + ) +} diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/index.ts b/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/index.ts new file mode 100644 index 0000000000..1db9119ab4 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/components/index.ts @@ -0,0 +1,4 @@ +export * from './DatabaseCell' +export * from './EndpointCell' +export * from './ResultCell' + diff --git a/redisinsight/ui/src/pages/redis-cluster/column-definitions/index.ts b/redisinsight/ui/src/pages/redis-cluster/column-definitions/index.ts new file mode 100644 index 0000000000..33b217bca0 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/column-definitions/index.ts @@ -0,0 +1,7 @@ +export * from './columns/result' +export * from './columns/database' +export * from './columns/endpoint' +export * from './columns/status' +export * from './columns/options' +export * from './columns/capabilities' +export * from './columns/selection' diff --git a/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.style.ts b/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.style.ts new file mode 100644 index 0000000000..c029e8507a --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.style.ts @@ -0,0 +1,4 @@ +export default { + panelCancelBtn: 'panelCancelBtn', +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.tsx b/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.tsx new file mode 100644 index 0000000000..20b935a7e4 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.tsx @@ -0,0 +1,48 @@ +import React from 'react' + +import { RiPopover } from 'uiSrc/components/base' +import { DestructiveButton, SecondaryButton } from 'uiSrc/components/base/forms/buttons' +import { Text } from 'uiSrc/components/base/text' +import styles from './CancelButton.style' + +import type { CancelButtonProps } from './CancelButton.types' + +export const CancelButton = ({ + isPopoverOpen, + onShowPopover, + onClosePopover, + onProceed, +}: CancelButtonProps) => ( + + Cancel + + } + > + + Your changes have not been saved. Do you want to proceed to + the list of databases? + +
+
+ + Proceed + +
+
+) + diff --git a/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.types.ts b/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.types.ts new file mode 100644 index 0000000000..b7fdcc98bb --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.types.ts @@ -0,0 +1,7 @@ +export interface CancelButtonProps { + isPopoverOpen: boolean + onShowPopover: () => void + onClosePopover: () => void + onProceed: () => void +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/components/SummaryText/SummaryText.tsx b/redisinsight/ui/src/pages/redis-cluster/components/SummaryText/SummaryText.tsx new file mode 100644 index 0000000000..f2e35d9be9 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/components/SummaryText/SummaryText.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +import { ColorText, Text } from 'uiSrc/components/base/text' +import type { SummaryTextProps } from './SummaryText.types' + +export const SummaryText = ({ countSuccessAdded, countFailAdded }: SummaryTextProps) => ( + + Summary: + {countSuccessAdded ? ( + + Successfully added {countSuccessAdded} database(s) + {countFailAdded ? '. ' : '.'} + + ) : null} + {countFailAdded ? ( + Failed to add {countFailAdded} database(s). + ) : null} + +) + diff --git a/redisinsight/ui/src/pages/redis-cluster/components/SummaryText/SummaryText.types.ts b/redisinsight/ui/src/pages/redis-cluster/components/SummaryText/SummaryText.types.ts new file mode 100644 index 0000000000..c8e0e17d13 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/components/SummaryText/SummaryText.types.ts @@ -0,0 +1,5 @@ +export interface SummaryTextProps { + countSuccessAdded: number + countFailAdded: number +} + diff --git a/redisinsight/ui/src/pages/redis-cluster/components/index.ts b/redisinsight/ui/src/pages/redis-cluster/components/index.ts new file mode 100644 index 0000000000..cc155781ff --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/components/index.ts @@ -0,0 +1,3 @@ +export { CancelButton } from './CancelButton/CancelButton' +export { SummaryText } from './SummaryText/SummaryText' + diff --git a/redisinsight/ui/src/pages/redis-cluster/useClusterDatabasesConfig.tsx b/redisinsight/ui/src/pages/redis-cluster/useClusterDatabasesConfig.tsx new file mode 100644 index 0000000000..2d828a5988 --- /dev/null +++ b/redisinsight/ui/src/pages/redis-cluster/useClusterDatabasesConfig.tsx @@ -0,0 +1,106 @@ +import { useCallback, useEffect, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { + addInstancesRedisCluster, + clusterSelector, + resetDataRedisCluster, + resetInstancesRedisCluster, +} from 'uiSrc/slices/instances/cluster' +import { Maybe, Nullable, setTitle } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { Pages } from 'uiSrc/constants' +import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces' +import { type ColumnDef } from 'uiSrc/components/base/layout/table' +import { + capabilitiesColumn, + databaseColumn, + endpointColumn, + optionsColumn, + resultColumn, + selectionColumn, + statusColumn, +} from './column-definitions' + +export const colFactory = (instances: Nullable) => { + let columns: ColumnDef[] = [ + databaseColumn(), + statusColumn(), + endpointColumn(), + capabilitiesColumn(), + optionsColumn(instances || []), + ] + if (instances && instances.length > 0) { + columns.unshift(selectionColumn()) + } + + const columnsResult: ColumnDef[] = [ + ...columns, + resultColumn(), + ] + // remove selection column from result columns + columnsResult.shift() + return [columns, columnsResult] +} +const sendCancelEvent = () => { + sendEventTelemetry({ + event: + TelemetryEvent.CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_CANCELLED, + }) +} +export const useClusterDatabasesConfig = () => { + const dispatch = useDispatch() + const history = useHistory() + + const { + credentials, + data: instances, + dataAdded: instancesAdded, + loading, + } = useSelector(clusterSelector) + + useEffect(() => { + setTitle('Auto-Discover Redis Enterprise Databases') + }, []) + + const handleClose = useCallback( + (sendEvent = true) => { + sendEvent && sendCancelEvent() + dispatch(resetDataRedisCluster()) + history.push(Pages.home) + }, + [dispatch, history], + ) + const handleBackAdding = useCallback( + (sendEvent = true) => { + sendEvent && sendCancelEvent() + dispatch(resetInstancesRedisCluster()) + history.push(Pages.home) + }, + [dispatch, history], + ) + + const handleAddInstances = useCallback( + (uids: Maybe[]) => { + dispatch(addInstancesRedisCluster({ uids, credentials })) + }, + [dispatch], + ) + + const [columns, columnsResult] = useMemo( + () => colFactory(instances), + [instances], + ) + + return { + columns, + columnsResult, + instances, + instancesAdded, + loading, + handleClose, + handleBackAdding, + handleAddInstances, + } +} diff --git a/redisinsight/ui/src/utils/common.ts b/redisinsight/ui/src/utils/common.ts index e26d6b5de9..f529254b39 100644 --- a/redisinsight/ui/src/utils/common.ts +++ b/redisinsight/ui/src/utils/common.ts @@ -65,3 +65,7 @@ export const openNewWindowDatabase = (location: string) => { window.app?.ipc?.invoke(IpcInvokeEvent.windowOpen, { location }) } + +export const handleCopy = (text = '') => { + navigator.clipboard.writeText(text) +} diff --git a/redisinsight/ui/src/utils/events/handlePasteHostName.ts b/redisinsight/ui/src/utils/events/handlePasteHostName.ts index 06c2791737..472d0893d0 100644 --- a/redisinsight/ui/src/utils/events/handlePasteHostName.ts +++ b/redisinsight/ui/src/utils/events/handlePasteHostName.ts @@ -1,12 +1,12 @@ const handlePasteHostName = ( onHostNamePaste: (text: string) => boolean, e: React.ClipboardEvent & { - originalEvent: { + originalEvent?: { clipboardData: DataTransfer | null } }, ) => { - const clipboardData = e.clipboardData || e.originalEvent.clipboardData + const clipboardData = e.clipboardData || e.originalEvent?.clipboardData /* * If the details were autofilled, stop the default behaviour * which would trigger a redundant onChange event. Autofill happens diff --git a/tests/e2e/rte.docker-compose.yml b/tests/e2e/rte.docker-compose.yml index be5be3b5ca..6ec9046da3 100644 --- a/tests/e2e/rte.docker-compose.yml +++ b/tests/e2e/rte.docker-compose.yml @@ -302,6 +302,7 @@ services: # redis enterprise redis-enterprise: logging: *logging + platform: linux/amd64 build: ./rte/redis-enterprise cap_add: - sys_resource diff --git a/tests/e2e/rte/redis-enterprise/Dockerfile b/tests/e2e/rte/redis-enterprise/Dockerfile index 8af7097d05..5981365b09 100644 --- a/tests/e2e/rte/redis-enterprise/Dockerfile +++ b/tests/e2e/rte/redis-enterprise/Dockerfile @@ -1,4 +1,4 @@ -FROM redislabs/redis:6.2.8-50 +FROM redislabs/redis:8.0.2-17 ## Set the env var to instruct RE to create a cluster on startup ENV BOOTSTRAP_ACTION create_cluster