diff --git a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx index aa4596f3be..aed642d73a 100644 --- a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx +++ b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx @@ -1,9 +1,11 @@ import cx from 'classnames' import * as d3 from 'd3' import { sumBy } from 'lodash' -import React, { useEffect, useRef } from 'react' -import { truncateNumberToRange } from 'uiSrc/utils' +import React, { useEffect, useRef, useState } from 'react' +import { flushSync } from 'react-dom' +import { Nullable, truncateNumberToRange } from 'uiSrc/utils' import { rgb, RGBColor } from 'uiSrc/utils/colors' +import { getPercentage } from 'uiSrc/utils/numbers' import styles from './styles.module.scss' @@ -11,6 +13,9 @@ export interface ChartData { value: number name: string color: RGBColor + meta?: { + [key: string]: any + } } interface IProps { @@ -32,8 +37,9 @@ interface IProps { arcLabelValue?: string tooltip?: string } - renderLabel?: (value: number) => string - renderTooltip?: (value: number) => string + renderLabel?: (data: ChartData) => string + renderTooltip?: (data: ChartData) => React.ReactElement | string + labelAs?: 'value' | 'percentage' } const ANIMATION_DURATION_MS = 100 @@ -47,8 +53,9 @@ const DonutChart = (props: IProps) => { title, config, classNames, + labelAs = 'value', renderLabel, - renderTooltip = (v) => v, + renderTooltip, } = props const margin = config?.margin || 98 @@ -56,8 +63,10 @@ const DonutChart = (props: IProps) => { const arcWidth = config?.arcWidth || 8 const percentToShowLabel = config?.percentToShowLabel || 5 + const [hoveredData, setHoveredData] = useState>(null) const svgRef = useRef(null) const tooltipRef = useRef(null) + const sum = sumBy(data, 'value') const arc = d3.arc>() .outerRadius(radius) @@ -74,12 +83,20 @@ const DonutChart = (props: IProps) => { .duration(ANIMATION_DURATION_MS) .attr('d', arcHover) - if (tooltipRef.current) { - tooltipRef.current.innerHTML = `${d.data.name}: ${renderTooltip(d.value)}` - tooltipRef.current.style.visibility = 'visible' - tooltipRef.current.style.top = `${e.pageY + 15}px` - tooltipRef.current.style.left = `${e.pageX + 15}px` + if (!tooltipRef.current) { + return } + + // calculate position after tooltip rendering (do update as synchronous operation) + if (e.type === 'mouseenter') { + flushSync(() => { setHoveredData(d.data) }) + } + + tooltipRef.current.style.top = `${e.pageY + 15}px` + tooltipRef.current.style.left = (window.innerWidth < (tooltipRef.current.scrollWidth + e.pageX + 20)) + ? `${e.pageX - tooltipRef.current.scrollWidth - 15}px` + : `${e.pageX + 15}px` + tooltipRef.current.style.visibility = 'visible' } const onMouseLeaveSlice = (e: MouseEvent) => { @@ -91,6 +108,7 @@ const DonutChart = (props: IProps) => { if (tooltipRef.current) { tooltipRef.current.style.visibility = 'hidden' + setHoveredData(null) } } @@ -148,11 +166,26 @@ const DonutChart = (props: IProps) => { .on('mouseenter mousemove', onMouseEnterSlice) .on('mouseleave', onMouseLeaveSlice) .append('tspan') - .text((d) => (isShowLabel(d) ? `: ${renderLabel ? renderLabel(d.value) : truncateNumberToRange(d.value)}` : '')) + .text((d) => { + if (!isShowLabel(d)) { + return '' + } + + if (renderLabel) { + return renderLabel(d.data) + } + + const separator = ': ' + if (labelAs === 'percentage') { + return `${separator}${getPercentage(d.value, sum)}%` + } + + return `${separator}${truncateNumberToRange(d.value)}` + }) .attr('class', cx(styles.chartLabelValue, classNames?.arcLabelValue)) }, [data]) - if (!data.length || sumBy(data, 'value') === 0) { + if (!data.length || sum === 0) { return null } @@ -163,7 +196,9 @@ const DonutChart = (props: IProps) => { className={cx(styles.tooltip, classNames?.tooltip)} data-testid="chart-value-tooltip" ref={tooltipRef} - /> + > + {(renderTooltip && hoveredData) ? renderTooltip(hoveredData) : (hoveredData?.value || '')} + {title && (
{title} 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 2fda31fb86..ed34a35b87 100644 --- a/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss +++ b/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss @@ -11,12 +11,12 @@ .tooltip { position: fixed; - background: var(--separatorColor); + background: var(--euiTooltipBackgroundColor); color: var(--htmlColor); padding: 10px; visibility: hidden; border-radius: 4px; - z-index: 5; + z-index: 100; } .chartLabel { diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.tsx b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.tsx index 9ca13fe647..3eaa6a4778 100644 --- a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.tsx +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.tsx @@ -1,26 +1,58 @@ import { EuiIcon, EuiTitle } from '@elastic/eui' import cx from 'classnames' +import { sumBy } from 'lodash' import React, { useEffect, useState } from 'react' 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 { ModifiedClusterNodes } from 'uiSrc/pages/clusterDetails/ClusterDetailsPage' import { formatBytes, Nullable } from 'uiSrc/utils' -import { numberWithSpaces } from 'uiSrc/utils/numbers' +import { getPercentage, numberWithSpaces } from 'uiSrc/utils/numbers' import styles from './styles.module.scss' const ClusterDetailsGraphics = ({ nodes, loading }: { nodes: Nullable, loading: boolean }) => { const [memoryData, setMemoryData] = useState([]) + const [memorySum, setMemorySum] = useState(0) const [keysData, setKeysData] = useState([]) + const [keysSum, setKeysSum] = useState(0) - const renderMemoryLabel = (value: number) => formatBytes(value, 1, false) as string - const renderMemoryTooltip = (value: number) => `${numberWithSpaces(value)} B` + const renderMemoryTooltip = (data: ChartData) => ( +
+
+ {data.name}: + {data.meta?.host}:{data.meta?.port} +
+ + {getPercentage(data.value, memorySum)}% + ( {formatBytes(data.value, 3, false)} ) + +
+ ) + + const renderKeysTooltip = (data: ChartData) => ( +
+
+ {data.name}: + {data.meta?.host}:{data.meta?.port} +
+ + {getPercentage(data.value, keysSum)}% + ( {numberWithSpaces(data.value)} ) + +
+ ) useEffect(() => { if (nodes) { - setMemoryData(nodes.map((n) => ({ value: n.usedMemory, name: n.letter, color: n.color })) as ChartData[]) - setKeysData(nodes.map((n) => ({ value: n.totalKeys, name: n.letter, color: n.color })) as ChartData[]) + const memory = nodes.map((n) => ({ value: n.usedMemory, name: n.letter, color: n.color, meta: { ...n } })) + const keys = nodes.map((n) => ({ value: n.totalKeys, name: n.letter, color: n.color, meta: { ...n } })) + + setMemoryData(memory as ChartData[]) + setKeysData(keys as ChartData[]) + + setMemorySum(sumBy(memory, 'value')) + setKeysSum(sumBy(keys, 'value')) } }, [nodes]) @@ -42,27 +74,36 @@ const ClusterDetailsGraphics = ({ nodes, loading }: { nodes: Nullable - - - Memory - +
+
+ + + Memory + +
+
+
{formatBytes(memorySum, 3)}
)} /> - - - Keys - +
+
+ + + Keys + +
+
+
{numberWithSpaces(keysSum)}
)} /> diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/styles.module.scss b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/styles.module.scss index 3b587cd5b4..7332f90891 100644 --- a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/styles.module.scss +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/styles.module.scss @@ -11,6 +11,12 @@ margin-top: 36px; } + .chartCenter { + display: flex; + flex-direction: column; + align-items: center; + } + .chartTitle { display: flex; align-items: center; @@ -20,6 +26,20 @@ } } + .titleSeparator { + height: 1px; + border: 0; + background-color: var(--separatorColorLight); + margin: 6px 0; + width: 60px; + } + + .centerCount { + margin-top: 2px; + font-weight: 500; + font-size: 14px; + } + .preloaderCircle { width: 180px; height: 180px; @@ -27,4 +47,16 @@ border-radius: 100%; background-color: var(--separatorColor); } + + .labelTooltip { + font-size: 12px; + + .tooltipPercentage { + margin-right: 6px; + } + + .tooltipTitle { + margin-bottom: 6px; + } + } } diff --git a/redisinsight/ui/src/utils/numbers.ts b/redisinsight/ui/src/utils/numbers.ts index eb8189d872..cc4b67112f 100644 --- a/redisinsight/ui/src/utils/numbers.ts +++ b/redisinsight/ui/src/utils/numbers.ts @@ -11,3 +11,8 @@ export const nullableNumberWithSpaces = (number: Nullable = 0) => { } return numberWithSpaces(number) } + +export const getPercentage = (value = 0, sum = 1, round = false, decimals = 2) => { + const percent = parseFloat(((value / sum) * 100).toFixed(decimals)) + return round ? Math.round(percent) : percent +} diff --git a/yarn.lock b/yarn.lock index acfba96dc3..ba1b26827b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2781,19 +2781,19 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^17.0.1": - version "17.0.37" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" - integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== +"@types/react@*", "@types/react@^18.0.20": + version "18.0.20" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.20.tgz#e4c36be3a55eb5b456ecf501bd4a00fd4fd0c9ab" + integrity sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^18.0.20": - version "18.0.20" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.20.tgz#e4c36be3a55eb5b456ecf501bd4a00fd4fd0c9ab" - integrity sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA== +"@types/react@^17.0.1": + version "17.0.37" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" + integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*"