Skip to content

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

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

Asheesh7298 wants to merge 3 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. Saving stores the selected time range when it reads as "the past N": a live tail interval (e.g. Last 1 hour with "Live tail ranges" on) or a window ending now (e.g. Last 1 days). The modal shows what the search will open with ("Opens with the past 1 day", "Opens in live tail for the past 1 hour", or "Opens in live tail") and a tooltip explains how to change it. Plain live tail stores nothing, and updating from it clears a stored range. A window in the past (e.g. brushed on the histogram) is never saved; updating from one keeps the stored range.
  • 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. The modal shows Opens with 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, switch the time picker to Live Tail and click Update. The modal shows Opens in live tail. 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, confirm the modal shows Opens with the past 1 day, 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.

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

7 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: 13 files, 5b96dcbc..HEAD (the three saved-search commits). The provided base 0262822 is a diverged main; the merge-base 5b96dcbc is the true fork point, so the review is scoped to the saved-search time-range change only.
Intent: Add an optional relative defaultTimeRange to saved searches so they open with a stored range (live tail for 1m–6h, a window ending now for longer) instead of always landing in Live Tail; allow clearing it via an explicit null on PATCH.

✅ No critical issues found.

The null-clearing path is sound: after SavedSearchSchema.partial(), only defaultTimeRange is nullable, so validateRequest returns 400 for null on every other field before the handler runs, and switching _.omitBy from _.isNil to _.isUndefined only lets null through for defaultTimeRange (confirmed by the orderBy: null → 400 test). The dashboard savedDateRange refactor to the shared RelativeDateRangeSchema is behavior-preserving, the list response still returns the field, and the landing useEffect cannot loop (once setSearchedConfig populates the config, isSearchConfigEmpty is false and the effect body is skipped; a re-run would also see isLive in the URL and bail). The three history: 'replace' URL writes are consistent, and the two e2e tests assert Back returns to the list.

🟡 P2 -- recommended

  • packages/api/src/routers/api/savedSearch.ts:92 -- The _.isUndefined change is correct only while defaultTimeRange is the sole nullable field in SavedSearchSchema; a future .nullable()/.nullish() field would silently have null written to the DB, and this invariant is guarded by just one orderBy-specific test.
    • Fix: Add a test asserting null is rejected for a representative non-nullable field and a code comment next to the omitBy call stating the invariant, or narrow the clear to defaultTimeRange explicitly.
    • kieran-typescript, api-contract
🔵 P3 nitpicks (5)
  • packages/app/src/components/SavedSearches/savedSearchTimeRange.ts:85 -- formatRelativeDuration (epoch-0 anchored, DST-safe) duplicates the formatDuration(intervalToDuration(...)) shape of getRelativeInterval in timeQuery.ts, inviting a future consolidation that reintroduces DST drift or a one-sided fix.
    • Fix: Cross-reference the two formatters in a comment noting the intentional epoch-0 anchor, or have getRelativeInterval delegate to the DST-safe helper.
  • packages/app/src/components/SavedSearches/savedSearchTimeRange.ts:90 -- CurrentTimeRange and SavedSearchLanding are structurally identical union aliases that can drift apart silently.
    • Fix: Share one type, or add a one-line comment that they intentionally mirror each other.
  • packages/app/src/components/SavedSearches/SaveTimeRangeField.tsx:17 -- The range == null branch of describeOpensWith ("Opens in live tail") has no coverage: the function is unexported and both e2e tests pre-select a non-default range.
    • Fix: Assert the default-state label in an e2e, or export and unit-test all three describeOpensWith branches.
  • packages/app/src/components/SavedSearches/savedSearchTimeRange.ts:49 -- A window of a live-tail-supported duration (e.g. exactly 15m/1h/6h) saved as a default reopens as live tail rather than a fixed window, because getSavedSearchLanding routes any isLiveTailDuration to kind: 'live'.
    • Fix: Accept as intended, or add a comment documenting that live-tail-eligible windows reopen as live tail.
  • packages/api/src/models/savedSearch.ts:89 -- defaultTimeRange is absent from toExternalJSON/the external v2 surface, so a UI-set value is invisible to and unclearable by external/agent clients and survives an external "full replace" PUT.
    • Fix: Document the internal-only scope inline, or add the field to the external request/response when parity is wanted (the PR notes external API v2 as a follow-up).
    • api-contract

Reviewers (5 returned): kieran-typescript, api-contract, testing, maintainability, previous-comments. Correctness, adversarial, frontend-races, and project-standards surfaces were additionally verified directly by the orchestrator (null-clearing path, history-replace writes, effect-loop safety, e2e-helper existence, changeset presence) with no P0/P1 found. The previous-comments reviewer confirmed all prior feedback is addressed — notably the maintainer's UX note, which the latest commit resolved by replacing the option control with descriptive text plus a tooltip.

Testing gaps:

  • No API test covers a PATCH that updates an existing saved search to a new defaultTimeRange value (the non-null pass-through); only create, update-omit (keep), and update-null (clear) are tested.
  • The DBSearchPage landing wiring (reading window.location.search, history: 'replace', label setting) is covered only by e2e, so e2e flakiness could mask a regression; the pure decision helpers are well unit-tested.
  • isRangeEndingNow is not tested for a window whose end is slightly in the future.

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.

label={
isLiveTailDuration(currentRange.value * 1000)
? `Show live tail for the past ${formatRelativeDuration(currentRange.value)}`
: `Show the past ${formatRelativeDuration(currentRange.value)}`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

UX Feedback: would it not make sense to just say 'Saving Saved Search with the past {relativeTime} as default search' or something along those lines? I feel like we should not give an option for this, but add a tooltip to explain how to customize

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.

Thanks for the suggestion,

Done in 3a4b8b1:

Removed the "When opened" choice. Saving now stores the selected range automatically.
The modal shows what the search will open with, e.g. "Opens with the past 1 day" or "Opens in live tail for the past 1 hour", with a tooltip explaining how to change it.
Ranges that end in the past aren't saved, and on Update they keep the stored range instead of clearing it.
Here's how it looks now. Happy to adjust the wording if you'd prefer something different.

Per review: drop the "When opened" choice from the save modal. Saving
stores the selected range when it reads as "the past N" (a live tail
interval or a window ending now). The modal shows what the search will
open with, and a tooltip explains how to change it.
[start, end]: [Date, Date],
nowMs: number,
): boolean {
const tolerance = Math.max(

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 — Fixed windows that ended hours or days ago get saved as "the past N", which the tooltip says can't happen

Allowing 10% of the duration lets a 30-day absolute range that ended 2 days ago (tolerance is 3 days) be stored as {relative, 2592000}, and a 7-day range that ended 16h ago passes too. The tooltip in SaveTimeRangeField.tsx says "Ranges that end in the past are not saved". Cap the slack at a fixed small amount, or decide from what the picker shows rather than from timestamps (next finding).

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

const range = toDefaultTimeRange(current.intervalMs);
return { value: range, opensWith: range };
}
if (isRangeEndingNow(current.range, nowMs)) {

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 — Dashboards and saved searches decide "is this relative" in two different ways

Dashboards get the relative range from the picker text with timeRangeInputToSeconds(displayedTimeInputValue, timeZone) (DBDashboardPage.tsx:2176, helper in components/TimePicker/utils.ts:166). This PR guesses from the window's end time with a tolerance instead. Reuse the existing picker-text approach, or move one shared rule into TimePicker/utils.ts, so the same picker choice saves the same way in both features.

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

'@hyperdx/common-utils': minor
---

feat: saved searches can store a default time range (e.g. past 1 day) to open with instead of live tail, chosen in the save modal under "When opened"

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 — Changeset mentions a "When opened" control that no longer exists

The last commit, "save the selected time range without asking", removed the chooser. The modal now only shows read-only "Opens with…" text. Reword the release note to say the selected range is saved automatically.

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

return formatDuration(intervalToDuration({ start: 0, end: seconds * 1000 }));
}

export type CurrentTimeRange =

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 — CurrentTimeRange is the same union as SavedSearchLanding in the same file

Both are {kind:'live'; intervalMs} | {kind:'window'; range}. Keep one exported type, e.g. type CurrentTimeRange = SavedSearchLanding. Also have the live landing return its range too, so DBSearchPage.tsx:1336 doesn't need to call parseRelativeTimeQuery a second time.

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

}

/** "1 day", "1 hour 30 minutes". Anchored at epoch 0 so DST never shifts it. */
export function formatRelativeDuration(seconds: number): string {

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 copies the existing getRelativeInterval formatting

timeQuery.ts:420 (getRelativeInterval) already formats a span with formatDuration(intervalToDuration(...)). Export one shared helper (anchored at epoch 0) and use it in both places.

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

savedSearch.id === savedSearchId && // Make sure we've loaded the correct saved search
isSearchConfigEmpty // Only populate if URL doesn't have explicit config
) {
const nowMs = performance.timeOrigin + performance.now();

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 — performance.timeOrigin + performance.now() is used to get around the Date.now() lint rule

Here and in openSaveSearchModal, both inside an effect/callback, which the rule's message says is fine. Use Date.now() with // eslint-disable-next-line no-restricted-syntax, as parseRelativeTimeQuery in timeQuery.ts already does, so the intent stays obvious.

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

return (
<Group gap={6} data-testid="save-search-time-range">
<Text size="xs">{describeOpensWith(opensWith)}</Text>
<Tooltip label={TOOLTIP} multiline w={280} withArrow>

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 — Keyboard users can't open the tooltip that explains how the range is saved

The Tooltip wraps a bare IconInfoCircle SVG, which can't take focus, so the explanation only shows on mouse hover. Wrap the icon in a focusable element such as <ActionIcon variant="subtle" aria-label=…>, or show the text inline under the label.

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

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

2 participants