Skip to content

Use sveltekit-search-params to simplify query string manipulation #168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"svelte-check": "^4.1.1",
"svelte-intersection-observer": "^1.0.0",
"svelte-radix": "^1.1.1",
"sveltekit-search-params": "^4.0.0-next.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.1",
Expand Down
29 changes: 13 additions & 16 deletions frontend/src/lib/components/ActionButtons/ActionButtons.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import { queryParameters } from 'sveltekit-search-params';

import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils';

Expand All @@ -7,14 +9,7 @@
import IconSquare3Stack3d from '~icons/heroicons/square-3-stack-3d-16-solid';
import IconXMark from '~icons/heroicons/x-mark-16-solid';

import { goto } from '$app/navigation';
import { page } from '$app/stores';
import {
getSortDirection,
updateContextSort,
type SortDirection,
type SortContext
} from '$lib/util/sort';
import { getSortParamKey, type SortContext, getSortParamConfig } from '$lib/util/sort';

let {
showCluster = false,
Expand All @@ -30,14 +25,16 @@

let activeCluster = showCluster ? 'GroupBys' : null;

let currentSort: SortDirection = $derived.by(() =>
getSortDirection($page.url.searchParams, context)
const sortKey = $derived(getSortParamKey(context));
const params = $derived(
queryParameters(
{ [sortKey]: getSortParamConfig() },
{ pushHistory: false, showDefaults: false }
)
);

function handleSort() {
const newSort: SortDirection = currentSort === 'asc' ? 'desc' : 'asc';
const url = updateContextSort($page.url, context, newSort);
goto(url, { replaceState: true });
function toggleSort() {
params[sortKey] = params[sortKey] === 'asc' ? 'desc' : 'asc';
}
</script>

Expand All @@ -61,9 +58,9 @@
<!-- Action Buttons Section -->
<div class="flex gap-3">
{#if showSort}
<Button variant="secondary" size="sm" icon="leading" on:click={handleSort}>
<Button variant="secondary" size="sm" icon="leading" on:click={toggleSort}>
<IconArrowsUpDown />
Sort {currentSort === 'asc' ? 'A-Z' : 'Z-A'}
Sort {params[sortKey] === 'asc' ? 'A-Z' : 'Z-A'}
</Button>
{/if}
<Button variant="secondary" size="sm" icon="leading" disabled>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,62 @@
<script lang="ts">
import { untrack } from 'svelte';
import { queryParameters, ssp } from 'sveltekit-search-params';
import type { DateRange } from 'bits-ui';
import { DateFormatter, getLocalTimeZone, fromAbsolute } from '@internationalized/date';

import {
DATE_RANGE_PARAM,
DATE_RANGES,
CUSTOM,
DATE_RANGE_START_PARAM,
DATE_RANGE_END_PARAM,
type DateRangeOption,
getDateRangeByValue
} from '$lib/constants/date-ranges';
import { Button } from '$lib/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '$lib/components/ui/popover';
import IconCalendarDateRange from '~icons/heroicons/calendar-date-range-16-solid';
import type { DateRange } from 'bits-ui';
import { DateFormatter, getLocalTimeZone, fromAbsolute } from '@internationalized/date';

import { cn } from '$lib/utils';
import { RangeCalendar } from '$lib/components/ui/range-calendar/index';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { untrack } from 'svelte';
import { parseDateRangeParams } from '$lib/util/date-ranges';

const params = queryParameters(
{
[DATE_RANGE_PARAM]: ssp.string(),
[DATE_RANGE_START_PARAM]: ssp.number(),
[DATE_RANGE_END_PARAM]: ssp.number()
},
{ pushHistory: false, showDefaults: false }
);

const df = new DateFormatter('en-US', {
dateStyle: 'medium'
});

let selectDateRange: DateRangeOption | undefined = $state(undefined);
let selectDateRange = $derived(getDateRangeByValue(params[DATE_RANGE_PARAM]));

let calendarDateRange: DateRange | undefined = $state({
start: undefined,
end: undefined
});

// Update `calendarDateRange` when `selectDateRange` (preset selection) changes
$effect(() => {
const selectedRange = selectDateRange?.getRange();
const isCustomPreset = selectDateRange?.value === CUSTOM;
untrack(() => {
calendarDateRange = {
start: fromAbsolute(
isCustomPreset ? params[DATE_RANGE_START_PARAM] : selectedRange?.[0],
getLocalTimeZone()
),
end: fromAbsolute(
isCustomPreset ? params[DATE_RANGE_END_PARAM] : selectedRange?.[1],
getLocalTimeZone()
)
};
});
});

let dateRangePopoverOpen = $state(false);
let calendarDateRangePopoverOpen = $state(false);

Expand All @@ -42,40 +69,23 @@
if (newSelectedDateRange && newSelectedDateRange.start && newSelectedDateRange.end) {
const startDate = newSelectedDateRange.start.toDate(getLocalTimeZone()).getTime();
const endDate = newSelectedDateRange.end.toDate(getLocalTimeZone()).getTime();
updateURLParams(CUSTOM, startDate.toString(), endDate.toString());
updateURLParams(CUSTOM, startDate, endDate);
calendarDateRangePopoverOpen = false;
}
}

function updateURLParams(range: string, start?: string, end?: string) {
const url = new URL($page.url);
url.searchParams.set(DATE_RANGE_PARAM, range);
function updateURLParams(range: string, start?: number, end?: number) {
params[DATE_RANGE_PARAM] = range;

if (range === CUSTOM && start && end) {
url.searchParams.set(DATE_RANGE_START_PARAM, start);
url.searchParams.set(DATE_RANGE_END_PARAM, end);
params[DATE_RANGE_START_PARAM] = start;
params[DATE_RANGE_END_PARAM] = end;
} else {
url.searchParams.delete(DATE_RANGE_START_PARAM);
url.searchParams.delete(DATE_RANGE_END_PARAM);
params[DATE_RANGE_START_PARAM] = null;
params[DATE_RANGE_END_PARAM] = null;
}

goto(url.toString(), { replaceState: true });
}

$effect(() => {
const url = new URL($page.url);
untrack(() => {
const { dateRangeValue, startTimestamp, endTimestamp } = parseDateRangeParams(
url.searchParams
);
selectDateRange = getDateRangeByValue(dateRangeValue);
calendarDateRange = {
start: fromAbsolute(Number(startTimestamp), getLocalTimeZone()),
end: fromAbsolute(Number(endTimestamp), getLocalTimeZone())
};
});
});

function getNonCustomDateRanges() {
return DATE_RANGES.filter((range) => range.value !== CUSTOM);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
<script lang="ts">
import { queryParameters } from 'sveltekit-search-params';

import { Button } from '$lib/components/ui/button';
import {
METRIC_LABELS,
METRIC_TYPES,
type MetricType,
getMetricTypeFromParams
getMetricTypeParamConfig
} from '$lib/types/MetricType/MetricType';
import { page } from '$app/stores';
import { goto } from '$app/navigation';

let selected = $derived(getMetricTypeFromParams(new URL($page.url).searchParams));

function toggle(value: MetricType) {
const url = new URL($page.url);
url.searchParams.set('metric', value);
goto(url, { replaceState: true });
}
const params = queryParameters(
{ metric: getMetricTypeParamConfig() },
{ pushHistory: false, showDefaults: false }
);
</script>

<div class="flex space-x-[1px]">
{#each METRIC_TYPES as metricType}
<Button
variant={selected === metricType ? 'default' : 'secondary'}
variant={params.metric === metricType ? 'default' : 'secondary'}
size="sm"
on:click={() => toggle(metricType)}
on:click={() => (params.metric = metricType)}
class="first:rounded-r-none last:rounded-l-none [&:not(:first-child):not(:last-child)]:rounded-none"
>
{METRIC_LABELS[metricType]}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/constants/date-ranges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ export const DATE_RANGES: DateRange[] = [

export type DateRangeOption = (typeof DATE_RANGES)[number];

export function getDateRangeByValue(value: string): DateRange | undefined {
export function getDateRangeByValue(value: string | null): DateRange | undefined {
return DATE_RANGES.find((range) => range.value === value);
}
10 changes: 10 additions & 0 deletions frontend/src/lib/types/MetricType/MetricType.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { EncodeAndDecodeOptions } from 'sveltekit-search-params/sveltekit-search-params';

export const METRIC_TYPES = ['jsd', 'hellinger', 'psi'] as const;
export type MetricType = (typeof METRIC_TYPES)[number];

Expand All @@ -15,6 +17,14 @@ export const METRIC_SCALES: Record<MetricType, { min: number; max: number }> = {
psi: { min: 0, max: 25 }
};

export function getMetricTypeParamConfig() {
return {
encode: (value) => value,
decode: (value) => (METRIC_TYPES.includes(value as MetricType) ? (value as MetricType) : null),
defaultValue: DEFAULT_METRIC_TYPE
} satisfies EncodeAndDecodeOptions<MetricType>;
}

export function getMetricTypeFromParams(searchParams: URLSearchParams): MetricType {
const metric = searchParams.get('metric');
return METRIC_TYPES.includes(metric as MetricType) ? (metric as MetricType) : DEFAULT_METRIC_TYPE;
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/lib/util/sort.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import type { FeatureResponse, JoinTimeSeriesResponse } from '$lib/types/Model/Model';
import type { EncodeAndDecodeOptions } from 'sveltekit-search-params/sveltekit-search-params';

export type SortDirection = 'asc' | 'desc';
export const SORT_DIRECTIONS = ['asc', 'desc'] as const;
export type SortDirection = (typeof SORT_DIRECTIONS)[number];
export type SortContext = 'drift' | 'distributions';

export function getSortParamKey(context: SortContext): string {
return `${context}Sort`;
}

export function getSortParamConfig() {
return {
encode: (value) => value,
decode: (value) =>
SORT_DIRECTIONS.includes(value as SortDirection) ? (value as SortDirection) : null,
defaultValue: 'asc'
} satisfies EncodeAndDecodeOptions<SortDirection>;
}

export function getSortDirection(
searchParams: URLSearchParams,
context: SortContext
Expand All @@ -15,12 +26,6 @@ export function getSortDirection(
return param === 'desc' ? 'desc' : 'asc';
}

export function updateContextSort(url: URL, context: SortContext, direction: SortDirection): URL {
const newUrl = new URL(url);
newUrl.searchParams.set(getSortParamKey(context), direction);
return newUrl;
}

export function sortDrift(
joinTimeseries: JoinTimeSeriesResponse,
direction: SortDirection
Expand Down
31 changes: 19 additions & 12 deletions frontend/src/routes/joins/[slug]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
<script lang="ts">
import { untrack, onMount } from 'svelte';
import EChart from '$lib/components/EChart/EChart.svelte';
import { connect } from 'echarts';
import type { EChartOption, EChartsType, ECElementEvent } from 'echarts';
import { queryParameters } from 'sveltekit-search-params';
import IntersectionObserver from 'svelte-intersection-observer';
import { fade } from 'svelte/transition';

import { Tabs, TabsList, TabsTrigger, TabsContent } from '$lib/components/ui/tabs';
import IconTableCells from '~icons/heroicons/table-cells-16-solid';
import IconChartLine from '~icons/zipline-ai/chart-line';

import { Tabs, TabsList, TabsTrigger, TabsContent } from '$lib/components/ui/tabs';
import CollapsibleSection from '$lib/components/CollapsibleSection/CollapsibleSection.svelte';
import { connect } from 'echarts';
import type { FeatureResponse, TimeSeriesItem } from '$lib/types/Model/Model';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { untrack } from 'svelte';
import PageHeader from '$lib/components/PageHeader/PageHeader.svelte';
import Separator from '$lib/components/ui/separator/separator.svelte';
import ResetZoomButton from '$lib/components/ResetZoomButton/ResetZoomButton.svelte';
import IntersectionObserver from 'svelte-intersection-observer';
import { fade } from 'svelte/transition';
import { Button } from '$lib/components/ui/button';
import { Api } from '$lib/api/api';
import InfoTooltip from '$lib/components/InfoTooltip/InfoTooltip.svelte';
Expand All @@ -28,9 +30,12 @@
import { getSeriesColor } from '$lib/util/chart';
import { handleChartHighlight } from '$lib/util/chart';
import ChartControls from '$lib/components/ChartControls/ChartControls.svelte';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { getSortDirection, sortDistributions, type SortContext } from '$lib/util/sort';
import {
getSortParamConfig,
getSortParamKey,
sortDistributions,
type SortContext
} from '$lib/util/sort';

const api = new Api();

Expand Down Expand Up @@ -447,10 +452,12 @@
loadDistributions();
});

const sortedDistributions = $derived.by(() => {
const distributionsSort = getSortDirection($page.url.searchParams, 'distributions');
return sortDistributions(distributions, distributionsSort);
});
const sortKey = getSortParamKey('distributions');
const params = queryParameters(
{ [sortKey]: getSortParamConfig() },
{ pushHistory: false, showDefaults: false }
);
const sortedDistributions = $derived(sortDistributions(distributions, params[sortKey]));

let selectedTab = $state<SortContext>('drift');

Expand Down
Loading