From edc52c61fda3c84d2969a2bb11de98751225db7c Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 28 Nov 2025 12:46:23 +0200 Subject: [PATCH 1/5] RI-7786: fix ClusterNodesTable --- .../cluster/ClusterNodeDetails.factory.ts | 51 ++++ .../ClusterDetailsPage.styles.ts | 17 ++ .../cluster-details/ClusterDetailsPage.tsx | 13 +- .../ClusterNodesTable.constants.ts | 66 +++++ .../ClusterNodesTable.spec.tsx | 77 ++++++ .../ClusterNodesTable.styles.ts | 5 + .../ClusterNodesTable/ClusterNodesTable.tsx | 25 ++ .../ClusterNodesTable.types.ts | 10 + .../ClusterNodesEmptyState.styles.ts | 6 + .../ClusterNodesEmptyState.tsx | 11 + .../ClusterNodesHostCell.styles.ts | 10 + .../ClusterNodesHostCell.tsx | 29 ++ .../ClusterNodesNumericCell.spec.tsx | 201 ++++++++++++++ .../ClusterNodesNumericCell.tsx | 43 +++ .../utils/formatters.ts | 17 ++ .../utils/isMaxColumnFieldValue.spec.ts | 148 ++++++++++ .../utils/isMaxColumnFieldValue.ts | 28 ++ .../ClusterNodesTable.spec.tsx | 133 --------- .../cluser-nodes-table/ClusterNodesTable.tsx | 253 ------------------ .../components/cluser-nodes-table/index.ts | 3 - .../cluser-nodes-table/styles.module.scss | 64 ----- .../pages/cluster-details/components/index.ts | 2 +- .../pages/cluster-details/styles.module.scss | 12 - 23 files changed, 752 insertions(+), 472 deletions(-) create mode 100644 redisinsight/ui/src/mocks/factories/cluster/ClusterNodeDetails.factory.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.constants.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.styles.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.tsx create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.styles.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.tsx create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.spec.tsx create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.tsx create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/formatters.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.spec.ts create mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts delete mode 100644 redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.spec.tsx delete mode 100644 redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.tsx delete mode 100644 redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/index.ts delete mode 100644 redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/styles.module.scss delete mode 100644 redisinsight/ui/src/pages/cluster-details/styles.module.scss diff --git a/redisinsight/ui/src/mocks/factories/cluster/ClusterNodeDetails.factory.ts b/redisinsight/ui/src/mocks/factories/cluster/ClusterNodeDetails.factory.ts new file mode 100644 index 0000000000..225b380e4f --- /dev/null +++ b/redisinsight/ui/src/mocks/factories/cluster/ClusterNodeDetails.factory.ts @@ -0,0 +1,51 @@ +import { Factory } from 'fishery' +import { faker } from '@faker-js/faker' +import { ClusterNodeDetails } from 'src/modules/cluster-monitor/models' + +enum NodeRole { + Primary = 'primary', + Replica = 'replica', +} + +enum HealthStatus { + Online = 'online', + Offline = 'offline', + Loading = 'loading', +} + +export const ClusterNodeDetailsFactory = Factory.define( + () => ({ + id: faker.string.uuid(), + version: faker.system.semver(), + mode: faker.helpers.arrayElement(['standalone', 'cluster', 'sentinel']), + host: faker.internet.ip(), + port: faker.internet.port(), + role: faker.helpers.arrayElement([NodeRole.Primary, NodeRole.Replica]), + health: faker.helpers.arrayElement([ + HealthStatus.Online, + HealthStatus.Offline, + HealthStatus.Loading, + ]), + slots: ['0-5460'], + totalKeys: faker.number.int({ min: 0, max: 1000000 }), + usedMemory: faker.number.int({ min: 1000000, max: 100000000 }), + opsPerSecond: faker.number.int({ min: 0, max: 10000 }), + connectionsReceived: faker.number.int({ min: 0, max: 10000 }), + connectedClients: faker.number.int({ min: 0, max: 100 }), + commandsProcessed: faker.number.int({ min: 0, max: 1000000000 }), + networkInKbps: faker.number.float({ + min: 0, + max: 10000, + fractionDigits: 2, + }), + networkOutKbps: faker.number.float({ + min: 0, + max: 10000, + fractionDigits: 2, + }), + cacheHitRatio: faker.number.float({ min: 0, max: 1, fractionDigits: 2 }), + replicationOffset: faker.number.int({ min: 0, max: 1000000 }), + uptimeSec: faker.number.int({ min: 0, max: 10000000 }), + replicas: [], + }), +) diff --git a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts new file mode 100644 index 0000000000..ddb108fe7c --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts @@ -0,0 +1,17 @@ +import styled from 'styled-components' + +type DivProps = { + children: React.ReactNode +} + +export const ClusterDetailsPageWrapper = styled.div` + height: 100%; + padding: 0 1.6rem; +` + +export const ClusterDetailsPageContent = styled.div` + overflow-y: auto; + overflow-x: hidden; + max-height: calc(100% - 134px); + max-width: 1920px; +` diff --git a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx index f29466515a..4331bbf752 100644 --- a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx +++ b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx @@ -33,7 +33,7 @@ import { ClusterNodesTable, } from './components' -import styles from './styles.module.scss' +import * as S from './ClusterDetailsPage.styles' export interface ModifiedClusterNodes extends ClusterNodeDetails { letter: string @@ -42,6 +42,7 @@ export interface ModifiedClusterNodes extends ClusterNodeDetails { } const POLLING_INTERVAL = 5_000 +const EMPTY_NODES: ModifiedClusterNodes[] = [] const ClusterDetailsPage = () => { let interval: NodeJS.Timeout @@ -134,13 +135,13 @@ const ClusterDetailsPage = () => { } return ( -
+ -
+ - -
-
+ + + ) } diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.constants.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.constants.ts new file mode 100644 index 0000000000..5275542344 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.constants.ts @@ -0,0 +1,66 @@ +import { ColumnDef, SortingState } from 'uiSrc/components/base/layout/table' + +import { ModifiedClusterNodes } from '../../ClusterDetailsPage' +import { ClusterNodesHostCell } from './components/ClusterNodesHostCell/ClusterNodesHostCell' +import { ClusterNodesNumericCell } from './components/ClusterNodesNumericCell/ClusterNodesNumericCell' + +export const DEFAULT_SORTING: SortingState = [ + { + id: 'host', + desc: false, + }, +] + +export const DEFAULT_CLUSTER_NODES_COLUMNS: ColumnDef[] = + [ + { + header: ({ table }) => `${table.options.data.length} Primary nodes`, + isHeaderCustom: true, + id: 'host', + accessorKey: 'host', + enableSorting: true, + cell: ClusterNodesHostCell, + }, + { + header: 'Commands/s', + id: 'opsPerSecond', + accessorKey: 'opsPerSecond', + enableSorting: true, + cell: ClusterNodesNumericCell, + }, + { + header: 'Network Input', + id: 'networkInKbps', + accessorKey: 'networkInKbps', + enableSorting: true, + cell: ClusterNodesNumericCell, + }, + { + header: 'Network Output', + id: 'networkOutKbps', + accessorKey: 'networkOutKbps', + enableSorting: true, + cell: ClusterNodesNumericCell, + }, + { + header: 'Total Memory', + id: 'usedMemory', + accessorKey: 'usedMemory', + enableSorting: true, + cell: ClusterNodesNumericCell, + }, + { + header: 'Total Keys', + id: 'totalKeys', + accessorKey: 'totalKeys', + enableSorting: true, + cell: ClusterNodesNumericCell, + }, + { + header: 'Clients', + id: 'connectedClients', + accessorKey: 'connectedClients', + enableSorting: true, + cell: ClusterNodesNumericCell, + }, + ] diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx new file mode 100644 index 0000000000..ad273db571 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import { getLetterByIndex } from 'uiSrc/utils' +import { rgb } from 'uiSrc/utils/colors' +import { render, screen } from 'uiSrc/utils/test-utils' + +import ClusterNodesTable from './ClusterNodesTable' +import { ModifiedClusterNodes } from '../../ClusterDetailsPage' +import { ClusterNodeDetailsFactory } from 'uiSrc/mocks/factories/cluster/ClusterNodeDetails.factory' + +const mockNodes = [ + ClusterNodeDetailsFactory.build({ + totalKeys: 1, + opsPerSecond: 1, + }), + ClusterNodeDetailsFactory.build({ + totalKeys: 4, + opsPerSecond: 1, + }), + ClusterNodeDetailsFactory.build({ + totalKeys: 10, + opsPerSecond: 0, + }), +].map((d, index) => ({ + ...d, + letter: getLetterByIndex(index), + index, + color: [0, 0, 0], +})) as ModifiedClusterNodes[] + +describe('ClusterNodesTable', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render loading content', () => { + render() + expect( + screen.getByTestId('primary-nodes-table-loading'), + ).toBeInTheDocument() + }) + + it('should render table', () => { + render() + expect(screen.getByTestId('primary-nodes-table')).toBeInTheDocument() + expect( + screen.queryByTestId('primary-nodes-table-loading'), + ).not.toBeInTheDocument() + }) + + it('should render table with 3 items', () => { + render() + expect(screen.getAllByTestId('node-letter')).toHaveLength(3) + }) + + it('should highlight max value for total keys', () => { + render() + expect(screen.getByTestId('totalKeys-value-max')).toHaveTextContent( + mockNodes[2].totalKeys.toString(), + ) + }) + + it('should not highlight max value for opsPerSecond with equals values', () => { + render() + expect( + screen.queryByTestId('opsPerSecond-value-max'), + ).not.toBeInTheDocument() + }) + + it('should render background color for each node', () => { + render() + mockNodes.forEach(({ letter, color }) => { + expect(screen.getByTestId(`node-color-${letter}`)).toHaveStyle({ + 'background-color': rgb(color), + }) + }) + }) +}) diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.styles.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.styles.ts new file mode 100644 index 0000000000..c7bb4ffb82 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.styles.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components' + +export const TableWrapper = styled.div<{ children: React.ReactNode }>` + margin: ${({ theme }) => theme.core.space.space050}; +` diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx new file mode 100644 index 0000000000..d21bb8c557 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +import { Table } from 'uiSrc/components/base/layout/table' + +import { + DEFAULT_CLUSTER_NODES_COLUMNS, + DEFAULT_SORTING, +} from './ClusterNodesTable.constants' +import { ClusterNodesEmptyState } from './components/ClusterNodesEmptyState/ClusterNodesEmptyState' +import { ClusterNodesTableProps } from './ClusterNodesTable.types' +import * as S from './ClusterNodesTable.styles' + +const ClusterNodesTable = ({ nodes }: ClusterNodesTableProps) => ( + + + +) + +export default ClusterNodesTable diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types.ts new file mode 100644 index 0000000000..cede8f5c0b --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types.ts @@ -0,0 +1,10 @@ +import { CellContext } from 'uiSrc/components/base/layout/table' +import { ModifiedClusterNodes } from '../../ClusterDetailsPage' + +export type ClusterNodesTableProps = { + nodes: ModifiedClusterNodes[] +} + +export type ClusterNodesTableCell = ( + props: CellContext, +) => React.ReactElement | null diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts new file mode 100644 index 0000000000..8fdb3abfac --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts @@ -0,0 +1,6 @@ +import styled from 'styled-components' + +export const EmptyStateWrapper = styled.div<{ children: React.ReactNode }>` + margin-top: 40px; + width: 100%; +` diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.tsx b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.tsx new file mode 100644 index 0000000000..97f0fc79b2 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +import { LoadingContent } from 'uiSrc/components' + +import * as S from './ClusterNodesEmptyState.styles' + +export const ClusterNodesEmptyState = () => ( + + + +) diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.styles.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.styles.ts new file mode 100644 index 0000000000..94400bcc0a --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.styles.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components' + +export const LineIndicator = styled.div<{ $backgroundColor: string }>` + position: absolute; + left: 0; + top: 1px; + bottom: 1px; + width: 3px; + background-color: ${({ $backgroundColor }) => $backgroundColor}; +` diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.tsx b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.tsx new file mode 100644 index 0000000000..b9f1dba8e2 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +import { rgb } from 'uiSrc/utils/colors' +import { Text } from 'uiSrc/components/base/text' +import { Row } from 'uiSrc/components/base/layout/flex' +import { ClusterNodesTableCell } from 'uiSrc/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types' + +import * as S from './ClusterNodesHostCell.styles' + +export const ClusterNodesHostCell: ClusterNodesTableCell = ({ + row: { + original: { letter, port, color, host }, + }, +}) => ( + <> + + + + {letter} + + + {host}:{port} + + + +) diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.spec.tsx b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.spec.tsx new file mode 100644 index 0000000000..ef6c0b916d --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.spec.tsx @@ -0,0 +1,201 @@ +import React from 'react' +import { render, screen } from 'uiSrc/utils/test-utils' +import { CellContext } from 'uiSrc/components/base/layout/table' +import { ModifiedClusterNodes } from '../../../../ClusterDetailsPage' +import { ClusterNodesNumericCell } from './ClusterNodesNumericCell' +import { ClusterNodeDetailsFactory } from 'uiSrc/mocks/factories/cluster/ClusterNodeDetails.factory' + +const mockNodes: ModifiedClusterNodes[] = [ + { + ...ClusterNodeDetailsFactory.build({ + totalKeys: 100, + usedMemory: 2867968, + opsPerSecond: 50, + connectedClients: 6, + networkInKbps: 10.5, + networkOutKbps: 5.2, + }), + letter: 'A', + index: 0, + color: [0, 0, 0], + }, + { + ...ClusterNodeDetailsFactory.build({ + totalKeys: 200, + usedMemory: 2825880, + opsPerSecond: 75, + connectedClients: 4, + networkInKbps: 20.3, + networkOutKbps: 10.1, + }), + letter: 'B', + index: 1, + color: [0, 0, 0], + }, + { + ...ClusterNodeDetailsFactory.build({ + totalKeys: 150, + usedMemory: 2886960, + opsPerSecond: 60, + connectedClients: 7, + networkInKbps: 15.7, + networkOutKbps: 8.3, + }), + letter: 'C', + index: 2, + color: [0, 0, 0], + }, +] + +const createMockCellContext = ( + nodeIndex: number, + field: keyof ModifiedClusterNodes, +): CellContext => + ({ + row: { + original: mockNodes[nodeIndex], + }, + column: { + id: field, + }, + table: { + options: { + data: mockNodes, + }, + }, + }) as CellContext + +describe('ClusterNodesNumericCell', () => { + describe('renderComponent', () => { + const renderComponent = ( + nodeIndex: number, + field: keyof ModifiedClusterNodes, + ) => { + const context = createMockCellContext(nodeIndex, field) + return render() + } + + it('should render numeric value', () => { + renderComponent(0, 'totalKeys') + expect(screen.getByTestId('totalKeys-value')).toHaveTextContent('100') + }) + + it('should render max value with semiBold variant', () => { + renderComponent(1, 'totalKeys') + expect(screen.getByTestId('totalKeys-value-max')).toBeInTheDocument() + }) + + it('should not render max indicator when value is not maximum', () => { + renderComponent(0, 'totalKeys') + expect( + screen.queryByTestId('totalKeys-value-max'), + ).not.toBeInTheDocument() + expect(screen.getByTestId('totalKeys-value')).toBeInTheDocument() + }) + + it('should format usedMemory with bytes formatter', () => { + renderComponent(0, 'usedMemory') + // formatBytes(2867968, 3, false) should format to something like "2.73 MB" + const element = screen.getByTestId('usedMemory-value') + expect(element.textContent).toMatch(/MB|KB|GB/) + }) + + it('should format networkInKbps with kb/s suffix', () => { + renderComponent(0, 'networkInKbps') + expect(screen.getByTestId('networkInKbps-value')).toHaveTextContent( + '10.5 kb/s', + ) + }) + + it('should format networkOutKbps with kb/s suffix', () => { + renderComponent(0, 'networkOutKbps') + expect(screen.getByTestId('networkOutKbps-value')).toHaveTextContent( + '5.2 kb/s', + ) + }) + + it('should return null for non-numeric fields', () => { + const context = createMockCellContext(0, 'host') + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('should handle zero values', () => { + const nodesWithZero = [ + { ...mockNodes[0], opsPerSecond: 0 }, + { ...mockNodes[1], opsPerSecond: 10 }, + { ...mockNodes[2], opsPerSecond: 5 }, + ] + + const context = { + row: { original: nodesWithZero[0] }, + column: { id: 'opsPerSecond' }, + table: { options: { data: nodesWithZero } }, + } as CellContext + + render() + expect(screen.getByTestId('opsPerSecond-value')).toHaveTextContent('0') + }) + + it('should not highlight max value when there is a tie', () => { + const nodesWithTie = [ + { ...mockNodes[0], connectedClients: 10 }, + { ...mockNodes[1], connectedClients: 10 }, + { ...mockNodes[2], connectedClients: 5 }, + ] + + const context = { + row: { original: nodesWithTie[0] }, + column: { id: 'connectedClients' }, + table: { options: { data: nodesWithTie } }, + } as CellContext + + render() + expect( + screen.queryByTestId('connectedClients-value-max'), + ).not.toBeInTheDocument() + expect(screen.getByTestId('connectedClients-value')).toBeInTheDocument() + }) + + it('should highlight max value for opsPerSecond', () => { + renderComponent(1, 'opsPerSecond') + expect(screen.getByTestId('opsPerSecond-value-max')).toBeInTheDocument() + }) + + it('should highlight max value for networkInKbps', () => { + renderComponent(1, 'networkInKbps') + expect(screen.getByTestId('networkInKbps-value-max')).toBeInTheDocument() + }) + + it('should highlight max value for networkOutKbps', () => { + renderComponent(1, 'networkOutKbps') + expect(screen.getByTestId('networkOutKbps-value-max')).toBeInTheDocument() + }) + + it('should highlight max value for connectedClients', () => { + renderComponent(2, 'connectedClients') + expect( + screen.getByTestId('connectedClients-value-max'), + ).toBeInTheDocument() + }) + + it('should format large numbers with spaces', () => { + const nodesWithLargeNumbers = [ + { ...mockNodes[0], totalKeys: 1000000 }, + { ...mockNodes[1], totalKeys: 500000 }, + { ...mockNodes[2], totalKeys: 250000 }, + ] + + const context = { + row: { original: nodesWithLargeNumbers[0] }, + column: { id: 'totalKeys' }, + table: { options: { data: nodesWithLargeNumbers } }, + } as CellContext + + render() + // numberWithSpaces should format 1000000 as "1 000 000" + const element = screen.getByTestId('totalKeys-value-max') + expect(element.textContent).toContain('000') + }) + }) +}) diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.tsx b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.tsx new file mode 100644 index 0000000000..e60bd22473 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.tsx @@ -0,0 +1,43 @@ +import React from 'react' + +import { numberWithSpaces } from 'uiSrc/utils/numbers' +import { RiTooltip } from 'uiSrc/components' +import { Text } from 'uiSrc/components/base/text' +import { ClusterNodesTableCell } from 'uiSrc/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types' + +import { isMaxColumnFieldValue } from './utils/isMaxColumnFieldValue' +import { + displayValueFormatter, + tooltipContentFormatter, +} from './utils/formatters' + +export const ClusterNodesNumericCell: ClusterNodesTableCell = ({ + row, + column, + table, +}) => { + const item = row.original + const field = column.id as keyof typeof item + const value = item[field] ?? 0 + + if (typeof value !== 'number') { + return null + } + + const data = table.options.data + const isMax = isMaxColumnFieldValue(field, value, data) + + const displayValue = (displayValueFormatter[field] ?? numberWithSpaces)(value) + const tooltipContent = tooltipContentFormatter[field]?.(value) + + return ( + + + {displayValue} + + + ) +} diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/formatters.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/formatters.ts new file mode 100644 index 0000000000..397d8d4ac9 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/formatters.ts @@ -0,0 +1,17 @@ +import { formatBytes } from 'uiSrc/utils' +import { numberWithSpaces } from 'uiSrc/utils/numbers' +import { ModifiedClusterNodes } from 'uiSrc/pages/cluster-details/ClusterDetailsPage' + +export const displayValueFormatter: Partial< + Record string> +> = { + usedMemory: (v) => formatBytes(v, 3, false).toString(), + networkInKbps: (v) => `${numberWithSpaces(v)} kb/s`, + networkOutKbps: (v) => `${numberWithSpaces(v)} kb/s`, +} + +export const tooltipContentFormatter: Partial< + Record string> +> = { + usedMemory: (v) => `${numberWithSpaces(v)} B`, +} diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.spec.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.spec.ts new file mode 100644 index 0000000000..33d3526935 --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.spec.ts @@ -0,0 +1,148 @@ +import { isMaxColumnFieldValue } from './isMaxColumnFieldValue' + +interface TestNode { + id: string + value: number + otherValue: number +} + +describe('isMaxColumnFieldValue', () => { + it('should return true when value is the maximum and unique', () => { + const data: TestNode[] = [ + { id: '1', value: 10, otherValue: 5 }, + { id: '2', value: 20, otherValue: 15 }, + { id: '3', value: 5, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 20, data)).toBe(true) + }) + + it('should return false when value is not the maximum', () => { + const data: TestNode[] = [ + { id: '1', value: 10, otherValue: 5 }, + { id: '2', value: 20, otherValue: 15 }, + { id: '3', value: 5, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 10, data)).toBe(false) + expect(isMaxColumnFieldValue('value', 5, data)).toBe(false) + }) + + it('should return false when there is a tie (multiple nodes with max value)', () => { + const data: TestNode[] = [ + { id: '1', value: 20, otherValue: 5 }, + { id: '2', value: 20, otherValue: 15 }, + { id: '3', value: 5, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 20, data)).toBe(false) + }) + + it('should return true when all other values are lower', () => { + const data: TestNode[] = [ + { id: '1', value: 100, otherValue: 5 }, + { id: '2', value: 50, otherValue: 15 }, + { id: '3', value: 25, otherValue: 25 }, + { id: '4', value: 10, otherValue: 30 }, + ] + + expect(isMaxColumnFieldValue('value', 100, data)).toBe(true) + }) + + it('should handle zero values correctly', () => { + const data: TestNode[] = [ + { id: '1', value: 0, otherValue: 5 }, + { id: '2', value: 10, otherValue: 15 }, + { id: '3', value: 5, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 10, data)).toBe(true) + expect(isMaxColumnFieldValue('value', 0, data)).toBe(false) + }) + + it('should return false when all values are zero', () => { + const data: TestNode[] = [ + { id: '1', value: 0, otherValue: 5 }, + { id: '2', value: 0, otherValue: 15 }, + { id: '3', value: 0, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 0, data)).toBe(false) + }) + + it('should return true when there is only one node', () => { + const data: TestNode[] = [{ id: '1', value: 10, otherValue: 5 }] + + expect(isMaxColumnFieldValue('value', 10, data)).toBe(true) + }) + + it('should handle empty data array', () => { + const data: TestNode[] = [] + + expect(isMaxColumnFieldValue('value', 10, data)).toBe(false) + }) + + it('should handle negative values', () => { + const data: TestNode[] = [ + { id: '1', value: -10, otherValue: 5 }, + { id: '2', value: -5, otherValue: 15 }, + { id: '3', value: -20, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', -5, data)).toBe(true) + expect(isMaxColumnFieldValue('value', -10, data)).toBe(false) + }) + + it('should handle large numbers', () => { + const data: TestNode[] = [ + { id: '1', value: 1000000, otherValue: 5 }, + { id: '2', value: 999999, otherValue: 15 }, + { id: '3', value: 500000, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 1000000, data)).toBe(true) + }) + + it('should work with different field names', () => { + const data: TestNode[] = [ + { id: '1', value: 10, otherValue: 5 }, + { id: '2', value: 20, otherValue: 15 }, + { id: '3', value: 5, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('otherValue', 25, data)).toBe(true) + expect(isMaxColumnFieldValue('otherValue', 15, data)).toBe(false) + }) + + it('should return false when value does not exist in data', () => { + const data: TestNode[] = [ + { id: '1', value: 10, otherValue: 5 }, + { id: '2', value: 20, otherValue: 15 }, + { id: '3', value: 5, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 100, data)).toBe(false) + }) + + it('should handle three-way tie', () => { + const data: TestNode[] = [ + { id: '1', value: 10, otherValue: 5 }, + { id: '2', value: 10, otherValue: 15 }, + { id: '3', value: 10, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 10, data)).toBe(false) + }) + + it('should handle decimal values', () => { + const data: TestNode[] = [ + { id: '1', value: 10.5, otherValue: 5 }, + { id: '2', value: 10.7, otherValue: 15 }, + { id: '3', value: 10.3, otherValue: 25 }, + ] + + expect(isMaxColumnFieldValue('value', 10.7, data)).toBe(true) + expect(isMaxColumnFieldValue('value', 10.5, data)).toBe(false) + }) +}) + diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts new file mode 100644 index 0000000000..126cd7851f --- /dev/null +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts @@ -0,0 +1,28 @@ +export const isMaxColumnFieldValue = ( + field: keyof T, + value: number, + data: T[], +): boolean => { + const numericValues = data + .map((node) => node[field] ?? 0) + .filter((v) => typeof v === 'number') + + if (numericValues.length === 0) { + return false + } + + const { max, count } = numericValues.reduce( + (prev, cur) => { + if (cur > prev.max) { + return { max: cur, count: 1 } + } + if (cur === prev.max) { + return { ...prev, count: prev.count + 1 } + } + return prev + }, + { max: numericValues[0], count: 0 }, + ) + + return value === max && count === 1 +} diff --git a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.spec.tsx b/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.spec.tsx deleted file mode 100644 index de65eae4eb..0000000000 --- a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.spec.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React from 'react' -import { getLetterByIndex } from 'uiSrc/utils' -import { rgb } from 'uiSrc/utils/colors' -import { render, screen } from 'uiSrc/utils/test-utils' - -import ClusterNodesTable from './ClusterNodesTable' -import { ModifiedClusterNodes } from '../../ClusterDetailsPage' - -const mockNodes = [ - { - id: '1', - host: '0.0.0.1', - port: 6379, - role: 'primary', - slots: ['10923-16383'], - health: 'online', - totalKeys: 1, - usedMemory: 2867968, - opsPerSecond: 1, - connectionsReceived: 13, - connectedClients: 6, - commandsProcessed: 5678, - networkInKbps: 0.02, - networkOutKbps: 0, - cacheHitRatio: 1, - replicationOffset: 6924, - uptimeSec: 5614, - version: '6.2.6', - mode: 'cluster', - replicas: [], - }, - { - id: '2', - host: '0.0.0.2', - port: 6379, - role: 'primary', - slots: ['0-5460'], - health: 'online', - totalKeys: 4, - usedMemory: 2825880, - opsPerSecond: 1, - connectionsReceived: 15, - connectedClients: 4, - commandsProcessed: 5667, - networkInKbps: 0.04, - networkOutKbps: 0, - cacheHitRatio: 1, - replicationOffset: 6910, - uptimeSec: 5609, - version: '6.2.6', - mode: 'cluster', - replicas: [], - }, - { - id: '3', - host: '0.0.0.3', - port: 6379, - role: 'primary', - slots: ['5461-10922'], - health: 'online', - totalKeys: 10, - usedMemory: 2886960, - opsPerSecond: 0, - connectionsReceived: 18, - connectedClients: 7, - commandsProcessed: 5697, - networkInKbps: 0.02, - networkOutKbps: 0, - cacheHitRatio: 0, - replicationOffset: 6991, - uptimeSec: 5609, - version: '6.2.6', - mode: 'cluster', - replicas: [], - }, -].map((d, index) => ({ - ...d, - letter: getLetterByIndex(index), - index, - color: [0, 0, 0], -})) as ModifiedClusterNodes[] - -describe('ClusterNodesTable', () => { - it('should render', () => { - expect( - render(), - ).toBeTruthy() - }) - - it('should render loading content', () => { - render() - expect( - screen.getByTestId('primary-nodes-table-loading'), - ).toBeInTheDocument() - expect(screen.queryByTestId('primary-nodes-table')).not.toBeInTheDocument() - }) - - it('should render table', () => { - render() - expect(screen.getByTestId('primary-nodes-table')).toBeInTheDocument() - expect( - screen.queryByTestId('primary-nodes-table-loading'), - ).not.toBeInTheDocument() - }) - - it('should render table with 3 items', () => { - render() - expect(screen.getAllByTestId('node-letter')).toHaveLength(3) - }) - - it('should highlight max value for total keys', () => { - render() - expect(screen.getByTestId('totalKeys-value-max')).toHaveTextContent( - mockNodes[2].totalKeys.toString(), - ) - }) - - it('should not highlight max value for opsPerSecond with equals values', () => { - render() - expect( - screen.queryByTestId('opsPerSecond-value-max'), - ).not.toBeInTheDocument() - }) - - it('should render background color for each node', () => { - render() - mockNodes.forEach(({ letter, color }) => { - expect(screen.getByTestId(`node-color-${letter}`)).toHaveStyle({ - 'background-color': rgb(color), - }) - }) - }) -}) 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 deleted file mode 100644 index 2cd7d5c4ec..0000000000 --- a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/ClusterNodesTable.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import cx from 'classnames' -import { map } from 'lodash' -import React, { useState } from 'react' - -import { LoadingContent } from 'uiSrc/components/base/layout' -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' -import { RiTooltip } from 'uiSrc/components' - -import { ModifiedClusterNodes } from '../../ClusterDetailsPage' -import { AllIconsType, RiIcon } from 'uiSrc/components/base/icons/RiIcon' -import styles from './styles.module.scss' - -const ClusterNodesTable = ({ - nodes, - loading, -}: { - nodes: Nullable - loading: boolean -}) => { - const [sort, setSort] = useState({ - field: 'host', - direction: 'asc', - }) - - const isMaxValue = (field: string, value: number) => { - const values = map(nodes, field) - return ( - Math.max(...values) === value && - values.filter((v) => v === value).length === 1 - ) - } - - const headerIconTemplate = (label: string, icon: AllIconsType) => ( -
- - {label} -
- ) - - const columns: ColumnDef[] = [ - { - header: `${nodes?.length} Primary nodes`, - id: 'host', - accessorKey: 'host', - enableSorting: true, - cell: ({ - row: { - original: { letter, port, color, host }, - }, - }) => ( - <> -
-
- - {letter} - - - {host}:{port} - -
- - ), - }, - { - header: () => headerIconTemplate('Commands/s', 'MeasureIconIcon'), - id: 'opsPerSecond', - accessorKey: 'opsPerSecond', - enableSorting: true, - cell: ({ - row: { - original: { opsPerSecond: value }, - }, - }) => { - const isMax = isMaxValue('opsPerSecond', value) - return ( - - {numberWithSpaces(value)} - - ) - }, - }, - { - header: () => headerIconTemplate('Network Input', 'InputIconIcon'), - id: 'networkInKbps', - accessorKey: 'networkInKbps', - enableSorting: true, - cell: ({ - row: { - original: { networkInKbps: value }, - }, - }) => { - const isMax = isMaxValue('networkInKbps', value) - return ( - <> - - {numberWithSpaces(value)} - - kb/s - - ) - }, - }, - { - header: () => headerIconTemplate('Network Output', 'OutputIconIcon'), - id: 'networkOutKbps', - accessorKey: 'networkOutKbps', - enableSorting: true, - cell: ({ - row: { - original: { networkOutKbps: value }, - }, - }) => { - const isMax = isMaxValue('networkOutKbps', value) - return ( - <> - - {numberWithSpaces(value)} - - kb/s - - ) - }, - }, - { - header: () => headerIconTemplate('Total Memory', 'MemoryIconIcon'), - id: 'usedMemory', - accessorKey: 'usedMemory', - enableSorting: true, - cell: ({ - row: { - original: { usedMemory: value }, - }, - }) => { - const [number, size] = formatBytes(value, 3, true) - const isMax = isMaxValue('usedMemory', value) - return ( - - <> - - {number} - - {size} - - - ) - }, - }, - { - header: () => headerIconTemplate('Total Keys', 'KeyIconIcon'), - id: 'totalKeys', - accessorKey: 'totalKeys', - enableSorting: true, - cell: ({ - row: { - original: { totalKeys: value }, - }, - }) => { - const isMax = isMaxValue('totalKeys', value) - return ( - - {numberWithSpaces(value)} - - ) - }, - }, - { - header: () => ( -
- - Clients -
- ), - id: 'connectedClients', - accessorKey: 'connectedClients', - enableSorting: true, - cell: ({ - row: { - original: { connectedClients: value }, - }, - }) => { - const isMax = isMaxValue('connectedClients', value) - return ( - - {numberWithSpaces(value)} - - ) - }, - }, - ] - - return ( -
- {loading && !nodes && ( -
- -
- )} - {nodes && ( -
-
- setSort({ - field: newSort[0].id, - direction: newSort[0].desc ? 'desc' : 'asc', - }) - } - /> - - )} - - ) -} - -export default ClusterNodesTable diff --git a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/index.ts b/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/index.ts deleted file mode 100644 index 36e8e9e491..0000000000 --- a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import ClusterNodesTable from './ClusterNodesTable' - -export default ClusterNodesTable diff --git a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/styles.module.scss b/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/styles.module.scss deleted file mode 100644 index 5a94725b9f..0000000000 --- a/redisinsight/ui/src/pages/cluster-details/components/cluser-nodes-table/styles.module.scss +++ /dev/null @@ -1,64 +0,0 @@ -$breakpoint-table: 1232px; - -.tableWrapper { - @include eui.scrollBar; - - overflow: auto; - position: relative; - max-height: 100%; -} - -.wrapper { - max-width: 1920px; - - .loading { - margin-top: 40px; - width: 100%; - } - - .valueUnit { - font: - normal normal normal 12px/18px Graphik, - sans-serif !important; - margin-left: 4px; - color: var(--euiColorMediumShade) !important; - } - - .nodeName { - margin-right: 12px; - display: block; - min-width: 24px; - font: - normal normal 500 13px/18px Graphik, - sans-serif !important; - } - - .headerCell { - display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: flex-end; - - .headerIcon { - fill: var(--textColorShade); - width: 24px; - height: 20px; - margin-bottom: 4px; - } - } - - .hostPort { - display: inline-flex; - font: - normal normal normal 13px/18px Graphik, - sans-serif !important; - } - - .nodeColor { - position: absolute; - left: 0; - top: 1px; - bottom: 1px; - width: 3px; - } -} diff --git a/redisinsight/ui/src/pages/cluster-details/components/index.ts b/redisinsight/ui/src/pages/cluster-details/components/index.ts index 54ed3f2c9a..bebff9f52e 100644 --- a/redisinsight/ui/src/pages/cluster-details/components/index.ts +++ b/redisinsight/ui/src/pages/cluster-details/components/index.ts @@ -1,5 +1,5 @@ import ClusterDetailsHeader from './cluster-details-header' import ClusterDetailsGraphics from './cluster-details-graphics' -import ClusterNodesTable from './cluser-nodes-table' +import ClusterNodesTable from './ClusterNodesTable/ClusterNodesTable' export { ClusterDetailsHeader, ClusterDetailsGraphics, ClusterNodesTable } diff --git a/redisinsight/ui/src/pages/cluster-details/styles.module.scss b/redisinsight/ui/src/pages/cluster-details/styles.module.scss deleted file mode 100644 index f4a0cbe469..0000000000 --- a/redisinsight/ui/src/pages/cluster-details/styles.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -.main { - height: 100%; -} - -.wrapper { - @include eui.scrollBar; - overflow-y: auto; - overflow-x: hidden; - max-height: calc(100% - 134px); - - max-width: 1920px; -} From 4dd1331b4095fe3a9e67a28bd4a2a0c4c245b6dd Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 28 Nov 2025 13:18:41 +0200 Subject: [PATCH 2/5] RI-7786: simplify isMaxColumnFieldValue --- .../utils/isMaxColumnFieldValue.ts | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts index 126cd7851f..4484d2609f 100644 --- a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts @@ -4,25 +4,11 @@ export const isMaxColumnFieldValue = ( data: T[], ): boolean => { const numericValues = data - .map((node) => node[field] ?? 0) + .map((node) => node[field]) .filter((v) => typeof v === 'number') - if (numericValues.length === 0) { - return false - } - - const { max, count } = numericValues.reduce( - (prev, cur) => { - if (cur > prev.max) { - return { max: cur, count: 1 } - } - if (cur === prev.max) { - return { ...prev, count: prev.count + 1 } - } - return prev - }, - { max: numericValues[0], count: 0 }, + return ( + Math.max(...numericValues) === value && + numericValues.filter((v) => v === value).length === 1 ) - - return value === max && count === 1 } From f783f600c206677f235019222f5a0259c2ed68b0 Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 28 Nov 2025 13:49:30 +0200 Subject: [PATCH 3/5] RI-7786: show loading state only during initial load or missing data --- .../cluster-details/ClusterDetailsPage.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx index 4331bbf752..6b67726725 100644 --- a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx +++ b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx @@ -1,5 +1,5 @@ import { orderBy } from 'lodash' -import React, { useContext, useEffect, useState } from 'react' +import React, { useContext, useEffect, useState, useMemo } from 'react' import { useSelector, useDispatch } from 'react-redux' import { useParams } from 'react-router-dom' import { ClusterNodeDetails } from 'src/modules/cluster-monitor/models' @@ -21,7 +21,6 @@ import { formatLongName, getDbIndex, getLetterByIndex, - Nullable, setTitle, } from 'uiSrc/utils' import { ColorScheme, getRGBColorByScheme, RGBColor } from 'uiSrc/utils/colors' @@ -42,7 +41,6 @@ export interface ModifiedClusterNodes extends ClusterNodeDetails { } const POLLING_INTERVAL = 5_000 -const EMPTY_NODES: ModifiedClusterNodes[] = [] const ClusterDetailsPage = () => { let interval: NodeJS.Timeout @@ -56,7 +54,6 @@ const ClusterDetailsPage = () => { const { loading, data } = useSelector(clusterDetailsSelector) const [isPageViewSent, setIsPageViewSent] = useState(false) - const [nodes, setNodes] = useState>(null) const dispatch = useDispatch() const { theme } = useContext(ThemeContext) @@ -104,18 +101,20 @@ const ClusterDetailsPage = () => { return () => clearInterval(interval) }, [instanceId, loading]) - useEffect(() => { + const nodes = useMemo(() => { if (data) { const nodes = orderBy(data.nodes, ['asc', 'host']) const shift = colorScheme.cHueRange / nodes.length - const modifiedNodes = nodes.map((d, index) => ({ + + return nodes.map((d, index) => ({ ...d, letter: getLetterByIndex(index), index, color: getRGBColorByScheme(index, shift, colorScheme), - })) - setNodes(modifiedNodes) + })) as ModifiedClusterNodes[] } + + return [] as ModifiedClusterNodes[] }, [data]) useEffect(() => { @@ -139,7 +138,7 @@ const ClusterDetailsPage = () => { - + ) From a3d13bf619e90c86a04f44ec857b0fa43d1d42ec Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 28 Nov 2025 14:01:25 +0200 Subject: [PATCH 4/5] RI-7786: remove leftover styles --- .../src/pages/cluster-details/ClusterDetailsPage.styles.ts | 7 ------- .../ui/src/pages/cluster-details/ClusterDetailsPage.tsx | 6 ++---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts index ddb108fe7c..ed0493ee51 100644 --- a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts +++ b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts @@ -8,10 +8,3 @@ export const ClusterDetailsPageWrapper = styled.div` height: 100%; padding: 0 1.6rem; ` - -export const ClusterDetailsPageContent = styled.div` - overflow-y: auto; - overflow-x: hidden; - max-height: calc(100% - 134px); - max-width: 1920px; -` diff --git a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx index 6b67726725..67a4b5bc23 100644 --- a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx +++ b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx @@ -136,10 +136,8 @@ const ClusterDetailsPage = () => { return ( - - - - + + ) } From 0da6f0a546b15e94a24adbb83a9ca4f426a6320f Mon Sep 17 00:00:00 2001 From: Krum Tyukenov Date: Fri, 28 Nov 2025 14:39:22 +0200 Subject: [PATCH 5/5] RI-7786: update styled divs --- .../ClusterDetailsPage.styles.ts | 8 +++----- .../ClusterNodesTable.spec.tsx | 10 ++++------ .../ClusterNodesTable.styles.ts | 5 ----- .../ClusterNodesTable/ClusterNodesTable.tsx | 17 +++++++---------- .../ClusterNodesEmptyState.styles.ts | 4 +++- 5 files changed, 17 insertions(+), 27 deletions(-) delete mode 100644 redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.styles.ts diff --git a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts index ed0493ee51..7dc8916ddd 100644 --- a/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts +++ b/redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts @@ -1,10 +1,8 @@ import styled from 'styled-components' -type DivProps = { - children: React.ReactNode -} - -export const ClusterDetailsPageWrapper = styled.div` +export const ClusterDetailsPageWrapper = styled.div< + React.HTMLAttributes +>` height: 100%; padding: 0 1.6rem; ` diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx index ad273db571..d79b5bce08 100644 --- a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx @@ -33,15 +33,13 @@ describe('ClusterNodesTable', () => { }) it('should render loading content', () => { - render() - expect( - screen.getByTestId('primary-nodes-table-loading'), - ).toBeInTheDocument() + const { container } = render() + expect(container).toBeInTheDocument() }) it('should render table', () => { - render() - expect(screen.getByTestId('primary-nodes-table')).toBeInTheDocument() + const { container } = render() + expect(container).toBeInTheDocument() expect( screen.queryByTestId('primary-nodes-table-loading'), ).not.toBeInTheDocument() diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.styles.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.styles.ts deleted file mode 100644 index c7bb4ffb82..0000000000 --- a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.styles.ts +++ /dev/null @@ -1,5 +0,0 @@ -import styled from 'styled-components' - -export const TableWrapper = styled.div<{ children: React.ReactNode }>` - margin: ${({ theme }) => theme.core.space.space050}; -` diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx index d21bb8c557..f9305cc11b 100644 --- a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx @@ -8,18 +8,15 @@ import { } from './ClusterNodesTable.constants' import { ClusterNodesEmptyState } from './components/ClusterNodesEmptyState/ClusterNodesEmptyState' import { ClusterNodesTableProps } from './ClusterNodesTable.types' -import * as S from './ClusterNodesTable.styles' const ClusterNodesTable = ({ nodes }: ClusterNodesTableProps) => ( - -
- +
) export default ClusterNodesTable diff --git a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts index 8fdb3abfac..7778e05822 100644 --- a/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts +++ b/redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts @@ -1,6 +1,8 @@ import styled from 'styled-components' -export const EmptyStateWrapper = styled.div<{ children: React.ReactNode }>` +export const EmptyStateWrapper = styled.div< + React.HTMLAttributes +>` margin-top: 40px; width: 100%; `