milestone 32: task-shaped configuration, file mode as a rendering, config status api
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
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
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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") };
|
||||
}
|
||||
Reference in New Issue
Block a user