Skip to content

Commit 302c018

Browse files
sean-zlaitechniq
andauthored
Use sveltekit-search-params to simplify query string manipulation (#168)
## Summary Simplify client side query string / URL management by using [sveltekit-search-params](https://github.com/paoloricciuti/sveltekit-search-params). This is especially helpful to manage types when reading and also setting/updating values without having to manually use `import { page } from '$app/stores'` and `import { goto } from '$app/navigation'` in reactive statements (`$derived`, `$effect`, etc) Also added `getSearchParamValues(searchParams, paramConfigs)` to simplify getting value server side, handling type parsing and defaults with the same configs. ## Checklist - [ ] Added Unit Tests - [ ] Covered by existing CI - [ ] Integration tested - [ ] Documentation update <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Dependencies** - Added `sveltekit-search-params` package to improve URL parameter management. - **Improvements** - Streamlined sorting and metric type selection mechanisms. - Enhanced type safety for sort directions and metric types. - Simplified state management in components, including date range selection. - Updated data fetching and sorting logic for improved organization and performance. - **Technical Updates** - Refactored URL parameter handling across multiple components. - Updated import and function structures to support new parameter management approach. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- av pr metadata This information is embedded by the av CLI when creating PRs to track the status of stacks when using Aviator. Please do not delete or edit this section of the PR. ``` {"parent":"main","parentHead":"","trunk":"main"} ``` --> --------- Co-authored-by: Sean Lynch <[email protected]>
1 parent d649c02 commit 302c018

File tree

11 files changed

+158
-96
lines changed

11 files changed

+158
-96
lines changed

frontend/package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"svelte-check": "^4.1.1",
4949
"svelte-intersection-observer": "^1.0.0",
5050
"svelte-radix": "^1.1.1",
51+
"sveltekit-search-params": "^4.0.0-next.0",
5152
"tailwindcss": "^3.4.17",
5253
"typescript": "^5.7.2",
5354
"typescript-eslint": "^8.18.1",

frontend/src/lib/components/ActionButtons/ActionButtons.svelte

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
import { queryParameters } from 'sveltekit-search-params';
3+
24
import { Button } from '$lib/components/ui/button';
35
import { cn } from '$lib/utils';
46
@@ -7,14 +9,7 @@
79
import IconSquare3Stack3d from '~icons/heroicons/square-3-stack-3d-16-solid';
810
import IconXMark from '~icons/heroicons/x-mark-16-solid';
911
10-
import { goto } from '$app/navigation';
11-
import { page } from '$app/stores';
12-
import {
13-
getSortDirection,
14-
updateContextSort,
15-
type SortDirection,
16-
type SortContext
17-
} from '$lib/util/sort';
12+
import { getSortParamKey, type SortContext, getSortParamsConfig } from '$lib/util/sort';
1813
1914
let {
2015
showCluster = false,
@@ -30,14 +25,13 @@
3025
3126
let activeCluster = showCluster ? 'GroupBys' : null;
3227
33-
let currentSort: SortDirection = $derived.by(() =>
34-
getSortDirection($page.url.searchParams, context)
28+
const sortKey = $derived(getSortParamKey(context));
29+
const params = $derived(
30+
queryParameters(getSortParamsConfig(context), { pushHistory: false, showDefaults: false })
3531
);
3632
37-
function handleSort() {
38-
const newSort: SortDirection = currentSort === 'asc' ? 'desc' : 'asc';
39-
const url = updateContextSort($page.url, context, newSort);
40-
goto(url, { replaceState: true });
33+
function toggleSort() {
34+
params[sortKey] = params[sortKey] === 'asc' ? 'desc' : 'asc';
4135
}
4236
</script>
4337

@@ -61,9 +55,9 @@
6155
<!-- Action Buttons Section -->
6256
<div class="flex gap-3">
6357
{#if showSort}
64-
<Button variant="secondary" size="sm" icon="leading" on:click={handleSort}>
58+
<Button variant="secondary" size="sm" icon="leading" on:click={toggleSort}>
6559
<IconArrowsUpDown />
66-
Sort {currentSort === 'asc' ? 'A-Z' : 'Z-A'}
60+
Sort {params[sortKey] === 'asc' ? 'A-Z' : 'Z-A'}
6761
</Button>
6862
{/if}
6963
<Button variant="secondary" size="sm" icon="leading" disabled>

frontend/src/lib/components/DateRangeSelector/DateRangeSelector.svelte

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,59 @@
11
<script lang="ts">
2+
import { untrack } from 'svelte';
3+
import { queryParameters } from 'sveltekit-search-params';
4+
import type { DateRange } from 'bits-ui';
5+
import { DateFormatter, getLocalTimeZone, fromAbsolute } from '@internationalized/date';
6+
27
import {
38
DATE_RANGE_PARAM,
49
DATE_RANGES,
510
CUSTOM,
611
DATE_RANGE_START_PARAM,
712
DATE_RANGE_END_PARAM,
8-
type DateRangeOption,
913
getDateRangeByValue
1014
} from '$lib/constants/date-ranges';
1115
import { Button } from '$lib/components/ui/button';
1216
import { Popover, PopoverContent, PopoverTrigger } from '$lib/components/ui/popover';
1317
import IconCalendarDateRange from '~icons/heroicons/calendar-date-range-16-solid';
14-
import type { DateRange } from 'bits-ui';
15-
import { DateFormatter, getLocalTimeZone, fromAbsolute } from '@internationalized/date';
18+
1619
import { cn } from '$lib/utils';
1720
import { RangeCalendar } from '$lib/components/ui/range-calendar/index';
18-
import { goto } from '$app/navigation';
19-
import { page } from '$app/stores';
20-
import { untrack } from 'svelte';
21-
import { parseDateRangeParams } from '$lib/util/date-ranges';
21+
import { getDateRangeParamsConfig } from '$lib/util/date-ranges';
22+
23+
const params = queryParameters(getDateRangeParamsConfig(), {
24+
pushHistory: false,
25+
showDefaults: false
26+
});
2227
2328
const df = new DateFormatter('en-US', {
2429
dateStyle: 'medium'
2530
});
2631
27-
let selectDateRange: DateRangeOption | undefined = $state(undefined);
32+
let selectDateRange = $derived(getDateRangeByValue(params[DATE_RANGE_PARAM]));
33+
2834
let calendarDateRange: DateRange | undefined = $state({
2935
start: undefined,
3036
end: undefined
3137
});
3238
39+
// Update `calendarDateRange` when `selectDateRange` (preset selection) changes
40+
$effect(() => {
41+
const selectedRange = selectDateRange?.getRange();
42+
const isCustomPreset = selectDateRange?.value === CUSTOM;
43+
untrack(() => {
44+
calendarDateRange = {
45+
start: fromAbsolute(
46+
isCustomPreset ? params[DATE_RANGE_START_PARAM] : selectedRange?.[0],
47+
getLocalTimeZone()
48+
),
49+
end: fromAbsolute(
50+
isCustomPreset ? params[DATE_RANGE_END_PARAM] : selectedRange?.[1],
51+
getLocalTimeZone()
52+
)
53+
};
54+
});
55+
});
56+
3357
let dateRangePopoverOpen = $state(false);
3458
let calendarDateRangePopoverOpen = $state(false);
3559
@@ -42,40 +66,23 @@
4266
if (newSelectedDateRange && newSelectedDateRange.start && newSelectedDateRange.end) {
4367
const startDate = newSelectedDateRange.start.toDate(getLocalTimeZone()).getTime();
4468
const endDate = newSelectedDateRange.end.toDate(getLocalTimeZone()).getTime();
45-
updateURLParams(CUSTOM, startDate.toString(), endDate.toString());
69+
updateURLParams(CUSTOM, startDate, endDate);
4670
calendarDateRangePopoverOpen = false;
4771
}
4872
}
4973
50-
function updateURLParams(range: string, start?: string, end?: string) {
51-
const url = new URL($page.url);
52-
url.searchParams.set(DATE_RANGE_PARAM, range);
74+
function updateURLParams(range: string, start?: number, end?: number) {
75+
params[DATE_RANGE_PARAM] = range;
5376
5477
if (range === CUSTOM && start && end) {
55-
url.searchParams.set(DATE_RANGE_START_PARAM, start);
56-
url.searchParams.set(DATE_RANGE_END_PARAM, end);
78+
params[DATE_RANGE_START_PARAM] = start;
79+
params[DATE_RANGE_END_PARAM] = end;
5780
} else {
58-
url.searchParams.delete(DATE_RANGE_START_PARAM);
59-
url.searchParams.delete(DATE_RANGE_END_PARAM);
81+
params[DATE_RANGE_START_PARAM] = null;
82+
params[DATE_RANGE_END_PARAM] = null;
6083
}
61-
62-
goto(url.toString(), { replaceState: true });
6384
}
6485
65-
$effect(() => {
66-
const url = new URL($page.url);
67-
untrack(() => {
68-
const { dateRangeValue, startTimestamp, endTimestamp } = parseDateRangeParams(
69-
url.searchParams
70-
);
71-
selectDateRange = getDateRangeByValue(dateRangeValue);
72-
calendarDateRange = {
73-
start: fromAbsolute(Number(startTimestamp), getLocalTimeZone()),
74-
end: fromAbsolute(Number(endTimestamp), getLocalTimeZone())
75-
};
76-
});
77-
});
78-
7986
function getNonCustomDateRanges() {
8087
return DATE_RANGES.filter((range) => range.value !== CUSTOM);
8188
}

frontend/src/lib/components/MetricTypeToggle/MetricTypeToggle.svelte

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
<script lang="ts">
2+
import { queryParameters } from 'sveltekit-search-params';
3+
24
import { Button } from '$lib/components/ui/button';
35
import {
46
METRIC_LABELS,
57
METRIC_TYPES,
6-
type MetricType,
7-
getMetricTypeFromParams
8+
getMetricTypeParamsConfig
89
} from '$lib/types/MetricType/MetricType';
9-
import { page } from '$app/stores';
10-
import { goto } from '$app/navigation';
11-
12-
let selected = $derived(getMetricTypeFromParams(new URL($page.url).searchParams));
1310
14-
function toggle(value: MetricType) {
15-
const url = new URL($page.url);
16-
url.searchParams.set('metric', value);
17-
goto(url, { replaceState: true });
18-
}
11+
const params = queryParameters(getMetricTypeParamsConfig(), {
12+
pushHistory: false,
13+
showDefaults: false
14+
});
1915
</script>
2016

2117
<div class="flex space-x-[1px]">
2218
{#each METRIC_TYPES as metricType}
2319
<Button
24-
variant={selected === metricType ? 'default' : 'secondary'}
20+
variant={params.metric === metricType ? 'default' : 'secondary'}
2521
size="sm"
26-
on:click={() => toggle(metricType)}
22+
on:click={() => (params.metric = metricType)}
2723
class="first:rounded-r-none last:rounded-l-none [&:not(:first-child):not(:last-child)]:rounded-none"
2824
>
2925
{METRIC_LABELS[metricType]}

frontend/src/lib/constants/date-ranges.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ export const DATE_RANGES: DateRange[] = [
6161

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

64-
export function getDateRangeByValue(value: string): DateRange | undefined {
64+
export function getDateRangeByValue(value: string | null): DateRange | undefined {
6565
return DATE_RANGES.find((range) => range.value === value);
6666
}

frontend/src/lib/types/MetricType/MetricType.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { EncodeAndDecodeOptions } from 'sveltekit-search-params/sveltekit-search-params';
2+
import { getSearchParamValues } from '$lib/util/search-params';
3+
14
export const METRIC_TYPES = ['jsd', 'hellinger', 'psi'] as const;
25
export type MetricType = (typeof METRIC_TYPES)[number];
36

@@ -15,7 +18,18 @@ export const METRIC_SCALES: Record<MetricType, { min: number; max: number }> = {
1518
psi: { min: 0, max: 25 }
1619
};
1720

21+
export function getMetricTypeParamsConfig() {
22+
return {
23+
metric: {
24+
encode: (value) => value,
25+
decode: (value) =>
26+
METRIC_TYPES.includes(value as MetricType) ? (value as MetricType) : null,
27+
defaultValue: DEFAULT_METRIC_TYPE
28+
} satisfies EncodeAndDecodeOptions<MetricType>
29+
};
30+
}
31+
1832
export function getMetricTypeFromParams(searchParams: URLSearchParams): MetricType {
19-
const metric = searchParams.get('metric');
20-
return METRIC_TYPES.includes(metric as MetricType) ? (metric as MetricType) : DEFAULT_METRIC_TYPE;
33+
const paramsConfig = getMetricTypeParamsConfig();
34+
return getSearchParamValues(searchParams, paramsConfig).metric;
2135
}

frontend/src/lib/util/date-ranges.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
1+
import { ssp } from 'sveltekit-search-params';
2+
13
import {
24
DATE_RANGE_PARAM,
35
DATE_RANGE_START_PARAM,
46
DATE_RANGE_END_PARAM,
57
PAST_1_WEEK,
68
getDateRangeByValue
79
} from '$lib/constants/date-ranges';
10+
import { getSearchParamValues } from './search-params';
811

912
export function getDateRange(range: string): [number, number] {
1013
const dateRange = getDateRangeByValue(range);
1114
return dateRange ? dateRange.getRange() : getDateRangeByValue(PAST_1_WEEK)!.getRange();
1215
}
1316

14-
export function parseDateRangeParams(searchParams: URLSearchParams) {
15-
const dateRangeValue = searchParams.get(DATE_RANGE_PARAM);
16-
const startParam = searchParams.get(DATE_RANGE_START_PARAM);
17-
const endParam = searchParams.get(DATE_RANGE_END_PARAM);
17+
export function getDateRangeParamsConfig() {
18+
return {
19+
[DATE_RANGE_PARAM]: ssp.string(PAST_1_WEEK),
20+
[DATE_RANGE_START_PARAM]: ssp.number(),
21+
[DATE_RANGE_END_PARAM]: ssp.number()
22+
};
23+
}
1824

19-
let startTimestamp: number;
20-
let endTimestamp: number;
25+
export function parseDateRangeParams(searchParams: URLSearchParams) {
26+
const paramsConfig = getDateRangeParamsConfig();
27+
const paramValues = getSearchParamValues(searchParams, paramsConfig);
2128

22-
if (startParam && endParam) {
23-
startTimestamp = Number(startParam);
24-
endTimestamp = Number(endParam);
25-
} else {
26-
[startTimestamp, endTimestamp] = getDateRange(dateRangeValue || PAST_1_WEEK);
29+
if (paramValues[DATE_RANGE_START_PARAM] == null || paramValues[DATE_RANGE_END_PARAM] == null) {
30+
const [start, end] = getDateRange(paramValues[DATE_RANGE_PARAM] || PAST_1_WEEK);
31+
paramValues[DATE_RANGE_START_PARAM] = start;
32+
paramValues[DATE_RANGE_END_PARAM] = end;
2733
}
2834

2935
return {
30-
dateRangeValue: dateRangeValue || PAST_1_WEEK,
31-
startTimestamp,
32-
endTimestamp
36+
dateRangeValue: paramValues[DATE_RANGE_PARAM],
37+
startTimestamp: paramValues[DATE_RANGE_START_PARAM],
38+
endTimestamp: paramValues[DATE_RANGE_END_PARAM]
3339
};
3440
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { EncodeAndDecodeOptions } from 'sveltekit-search-params/sveltekit-search-params';
2+
3+
/** Get URLSearchParam values from `sveltekit-search-params` config.
4+
* Mostly useful server-side (use `queryParameters()` client side)
5+
**/
6+
export function getSearchParamValues(
7+
searchParams: URLSearchParams,
8+
paramsConfig: Record<string, EncodeAndDecodeOptions>
9+
) {
10+
const paramEntries = Object.entries(paramsConfig).map(([paramName, paramConfig]) => {
11+
const value = searchParams.get(paramName);
12+
let decodedValue = paramConfig.decode(value) ?? paramConfig.defaultValue;
13+
return [paramName, decodedValue];
14+
});
15+
return Object.fromEntries(paramEntries);
16+
}

frontend/src/lib/util/sort.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import type { FeatureResponse, JoinTimeSeriesResponse } from '$lib/types/Model/Model';
2+
import type { EncodeAndDecodeOptions } from 'sveltekit-search-params/sveltekit-search-params';
23

3-
export type SortDirection = 'asc' | 'desc';
4+
export const SORT_DIRECTIONS = ['asc', 'desc'] as const;
5+
export type SortDirection = (typeof SORT_DIRECTIONS)[number];
46
export type SortContext = 'drift' | 'distributions';
57

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

12+
export function getSortParamsConfig(context: SortContext) {
13+
const sortKey = getSortParamKey(context);
14+
return {
15+
[sortKey]: {
16+
encode: (value) => value,
17+
decode: (value) =>
18+
SORT_DIRECTIONS.includes(value as SortDirection) ? (value as SortDirection) : null,
19+
defaultValue: 'asc'
20+
} satisfies EncodeAndDecodeOptions<SortDirection>
21+
};
22+
}
23+
1024
export function getSortDirection(
1125
searchParams: URLSearchParams,
1226
context: SortContext
@@ -15,12 +29,6 @@ export function getSortDirection(
1529
return param === 'desc' ? 'desc' : 'asc';
1630
}
1731

18-
export function updateContextSort(url: URL, context: SortContext, direction: SortDirection): URL {
19-
const newUrl = new URL(url);
20-
newUrl.searchParams.set(getSortParamKey(context), direction);
21-
return newUrl;
22-
}
23-
2432
export function sortDrift(
2533
joinTimeseries: JoinTimeSeriesResponse,
2634
direction: SortDirection

0 commit comments

Comments
 (0)