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 all 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
26 changes: 10 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, getSortParamsConfig } from '$lib/util/sort';

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

let activeCluster = showCluster ? 'GroupBys' : null;

let currentSort: SortDirection = $derived.by(() =>
getSortDirection($page.url.searchParams, context)
const sortKey = $derived(getSortParamKey(context));
const params = $derived(
queryParameters(getSortParamsConfig(context), { 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 +55,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,59 @@
<script lang="ts">
import { untrack } from 'svelte';
import { queryParameters } 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';
import { getDateRangeParamsConfig } from '$lib/util/date-ranges';

const params = queryParameters(getDateRangeParamsConfig(), {
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 +66,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
getMetricTypeParamsConfig
} 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(getMetricTypeParamsConfig(), {
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);
}
18 changes: 16 additions & 2 deletions frontend/src/lib/types/MetricType/MetricType.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { EncodeAndDecodeOptions } from 'sveltekit-search-params/sveltekit-search-params';
import { getSearchParamValues } from '$lib/util/search-params';

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

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

export function getMetricTypeParamsConfig() {
return {
metric: {
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;
const paramsConfig = getMetricTypeParamsConfig();
return getSearchParamValues(searchParams, paramsConfig).metric;
}
34 changes: 20 additions & 14 deletions frontend/src/lib/util/date-ranges.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
import { ssp } from 'sveltekit-search-params';

import {
DATE_RANGE_PARAM,
DATE_RANGE_START_PARAM,
DATE_RANGE_END_PARAM,
PAST_1_WEEK,
getDateRangeByValue
} from '$lib/constants/date-ranges';
import { getSearchParamValues } from './search-params';

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

export function parseDateRangeParams(searchParams: URLSearchParams) {
const dateRangeValue = searchParams.get(DATE_RANGE_PARAM);
const startParam = searchParams.get(DATE_RANGE_START_PARAM);
const endParam = searchParams.get(DATE_RANGE_END_PARAM);
export function getDateRangeParamsConfig() {
return {
[DATE_RANGE_PARAM]: ssp.string(PAST_1_WEEK),
[DATE_RANGE_START_PARAM]: ssp.number(),
[DATE_RANGE_END_PARAM]: ssp.number()
};
}

let startTimestamp: number;
let endTimestamp: number;
export function parseDateRangeParams(searchParams: URLSearchParams) {
const paramsConfig = getDateRangeParamsConfig();
const paramValues = getSearchParamValues(searchParams, paramsConfig);

if (startParam && endParam) {
startTimestamp = Number(startParam);
endTimestamp = Number(endParam);
} else {
[startTimestamp, endTimestamp] = getDateRange(dateRangeValue || PAST_1_WEEK);
if (paramValues[DATE_RANGE_START_PARAM] == null || paramValues[DATE_RANGE_END_PARAM] == null) {
const [start, end] = getDateRange(paramValues[DATE_RANGE_PARAM] || PAST_1_WEEK);
paramValues[DATE_RANGE_START_PARAM] = start;
paramValues[DATE_RANGE_END_PARAM] = end;
Comment on lines +29 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add timestamp validation

Ensure start timestamp is before end timestamp.

 if (paramValues[DATE_RANGE_START_PARAM] == null || paramValues[DATE_RANGE_END_PARAM] == null) {
   const [start, end] = getDateRange(paramValues[DATE_RANGE_PARAM] || PAST_1_WEEK);
   paramValues[DATE_RANGE_START_PARAM] = start;
   paramValues[DATE_RANGE_END_PARAM] = end;
+} else if (paramValues[DATE_RANGE_START_PARAM] > paramValues[DATE_RANGE_END_PARAM]) {
+  const [start, end] = getDateRange(PAST_1_WEEK);
+  paramValues[DATE_RANGE_START_PARAM] = start;
+  paramValues[DATE_RANGE_END_PARAM] = end;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (paramValues[DATE_RANGE_START_PARAM] == null || paramValues[DATE_RANGE_END_PARAM] == null) {
const [start, end] = getDateRange(paramValues[DATE_RANGE_PARAM] || PAST_1_WEEK);
paramValues[DATE_RANGE_START_PARAM] = start;
paramValues[DATE_RANGE_END_PARAM] = end;
if (paramValues[DATE_RANGE_START_PARAM] == null || paramValues[DATE_RANGE_END_PARAM] == null) {
const [start, end] = getDateRange(paramValues[DATE_RANGE_PARAM] || PAST_1_WEEK);
paramValues[DATE_RANGE_START_PARAM] = start;
paramValues[DATE_RANGE_END_PARAM] = end;
} else if (paramValues[DATE_RANGE_START_PARAM] > paramValues[DATE_RANGE_END_PARAM]) {
const [start, end] = getDateRange(PAST_1_WEEK);
paramValues[DATE_RANGE_START_PARAM] = start;
paramValues[DATE_RANGE_END_PARAM] = end;
}

}

return {
dateRangeValue: dateRangeValue || PAST_1_WEEK,
startTimestamp,
endTimestamp
dateRangeValue: paramValues[DATE_RANGE_PARAM],
startTimestamp: paramValues[DATE_RANGE_START_PARAM],
endTimestamp: paramValues[DATE_RANGE_END_PARAM]
};
}
16 changes: 16 additions & 0 deletions frontend/src/lib/util/search-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { EncodeAndDecodeOptions } from 'sveltekit-search-params/sveltekit-search-params';

/** Get URLSearchParam values from `sveltekit-search-params` config.
* Mostly useful server-side (use `queryParameters()` client side)
**/
export function getSearchParamValues(
searchParams: URLSearchParams,
paramsConfig: Record<string, EncodeAndDecodeOptions>
) {
const paramEntries = Object.entries(paramsConfig).map(([paramName, paramConfig]) => {
const value = searchParams.get(paramName);
let decodedValue = paramConfig.decode(value) ?? paramConfig.defaultValue;
return [paramName, decodedValue];
});
return Object.fromEntries(paramEntries);
}
Loading
Loading