From 72fb178e5c75ac053e5f0e8be107d79f75b16f16 Mon Sep 17 00:00:00 2001 From: Lina Jodoin Date: Thu, 15 Jan 2026 15:20:51 -0800 Subject: [PATCH 1/5] Use CountSchedules for counting schedules --- src/lib/services/workflow-counts.ts | 26 +++++++++----------------- src/lib/types/api.ts | 2 +- src/lib/types/workflows.ts | 5 +++++ src/lib/utilities/route-for-api.ts | 1 + 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/lib/services/workflow-counts.ts b/src/lib/services/workflow-counts.ts index 36d93c4c81..07bd40565e 100644 --- a/src/lib/services/workflow-counts.ts +++ b/src/lib/services/workflow-counts.ts @@ -1,4 +1,7 @@ -import type { CountWorkflowExecutionsResponse } from '$lib/types/workflows'; +import type { + CountSchedulesResponse, + CountWorkflowExecutionsResponse, +} from '$lib/types/workflows'; import { requestFromAPI } from '$lib/utilities/request-from-api'; import { routeForApi } from '$lib/utilities/route-for-api'; import { TASK_FAILURES_QUERY } from '$lib/utilities/workflow-task-failures'; @@ -73,23 +76,12 @@ export const fetchScheduleCount = async ({ namespace: string; query?: string; }): Promise => { - const scheduleFixedQuery = - 'TemporalNamespaceDivision="TemporalScheduler" AND ExecutionStatus="Running"'; - - const fullQuery = query - ? `${scheduleFixedQuery} AND ${query}` - : scheduleFixedQuery; - const countRoute = routeForApi('workflows.count', { + const countRoute = routeForApi('schedules.count', { namespace, }); - const { count } = await requestFromAPI( - countRoute, - { - params: { - query: fullQuery, - }, - notifyOnError: false, - }, - ); + const { count } = await requestFromAPI(countRoute, { + params: query ? { query } : {}, + notifyOnError: false, + }); return count ?? '0'; }; diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index 480da8c8be..c008ac21f1 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -44,7 +44,7 @@ export type ParameterlessAPIRoutePath = | 'nexus-endpoints' | 'namespaces'; export type WorkerAPIRoutePath = 'worker-task-reachability'; -export type SchedulesAPIRoutePath = 'schedules'; +export type SchedulesAPIRoutePath = 'schedules' | 'schedules.count'; export type ScheduleAPIRoutePath = | 'schedule' | 'schedule.patch' diff --git a/src/lib/types/workflows.ts b/src/lib/types/workflows.ts index 9bca54e37b..67f724644e 100644 --- a/src/lib/types/workflows.ts +++ b/src/lib/types/workflows.ts @@ -54,6 +54,11 @@ export type CountWorkflowExecutionsResponse = { groups?: { count: string; groupValues: Payloads }[]; }; +export type CountSchedulesResponse = { + count?: string; + groups?: { count: string; groupValues: Payloads }[]; +}; + export type WorkflowExecutionConfig = Replace< import('$lib/types').WorkflowExecutionConfig, { diff --git a/src/lib/utilities/route-for-api.ts b/src/lib/utilities/route-for-api.ts index 0fc28685e1..1c576b8974 100644 --- a/src/lib/utilities/route-for-api.ts +++ b/src/lib/utilities/route-for-api.ts @@ -141,6 +141,7 @@ export function pathForApi( 'schedule.patch': `/namespaces/${parameters?.namespace}/schedules/${parameters?.scheduleId}/patch`, 'schedule.edit': `/namespaces/${parameters?.namespace}/schedules/${parameters?.scheduleId}/update`, schedules: `/namespaces/${parameters?.namespace}/schedules`, + 'schedules.count': `/namespaces/${parameters?.namespace}/schedule-count`, settings: '/settings', 'task-queue': `/namespaces/${parameters?.namespace}/task-queues/${parameters?.queue}`, 'task-queue.compatibility': `/namespaces/${parameters?.namespace}/task-queues/${parameters?.queue}/worker-build-id-compatibility`, From 3d9f224f22d9868f1ba9186695d6f1c74a7b0573 Mon Sep 17 00:00:00 2001 From: Lina Jodoin Date: Wed, 28 Jan 2026 16:07:01 -0800 Subject: [PATCH 2/5] add legacy fallback --- src/lib/services/workflow-counts.ts | 43 +++++++++++++++++++++++------ src/lib/utilities/handle-error.ts | 4 +++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/lib/services/workflow-counts.ts b/src/lib/services/workflow-counts.ts index 07bd40565e..3bf2d68eb6 100644 --- a/src/lib/services/workflow-counts.ts +++ b/src/lib/services/workflow-counts.ts @@ -2,6 +2,7 @@ import type { CountSchedulesResponse, CountWorkflowExecutionsResponse, } from '$lib/types/workflows'; +import { isNotImplemented } from '$lib/utilities/handle-error'; import { requestFromAPI } from '$lib/utilities/request-from-api'; import { routeForApi } from '$lib/utilities/route-for-api'; import { TASK_FAILURES_QUERY } from '$lib/utilities/workflow-task-failures'; @@ -69,6 +70,27 @@ export const fetchWorkflowCountByExecutionStatus = async ({ return { count: count ?? '0', groups }; }; +// Uses the API in a private/unsupported way that will stop working in a future server release. +const fetchScheduleCountLegacy = async ( + namespace: string, + query?: string, +): Promise => { + const scheduleFixedQuery = + 'TemporalNamespaceDivision="TemporalScheduler" AND ExecutionStatus="Running"'; + const fullQuery = query + ? `${scheduleFixedQuery} AND ${query}` + : scheduleFixedQuery; + const countRoute = routeForApi('workflows.count', { namespace }); + const { count } = await requestFromAPI( + countRoute, + { + params: { query: fullQuery }, + notifyOnError: false, + }, + ); + return count ?? '0'; +}; + export const fetchScheduleCount = async ({ namespace, query, @@ -76,12 +98,17 @@ export const fetchScheduleCount = async ({ namespace: string; query?: string; }): Promise => { - const countRoute = routeForApi('schedules.count', { - namespace, - }); - const { count } = await requestFromAPI(countRoute, { - params: query ? { query } : {}, - notifyOnError: false, - }); - return count ?? '0'; + try { + const countRoute = routeForApi('schedules.count', { namespace }); + const { count } = await requestFromAPI(countRoute, { + params: query ? { query } : {}, + notifyOnError: false, + }); + return count ?? '0'; + } catch (error: unknown) { + if (isNotImplemented(error)) { + return fetchScheduleCountLegacy(namespace, query); + } + throw error; + } }; diff --git a/src/lib/utilities/handle-error.ts b/src/lib/utilities/handle-error.ts index f108d7ad13..0fe808465a 100644 --- a/src/lib/utilities/handle-error.ts +++ b/src/lib/utilities/handle-error.ts @@ -78,6 +78,10 @@ export const isForbidden = (error: unknown): error is TemporalAPIError => { return hasStatusCode(error, 403); }; +export const isNotImplemented = (error: unknown): error is TemporalAPIError => { + return hasStatusCode(error, 501); +}; + const hasStatusCode = ( error: unknown, statusCode: number | string, From c73d186b446678c2b83492e12adc1f03cbcf2f01 Mon Sep 17 00:00:00 2001 From: Lina Jodoin Date: Mon, 2 Feb 2026 13:52:43 -0800 Subject: [PATCH 3/5] fix integ tests --- tests/integration/schedule-edit.spec.ts | 8 +++++++- tests/integration/schedules-list.spec.ts | 6 +++--- tests/test-utilities/mock-apis.ts | 9 +++++++-- tests/test-utilities/mocks/schedules-count.ts | 10 ++++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 tests/test-utilities/mocks/schedules-count.ts diff --git a/tests/integration/schedule-edit.spec.ts b/tests/integration/schedule-edit.spec.ts index adc03257d3..c83b841c89 100644 --- a/tests/integration/schedule-edit.spec.ts +++ b/tests/integration/schedule-edit.spec.ts @@ -1,6 +1,10 @@ import { expect, test } from '@playwright/test'; -import { mockScheduleApi, mockSchedulesApis } from '~/test-utilities/mock-apis'; +import { + mockScheduleApi, + mockSchedulesApis, + SCHEDULES_COUNT_API, +} from '~/test-utilities/mock-apis'; const schedulesUrl = '/namespaces/default/schedules'; const scheduleEditUrl = '/namespaces/default/schedules/test-schedule/edit'; @@ -14,6 +18,8 @@ test.describe('Schedules List with schedules', () => { test('selects schedule and edits', async ({ page }) => { await page.goto(schedulesUrl); + await page.waitForResponse(SCHEDULES_COUNT_API); + const createButton = page.getByTestId('create-schedule'); await expect(createButton).toBeEnabled(); diff --git a/tests/integration/schedules-list.spec.ts b/tests/integration/schedules-list.spec.ts index 37682cbcf4..2ca439f90c 100644 --- a/tests/integration/schedules-list.spec.ts +++ b/tests/integration/schedules-list.spec.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { mockSchedulesApis, - WORKFLOWS_COUNT_API, + SCHEDULES_COUNT_API, } from '~/test-utilities/mock-apis'; const schedulesUrl = '/namespaces/default/schedules'; @@ -17,7 +17,7 @@ test.describe('Schedules List with no schedules', () => { }) => { await page.goto(schedulesUrl); - await page.waitForResponse(WORKFLOWS_COUNT_API); + await page.waitForResponse(SCHEDULES_COUNT_API); const namespace = await page.locator('h1').innerText(); expect(namespace).toBe('0 Schedules'); @@ -36,7 +36,7 @@ test.describe('Schedules List with schedules', () => { }) => { await page.goto(schedulesUrl); - await page.waitForResponse(WORKFLOWS_COUNT_API); + await page.waitForResponse(SCHEDULES_COUNT_API); const namespace = await page.locator('h1').innerText(); expect(namespace).toBe('15 Schedules'); diff --git a/tests/test-utilities/mock-apis.ts b/tests/test-utilities/mock-apis.ts index fb8db4a082..98ffd37267 100644 --- a/tests/test-utilities/mock-apis.ts +++ b/tests/test-utilities/mock-apis.ts @@ -13,6 +13,7 @@ import { import { mockNamespaceApi } from './mocks/namespace'; import { mockNamespacesApi, NAMESPACES_API } from './mocks/namespaces'; import { mockSchedulesApi } from './mocks/schedules'; +import { mockSchedulesCountApi } from './mocks/schedules-count'; import { mockSearchAttributesApi } from './mocks/search-attributes'; import { mockSettingsApi, SETTINGS_API } from './mocks/settings'; import { mockSystemInfoApi } from './mocks/system-info'; @@ -40,6 +41,10 @@ export { SEARCH_ATTRIBUTES_API, } from './mocks/search-attributes'; export { mockScheduleApi, SCHEDULE_API } from './mocks/schedules'; +export { + mockSchedulesCountApi, + SCHEDULES_COUNT_API, +} from './mocks/schedules-count'; export { mockWorkflowsApi, WORKFLOWS_API } from './mocks/workflows'; export { mockWorkflowApi, WORKFLOW_API } from './mocks/workflow'; export { @@ -78,7 +83,7 @@ export const mockWorkflowsApis = (page: Page) => { export const mockSchedulesApis = ( page: Page, empty = false, - emptyWorkflowsCount = false, + emptySchedulesCount = false, customSearchAttributes?: Partial, ) => { return Promise.all([ @@ -86,7 +91,7 @@ export const mockSchedulesApis = ( mockNamespaceApis(page), mockSearchAttributesApi(page, customSearchAttributes), mockSchedulesApi(page, empty), - mockWorkflowsCountApi(page, emptyWorkflowsCount), + mockSchedulesCountApi(page, emptySchedulesCount), ]); }; diff --git a/tests/test-utilities/mocks/schedules-count.ts b/tests/test-utilities/mocks/schedules-count.ts new file mode 100644 index 0000000000..bf909ea3d7 --- /dev/null +++ b/tests/test-utilities/mocks/schedules-count.ts @@ -0,0 +1,10 @@ +import type { Page } from '@playwright/test'; + +export const SCHEDULES_COUNT_API = + /\/api\/v1\/namespaces\/[^/]+\/schedule-count(\?.*)?$/; + +export const mockSchedulesCountApi = (page: Page, empty = false) => { + return page.route(SCHEDULES_COUNT_API, (route) => { + return route.fulfill({ json: { count: empty ? '0' : '15' } }); + }); +}; From 507bcca4d3efa8d99a0389eb805672d2bf4d5cff Mon Sep 17 00:00:00 2001 From: Lina Jodoin Date: Mon, 2 Feb 2026 16:01:01 -0800 Subject: [PATCH 4/5] trigger CI From 451bb09660def81864cfddf505d721c9ad2a3bc9 Mon Sep 17 00:00:00 2001 From: Lina Jodoin Date: Wed, 11 Feb 2026 15:55:52 -0800 Subject: [PATCH 5/5] Fall back to legacy schedule count on 404 Older servers that predate the CountSchedules API (added in v1.62.0) return 404 instead of 501. Treat both status codes as a signal to use the legacy workflow-count-based path. --- src/lib/services/workflow-counts.ts | 4 ++-- src/lib/utilities/handle-error.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/services/workflow-counts.ts b/src/lib/services/workflow-counts.ts index 3bf2d68eb6..920ff2cfe4 100644 --- a/src/lib/services/workflow-counts.ts +++ b/src/lib/services/workflow-counts.ts @@ -2,7 +2,7 @@ import type { CountSchedulesResponse, CountWorkflowExecutionsResponse, } from '$lib/types/workflows'; -import { isNotImplemented } from '$lib/utilities/handle-error'; +import { isNotFound, isNotImplemented } from '$lib/utilities/handle-error'; import { requestFromAPI } from '$lib/utilities/request-from-api'; import { routeForApi } from '$lib/utilities/route-for-api'; import { TASK_FAILURES_QUERY } from '$lib/utilities/workflow-task-failures'; @@ -106,7 +106,7 @@ export const fetchScheduleCount = async ({ }); return count ?? '0'; } catch (error: unknown) { - if (isNotImplemented(error)) { + if (isNotImplemented(error) || isNotFound(error)) { return fetchScheduleCountLegacy(namespace, query); } throw error; diff --git a/src/lib/utilities/handle-error.ts b/src/lib/utilities/handle-error.ts index 0fe808465a..c5a246a389 100644 --- a/src/lib/utilities/handle-error.ts +++ b/src/lib/utilities/handle-error.ts @@ -78,6 +78,10 @@ export const isForbidden = (error: unknown): error is TemporalAPIError => { return hasStatusCode(error, 403); }; +export const isNotFound = (error: unknown): error is TemporalAPIError => { + return hasStatusCode(error, 404); +}; + export const isNotImplemented = (error: unknown): error is TemporalAPIError => { return hasStatusCode(error, 501); };