Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 48 additions & 13 deletions redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
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'

export interface ChartData {
value: number
name: string
color: RGBColor
meta?: {
[key: string]: any
}
}

interface IProps {
Expand All @@ -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
Expand All @@ -47,17 +53,20 @@ const DonutChart = (props: IProps) => {
title,
config,
classNames,
labelAs = 'value',
renderLabel,
renderTooltip = (v) => v,
renderTooltip,
} = props

const margin = config?.margin || 98
const radius = config?.radius || (width / 2 - margin)
const arcWidth = config?.arcWidth || 8
const percentToShowLabel = config?.percentToShowLabel || 5

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

const arc = d3.arc<d3.PieArcDatum<ChartData>>()
.outerRadius(radius)
Expand All @@ -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) => {
Expand All @@ -91,6 +108,7 @@ const DonutChart = (props: IProps) => {

if (tooltipRef.current) {
tooltipRef.current.style.visibility = 'hidden'
setHoveredData(null)
}
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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 || '')}
</div>
{title && (
<div className={styles.innerTextContainer}>
{title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ModifiedClusterNodes[]>, loading: boolean }) => {
const [memoryData, setMemoryData] = useState<ChartData[]>([])
const [memorySum, setMemorySum] = useState(0)
const [keysData, setKeysData] = useState<ChartData[]>([])
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) => (
<div className={styles.labelTooltip}>
<div className={styles.tooltipTitle}>
<span data-testid="tooltip-node-name">{data.name}: </span>
<span data-testid="tooltip-host-port">{data.meta?.host}:{data.meta?.port}</span>
</div>
<b>
<span className={styles.tooltipPercentage} data-testid="tooltip-node-percent">{getPercentage(data.value, memorySum)}%</span>
<span data-testid="tooltip-total-memory">(&thinsp;{formatBytes(data.value, 3, false)}&thinsp;)</span>
</b>
</div>
)

const renderKeysTooltip = (data: ChartData) => (
<div className={styles.labelTooltip}>
<div className={styles.tooltipTitle}>
<span data-testid="tooltip-node-name">{data.name}: </span>
<span data-testid="tooltip-host-port">{data.meta?.host}:{data.meta?.port}</span>
</div>
<b>
<span className={styles.tooltipPercentage} data-testid="tooltip-node-percent">{getPercentage(data.value, keysSum)}%</span>
<span data-testid="tooltip-total-keys">(&thinsp;{numberWithSpaces(data.value)}&thinsp;)</span>
</b>
</div>
)

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])

Expand All @@ -42,27 +74,36 @@ const ClusterDetailsGraphics = ({ nodes, loading }: { nodes: Nullable<ModifiedCl
<DonutChart
name="memory"
data={memoryData}
renderLabel={renderMemoryLabel}
renderTooltip={renderMemoryTooltip}
labelAs="percentage"
title={(
<div className={styles.chartTitle} data-testid="donut-title-memory">
<EuiIcon type={MemoryIconSvg} className={styles.icon} size="m" />
<EuiTitle size="xs">
<span>Memory</span>
</EuiTitle>
<div className={styles.chartCenter}>
<div className={styles.chartTitle} data-testid="donut-title-memory">
<EuiIcon type={MemoryIconSvg} className={styles.icon} size="m" />
<EuiTitle size="xs">
<span>Memory</span>
</EuiTitle>
</div>
<hr className={styles.titleSeparator} />
<div className={styles.centerCount}>{formatBytes(memorySum, 3)}</div>
</div>
)}
/>
<DonutChart
name="keys"
data={keysData}
renderTooltip={numberWithSpaces}
renderTooltip={renderKeysTooltip}
labelAs="percentage"
title={(
<div className={styles.chartTitle} data-testid="donut-title-keys">
<EuiIcon type={KeyIconSvg} className={styles.icon} size="m" />
<EuiTitle size="xs">
<span>Keys</span>
</EuiTitle>
<div className={styles.chartCenter}>
<div className={styles.chartTitle} data-testid="donut-title-keys">
<EuiIcon type={KeyIconSvg} className={styles.icon} size="m" />
<EuiTitle size="xs">
<span>Keys</span>
</EuiTitle>
</div>
<hr className={styles.titleSeparator} />
<div className={styles.centerCount}>{numberWithSpaces(keysSum)}</div>
</div>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
margin-top: 36px;
}

.chartCenter {
display: flex;
flex-direction: column;
align-items: center;
}

.chartTitle {
display: flex;
align-items: center;
Expand All @@ -20,11 +26,37 @@
}
}

.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;
margin: 60px 0;
border-radius: 100%;
background-color: var(--separatorColor);
}

.labelTooltip {
font-size: 12px;

.tooltipPercentage {
margin-right: 6px;
}

.tooltipTitle {
margin-bottom: 6px;
}
}
}
5 changes: 5 additions & 0 deletions redisinsight/ui/src/utils/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export const nullableNumberWithSpaces = (number: Nullable<number> = 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
}
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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" "*"
Expand Down