Skip to content

feat: open saved searches with a saved default time range - #3593

Open
Asheesh7298 wants to merge 2 commits into
ClickHouse:mainfrom
Asheesh7298:ai/saved-search-default-time-range
Open

Asheesh7298 wants to merge 2 commits into
ClickHouse:mainfrom
Asheesh7298:ai/saved-search-default-time-range

Conversation

@Asheesh7298

@Asheesh7298 Asheesh7298 commented Nov 7, 2026 •

Copy link
Copy Markdown

Summary

Closes #3567.

Saved searches can now store a default time range (for example "past 1 day") and open with it instead of always landing in Live Tail. This follows the approach agreed on the issue, which is the same one #3073 used for dashboards.

  • Storage. A new optional defaultTimeRange field on saved searches, shaped { type: 'relative', value: <seconds> } like the relative case of the dashboard savedDateRange. That relative variant is now a named RelativeDateRangeSchema, and both schemas use it. Saved searches accept only relative ranges: as agreed on the issue, fixed windows are out for now. Existing searches don't have the field and behave as before, so there's no migration.

  • Opening. When the URL carries no time (nav, list and Spotlight links are a bare /search/<id>), a stored range that live tail supports (1 minute to 6 hours) opens as live tail with that interval. Longer ranges open as [now − N, now] with live tail off, the same as dashboards. Links that already carry a time (isLive or liveInterval, e.g. alert links) keep winning. The URL writes use replace: a push would leave the bare URL in history, and Back would land on it and apply the range again.

  • Saving. The save modal has a "When opened" choice:

    • Show live tail (past 15 minutes). The default behaviour, so nothing is stored.
    • Show the saved time range. Offered when a range is stored, and selected by default. Clicking Update after zooming the histogram therefore doesn't overwrite the stored range.
    • The current range, shown as "Show live tail for the past 1 hour" for a live tail interval or "Show the past 1 day" for a window. Offered only for a live tail interval, or a window that ends now. A range picked from dates in the past is never saved as "the past N".
  • Clearing. The PATCH route used _.omitBy(…, _.isNil), which made it impossible to remove a stored range. It now drops only undefined. No other saved-search field accepts null, so validation still rejects null for those (there's a test for this).

  • Not in this PR. These are follow-ups mentioned on the issue:

    • keeping the range relative in the URL after opening, so copied links stay "past 1 day";
    • external API v2 and OpenAPI;
    • MCP and CLI.

Screenshots or video

Before After
before after

How to reproduce

Starting point: yarn app:dev:local, onboarding → Connect to Demo Server.

On main:

  1. Open Search and pick Demo Logs. In the time picker, choose Last 1 days.
  2. Click Save, name it, and save.
  3. Open the saved search from the saved searches list. It opens in Live Tail (last 15 minutes).

On this branch:

  1. Same as step 1 above.
  2. Click Save. Under When opened, pick Show the past 1 day, name it, and save.
  3. Open the saved search from the saved searches list. It opens on the past day with live tail off. Press Back and you return to the list.
  4. Open it again and click Update. Show the saved time range (past 1 day) is selected. Pick Show live tail and click Update. Opening the search from the list now gives Live Tail again.

How to test on Vercel preview

Preview routes: /search, /search/list

Steps:

  1. Open /search, select the Demo Logs source, open the time picker and click Last 1 days.
  2. Click Save, enter a name, select Show the past 1 day under When opened, and click Save.
  3. Open /search/list and click the saved search you just created.
  4. Verify the URL contains isLive=false and the time picker shows a range of about one day ending now.

References

Saved searches can store a relative defaultTimeRange (e.g. past 1 day)
that is applied when the search is opened without a time in the URL,
instead of always landing in live tail. The save modal gets a "When
opened" choice. Same shape as the relative case of dashboard
savedDateRange; fixed windows are not accepted.

The saved-search PATCH route now drops only undefined, so sending null
clears the stored range.

Refs ClickHouse#3567
@Asheesh7298
Asheesh7298 requested a review from a team as a code owner November 7, 2026 10:14
@vercel

vercel Bot commented Nov 7, 2026

Copy link
Copy Markdown

@Asheesh7298 is attempting to deploy a commit to the HyperDX Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added external needs-vouch Author needs a maintainer to vouch for them labels Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Hi @Asheesh7298, thanks for the pull request!

Before we review code from a first-time contributor, a maintainer vouches for them, and you're not on our list yet. There's nothing else to file and this PR stays open — a maintainer will look at it, usually within a day or two, and vouch for you right here if it's a genuine attempt at the problem.

To make that quick, check the PR description has:

  • The issue it addresses (ideally one labelled Accepting PR) and why the change is needed
  • Steps to reproduce the problem and verify the fix, starting from https://play.hyperdx.io or yarn app:dev:local connected to the demo server
  • Before/after screenshots if anything in the UI changed

More detail in our contributing guide.

// Live updates pause while the save modal is open, so the window is stable.
const options = useMemo(() => {
const current: CurrentTimeRange =
isLive || openedAtMs == null

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.

🟠 major — A relative interval in live mode (Last 1–6 hours etc.) can't be saved as the default time range

In relative mode, onTimePickerRelativeSearch (DBSearchPage.tsx:2246) sets isLive=true and liveInterval=rangeMs for every option with relativeSupport (1 minute to 6 hours, TimePicker/utils.ts:185-193). This hook treats any isLive as { kind: 'live' }, so the modal only offers "Show live tail" and says "To save a time range, pick one first", even though the user just picked one. When the saved search reopens, it drops the chosen 1h interval and falls back to 15m live tail. Fix: pass interval into the hook. When isLive && interval !== LIVE_TAIL_DURATION_MS, offer current as { type: 'relative', value: interval / 1000 }.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 437ab94. A live tail interval (1 minute to 6 hours) can now be saved, and those ranges reopen as live tail with that interval. Longer ranges still open as a window ending now. Added unit and e2e coverage.

*/
export const RelativeDateRangeSchema = z.object({
type: z.literal('relative'),
value: z.number(),

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 — RelativeDateRangeSchema accepts zero and negative durations, so the client has to guard against them

value: z.number() lets POST/PATCH store { type: 'relative', value: 0 } or -60. getSavedSearchLandingRange then silently ignores these values (savedSearchTimeRange.ts:29), and the unit tests pin that workaround. Make the schema z.number().positive() (this suits dashboards too) so invalid ranges are rejected with a 400. Then drop the <= 0 check on the client.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 437ab94. defaultTimeRange now requires a positive duration and returns 400 otherwise, and the client guard is removed. I left the shared RelativeDateRangeSchema as is so dashboard validation doesn't change in this PR.

if (!Number.isFinite(stored.value) || stored.value <= 0) return null;
// Whole seconds, like parseRelativeTimeQuery, so re-parsing the displayed
// text on the first submit lands on the same from/to.
const end = Math.floor(nowMs / 1000) * 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 — New helpers duplicate existing time helpers in timeQuery.ts

getSavedSearchLandingRange rebuilds parseRelativeTimeQuery (packages/app/src/timeQuery.ts:46): the end is truncated to the second and the duration is subtracted. The comment even names it. formatRelativeDuration (line 67) also repeats the formatDuration(intervalToDuration(...)) logic of getRelativeInterval (timeQuery.ts:417). Fix: give parseRelativeTimeQuery an optional nowMs argument and call it. Export one duration formatter from timeQuery.ts and use it in both places, instead of keeping a second copy (this follows the REQUIRED DRY rule).

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Partly fixed in 437ab94: opening a search now calls parseRelativeTimeQuery, which takes an optional now. I kept formatRelativeDuration separate on purpose. It anchors at epoch 0, so a saved "1 day" never prints as "23 hours" across a DST change, which getRelativeInterval can do because it uses the real start and end dates.

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

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

2 posted as inline comment(s) on the changed lines.


Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

Scope: PR #3593 — open saved searches with a saved default time range. 13 files, 2 commits, diffed against merge-base 5b96dcbc.
Intent: Add an optional relative defaultTimeRange to saved searches so they open on a stored window (or live-tail interval) instead of always landing in Live Tail, with a "When opened" save-modal choice and a PATCH route that can clear the field.

✅ No critical issues found.

The high-risk areas check out. The PATCH change from _.omitBy(…, _.isNil) to _.isUndefined is safe: defaultTimeRange is the only nullish field in SavedSearchSchema, and validateRequest rejects null for every other field before the handler runs (covered by the "still rejects null" test). The landing useEffect is self-terminating — once setSearchedConfig populates the config, isSearchConfigEmpty is false, so the range is not re-applied. Schema round-trip, null-clearing, and backward compatibility for pre-existing searches are all covered by the integration tests.

🟡 P2 — recommended

  • packages/app/src/components/SavedSearches/savedSearchTimeRange.ts:42 — getSavedSearchLanding dropped its former if (!Number.isFinite(stored.value) || stored.value <= 0) return null guard (and its zero/negative/infinite tests were removed), now trusting that defaultTimeRange is always server-validated as positive; the frontend RelativeDateRange type is plain { value: number }, so a 0/negative/Infinity value produces a degenerate or Invalid Date window with no guard.
    • Fix: Reinstate a positivity/finiteness guard in getSavedSearchLanding (or tighten the exported RelativeDateRange type) so the frontend util does not depend solely on server validation.
🔵 P3 nitpicks (1)
  • packages/app/src/components/SavedSearches/savedSearchTimeRange.ts:34 — SavedSearchLanding and CurrentTimeRange are structurally identical discriminated unions ({ kind:'live'; intervalMs }|{ kind:'window'; range }) declared in the same module.
    • Fix: Collapse them into one shared type to avoid drift if either variant changes.

Reviewers (8): correctness, testing, maintainability, project-standards, kieran-typescript, api-contract, julik-frontend-races, previous-comments.

Testing gaps: The DBSearchPage landing useEffect branches are exercised via e2e and the underlying getSavedSearchLanding/resolveDefaultTimeRange logic has solid unit coverage; no substantive gap in new behavior.

Note: the forced completion fired before 7 of the 8 dispatched reviewers returned structured findings; the findings above reflect the orchestrator's own full-diff analysis plus the completed previous-comments reviewer (which found nothing, as the prior comments are automated Vercel/first-contributor bot messages with no code feedback). Severities were graded down per the ship-blocker rubric.

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

Address review feedback:
- A live tail interval picked in the time picker (1 minute to 6 hours)
  can now be saved. Ranges live tail supports reopen as live tail with
  that interval; longer ones open as a window ending now.
- defaultTimeRange rejects zero and negative durations.
- Reuse parseRelativeTimeQuery (now takes an optional "now") instead of
  rebuilding it.

/** "1 day", "1 hour 30 minutes". Anchored at epoch 0 so DST never shifts it. */
export function formatRelativeDuration(seconds: number): string {
return formatDuration(intervalToDuration({ start: 0, end: seconds * 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 — formatRelativeDuration re-implements the duration formatting that getRelativeInterval in timeQuery.ts already does

getRelativeInterval (packages/app/src/timeQuery.ts:420) already does formatDuration(intervalToDuration(...)) to build the time picker's "Past …" label. This PR adds a second copy of that call in a new module, so the saved-search labels and the picker labels can drift apart. Extract one shared duration formatter in timeQuery.ts, use it from both getRelativeInterval and the save modal, and delete the copy, as the REQUIRED DRY convention asks.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same point as the earlier thread: I kept this separate on purpose. It anchors at epoch 0 so a saved duration like "1 day" never prints as "23 hours" across a DST change, which getRelativeInterval can do because it formats real start/end dates. Making both share it would change the picker's existing label, so I'd rather leave that for a separate change.


// A pick only counts for the opening it was made in. Until then the default
// applies, which can change once the stored saved search loads.
const [picked, setPicked] = useState<{

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 hook's per-opening pick logic or the landing effect in DBSearchPage

Unit tests only cover the pure helpers. Nothing exercises the logic that drops a pick when openedAtMs changes, or that falls back to defaultChoice once stored loads. Nothing tests the DBSearchPage landing effect either, including whether it skips URLs that carry isLive/liveInterval. The e2e only covers the two create paths, so a regression where a reopened modal keeps the last pick (and Update overwrites the stored range) would still pass. Add a renderHook test for useSaveTimeRangeField (pick, close, reopen, expect default) and an update-flow case to DBSearchPage.saveSearch.test.tsx.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fair point. The choice logic and the update resolver are unit tested, and the landing path is checked by the two e2e tests (window and live interval). I'm stopping bot-driven changes after two review rounds per AGENTS.md. Happy to add a renderHook test for the pick/reopen behaviour if a maintainer would like it in this PR.

@Asheesh7298

Copy link
Copy Markdown
Author

@brandon-pereira this is ready for a look when you have time. It follows the plan from #3567 (defaultTimeRange, relative ranges only). Two automated review rounds are addressed: the live tail interval case is fixed, and the remaining minor notes are answered in their threads.

This branch has not been deployed

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

Labels

external needs-vouch Author needs a maintainer to vouch for them

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Maintain relative date selection for saved/linked searches

1 participant