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,