Skip to content

Commit de0b4fc

Browse files
Search Relative Time Queries (#1305)
Adds "Relative Time" switch to TimePicker component (if relative time is supported by parent). When enabled, searches will work similar to Live Tail but be relative to the option selected. <img width="555" height="418" alt="Screenshot 2025-10-27 at 2 05 25 PM" src="https://github.com/user-attachments/assets/20d38011-d5d0-479f-a8ea-6b0be441ca87" /> Some notes: 1. When relative is enabled, I disabled very large time ranges to prioritize performance. 2. If you select "Last 15 mins" then reload, the Input will save "Live Tail" because these are the same option, this should be an edge case. 3. In the future, we might want to make "Relative Time" the default, but I didn't want to immediately do that. We could probably improve the UX further (cc @elizabetdev). 4. Moves a lot of the "Live Tail" logic out of various spots and centralizes it in a unified spot to support other values Fixes HDX-2653
1 parent 808413f commit de0b4fc

File tree

11 files changed

+584
-87
lines changed

11 files changed

+584
-87
lines changed

.changeset/nine-dragons-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
Adds "Relative Time" switch to TimePicker component (if relative time is supported by parent). When enabled, searches will work similar to Live Tail but be relative to the option selected.

.prettierignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Ignore artifacts:
22
dist
33
coverage
4-
tests
54
.volumes

packages/app/.stylelintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.next
22
.storybook
33
node_modules
4-
coverage
4+
coverage
5+
playwright-report

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"ky": "^0.30.0",
6464
"ky-universal": "^0.10.1",
6565
"lodash": "^4.17.21",
66+
"ms": "^2.1.3",
6667
"next": "^14.2.32",
6768
"next-query-params": "^4.1.0",
6869
"next-runtime-env": "1",
@@ -125,6 +126,7 @@
125126
"@types/intercom-web": "^2.8.18",
126127
"@types/jest": "^29.5.14",
127128
"@types/lodash": "^4.14.186",
129+
"@types/ms": "^0.7.31",
128130
"@types/object-hash": "^2.2.1",
129131
"@types/pluralize": "^0.0.29",
130132
"@types/react": "18.3.1",

packages/app/playwright.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default defineConfig({
2424
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
2525
use: {
2626
/* Base URL to use in actions like `await page.goto('/')`. */
27-
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
27+
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8081',
2828
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
2929
trace: 'on-first-retry',
3030
/* Take screenshot on failure */
@@ -49,8 +49,8 @@ export default defineConfig({
4949
/* Run your local dev server before starting the tests */
5050
webServer: {
5151
command:
52-
'NEXT_PUBLIC_IS_LOCAL_MODE=true NEXT_TELEMETRY_DISABLED=1 yarn run dev',
53-
port: 8080,
52+
'NEXT_PUBLIC_IS_LOCAL_MODE=true NEXT_TELEMETRY_DISABLED=1 PORT=8081 yarn run dev',
53+
port: 8081,
5454
reuseExistingServer: !process.env.CI,
5555
timeout: 180 * 1000,
5656
stdout: 'pipe',

packages/app/src/DBSearchPage.tsx

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Link from 'next/link';
1212
import router from 'next/router';
1313
import {
1414
parseAsBoolean,
15+
parseAsInteger,
1516
parseAsJson,
1617
parseAsString,
1718
parseAsStringEnum,
@@ -43,7 +44,6 @@ import {
4344
Flex,
4445
Grid,
4546
Group,
46-
Input,
4747
Menu,
4848
Modal,
4949
Paper,
@@ -91,14 +91,22 @@ import {
9191
useSource,
9292
useSources,
9393
} from '@/source';
94-
import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery';
94+
import {
95+
parseRelativeTimeQuery,
96+
parseTimeQuery,
97+
useNewTimeQuery,
98+
} from '@/timeQuery';
9599
import { QUERY_LOCAL_STORAGE, useLocalStorage, usePrevious } from '@/utils';
96100

97101
import { SQLPreview } from './components/ChartSQLPreview';
98102
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
99103
import PatternTable from './components/PatternTable';
100104
import { DBSearchHeatmapChart } from './components/Search/DBSearchHeatmapChart';
101105
import SourceSchemaPreview from './components/SourceSchemaPreview';
106+
import {
107+
getRelativeTimeOptionLabel,
108+
LIVE_TAIL_DURATION_MS,
109+
} from './components/TimePicker/utils';
102110
import { useTableMetadata } from './hooks/useMetadata';
103111
import { useSqlSuggestions } from './hooks/useSqlSuggestions';
104112
import {
@@ -417,7 +425,7 @@ function useLiveUpdate({
417425
onTimeRangeSelect: (
418426
start: Date,
419427
end: Date,
420-
displayedTimeInputValue?: string | undefined,
428+
displayedTimeInputValue?: string | null,
421429
) => void;
422430
pause: boolean;
423431
}) {
@@ -426,7 +434,7 @@ function useLiveUpdate({
426434
const [refreshOnVisible, setRefreshOnVisible] = useState(false);
427435

428436
const refresh = useCallback(() => {
429-
onTimeRangeSelect(new Date(Date.now() - interval), new Date(), 'Live Tail');
437+
onTimeRangeSelect(new Date(Date.now() - interval), new Date(), null);
430438
}, [onTimeRangeSelect, interval]);
431439

432440
// When the user comes back to the app after switching tabs, we immediately refresh the list.
@@ -664,8 +672,10 @@ function DBSearchPage() {
664672
parseAsString,
665673
);
666674

667-
const [_isLive, setIsLive] = useQueryState('isLive', parseAsBoolean);
668-
const isLive = _isLive ?? true;
675+
const [isLive, setIsLive] = useQueryState(
676+
'isLive',
677+
parseAsBoolean.withDefault(true),
678+
);
669679

670680
useEffect(() => {
671681
if (analysisMode === 'delta' || analysisMode === 'pattern') {
@@ -727,7 +737,7 @@ function DBSearchPage() {
727737
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
728738
useState('Live Tail');
729739

730-
const { from, to, isReady, searchedTimeRange, onSearch, onTimeRangeSelect } =
740+
const { isReady, searchedTimeRange, onSearch, onTimeRangeSelect } =
731741
useNewTimeQuery({
732742
initialDisplayValue: 'Live Tail',
733743
initialTimeRange: defaultTimeRange,
@@ -736,18 +746,6 @@ function DBSearchPage() {
736746
updateInput: !isLive,
737747
});
738748

739-
// If live tail is null, but time range exists, don't live tail
740-
// If live tail is null, and time range is null, let's live tail
741-
useEffect(() => {
742-
if (_isLive == null && isReady) {
743-
if (from == null && to == null) {
744-
setIsLive(true);
745-
} else {
746-
setIsLive(false);
747-
}
748-
}
749-
}, [_isLive, setIsLive, from, to, isReady]);
750-
751749
// Sync url state back with form state
752750
// (ex. for history navigation)
753751
// TODO: Check if there are any bad edge cases here
@@ -1014,9 +1012,29 @@ function DBSearchPage() {
10141012
// State for collapsing all expanded rows when resuming live tail
10151013
const [collapseAllRows, setCollapseAllRows] = useState(false);
10161014

1015+
const [interval, setInterval] = useQueryState(
1016+
'liveInterval',
1017+
parseAsInteger.withDefault(LIVE_TAIL_DURATION_MS),
1018+
);
1019+
1020+
const updateRelativeTimeInputValue = useCallback((interval: number) => {
1021+
const label = getRelativeTimeOptionLabel(interval);
1022+
if (label) {
1023+
setDisplayedTimeInputValue(label);
1024+
}
1025+
}, []);
1026+
1027+
useEffect(() => {
1028+
if (isReady && isLive) {
1029+
updateRelativeTimeInputValue(interval);
1030+
}
1031+
// we only want this to run on initial mount
1032+
// eslint-disable-next-line react-hooks/exhaustive-deps
1033+
}, [updateRelativeTimeInputValue, isReady]);
1034+
10171035
useLiveUpdate({
10181036
isLive,
1019-
interval: 1000 * 60 * 15,
1037+
interval,
10201038
refreshFrequency: 4000,
10211039
onTimeRangeSelect,
10221040
pause: isAnyQueryFetching || !queryReady || !isTabVisible,
@@ -1043,13 +1061,12 @@ function DBSearchPage() {
10431061

10441062
const handleResumeLiveTail = useCallback(() => {
10451063
setIsLive(true);
1046-
setDisplayedTimeInputValue('Live Tail');
1064+
updateRelativeTimeInputValue(interval);
10471065
// Trigger collapsing all expanded rows
10481066
setCollapseAllRows(true);
10491067
// Reset the collapse trigger after a short delay
10501068
setTimeout(() => setCollapseAllRows(false), 100);
1051-
onSearch('Live Tail');
1052-
}, [onSearch, setIsLive]);
1069+
}, [interval, updateRelativeTimeInputValue, setIsLive]);
10531070

10541071
const dbSqlRowTableConfig = useMemo(() => {
10551072
if (chartConfig == null) {
@@ -1508,14 +1525,21 @@ function DBSearchPage() {
15081525
inputValue={displayedTimeInputValue}
15091526
setInputValue={setDisplayedTimeInputValue}
15101527
onSearch={range => {
1511-
if (range === 'Live Tail') {
1512-
setIsLive(true);
1513-
} else {
1514-
setIsLive(false);
1515-
}
1528+
setIsLive(false);
15161529
onSearch(range);
15171530
}}
1531+
onRelativeSearch={rangeMs => {
1532+
const _range = parseRelativeTimeQuery(rangeMs);
1533+
setIsLive(true);
1534+
setInterval(rangeMs);
1535+
onTimeRangeSelect(_range[0], _range[1], null);
1536+
}}
15181537
showLive={analysisMode === 'results'}
1538+
isLiveMode={isLive}
1539+
// Default to relative time mode if the user has made changes to interval and reloaded.
1540+
defaultRelativeTimeMode={
1541+
isLive && interval !== LIVE_TAIL_DURATION_MS
1542+
}
15191543
/>
15201544
<Button
15211545
data-testid="search-submit-button"

0 commit comments

Comments
 (0)