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
7 changes: 7 additions & 0 deletions .changeset/orange-pens-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/api": patch
"@hyperdx/app": patch
---

feat: relative date ranges for dashboards can now be saved
4 changes: 4 additions & 0 deletions packages/api/src/models/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export default mongoose.model<IDashboard>(
savedQuery: { type: String, required: false },
savedQueryLanguage: { type: String, required: false },
savedFilterValues: { type: mongoose.Schema.Types.Array, required: false },
savedDateRange: {
type: mongoose.Schema.Types.Mixed,
required: false,
},
containers: { type: mongoose.Schema.Types.Array, required: false },
createdBy: {
type: mongoose.Schema.Types.ObjectId,
Expand Down
103 changes: 85 additions & 18 deletions packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
Group,
Indicator,
List,
Loader,
Menu,
Modal,
Paper,
Expand Down Expand Up @@ -147,7 +148,10 @@ import { PageHeader } from '@/components/PageHeader';
import { PageLayout } from '@/components/PageLayout';
import { SqlVariablesProvider } from '@/components/SQLEditor/variableCompletions';
import { TimePicker } from '@/components/TimePicker';
import { parseTimeRangeInput } from '@/components/TimePicker/utils';
import {
parseTimeRangeInput,
timeRangeInputToSeconds,
} from '@/components/TimePicker/utils';
import {
Dashboard,
type Tile,
Expand Down Expand Up @@ -211,6 +215,7 @@ import {
} from './source';
import {
dateRangeToString,
parseRelativeTimeQuery,
useDefaultTimeRange,
useNewTimeQuery,
} from './timeQuery';
Expand Down Expand Up @@ -1855,15 +1860,14 @@ function DashboardContainerRow({

const DEFAULT_INTERVAL = 'Past 1h';

function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
const defaultTimeRange = useDefaultTimeRange(DEFAULT_INTERVAL);
const brandName = useBrandDisplayName();
const confirm = useConfirm();

const router = useRouter();
const dashboardId = router.query.dashboardId as string | undefined;
const { enterKioskMode, exitKioskMode, isKioskMode } =
useDashboardKioskMode();
function DBDashboardPage({
dashboardProps,
defaultTimeInput = DEFAULT_INTERVAL,
}: {
dashboardProps: ReturnType<typeof useDashboard>;
defaultTimeInput?: string;
}) {
const defaultTimeRange = useDefaultTimeRange(defaultTimeInput);

const {
dashboard,
Expand All @@ -1872,10 +1876,17 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
isLocalDashboard,
isFetching: isFetchingDashboard,
isSetting: isSavingDashboard,
} = useDashboard({
dashboardId: dashboardId as string | undefined,
presetConfig,
});
} = dashboardProps;
const brandName = useBrandDisplayName();
const confirm = useConfirm();
const {
userPreferences: { isUTC },
} = useUserPreferences();

const router = useRouter();
const dashboardId = router.query.dashboardId as string | undefined;
const { enterKioskMode, exitKioskMode, isKioskMode } =
useDashboardKioskMode();

const { data: sources } = useSources();
const { data: connections } = useConnections();
Expand Down Expand Up @@ -2033,10 +2044,10 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
}, [router.isReady, watchedGranularity, granularity, setGranularity]);

const [displayedTimeInputValue, setDisplayedTimeInputValue] =
useState(DEFAULT_INTERVAL);
useState(defaultTimeInput);

const { searchedTimeRange, onSearch, onTimeRangeSelect } = useNewTimeQuery({
initialDisplayValue: DEFAULT_INTERVAL,
initialDisplayValue: defaultTimeInput,
initialTimeRange: defaultTimeRange,
setDisplayedTimeInputValue,
});
Expand Down Expand Up @@ -2113,6 +2124,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
dashboard?.savedQuery,
dashboard?.savedQueryLanguage,
dashboard?.savedFilterValues,
dashboard?.savedDateRange,
Comment thread
knudtty marked this conversation as resolved.
Resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — savedDateRange and onTimeRangeSelect added as deps of an effect that uses neither

The initialization effect body (lines 1994–2034) never reads dashboard.savedDateRange or calls onTimeRangeSelect; the two new deps only make the effect look like it applies the saved range when the range is actually applied through defaultTimeInput. Drop both deps, or move the range application into this effect where the initializedDashboardRef / URL-precedence logic already lives.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Dead dependencies added to the dashboard-initialization effect

dashboard?.savedDateRange and onTimeRangeSelect were added to the dependency list, but nothing in the effect body (lines 1994-2034) reads either — the saved range is applied through the defaultTimeInput prop instead, and the effect early-returns via initializedDashboardRef. Drop both deps (or move the range application into this effect, which is what they suggest was intended).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — savedDateRange and onTimeRangeSelect added as deps to an effect that reads neither

The initialization effect at line 2081 never touches dashboard.savedDateRange or calls onTimeRangeSelect — the restore happens in DBDashboardPageGuarded instead. These two deps only cause extra no-op runs (the initializedDashboardRef guard short-circuits) and mislead the next reader into thinking the time-range restore lives here. Remove both.

isLocalDashboard,
isFetchingDashboard,
router.isReady,
Expand All @@ -2121,6 +2133,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
setWhere,
setWhereLanguage,
setFilterValueEntries,
onTimeRangeSelect,
]);

// Sync changes to the URL params into the form
Expand Down Expand Up @@ -2150,18 +2163,30 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
? filterValueEntries
: [];

const currentRelativeDateRange = timeRangeInputToSeconds(
Comment thread
knudtty marked this conversation as resolved.
Resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Unparseable input keeps the old saved range while the toast claims it was saved

If displayedTimeInputValue doesn't parse (mid-edit text, Live Tail), currentRelativeDateRange is null, the if is skipped so a previously saved range silently survives, yet the notification still says the relative time range was saved. Either clear draft.savedDateRange = null in the null case or keep the range out of the toast copy when nothing was written.

displayedTimeInputValue,
isUTC,
);

setDashboard(
produce(dashboard, draft => {
draft.savedQuery = currentWhere;
draft.savedQueryLanguage = currentWhereLanguage;
draft.savedFilterValues = currentFilterValues;
// Only supporting relative date range saving ATM
if (currentRelativeDateRange) {
Comment thread
knudtty marked this conversation as resolved.
Resolved
Comment thread
knudtty marked this conversation as resolved.
Resolved
Comment thread
knudtty marked this conversation as resolved.
Resolved
Comment thread
knudtty marked this conversation as resolved.
Resolved
draft.savedDateRange = {
type: 'relative',
value: currentRelativeDateRange,
};
}
Comment thread
knudtty marked this conversation as resolved.
Resolved
}),
() => {
notifications.show({
color: 'green',
title: 'Query saved and executed',
message:
'Filter query and dropdown values have been saved with the dashboard',
'Filter query, dropdown values, and relative time range have been saved with the dashboard',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Unparseable time input leaves a stale saved range while the toast claims it was saved

if (currentRelativeDateRange) skips the write when the input doesn't parse (e.g. Live Tail, or a half-typed value), but the notification still reports "…and relative time range have been saved with the dashboard", and any previously saved range silently survives. Clear draft.savedDateRange = null in the else branch, or word the toast off the value actually written.

Comment thread
knudtty marked this conversation as resolved.
Resolved
autoClose: 3000,
});
},
Expand All @@ -2173,6 +2198,8 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
getValues,
filterValueEntries,
onSubmit,
isUTC,
displayedTimeInputValue,
]);
const handleRemoveSavedQuery = useCallback(() => {
Comment thread
knudtty marked this conversation as resolved.
Resolved
if (!dashboard || isLocalDashboard) return;
Expand All @@ -2182,6 +2209,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
draft.savedQuery = null;
draft.savedQueryLanguage = null;
draft.savedFilterValues = [];
draft.savedDateRange = null;
}),
() => {
notifications.show({
Expand Down Expand Up @@ -3484,7 +3512,46 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
);
}

const DBDashboardPageDynamic = dynamic(async () => DBDashboardPage, {
function DBDashboardPageGuarded({
presetConfig,
}: {
presetConfig?: Dashboard;
}) {
const router = useRouter();
const dashboardId = router.query.dashboardId as string | undefined;
const dashboardProps = useDashboard({
dashboardId: dashboardId as string | undefined,
presetConfig,
});
const {
userPreferences: { isUTC },
} = useUserPreferences();

const savedDateRange = dashboardProps.dashboard?.savedDateRange;
// Keyed on the saved range, not the render: a relative range re-stringifies
// to a new value every render, and useNewTimeQuery resets the input on change.
// Valid URL from/to still win: useNewTimeQuery overwrites the input from them.
const defaultTimeInput = useMemo(() => {
if (!savedDateRange) return undefined;
const [start, end] =
savedDateRange.type === 'relative'
? parseRelativeTimeQuery(savedDateRange.value * 1000)
: savedDateRange.value.map(v => new Date(v));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — historical values are read as milliseconds while relative is seconds

savedDateRange.value * 1000 treats the relative value as seconds, but savedDateRange.value.map(v => new Date(v)) treats the historical pair as epoch millis — the new schema test uses [1700000000, 1700003600], i.e. epoch seconds, which this branch would render as Jan 1970. Pin the unit in DashboardSchema (a comment plus a consistent * 1000) before the format is persisted and impossible to change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Resolved dates are round-tripped through a year-less display string

The guard resolves savedDateRange to real Dates, throws them away by formatting with dateRangeToString (format 'normal' = MMM d HH:mm:ss, no year — see TIME_TOKENS in packages/common-utils/src/core/utils.ts:590), and the child re-derives them with chrono at line 1958. For the historical branch handled right here — writable today via PATCH /api/dashboards/:id, whose body is DashboardSchema.partial() (packages/api/src/routers/api/dashboards.ts:179) — a range from a previous year comes back parsed into the current year, so the dashboard loads the wrong window. The as [Date, Date] cast at line 1958 also hides the [null, null] that parseTimeQuery returns when the string doesn't parse, which would flow into searchedTimeRange. Pass the resolved [Date, Date] down next to the display string instead of re-parsing it.

Comment thread
knudtty marked this conversation as resolved.
Resolved
// TODO: show relative ranges as "Past Xh" via getRelativeInterval
return dateRangeToString([start, end], isUTC);
Comment thread
knudtty marked this conversation as resolved.
Resolved
}, [savedDateRange, isUTC]);

if (!dashboardProps || !router.isReady) return <Loader size="lg" />;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — !dashboardProps can never be true, so the guard doesn't guard the fetch

useDashboard always returns an object literal, so only !router.isReady is doing anything: the child still mounts while the remote dashboard is loading, with savedDateRange undefined and defaultTimeInput 'Past 1h', and the saved range only lands later via the initialTimeRange effect. Gate on the data instead — e.g. if (!router.isReady || (!dashboardProps.isLocalDashboard && dashboardProps.isFetching)).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — !dashboardProps can never be true

useDashboard returns a fresh object literal on every render (packages/app/src/dashboard.ts:274), so this half of the guard is unreachable. Reduce it to if (!router.isReady), or gate on something meaningful such as dashboardProps.isFetching.


return (
<DBDashboardPage
dashboardProps={dashboardProps}
defaultTimeInput={defaultTimeInput}
/>
);
}

const DBDashboardPageDynamic = dynamic(async () => DBDashboardPageGuarded, {
ssr: false,
});

Expand Down
33 changes: 32 additions & 1 deletion packages/app/src/components/TimePicker/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { dateParser, parseTimeRangeInput } from '@/components/TimePicker/utils';
import {
dateParser,
parseTimeRangeInput,
timeRangeInputToSeconds,
} from '@/components/TimePicker/utils';

describe('dateParser', () => {
let mockDate: Date;
Expand Down Expand Up @@ -154,3 +158,30 @@ describe('parseTimeRangeInput', () => {
]);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — New tests cover only the pure helper, not the feature

timeRangeInputToSeconds and the Zod shape are tested, but nothing exercises the actual behaviour this PR adds — handleSaveQuery writing savedDateRange, DBDashboardPageGuarded deriving defaultTimeInput from it, or URL from/to taking precedence (the branch that produces the [null, null] range above). Add a test around the guard's derivation, and extend the saved-defaults e2e in packages/app/tests/e2e/features/dashboard.spec.ts:1245 to reload and assert the restored time range.

describe('timeRangeInputToSeconds', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-15T22:00:00'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — New tests cover only the helper, not the feature the PR is about

The added tests exercise timeRangeInputToSeconds and the Zod field, but nothing covers the two pieces of real logic: the initialization branch at DBDashboardPage.tsx:2026-2033 (saved range applied only when from/to are absent from the URL) and the save branch at 2084-2094. A regression that stopped applying the saved range, or that let a URL range be overwritten by it, would still pass. The existing e2e dashboard.spec.ts:1245 ("URL query params overriding saved query") is the natural place to add the parallel date-range case; the isUTC: true path of the helper is also untested.

});

afterEach(() => {
jest.useRealTimers();
});

it('returns the relative range length in seconds', () => {
expect(timeRangeInputToSeconds('Past 1h', false)).toBe(3600);
expect(timeRangeInputToSeconds('Past 15m', false)).toBe(900);
});

it('returns the span of an absolute range', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — New tests never exercise the feature, and one pins the absolute-as-relative bug as correct

The only new coverage is timeRangeInputToSeconds plus a DashboardSchema parse; nothing tests the actual behaviour — DBDashboardPageGuarded turning a stored savedDateRange back into defaultTimeInput, handleSaveQuery writing it, or URL from/to taking precedence — and the four e2e edits only update notification copy. 'returns the span of an absolute range' asserts the exact behaviour that makes an absolute pick get saved as relative, so it will keep passing after that bug is fixed. Add an e2e that saves a default range, reloads without from/to, and asserts the picker/tiles use the saved window, plus one that reloads with from/to and asserts the URL wins.

expect(timeRangeInputToSeconds('2024-01-15 to 2024-01-16', false)).toBe(
86400,
);
});

it('returns null for unparseable input', () => {
expect(timeRangeInputToSeconds('', false)).toBeNull();
expect(timeRangeInputToSeconds('not a date', false)).toBeNull();
});
});
10 changes: 10 additions & 0 deletions packages/app/src/components/TimePicker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ export function parseTimeRangeInput(
}
}

// Duration of a time range input in seconds, or null if it doesn't parse.
export function timeRangeInputToSeconds(
str: string,
isUTC: boolean,
): number | null {
const [start, end] = parseTimeRangeInput(str, isUTC);
if (start == null || end == null) return null;
return (end.getTime() - start.getTime()) / 1000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Persisted duration is fractional seconds, breaking the existing millisecond-integer convention

(end - start) / 1000 is unrounded, and in real (non-faked) time parseTimeRangeInput builds start and the end fallback from two separate new Date() calls (utils.ts:10 and utils.ts:63), so "Past 1h" persists as e.g. 3600.002 rather than 3600 — the unit test only gets 3600 because it freezes the clock. Every other relative range in the app is a whole-millisecond integer (RELATIVE_TIME_OPTIONS, LIVE_TAIL_DURATION_MS, and the persisted liveInterval query state at DBSearchPage.tsx:1594-1597), which is also what getRelativeTimeOptionLabel keys off. Store rounded milliseconds and drop the * 1000 at DBDashboardPage.tsx:2029, and tighten the schema to z.number().int().nonnegative().nullish() — this is a persisted format, so old values stay valid forever.

}

export const LIVE_TAIL_TIME_QUERY = 'Live Tail' as const;
export const LIVE_TAIL_DURATION_MS = ms('15m');

Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DashboardContainer,
DashboardFilter,
DashboardFilterValue,
DashboardWithoutId,
resolveChartPaletteToken,
SavedChartConfig,
SearchConditionLanguage,
Expand Down Expand Up @@ -39,6 +40,7 @@ export type Dashboard = {
savedQuery?: string | null;
savedQueryLanguage?: SearchConditionLanguage | null;
savedFilterValues?: DashboardFilterValue[];
savedDateRange?: DashboardWithoutId['savedDateRange'];
containers?: DashboardContainer[];
createdAt?: string;
updatedAt?: string;
Expand Down
11 changes: 7 additions & 4 deletions packages/app/src/timeQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,27 +522,30 @@ export function useNewTimeQuery({
};
}

function getRangeOrFallback(query: string): [Date, Date] {
function getRangeOrFallback(query: string, isUTC: boolean): [Date, Date] {
return (
parseValidTimeRange(query, false) ??
parseValidTimeRange(query, isUTC) ??
// This fallback catches arbitrary inputs but shouldn't be reachable
// for standard literal labels (e.g., 'Past 1h') since they are always valid.
parseRelativeTimeQuery(60 * 60 * 1000)
);
}

export function useDefaultTimeRange(query: string): [Date, Date] {
const {
userPreferences: { isUTC },
} = useUserPreferences();
const [state, setState] = useState<{ query: string; range: [Date, Date] }>(
() => ({
query,
range: getRangeOrFallback(query),
range: getRangeOrFallback(query, isUTC),
}),
);

if (state.query !== query) {
const newState = {
query,
range: getRangeOrFallback(query),
range: getRangeOrFallback(query, isUTC),
};
setState(newState);
return newState.range;
Expand Down
8 changes: 4 additions & 4 deletions packages/app/tests/e2e/features/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@
await dashboardPage.chartEditor.setChartName('My Unsaved Chart');

await dashboardPage.page.keyboard.press('Escape');
await expect(dashboardPage.unsavedChangesConfirmModal).toBeAttached({

Check failure on line 279 in packages/app/tests/e2e/features/dashboard.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests / E2E Tests - Shard 2

[chromium] › tests/e2e/features/dashboard.spec.ts:274:7 › Dashboard › should warn when closing tile editor with unsaved changes @dashboard

1) [chromium] › tests/e2e/features/dashboard.spec.ts:274:7 › Dashboard › should warn when closing tile editor with unsaved changes @dashboard Error: expect(locator).toBeAttached() failed Locator: getByRole('dialog').filter({ has: getByTestId('confirm-confirm-button') }) Expected: attached Timeout: 5000ms Error: element(s) not found Call log: - Expect "toBeAttached" with timeout 5000ms - waiting for getByRole('dialog').filter({ has: getByTestId('confirm-confirm-button') }) 277 | 278 | await dashboardPage.page.keyboard.press('Escape'); > 279 | await expect(dashboardPage.unsavedChangesConfirmModal).toBeAttached({ | ^ 280 | timeout: 5000, 281 | }); 282 | at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/dashboard.spec.ts:279:60
timeout: 5000,
});

Expand Down Expand Up @@ -1201,7 +1201,7 @@

// Wait for success notification
const notification = dashboardPage.page.locator(
'text=/Filter query and dropdown values/i',
'text=/Filter query, dropdown values, and relative time range/i',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — No test covers the save→reload round trip this PR adds

The three e2e edits only update the notification regex, and the new unit tests cover timeRangeInputToSeconds and the Zod shape — nothing asserts that a saved range comes back on load, which is the whole feature, and nothing pins the relative-vs-absolute distinction the save path depends on. should save and restore query and filter values (line 1151) already saves defaults, navigates away, and returns with URL params cleared; add an assertion on the TimePicker input there.

);
await expect(notification).toBeVisible({ timeout: 5000 });
});
Expand Down Expand Up @@ -1266,7 +1266,7 @@
// Wait for the save success notification rather than a blind sleep, so
// we only read the URL once the save has actually landed.
const notification = dashboardPage.page.locator(
'text=/Filter query and dropdown values/i',
'text=/Filter query, dropdown values, and relative time range/i',
);
await expect(notification).toBeVisible({ timeout: 5000 });

Expand Down Expand Up @@ -2649,7 +2649,7 @@

// Wait for success notification
const notification = dashboardPage.page.locator(
'text=/Filter query and dropdown values/i',
'text=/Filter query, dropdown values, and relative time range/i',
);
await expect(notification).toBeVisible({ timeout: 5000 });
});
Expand All @@ -2670,7 +2670,7 @@

// Wait for success notification
const notification = dashboardPage.page.locator(
'text=/Filter query and dropdown values/i',
'text=/Filter query, dropdown values, and relative time range/i',
);
await expect(notification).toBeVisible({ timeout: 5000 });
});
Expand Down Expand Up @@ -2965,7 +2965,7 @@
columnIndex: number,
substring: string,
) => {
await expect

Check failure on line 2968 in packages/app/tests/e2e/features/dashboard.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests / E2E Tests - Shard 2

[chromium] › tests/e2e/features/dashboard.spec.ts:2874:11 › Dashboard › Table chart - per-series number formats › per-series format overrides chart-wide format and falls back when reset to inherit @dashboard @full-stack @dashboard

2) [chromium] › tests/e2e/features/dashboard.spec.ts:2874:11 › Dashboard › Table chart - per-series number formats › per-series format overrides chart-wide format and falls back when reset to inherit @dashboard @full-stack @dashboard › Reset Series 0 (CountA) to Inherit; column falls back to chart-wide Currency Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBe(expected) // Object.is equality Expected: true Received: false Call Log: - Timeout 15000ms exceeded while waiting on the predicate 2966 | substring: string, 2967 | ) => { > 2968 | await expect | ^ 2969 | .poll( 2970 | async () => { 2971 | const cells = await dashboardPage.getTileTableCellTexts( at expectAllTileCellsToContain (/home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/dashboard.spec.ts:2968:11) at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/dashboard.spec.ts:3031:11 at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/dashboard.spec.ts:3003:9

Check failure on line 2968 in packages/app/tests/e2e/features/dashboard.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests / E2E Tests - Shard 2

[chromium] › tests/e2e/features/dashboard.spec.ts:2874:11 › Dashboard › Table chart - per-series number formats › per-series format overrides chart-wide format and falls back when reset to inherit @dashboard @full-stack @dashboard

2) [chromium] › tests/e2e/features/dashboard.spec.ts:2874:11 › Dashboard › Table chart - per-series number formats › per-series format overrides chart-wide format and falls back when reset to inherit @dashboard @full-stack @dashboard › Reset Series 0 (CountA) to Inherit; column falls back to chart-wide Currency Error: expect(received).toBe(expected) // Object.is equality Expected: true Received: false Call Log: - Timeout 15000ms exceeded while waiting on the predicate 2966 | substring: string, 2967 | ) => { > 2968 | await expect | ^ 2969 | .poll( 2970 | async () => { 2971 | const cells = await dashboardPage.getTileTableCellTexts( at expectAllTileCellsToContain (/home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/dashboard.spec.ts:2968:11) at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/dashboard.spec.ts:3031:11 at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/dashboard.spec.ts:3003:9
.poll(
async () => {
const cells = await dashboardPage.getTileTableCellTexts(
Expand Down
22 changes: 22 additions & 0 deletions packages/common-utils/src/__tests__/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,28 @@ describe('DashboardFilterValueSchema', () => {
});
});

describe('DashboardSchema.savedDateRange', () => {
const base = { id: 'd1', name: 'Dashboard', tiles: [], tags: [] };
const parse = (savedDateRange: unknown) =>
DashboardSchema.safeParse({ ...base, savedDateRange }).success;

it('accepts relative, historical, null, or absent', () => {
expect(parse({ type: 'relative', value: 3600 })).toBe(true);
expect(parse({ type: 'historical', value: [1700000000, 1700003600] })).toBe(
true,
);
expect(parse(null)).toBe(true);
expect(parse(undefined)).toBe(true);
});

it('rejects malformed values', () => {
expect(parse({ type: 'relative', value: '1h' })).toBe(false);
expect(parse({ type: 'historical', value: [1700000000] })).toBe(false);
expect(parse({ type: 'other', value: 1 })).toBe(false);
expect(parse(3600)).toBe(false);
});
});

describe('MetricFormulaSchema', () => {
it('parses an expression-only formula', () => {
const result = MetricFormulaSchema.safeParse({
Expand Down
Loading
Loading