Skip to content

Commit ee7ab37

Browse files
authored
Merge pull request #1207 from RedisInsight/fe/feature/RI-3585_update-donut-charts
#RI-3585 - add endpoints, percentage and totals to Memory and Keys graphs
2 parents 103f63f + 432d591 commit ee7ab37

File tree

5 files changed

+145
-32
lines changed

5 files changed

+145
-32
lines changed

redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import cx from 'classnames'
22
import * as d3 from 'd3'
33
import { sumBy } from 'lodash'
4-
import React, { useEffect, useRef } from 'react'
5-
import { truncateNumberToRange } from 'uiSrc/utils'
4+
import React, { useEffect, useRef, useState } from 'react'
5+
import { flushSync } from 'react-dom'
6+
import { Nullable, truncateNumberToRange } from 'uiSrc/utils'
67
import { rgb, RGBColor } from 'uiSrc/utils/colors'
8+
import { getPercentage } from 'uiSrc/utils/numbers'
79

810
import styles from './styles.module.scss'
911

1012
export interface ChartData {
1113
value: number
1214
name: string
1315
color: RGBColor
16+
meta?: {
17+
[key: string]: any
18+
}
1419
}
1520

1621
interface IProps {
@@ -32,8 +37,9 @@ interface IProps {
3237
arcLabelValue?: string
3338
tooltip?: string
3439
}
35-
renderLabel?: (value: number) => string
36-
renderTooltip?: (value: number) => string
40+
renderLabel?: (data: ChartData) => string
41+
renderTooltip?: (data: ChartData) => React.ReactElement | string
42+
labelAs?: 'value' | 'percentage'
3743
}
3844

3945
const ANIMATION_DURATION_MS = 100
@@ -47,17 +53,20 @@ const DonutChart = (props: IProps) => {
4753
title,
4854
config,
4955
classNames,
56+
labelAs = 'value',
5057
renderLabel,
51-
renderTooltip = (v) => v,
58+
renderTooltip,
5259
} = props
5360

5461
const margin = config?.margin || 98
5562
const radius = config?.radius || (width / 2 - margin)
5663
const arcWidth = config?.arcWidth || 8
5764
const percentToShowLabel = config?.percentToShowLabel || 5
5865

66+
const [hoveredData, setHoveredData] = useState<Nullable<ChartData>>(null)
5967
const svgRef = useRef<SVGSVGElement>(null)
6068
const tooltipRef = useRef<HTMLDivElement>(null)
69+
const sum = sumBy(data, 'value')
6170

6271
const arc = d3.arc<d3.PieArcDatum<ChartData>>()
6372
.outerRadius(radius)
@@ -74,12 +83,20 @@ const DonutChart = (props: IProps) => {
7483
.duration(ANIMATION_DURATION_MS)
7584
.attr('d', arcHover)
7685

77-
if (tooltipRef.current) {
78-
tooltipRef.current.innerHTML = `${d.data.name}: ${renderTooltip(d.value)}`
79-
tooltipRef.current.style.visibility = 'visible'
80-
tooltipRef.current.style.top = `${e.pageY + 15}px`
81-
tooltipRef.current.style.left = `${e.pageX + 15}px`
86+
if (!tooltipRef.current) {
87+
return
8288
}
89+
90+
// calculate position after tooltip rendering (do update as synchronous operation)
91+
if (e.type === 'mouseenter') {
92+
flushSync(() => { setHoveredData(d.data) })
93+
}
94+
95+
tooltipRef.current.style.top = `${e.pageY + 15}px`
96+
tooltipRef.current.style.left = (window.innerWidth < (tooltipRef.current.scrollWidth + e.pageX + 20))
97+
? `${e.pageX - tooltipRef.current.scrollWidth - 15}px`
98+
: `${e.pageX + 15}px`
99+
tooltipRef.current.style.visibility = 'visible'
83100
}
84101

85102
const onMouseLeaveSlice = (e: MouseEvent) => {
@@ -91,6 +108,7 @@ const DonutChart = (props: IProps) => {
91108

92109
if (tooltipRef.current) {
93110
tooltipRef.current.style.visibility = 'hidden'
111+
setHoveredData(null)
94112
}
95113
}
96114

@@ -148,11 +166,26 @@ const DonutChart = (props: IProps) => {
148166
.on('mouseenter mousemove', onMouseEnterSlice)
149167
.on('mouseleave', onMouseLeaveSlice)
150168
.append('tspan')
151-
.text((d) => (isShowLabel(d) ? `: ${renderLabel ? renderLabel(d.value) : truncateNumberToRange(d.value)}` : ''))
169+
.text((d) => {
170+
if (!isShowLabel(d)) {
171+
return ''
172+
}
173+
174+
if (renderLabel) {
175+
return renderLabel(d.data)
176+
}
177+
178+
const separator = ': '
179+
if (labelAs === 'percentage') {
180+
return `${separator}${getPercentage(d.value, sum)}%`
181+
}
182+
183+
return `${separator}${truncateNumberToRange(d.value)}`
184+
})
152185
.attr('class', cx(styles.chartLabelValue, classNames?.arcLabelValue))
153186
}, [data])
154187

155-
if (!data.length || sumBy(data, 'value') === 0) {
188+
if (!data.length || sum === 0) {
156189
return null
157190
}
158191

@@ -163,7 +196,9 @@ const DonutChart = (props: IProps) => {
163196
className={cx(styles.tooltip, classNames?.tooltip)}
164197
data-testid="chart-value-tooltip"
165198
ref={tooltipRef}
166-
/>
199+
>
200+
{(renderTooltip && hoveredData) ? renderTooltip(hoveredData) : (hoveredData?.value || '')}
201+
</div>
167202
{title && (
168203
<div className={styles.innerTextContainer}>
169204
{title}

redisinsight/ui/src/components/charts/donut-chart/styles.module.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111

1212
.tooltip {
1313
position: fixed;
14-
background: var(--separatorColor);
14+
background: var(--euiTooltipBackgroundColor);
1515
color: var(--htmlColor);
1616
padding: 10px;
1717
visibility: hidden;
1818
border-radius: 4px;
19-
z-index: 5;
19+
z-index: 100;
2020
}
2121

2222
.chartLabel {

redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.tsx

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,58 @@
11
import { EuiIcon, EuiTitle } from '@elastic/eui'
22
import cx from 'classnames'
3+
import { sumBy } from 'lodash'
34
import React, { useEffect, useState } from 'react'
45
import { DonutChart } from 'uiSrc/components/charts'
56
import { ChartData } from 'uiSrc/components/charts/donut-chart/DonutChart'
67
import { KeyIconSvg, MemoryIconSvg } from 'uiSrc/components/database-overview/components/icons'
78
import { ModifiedClusterNodes } from 'uiSrc/pages/clusterDetails/ClusterDetailsPage'
89
import { formatBytes, Nullable } from 'uiSrc/utils'
9-
import { numberWithSpaces } from 'uiSrc/utils/numbers'
10+
import { getPercentage, numberWithSpaces } from 'uiSrc/utils/numbers'
1011

1112
import styles from './styles.module.scss'
1213

1314
const ClusterDetailsGraphics = ({ nodes, loading }: { nodes: Nullable<ModifiedClusterNodes[]>, loading: boolean }) => {
1415
const [memoryData, setMemoryData] = useState<ChartData[]>([])
16+
const [memorySum, setMemorySum] = useState(0)
1517
const [keysData, setKeysData] = useState<ChartData[]>([])
18+
const [keysSum, setKeysSum] = useState(0)
1619

17-
const renderMemoryLabel = (value: number) => formatBytes(value, 1, false) as string
18-
const renderMemoryTooltip = (value: number) => `${numberWithSpaces(value)} B`
20+
const renderMemoryTooltip = (data: ChartData) => (
21+
<div className={styles.labelTooltip}>
22+
<div className={styles.tooltipTitle}>
23+
<span data-testid="tooltip-node-name">{data.name}: </span>
24+
<span data-testid="tooltip-host-port">{data.meta?.host}:{data.meta?.port}</span>
25+
</div>
26+
<b>
27+
<span className={styles.tooltipPercentage} data-testid="tooltip-node-percent">{getPercentage(data.value, memorySum)}%</span>
28+
<span data-testid="tooltip-total-memory">(&thinsp;{formatBytes(data.value, 3, false)}&thinsp;)</span>
29+
</b>
30+
</div>
31+
)
32+
33+
const renderKeysTooltip = (data: ChartData) => (
34+
<div className={styles.labelTooltip}>
35+
<div className={styles.tooltipTitle}>
36+
<span data-testid="tooltip-node-name">{data.name}: </span>
37+
<span data-testid="tooltip-host-port">{data.meta?.host}:{data.meta?.port}</span>
38+
</div>
39+
<b>
40+
<span className={styles.tooltipPercentage} data-testid="tooltip-node-percent">{getPercentage(data.value, keysSum)}%</span>
41+
<span data-testid="tooltip-total-keys">(&thinsp;{numberWithSpaces(data.value)}&thinsp;)</span>
42+
</b>
43+
</div>
44+
)
1945

2046
useEffect(() => {
2147
if (nodes) {
22-
setMemoryData(nodes.map((n) => ({ value: n.usedMemory, name: n.letter, color: n.color })) as ChartData[])
23-
setKeysData(nodes.map((n) => ({ value: n.totalKeys, name: n.letter, color: n.color })) as ChartData[])
48+
const memory = nodes.map((n) => ({ value: n.usedMemory, name: n.letter, color: n.color, meta: { ...n } }))
49+
const keys = nodes.map((n) => ({ value: n.totalKeys, name: n.letter, color: n.color, meta: { ...n } }))
50+
51+
setMemoryData(memory as ChartData[])
52+
setKeysData(keys as ChartData[])
53+
54+
setMemorySum(sumBy(memory, 'value'))
55+
setKeysSum(sumBy(keys, 'value'))
2456
}
2557
}, [nodes])
2658

@@ -42,27 +74,36 @@ const ClusterDetailsGraphics = ({ nodes, loading }: { nodes: Nullable<ModifiedCl
4274
<DonutChart
4375
name="memory"
4476
data={memoryData}
45-
renderLabel={renderMemoryLabel}
4677
renderTooltip={renderMemoryTooltip}
78+
labelAs="percentage"
4779
title={(
48-
<div className={styles.chartTitle} data-testid="donut-title-memory">
49-
<EuiIcon type={MemoryIconSvg} className={styles.icon} size="m" />
50-
<EuiTitle size="xs">
51-
<span>Memory</span>
52-
</EuiTitle>
80+
<div className={styles.chartCenter}>
81+
<div className={styles.chartTitle} data-testid="donut-title-memory">
82+
<EuiIcon type={MemoryIconSvg} className={styles.icon} size="m" />
83+
<EuiTitle size="xs">
84+
<span>Memory</span>
85+
</EuiTitle>
86+
</div>
87+
<hr className={styles.titleSeparator} />
88+
<div className={styles.centerCount}>{formatBytes(memorySum, 3)}</div>
5389
</div>
5490
)}
5591
/>
5692
<DonutChart
5793
name="keys"
5894
data={keysData}
59-
renderTooltip={numberWithSpaces}
95+
renderTooltip={renderKeysTooltip}
96+
labelAs="percentage"
6097
title={(
61-
<div className={styles.chartTitle} data-testid="donut-title-keys">
62-
<EuiIcon type={KeyIconSvg} className={styles.icon} size="m" />
63-
<EuiTitle size="xs">
64-
<span>Keys</span>
65-
</EuiTitle>
98+
<div className={styles.chartCenter}>
99+
<div className={styles.chartTitle} data-testid="donut-title-keys">
100+
<EuiIcon type={KeyIconSvg} className={styles.icon} size="m" />
101+
<EuiTitle size="xs">
102+
<span>Keys</span>
103+
</EuiTitle>
104+
</div>
105+
<hr className={styles.titleSeparator} />
106+
<div className={styles.centerCount}>{numberWithSpaces(keysSum)}</div>
66107
</div>
67108
)}
68109
/>

redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/styles.module.scss

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
margin-top: 36px;
1212
}
1313

14+
.chartCenter {
15+
display: flex;
16+
flex-direction: column;
17+
align-items: center;
18+
}
19+
1420
.chartTitle {
1521
display: flex;
1622
align-items: center;
@@ -20,11 +26,37 @@
2026
}
2127
}
2228

29+
.titleSeparator {
30+
height: 1px;
31+
border: 0;
32+
background-color: var(--separatorColorLight);
33+
margin: 6px 0;
34+
width: 60px;
35+
}
36+
37+
.centerCount {
38+
margin-top: 2px;
39+
font-weight: 500;
40+
font-size: 14px;
41+
}
42+
2343
.preloaderCircle {
2444
width: 180px;
2545
height: 180px;
2646
margin: 60px 0;
2747
border-radius: 100%;
2848
background-color: var(--separatorColor);
2949
}
50+
51+
.labelTooltip {
52+
font-size: 12px;
53+
54+
.tooltipPercentage {
55+
margin-right: 6px;
56+
}
57+
58+
.tooltipTitle {
59+
margin-bottom: 6px;
60+
}
61+
}
3062
}

redisinsight/ui/src/utils/numbers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ export const nullableNumberWithSpaces = (number: Nullable<number> = 0) => {
1111
}
1212
return numberWithSpaces(number)
1313
}
14+
15+
export const getPercentage = (value = 0, sum = 1, round = false, decimals = 2) => {
16+
const percent = parseFloat(((value / sum) * 100).toFixed(decimals))
17+
return round ? Math.round(percent) : percent
18+
}

0 commit comments

Comments
 (0)