feat: Track clients & operations filters using search params (#5616) (#5617)

Co-authored-by: David Guilherme <davidlguilherme@gmail.com>
This commit is contained in:
Kamil Kisiela 2024-09-24 01:24:06 -07:00 committed by GitHub
parent 2f053088f8
commit 498f621e7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 95 additions and 24 deletions

View file

@ -29,7 +29,7 @@ export function OperationsFallback({
<AlertCircleIcon className="size-4" />
<AlertTitle>No stats available yet.</AlertTitle>
<AlertDescription>
There is no information available for the selected date range.
There is no information available for the selected filters.
</AlertDescription>
</Alert>
</div>

View file

@ -54,7 +54,7 @@ function OperationsFilter({
}
const [selectedItems, setSelectedItems] = useState<string[]>(() =>
selected?.length ? selected : getOperationHashes(),
getOperationHashes().filter(hash => selected?.includes(hash) ?? true),
);
const onSelect = useCallback(
@ -401,7 +401,7 @@ function ClientsFilter({
}
const [selectedItems, setSelectedItems] = useState<string[]>(() =>
selected?.length ? selected : getClientNames(),
getClientNames().filter(name => selected?.includes(name) ?? true),
);
const onSelect = useCallback(

View file

@ -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}
</Link>

View file

@ -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']);
},
});
}

View file

@ -85,6 +85,7 @@ export function useDateRangeController(args: {
setSelectedPreset(preset: Preset) {
void router.navigate({
search: {
...searchParams,
from: preset.range.from,
to: preset.range.to,
},

View file

@ -0,0 +1,35 @@
import { useRouter } from '@tanstack/react-router';
type SearchParamsFilter = string | string[];
export function useSearchParamsFilter<TValue extends SearchParamsFilter>(
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(',');
}

View file

@ -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 });
});
});
});

View file

@ -0,0 +1,11 @@
export const pick = <TValue extends Record<string, any>>(value: TValue, keys: string[]) => {
return keys.reduce(
(acc, key) => {
if (key in value) {
acc[key] = value[key];
}
return acc;
},
{} as Record<string, any>,
);
};

View file

@ -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}
</Link>

View file

@ -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<string[]>([]);
const [selectedClients, setSelectedClients] = useSearchParamsFilter<string[]>('clients', []);
const operationsList = useMemo(() => [operationHash], [operationHash]);
const [result] = useQuery({

View file

@ -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<string[]>([]);
const [selectedClients, setSelectedClients] = useState<string[]>([]);
const [selectedOperations, setSelectedOperations] = useSearchParamsFilter<string[]>(
'operations',
[],
);
const [selectedClients, setSelectedClients] = useSearchParamsFilter<string[]>('clients', []);
const dateRangeController = useDateRangeController({
dataRetentionInDays,
defaultPreset: presetLast7Days,