Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
/**
|
|
* The configuration pages' URL state, validated as pure functions so each
|
|
* route's `validateSearch` stays a one-liner and every rejection is testable
|
|
* without a router.
|
|
*
|
|
* A tab that is not one of the page's tabs falls back to that page's default:
|
|
* a hand-typed `?tab=upstreams` on Protection opens Protection's first tab
|
|
* rather than an empty panel.
|
|
*/
|
|
|
|
export const PROTECTION_TABS = ["groups", "sources"] as const;
|
|
export type ProtectionTab = (typeof PROTECTION_TABS)[number];
|
|
|
|
export const RESOLUTION_TABS = ["upstreams", "records", "zones"] as const;
|
|
export type ResolutionTab = (typeof RESOLUTION_TABS)[number];
|
|
|
|
/**
|
|
* Both fields are optional so a link to the page need not spell out a tab it
|
|
* does not care about — the same shape Overview's `period` uses. Validation
|
|
* still resolves an unknown tab to the page default, so a *rendered* page
|
|
* always has one.
|
|
*/
|
|
export interface ProtectionSearch {
|
|
tab?: ProtectionTab;
|
|
/**
|
|
* The selected group. Absent until the page resolves one, which it then
|
|
* writes back so a view of the page is a link.
|
|
*/
|
|
group?: number;
|
|
}
|
|
|
|
export interface ResolutionSearch {
|
|
tab?: ResolutionTab;
|
|
}
|
|
|
|
function validateTab<T extends string>(options: readonly T[], value: unknown, fallback: T): T {
|
|
return options.includes(value as T) ? (value as T) : fallback;
|
|
}
|
|
|
|
/**
|
|
* A group row id. Ids are positive integers, so anything else — a fraction, a
|
|
* string, a zero — is not an id that could ever exist and becomes `undefined`.
|
|
* An id that is well-formed but unknown is a different case: the page has to
|
|
* load the groups before it can tell, so it falls back there, not here.
|
|
*/
|
|
export function validateGroupId(value: unknown): number | undefined {
|
|
return Number.isSafeInteger(value) && (value as number) > 0 ? (value as number) : undefined;
|
|
}
|
|
|
|
export function validateProtectionSearch(search: Record<string, unknown>): ProtectionSearch {
|
|
return {
|
|
tab: validateTab(PROTECTION_TABS, search["tab"], "groups"),
|
|
group: validateGroupId(search["group"]),
|
|
};
|
|
}
|
|
|
|
export function validateResolutionSearch(search: Record<string, unknown>): ResolutionSearch {
|
|
return { tab: validateTab(RESOLUTION_TABS, search["tab"], "upstreams") };
|
|
}
|