Repository navigation
feat: open saved searches with a saved default time range #3593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@hyperdx/api': minor | ||
| '@hyperdx/app': minor | ||
| '@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" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -153,6 +153,11 @@ import { | |
| import ChartSQLPreview, { SQLPreview } from './components/ChartSQLPreview'; | ||
| import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar'; | ||
| import PatternTable from './components/PatternTable'; | ||
| import { getSavedSearchLanding } from './components/SavedSearches/savedSearchTimeRange'; | ||
| import { | ||
| SaveTimeRangeField, | ||
| useSaveTimeRangeField, | ||
| } from './components/SavedSearches/SaveTimeRangeField'; | ||
| import { DBSearchHeatmapChart } from './components/Search/DBSearchHeatmapChart'; | ||
| import DirectTraceSidePanel from './components/Search/DirectTraceSidePanel'; | ||
| import SourceSchemaPreview, { | ||
|
|
@@ -465,13 +470,21 @@ function SaveSearchModalComponent({ | |
| onCreate, | ||
| isUpdate, | ||
| savedSearchId, | ||
| isLive, | ||
| liveIntervalMs, | ||
| searchedTimeRange, | ||
| openedAtMs, | ||
| }: { | ||
| searchedConfig: SearchConfig; | ||
| opened: boolean; | ||
| onClose: () => void; | ||
| onCreate: (savedSearchId: string) => Promise<void>; | ||
| isUpdate: boolean; | ||
| savedSearchId: string | undefined | null; | ||
| isLive: boolean; | ||
| liveIntervalMs: number; | ||
| searchedTimeRange: [Date, Date]; | ||
| openedAtMs: number | null; | ||
| }) { | ||
| const { data: savedSearch } = useSavedSearch( | ||
| { id: savedSearchId ?? '' }, | ||
|
|
@@ -480,6 +493,15 @@ function SaveSearchModalComponent({ | |
| }, | ||
| ); | ||
|
|
||
| const timeRangeOutcome = useSaveTimeRangeField({ | ||
| openedAtMs, | ||
| isLive, | ||
| liveIntervalMs, | ||
| searchedTimeRange, | ||
| isUpdate, | ||
| stored: savedSearch?.defaultTimeRange, | ||
| }); | ||
|
|
||
| const { | ||
| control, | ||
| handleSubmit, | ||
|
|
@@ -528,6 +550,9 @@ function SaveSearchModalComponent({ | |
| e.preventDefault(); | ||
|
|
||
| handleSubmit(async ({ name }) => { | ||
| const timeRange = timeRangeOutcome.value; | ||
| const defaultTimeRangeUpdate = | ||
| timeRange === undefined ? {} : { defaultTimeRange: timeRange }; | ||
| if (isUpdate) { | ||
| if (savedSearchId == null) { | ||
| throw new Error('savedSearchId is required for update'); | ||
|
|
@@ -545,6 +570,7 @@ function SaveSearchModalComponent({ | |
| orderBy: searchedConfig.orderBy ?? '', | ||
| filters: searchedConfig.filters ?? [], | ||
| tags: tags, | ||
| ...defaultTimeRangeUpdate, | ||
| }, | ||
| { | ||
| onSuccess: () => { | ||
|
|
@@ -573,6 +599,7 @@ function SaveSearchModalComponent({ | |
| orderBy: searchedConfig.orderBy ?? '', | ||
| filters: searchedConfig.filters ?? [], | ||
| tags: tags, | ||
| ...defaultTimeRangeUpdate, | ||
| }); | ||
|
|
||
| await onCreate(savedSearch.id); | ||
|
|
@@ -701,6 +728,7 @@ function SaveSearchModalComponent({ | |
| </Tags> | ||
| </Group> | ||
| </Box> | ||
| <SaveTimeRangeField opensWith={timeRangeOutcome.opensWith} /> | ||
| <Button | ||
| data-testid="save-search-submit-button" | ||
| variant="primary" | ||
|
|
@@ -1131,6 +1159,10 @@ function DBSearchPageContent() { | |
| 'isLive', | ||
| parseAsBoolean.withDefault(true), | ||
| ); | ||
| const [interval, setInterval] = useQueryState( | ||
| 'liveInterval', | ||
| parseAsInteger.withDefault(LIVE_TAIL_DURATION_MS), | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| if (analysisMode === 'delta' || analysisMode === 'pattern') { | ||
|
|
@@ -1291,6 +1323,28 @@ function DBSearchPageContent() { | |
| 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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Here and in 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 landing = getSavedSearchLanding( | ||
| savedSearch.defaultTimeRange, | ||
| window.location.search, | ||
| nowMs, | ||
| ); | ||
| // Replace, not push: a pushed entry would leave the bare URL behind, | ||
| // and Back would land on it and re-apply the range forever. | ||
| if (landing?.kind === 'live') { | ||
| // Same writes as picking a live tail range in the time picker. | ||
| const [start, end] = parseRelativeTimeQuery(landing.intervalMs, nowMs); | ||
| setIsLive(true, { history: 'replace' }); | ||
| setInterval(landing.intervalMs, { history: 'replace' }); | ||
| onTimeRangeSelect(start, end, null, { history: 'replace' }); | ||
| const label = getRelativeTimeOptionLabel(landing.intervalMs); | ||
| if (label) setDisplayedTimeInputValue(label); | ||
| } else if (landing?.kind === 'window') { | ||
| setIsLive(false, { history: 'replace' }); | ||
| onTimeRangeSelect(landing.range[0], landing.range[1], undefined, { | ||
| history: 'replace', | ||
| }); | ||
| } | ||
| setSearchedConfig({ | ||
| source: savedSearch.source, | ||
| where: savedSearch.where, | ||
|
|
@@ -1327,6 +1381,9 @@ function DBSearchPageContent() { | |
| defaultSourceId, | ||
| directTraceId, | ||
| sources, | ||
| setIsLive, | ||
| setInterval, | ||
| onTimeRangeSelect, | ||
| ]); | ||
|
|
||
| const [_queryErrors, setQueryErrors] = useState<{ | ||
|
|
@@ -1593,9 +1650,19 @@ function DBSearchPageContent() { | |
| }, [setIsLive]); | ||
|
|
||
| const [modelFormExpanded, setModelFormExpanded] = useState(false); // Used in local mode | ||
| // openedAtMs is read in the click handler: the modal compares it with the | ||
| // searched window to decide whether that window reads as "the past N". | ||
| const [saveSearchModalState, setSaveSearchModalState] = useState< | ||
| 'create' | 'update' | undefined | ||
| { mode: 'create' | 'update'; openedAtMs: number } | undefined | ||
| >(undefined); | ||
| const openSaveSearchModal = useCallback( | ||
| (mode: 'create' | 'update') => | ||
| setSaveSearchModalState({ | ||
| mode, | ||
| openedAtMs: performance.timeOrigin + performance.now(), | ||
| }), | ||
| [], | ||
| ); | ||
| const chartSearchConfig = useMemo( | ||
| () => ({ | ||
| select: searchedConfig.select ?? '', | ||
|
|
@@ -1644,7 +1711,7 @@ function DBSearchPageContent() { | |
| const deleteSavedSearch = useDeleteSavedSearch(); | ||
| const onSaveSearch = useCallback(() => { | ||
| if (savedSearch == null) { | ||
| setSaveSearchModalState('create'); | ||
| openSaveSearchModal('create'); | ||
| } else { | ||
| handleSubmit(s => { | ||
| updateSavedSearch.mutate( | ||
|
|
@@ -1661,7 +1728,13 @@ function DBSearchPageContent() { | |
| ); | ||
| })(); | ||
| } | ||
| }, [savedSearch, updateSavedSearch, onSubmit, handleSubmit]); | ||
| }, [ | ||
| savedSearch, | ||
| updateSavedSearch, | ||
| onSubmit, | ||
| handleSubmit, | ||
| openSaveSearchModal, | ||
| ]); | ||
|
|
||
| const handleUpdateTags = useCallback( | ||
| (newTags: string[]) => { | ||
|
|
@@ -1721,11 +1794,6 @@ function DBSearchPageContent() { | |
| // State for collapsing all expanded rows when resuming live tail | ||
| const [collapseAllRows, setCollapseAllRows] = useState(false); | ||
|
|
||
| const [interval, setInterval] = useQueryState( | ||
| 'liveInterval', | ||
| parseAsInteger.withDefault(LIVE_TAIL_DURATION_MS), | ||
| ); | ||
|
|
||
| const [refreshFrequency, setRefreshFrequency] = useQueryState( | ||
| 'refreshFrequency', | ||
| parseAsInteger.withDefault(DEFAULT_REFRESH_FREQUENCY), | ||
|
|
@@ -2482,7 +2550,7 @@ function DBSearchPageContent() { | |
| }); | ||
| }} | ||
| onClickSaveAsNew={() => { | ||
| setSaveSearchModalState('create'); | ||
| openSaveSearchModal('create'); | ||
| }} | ||
| /> | ||
| </Group> | ||
|
|
@@ -2543,7 +2611,7 @@ function DBSearchPageContent() { | |
| variant="secondary" | ||
| size="xs" | ||
| onClick={() => { | ||
| setSaveSearchModalState('update'); | ||
| openSaveSearchModal('update'); | ||
| }} | ||
| style={{ flexShrink: 0 }} | ||
| > | ||
|
|
@@ -2668,8 +2736,12 @@ function DBSearchPageContent() { | |
| onClose={clearSaveSearchModalState} | ||
| onCreate={onSavedSearchCreated} | ||
| searchedConfig={searchedConfig} | ||
| isUpdate={saveSearchModalState === 'update'} | ||
| isUpdate={saveSearchModalState?.mode === 'update'} | ||
| savedSearchId={savedSearchId} | ||
| isLive={isLive} | ||
| liveIntervalMs={interval} | ||
| searchedTimeRange={searchedTimeRange} | ||
| openedAtMs={saveSearchModalState?.openedAtMs ?? null} | ||
| /> | ||
| )} | ||
| <DirectTraceSidePanel | ||
|
|
||
There was a problem hiding this comment.
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