diff --git a/packages/web/app/src/components/target/insights/Fallback.tsx b/packages/web/app/src/components/target/insights/Fallback.tsx
index 51b8dce8b..07e99073f 100644
--- a/packages/web/app/src/components/target/insights/Fallback.tsx
+++ b/packages/web/app/src/components/target/insights/Fallback.tsx
@@ -29,7 +29,7 @@ export function OperationsFallback({
No stats available yet.
- There is no information available for the selected date range.
+ There is no information available for the selected filters.
diff --git a/packages/web/app/src/components/target/insights/Filters.tsx b/packages/web/app/src/components/target/insights/Filters.tsx
index 9186487b7..afa3ec7ad 100644
--- a/packages/web/app/src/components/target/insights/Filters.tsx
+++ b/packages/web/app/src/components/target/insights/Filters.tsx
@@ -54,7 +54,7 @@ function OperationsFilter({
}
const [selectedItems, setSelectedItems] = useState(() =>
- selected?.length ? selected : getOperationHashes(),
+ getOperationHashes().filter(hash => selected?.includes(hash) ?? true),
);
const onSelect = useCallback(
@@ -401,7 +401,7 @@ function ClientsFilter({
}
const [selectedItems, setSelectedItems] = useState(() =>
- selected?.length ? selected : getClientNames(),
+ getClientNames().filter(name => selected?.includes(name) ?? true),
);
const onSelect = useCallback(
diff --git a/packages/web/app/src/components/target/insights/List.tsx b/packages/web/app/src/components/target/insights/List.tsx
index 04a2a2c22..58f75bb5f 100644
--- a/packages/web/app/src/components/target/insights/List.tsx
+++ b/packages/web/app/src/components/target/insights/List.tsx
@@ -10,6 +10,7 @@ import { env } from '@/env/frontend';
import { FragmentType, graphql, useFragment } from '@/gql';
import { DateRangeInput } from '@/gql/graphql';
import { useDecimal, useFormattedDuration, useFormattedNumber } from '@/lib/hooks';
+import { pick } from '@/lib/object';
import { ChevronUpIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { Link } from '@tanstack/react-router';
import {
@@ -74,10 +75,11 @@ function OperationRow({
operationName: operation.name,
operationHash: operation.hash,
}}
- search={{
+ search={searchParams => ({
+ ...pick(searchParams, ['clients']),
from: selectedPeriod?.from ? encodeURIComponent(selectedPeriod.from) : undefined,
to: selectedPeriod?.to ? encodeURIComponent(selectedPeriod.to) : undefined,
- }}
+ })}
>
{operation.name}
diff --git a/packages/web/app/src/components/target/insights/Stats.tsx b/packages/web/app/src/components/target/insights/Stats.tsx
index d01fa53bd..b3335affa 100644
--- a/packages/web/app/src/components/target/insights/Stats.tsx
+++ b/packages/web/app/src/components/target/insights/Stats.tsx
@@ -27,6 +27,7 @@ import {
useFormattedNumber,
useFormattedThroughput,
} from '@/lib/hooks';
+import { pick } from '@/lib/object';
import { useChartStyles } from '@/utils';
import { useRouter } from '@tanstack/react-router';
import { OperationsFallback } from './Fallback';
@@ -567,12 +568,7 @@ function ClientsStats(props: {
name: ev.value,
},
search(searchParams) {
- if ('from' in searchParams && 'to' in searchParams) {
- return {
- from: searchParams.from,
- to: searchParams.to,
- };
- }
+ return pick(searchParams, ['from', 'to']);
},
});
}
diff --git a/packages/web/app/src/lib/hooks/use-date-range-controller.ts b/packages/web/app/src/lib/hooks/use-date-range-controller.ts
index 445d2f77b..16702fb17 100644
--- a/packages/web/app/src/lib/hooks/use-date-range-controller.ts
+++ b/packages/web/app/src/lib/hooks/use-date-range-controller.ts
@@ -85,6 +85,7 @@ export function useDateRangeController(args: {
setSelectedPreset(preset: Preset) {
void router.navigate({
search: {
+ ...searchParams,
from: preset.range.from,
to: preset.range.to,
},
diff --git a/packages/web/app/src/lib/hooks/use-search-params-filters.ts b/packages/web/app/src/lib/hooks/use-search-params-filters.ts
new file mode 100644
index 000000000..671a09925
--- /dev/null
+++ b/packages/web/app/src/lib/hooks/use-search-params-filters.ts
@@ -0,0 +1,35 @@
+import { useRouter } from '@tanstack/react-router';
+
+type SearchParamsFilter = string | string[];
+
+export function useSearchParamsFilter(
+ name: string,
+ defaultState: TValue,
+): [TValue, (value: TValue) => void] {
+ const router = useRouter();
+ const searchParams = router.latestLocation.search as any;
+
+ const rawSearchValue =
+ ((name as string) in searchParams && (searchParams[name] as string)) || null;
+ const searchValue = (deserializeSearchValue(rawSearchValue) ?? defaultState) as TValue;
+
+ const setSearchValue = (value: TValue) => {
+ void router.navigate({
+ search: {
+ ...searchParams,
+ [name]: serializeSearchValue(value),
+ },
+ replace: true,
+ });
+ };
+
+ return [searchValue, setSearchValue];
+}
+
+function serializeSearchValue(value: string | string[]) {
+ return Array.isArray(value) ? value.join(',') : value;
+}
+
+function deserializeSearchValue(value: string | null) {
+ return value?.split(',');
+}
diff --git a/packages/web/app/src/lib/object.spec.ts b/packages/web/app/src/lib/object.spec.ts
new file mode 100644
index 000000000..d96643e41
--- /dev/null
+++ b/packages/web/app/src/lib/object.spec.ts
@@ -0,0 +1,27 @@
+import { pick } from './object';
+
+describe('object', () => {
+ describe('#pick', () => {
+ it('returns a new object with only picked keys', () => {
+ const input = { a: 1, b: 2, c: '1234' };
+ const result = pick(input, ['a', 'c']);
+ expect(result).toEqual({ a: 1, c: '1234' });
+ expect(result).not.toBe(input);
+ });
+
+ it('returns an empty object if no key is present on input', () => {
+ const input = { a: 1, b: 2, c: '1234' };
+ expect(pick(input, ['d'])).toEqual({});
+ });
+
+ it('returns an empty object if no key is passed', () => {
+ const input = { a: 1, b: 2, c: '1234' };
+ expect(pick(input, [])).toEqual({});
+ });
+
+ it('returns an object with only presented keys', () => {
+ const input = { a: 1, b: 2, c: '1234' };
+ expect(pick(input, ['a', 'd'])).toEqual({ a: 1 });
+ });
+ });
+});
diff --git a/packages/web/app/src/lib/object.ts b/packages/web/app/src/lib/object.ts
new file mode 100644
index 000000000..573d1b5e3
--- /dev/null
+++ b/packages/web/app/src/lib/object.ts
@@ -0,0 +1,11 @@
+export const pick = >(value: TValue, keys: string[]) => {
+ return keys.reduce(
+ (acc, key) => {
+ if (key in value) {
+ acc[key] = value[key];
+ }
+ return acc;
+ },
+ {} as Record,
+ );
+};
diff --git a/packages/web/app/src/pages/target-insights-client.tsx b/packages/web/app/src/pages/target-insights-client.tsx
index acc37a4a0..1634da295 100644
--- a/packages/web/app/src/pages/target-insights-client.tsx
+++ b/packages/web/app/src/pages/target-insights-client.tsx
@@ -16,6 +16,7 @@ import { CHART_PRIMARY_COLOR } from '@/constants';
import { graphql } from '@/gql';
import { formatNumber, formatThroughput, toDecimal } from '@/lib/hooks';
import { useDateRangeController } from '@/lib/hooks/use-date-range-controller';
+import { pick } from '@/lib/object';
import { useChartStyles } from '@/utils';
import { Link } from '@tanstack/react-router';
@@ -280,14 +281,7 @@ function ClientView(props: {
operationName: operation.name,
operationHash: operation.operationHash ?? '_',
}}
- search={searchParams => {
- if ('from' in searchParams && 'to' in searchParams) {
- return {
- from: searchParams.from,
- to: searchParams.to,
- };
- }
- }}
+ search={searchParams => pick(searchParams, ['from', 'to'])}
>
{operation.name}
diff --git a/packages/web/app/src/pages/target-insights-operation.tsx b/packages/web/app/src/pages/target-insights-operation.tsx
index b489675ed..8f96e23b5 100644
--- a/packages/web/app/src/pages/target-insights-operation.tsx
+++ b/packages/web/app/src/pages/target-insights-operation.tsx
@@ -1,4 +1,4 @@
-import { ReactElement, useMemo, useState } from 'react';
+import { ReactElement, useMemo } from 'react';
import { AlertCircleIcon, RefreshCw } from 'lucide-react';
import { useQuery } from 'urql';
import { Section } from '@/components/common';
@@ -16,6 +16,7 @@ import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
import { FragmentType, graphql, useFragment } from '@/gql';
import { useDateRangeController } from '@/lib/hooks/use-date-range-controller';
+import { useSearchParamsFilter } from '@/lib/hooks/use-search-params-filters';
const GraphQLOperationBody_OperationFragment = graphql(`
fragment GraphQLOperationBody_OperationFragment on Operation {
@@ -69,7 +70,7 @@ function OperationView({
dataRetentionInDays,
defaultPreset: presetLast1Day,
});
- const [selectedClients, setSelectedClients] = useState([]);
+ const [selectedClients, setSelectedClients] = useSearchParamsFilter('clients', []);
const operationsList = useMemo(() => [operationHash], [operationHash]);
const [result] = useQuery({
diff --git a/packages/web/app/src/pages/target-insights.tsx b/packages/web/app/src/pages/target-insights.tsx
index 4358ee7b0..d328b20d4 100644
--- a/packages/web/app/src/pages/target-insights.tsx
+++ b/packages/web/app/src/pages/target-insights.tsx
@@ -1,4 +1,4 @@
-import { ReactElement, useState } from 'react';
+import { ReactElement } from 'react';
import { RefreshCw } from 'lucide-react';
import { useQuery } from 'urql';
import { Page, TargetLayout } from '@/components/layouts/target';
@@ -16,6 +16,7 @@ import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
import { graphql } from '@/gql';
import { useDateRangeController } from '@/lib/hooks/use-date-range-controller';
+import { useSearchParamsFilter } from '@/lib/hooks/use-search-params-filters';
function OperationsView({
organizationCleanId,
@@ -28,8 +29,11 @@ function OperationsView({
targetCleanId: string;
dataRetentionInDays: number;
}): ReactElement {
- const [selectedOperations, setSelectedOperations] = useState([]);
- const [selectedClients, setSelectedClients] = useState([]);
+ const [selectedOperations, setSelectedOperations] = useSearchParamsFilter(
+ 'operations',
+ [],
+ );
+ const [selectedClients, setSelectedClients] = useSearchParamsFilter('clients', []);
const dateRangeController = useDateRangeController({
dataRetentionInDays,
defaultPreset: presetLast7Days,