Skip to content

Commit d443ddb

Browse files
authored
Lookout UI: configurable formats for numbers and timestamps (#4272)
Make the Lookout UI display timestamps and numbers in a consistent format across the application. This format can be configured by the user as per their personal preferences.
1 parent 69d5181 commit d443ddb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1573
-285
lines changed

config/lookout/config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,14 @@ uiConfig:
3636
alertMessageMd: |
3737
This will only work if the container is still running.
3838
alertLevel: info
39+
pinnedTimeZoneIdentifiers:
40+
- America/New_York
41+
- Europe/London
42+
- Europe/Paris
43+
- Asia/Tokyo
44+
- Asia/Shanghai
45+
- America/Los_Angeles
46+
- America/Chicago
47+
- Australia/Sydney
48+
- Asia/Dubai
49+
- Asia/Kolkata

internal/lookout/configuration/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,8 @@ type UIConfig struct {
8484
CommandSpecs []CommandSpec
8585

8686
Backend string `json:",omitempty"`
87+
88+
// PinnedTimeZoneIdentifiers is the list of identifiers of IANA time zones to be displayed at
89+
// the top of the time zone selector.
90+
PinnedTimeZoneIdentifiers []string
8791
}

internal/lookoutui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@
3535
"@mui/material": "^6.4.7",
3636
"@tanstack/react-query": "^5.62.3",
3737
"@tanstack/react-table": "^8.7.0",
38-
"date-fns": "^2.29.3",
39-
"date-fns-tz": "^1.3.7",
38+
"dayjs": "^1.11.13",
4039
"js-yaml": "^4.0.0",
4140
"lodash": "^4.17.21",
4241
"markdown-to-jsx": "^7.7.4",
@@ -51,6 +50,7 @@
5150
"react-dom": "^19",
5251
"react-router-dom": "7.2.0",
5352
"react-virtuoso": "^4.12.3",
53+
"timezone-support": "^3.1.0",
5454
"use-debounce": "^10.0.4",
5555
"uuid": "^11.1.0",
5656
"validator": "^13.7.0",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest"
2+
3+
import { formatNumber, NUMBER_NOTATIONS, NumberNotation } from "./formatNumber"
4+
import { SUPPORTED_LOCALES, SupportedLocale } from "./locales"
5+
6+
describe("formatNumber", () => {
7+
describe("using browser locale en-IN", () => {
8+
beforeEach(() => {
9+
vi.stubGlobal("navigator", { language: "en-IN" })
10+
})
11+
12+
afterEach(() => {
13+
vi.unstubAllGlobals()
14+
})
15+
16+
const testCasesLocaleEnIn: [number, Record<NumberNotation, string>][] = [
17+
[-10_000, { standard: "-10,000", compact: "-10K", scientific: "-1E4", engineering: "-10E3" }],
18+
[0, { standard: "0", compact: "0", scientific: "0E0", engineering: "0E0" }],
19+
[0.034, { standard: "0.034", compact: "0.034", scientific: "3.4E-2", engineering: "34E-3" }],
20+
[4, { standard: "4", compact: "4", scientific: "4E0", engineering: "4E0" }],
21+
[5.789, { standard: "5.789", compact: "5.8", scientific: "5.789E0", engineering: "5.789E0" }],
22+
[180, { standard: "180", compact: "180", scientific: "1.8E2", engineering: "180E0" }],
23+
[1_337, { standard: "1,337", compact: "1.3K", scientific: "1.337E3", engineering: "1.337E3" }],
24+
[42_866.29, { standard: "42,866.29", compact: "43K", scientific: "4.287E4", engineering: "42.866E3" }],
25+
[
26+
77_502_040_708,
27+
{ standard: "77,50,20,40,708", compact: "7.8KCr", scientific: "7.75E10", engineering: "77.502E9" },
28+
],
29+
]
30+
31+
testCasesLocaleEnIn.forEach(([n, expectedForNotation]) => {
32+
NUMBER_NOTATIONS.forEach((notation) => {
33+
it(`formats ${n} in ${notation} notation`, () => {
34+
expect(formatNumber(n, { locale: "browser", notation })).eq(expectedForNotation[notation])
35+
})
36+
})
37+
})
38+
})
39+
40+
describe("using locale de", () => {
41+
const testCasesLocaleDe: [number, Record<NumberNotation, string>][] = [
42+
[-10_000, { standard: "-10.000", compact: "-10.000", scientific: "-1E4", engineering: "-10E3" }],
43+
[0, { standard: "0", compact: "0", scientific: "0E0", engineering: "0E0" }],
44+
[0.034, { standard: "0,034", compact: "0,034", scientific: "3,4E-2", engineering: "34E-3" }],
45+
[4, { standard: "4", compact: "4", scientific: "4E0", engineering: "4E0" }],
46+
[5.789, { standard: "5,789", compact: "5,8", scientific: "5,789E0", engineering: "5,789E0" }],
47+
[180, { standard: "180", compact: "180", scientific: "1,8E2", engineering: "180E0" }],
48+
[1_337, { standard: "1.337", compact: "1337", scientific: "1,337E3", engineering: "1,337E3" }],
49+
[42_866.29, { standard: "42.866,29", compact: "42.866", scientific: "4,287E4", engineering: "42,866E3" }],
50+
[
51+
77_502_040_708,
52+
{ standard: "77.502.040.708", compact: "78\u00A0Mrd.", scientific: "7,75E10", engineering: "77,502E9" },
53+
],
54+
]
55+
56+
testCasesLocaleDe.forEach(([n, expectedForNotation]) => {
57+
NUMBER_NOTATIONS.forEach((notation) => {
58+
it(`formats ${n} in ${notation} notation`, () => {
59+
expect(formatNumber(n, { locale: "de", notation })).eq(expectedForNotation[notation])
60+
})
61+
})
62+
})
63+
})
64+
65+
const testCasesStandardNotation: Record<SupportedLocale, string> = {
66+
"en-IN": "49,72,92,23,812",
67+
"en-US": "49,729,223,812",
68+
"en-GB": "49,729,223,812",
69+
"en-AU": "49,729,223,812",
70+
fr: "49\u202F729\u202F223\u202F812",
71+
de: "49.729.223.812",
72+
es: "49.729.223.812",
73+
"es-MX": "49,729,223,812",
74+
"pt-BR": "49.729.223.812",
75+
it: "49.729.223.812",
76+
"zh-CN": "49,729,223,812",
77+
ru: "49\u00A0729\u00A0223\u00A0812",
78+
ja: "49,729,223,812",
79+
ar: "49,729,223,812",
80+
}
81+
82+
SUPPORTED_LOCALES.forEach((locale) => {
83+
it(`formats 49,729,223,812 in standard notation for locale ${locale}`, () => {
84+
expect(formatNumber(49_729_223_812, { locale, notation: "standard" })).eq(testCasesStandardNotation[locale])
85+
})
86+
})
87+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { BROWSER_LOCALE, getBrowserSupportedLocale, SupportedLocale } from "./locales"
2+
3+
export const NUMBER_NOTATIONS = ["standard", "compact", "scientific", "engineering"] as const
4+
export const numberNotationsSet = new Set<string>(NUMBER_NOTATIONS)
5+
6+
export type NumberNotation = (typeof NUMBER_NOTATIONS)[number]
7+
8+
export const DEFAULT_NUMBER_NOTATION: NumberNotation = "standard"
9+
10+
export const numberNotationDisplayNames: Record<NumberNotation, string> = {
11+
standard: "Standard",
12+
compact: "Compact",
13+
scientific: "Scientific",
14+
engineering: "Engineering",
15+
}
16+
17+
export const isNumberNotation = (n: string): n is NumberNotation => numberNotationsSet.has(n)
18+
19+
export interface FormatNumberOptions {
20+
locale: SupportedLocale | typeof BROWSER_LOCALE
21+
notation: NumberNotation
22+
}
23+
24+
export const formatNumber = (n: number, { locale, notation }: FormatNumberOptions) =>
25+
Intl.NumberFormat(locale === BROWSER_LOCALE ? getBrowserSupportedLocale() : locale, { notation }).format(n)

0 commit comments

Comments
 (0)