diff --git a/eslint.config.js b/eslint.config.js index 15c2cb52db..b15a97fd66 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -22,6 +22,7 @@ const sharedPlugins = { }; const sharedRules = { + 'svelte/no-navigation-without-resolve': 'off', 'no-undef': 'off', quotes: ['error', 'single', { avoidEscape: true }], '@typescript-eslint/no-unused-vars': [ @@ -126,7 +127,6 @@ export default [ rules: { ...sharedRules, 'svelte/require-each-key': 'warn', - 'svelte/no-navigation-without-resolve': 'warn', 'svelte/no-reactive-reassign': 'warn', 'svelte/no-reactive-functions': 'warn', 'svelte/no-reactive-literals': 'warn', diff --git a/src/lib/pages/workflow-history.svelte b/src/lib/pages/workflow-history.svelte index 8581b8f24e..6e5775e774 100644 --- a/src/lib/pages/workflow-history.svelte +++ b/src/lib/pages/workflow-history.svelte @@ -2,12 +2,12 @@ import { onMount } from 'svelte'; import { goto } from '$app/navigation'; - import { page } from '$app/stores'; + import { page } from '$app/state'; import { eventFilterSort } from '$lib/stores/event-view'; import { routeForEventHistory } from '$lib/utilities/route-for'; - const { namespace, workflow, run } = $page.params; + const { namespace, workflow, run } = page.params; onMount(async () => { const queryParams = { diff --git a/src/lib/stores/schedules.ts b/src/lib/stores/schedules.ts index 4e4b247599..7bb6c3a6a0 100644 --- a/src/lib/stores/schedules.ts +++ b/src/lib/stores/schedules.ts @@ -1,7 +1,6 @@ import { writable } from 'svelte/store'; import { goto } from '$app/navigation'; -import { resolve } from '$app/paths'; import { translate } from '$lib/i18n/translate'; import { createSchedule, editSchedule } from '$lib/services/schedule-service'; @@ -173,7 +172,7 @@ export const submitCreateSchedule = async ({ createTimeout = setTimeout(() => { error.set(''); loading.set(false); - goto(resolve(routeForSchedules({ namespace }), {})); + goto(routeForSchedules({ namespace })); }, 2000); } }; @@ -266,7 +265,7 @@ export const submitEditSchedule = async ( } else { clearTimeout(editTimeout); editTimeout = setTimeout(() => { - goto(resolve(routeForSchedule({ namespace, scheduleId: name }), {})); + goto(routeForSchedule({ namespace, scheduleId: name })); error.set(''); loading.set(false); }, 2000); diff --git a/src/lib/utilities/route-for-base-path.test.ts b/src/lib/utilities/route-for-base-path.test.ts new file mode 100644 index 0000000000..eefa10464a --- /dev/null +++ b/src/lib/utilities/route-for-base-path.test.ts @@ -0,0 +1,232 @@ +import { describe, expect, it, vi } from 'vitest'; + +const BASE_PATH = '/temporal'; + +vi.mock('$app/paths', () => ({ + resolve: (route: string, params?: Record) => { + let resolved = route; + if (params) { + resolved = Object.entries(params).reduce( + (path, [key, value]) => path.replace(`[${key}]`, value), + resolved, + ); + } + return `${BASE_PATH}${resolved}`; + }, +})); + +import * as routeForModule from './route-for'; +import { + routeForArchivalEventHistory, + routeForArchivalWorkflows, + routeForAuthentication, + routeForBatchOperation, + routeForBatchOperations, + routeForCallStack, + routeForEventHistory, + routeForEventHistoryEvent, + routeForEventHistoryImport, + routeForLoginPage, + routeForNamespace, + routeForNamespaces, + routeForNamespaceSelector, + routeForNexus, + routeForNexusEndpoint, + routeForNexusEndpointCreate, + routeForNexusEndpointEdit, + routeForNexusLinks, + routeForPendingActivities, + routeForRelationships, + routeForSchedule, + routeForScheduleCreate, + routeForScheduleEdit, + routeForSchedules, + routeForStandaloneActivities, + routeForStandaloneActivitiesWithQuery, + routeForStandaloneActivityDetails, + routeForStandaloneActivityMetadata, + routeForStandaloneActivitySearchAttributes, + routeForStandaloneActivityWorkers, + routeForStartStandaloneActivity, + routeForTaskQueue, + routeForTimeline, + routeForUserMetadata, + routeForWorkerDeployment, + routeForWorkerDeployments, + routeForWorkerDeploymentVersion, + routeForWorkers, + routeForWorkflow, + routeForWorkflowMemo, + routeForWorkflowQuery, + routeForWorkflows, + routeForWorkflowSearchAttributes, + routeForWorkflowStart, + routeForWorkflowsWithQuery, + routeForWorkflowUpdate, +} from './route-for'; + +describe('routeFor functions should resolve the base path exactly once', () => { + const namespaceParams = { namespace: 'default' }; + const workflowParams = { + namespace: 'default', + workflow: 'wf-id', + run: 'run-id', + }; + const scheduleParams = { namespace: 'default', scheduleId: 'sched-1' }; + + const activityParams = { + namespace: 'default', + activityId: 'act-1', + runId: 'run-1', + }; + + const cases: [string, () => string | undefined][] = [ + ['routeForNamespaces', () => routeForNamespaces()], + ['routeForNexus', () => routeForNexus()], + ['routeForNexusEndpoint', () => routeForNexusEndpoint('ep-1')], + ['routeForNexusEndpointEdit', () => routeForNexusEndpointEdit('ep-1')], + ['routeForNexusEndpointCreate', () => routeForNexusEndpointCreate()], + ['routeForNamespace', () => routeForNamespace(namespaceParams)], + ['routeForNamespaceSelector', () => routeForNamespaceSelector()], + ['routeForWorkflows', () => routeForWorkflows(namespaceParams)], + [ + 'routeForArchivalWorkflows', + () => routeForArchivalWorkflows(namespaceParams), + ], + ['routeForWorkflow', () => routeForWorkflow(workflowParams)], + ['routeForSchedules', () => routeForSchedules(namespaceParams)], + ['routeForScheduleCreate', () => routeForScheduleCreate(namespaceParams)], + ['routeForSchedule', () => routeForSchedule(scheduleParams)], + ['routeForScheduleEdit', () => routeForScheduleEdit(scheduleParams)], + [ + 'routeForArchivalEventHistory', + () => routeForArchivalEventHistory(workflowParams), + ], + [ + 'routeForEventHistoryEvent', + () => routeForEventHistoryEvent({ ...workflowParams, eventId: '1' }), + ], + ['routeForEventHistory', () => routeForEventHistory(workflowParams)], + ['routeForTimeline', () => routeForTimeline(workflowParams)], + ['routeForWorkers', () => routeForWorkers(workflowParams)], + [ + 'routeForWorkerDeployments', + () => routeForWorkerDeployments(namespaceParams), + ], + [ + 'routeForWorkerDeployment', + () => + routeForWorkerDeployment({ + namespace: 'default', + deployment: 'dep-1', + }), + ], + [ + 'routeForWorkerDeploymentVersion', + () => + routeForWorkerDeploymentVersion({ + namespace: 'default', + deployment: 'dep-1', + version: 'v1', + }), + ], + ['routeForRelationships', () => routeForRelationships(workflowParams)], + [ + 'routeForTaskQueue', + () => routeForTaskQueue({ namespace: 'default', queue: 'q-1' }), + ], + ['routeForCallStack', () => routeForCallStack(workflowParams)], + ['routeForWorkflowQuery', () => routeForWorkflowQuery(workflowParams)], + ['routeForUserMetadata', () => routeForUserMetadata(workflowParams)], + [ + 'routeForWorkflowSearchAttributes', + () => routeForWorkflowSearchAttributes(workflowParams), + ], + ['routeForWorkflowMemo', () => routeForWorkflowMemo(workflowParams)], + ['routeForWorkflowUpdate', () => routeForWorkflowUpdate(workflowParams)], + [ + 'routeForPendingActivities', + () => routeForPendingActivities(workflowParams), + ], + ['routeForNexusLinks', () => routeForNexusLinks(workflowParams)], + ['routeForEventHistoryImport', () => routeForEventHistoryImport()], + ['routeForBatchOperations', () => routeForBatchOperations(namespaceParams)], + [ + 'routeForBatchOperation', + () => routeForBatchOperation({ namespace: 'default', jobId: 'job-1' }), + ], + [ + 'routeForStandaloneActivities', + () => routeForStandaloneActivities(namespaceParams), + ], + [ + 'routeForStandaloneActivitiesWithQuery', + () => + routeForStandaloneActivitiesWithQuery(namespaceParams, 'test-query'), + ], + [ + 'routeForStartStandaloneActivity', + () => routeForStartStandaloneActivity(namespaceParams), + ], + [ + 'routeForStandaloneActivityDetails', + () => routeForStandaloneActivityDetails(activityParams), + ], + [ + 'routeForStandaloneActivityWorkers', + () => routeForStandaloneActivityWorkers(activityParams), + ], + [ + 'routeForStandaloneActivitySearchAttributes', + () => routeForStandaloneActivitySearchAttributes(activityParams), + ], + [ + 'routeForStandaloneActivityMetadata', + () => routeForStandaloneActivityMetadata(activityParams), + ], + [ + 'routeForWorkflowStart', + () => routeForWorkflowStart({ namespace: 'default' }), + ], + [ + 'routeForWorkflowsWithQuery', + () => routeForWorkflowsWithQuery({ namespace: 'default', query: 'test' }), + ], + [ + 'routeForAuthentication', + () => + routeForAuthentication({ + settings: { auth: {}, baseUrl: 'https://example.com' }, + searchParams: new URLSearchParams(), + }), + ], + ['routeForLoginPage', () => routeForLoginPage('', false)], + ]; + + it.each(cases)('%s should resolve the base path', (_name, fn) => { + const result = fn(); + expect(typeof result).toBe('string'); + expect(result?.length).toBeGreaterThan(0); + expect(result).toMatch(new RegExp(`${BASE_PATH}`)); + expect(result).not.toMatch(new RegExp(`${BASE_PATH}${BASE_PATH}`)); + }); + + it('should have a test case for every exported routeFor function', () => { + const testedNames = new Set(cases.map(([name]) => name)); + const exportedRouteForFunctions = Object.keys(routeForModule).filter( + (key) => + key.startsWith('routeFor') && + typeof routeForModule[key as keyof typeof routeForModule] === + 'function', + ); + + const missing = exportedRouteForFunctions.filter( + (name) => !testedNames.has(name), + ); + if (missing.length > 0) { + throw new Error( + `Missing base path test cases for: ${missing.join(', ')}. Add them to the cases array above.`, + ); + } + }); +}); diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts index d0ce22f3b8..413a07f448 100644 --- a/src/lib/utilities/route-for.ts +++ b/src/lib/utilities/route-for.ts @@ -102,19 +102,19 @@ export const routeForNamespaceSelector = (): ResolvedPathname => { export const routeForWorkflows = ( parameters: NamespaceParameter, ): ResolvedPathname => { - return resolve(`${routeForNamespace(parameters)}/workflows`, {}); + return `${routeForNamespace(parameters)}/workflows`; }; export const routeForStandaloneActivities = ( parameters: NamespaceParameter, -): string => { +): ResolvedPathname => { return `${routeForNamespace(parameters)}/activities`; }; export const routeForStandaloneActivitiesWithQuery = ( parameters: NamespaceParameter, queryString: string, -): string => { +): ResolvedPathname => { const params = new URLSearchParams(); params.set('query', queryString); @@ -123,7 +123,7 @@ export const routeForStandaloneActivitiesWithQuery = ( export const routeForStartStandaloneActivity = ( parameters: NamespaceParameter & Partial, -): string => { +): ResolvedPathname => { const params = { activityId: parameters.activityId ?? '', activityType: parameters.activityType ?? '', @@ -136,39 +136,33 @@ export const routeForStartStandaloneActivity = ( const routeForStandaloneActivityBase = ( parameters: NamespaceParameter & { activityId: string; runId: string }, -) => { +): ResolvedPathname => { const activityId = encodeURIForSvelte(parameters.activityId); - return resolve( - `${routeForStandaloneActivities(parameters)}/[activityId]/[runId]`, - { - activityId, - runId: parameters.runId, - }, - ); + return `${routeForStandaloneActivities(parameters)}/${activityId}/${parameters.runId}`; }; export const routeForStandaloneActivityDetails = ( parameters: NamespaceParameter & { activityId: string; runId: string }, -) => { +): ResolvedPathname => { return `${routeForStandaloneActivityBase(parameters)}/details`; }; export const routeForStandaloneActivityWorkers = ( parameters: NamespaceParameter & { activityId: string; runId: string }, -) => { +): ResolvedPathname => { return `${routeForStandaloneActivityBase(parameters)}/workers`; }; export const routeForStandaloneActivitySearchAttributes = ( parameters: NamespaceParameter & { activityId: string; runId: string }, -) => { +): ResolvedPathname => { return `${routeForStandaloneActivityBase(parameters)}/search-attributes`; }; export const routeForStandaloneActivityMetadata = ( parameters: NamespaceParameter & { activityId: string; runId: string }, -) => { +): ResolvedPathname => { return `${routeForStandaloneActivityBase(parameters)}/metadata`; }; @@ -185,7 +179,7 @@ export const routeForWorkflowStart = ({ runId, taskQueue, workflowType, -}: StartWorkflowParameters): string => { +}: StartWorkflowParameters): ResolvedPathname => { return toURL(`${routeForNamespace({ namespace })}/workflows/start-workflow`, { workflowId: workflowId || '', runId: runId || '', @@ -198,7 +192,7 @@ export const routeForWorkflowsWithQuery = ({ namespace, query, page, -}: WorkflowsParameter): string | undefined => { +}: WorkflowsParameter): ResolvedPathname | undefined => { if (!BROWSER) { return undefined; } @@ -212,7 +206,7 @@ export const routeForWorkflowsWithQuery = ({ export const routeForArchivalWorkflows = ( parameters: NamespaceParameter, ): ResolvedPathname => { - return resolve(`${routeForNamespace(parameters)}/archival`, {}); + return `${routeForNamespace(parameters)}/archival`; }; export const routeForWorkflow = ({ @@ -222,19 +216,19 @@ export const routeForWorkflow = ({ }: WorkflowParameters): ResolvedPathname => { const id = encodeURIForSvelte(workflow); - return resolve(`${routeForWorkflows(parameters)}/[id]/[run]`, { id, run }); + return `${routeForWorkflows(parameters)}/${id}/${run}`; }; export const routeForSchedules = ( parameters: NamespaceParameter, ): ResolvedPathname => { - return resolve(`${routeForNamespace(parameters)}/schedules`, {}); + return `${routeForNamespace(parameters)}/schedules`; }; export const routeForScheduleCreate = ({ namespace, }: NamespaceParameter): ResolvedPathname => { - return resolve(`${routeForSchedules({ namespace })}/create`, {}); + return `${routeForSchedules({ namespace })}/create`; }; export const routeForSchedule = ({ @@ -243,7 +237,7 @@ export const routeForSchedule = ({ }: ScheduleParameters): ResolvedPathname => { const id = encodeURIForSvelte(scheduleId); - return resolve(`${routeForSchedules({ namespace })}/[id]`, { id }); + return `${routeForSchedules({ namespace })}/${id}`; }; export const routeForScheduleEdit = ({ @@ -252,7 +246,7 @@ export const routeForScheduleEdit = ({ }: ScheduleParameters): ResolvedPathname => { const id = encodeURIForSvelte(scheduleId); - return resolve(`${routeForSchedules({ namespace })}/[id]/edit`, { id }); + return `${routeForSchedules({ namespace })}/${id}/edit`; }; export const routeForArchivalEventHistory = ({ @@ -261,17 +255,14 @@ export const routeForArchivalEventHistory = ({ ...parameters }: WorkflowParameters): ResolvedPathname => { const id = encodeURIForSvelte(workflow); - return resolve( - `${routeForArchivalWorkflows(parameters)}/[id]/[run]/history`, - { id, run }, - ); + return `${routeForArchivalWorkflows(parameters)}/${id}/${run}/history`; }; export const routeForEventHistory = ({ queryParams, archival, ...parameters -}: EventHistoryParameters & { archival?: boolean }): string => { +}: EventHistoryParameters & { archival?: boolean }): ResolvedPathname => { if (archival) return toURL(routeForArchivalEventHistory(parameters)); const eventHistoryPath = `${routeForWorkflow(parameters)}/history`; return toURL(`${eventHistoryPath}`, queryParams); @@ -282,9 +273,7 @@ export const routeForEventHistoryEvent = ({ requestId, ...parameters }: EventParameters): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/history/events/[id]`, { - id: eventId || requestId, - }); + return `${routeForWorkflow(parameters)}/history/events/${eventId || requestId}`; }; export const routeForTimeline = ({ @@ -294,7 +283,7 @@ export const routeForTimeline = ({ }: WorkflowParameters & { queryParams?: Record; archival?: boolean; -}): string => { +}): ResolvedPathname => { if (archival) return toURL(routeForArchivalEventHistory(parameters)); const path = `${routeForWorkflow(parameters)}/timeline`; return toURL(path, queryParams); @@ -303,7 +292,7 @@ export const routeForTimeline = ({ export const routeForWorkers = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/workers`, {}); + return `${routeForWorkflow(parameters)}/workers`; }; export const routeForWorkerDeployments = ({ @@ -311,7 +300,9 @@ export const routeForWorkerDeployments = ({ }: { namespace: string; }): ResolvedPathname => { - return resolve('/namespaces/[namespace]/worker-deployments', { namespace }); + return resolve('/namespaces/[namespace]/worker-deployments', { + namespace, + }); }; export const routeForWorkerDeployment = ({ @@ -337,19 +328,16 @@ export const routeForWorkerDeploymentVersion = ({ deployment: string; version: string; }): ResolvedPathname => { - return resolve( - `${routeForWorkerDeployment({ - namespace, - deployment, - })}/version/[version]`, - { version }, - ); + return `${routeForWorkerDeployment({ + namespace, + deployment, + })}/version/${version}`; }; export const routeForRelationships = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/relationships`, {}); + return `${routeForWorkflow(parameters)}/relationships`; }; export const routeForTaskQueue = ( @@ -357,60 +345,57 @@ export const routeForTaskQueue = ( ): ResolvedPathname => { const queue = encodeURIForSvelte(parameters.queue); - return resolve( - `${routeForNamespace({ - namespace: parameters.namespace, - })}/task-queues/[queue]`, - { queue }, - ); + return `${routeForNamespace({ + namespace: parameters.namespace, + })}/task-queues/${queue}`; }; export const routeForCallStack = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/call-stack`, {}); + return `${routeForWorkflow(parameters)}/call-stack`; }; export const routeForWorkflowQuery = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/query`, {}); + return `${routeForWorkflow(parameters)}/query`; }; export const routeForUserMetadata = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/user-metadata`, {}); + return `${routeForWorkflow(parameters)}/user-metadata`; }; export const routeForWorkflowSearchAttributes = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/search-attributes`, {}); + return `${routeForWorkflow(parameters)}/search-attributes`; }; export const routeForWorkflowMemo = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/memo`, {}); + return `${routeForWorkflow(parameters)}/memo`; }; export const routeForWorkflowUpdate = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/update`, {}); + return `${routeForWorkflow(parameters)}/update`; }; export const routeForPendingActivities = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/pending-activities`, {}); + return `${routeForWorkflow(parameters)}/pending-activities`; }; export const routeForNexusLinks = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/nexus-links`, {}); + return `${routeForWorkflow(parameters)}/nexus-links`; }; export const routeForAuthentication = ( @@ -472,7 +457,9 @@ export const routeForBatchOperations = ({ }: { namespace: string; }): ResolvedPathname => { - return resolve('/namespaces/[namespace]/batch-operations', { namespace }); + return resolve('/namespaces/[namespace]/batch-operations', { + namespace, + }); }; export const routeForBatchOperation = ({ diff --git a/src/routes/(app)/nexus/[id]/edit/+page.svelte b/src/routes/(app)/nexus/[id]/edit/+page.svelte index c79737d025..f4c44f50bb 100644 --- a/src/routes/(app)/nexus/[id]/edit/+page.svelte +++ b/src/routes/(app)/nexus/[id]/edit/+page.svelte @@ -1,6 +1,6 @@ {#if endpoint}
- + {translate('nexus.back-to-endpoint')}
{/if}