Merge pull request #14914 from prometheus/julius/new-ui-improvements

New UI: Better time formatting + tests, better styling
This commit is contained in:
Julius Volz 2024-09-17 08:36:29 +02:00 committed by GitHub
commit 2c87817e40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 147 additions and 63 deletions

View file

@ -0,0 +1,104 @@
import { humanizeDuration, formatPrometheusDuration } from "./formatTime";
describe("formatPrometheusDuration", () => {
test('returns "0s" for 0 milliseconds', () => {
expect(formatPrometheusDuration(0)).toBe("0s");
});
test("formats milliseconds correctly", () => {
expect(formatPrometheusDuration(1)).toBe("1ms");
expect(formatPrometheusDuration(999)).toBe("999ms");
});
test("formats seconds correctly", () => {
expect(formatPrometheusDuration(1000)).toBe("1s");
expect(formatPrometheusDuration(1500)).toBe("1s500ms");
expect(formatPrometheusDuration(59999)).toBe("59s999ms");
});
test("formats minutes correctly", () => {
expect(formatPrometheusDuration(60000)).toBe("1m");
expect(formatPrometheusDuration(120000)).toBe("2m");
expect(formatPrometheusDuration(3599999)).toBe("59m59s999ms");
});
test("formats hours correctly", () => {
expect(formatPrometheusDuration(3600000)).toBe("1h");
expect(formatPrometheusDuration(7200000)).toBe("2h");
expect(formatPrometheusDuration(86399999)).toBe("23h59m59s999ms");
});
test("formats days correctly", () => {
expect(formatPrometheusDuration(86400000)).toBe("1d");
expect(formatPrometheusDuration(172800000)).toBe("2d");
expect(formatPrometheusDuration(86400000 * 365 - 1)).toBe(
"364d23h59m59s999ms"
);
});
test("handles negative durations", () => {
expect(formatPrometheusDuration(-1000)).toBe("-1s");
expect(formatPrometheusDuration(-86400000)).toBe("-1d");
});
test("combines multiple units correctly", () => {
expect(
formatPrometheusDuration(86400000 + 3600000 + 60000 + 1000 + 1)
).toBe("1d1h1m1s1ms");
});
test("omits zero values", () => {
expect(formatPrometheusDuration(86400000 + 1000)).toBe("1d1s");
});
});
describe("humanizeDuration", () => {
test('returns "0s" for 0 milliseconds', () => {
expect(humanizeDuration(0)).toBe("0s");
});
test("formats milliseconds correctly", () => {
expect(humanizeDuration(1)).toBe("1ms");
expect(humanizeDuration(999)).toBe("999ms");
});
test("formats seconds correctly", () => {
expect(humanizeDuration(1000)).toBe("1s");
expect(humanizeDuration(1500)).toBe("1.5s");
expect(humanizeDuration(59999)).toBe("59.999s");
});
test("formats minutes correctly", () => {
expect(humanizeDuration(60000)).toBe("1m");
expect(humanizeDuration(120000)).toBe("2m");
expect(humanizeDuration(3599999)).toBe("59m 59.999s");
});
test("formats hours correctly", () => {
expect(humanizeDuration(3600000)).toBe("1h");
expect(humanizeDuration(7200000)).toBe("2h");
expect(humanizeDuration(86399999)).toBe("23h 59m 59.999s");
});
test("formats days correctly", () => {
expect(humanizeDuration(86400000)).toBe("1d");
expect(humanizeDuration(172800000)).toBe("2d");
expect(humanizeDuration(86400000 * 365 - 1)).toBe("364d 23h 59m 59.999s");
expect(humanizeDuration(86400000 * 365 - 1)).toBe("364d 23h 59m 59.999s");
});
test("handles negative durations", () => {
expect(humanizeDuration(-1000)).toBe("-1s");
expect(humanizeDuration(-86400000)).toBe("-1d");
});
test("combines multiple units correctly", () => {
expect(humanizeDuration(86400000 + 3600000 + 60000 + 1000 + 1)).toBe(
"1d 1h 1m 1.001s"
);
});
test("omits zero values", () => {
expect(humanizeDuration(86400000 + 1000)).toBe("1d 1s");
});
});

View file

@ -45,37 +45,55 @@ export const parsePrometheusDuration = (durationStr: string): number | null => {
return dur; return dur;
}; };
// Format a duration in milliseconds into a Prometheus duration string like "1d2h3m4s". // Used by:
export const formatPrometheusDuration = (d: number): string => { // - formatPrometheusDuration() => "5d5m2s123ms"
let ms = d; // - humanizeDuration() => "5d 5m 2.123s"
let r = ""; const formatDuration = (
if (ms === 0) { d: number,
componentSeparator?: string,
showFractionalSeconds?: boolean
): string => {
if (d === 0) {
return "0s"; return "0s";
} }
const f = (unit: string, mult: number, exact: boolean) => { const sign = d < 0 ? "-" : "";
let ms = Math.abs(d);
const r: string[] = [];
for (const { unit, mult, exact } of [
// Only format years and weeks if the remainder is zero, as it is often
// easier to read 90d than 12w6d.
{ unit: "y", mult: 1000 * 60 * 60 * 24 * 365, exact: true },
{ unit: "w", mult: 1000 * 60 * 60 * 24 * 7, exact: true },
{ unit: "d", mult: 1000 * 60 * 60 * 24, exact: false },
{ unit: "h", mult: 1000 * 60 * 60, exact: false },
{ unit: "m", mult: 1000 * 60, exact: false },
{ unit: "s", mult: 1000, exact: false },
{ unit: "ms", mult: 1, exact: false },
]) {
if (exact && ms % mult !== 0) { if (exact && ms % mult !== 0) {
return; continue;
} }
const v = Math.floor(ms / mult); const v = Math.floor(ms / mult);
if (v > 0) { if (v > 0) {
r += `${v}${unit}`;
ms -= v * mult; ms -= v * mult;
if (showFractionalSeconds && unit === "s" && ms > 0) {
// Show "2.34s" instead of "2s 340ms".
r.push(`${parseFloat((v + ms / 1000).toFixed(3))}s`);
break;
} else {
r.push(`${v}${unit}`);
}
} }
}; }
// Only format years and weeks if the remainder is zero, as it is often return sign + r.join(componentSeparator || "");
// easier to read 90d than 12w6d. };
f("y", 1000 * 60 * 60 * 24 * 365, true);
f("w", 1000 * 60 * 60 * 24 * 7, true);
f("d", 1000 * 60 * 60 * 24, false); // Format a duration in milliseconds into a Prometheus duration string like "1d2h3m4s".
f("h", 1000 * 60 * 60, false); export const formatPrometheusDuration = (d: number): string => {
f("m", 1000 * 60, false); return formatDuration(d);
f("s", 1000, false);
f("ms", 1, false);
return r;
}; };
export function parseTime(timeText: string): number { export function parseTime(timeText: string): number {
@ -85,37 +103,7 @@ export function parseTime(timeText: string): number {
export const now = (): number => dayjs().valueOf(); export const now = (): number => dayjs().valueOf();
export const humanizeDuration = (milliseconds: number): string => { export const humanizeDuration = (milliseconds: number): string => {
if (milliseconds === 0) { return formatDuration(milliseconds, " ", true);
return "0s";
}
const sign = milliseconds < 0 ? "-" : "";
const duration = dayjs.duration(Math.abs(milliseconds), "ms");
const ms = Math.floor(duration.milliseconds());
const s = Math.floor(duration.seconds());
const m = Math.floor(duration.minutes());
const h = Math.floor(duration.hours());
const d = Math.floor(duration.asDays());
const parts: string[] = [];
if (d !== 0) {
parts.push(`${d}d`);
}
if (h !== 0) {
parts.push(`${h}h`);
}
if (m !== 0) {
parts.push(`${m}m`);
}
if (s !== 0) {
if (ms !== 0) {
parts.push(`${s}.${ms}s`);
} else {
parts.push(`${s}s`);
}
} else if (milliseconds !== 0) {
parts.push(`${milliseconds.toFixed(3)}ms`);
}
return sign + parts.join(" ");
}; };
export const humanizeDurationRelative = ( export const humanizeDurationRelative = (

View file

@ -99,7 +99,7 @@ const MetricsExplorer: FC<MetricsExplorerProps> = ({
{items.map((m) => ( {items.map((m) => (
<Table.Tr key={m.original}> <Table.Tr key={m.original}>
<Table.Td> <Table.Td>
<Group justify="space-between"> <Group justify="space-between" wrap="nowrap">
{debouncedFilterText === "" ? ( {debouncedFilterText === "" ? (
m.original m.original
) : ( ) : (

View file

@ -305,10 +305,9 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
<Table> <Table>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th w="30%">Endpoint</Table.Th> <Table.Th w="25%">Endpoint</Table.Th>
<Table.Th>Labels</Table.Th> <Table.Th>Labels</Table.Th>
<Table.Th w="10%">Last scrape</Table.Th> <Table.Th w={230}>Last scrape</Table.Th>
{/* <Table.Th w="10%">Scrape duration</Table.Th> */}
<Table.Th w={100}>State</Table.Th> <Table.Th w={100}>State</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
@ -337,17 +336,12 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
/> />
</Table.Td> </Table.Td>
<Table.Td valign="top"> <Table.Td valign="top">
<Group <Group gap="xs" wrap="wrap">
gap="xs"
wrap="wrap"
justify="space-between"
>
<Tooltip <Tooltip
label="Last target scrape" label="Last target scrape"
withArrow withArrow
> >
<Badge <Badge
w="max-content"
variant="light" variant="light"
className={badgeClasses.statsBadge} className={badgeClasses.statsBadge}
styles={{ styles={{
@ -369,7 +363,6 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
withArrow withArrow
> >
<Badge <Badge
w="max-content"
variant="light" variant="light"
className={badgeClasses.statsBadge} className={badgeClasses.statsBadge}
styles={{ styles={{
@ -390,7 +383,6 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
</Table.Td> </Table.Td>
<Table.Td valign="top"> <Table.Td valign="top">
<Badge <Badge
w="max-content"
className={healthBadgeClass(target.health)} className={healthBadgeClass(target.health)}
> >
{target.health} {target.health}