diff --git a/.changeset/nine-dragons-clap.md b/.changeset/nine-dragons-clap.md new file mode 100644 index 000000000..62de4ffed --- /dev/null +++ b/.changeset/nine-dragons-clap.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +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. diff --git a/.prettierignore b/.prettierignore index 6b540313c..ff2b07e26 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,4 @@ # Ignore artifacts: dist coverage -tests .volumes diff --git a/packages/app/.stylelintignore b/packages/app/.stylelintignore index f9a6ffa62..1ef1a71a8 100644 --- a/packages/app/.stylelintignore +++ b/packages/app/.stylelintignore @@ -1,4 +1,5 @@ .next .storybook node_modules -coverage \ No newline at end of file +coverage +playwright-report \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index 39768c200..66bc55791 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -63,6 +63,7 @@ "ky": "^0.30.0", "ky-universal": "^0.10.1", "lodash": "^4.17.21", + "ms": "^2.1.3", "next": "^14.2.32", "next-query-params": "^4.1.0", "next-runtime-env": "1", @@ -125,6 +126,7 @@ "@types/intercom-web": "^2.8.18", "@types/jest": "^29.5.14", "@types/lodash": "^4.14.186", + "@types/ms": "^0.7.31", "@types/object-hash": "^2.2.1", "@types/pluralize": "^0.0.29", "@types/react": "18.3.1", diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts index 3f1602ee2..498fb6fb2 100644 --- a/packages/app/playwright.config.ts +++ b/packages/app/playwright.config.ts @@ -24,7 +24,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080', + baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8081', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', /* Take screenshot on failure */ @@ -49,8 +49,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { command: - 'NEXT_PUBLIC_IS_LOCAL_MODE=true NEXT_TELEMETRY_DISABLED=1 yarn run dev', - port: 8080, + 'NEXT_PUBLIC_IS_LOCAL_MODE=true NEXT_TELEMETRY_DISABLED=1 PORT=8081 yarn run dev', + port: 8081, reuseExistingServer: !process.env.CI, timeout: 180 * 1000, stdout: 'pipe', diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index 96bd604c6..834668b8b 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -12,6 +12,7 @@ import Link from 'next/link'; import router from 'next/router'; import { parseAsBoolean, + parseAsInteger, parseAsJson, parseAsString, parseAsStringEnum, @@ -43,7 +44,6 @@ import { Flex, Grid, Group, - Input, Menu, Modal, Paper, @@ -91,7 +91,11 @@ import { useSource, useSources, } from '@/source'; -import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery'; +import { + parseRelativeTimeQuery, + parseTimeQuery, + useNewTimeQuery, +} from '@/timeQuery'; import { QUERY_LOCAL_STORAGE, useLocalStorage, usePrevious } from '@/utils'; import { SQLPreview } from './components/ChartSQLPreview'; @@ -99,6 +103,10 @@ import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar'; import PatternTable from './components/PatternTable'; import { DBSearchHeatmapChart } from './components/Search/DBSearchHeatmapChart'; import SourceSchemaPreview from './components/SourceSchemaPreview'; +import { + getRelativeTimeOptionLabel, + LIVE_TAIL_DURATION_MS, +} from './components/TimePicker/utils'; import { useTableMetadata } from './hooks/useMetadata'; import { useSqlSuggestions } from './hooks/useSqlSuggestions'; import { @@ -417,7 +425,7 @@ function useLiveUpdate({ onTimeRangeSelect: ( start: Date, end: Date, - displayedTimeInputValue?: string | undefined, + displayedTimeInputValue?: string | null, ) => void; pause: boolean; }) { @@ -426,7 +434,7 @@ function useLiveUpdate({ const [refreshOnVisible, setRefreshOnVisible] = useState(false); const refresh = useCallback(() => { - onTimeRangeSelect(new Date(Date.now() - interval), new Date(), 'Live Tail'); + onTimeRangeSelect(new Date(Date.now() - interval), new Date(), null); }, [onTimeRangeSelect, interval]); // When the user comes back to the app after switching tabs, we immediately refresh the list. @@ -664,8 +672,10 @@ function DBSearchPage() { parseAsString, ); - const [_isLive, setIsLive] = useQueryState('isLive', parseAsBoolean); - const isLive = _isLive ?? true; + const [isLive, setIsLive] = useQueryState( + 'isLive', + parseAsBoolean.withDefault(true), + ); useEffect(() => { if (analysisMode === 'delta' || analysisMode === 'pattern') { @@ -727,7 +737,7 @@ function DBSearchPage() { const [displayedTimeInputValue, setDisplayedTimeInputValue] = useState('Live Tail'); - const { from, to, isReady, searchedTimeRange, onSearch, onTimeRangeSelect } = + const { isReady, searchedTimeRange, onSearch, onTimeRangeSelect } = useNewTimeQuery({ initialDisplayValue: 'Live Tail', initialTimeRange: defaultTimeRange, @@ -736,18 +746,6 @@ function DBSearchPage() { updateInput: !isLive, }); - // If live tail is null, but time range exists, don't live tail - // If live tail is null, and time range is null, let's live tail - useEffect(() => { - if (_isLive == null && isReady) { - if (from == null && to == null) { - setIsLive(true); - } else { - setIsLive(false); - } - } - }, [_isLive, setIsLive, from, to, isReady]); - // Sync url state back with form state // (ex. for history navigation) // TODO: Check if there are any bad edge cases here @@ -1014,9 +1012,29 @@ function DBSearchPage() { // 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 updateRelativeTimeInputValue = useCallback((interval: number) => { + const label = getRelativeTimeOptionLabel(interval); + if (label) { + setDisplayedTimeInputValue(label); + } + }, []); + + useEffect(() => { + if (isReady && isLive) { + updateRelativeTimeInputValue(interval); + } + // we only want this to run on initial mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [updateRelativeTimeInputValue, isReady]); + useLiveUpdate({ isLive, - interval: 1000 * 60 * 15, + interval, refreshFrequency: 4000, onTimeRangeSelect, pause: isAnyQueryFetching || !queryReady || !isTabVisible, @@ -1043,13 +1061,12 @@ function DBSearchPage() { const handleResumeLiveTail = useCallback(() => { setIsLive(true); - setDisplayedTimeInputValue('Live Tail'); + updateRelativeTimeInputValue(interval); // Trigger collapsing all expanded rows setCollapseAllRows(true); // Reset the collapse trigger after a short delay setTimeout(() => setCollapseAllRows(false), 100); - onSearch('Live Tail'); - }, [onSearch, setIsLive]); + }, [interval, updateRelativeTimeInputValue, setIsLive]); const dbSqlRowTableConfig = useMemo(() => { if (chartConfig == null) { @@ -1508,14 +1525,21 @@ function DBSearchPage() { inputValue={displayedTimeInputValue} setInputValue={setDisplayedTimeInputValue} onSearch={range => { - if (range === 'Live Tail') { - setIsLive(true); - } else { - setIsLive(false); - } + setIsLive(false); onSearch(range); }} + onRelativeSearch={rangeMs => { + const _range = parseRelativeTimeQuery(rangeMs); + setIsLive(true); + setInterval(rangeMs); + onTimeRangeSelect(_range[0], _range[1], null); + }} showLive={analysisMode === 'results'} + isLiveMode={isLive} + // Default to relative time mode if the user has made changes to interval and reloaded. + defaultRelativeTimeMode={ + isLive && interval !== LIVE_TAIL_DURATION_MS + } /> @@ -264,12 +309,10 @@ export const TimePicker = ({ color="gray" variant="light" onClick={handleMove.bind(null, { hours: 1 })} - disabled={isLiveMode} + disabled={isLiveMode || isRelative} > 1h forward - - @@ -282,8 +325,19 @@ export const TimePicker = ({ ) : (