Skip to content

Commit 92cb8df

Browse files
committed
#RI-3368 - add memory & keys charts for cluster details
1 parent 06f6b4c commit 92cb8df

File tree

23 files changed

+1159
-55
lines changed

23 files changed

+1159
-55
lines changed

jest.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
'rehype-stringify': '<rootDir>/redisinsight/__mocks__/rehypeStringify.js',
1717
'unist-util-visit': '<rootDir>/redisinsight/__mocks__/unistUtilsVisit.js',
1818
'react-children-utilities': '<rootDir>/redisinsight/__mocks__/react-children-utilities.js',
19+
d3: '<rootDir>/node_modules/d3/dist/d3.min.js',
1920
},
2021
setupFiles: [
2122
'<rootDir>/redisinsight/ui/src/setup-env.ts',
@@ -38,6 +39,11 @@ module.exports = {
3839
transformIgnorePatterns: [
3940
'node_modules/(?!(monaco-editor|react-monaco-editor)/)',
4041
],
42+
// TODO: add tests for plugins
43+
modulePathIgnorePatterns: [
44+
'<rootDir>/redisinsight/ui/src/packages',
45+
'<rootDir>/redisinsight/ui/src/mocks',
46+
],
4147
coverageThreshold: {
4248
global: {
4349
statements: 70,

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"@testing-library/user-event": "^14.4.3",
111111
"@types/axios": "^0.14.0",
112112
"@types/classnames": "^2.2.11",
113+
"@types/d3": "^7.4.0",
113114
"@types/date-fns": "^2.6.0",
114115
"@types/detect-port": "^1.3.0",
115116
"@types/electron-store": "^3.2.0",
@@ -216,6 +217,7 @@
216217
"buffer": "^6.0.3",
217218
"classnames": "^2.3.1",
218219
"connection-string": "^4.3.2",
220+
"d3": "^7.6.1",
219221
"date-fns": "^2.16.1",
220222
"detect-port": "^1.3.0",
221223
"electron-context-menu": "^3.1.0",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react'
2+
import { render, screen, fireEvent } from 'uiSrc/utils/test-utils'
3+
4+
import DonutChart, { ChartData } from './DonutChart'
5+
6+
const mockData: ChartData[] = [
7+
{ value: 1, name: 'A', color: [0, 0, 0] },
8+
{ value: 5, name: 'B', color: [10, 10, 10] },
9+
{ value: 10, name: 'C', color: [20, 20, 20] },
10+
{ value: 2, name: 'D', color: [30, 30, 30] },
11+
{ value: 30, name: 'E', color: [40, 40, 40] },
12+
{ value: 15, name: 'F', color: [50, 50, 50] },
13+
]
14+
15+
describe('DonutChart', () => {
16+
it('should render with empty data', () => {
17+
expect(render(<DonutChart data={[]} />)).toBeTruthy()
18+
})
19+
20+
it('should render with data', () => {
21+
expect(render(<DonutChart data={mockData} />)).toBeTruthy()
22+
})
23+
24+
it('should render svg', () => {
25+
render(<DonutChart data={mockData} name="test" />)
26+
expect(screen.getByTestId('donut-test')).toBeInTheDocument()
27+
})
28+
29+
it('should render arcs and labels', () => {
30+
render(<DonutChart data={mockData} />)
31+
mockData.forEach(({ value, name }) => {
32+
expect(screen.getByTestId(`arc-${name}-${value}`)).toBeInTheDocument()
33+
expect(screen.getByTestId(`label-${name}-${value}`)).toBeInTheDocument()
34+
})
35+
})
36+
37+
it('should do not render label value if value less than 5%', () => {
38+
render(<DonutChart data={mockData} config={{ percentToShowLabel: 5 }} />)
39+
expect(screen.getByTestId('label-A-1')).toHaveTextContent('')
40+
})
41+
42+
it('should render label value if value more than 5%', () => {
43+
render(<DonutChart data={mockData} config={{ percentToShowLabel: 5 }} />)
44+
expect(screen.getByTestId('label-E-30')).toHaveTextContent('E: 30')
45+
})
46+
47+
it('should call render tooltip and label methods', () => {
48+
const renderLabel = jest.fn()
49+
const renderTooltip = jest.fn()
50+
render(<DonutChart data={mockData} renderLabel={renderLabel} renderTooltip={renderTooltip} />)
51+
expect(renderLabel).toBeCalled()
52+
53+
fireEvent.mouseEnter(screen.getByTestId('arc-A-1'))
54+
expect(renderTooltip).toBeCalled()
55+
})
56+
57+
it('should set tooltip as visible on hover and hidden on leave', () => {
58+
render(<DonutChart data={mockData} />)
59+
60+
fireEvent.mouseEnter(screen.getByTestId('arc-A-1'))
61+
expect(screen.getByTestId('chart-value-tooltip')).toBeVisible()
62+
63+
fireEvent.mouseLeave(screen.getByTestId('arc-A-1'))
64+
expect(screen.getByTestId('chart-value-tooltip')).not.toBeVisible()
65+
})
66+
})
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import cx from 'classnames'
2+
import * as d3 from 'd3'
3+
import React, { useEffect, useRef } from 'react'
4+
import { truncateNumberToRange } from 'uiSrc/utils'
5+
import { rgb, RGBColor } from 'uiSrc/utils/colors'
6+
7+
import styles from './styles.module.scss'
8+
9+
export interface ChartData {
10+
value: number
11+
name: string
12+
color: RGBColor
13+
}
14+
15+
interface IProps {
16+
name?: string
17+
data: ChartData[]
18+
width?: number
19+
height?: number
20+
title?: React.ReactElement | string
21+
config?: {
22+
percentToShowLabel?: number
23+
arcWidth?: number
24+
margin?: number
25+
radius?: number
26+
}
27+
classNames?: {
28+
chart?: string
29+
arc?: string
30+
arcLabel?: string
31+
arcLabelValue?: string
32+
tooltip?: string
33+
}
34+
renderLabel?: (value: number) => string
35+
renderTooltip?: (value: number) => string
36+
}
37+
38+
const ANIMATION_DURATION_MS = 100
39+
40+
const DonutChart = (props: IProps) => {
41+
const {
42+
name = '',
43+
data,
44+
width = 328,
45+
height = 300,
46+
title,
47+
config,
48+
classNames,
49+
renderLabel,
50+
renderTooltip = (v) => v,
51+
} = props
52+
53+
const margin = config?.margin || 72
54+
const radius = config?.radius || (width / 2 - margin)
55+
const arcWidth = config?.arcWidth || 8
56+
const percentToShowLabel = config?.percentToShowLabel || 5
57+
58+
const svgRef = useRef<SVGSVGElement>(null)
59+
const tooltipRef = useRef<HTMLDivElement>(null)
60+
61+
const arc = d3.arc<d3.PieArcDatum<ChartData>>()
62+
.outerRadius(radius)
63+
.innerRadius(radius - arcWidth)
64+
65+
const arcHover = d3.arc<d3.PieArcDatum<ChartData>>()
66+
.outerRadius(radius + 4)
67+
.innerRadius(radius - arcWidth)
68+
69+
const onMouseEnterSlice = (e: MouseEvent, d: d3.PieArcDatum<ChartData>) => {
70+
d3
71+
.select<SVGPathElement, d3.PieArcDatum<ChartData>>(e.target as SVGPathElement)
72+
.transition()
73+
.duration(ANIMATION_DURATION_MS)
74+
.attr('d', arcHover)
75+
76+
if (tooltipRef.current) {
77+
tooltipRef.current.innerHTML = `${d.data.name}: ${renderTooltip(d.value)}`
78+
tooltipRef.current.style.visibility = 'visible'
79+
tooltipRef.current.style.top = `${e.pageY + 15}px`
80+
tooltipRef.current.style.left = `${e.pageX + 15}px`
81+
}
82+
}
83+
84+
const onMouseLeaveSlice = (e: MouseEvent) => {
85+
d3
86+
.select<SVGPathElement, d3.PieArcDatum<ChartData>>(e.target as SVGPathElement)
87+
.transition()
88+
.duration(ANIMATION_DURATION_MS)
89+
.attr('d', arc)
90+
91+
if (tooltipRef.current) {
92+
tooltipRef.current.style.visibility = 'hidden'
93+
}
94+
}
95+
96+
const isShowLabel = (d: d3.PieArcDatum<ChartData>) =>
97+
d.endAngle - d.startAngle > (Math.PI * 2) / (100 / percentToShowLabel)
98+
99+
const getLabelPosition = (d: d3.PieArcDatum<ChartData>) => {
100+
const [x, y] = arc.centroid(d)
101+
const h = Math.sqrt(x * x + y * y)
102+
return `translate(${(x / h) * (radius + 16)}, ${((y + 4) / h) * (radius + 16)})`
103+
}
104+
105+
useEffect(() => {
106+
const pie = d3.pie<ChartData>().value((d: ChartData) => d.value).sort(null)
107+
const dataReady = pie(data)
108+
109+
d3
110+
.select(svgRef.current)
111+
.select('g')
112+
.remove()
113+
114+
const svg = d3
115+
.select(svgRef.current)
116+
.attr('width', width)
117+
.attr('height', height)
118+
.attr('data-testid', `donut-${name}`)
119+
.attr('class', cx(classNames?.chart))
120+
.append('g')
121+
.attr('transform', `translate(${width / 2},${height / 2})`)
122+
123+
// add arcs
124+
svg
125+
.selectAll()
126+
.data(dataReady)
127+
.enter()
128+
.append('path')
129+
.attr('data-testid', (d) => `arc-${d.data.name}-${d.data.value}`)
130+
.attr('d', arc)
131+
.attr('fill', (d) => rgb(d.data.color))
132+
.attr('class', cx(styles.arc, classNames?.arc))
133+
.on('mouseenter mousemove', onMouseEnterSlice)
134+
.on('mouseleave', onMouseLeaveSlice)
135+
136+
// add labels
137+
svg
138+
.selectAll()
139+
.data(dataReady)
140+
.enter()
141+
.append('text')
142+
.attr('class', cx(styles.chartLabel, classNames?.arcLabel))
143+
.attr('transform', getLabelPosition)
144+
.text((d) => (isShowLabel(d) ? d.data.name : ''))
145+
.attr('data-testid', (d) => `label-${d.data.name}-${d.data.value}`)
146+
.style('text-anchor', (d) => ((d.endAngle + d.startAngle) / 2 > Math.PI ? 'end' : 'start'))
147+
.on('mouseenter mousemove', onMouseEnterSlice)
148+
.on('mouseleave', onMouseLeaveSlice)
149+
.append('tspan')
150+
.text((d) => (isShowLabel(d) ? `: ${renderLabel ? renderLabel(d.value) : truncateNumberToRange(d.value)}` : ''))
151+
.attr('class', cx(styles.chartLabelValue, classNames?.arcLabelValue))
152+
}, [data])
153+
154+
return (
155+
<div className={styles.wrapper}>
156+
<svg ref={svgRef} />
157+
<div
158+
className={cx(styles.tooltip, classNames?.tooltip)}
159+
data-testid="chart-value-tooltip"
160+
ref={tooltipRef}
161+
/>
162+
{title && (
163+
<div className={styles.innerTextContainer}>
164+
{title}
165+
</div>
166+
)}
167+
</div>
168+
)
169+
}
170+
171+
export default DonutChart
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import DonutChart from './DonutChart'
2+
3+
export default DonutChart
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.wrapper {
2+
position: relative;
3+
}
4+
5+
.innerTextContainer {
6+
position: absolute;
7+
top: 50%;
8+
left: 50%;
9+
transform: translate(-50%, -50%);
10+
}
11+
12+
.tooltip {
13+
position: fixed;
14+
background: var(--separatorColor);
15+
color: var(--htmlColor);
16+
padding: 10px;
17+
visibility: hidden;
18+
border-radius: 4px;
19+
z-index: 5;
20+
}
21+
22+
.chartLabel {
23+
fill: var(--euiTextSubduedColor);
24+
font-size: 12px;
25+
font-weight: bold;
26+
27+
.chartLabelValue {
28+
font-weight: normal;
29+
}
30+
}
31+
32+
.arc {
33+
stroke: var(--euiColorLightestShade);
34+
stroke-width: 2px;
35+
cursor: pointer;
36+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import DonutChart from './donut-chart'
2+
3+
export {
4+
DonutChart
5+
}

0 commit comments

Comments
 (0)