From ed72644d5307f9f34a4590b12612a75e6a197aa4 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Mon, 3 Oct 2022 17:57:20 +0400 Subject: [PATCH 1/2] #RI-3517 - add summary per data for analysis page --- .../charts/donut-chart/DonutChart.spec.tsx | 34 +++- .../charts/donut-chart/DonutChart.tsx | 12 +- .../charts/donut-chart/styles.module.scss | 1 + .../src/components/group-badge/GroupBadge.tsx | 2 +- redisinsight/ui/src/constants/keys.ts | 4 + .../analysis-data-view/AnalysisDataView.tsx | 6 +- .../summary-per-data/SummaryPerData.spec.tsx | 151 ++++++++++++++++++ .../summary-per-data/SummaryPerData.tsx | 126 +++++++++++++++ .../components/summary-per-data/index.ts | 3 + .../summary-per-data/styles.module.scss | 59 +++++++ .../top-namespace-view/TopNamespaceView.tsx | 2 +- .../pages/databaseAnalysis/styles.module.scss | 7 +- .../themes/dark_theme/_dark_theme.lazy.scss | 1 + .../themes/dark_theme/_theme_color.scss | 1 + .../themes/light_theme/_light_theme.lazy.scss | 1 + .../themes/light_theme/_theme_color.scss | 1 + 16 files changed, 395 insertions(+), 16 deletions(-) create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.spec.tsx create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.tsx create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/index.ts create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/styles.module.scss diff --git a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.spec.tsx b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.spec.tsx index a574e157ef..36d39b6c9e 100644 --- a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.spec.tsx +++ b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.spec.tsx @@ -1,4 +1,6 @@ +import { sumBy } from 'lodash' import React from 'react' +import { getPercentage } from 'uiSrc/utils/numbers' import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' import DonutChart, { ChartData } from './DonutChart' @@ -12,6 +14,8 @@ const mockData: ChartData[] = [ { value: 15, name: 'F', color: [50, 50, 50] }, ] +const sum = sumBy(mockData, 'value') + describe('DonutChart', () => { it('should render with empty data', () => { expect(render()).toBeTruthy() @@ -71,11 +75,27 @@ describe('DonutChart', () => { it('should call render tooltip and label methods', () => { const renderLabel = jest.fn() const renderTooltip = jest.fn() - render() - expect(renderLabel).toBeCalled() + render( + + ) + expect(renderLabel).toBeCalledTimes(mockData.length) + + fireEvent.mouseEnter(screen.getByTestId('arc-A-1')) + expect(renderTooltip).toBeCalledWith(mockData[0]) + }) + + it('should render provided tooltip', () => { + const renderTooltip = () => () + + render() fireEvent.mouseEnter(screen.getByTestId('arc-A-1')) - expect(renderTooltip).toBeCalled() + expect(screen.getByTestId('label')).toBeInTheDocument() }) it('should set tooltip as visible on hover and hidden on leave', () => { @@ -87,4 +107,12 @@ describe('DonutChart', () => { fireEvent.mouseLeave(screen.getByTestId('arc-A-1')) expect(screen.getByTestId('chart-value-tooltip')).not.toBeVisible() }) + + it('should display values with percentage', () => { + render() + + mockData.forEach(({ value, name }) => { + expect(screen.getByTestId(`label-${name}-${value}`)).toHaveTextContent(`: ${getPercentage(value, sum)}%`) + }) + }) }) diff --git a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx index aed642d73a..aadfcade78 100644 --- a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx +++ b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx @@ -1,6 +1,6 @@ import cx from 'classnames' import * as d3 from 'd3' -import { sumBy } from 'lodash' +import { isString, sumBy } from 'lodash' import React, { useEffect, useRef, useState } from 'react' import { flushSync } from 'react-dom' import { Nullable, truncateNumberToRange } from 'uiSrc/utils' @@ -12,7 +12,7 @@ import styles from './styles.module.scss' export interface ChartData { value: number name: string - color: RGBColor + color: RGBColor | string meta?: { [key: string]: any } @@ -61,7 +61,7 @@ const DonutChart = (props: IProps) => { const margin = config?.margin || 98 const radius = config?.radius || (width / 2 - margin) const arcWidth = config?.arcWidth || 8 - const percentToShowLabel = config?.percentToShowLabel || 5 + const percentToShowLabel = config?.percentToShowLabel ?? 5 const [hoveredData, setHoveredData] = useState>(null) const svgRef = useRef(null) @@ -113,12 +113,12 @@ const DonutChart = (props: IProps) => { } const isShowLabel = (d: d3.PieArcDatum) => - d.endAngle - d.startAngle > (Math.PI * 2) / (100 / percentToShowLabel) + (percentToShowLabel > 0 ? d.endAngle - d.startAngle > (Math.PI * 2) / (100 / percentToShowLabel) : true) const getLabelPosition = (d: d3.PieArcDatum) => { const [x, y] = arc.centroid(d) const h = Math.sqrt(x * x + y * y) - return `translate(${(x / h) * (radius + 16)}, ${((y + 4) / h) * (radius + 16)})` + return `translate(${(x / h) * (radius + 12)}, ${((y + 4) / h) * (radius + 12)})` } useEffect(() => { @@ -147,7 +147,7 @@ const DonutChart = (props: IProps) => { .append('path') .attr('data-testid', (d) => `arc-${d.data.name}-${d.data.value}`) .attr('d', arc) - .attr('fill', (d) => rgb(d.data.color)) + .attr('fill', (d) => (isString(d.data.color) ? d.data.color : rgb(d.data.color))) .attr('class', cx(styles.arc, classNames?.arc)) .on('mouseenter mousemove', onMouseEnterSlice) .on('mouseleave', onMouseLeaveSlice) diff --git a/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss b/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss index ed34a35b87..ca201fc111 100644 --- a/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss +++ b/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss @@ -23,6 +23,7 @@ fill: var(--euiTextSubduedColor); font-size: 12px; font-weight: bold; + letter-spacing: -0.12px !important; .chartLabelValue { font-weight: normal; diff --git a/redisinsight/ui/src/components/group-badge/GroupBadge.tsx b/redisinsight/ui/src/components/group-badge/GroupBadge.tsx index f6a9ad1a8c..f613c716eb 100644 --- a/redisinsight/ui/src/components/group-badge/GroupBadge.tsx +++ b/redisinsight/ui/src/components/group-badge/GroupBadge.tsx @@ -15,7 +15,7 @@ export interface Props { const GroupBadge = ({ type, name = '', className = '', onDelete, compressed }: Props) => ( { /> )}
- +
+ + +
) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.spec.tsx new file mode 100644 index 0000000000..861e0f7af9 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.spec.tsx @@ -0,0 +1,151 @@ +import React from 'react' +import { GROUP_TYPES_DISPLAY } from 'uiSrc/constants' +import { render, screen } from 'uiSrc/utils/test-utils' +import { DatabaseAnalysis, SimpleTypeSummary } from 'apiSrc/modules/database-analysis/models' + +import SummaryPerData from './SummaryPerData' + +const mockData = { + totalMemory: { + total: 75, + types: [ + { + type: 'hash', + total: 18 + }, + { + type: 'TSDB-TYPE', + total: 11 + }, + { + type: 'string', + total: 10 + }, + { + type: 'list', + total: 9 + }, + { + type: 'stream', + total: 8 + }, + { + type: 'zset', + total: 8 + }, + { + type: 'set', + total: 7 + }, + { + type: 'graphdata', + total: 2 + }, + { + type: 'ReJSON-RL', + total: 1 + }, + { + type: 'MBbloom--', + total: 1 + } + ] + }, + totalKeys: { + total: 1168424, + types: [ + { + type: 'hash', + total: 572813 + }, + { + type: 'zset', + total: 233571 + }, + { + type: 'set', + total: 138184 + }, + { + type: 'list', + total: 95886 + }, + { + type: 'stream', + total: 79532 + }, + { + type: 'TSDB-TYPE', + total: 47143 + }, + { + type: 'string', + total: 891 + }, + { + type: 'MBbloom--', + total: 272 + }, + { + type: 'graphdata', + total: 72 + }, + { + type: 'ReJSON-RL', + total: 60 + } + ] + } +} as DatabaseAnalysis + +const getName = (t: SimpleTypeSummary) => + (t.type in GROUP_TYPES_DISPLAY ? (GROUP_TYPES_DISPLAY as any)[t.type] : t.type) + +describe('SummaryPerData', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render nothing without data', () => { + render() + + expect(screen.queryByTestId('summary-per-data-loading')).not.toBeInTheDocument() + expect(screen.queryByTestId('summary-per-data')).not.toBeInTheDocument() + }) + + it('should render loading', () => { + render() + + expect(screen.getByTestId('summary-per-data-loading')).toBeInTheDocument() + }) + + it('should render charts', () => { + render() + expect(screen.getByTestId('donut-memory')).toBeInTheDocument() + expect(screen.queryByTestId('donut-keys')).toBeInTheDocument() + }) + + it('should render chart arcs', () => { + render() + + mockData.totalKeys.types.forEach((t) => { + expect(screen.getByTestId(`arc-${getName(t)}-${t.total}`)).toBeInTheDocument() + }) + + mockData.totalMemory.types.forEach((t) => { + expect(screen.getByTestId(`arc-${getName(t)}-${t.total}`)).toBeInTheDocument() + }) + }) + + it('should render chart labels', () => { + render() + + mockData.totalKeys.types.forEach((t) => { + expect(screen.getByTestId(`label-${getName(t)}-${t.total}`)).toBeInTheDocument() + }) + + mockData.totalMemory.types.forEach((t) => { + expect(screen.getByTestId(`label-${getName(t)}-${t.total}`)).toBeInTheDocument() + }) + }) +}) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.tsx new file mode 100644 index 0000000000..c5e444b7ab --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.tsx @@ -0,0 +1,126 @@ +import { EuiIcon, EuiTitle } from '@elastic/eui' +import cx from 'classnames' +import React, { useEffect, useState } from 'react' +import { DatabaseAnalysis, SimpleTypeSummary } from 'apiSrc/modules/database-analysis/models' +import { DonutChart } from 'uiSrc/components/charts' +import { ChartData } from 'uiSrc/components/charts/donut-chart/DonutChart' +import { KeyIconSvg, MemoryIconSvg } from 'uiSrc/components/database-overview/components/icons' +import { GROUP_TYPES_COLORS, GROUP_TYPES_DISPLAY, GroupTypesColors, GroupTypesDisplay } from 'uiSrc/constants' +import { formatBytes, Nullable } from 'uiSrc/utils' +import { getPercentage, numberWithSpaces } from 'uiSrc/utils/numbers' + +import styles from './styles.module.scss' + +export interface Props { + data: Nullable + loading: boolean +} + +const SummaryPerData = ({ data, loading }: Props) => { + const { totalMemory, totalKeys } = data || {} + const [memoryData, setMemoryData] = useState([]) + const [keysData, setKeysData] = useState([]) + + const getChartData = (t: SimpleTypeSummary) => ({ + value: t.total, + name: t.type in GROUP_TYPES_DISPLAY ? GROUP_TYPES_DISPLAY[t.type as GroupTypesDisplay] : t.type, + color: t.type in GROUP_TYPES_COLORS ? GROUP_TYPES_COLORS[t.type as GroupTypesColors] : 'var(--defaultTypeColor)', + meta: { ...t } + }) + + useEffect(() => { + if (data) { + setMemoryData(totalMemory?.types?.map(getChartData) as ChartData[]) + setKeysData(totalKeys?.types?.map(getChartData) as ChartData[]) + } + }, [data]) + + if (loading && !totalMemory && !totalKeys) { + return ( +
+
+
+
+ ) + } + + if ((!totalMemory || memoryData.length === 0) && (!totalKeys || keysData.length === 0)) { + return null + } + + const renderMemoryTooltip = (data: ChartData) => ( +
+ + {data.name}: + + {getPercentage(data.value, totalMemory?.total)}% + + ( {formatBytes(data.value, 3, false)} ) + +
+ ) + + const renderKeysTooltip = (data: ChartData) => ( +
+ + {data.name}: + + {getPercentage(data.value, totalKeys?.total)}% + + ( {numberWithSpaces(data.value)} ) + +
+ ) + + return ( +
+ +

SUMMARY PER DATA TYPE

+
+
+ +
+ + + Memory + +
+
+
{formatBytes(totalMemory?.total || 0, 3)}
+
+ )} + /> + +
+ + + Keys + +
+
+
{numberWithSpaces(totalKeys?.total || 0)}
+
+ )} + /> +
+
+ ) +} + +export default SummaryPerData diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/index.ts b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/index.ts new file mode 100644 index 0000000000..997d27b633 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/index.ts @@ -0,0 +1,3 @@ +import SummaryPerData from './SummaryPerData' + +export default SummaryPerData diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/styles.module.scss new file mode 100644 index 0000000000..dc1833368e --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/styles.module.scss @@ -0,0 +1,59 @@ +.chartsWrapper { + background-color: var(--euiColorLightestShade); + border-radius: 16px; + + display: flex; + align-items: center; + justify-content: space-around; + margin-bottom: 24px; + margin-top: 16px; + overflow: hidden; + + &.loadingWrapper { + margin-top: 36px; + } + + .preloaderCircle { + width: 180px; + height: 180px; + margin: 60px 0; + border-radius: 100%; + background-color: var(--separatorColor); + } + + .chartCenter { + display: flex; + flex-direction: column; + align-items: center; + } + + .chartTitle { + display: flex; + align-items: center; + + .icon { + margin-right: 10px; + } + } + + .titleSeparator { + height: 1px; + border: 0; + background-color: var(--separatorColorLight); + margin: 6px 0; + width: 60px; + } + + .centerCount { + margin-top: 2px; + font-weight: 500; + font-size: 14px; + } + + .labelTooltip { + font-size: 12px; + .tooltipPercentage { + margin-right: 8px; + } + } +} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace-view/TopNamespaceView.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace-view/TopNamespaceView.tsx index 8db74f98da..f4e99b5d96 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace-view/TopNamespaceView.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace-view/TopNamespaceView.tsx @@ -23,7 +23,7 @@ const TopNamespaceView = (props: Props) => { return (
- +

TOP NAMESPACES

Date: Tue, 4 Oct 2022 10:35:36 +0400 Subject: [PATCH 2/2] #RI-3517 - fix pr comments --- .../components/summary-per-data/SummaryPerData.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.tsx index c5e444b7ab..59ba4af422 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/summary-per-data/SummaryPerData.tsx @@ -1,7 +1,7 @@ import { EuiIcon, EuiTitle } from '@elastic/eui' import cx from 'classnames' import React, { useEffect, useState } from 'react' -import { DatabaseAnalysis, SimpleTypeSummary } from 'apiSrc/modules/database-analysis/models' + import { DonutChart } from 'uiSrc/components/charts' import { ChartData } from 'uiSrc/components/charts/donut-chart/DonutChart' import { KeyIconSvg, MemoryIconSvg } from 'uiSrc/components/database-overview/components/icons' @@ -9,6 +9,8 @@ import { GROUP_TYPES_COLORS, GROUP_TYPES_DISPLAY, GroupTypesColors, GroupTypesDi import { formatBytes, Nullable } from 'uiSrc/utils' import { getPercentage, numberWithSpaces } from 'uiSrc/utils/numbers' +import { DatabaseAnalysis, SimpleTypeSummary } from 'apiSrc/modules/database-analysis/models' + import styles from './styles.module.scss' export interface Props {