Skip to content

feat: relative date ranges can be saved for dashboards - #3073

Open
knudtty wants to merge 7 commits into
mainfrom
aaron/save-dashboard-relative-date-range
Open

feat: relative date ranges can be saved for dashboards#3073
knudtty wants to merge 7 commits into
mainfrom
aaron/save-dashboard-relative-date-range

Conversation

@knudtty

@knudtty knudtty commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Allows date ranges to be saved for dashboards. If a user clicks "Save Query & Filters as default", the time range will be used when loading the dashboard in the future.

Screenshots or video

export-1788457467064.mp4

References

  • Linear Issue: Closes HDX-4915

@knudtty
knudtty requested a review from a team September 3, 2026 17:45
@knudtty knudtty self-assigned this Sep 3, 2026
@knudtty
knudtty requested review from wrn14897 and removed request for a team September 3, 2026 17:45
@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f557179

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Patch
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Sep 4, 2026 7:11pm UTC
hyperdx-storybook Ready Ready Preview Sep 4, 2026 7:11pm UTC

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)
  • Touches API routes or data models — hidden complexity risk

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 5
  • Production lines changed: 140 (+ 63 in test files, excluded from tier calculation)
  • Branch: aaron/save-dashboard-relative-date-range
  • Author: knudtty

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 342 passed • 1 skipped • 1445s

Status Count
✅ Passed 342
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds persisted relative time-range defaults to dashboards while preserving explicit URL time ranges as the higher-priority view state.

  • Adds the validated savedDateRange field to shared and API dashboard models.
  • Saves or removes the current relative range alongside dashboard query and filter defaults.
  • Derives dashboard time-query initialization from the saved range after dashboard data loads.
  • Adds shared-schema and time-input utility coverage and updates affected end-to-end assertions.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/app/src/DBDashboardPage.tsx Integrates saved relative ranges into dashboard save, removal, loading, URL precedence, and dashboard-switch initialization.
packages/app/src/components/TimePicker/utils.ts Adds a focused helper that parses a time input and returns its duration in seconds.
packages/common-utils/src/types.ts Adds a discriminated, nullable saved date-range schema shared across dashboard consumers.
packages/api/src/models/dashboard.ts Extends dashboard persistence to retain the validated saved date-range payload.
packages/app/tests/e2e/features/dashboard.spec.ts Updates save-success assertions to match the expanded persisted-default behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Load dashboard] --> B{URL has from and to?}
  B -->|Yes| C[Use explicit URL range]
  B -->|No| D{Saved date range exists?}
  D -->|Yes| E[Derive saved relative range]
  D -->|No| F[Use Past 1h]
  C --> G[Render dashboard queries]
  E --> G
  F --> G
  H[Save query and filters] --> I[Persist relative range duration]
  I --> A
Loading

Reviews (7): Last reviewed commit: "fix: add dashboard guard to properly han..." | Re-trigger Greptile

Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
? filterValueEntries
: [];

const currentRelativeDateRange =

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.

🔵 minorhandleSaveQuery saves the pre-submit time range, unlike the query it saves from post-submit form values

onSubmit() on line 2065 applies displayedTimeInputValue (which TimePicker.tsx:299 updates on every keystroke, without searching), but searchedTimeRange only refreshes after the from/to round-trip, so typing a new range into the picker and then clicking the menu item persists the old duration while the dashboard shows the new one. Derive the duration from the value being submitted — parseTimeRangeInput(displayedTimeInputValue) is already imported at line 149 — rather than from searchedTimeRange.

Comment thread packages/app/src/DBDashboardPage.tsx Outdated

// Initialize dashboard with the saved date range
if (dashboard.savedRelativeDateRange) {
onTimeRangeSelect(

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 — Applying the saved range on load pushes a history entry, so Back re-lands on the dashboard

onTimeRangeSelect writes from/to through useQueryStates(..., { history: 'push' }) (packages/app/src/timeQuery.ts:442-447), while the saved-query init path uses useQueryState defaults (replace) — so arriving from the dashboards list now costs two Back presses, and the first one drops the range to Past 1h. Apply the saved range without a push (e.g. feed it as initialTimeRange to useNewTimeQuery, which also makes URL from/to win naturally).

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

PR Review

7 finding(s): 🔴 0 critical · 🟠 2 major · 🔵 5 minor

6 posted as inline comment(s) on the changed lines. 1 listed below.

Findings outside the changed lines

  • 🟠 packages/app/src/DBDashboardPage.tsx:2758A saved date range can't be removed when there is no saved query/filterhasSavedQueryAndFilterDefaults still only checks savedQuery || savedFilterValues?.length, but handleSaveQuery now also persists savedDateRange. Save a time range on a dashboard with an empty WHERE and no filter values: savedQuery stays null and savedFilterValues stays [], so the flag is false, the "Remove Default Query & Filters" menu item (line 2990) never renders, and the user has no way to clear the range that now overrides their view on every load (the menu also keeps saying "Save …" instead of "Update …"). Add || dashboard?.savedDateRange to the flag.

Severity is the reviewer's own estimate and is used for ordering, not filtering.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Relative dashboard date ranges are now persisted as a discriminated-union savedDateRange and re-applied on load. Several P1s from earlier review rounds are resolved in this diff: the saved range is routed through useNewTimeQuery's initialTimeRange so explicit URL from/to still win; the relative anchor is recomputed via parseRelativeTimeQuery in a useMemo keyed on savedDateRange (re-anchors on dashboard switch); the save now derives from displayedTimeInputValue rather than a stale searchedTimeRange; and restore no longer pushes a history entry. The remaining items are validation, removal-UX, and test-coverage gaps.

✅ No critical issues found.

🟡 P2 — recommended

  • packages/app/src/DBDashboardPage.tsx:2758hasSavedQueryAndFilterDefaults gates the "Remove Default Query & Filters" menu item on savedQuery/savedFilterValues only, so a dashboard saved with a time range but no query and no filter values can never surface the remove action and stays pinned to that window.
    • Fix: Include savedDateRange in the hasSavedQueryAndFilterDefaults predicate so the remove item renders whenever any default is present.
    • correctness, previous-comments
  • packages/common-utils/src/types.ts:2057 — The relative branch validates value as a bare z.number(), and DashboardSchema.partial() is the only validation on the PATCH path, so a request with a negative value is stored and parseRelativeTimeQuery then yields start > end, producing an inverted range and empty tiles.
    • Fix: Constrain the relative value with .positive() (and consider .int() plus an upper bound) so out-of-range durations are rejected at the API boundary.
    • correctness, previous-comments
  • packages/app/tests/e2e/features/dashboard.spec.ts:1203 — The e2e specs for this feature only update the notification-text regex and still assert solely the WHERE input and filter chips, so no test fails whether or not the date range is saved, restored, or removed.
    • Fix: Extend one spec to save a non-default range, reload without from/to, and assert the applied span, plus the URL-precedence and removal paths.
    • testing, previous-comments
  • packages/app/src/DBDashboardPage.tsx:2079timeRangeInputToSeconds measures only the span of the picker input, so an absolute selection (e.g. a fixed incident window) is persisted as a relative duration and re-anchored to now on the next visit, with no UI signal that the absolute range was reinterpreted; the author has stated absolute ranges are out of scope, so this is a follow-up rather than a blocker.
    • Fix: Gate saving to relative selections only, or record a discriminated absolute value, so an absolute pick is not silently stored as a rolling duration.
    • correctness, previous-comments
🔵 P3 nitpicks (4)
  • packages/app/src/components/TimePicker/utils.ts:82(end.getTime() - start.getTime()) / 1000 is unrounded, so in real (non-faked) time "Past 1h" persists as a fractional value like 3600.002, diverging from the whole-integer duration convention used elsewhere in the app.
    • Fix: Round the result before returning.
  • packages/app/src/DBDashboardPage.tsx:2040 — The initialization useEffect lists dashboard?.savedDateRange and onTimeRangeSelect in its dependency array, but the effect body no longer references either; they are dead dependencies that misrepresent the effect's inputs.
    • Fix: Drop the unused dependencies from the array.
  • packages/app/src/DBDashboardPage.tsx:3448 — The historical read branch maps value through new Date(v) (treating the numbers as milliseconds) while the relative branch multiplies by 1000, and the schema/tests use epoch-second-scale historical values, so the future historical path would misinterpret units.
    • Fix: Normalize the historical branch to the same unit the values are stored in before it ships.
  • packages/app/src/DBDashboardPage.tsx:3454if (!dashboardProps || !router.isReady) guards on dashboardProps, which is a hook return object that is always truthy, so only the router.isReady check is meaningful.
    • Fix: Guard on a real loading signal (e.g. isFetchingDashboard) or drop the always-true condition.

Reviewers (5): correctness, kieran-typescript, julik-frontend-races, testing, previous-comments.

Testing gaps:

  • No end-to-end coverage of the date-range save → restore round-trip, URL from/to precedence over a saved range, or removal of a saved range.
  • Fractional-second persistence in real (non-faked) time is not exercised by the unit tests, which freeze the clock.

Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/common-utils/src/types.ts Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
const [start, end] = parseRelativeTimeQuery(
dashboard.savedRelativeDateRange * 1000,
);
onTimeRangeSelect(start, end);

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 — Restoring the saved range pushes a browser history entry on every dashboard open

onTimeRangeSelect writes through useQueryStates(timeRangeQueryStateMap, { history: 'push' }) (timeQuery.ts:442-447), so applying the saved range during initialization adds a history entry the user never created: pressing Back after opening the dashboard returns to the same dashboard without from/to (which resets the range to Past 1h) instead of going to the previous page. Apply the restored range with history: 'replace' semantics — e.g. an option on onTimeRangeSelect, matching how the other restored defaults (setWhere, setFilterValueEntries) use nuqs' default replace behaviour.

Comment thread packages/common-utils/src/types.ts Outdated
savedQuery: z.string().nullable().optional(),
savedQueryLanguage: SearchConditionLanguageSchema.nullable().optional(),
savedFilterValues: z.array(DashboardFilterValueSchema).optional(),
savedRelativeDateRange: z.number().nullish(),

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 — Schema admits zero and negative durations

savedRelativeDateRange: z.number().nullish() accepts any finite number, and the PATCH body schema (packages/api/src/routers/api/dashboards.ts:179) is the only validation on the way to Mongo. PATCH /api/dashboards/:id {"savedRelativeDateRange": -3600} stores it, and on load parseRelativeTimeQuery(-3600000) yields start > end, so every tile queries an inverted range. Use z.number().positive().nullish().

Comment thread packages/app/src/DBDashboardPage.tsx Outdated
}

// Initialize dashboard with the saved date range
if (!hasDateRangeInUrl) {

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 → restore round-trip for the date range

The save path (duration derivation, line 2081) and the restore path (line 2022) are both untested: dashboard.spec.ts:1245 and dashboard-filter-value-format.spec.ts:354 already drive saveQueryAndFiltersAsDefault() and assert the query/filter defaults restore on reload, but nothing asserts the time range. Extend one of those specs to select a known window, save defaults, reopen the dashboard with no params, and assert the resulting from/to span matches the saved duration — that would also have caught the hidden "Remove" menu item above.

Comment thread packages/app/src/DBDashboardPage.tsx
): 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.

Comment thread packages/app/src/DBDashboardPage.tsx Outdated
const [start, end] = parseRelativeTimeQuery(
dashboard.savedRelativeDateRange * 1000,
);
onTimeRangeSelect(start, end);

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 — Applying the saved range on load pushes a history entry, trapping the back button

onTimeRangeSelect writes through setTimeRangeQuery, which is configured history: 'push' (timeQuery.ts:442-447). Opening a dashboard that has a saved range therefore adds a ?from=…&to=… entry on mount: pressing Back from the dashboard list returns to the same dashboard with the range silently reset to Past 1h (the from == null && to == null branch at timeQuery.ts:480), and the user must press Back twice to leave. Apply the initial range with replace semantics — e.g. add a replace option to onTimeRangeSelect/setTimeRangeQuery — since this write is not user-initiated navigation.

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.

@knudtty
knudtty marked this pull request as draft September 3, 2026 21:11
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/common-utils/src/__tests__/types.test.ts Outdated
Comment thread packages/app/src/DBDashboardPage.tsx
Comment thread packages/app/src/DBDashboardPage.tsx
Comment thread packages/app/src/dashboard.ts Outdated
savedFilterValues?: DashboardFilterValue[];
savedDateRange?:
| { type: 'relative'; value: number }
| { type: 'historical'; value: [Date, Date] }

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 — App Dashboard.savedDateRange disagrees with DashboardSchema on the historical variant

The app type declares {type:'historical'; value:[Date, Date]} while the wire schema declares value: z.array(z.number()).length(2) (packages/common-utils/src/types.ts:2063-2066), and JSON from the API can never carry Dates — so const [start, end] = dateRange.value at packages/app/src/DBDashboardPage.tsx:1969 is typed Date but receives numbers for any dashboard written through the API or MCP. Derive the app field from the shared schema (z.infer<typeof DashboardSchema>['savedDateRange']) instead of restating it, per the "one source of truth" rule in the conventions, and document what the two numbers mean (epoch ms).

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 packages/app/src/DBDashboardPage.tsx
Comment thread packages/app/src/DBDashboardPage.tsx Outdated
Comment thread packages/app/src/DBDashboardPage.tsx
Comment thread packages/app/src/DBDashboardPage.tsx
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.

🔵 minorhistorical 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.

return dateRangeToString([start, end], isUTC);
}, [savedDateRange, hasUrlRange, 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)).

dashboard?.savedQuery,
dashboard?.savedQueryLanguage,
dashboard?.savedFilterValues,
dashboard?.savedDateRange,

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.

🔵 minorsavedDateRange 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.

? filterValueEntries
: [];

const currentRelativeDateRange = timeRangeInputToSeconds(

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.

]);
});
});

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.

Comment thread packages/app/src/DBDashboardPage.tsx
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 — 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.

savedQuery: z.string().nullable().optional(),
savedQueryLanguage: SearchConditionLanguageSchema.nullable().optional(),
savedFilterValues: z.array(DashboardFilterValueSchema).optional(),
savedDateRange: z

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.value units are unspecified and inconsistent between variants

relative is written in seconds (DBDashboardPage.tsx:2079) and read back as value * 1000, while the only reader of historical does new Date(v), i.e. epoch milliseconds — even though the new test uses second-scale epochs (1700000000). Every other relative duration in the app is milliseconds (RELATIVE_TIME_OPTIONS, LIVE_TAIL_DURATION_MS, the liveInterval param in DBSearchPage.tsx:1594), and getRelativeTimeOptionLabel — the helper the line 3450 TODO needs — takes ms. The seconds value is also fractional ((end - start) / 1000 over two wall-clock reads, so ~3600.002), which will never match a label. Store ms as an integer in both variants and document the unit in the schema; this format is persisted, so old values stay valid forever.

dashboard?.savedQuery,
dashboard?.savedQueryLanguage,
dashboard?.savedFilterValues,
dashboard?.savedDateRange,

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).

return dateRangeToString([start, end], isUTC);
}, [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

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.

// 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant