import { createParser, parseAsArrayOf, parseAsString, parseAsStringLiteral } from 'nuqs/server' import { CORE_TRIGGER_TYPES, type LogLevel, type TimeRange, type TriggerType, } from '@/stores/logs/filters/types' /** * Co-located, typed URL query-param definitions for the logs feature. The * client hook (`useLogFilters`) consumes this typed param definition as the * single source of truth. * * The encoding here intentionally preserves the exact wire format the logs page * shipped before nuqs: `timeRange` uses kebab tokens, `level` / `workflowIds` / * `folderIds` / `triggers` are comma-joined, and `search` is trimmed. */ const DEFAULT_TIME_RANGE: TimeRange = 'All time' /** Maps a {@link TimeRange} label to its stable URL token and back. */ const TIME_RANGE_TO_TOKEN: Record = { 'All time': 'all-time', 'Past 30 minutes': 'past-30-minutes', 'Past hour': 'past-hour', 'Past 6 hours': 'past-6-hours', 'Past 12 hours': 'past-12-hours', 'Past 24 hours': 'past-24-hours', 'Past 3 days': 'past-3-days', 'Past 7 days': 'past-7-days', 'Past 14 days': 'past-14-days', 'Past 30 days': 'past-30-days', 'Custom range': 'custom', } const TOKEN_TO_TIME_RANGE: Record = Object.fromEntries( Object.entries(TIME_RANGE_TO_TOKEN).map(([label, token]) => [token, label as TimeRange]) ) as Record /** * Parser for the `timeRange` param. Serializes labels to kebab tokens and * tolerantly maps unknown tokens back to the default ("All time"). */ export const parseAsTimeRange = createParser({ parse(value) { return TOKEN_TO_TIME_RANGE[value] ?? DEFAULT_TIME_RANGE }, serialize(value) { return TIME_RANGE_TO_TOKEN[value] ?? 'all-time' }, }) const VALID_LEVELS = ['error', 'info', 'running', 'pending'] as const /** * Parser for the `level` param. `level` is a comma-joined list of statuses on * the wire but is surfaced as a single `LogLevel` value ("all", a single status, * or a comma-joined string) to match the existing store contract. */ export const parseAsLogLevel = createParser({ parse(value) { const levels = value .split(',') .filter((l): l is (typeof VALID_LEVELS)[number] => (VALID_LEVELS as readonly string[]).includes(l) ) if (levels.length === 0) return 'all' if (levels.length === 1) return levels[0] return levels.join(',') as LogLevel }, serialize(value) { return value }, }) const CORE_TRIGGER_SET = new Set(CORE_TRIGGER_TYPES) /** * Parser for the `triggers` param, restricted to known core trigger types. * Surfaced as `TriggerType[]` to match the consumer contract — unknown tokens * are dropped (mirrors the prior `parseTriggerArrayFromURL` behavior). */ export const parseAsTriggers = createParser({ parse(value) { const triggers = value.split(',').filter((t): t is TriggerType => CORE_TRIGGER_SET.has(t)) return triggers }, serialize(value) { return value.join(',') }, eq(a, b) { return a.length === b.length && a.every((v, i) => v === b[i]) }, }).withDefault([]) /** * The nuqs parser map for every URL-synced logs filter. `clearOnDefault` keeps * the URL clean (params drop out when they hold their default value) and * `history: 'replace'` matches the prior `history.replaceState` behavior so * filter changes don't pollute the browser back stack. */ export const logFilterParsers = { timeRange: parseAsTimeRange.withDefault(DEFAULT_TIME_RANGE), startDate: parseAsString, endDate: parseAsString, level: parseAsLogLevel.withDefault('all'), workflowIds: parseAsArrayOf(parseAsString).withDefault([]), folderIds: parseAsArrayOf(parseAsString).withDefault([]), triggers: parseAsTriggers, search: parseAsString.withDefault(''), } as const /** Shared nuqs options for the logs filters: clean URLs, no back-stack churn. */ export const logFilterUrlKeys = { history: 'replace', clearOnDefault: true, } as const /** * Read-only deep link to a specific execution. Resolves to a log row and opens * the details sidebar on load. Intentionally NOT stripped — the link stays * shareable — so it carries no `clearOnDefault`/`history` options here. */ export const executionIdParam = { key: 'executionId', parser: parseAsString, } as const const LOG_DETAILS_TABS = ['overview', 'trace'] as const /** * Active tab of the log-details sidebar (`overview` / `trace`). Deep-linkable so * a shared link can land on the trace view; `replace` keeps it off the back * stack and `clearOnDefault` drops it from the URL when on the default tab. */ export const logDetailsTabParam = { key: 'tab', parser: parseAsStringLiteral(LOG_DETAILS_TABS).withDefault('overview'), } as const /** Tab change is view-state, not a destination: replace, clean URL on default. */ export const logDetailsTabUrlKeys = { history: 'replace', clearOnDefault: true, } as const