mirror of
https://github.com/graphql-hive/console
synced 2026-05-23 17:18:23 +00:00
Co-authored-by: David Guilherme <davidlguilherme@gmail.com>
This commit is contained in:
parent
2f053088f8
commit
498f621e7d
11 changed files with 95 additions and 24 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export function useDateRangeController(args: {
|
|||
setSelectedPreset(preset: Preset) {
|
||||
void router.navigate({
|
||||
search: {
|
||||
...searchParams,
|
||||
from: preset.range.from,
|
||||
to: preset.range.to,
|
||||
},
|
||||
|
|
|
|||
35
packages/web/app/src/lib/hooks/use-search-params-filters.ts
Normal file
35
packages/web/app/src/lib/hooks/use-search-params-filters.ts
Normal 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(',');
|
||||
}
|
||||
27
packages/web/app/src/lib/object.spec.ts
Normal file
27
packages/web/app/src/lib/object.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
11
packages/web/app/src/lib/object.ts
Normal file
11
packages/web/app/src/lib/object.ts
Normal 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>,
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue