diff --git a/packages/app/.storybook/preview.tsx b/packages/app/.storybook/preview.tsx
index 6ac5a54030..59fd244c35 100644
--- a/packages/app/.storybook/preview.tsx
+++ b/packages/app/.storybook/preview.tsx
@@ -1,7 +1,6 @@
import React from 'react';
-import { NextAdapter } from 'next-query-params';
import { initialize, mswLoader } from 'msw-storybook-addon';
-import { QueryParamProvider } from 'use-query-params';
+import { NuqsAdapter } from 'nuqs/adapters/next/pages';
import type { Preview } from '@storybook/nextjs';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@@ -88,16 +87,16 @@ const preview: Preview = {
return (
-
-
+
+
-
-
+
+
);
},
diff --git a/packages/app/jest.config.js b/packages/app/jest.config.js
index f45dc96e0d..17d99f7884 100644
--- a/packages/app/jest.config.js
+++ b/packages/app/jest.config.js
@@ -14,7 +14,7 @@ module.exports = {
globalSetup: '/global-setup.js',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
- transformIgnorePatterns: ['/node_modules/'],
+ transformIgnorePatterns: ['/node_modules/(?!(nuqs)/)'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'^@/(.*)$': '/src/$1',
diff --git a/packages/app/package.json b/packages/app/package.json
index 1e719baa34..22db14a17c 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -68,11 +68,10 @@
"lodash": "^4.17.21",
"ms": "^2.1.3",
"next": "^16.1.5",
- "next-query-params": "^4.3.1",
"next-runtime-env": "1",
"next-seo": "^4.28.1",
"numbro": "^2.4.0",
- "nuqs": "1.17.0",
+ "nuqs": "^2.8.8",
"object-hash": "^3.0.0",
"react": "^19.2.3",
"react-copy-to-clipboard": "^5.1.0",
@@ -91,7 +90,6 @@
"recharts": "^2.12.7",
"rrweb": "2.0.0-alpha.8",
"sass": "^1.54.8",
- "serialize-query-params": "^2.0.2",
"sql-formatter": "^15.4.0",
"sqlstring": "^2.3.3",
"store2": "^2.14.3",
@@ -99,14 +97,12 @@
"timestamp-nano": "^1.0.1",
"uplot": "^1.6.31",
"uplot-react": "^1.2.2",
- "use-query-params": "^2.1.2",
"zod": "3.25"
},
"devDependencies": {
"@chromatic-com/storybook": "^4.1.3",
"@eslint/compat": "^2.0.0",
"@hookform/devtools": "^4.3.1",
- "@jedmao/location": "^3.0.0",
"@playwright/test": "^1.57.0",
"@storybook/addon-docs": "^10.1.4",
"@storybook/addon-links": "^10.1.4",
diff --git a/packages/app/pages/_app.tsx b/packages/app/pages/_app.tsx
index 9c4aa2da46..88dbceb603 100644
--- a/packages/app/pages/_app.tsx
+++ b/packages/app/pages/_app.tsx
@@ -2,10 +2,9 @@ import React, { useEffect } from 'react';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import Head from 'next/head';
-import { NextAdapter } from 'next-query-params';
import randomUUID from 'crypto-randomuuid';
import { enableMapSet } from 'immer';
-import { QueryParamProvider } from 'use-query-params';
+import { NuqsAdapter } from 'nuqs/adapters/next/pages';
import HyperDX from '@hyperdx/browser';
import {
MutationCache,
@@ -27,7 +26,6 @@ import { AppThemeProvider, useAppTheme } from '@/theme/ThemeProvider';
import { ThemeWrapper } from '@/ThemeWrapper';
import { NextApiConfigResponseData } from '@/types';
import { ConfirmProvider } from '@/useConfirm';
-import { QueryParamProvider as HDXQueryParamProvider } from '@/useQueryParam';
import {
SystemColorSchemeScript,
useResolvedColorScheme,
@@ -175,14 +173,12 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
-
-
-
-
-
-
-
-
+
+
+
+
+
+
);
diff --git a/packages/app/src/BenchmarkPage.tsx b/packages/app/src/BenchmarkPage.tsx
index 6de37eb799..4320bcb241 100644
--- a/packages/app/src/BenchmarkPage.tsx
+++ b/packages/app/src/BenchmarkPage.tsx
@@ -147,13 +147,13 @@ function useIndexes(
}
function BenchmarkPage() {
- const [queries, setQueries] = useQueryState(
+ const [queries, setQueries] = useQueryState(
'queries',
- parseAsJson(),
+ parseAsJson(v => v as string[]),
);
- const [connections, setConnections] = useQueryState(
+ const [connections, setConnections] = useQueryState(
'connections',
- parseAsJson(),
+ parseAsJson(v => v as string[]),
);
const [iterations, setIterations] = useQueryState(
'iterations',
diff --git a/packages/app/src/DBChartPage.tsx b/packages/app/src/DBChartPage.tsx
index bf918f3767..3709bbe0ca 100644
--- a/packages/app/src/DBChartPage.tsx
+++ b/packages/app/src/DBChartPage.tsx
@@ -223,7 +223,7 @@ function DBChartExplorerPage() {
const [chartConfig, setChartConfig] = useQueryState(
'config',
- parseAsJson().withDefault({
+ parseAsJson(v => v as SavedChartConfig).withDefault({
...DEFAULT_CHART_CONFIG,
source: sources?.[0]?.id ?? '',
}),
diff --git a/packages/app/src/DBDashboardImportPage.tsx b/packages/app/src/DBDashboardImportPage.tsx
index 2e46a6b7d8..021f976625 100644
--- a/packages/app/src/DBDashboardImportPage.tsx
+++ b/packages/app/src/DBDashboardImportPage.tsx
@@ -3,8 +3,8 @@ import dynamic from 'next/dynamic';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { filter } from 'lodash';
+import { useQueryState } from 'nuqs';
import { Controller, useForm, useWatch } from 'react-hook-form';
-import { StringParam, useQueryParam } from 'use-query-params';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { convertToDashboardDocument } from '@hyperdx/common-utils/dist/core/utils';
@@ -190,7 +190,7 @@ type SourceResolutionFormValues = z.infer;
function Mapping({ input }: { input: Input }) {
const router = useRouter();
const { data: sources } = useSources();
- const [dashboardId] = useQueryParam('dashboardId', StringParam);
+ const [dashboardId] = useQueryState('dashboardId');
const { handleSubmit, getFieldState, control, setValue } =
useForm({
diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx
index 1f07d5622d..f1894c019e 100644
--- a/packages/app/src/DBSearchPage.tsx
+++ b/packages/app/src/DBSearchPage.tsx
@@ -801,7 +801,7 @@ const queryStateMap = {
where: parseAsStringWithNewLines,
select: parseAsStringWithNewLines,
whereLanguage: parseAsStringEnum<'sql' | 'lucene'>(['sql', 'lucene']),
- filters: parseAsJson(),
+ filters: parseAsJson(v => v as Filter[]),
orderBy: parseAsStringWithNewLines,
};
diff --git a/packages/app/src/NamespaceDetailsSidePanel.tsx b/packages/app/src/NamespaceDetailsSidePanel.tsx
index ce0967e2d4..44ae0bec6f 100644
--- a/packages/app/src/NamespaceDetailsSidePanel.tsx
+++ b/packages/app/src/NamespaceDetailsSidePanel.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { StringParam, useQueryParam, withDefault } from 'use-query-params';
+import { parseAsString, useQueryState } from 'nuqs';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import { convertDateRangeToGranularityString } from '@hyperdx/common-utils/dist/core/utils';
import { TSource } from '@hyperdx/common-utils/dist/types';
@@ -229,12 +229,9 @@ export default function NamespaceDetailsSidePanel({
metricSource: TSource;
logSource: TSource;
}) {
- const [namespaceName, setNamespaceName] = useQueryParam(
+ const [namespaceName, setNamespaceName] = useQueryState(
'namespaceName',
- withDefault(StringParam, ''),
- {
- updateType: 'replaceIn',
- },
+ parseAsString.withDefault(''),
);
const contextZIndex = useZIndex();
@@ -320,7 +317,7 @@ export default function NamespaceDetailsSidePanel({
]);
const handleClose = React.useCallback(() => {
- setNamespaceName(undefined);
+ setNamespaceName(null);
}, [setNamespaceName]);
if (!namespaceName) {
diff --git a/packages/app/src/NodeDetailsSidePanel.tsx b/packages/app/src/NodeDetailsSidePanel.tsx
index 4dfc2cab29..60b22a0ecc 100644
--- a/packages/app/src/NodeDetailsSidePanel.tsx
+++ b/packages/app/src/NodeDetailsSidePanel.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { StringParam, useQueryParam, withDefault } from 'use-query-params';
+import { parseAsString, useQueryState } from 'nuqs';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import { convertDateRangeToGranularityString } from '@hyperdx/common-utils/dist/core/utils';
import { TSource } from '@hyperdx/common-utils/dist/types';
@@ -242,12 +242,9 @@ export default function NodeDetailsSidePanel({
metricSource: TSource;
logSource: TSource;
}) {
- const [nodeName, setNodeName] = useQueryParam(
+ const [nodeName, setNodeName] = useQueryState(
'nodeName',
- withDefault(StringParam, ''),
- {
- updateType: 'replaceIn',
- },
+ parseAsString.withDefault(''),
);
const contextZIndex = useZIndex();
@@ -333,7 +330,7 @@ export default function NodeDetailsSidePanel({
]);
const handleClose = React.useCallback(() => {
- setNodeName(undefined);
+ setNodeName(null);
}, [setNodeName]);
if (!nodeName) {
diff --git a/packages/app/src/PodDetailsSidePanel.tsx b/packages/app/src/PodDetailsSidePanel.tsx
index bc32115e14..d4ab36718b 100644
--- a/packages/app/src/PodDetailsSidePanel.tsx
+++ b/packages/app/src/PodDetailsSidePanel.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { StringParam, useQueryParam, withDefault } from 'use-query-params';
+import { parseAsString, useQueryState } from 'nuqs';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import { convertDateRangeToGranularityString } from '@hyperdx/common-utils/dist/core/utils';
import { TSource } from '@hyperdx/common-utils/dist/types';
@@ -223,12 +223,9 @@ export default function PodDetailsSidePanel({
logSource: TSource;
metricSource: TSource;
}) {
- const [podName, setPodName] = useQueryParam(
+ const [podName, setPodName] = useQueryState(
'podName',
- withDefault(StringParam, ''),
- {
- updateType: 'replaceIn',
- },
+ parseAsString.withDefault(''),
);
const [rowId, setRowId] = React.useState(null);
@@ -243,8 +240,8 @@ export default function PodDetailsSidePanel({
// If we're in a nested side panel, we need to use a higher z-index
// TODO: This is a hack
- const [nodeName] = useQueryParam('nodeName', StringParam);
- const [namespaceName] = useQueryParam('namespaceName', StringParam);
+ const [nodeName] = useQueryState('nodeName');
+ const [namespaceName] = useQueryState('namespaceName');
const isNested = !!nodeName || !!namespaceName;
const contextZIndex = useZIndex();
const drawerZIndex = contextZIndex + 10 + (isNested ? 100 : 0);
@@ -333,7 +330,7 @@ export default function PodDetailsSidePanel({
// If we're in a nested side panel, don't close the drawer
return;
}
- setPodName(undefined);
+ setPodName(null);
}, [rowId, setPodName]);
if (!podName) {
diff --git a/packages/app/src/SessionsPage.tsx b/packages/app/src/SessionsPage.tsx
index 46a756f947..cd2a5b4020 100644
--- a/packages/app/src/SessionsPage.tsx
+++ b/packages/app/src/SessionsPage.tsx
@@ -1,10 +1,13 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Head from 'next/head';
import { sub } from 'date-fns';
-import { parseAsString, parseAsStringEnum, useQueryStates } from 'nuqs';
+import {
+ parseAsInteger,
+ parseAsString,
+ parseAsStringEnum,
+ useQueryStates,
+} from 'nuqs';
import { useForm, useWatch } from 'react-hook-form';
-import { NumberParam } from 'serialize-query-params';
-import { StringParam, useQueryParams, withDefault } from 'use-query-params';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import {
SearchCondition,
@@ -329,16 +332,9 @@ export default function SessionsPage() {
[],
);
- const [selectedSessionQuery, setSelectedSessionQuery] = useQueryParams(
- {
- sid: withDefault(StringParam, undefined),
- sfrom: withDefault(NumberParam, undefined),
- sto: withDefault(NumberParam, undefined),
- },
- {
- updateType: 'pushIn',
- enableBatching: true,
- },
+ const [selectedSessionQuery, setSelectedSessionQuery] = useQueryStates(
+ { sid: parseAsString, sfrom: parseAsInteger, sto: parseAsInteger },
+ { history: 'push' },
);
const selectedSession = useMemo(() => {
@@ -357,9 +353,9 @@ export default function SessionsPage() {
(session: Session | undefined) => {
if (session == null) {
setSelectedSessionQuery({
- sid: undefined,
- sfrom: undefined,
- sto: undefined,
+ sid: null,
+ sfrom: null,
+ sto: null,
});
} else {
setSelectedSessionQuery({
diff --git a/packages/app/src/__tests__/timeQuery.test.tsx b/packages/app/src/__tests__/timeQuery.test.tsx
index 40cf31c9c3..d2af9183d8 100644
--- a/packages/app/src/__tests__/timeQuery.test.tsx
+++ b/packages/app/src/__tests__/timeQuery.test.tsx
@@ -1,12 +1,8 @@
import * as React from 'react';
import { useImperativeHandle } from 'react';
import { useRouter } from 'next/router';
-import { NextAdapter } from 'next-query-params';
-import { QueryParamProvider } from 'use-query-params';
-import { LocationMock } from '@jedmao/location';
-import { render } from '@testing-library/react';
-
-import { TestRouter } from '@/fixtures';
+import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
+import { act, render } from '@testing-library/react';
import {
getLiveTailTimeRange,
@@ -27,17 +23,22 @@ jest.mock('next/router', () => ({
function TestWrapper({
children,
isUTC,
+ searchParams,
}: {
children: React.ReactNode;
isUTC?: boolean;
+ searchParams?: Record;
}) {
const { setUserPreference } = useUserPreferences();
React.useEffect(() => {
setUserPreference({ isUTC });
}, [setUserPreference, isUTC]);
+
return (
- {children}
+
+ {children}
+
);
}
@@ -52,37 +53,14 @@ const TestComponent = React.forwardRef(function Component(
return null;
});
-const { location: savedLocation } = window;
-
-// TODO: Issues with testing nuqs :(
-// https://github.com/47ng/nuqs/issues/259
-describe.skip('useTimeQuery tests', () => {
- let testRouter: TestRouter;
- let locationMock: LocationMock;
-
- beforeAll(() => {
- // @ts-ignore - This complains because we can only delete optional operands
- delete window.location;
- });
-
+describe('useNewTimeQuery tests', () => {
beforeEach(() => {
jest.resetAllMocks();
- locationMock = new LocationMock('https://www.hyperdx.io/');
- testRouter = new TestRouter(locationMock);
- // @ts-ignore - this is a mock
- window.location = locationMock;
-
- (useRouter as jest.Mock).mockReturnValue(testRouter);
-
+ (useRouter as jest.Mock).mockReturnValue({ isReady: true });
jest.useFakeTimers().setSystemTime(new Date(INITIAL_DATE_STRING));
});
- afterAll(() => {
- // @ts-ignore - this is a mock
- window.location = savedLocation;
- });
-
- it('initializes successfully to a non-UTC time', async () => {
+ it('displays initial time range as a formatted string when no url params', async () => {
const timeQueryRef = React.createRef();
render(
@@ -94,13 +72,16 @@ describe.skip('useTimeQuery tests', () => {
,
);
- // The live tail time range is 15 mins
+ // The live tail time range is 15 mins before the fixed time
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 11:45:00 - Oct 3 12:00:00"`,
);
+ expect(timeQueryRef.current?.searchedTimeRange).toEqual(
+ getLiveTailTimeRange(),
+ );
});
- it('initializes successfully to a UTC time', async () => {
+ it('displays initial time range in UTC when isUTC is set', async () => {
const timeQueryRef = React.createRef();
render(
@@ -112,57 +93,20 @@ describe.skip('useTimeQuery tests', () => {
,
);
- // The live tail time range is 15 mins
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 15:45:00 - Oct 3 16:00:00"`,
);
});
- it('can be overridden by `tq` url param', async () => {
+ it('accepts `from` and `to` url params and updates searchedTimeRange', async () => {
const timeQueryRef = React.createRef();
- testRouter.replace('/search?tq=Last+4H');
- const { rerender } = render(
-
-
- ,
- );
- jest.runAllTimers();
-
- rerender(
-
-
- ,
- );
- jest.runAllTimers();
-
- // Once the hook runs, it will unset the `tq` param and replace it with
- // a `from` and `to`
- expect(locationMock.searchParams.get('tq')).toBeNull();
- // `From` should be 10/03/23 at 8:00am EDT
- expect(locationMock.searchParams.get('from')).toBe('1696334400000');
- // `To` should be 10/03/23 at 12:00pm EDT
- expect(locationMock.searchParams.get('to')).toBe('1696348800000');
- expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
- `"Oct 3 08:00:00 - Oct 3 12:00:00"`,
- );
- });
-
- it('browser navigation of from/to qparmas updates the searched time range', async () => {
- const timeQueryRef = React.createRef();
- testRouter.setIsReady(false);
- testRouter.replace('/search');
-
- const result = render(
-
+ render(
+ // 10/03/23 from 04:00am EDT to 08:00am EDT
+
@@ -170,72 +114,23 @@ describe.skip('useTimeQuery tests', () => {
);
jest.runAllTimers();
- testRouter.setIsReady(true);
-
- result.rerender(
-
-
- ,
- );
-
- expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
- `"Past 1h"`,
- );
- expect(timeQueryRef.current?.searchedTimeRange).toMatchInlineSnapshot(`
- Array [
- 2023-10-03T15:45:00.000Z,
- 2023-10-03T16:00:00.000Z,
- ]
- `);
-
- // 10/03/23 from 04:00am EDT to 08:00am EDT
- testRouter.replace('/search?from=1696320000000&to=1696334400000');
-
- result.rerender(
-
-
- ,
- );
-
- result.rerender(
-
-
- ,
- );
-
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 04:00:00 - Oct 3 08:00:00"`,
);
expect(timeQueryRef.current?.searchedTimeRange).toMatchInlineSnapshot(`
- Array [
+ [
2023-10-03T08:00:00.000Z,
2023-10-03T12:00:00.000Z,
]
`);
});
- it('overrides initial value with async updated `from` and `to` params', async () => {
+ it('falls back to initialTimeRange when url params are invalid', async () => {
const timeQueryRef = React.createRef();
- // 10/03/23 from 04:00am EDT to 08:00am EDT
- testRouter.setIsReady(false);
- testRouter.replace('/search');
- const result = render(
-
+ render(
+
@@ -243,78 +138,60 @@ describe.skip('useTimeQuery tests', () => {
);
jest.runAllTimers();
- testRouter.replace('/search?from=1696320000000&to=1696334400000');
- testRouter.setIsReady(true);
-
- result.rerender(
-
-
- ,
- );
-
+ // nuqs returns null for invalid integers, so the hook falls back to initialTimeRange
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
- `"Oct 3 04:00:00 - Oct 3 08:00:00"`,
+ `"Oct 3 11:45:00 - Oct 3 12:00:00"`,
+ );
+ expect(timeQueryRef.current?.searchedTimeRange).toEqual(
+ getLiveTailTimeRange(),
);
- expect(timeQueryRef.current?.searchedTimeRange).toMatchInlineSnapshot(`
- Array [
- 2023-10-03T08:00:00.000Z,
- 2023-10-03T12:00:00.000Z,
- ]
- `);
});
- it('accepts `from` and `to` url params', async () => {
+ it('does not update displayedTimeInputValue until router is ready', async () => {
const timeQueryRef = React.createRef();
- // 10/03/23 from 04:00am EDT to 08:00am EDT
- testRouter.replace('/search?from=1696320000000&to=1696334400000');
+ (useRouter as jest.Mock).mockReturnValue({ isReady: false });
- render(
-
+ // Start with no url params and router not ready
+ const { rerender } = render(
+
,
);
jest.runAllTimers();
- expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
- `"Oct 3 04:00:00 - Oct 3 08:00:00"`,
- );
- });
-
- it('handles bad input in `from` and `to` url params', async () => {
- const timeQueryRef = React.createRef();
- testRouter.replace('/search?from=abc&to=def');
+ // While not ready, displayedTimeInputValue stays at initialDisplayValue
+ expect(timeQueryRef.current?.displayedTimeInputValue).toBe('Past 1h');
- render(
-
+ // Make router ready — useEffect fires and picks up empty URL params
+ (useRouter as jest.Mock).mockReturnValue({ isReady: true });
+ rerender(
+
,
);
jest.runAllTimers();
- // Should initialize to the initial time range 11:45am - 12:00pm
- expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
- `"Oct 3 11:45:00 - Oct 3 12:00:00"`,
+ // With showRelativeInterval + no params, keeps initialDisplayValue
+ expect(timeQueryRef.current?.displayedTimeInputValue).toBe('Past 1h');
+ expect(timeQueryRef.current?.searchedTimeRange).toEqual(
+ getLiveTailTimeRange(),
);
});
- it('prefers `tq` param over `from` and `to` params', async () => {
+ it('updates searchedTimeRange when onTimeRangeSelect is called', async () => {
const timeQueryRef = React.createRef();
- // 10/03/23 from 04:00am EDT to 08:00am EDT, tq says last 1 hour
- testRouter.replace(
- '/search?from=1696320000000&to=1696334400000&tq=Past+1h',
- );
- const result = render(
+ render(
{
/>
,
);
- jest.runAllTimers();
- result.rerender(
-
-
- ,
- );
- jest.runAllTimers();
+ // Simulate user selecting a time range
+ act(() => {
+ timeQueryRef.current?.onTimeRangeSelect(
+ // 10/03/23 from 04:00am EDT to 08:00am EDT
+ new Date(1696320000000),
+ new Date(1696334400000),
+ );
+ });
- // The time range should be the last 1 hour even though the `from` and `to`
- // params are passed in.
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
- `"Oct 3 11:00:00 - Oct 3 12:00:00"`,
+ `"Oct 3 04:00:00 - Oct 3 08:00:00"`,
);
+ expect(timeQueryRef.current?.searchedTimeRange).toMatchInlineSnapshot(`
+ [
+ 2023-10-03T08:00:00.000Z,
+ 2023-10-03T12:00:00.000Z,
+ ]
+ `);
});
- it('enables custom display value', async () => {
+ it('preserves initialDisplayValue when showRelativeInterval is set', async () => {
const timeQueryRef = React.createRef();
- testRouter.replace('/search');
- const initialDisplayValue = 'Live Tail';
+ const initialDisplayValue = 'Past 1h';
render(
,
diff --git a/packages/app/src/components/AppNav/AppNav.tsx b/packages/app/src/components/AppNav/AppNav.tsx
index 279abb2452..368202a39e 100644
--- a/packages/app/src/components/AppNav/AppNav.tsx
+++ b/packages/app/src/components/AppNav/AppNav.tsx
@@ -4,12 +4,11 @@ import Router, { useRouter } from 'next/router';
import cx from 'classnames';
import Fuse from 'fuse.js';
import {
- NumberParam,
- StringParam,
- useQueryParam,
- useQueryParams,
- withDefault,
-} from 'use-query-params';
+ parseAsInteger,
+ parseAsString,
+ useQueryState,
+ useQueryStates,
+} from 'nuqs';
import HyperDX from '@hyperdx/browser';
import { AlertState } from '@hyperdx/common-utils/dist/types';
import {
@@ -431,14 +430,14 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
const router = useRouter();
const { pathname, query } = router;
- const [timeRangeQuery] = useQueryParams({
- from: withDefault(NumberParam, -1),
- to: withDefault(NumberParam, -1),
- });
- const [inputTimeQuery] = useQueryParam('tq', withDefault(StringParam, ''), {
- updateType: 'pushIn',
- enableBatching: true,
+ const [timeRangeQuery] = useQueryStates({
+ from: parseAsInteger.withDefault(-1),
+ to: parseAsInteger.withDefault(-1),
});
+ const [inputTimeQuery] = useQueryState(
+ 'tq',
+ parseAsString.withDefault('').withOptions({ history: 'push' }),
+ );
const { data: meData } = api.useMe();
diff --git a/packages/app/src/components/DBTracePanel.tsx b/packages/app/src/components/DBTracePanel.tsx
index 88b5d1bd22..d1d7503f02 100644
--- a/packages/app/src/components/DBTracePanel.tsx
+++ b/packages/app/src/components/DBTracePanel.tsx
@@ -96,7 +96,9 @@ export default function DBTracePanel({
const [eventRowWhere, setEventRowWhere] = useQueryState(
'eventRowWhere',
- parseAsJson<{ id: string; type: string; aliasWith: WithClause[] }>(),
+ parseAsJson<{ id: string; type: string; aliasWith: WithClause[] }>(
+ v => v as { id: string; type: string; aliasWith: WithClause[] },
+ ),
);
const {
diff --git a/packages/app/src/dashboard.ts b/packages/app/src/dashboard.ts
index 33070157dd..82af5f018b 100644
--- a/packages/app/src/dashboard.ts
+++ b/packages/app/src/dashboard.ts
@@ -96,7 +96,7 @@ export function useDashboard({
const [localDashboard, setLocalDashboard] = useQueryState(
'dashboard',
- parseAsJson(),
+ parseAsJson(v => v as Dashboard),
);
const updateDashboard = useUpdateDashboard();
diff --git a/packages/app/src/fixtures.ts b/packages/app/src/fixtures.ts
deleted file mode 100644
index 570f2cc559..0000000000
--- a/packages/app/src/fixtures.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Router } from 'next/router';
-import type { UrlObject } from 'url';
-import { LocationMock } from '@jedmao/location';
-
-type PartialRouter = Partial;
-
-export const BASE_URL = 'https://www.hyperdx.io';
-
-/**
- * A Router to be used for testing which provides the bare minimum needed
- * for the useQueryParam(s) hook and NextAdapter to work.
- */
-export class TestRouter implements PartialRouter {
- isReady = true;
- pathname = '/';
- private currentUrl = '';
- private history: string[] = [];
-
- constructor(private locationMock: LocationMock) {}
-
- replace = (url: string | UrlObject) => {
- this.locationMock.assign(`${BASE_URL}${url}`);
- this.currentUrl = TestRouter.getURLString(url);
- this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
- return Promise.resolve(true);
- };
-
- push = (url: string | UrlObject) => {
- this.history.push(this.currentUrl);
- this.currentUrl = TestRouter.getURLString(url);
- this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
- return Promise.resolve(true);
- };
-
- setIsReady = (isReady: boolean) => {
- this.isReady = isReady;
- };
-
- get asPath() {
- return this.pathname;
- }
-
- static getURLString(url: string | UrlObject): string {
- if (typeof url === 'string') {
- return url;
- }
- return `${url.pathname}${url.search}`;
- }
-
- getParams(): URLSearchParams {
- return new URL(`${BASE_URL}${this.currentUrl}`).searchParams;
- }
-}
diff --git a/packages/app/src/hooks/useDashboardFilters.tsx b/packages/app/src/hooks/useDashboardFilters.tsx
index b14212d3bf..f524cd75ea 100644
--- a/packages/app/src/hooks/useDashboardFilters.tsx
+++ b/packages/app/src/hooks/useDashboardFilters.tsx
@@ -7,7 +7,7 @@ import { FilterState, filtersToQuery, parseQuery } from '@/searchFilters';
const useDashboardFilters = (filters: DashboardFilter[]) => {
const [filterQueries, setFilterQueries] = useQueryState(
'filters',
- parseAsJson(),
+ parseAsJson(v => v as Filter[]),
);
const setFilterValue = useCallback(
diff --git a/packages/app/src/timeQuery.ts b/packages/app/src/timeQuery.ts
index 747df6b7e8..426ee7cc55 100644
--- a/packages/app/src/timeQuery.ts
+++ b/packages/app/src/timeQuery.ts
@@ -16,14 +16,13 @@ import {
sub,
subMilliseconds,
} from 'date-fns';
-import { parseAsFloat, useQueryStates } from 'nuqs';
import {
- NumberParam,
- StringParam,
- useQueryParam,
- useQueryParams,
- withDefault,
-} from 'use-query-params';
+ parseAsFloat,
+ parseAsInteger,
+ parseAsString,
+ useQueryState,
+ useQueryStates,
+} from 'nuqs';
import { formatDate } from '@hyperdx/common-utils/dist/core/utils';
import { DateRange } from '@hyperdx/common-utils/dist/types';
@@ -95,15 +94,9 @@ export function useTimeQuery({
undefined | string
>(undefined);
- const [_timeRangeQuery, setTimeRangeQuery] = useQueryParams(
- {
- from: withDefault(NumberParam, undefined),
- to: withDefault(NumberParam, undefined),
- },
- {
- updateType: 'pushIn',
- enableBatching: true,
- },
+ const [_timeRangeQuery, setTimeRangeQuery] = useQueryStates(
+ { from: parseAsInteger, to: parseAsInteger },
+ { history: 'push' },
);
const timeRangeQuery = useMemo(
@@ -115,13 +108,9 @@ export function useTimeQuery({
);
// Allow browser back/fwd button to modify the displayed time input value
- const [inputTimeQuery, setInputTimeQuery] = useQueryParam(
+ const [inputTimeQuery, setInputTimeQuery] = useQueryState(
'tq',
- withDefault(StringParam, ''),
- {
- updateType: 'pushIn',
- enableBatching: true,
- },
+ parseAsString.withDefault('').withOptions({ history: 'push' }),
);
const prevInputTimeQuery = usePrevious(inputTimeQuery);
diff --git a/packages/app/src/useQueryParam.tsx b/packages/app/src/useQueryParam.tsx
deleted file mode 100644
index 15b6398afb..0000000000
--- a/packages/app/src/useQueryParam.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useState,
-} from 'react';
-import { useRouter } from 'next/router';
-
-import { usePrevious } from './utils';
-
-type QueryParamContextType = Record & {
- setState: (state: any) => void;
-};
-
-const QueryParamContext = createContext({
- setState: _ => {},
-});
-
-export const QueryParamProvider = ({
- children,
-}: {
- children: React.ReactNode;
-}) => {
- const router = useRouter();
-
- const prevRouterQuery = usePrevious(router.query);
-
- const setState = useCallback(
- (state: Record) => {
- // eslint-disable-next-line react-hooks/immutability
- setCache(oldCache => {
- const newCache = {
- ...oldCache,
- ...state,
- };
- const { setState: _, ...newQuery } = newCache;
-
- router.push({
- query: newQuery,
- });
-
- return newCache;
- });
- },
- [router],
- );
-
- const initState: QueryParamContextType = {
- setState,
- };
-
- const [cache, setCache] = useState(initState);
-
- // Update cache if query param changes
- useEffect(() => {
- if (router.isReady && prevRouterQuery != router.query) {
- setCache(oldCache => ({ ...oldCache, ...router.query }));
- }
- }, [setState, router.isReady, router.query, cache, prevRouterQuery]);
-
- return (
-
- {children}
-
- );
-};
-
-export function useQueryParam(
- key: string,
- defaultValue: T,
- options: {
- queryParamConfig: {
- encode: (
- value: T | undefined,
- ) => string | (string | null)[] | null | undefined;
- decode: (
- input: string | (string | null)[] | null | undefined,
- ) => T | undefined;
- };
- } = {
- queryParamConfig: {
- encode: (value: T | undefined) => JSON.stringify(value),
- decode: (input: string | (string | null)[] | null | undefined) =>
- Array.isArray(input)
- ? input.map(i => (i != null ? JSON.parse(i) : undefined))
- : input != null
- ? JSON.parse(input)
- : undefined,
- },
- },
-): [T, (value: T) => void] {
- const qParamContext = useContext(QueryParamContext);
-
- const setValue = (value: T) => {
- qParamContext.setState({ [key]: options.queryParamConfig.encode(value) });
- };
-
- const value =
- options.queryParamConfig.decode(qParamContext[key]) ?? defaultValue;
-
- return [value, setValue];
-}
diff --git a/yarn.lock b/yarn.lock
index 262a534937..523d6e174e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4311,7 +4311,6 @@ __metadata:
"@hyperdx/browser": "npm:^0.21.1"
"@hyperdx/common-utils": "npm:^0.13.0"
"@hyperdx/node-opentelemetry": "npm:^0.9.0"
- "@jedmao/location": "npm:^3.0.0"
"@lezer/highlight": "npm:^1.2.0"
"@mantine/core": "npm:^7.17.8"
"@mantine/dates": "npm:^7.17.8"
@@ -4385,11 +4384,10 @@ __metadata:
msw: "npm:^2.3.0"
msw-storybook-addon: "npm:^2.0.2"
next: "npm:^16.1.5"
- next-query-params: "npm:^4.3.1"
next-runtime-env: "npm:1"
next-seo: "npm:^4.28.1"
numbro: "npm:^2.4.0"
- nuqs: "npm:1.17.0"
+ nuqs: "npm:^2.8.8"
object-hash: "npm:^3.0.0"
postcss: "npm:^8.4.38"
postcss-preset-mantine: "npm:^1.15.0"
@@ -4411,7 +4409,6 @@ __metadata:
recharts: "npm:^2.12.7"
rrweb: "npm:2.0.0-alpha.8"
sass: "npm:^1.54.8"
- serialize-query-params: "npm:^2.0.2"
sql-formatter: "npm:^15.4.0"
sqlstring: "npm:^2.3.3"
store2: "npm:^2.14.3"
@@ -4425,7 +4422,6 @@ __metadata:
typescript: "npm:^5.9.3"
uplot: "npm:^1.6.31"
uplot-react: "npm:^1.2.2"
- use-query-params: "npm:^2.1.2"
zod: "npm:3.25"
languageName: unknown
linkType: soft
@@ -4935,13 +4931,6 @@ __metadata:
languageName: node
linkType: hard
-"@jedmao/location@npm:^3.0.0":
- version: 3.0.0
- resolution: "@jedmao/location@npm:3.0.0"
- checksum: 10c0/dded42a7991aaf625c9696519aec188834b205b066e1696cd42280b70dff080d85f79353c3c21dd9ec8e9c122f193080c5a552d0916368dc13e1b03b11507184
- languageName: node
- linkType: hard
-
"@jest/console@npm:30.2.0":
version: 30.2.0
resolution: "@jest/console@npm:30.2.0"
@@ -8058,7 +8047,7 @@ __metadata:
languageName: node
linkType: hard
-"@standard-schema/spec@npm:^1.0.0":
+"@standard-schema/spec@npm:1.0.0, @standard-schema/spec@npm:^1.0.0":
version: 1.0.0
resolution: "@standard-schema/spec@npm:1.0.0"
checksum: 10c0/a1ab9a8bdc09b5b47aa8365d0e0ec40cc2df6437be02853696a0e377321653b0d3ac6f079a8c67d5ddbe9821025584b1fb71d9cc041a6666a96f1fadf2ece15f
@@ -20044,7 +20033,7 @@ __metadata:
languageName: node
linkType: hard
-"mitt@npm:^3.0.0, mitt@npm:^3.0.1":
+"mitt@npm:^3.0.0":
version: 3.0.1
resolution: "mitt@npm:3.0.1"
checksum: 10c0/3ab4fdecf3be8c5255536faa07064d05caa3dd332bd318ff02e04621f7b3069ca1de9106cfe8e7ced675abfc2bec2ce4c4ef321c4a1bb1fb29df8ae090741913
@@ -20340,19 +20329,6 @@ __metadata:
languageName: node
linkType: hard
-"next-query-params@npm:^4.3.1":
- version: 4.3.1
- resolution: "next-query-params@npm:4.3.1"
- dependencies:
- tslib: "npm:^2.0.3"
- peerDependencies:
- next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- use-query-params: ^2.0.0
- checksum: 10c0/d5752b77b7d3606f36703ac664cf95a75fe0bf1c297c1f06083fe61883a6c037d633558208d4f3e03744b31a079a712ced025da7bdb6f90216eb0ec17b7509a3
- languageName: node
- linkType: hard
-
"next-runtime-env@npm:1":
version: 1.7.4
resolution: "next-runtime-env@npm:1.7.4"
@@ -20665,14 +20641,30 @@ __metadata:
languageName: node
linkType: hard
-"nuqs@npm:1.17.0":
- version: 1.17.0
- resolution: "nuqs@npm:1.17.0"
+"nuqs@npm:^2.8.8":
+ version: 2.8.8
+ resolution: "nuqs@npm:2.8.8"
dependencies:
- mitt: "npm:^3.0.1"
+ "@standard-schema/spec": "npm:1.0.0"
peerDependencies:
- next: ">=13.4 <14.0.2 || ^14.0.3"
- checksum: 10c0/98e26518fdeda9518defe8abb34fd722f9fbc6d62dc7713dbc7bf365b5afc4c7c934088d7c7ed528621c222017841411241f0cfe97c615a3e7fb4d80354e8349
+ "@remix-run/react": ">=2"
+ "@tanstack/react-router": ^1
+ next: ">=14.2.0"
+ react: ">=18.2.0 || ^19.0.0-0"
+ react-router: ^5 || ^6 || ^7
+ react-router-dom: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ "@remix-run/react":
+ optional: true
+ "@tanstack/react-router":
+ optional: true
+ next:
+ optional: true
+ react-router:
+ optional: true
+ react-router-dom:
+ optional: true
+ checksum: 10c0/d3a5503268d5a297c5b5acbd0c6f362f7462f67fea31f88264748ccab7788343bd428b49b7e63f5c5be08afcb22a2ee569fd36b6d0b8f95b8cfca7013ea700e3
languageName: node
linkType: hard
@@ -23826,13 +23818,6 @@ __metadata:
languageName: node
linkType: hard
-"serialize-query-params@npm:^2.0.2":
- version: 2.0.2
- resolution: "serialize-query-params@npm:2.0.2"
- checksum: 10c0/fdef1e8eb45ce585b12535ab7e546a2583220a2deb969359ccef50519dc541dd6690c00c8de7a2465b9b0b0072a3308b55df77bd568284989df1e72869c9ee3f
- languageName: node
- linkType: hard
-
"serve-static@npm:^1.16.0":
version: 1.16.3
resolution: "serve-static@npm:1.16.3"
@@ -26455,18 +26440,6 @@ __metadata:
languageName: node
linkType: hard
-"use-query-params@npm:^2.1.2":
- version: 2.2.0
- resolution: "use-query-params@npm:2.2.0"
- dependencies:
- serialize-query-params: "npm:^2.0.2"
- peerDependencies:
- react: ">=16.8.0"
- react-dom: ">=16.8.0"
- checksum: 10c0/5f92511b6643e91609d1a0f043be93f0d989260a2476214c9b98b5885ff03cebe4240bbe75527a8a73c05653dae2c8a5769f72ba5ae0eacc2b0001d4732cc6b3
- languageName: node
- linkType: hard
-
"use-sidecar@npm:^1.1.3":
version: 1.1.3
resolution: "use-sidecar@npm:1.1.3"