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"