diff --git a/src/ui/src/containers/format-data/format-data-test.tsx b/src/ui/src/containers/format-data/format-data-test.tsx index 68439bee8ef..c1483989210 100644 --- a/src/ui/src/containers/format-data/format-data-test.tsx +++ b/src/ui/src/containers/format-data/format-data-test.tsx @@ -26,38 +26,89 @@ import { DARK_THEME } from 'app/components'; import { AlertData, BasicDurationRenderer, + dataWithUnitsToString, LatencyDurationRenderer, formatBytes, formatDuration, } from './format-data'; /* eslint-disable react-memo/require-usememo */ -describe('formatters Test', () => { - it('should handle 0 Bytes correctly', () => { - expect(formatBytes(0)).toEqual({ units: '\u00a0B', val: '0' }); - }); - +describe('formatDuration test', () => { it('should handle 0 duration correctly', () => { - expect(formatDuration(0)).toEqual({ units: 'ns', val: '0' }); - }); - it('should handle |x| < 1 Bytes correctly', () => { - expect(formatBytes(0.1)).toEqual({ units: '\u00a0B', val: '0.1' }); + expect(formatDuration(0)).toEqual({ units: ['ns'], val: ['0'] }); }); - it('should handle |x| < 1 duration correctly', () => { - expect(formatDuration(0.1)).toEqual({ units: 'ns', val: '0.1' }); + expect(formatDuration(0.1)).toEqual({ units: ['ns'], val: ['0.1'] }); }); it('should handle x < 0 duration correctly', () => { - expect(formatDuration(-2)).toEqual({ units: 'ns', val: '-2' }); + expect(formatDuration(-2)).toEqual({ units: ['ns'], val: ['-2'] }); + }); + it('should handle large negative durations correctly', () => { + expect(formatDuration(-12 * 60 * 60 * 1000 * 1000 * 1000 - 1335144)).toEqual( + { units: ['hours', 'min'], val: ['-12', '0'] }); + }); + it('should handle nanoseconds correctly', () => { + expect(formatDuration(144)).toEqual({ units: ['ns'], val: ['144'] }); + }); + it('should handle microseconds correctly', () => { + expect(formatDuration(5144)).toEqual({ units: ['\u00b5s'], val: ['5.1'] }); + }); + it('should handle milliseconds correctly', () => { + expect(formatDuration(5 * 1000 * 1000)).toEqual({ units: ['ms'], val: ['5'] }); + }); + it('should handle seconds correctly', () => { + expect(formatDuration(13 * 1000 * 1000 * 1000 + 1242)).toEqual({ units: ['\u00a0s'], val: ['13'] }); + }); + it('should handle minutes correctly', () => { + expect(formatDuration(5 * 60 * 1000 * 1000 * 1000 + 1334)).toEqual( + { units: ['min', 's'], val: ['5', '0'] }); + }); + it('should handle hours correctly', () => { + expect(formatDuration(12 * 60 * 60 * 1000 * 1000 * 1000 + 1335144)).toEqual( + { units: ['hours', 'min'], val: ['12', '0'] }); + }); + it('should handle days correctly', () => { + expect(formatDuration(25 * 24 * 60 * 60 * 1000 * 1000 * 1000 + 133514124)).toEqual( + { units: ['days', 'hours'], val: ['25', '0'] }); + }); +}); + +describe('formatBytes Test', () => { + it('should handle 0 Bytes correctly', () => { + expect(formatBytes(0)).toEqual({ units: ['\u00a0B'], val: ['0'] }); + }); + it('should handle |x| < 1 Bytes correctly', () => { + expect(formatBytes(0.1)).toEqual({ units: ['\u00a0B'], val: ['0.1'] }); }); it('should handle x < 0 bytes correctly', () => { - expect(formatBytes(-2048)).toEqual({ units: 'KB', val: '-2' }); + expect(formatBytes(-2048)).toEqual({ units: ['KB'], val: ['-2'] }); }); it('should handle large bytes correctly', () => { - expect(formatBytes(1024 ** 9)).toEqual({ units: 'YB', val: '1024' }); + expect(formatBytes(1024 ** 9)).toEqual({ units: ['YB'], val: ['1024'] }); + }); +}); + +describe('dataWithUnitsToString test', () => { + it('should handle 0 duration correctly', () => { + expect(dataWithUnitsToString({ units: ['ns'], val: ['0'] })).toEqual('0 ns'); + }); + it('should handle |x| < 1 duration correctly', () => { + expect(dataWithUnitsToString({ units: ['ns'], val: ['0.1'] })).toEqual( + '0.1 ns'); }); it('should handle large durations correctly', () => { - expect(formatDuration(1000 ** 4)).toEqual({ units: '\u00A0s', val: '1000' }); + expect(dataWithUnitsToString({ units: ['min', 's'], val: ['10', '35'] })).toEqual( + '10 min 35 s'); + }); + it('should handle large negative durations correctly', () => { + expect(dataWithUnitsToString({ units: ['min', 's'], val: ['-10', '35'] })).toEqual( + '-10 min 35 s'); + }); + it('should handle x < 0 bytes correctly', () => { + expect(dataWithUnitsToString({ units: ['KB'], val: ['-2'] })).toEqual('-2 KB'); + }); + it('should handle bytes correctly', () => { + expect(dataWithUnitsToString({ units: ['YB'], val: ['1024'] })).toEqual('1024 YB'); }); }); diff --git a/src/ui/src/containers/format-data/format-data.tsx b/src/ui/src/containers/format-data/format-data.tsx index 3d2fcb6246c..e550f39eb79 100644 --- a/src/ui/src/containers/format-data/format-data.tsx +++ b/src/ui/src/containers/format-data/format-data.tsx @@ -91,10 +91,14 @@ export const PortRenderer: React.FC<{ data: any }> = React.memo( PortRenderer.displayName = 'PortRenderer'; export interface DataWithUnits { - val: string; - units: string; + val: string[]; + units: string[]; } +export const dataWithUnitsToString = (dataWithUnits: DataWithUnits): string => ( + dataWithUnits?.val.map((v, i) => `${v} ${dataWithUnits.units[i]}`).join(' ') +); + export const formatScaled = (data: number, scale: number, suffixes: string[], decimals = 2): DataWithUnits => { const dm = decimals < 0 ? 0 : decimals; let i = Math.floor(Math.log(Math.abs(data)) / Math.log(scale)); @@ -104,28 +108,61 @@ export const formatScaled = (data: number, scale: number, suffixes: string[], de const units = suffixes[i]; return { - val, - units, + val: [ val ], + units: [ units ], }; }; export const formatBytes = (data: number): DataWithUnits => ( formatScaled(data, 1024, + // \u00a0 is a space, written in order to prevent it from being stripped by React. ['\u00a0B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 1) ); -export const formatDuration = (data: number): DataWithUnits => ( - formatScaled(data, +const nanosPerS = 1000 * 1000 * 1000; +const nanosPerMin = 60 * nanosPerS; +const nanosPerH = 60 * nanosPerMin; +const nanosPerD = 24 * nanosPerH; + +export const formatDuration = (data: number): DataWithUnits => { + const absRounded = Math.abs(Math.round(data)); + const days = Math.floor(absRounded / nanosPerD); + const hours = Math.floor((absRounded % nanosPerD) / nanosPerH); + const min = Math.floor((absRounded % nanosPerH) / nanosPerMin); + const seconds = Math.floor((absRounded % nanosPerMin) / nanosPerS); + + if (days > 0) { + return { + val: [`${days * Math.sign(data)}`, `${hours}`], + units: ['days', 'hours'], + }; + } + if (hours > 0) { + return { + val: [`${hours * Math.sign(data)}`, `${min}`], + units: ['hours', 'min'], + }; + } + if (min > 0) { + return { + val: [`${min * Math.sign(data)}`, `${seconds}`], + units: ['min', 's'], + }; + } + return formatScaled(data, 1000, + // \u00a0 is a space, which can sometimes be stripped by React if written as ' '. + // \u00b5 is μ. ['ns', '\u00b5s', 'ms', '\u00a0s'], - 1) -); + 1); +}; export const formatThroughput = (data: number): DataWithUnits => ( formatScaled(data * 1E9, 1000, + // \u00a0 is a space, which can sometimes be stripped by React if written as ' '. ['\u00a0/s', 'K/s', 'M/s', 'B/s'], 1) ); @@ -133,6 +170,7 @@ export const formatThroughput = (data: number): DataWithUnits => ( export const formatThroughputBytes = (data: number): DataWithUnits => ( formatScaled(data * 1E9, 1024, + // \u00a0 is a space, which can sometimes be stripped by React if written as ' '. ['\u00a0B/s', 'KB/s', 'MB/s', 'GB/s'], 1) ); @@ -152,10 +190,18 @@ const useRenderValueWithUnitsStyles = makeStyles(({ typography }: Theme) => crea const RenderValueWithUnits: React.FC<{ data: DataWithUnits }> = React.memo(({ data }) => { const classes = useRenderValueWithUnitsStyles(); return ( - <> - {`${data.val}\u00A0`} - {data.units} - + + { + // \u00a0 is a space, written in order to prevent it from being stripped by React. + data.val.map((val, i) => ( + + {i > 0 && {'\u00a0'}} + {`${val}\u00A0`} + {data.units[i]} + + )) + } + ); }); RenderValueWithUnits.displayName = 'RenderValueWithUnits'; @@ -230,8 +276,8 @@ export const formatPercent = (data: number): DataWithUnits => { const units = '%'; return { - val, - units, + val: [val], + units: [units], }; }; @@ -286,8 +332,8 @@ export const formatBySemType = (semType: SemanticType, val: any): DataWithUnits return formatFnMd.formatFn(val); } return { - val, - units: '', + val: [ val ], + units: [''], }; }; diff --git a/src/ui/src/containers/live-data-table/renderers.tsx b/src/ui/src/containers/live-data-table/renderers.tsx index 7068b80b620..0c181649445 100644 --- a/src/ui/src/containers/live-data-table/renderers.tsx +++ b/src/ui/src/containers/live-data-table/renderers.tsx @@ -27,6 +27,7 @@ import { AlertData, BytesRenderer, CPUData, + dataWithUnitsToString, DataWithUnits, BasicDurationRenderer, formatDuration, @@ -60,12 +61,6 @@ import { ColumnDisplayInfo, QuantilesDisplayState } from './column-display-info' interface Quant { p50: number; p90: number; p99: number; } -// Helper to durationQuantilesRenderer since it takes in a string, rather than a span -// for p50Display et al. -function dataWithUnitsToString(dataWithUnits: DataWithUnits): string { - return `${dataWithUnits.val} ${dataWithUnits.units}`; -} - interface LiveCellProps { data: any; } diff --git a/src/ui/src/containers/live-widgets/graph/request-graph-manager.ts b/src/ui/src/containers/live-widgets/graph/request-graph-manager.ts index 20726ffc898..7c655b31490 100644 --- a/src/ui/src/containers/live-widgets/graph/request-graph-manager.ts +++ b/src/ui/src/containers/live-widgets/graph/request-graph-manager.ts @@ -18,7 +18,7 @@ import { data as visData } from 'vis-network/standalone'; -import { formatBySemType } from 'app/containers/format-data/format-data'; +import { dataWithUnitsToString, formatBySemType } from 'app/containers/format-data/format-data'; import { deepLinkURLFromSemanticType, EmbedState } from 'app/containers/live-widgets/utils/live-view-params'; import { WidgetDisplay } from 'app/containers/live/vis'; import { Relation, SemanticType } from 'app/types/generated/vizierapi_pb'; @@ -129,7 +129,7 @@ const humanReadableMetric = (value: any, semType: SemanticType, defaultUnits: st return `${formatFloat64Data(value)}${defaultUnits}`; } const valWithUnits = formatBySemType(semType, value); - return `${valWithUnits.val} ${valWithUnits.units}`; + return dataWithUnitsToString(valWithUnits); }; const getEdgeText = (edge: EdgeStats, display: RequestGraphDisplay,