diff --git a/.gitignore b/.gitignore index d8f38b5409..751271e0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ go.work.sum .claude/settings.local.json .claude/* !.claude/skills/ -.sisyphus/* \ No newline at end of file +.omc +.sisyphus/* diff --git a/src/lib/components/lines-and-dots/workflow-details.svelte b/src/lib/components/lines-and-dots/workflow-details.svelte index c5c7e03698..a07d1fa818 100644 --- a/src/lib/components/lines-and-dots/workflow-details.svelte +++ b/src/lib/components/lines-and-dots/workflow-details.svelte @@ -305,7 +305,7 @@ {/if} {#if sdk && sdkVersion} - SDK + {translate('workflows.sdk')} diff --git a/src/lib/components/shared-search-attribute-filter/filter-bar.svelte b/src/lib/components/shared-search-attribute-filter/filter-bar.svelte new file mode 100644 index 0000000000..f1d5dd0e85 --- /dev/null +++ b/src/lib/components/shared-search-attribute-filter/filter-bar.svelte @@ -0,0 +1,56 @@ + + +
+
+
+ + +
+
+ +
+
+ {#if viewManualQuery} + + {/if} +
diff --git a/src/lib/components/standalone-activities/activities-summary-filter-bar/dropdown-filter-list.svelte b/src/lib/components/shared-search-attribute-filter/filter-list.svelte similarity index 50% rename from src/lib/components/standalone-activities/activities-summary-filter-bar/dropdown-filter-list.svelte rename to src/lib/components/shared-search-attribute-filter/filter-list.svelte index 6db1c9e76c..6a5050decd 100644 --- a/src/lib/components/standalone-activities/activities-summary-filter-bar/dropdown-filter-list.svelte +++ b/src/lib/components/shared-search-attribute-filter/filter-list.svelte @@ -1,47 +1,59 @@
- - + +
diff --git a/src/lib/components/standalone-activities/activities-summary-filter-bar/manual-query.svelte b/src/lib/components/shared-search-attribute-filter/manual-query.svelte similarity index 75% rename from src/lib/components/standalone-activities/activities-summary-filter-bar/manual-query.svelte rename to src/lib/components/shared-search-attribute-filter/manual-query.svelte index 1221aa6f23..701649a394 100644 --- a/src/lib/components/standalone-activities/activities-summary-filter-bar/manual-query.svelte +++ b/src/lib/components/shared-search-attribute-filter/manual-query.svelte @@ -1,4 +1,5 @@
-
-{/snippet} - -
-
-
- - -
- {@render actionToggleButtons()} -
- {#if viewManualQuery} - - {/if} -
+ diff --git a/src/lib/components/task-queue/worker-info.svelte b/src/lib/components/task-queue/worker-info.svelte new file mode 100644 index 0000000000..1eb54133cf --- /dev/null +++ b/src/lib/components/task-queue/worker-info.svelte @@ -0,0 +1,364 @@ + + +
+
+ +

+ +

+
+ + + + {translate('workflows.last-heartbeat')} + + + + {translate('common.start')} + + + + + {translate('common.task-queue')} + + + + {translate('workers.sdk')} + + + + + + +
+
+ {@render taskSlotCard( + translate('common.workflows-plural', { count: 1 }), + heartbeat?.workflowTaskSlotsInfo, + heartbeat?.workflowPollerInfo, + )} + {@render taskSlotCard( + translate('common.activities-plural', { count: 1 }), + heartbeat?.activityTaskSlotsInfo, + heartbeat?.activityPollerInfo, + )} + {@render taskSlotCard( + translate('workers.nexus-tasks'), + heartbeat?.nexusTaskSlotsInfo, + heartbeat?.nexusPollerInfo, + )} + {@render taskSlotCard( + translate('workers.local-activities'), + heartbeat?.localActivitySlotsInfo, + null, + )} +
+ +
+ {@render hostInfo()} + {@render workflowCache()} + {@render diagnostics()} +
+
+
+ +{#snippet taskSlotCard( + title: string, + slots: WorkerSlotsInfo, + poller: WorkerPollerInfo, +)} + +
+

{title}

+ {slots.slotSupplierKind} +
+ +
+
+
+ {translate('workers.slots')} +
+
+

+ {slots.currentUsedSlots ?? 0} +

+

+ {#if slots.currentAvailableSlots} + {slots.currentAvailableSlots - slots.currentUsedSlots || 0} + {:else} + - + {/if} +

+
+
+

{translate('workers.used')}

+

+ {#if slots.currentAvailableSlots} + {translate('workers.available-out-of', { + count: slots.currentAvailableSlots, + })} + {:else} + {translate('workers.none-available')} + {/if} +

+
+
+ +
+
+ {translate('workers.tasks-processed')} +
+ + {(slots.totalProcessedTasks ?? 0).toLocaleString()} + +
+ + {#if poller} +
+
+ {translate('workers.poller')} + + {poller.isAutoscaling ? 'Autoscaling' : 'Manual'} + +
+ + {poller.currentPollers ?? 0} + +
+ {#if poller.lastSuccessfulPollTime} + {translate('workers.last-poll')} + + {:else} + {translate('workers.no-activity')} + {/if} +
+
+ {/if} +
+
+{/snippet} + +{#snippet goDependencyWarning()} + +

{translate('workers.go-dependency-warning-description')}

+ {translate('workers.go-dependency-warning-link')} +
+{/snippet} + +{#snippet hostUsage()} + +
+
+ + + {translate('workers.cpu-usage')} + + {heartbeat?.hostInfo?.currentHostCpuUsage.toFixed(0)}% +
+
+
+
+
+
+
+ + + {translate('workers.memory-usage')} + + {heartbeat?.hostInfo?.currentHostMemUsage.toFixed(0)}% +
+
+
+
+
+ {#if goDependencyPotentiallyMissing} + {@render goDependencyWarning()} + {/if} +
+{/snippet} + +{#snippet hostInfo()} +
+ +

+ {translate('workers.host-info')} +

+
+
{translate('workers.host-name')}
+
{heartbeat?.hostInfo?.hostName}
+ +
{translate('workers.process-id')}
+
{heartbeat?.hostInfo?.processId}
+ +
+ {translate('workers.worker-grouping')} +
+
{heartbeat?.hostInfo?.workerGroupingKey}
+
+
+ {@render hostUsage()} +
+{/snippet} + +{#snippet workflowCache()} + +

+ {translate('workers.workflow-cache')} +

+
+
+ + {currentStickyCacheSize.toLocaleString()} + +
+ {translate('workers.cache-size')} +
+
+
+ + {cacheHitRate}% + +
+ {translate('workers.cache-hits')} +
+
+
+
+ - +
+ {translate('workers.active-thread-count')} +
+
+
+{/snippet} + +{#snippet diagnostics()} + +

+ {translate('workers.diagnostics')} +

+
+
+ - % +
+ {translate('workers.poll-success-rate')} +
+
+
+ {translate('common.none')} +
+ {translate('workers.rate-limit')} +
+
+
+
+{/snippet} diff --git a/src/lib/components/task-queue/worker-insights.svelte b/src/lib/components/task-queue/worker-insights.svelte new file mode 100644 index 0000000000..aba0638192 --- /dev/null +++ b/src/lib/components/task-queue/worker-insights.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/components/task-queue/worker-totals.svelte b/src/lib/components/task-queue/worker-totals.svelte new file mode 100644 index 0000000000..d035fed777 --- /dev/null +++ b/src/lib/components/task-queue/worker-totals.svelte @@ -0,0 +1,65 @@ + + +
+ +
+ {translate('workers.active-workers')} +
+
{active}
+
+ + + {translate('workflows.running')} + +
+
+ +
+ {translate('workers.inactive-workers')} +
+
{inactive}
+
+ + {translate('common.inactive')} + +
+
+ + +
+ {translate('workers.total-processed-tasks')} +
+
{totalTasks}
+
+ {workflowTasks} + {translate('common.workflows-plural', { count: 1 })} ยท {activityTasks} + {translate('common.activities-plural', { count: 1 })} +
+
+
diff --git a/src/lib/components/worker-table.svelte b/src/lib/components/worker-table.svelte index 52737720ca..2af0455b96 100644 --- a/src/lib/components/worker-table.svelte +++ b/src/lib/components/worker-table.svelte @@ -178,7 +178,7 @@ {:else} - + {/each} diff --git a/src/lib/components/workers/no-workers-polling-alert.svelte b/src/lib/components/workers/no-workers-polling-alert.svelte new file mode 100644 index 0000000000..ce450c7c87 --- /dev/null +++ b/src/lib/components/workers/no-workers-polling-alert.svelte @@ -0,0 +1,34 @@ + + + diff --git a/src/lib/components/workers/worker-status.svelte b/src/lib/components/workers/worker-status.svelte new file mode 100644 index 0000000000..dab8c099f1 --- /dev/null +++ b/src/lib/components/workers/worker-status.svelte @@ -0,0 +1,46 @@ + + +
+ + {label[status]} + {#if isRunning} + + {/if} + +
diff --git a/src/lib/components/workers/workers-table/task-queue-workers-table.svelte b/src/lib/components/workers/workers-table/task-queue-workers-table.svelte new file mode 100644 index 0000000000..96fe3fc2bd --- /dev/null +++ b/src/lib/components/workers/workers-table/task-queue-workers-table.svelte @@ -0,0 +1,33 @@ + + +{#if useFallback} + {#await getPollers({ queue: taskQueue, namespace }) then workers} + + {/await} +{:else} + +{/if} diff --git a/src/lib/components/workers/workers-table/workers-table-cell.svelte b/src/lib/components/workers/workers-table/workers-table-cell.svelte new file mode 100644 index 0000000000..7ce99217e0 --- /dev/null +++ b/src/lib/components/workers/workers-table/workers-table-cell.svelte @@ -0,0 +1,92 @@ + + + + {#if href} + {value} + {:else if children} + {@render children?.()} + {:else} + {value} + {/if} + + diff --git a/src/lib/components/workers/workers-table/workers-table-row.svelte b/src/lib/components/workers/workers-table/workers-table-row.svelte new file mode 100644 index 0000000000..2c73ec3326 --- /dev/null +++ b/src/lib/components/workers/workers-table/workers-table-row.svelte @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + diff --git a/src/lib/components/workers/workers-table/workers-table-with-search.svelte b/src/lib/components/workers/workers-table/workers-table-with-search.svelte new file mode 100644 index 0000000000..1094ec88f8 --- /dev/null +++ b/src/lib/components/workers/workers-table/workers-table-with-search.svelte @@ -0,0 +1,20 @@ + + +{#key query} + +{/key} diff --git a/src/lib/components/workers/workers-table/workers-table.svelte b/src/lib/components/workers/workers-table/workers-table.svelte new file mode 100644 index 0000000000..64298ddac0 --- /dev/null +++ b/src/lib/components/workers/workers-table/workers-table.svelte @@ -0,0 +1,58 @@ + + + + + {translate('workers.workers')} + + + + {#each columns as { label } (label)} + {label} + {/each} + + {#each visibleItems as worker (worker.workerHeartbeat.workerInstanceKey)} + + {/each} + + + + + diff --git a/src/lib/components/workflow/workflow-call-stack-error.svelte b/src/lib/components/workflow/workflow-call-stack-error.svelte deleted file mode 100644 index 9cb6992968..0000000000 --- a/src/lib/components/workflow/workflow-call-stack-error.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - -{#if runningWithNoWorkers} -
- - {translate('workflows.workflow-error-no-workers-description', { - taskQueue: workflow?.taskQueue ?? '', - })} - -
-{/if} diff --git a/src/lib/components/workflow/workflow-summary.svelte b/src/lib/components/workflow/workflow-summary.svelte index 6c69b7135e..0d4939ec35 100644 --- a/src/lib/components/workflow/workflow-summary.svelte +++ b/src/lib/components/workflow/workflow-summary.svelte @@ -10,7 +10,7 @@ workflowSummaryViewOpen, } from '$lib/stores/workflow-run'; import { formatDistanceAbbreviated } from '$lib/utilities/format-time'; - import { routeForWorkers } from '$lib/utilities/route-for'; + import { routeForWorkflowWorkers } from '$lib/utilities/route-for'; $: ({ workflow } = $workflowRun); $: elapsedTime = formatDistanceAbbreviated({ @@ -62,7 +62,7 @@
.copy-or-filter { - @apply absolute bottom-0 right-0 top-0 inline-flex gap-2 px-2; + @apply absolute bottom-0 right-0 top-0 inline-flex gap-1 px-1; } .copy-or-filter-button { - @apply surface-primary relative top-[50%] h-fit translate-y-[-50%] rounded-full p-1 text-primary hover:surface-inverse; + @apply surface-primary relative top-[40%] h-7 w-7 translate-y-[-40%] rounded-full p-0.5 text-primary hover:surface-inverse; } .filtered { diff --git a/src/lib/holocene/icon/paths.ts b/src/lib/holocene/icon/paths.ts index 9a4b81c346..56b32118cf 100644 --- a/src/lib/holocene/icon/paths.ts +++ b/src/lib/holocene/icon/paths.ts @@ -83,6 +83,7 @@ import lock from './svg/lock.svelte'; import logout from './svg/logout.svelte'; import marker from './svg/marker.svelte'; import merge from './svg/merge.svelte'; +import microchip from './svg/microchip.svelte'; import microsoft from './svg/microsoft.svelte'; import minimize from './svg/minimize.svelte'; import moon from './svg/moon.svelte'; @@ -227,6 +228,7 @@ export const icons = { logout, marker, merge, + microchip, microsoft, minimize, moon, diff --git a/src/lib/holocene/icon/svg/microchip.svelte b/src/lib/holocene/icon/svg/microchip.svelte new file mode 100644 index 0000000000..adb80b665a --- /dev/null +++ b/src/lib/holocene/icon/svg/microchip.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/lib/i18n/locales/en/common.ts b/src/lib/i18n/locales/en/common.ts index a65ec46161..382d12d964 100644 --- a/src/lib/i18n/locales/en/common.ts +++ b/src/lib/i18n/locales/en/common.ts @@ -108,6 +108,8 @@ export const Strings = { workflows: 'Workflows', 'workflows-plural_one': 'Workflow', 'workflows-plural_other': 'Workflows', + 'activities-plural_one': 'Activity', + 'activities-plural_other': 'Activities', schedules: 'Schedules', 'schedules-plural_one': '{{ count }} Schedule', 'schedules-plural_other': '{{ count }} Schedules', diff --git a/src/lib/i18n/locales/en/workers.ts b/src/lib/i18n/locales/en/workers.ts index 887059633e..e4135e7491 100644 --- a/src/lib/i18n/locales/en/workers.ts +++ b/src/lib/i18n/locales/en/workers.ts @@ -1,7 +1,11 @@ export const Namespace = 'workers' as const; export const Strings = { + worker: 'Worker', workers: 'Workers', + 'worker-views': 'Worker Views', + 'view-all-workers': 'View All Workers', + 'back-to-workers': 'Back to Workers', version: 'Version', versioning: 'Versioning', retirability: 'Retirability', @@ -10,6 +14,14 @@ export const Strings = { 'redirect-rules': 'Redirect Rules', default: 'Default', overall: 'Overall', + 'host-name': 'Host Name', + identity: 'Identity', + instance: 'Instance', + 'task-queue': 'Task Queue', + status: 'Status', + sdk: 'Worker SDK', + 'empty-state-title': 'No Workers Found', + 'error-message-fetching': 'An error occurred while fetching workers.', 'compatible-build-ids': 'Compatible Build IDs', 'version-sets': 'Version Sets', 'no-version-sets-found': 'No Version Sets found', @@ -26,4 +38,34 @@ export const Strings = { 'Viewing workers for pinned Build ID. Go to the Task Queue page to view all workers.', 'viewing-auto-upgrade-build-ids': 'Viewing workers for current and ramping Build IDs. Go to the Task Queue page to view all workers.', + 'active-workers': 'Active Workers', + 'inactive-workers': 'Inactive Workers', + 'total-processed-tasks': 'Total Processed Tasks', + 'nexus-tasks': 'Nexus Tasks', + 'local-activities': 'Local Activities', + slots: 'Slots', + used: 'Used', + 'available-out-of': 'Available out of {{count}}', + 'none-available': 'None Available', + 'tasks-processed': 'Tasks Processed', + 'last-poll': 'Last Poll', + 'no-activity': 'No Activity', + 'host-info': 'Host Info', + 'process-id': 'Process ID', + 'worker-grouping': 'Worker Grouping', + 'cpu-usage': 'CPU Usage', + 'memory-usage': 'Memory Usage', + 'workflow-cache': 'Workflow Cache', + 'cache-size': 'Cache size', + 'cache-hits': 'Cache hits', + 'active-thread-count': 'Active thread count', + diagnostics: 'Diagnostics', + 'poll-success-rate': 'Poll success rate', + 'rate-limit': 'Rate limit', + 'troubleshooting-workers-link': 'Troubleshooting workers', + poller: 'Poller', + 'go-dependency-warning': 'Potential missing dependency', + 'go-dependency-warning-description': + 'This worker is not showing any usage. You may need to enable a dependency for the Go SDK in order to see this data.', + 'go-dependency-warning-link': 'Learn more.', } as const; diff --git a/src/lib/i18n/locales/en/workflows.ts b/src/lib/i18n/locales/en/workflows.ts index d06ea88181..27494b8c70 100644 --- a/src/lib/i18n/locales/en/workflows.ts +++ b/src/lib/i18n/locales/en/workflows.ts @@ -155,9 +155,11 @@ export const Strings = { 'workflow-404-title': 'This is not the Workflow you are looking for', 'workflow-error-title': 'We are having technical difficulties retrieving this Workflow', - 'workflow-error-no-workers-title': 'No Workers Running', + 'workflow-error-no-workers-title': 'No workers polling', 'workflow-error-no-workers-description': - 'There are no Workers polling the {{taskQueue}} Task Queue.', + 'There are no workers polling the {{taskQueue}} task queue.', + 'workers-alert-description': + 'Check your deployment or orchestration system (Kubernetes, ECS, etc.). Review worker logs for crash information. Restart or redeploy workers.', 'workflow-error-no-compatible-workers-title': 'No Compatible Workers Running', 'workflow-error-no-compatible-workers-description': 'There are no compatible Workers polling the {{taskQueue}} Task Queue.', @@ -203,7 +205,6 @@ export const Strings = { 'workflow-task-handler': 'Workflow Task Handler', 'activity-handler': 'Activity Handler', 'nexus-handler': 'Nexus Handler', - 'workers-empty-state': 'No Workers Found', 'call-stack-empty-state': 'No Call Stacks Found', 'no-workers-failure-message': 'This will fail if you have no workers running.', @@ -348,4 +349,5 @@ export const Strings = { 'timeline-minimized': 'Timeline and Event History are collapsed to minimized height', 'timeline-expanded': 'Timeline and Event History are expanded to full height', + sdk: 'Workflow SDK', } as const; diff --git a/src/lib/layouts/workflow-header.svelte b/src/lib/layouts/workflow-header.svelte index c601ae4b3f..ea9ab871cd 100644 --- a/src/lib/layouts/workflow-header.svelte +++ b/src/lib/layouts/workflow-header.svelte @@ -6,7 +6,6 @@ import CodecServerErrorBanner from '$lib/components/codec-server-error-banner.svelte'; import WorkflowDetails from '$lib/components/lines-and-dots/workflow-details.svelte'; import { timestamp } from '$lib/components/timestamp.svelte'; - import WorkflowCallStackError from '$lib/components/workflow/workflow-call-stack-error.svelte'; import WorkflowActions from '$lib/components/workflow-actions.svelte'; import WorkflowStatus from '$lib/components/workflow-status.svelte'; import Alert from '$lib/holocene/alert.svelte'; @@ -40,11 +39,11 @@ routeForRelationships, routeForTimeline, routeForUserMetadata, - routeForWorkers, routeForWorkflowMemo, routeForWorkflowQuery, routeForWorkflows, routeForWorkflowSearchAttributes, + routeForWorkflowWorkers, } from '$lib/utilities/route-for'; import { isWorkflowTaskFailure } from '$lib/utilities/workflow-task-failures'; @@ -175,7 +174,6 @@ - {#if cancelInProgress}
diff --git a/src/lib/layouts/workflow-run-layout.svelte b/src/lib/layouts/workflow-run-layout.svelte index 452745e34b..152c7591e0 100644 --- a/src/lib/layouts/workflow-run-layout.svelte +++ b/src/lib/layouts/workflow-run-layout.svelte @@ -8,7 +8,6 @@ import SkeletonWorkflow from '$lib/holocene/skeleton/workflow.svelte'; import { translate } from '$lib/i18n/translate'; import WorkflowHeader from '$lib/layouts/workflow-header.svelte'; - import { Action } from '$lib/models/workflow-actions'; import { fetchAllEvents, throttleRefresh, diff --git a/src/lib/models/worker-status.ts b/src/lib/models/worker-status.ts new file mode 100644 index 0000000000..6e1f2f2b53 --- /dev/null +++ b/src/lib/models/worker-status.ts @@ -0,0 +1,13 @@ +export type WorkerStatus = 'Unspecified' | 'Running' | 'ShuttingDown' | null; + +export type WorkerFilters = readonly (WorkerStatus | 'All')[]; + +export const workerStatuses: readonly WorkerStatus[] = [ + 'Running', + 'ShuttingDown', +] as const; + +export const workerStatusFilters: WorkerFilters = [ + 'All', + ...workerStatuses, +] as const; diff --git a/src/lib/pages/deployments.svelte b/src/lib/pages/deployments.svelte index bbb6f13b19..f3cfe78a81 100644 --- a/src/lib/pages/deployments.svelte +++ b/src/lib/pages/deployments.svelte @@ -3,7 +3,6 @@ import DeploymentTableRow from '$lib/components/deployments/deployment-table-row.svelte'; import Alert from '$lib/holocene/alert.svelte'; - import Badge from '$lib/holocene/badge.svelte'; import EmptyState from '$lib/holocene/empty-state.svelte'; import Link from '$lib/holocene/link.svelte'; import PaginatedTable from '$lib/holocene/table/paginated-table/api-paginated.svelte'; @@ -39,13 +38,6 @@ ]; -
-

- {translate('deployments.worker-deployments')} -

- Public Preview -
- {#key [namespace]} import { page } from '$app/state'; - import WorkerTable from '$lib/components/worker-table.svelte'; - import { getPollers } from '$lib/services/pollers-service'; + import TaskQueueWorkerInsights from '$lib/components/task-queue/worker-insights.svelte'; - const { queue: taskQueue, namespace } = $derived(page.params); + const { queue: taskQueue } = $derived(page.params); -{#await getPollers({ queue: taskQueue, namespace }) then workers} -
-

- {taskQueue} -

- -
-{/await} +
+

Task Queue

+

+ {taskQueue} +

+ +
diff --git a/src/lib/pages/workflow-workers.svelte b/src/lib/pages/workflow-workers.svelte deleted file mode 100644 index 8e94b72878..0000000000 --- a/src/lib/pages/workflow-workers.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
- -

- {translate('common.task-queue')}: - {taskQueue} -

-
-
diff --git a/src/lib/services/worker-service.ts b/src/lib/services/worker-service.ts new file mode 100644 index 0000000000..db70f87f42 --- /dev/null +++ b/src/lib/services/worker-service.ts @@ -0,0 +1,49 @@ +import type { + DescribeWorkerRequest, + DescribeWorkerResponse, + ListWorkersRequest, + ListWorkersResponse, + WorkerInfo, +} from '$lib/types'; +import { requestFromAPI } from '$lib/utilities/request-from-api'; +import { routeForApi } from '$lib/utilities/route-for-api'; + +type PaginatedWorkerListPromise = ( + pageSize: number, + token: string, +) => Promise<{ items: WorkerInfo[]; nextPageToken: string }>; + +export const fetchPaginatedWorkers = async ( + parameters: ListWorkersRequest, + request = fetch, +): Promise => { + return (pageSize = 100, token = '') => { + const route = routeForApi('workers', parameters); + return requestFromAPI(route, { + request, + params: { + maximumPageSize: String(pageSize), + nextPageToken: token, + query: parameters.query, + }, + }).then(({ workersInfo, nextPageToken }) => { + // if (!workersInfo) { + // throw new Error('No workers info in response'); + // } + return { + items: workersInfo ?? [], + nextPageToken: nextPageToken ? String(nextPageToken) : '', + }; + }); + }; +}; + +export async function describeWorker( + parameters: DescribeWorkerRequest, + request = fetch, +): Promise { + const route = routeForApi('worker', parameters); + return await requestFromAPI(route, { + request, + }); +} diff --git a/src/lib/stores/filters.ts b/src/lib/stores/filters.ts index 7eaccadd83..46c125b439 100644 --- a/src/lib/stores/filters.ts +++ b/src/lib/stores/filters.ts @@ -84,6 +84,21 @@ export const activityFilters = writable( updateActivityFilters, ); +const updateWorkerFilters: StartStopNotifier = ( + set, +) => { + return parameters.subscribe(({ query }) => { + if (!query && get(workerFilters).length) { + set([]); + } + }); +}; + +export const workerFilters = writable( + [], + updateWorkerFilters, +); + const updateEventCategoryFilter: StartStopNotifier< EventTypeCategory[] | null > = (set) => { diff --git a/src/lib/stores/search-attributes.ts b/src/lib/stores/search-attributes.ts index a7939e1f01..df21b05633 100644 --- a/src/lib/stores/search-attributes.ts +++ b/src/lib/stores/search-attributes.ts @@ -1,4 +1,4 @@ -import { derived, get, type Readable, writable } from 'svelte/store'; +import { derived, get, readable, type Readable, writable } from 'svelte/store'; import { z } from 'zod/v3'; @@ -191,6 +191,30 @@ export const sortedSearchAttributeOptions: Readable = }); }); +export const workerSearchAttributes: Readable = readable({ + WorkerStatus: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + WorkerInstanceKey: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + WorkerIdentity: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + HostName: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + TaskQueue: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + StartTime: SEARCH_ATTRIBUTE_TYPE.DATETIME, + LastHeartbeatTime: SEARCH_ATTRIBUTE_TYPE.DATETIME, + DeploymentName: SEARCH_ATTRIBUTE_TYPE.DATETIME, + BuildId: SEARCH_ATTRIBUTE_TYPE.DATETIME, + SdkName: SEARCH_ATTRIBUTE_TYPE.DATETIME, + SdkVersion: SEARCH_ATTRIBUTE_TYPE.DATETIME, +}); + +export const workerSearchAttributeOptions: Readable = + derived(workerSearchAttributes, ($workerSearchAttributes) => { + return Object.entries($workerSearchAttributes).map(([key, value]) => { + return { + label: key, + value: key, + type: value, + }; + }); + }); export const activitySearchAttributeOptions: Readable = derived(customSearchAttributeOptions, ($customSearchAttributeOptions) => { return [ diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index 7aa11f7afa..619600928a 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -43,7 +43,10 @@ export type ParameterlessAPIRoutePath = | 'user' | 'nexus-endpoints' | 'namespaces'; -export type WorkerAPIRoutePath = 'worker-task-reachability'; +export type WorkerAPIRoutePath = + | 'worker-task-reachability' + | 'workers' + | 'worker'; export type SchedulesAPIRoutePath = 'schedules' | 'schedules.count'; export type ScheduleAPIRoutePath = | 'schedule' @@ -99,6 +102,7 @@ export type APIRouteParameters = { endpointId: string; deploymentName: string; version: string; + workerInstanceKey: string; }; export type StandaloneActivitiesParameters = Pick< diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 2c977f7d38..cd35cfe894 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -49,6 +49,14 @@ export type PauseWorkflowRequest = temporal.api.workflowservice.v1.IPauseWorkflowExecutionRequest; export type UnpauseWorkflowRequest = temporal.api.workflowservice.v1.IUnpauseWorkflowExecutionRequest; +export type ListWorkersRequest = + temporal.api.workflowservice.v1.IListWorkersRequest; +export type ListWorkersResponse = + temporal.api.workflowservice.v1.IListWorkersResponse; +export type DescribeWorkerRequest = + temporal.api.workflowservice.v1.IDescribeWorkerRequest; +export type DescribeWorkerResponse = + temporal.api.workflowservice.v1.IDescribeWorkerResponse; // api.history @@ -178,6 +186,7 @@ export type PendingNexusOperationState = export type CallbackState = temporal.api.enums.v1.CallbackState; export type VersioningBehavior = temporal.api.enums.v1.VersioningBehavior; export type EventType = temporal.api.enums.v1.EventType; +export type WorkerStatus = temporal.api.enums.v1.WorkerStatus; // temporal.api.enums.v1.ResetReapplyExcludeType export enum ResetReapplyExcludeType { @@ -271,6 +280,18 @@ export type EventLink = temporal.api.common.v1.ILink; // api.failure export type Failure = temporal.api.failure.v1.IFailure; +// api.worker +export type WorkerHostInfo = temporal.api.worker.v1.IWorkerHostInfo & { + workerGroupingKey?: string; +}; +export type WorkerHeartbeat = temporal.api.worker.v1.IWorkerHeartbeat & { + hostInfo?: WorkerHostInfo; +}; +export type WorkerPollerInfo = temporal.api.worker.v1.IWorkerPollerInfo; +export type WorkerSlotsInfo = temporal.api.worker.v1.IWorkerSlotsInfo; +export type WorkerInfo = temporal.api.worker.v1.IWorkerInfo; +export type PluginInfo = temporal.api.worker.v1.PluginInfo; + // google export type Timestamp = google.protobuf.ITimestamp; diff --git a/src/lib/utilities/get-sdk-version.ts b/src/lib/utilities/get-sdk-version.ts index 182ca4f948..a5f99dda4d 100644 --- a/src/lib/utilities/get-sdk-version.ts +++ b/src/lib/utilities/get-sdk-version.ts @@ -2,6 +2,18 @@ import type { WorkflowTaskCompletedEvent } from '$lib/types/events'; import { capitalize } from './format-camel-case'; +export const formatSDKName = (sdkName: string | undefined): string => { + let sdk = ''; + if (!sdkName) return sdk; + + sdk = capitalize(sdkName.split('-')[1]); + if (sdk === 'Dotnet') { + sdk = '.NET'; + } + + return sdk; +}; + export const getSDKandVersion = ( tasks: WorkflowTaskCompletedEvent[], ): { sdk: string; version: string } => { @@ -16,10 +28,7 @@ export const getSDKandVersion = ( const sdkName = sdkMetadata?.sdkName; const sdkVersion = sdkMetadata?.sdkVersion; if (sdkName) { - sdk = capitalize(sdkName.split('-')[1]); - if (sdk === 'Dotnet') { - sdk = '.NET'; - } + sdk = formatSDKName(sdkName); } if (sdkVersion) { version = sdkVersion; diff --git a/src/lib/utilities/route-for-api.ts b/src/lib/utilities/route-for-api.ts index ec198ddbb3..b8e724a400 100644 --- a/src/lib/utilities/route-for-api.ts +++ b/src/lib/utilities/route-for-api.ts @@ -3,6 +3,7 @@ import { get } from 'svelte/store'; import { resolve } from '$app/paths'; import { page } from '$app/stores'; +import type { DescribeWorkerRequest } from '$lib/types'; import type { APIRouteParameters, APIRoutePath, @@ -120,6 +121,7 @@ const encode = ( endpointId: '', deploymentName: '', version: '', + workerInstanceKey: '', }, ); }; @@ -151,6 +153,8 @@ export function pathForApi( 'task-queue.compatibility': `/namespaces/${parameters?.namespace}/task-queues/${parameters?.queue}/worker-build-id-compatibility`, 'task-queue.rules': `/namespaces/${parameters?.namespace}/task-queues/${parameters?.queue}/worker-versioning-rules`, user: '/me', + workers: `/namespaces/${parameters?.namespace}/workers`, + worker: `/namespaces/${parameters?.namespace}/workers/describe/${parameters?.workerInstanceKey}`, 'worker-task-reachability': `/namespaces/${parameters?.namespace}/worker-task-reachability`, 'workflow.terminate': `/namespaces/${parameters?.namespace}/workflows/${parameters?.workflowId}/terminate`, 'workflow.cancel': `/namespaces/${parameters?.namespace}/workflows/${parameters?.workflowId}/cancel`, @@ -211,7 +215,7 @@ export function routeForApi( ): string; export function routeForApi( route: WorkerAPIRoutePath, - parameters: NamespaceRouteParameters, + parameters: DescribeWorkerRequest, shouldEncode?: boolean, ): string; export function routeForApi( diff --git a/src/lib/utilities/route-for.test.ts b/src/lib/utilities/route-for.test.ts index 8204b1b8d3..f77ee3a2a0 100644 --- a/src/lib/utilities/route-for.test.ts +++ b/src/lib/utilities/route-for.test.ts @@ -19,11 +19,11 @@ import { routeForScheduleCreate, routeForSchedules, routeForTaskQueue, - routeForWorkers, routeForWorkflow, routeForWorkflowQuery, routeForWorkflows, routeForWorkflowsWithQuery, + routeForWorkflowWorkers, } from './route-for'; describe('routeFor', () => { @@ -117,7 +117,7 @@ describe('routeFor', () => { }); it('should route to "workers"', () => { - const path = routeForWorkers({ + const path = routeForWorkflowWorkers({ namespace: 'default', workflow: 'abc', run: 'def', diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts index d0ce22f3b8..40a3969978 100644 --- a/src/lib/utilities/route-for.ts +++ b/src/lib/utilities/route-for.ts @@ -301,9 +301,15 @@ export const routeForTimeline = ({ }; export const routeForWorkers = ( + parameters: NamespaceParameter, +): ResolvedPathname => { + return `${routeForNamespace({ namespace: parameters.namespace })}/workers`; +}; + +export const routeForWorkflowWorkers = ( parameters: WorkflowParameters, ): ResolvedPathname => { - return resolve(`${routeForWorkflow(parameters)}/workers`, {}); + return `${routeForWorkflow(parameters)}/workers`; }; export const routeForWorkerDeployments = ({ @@ -314,6 +320,20 @@ export const routeForWorkerDeployments = ({ return resolve('/namespaces/[namespace]/worker-deployments', { namespace }); }; +export const routeForWorkerInstance = ({ + namespace, + workerInstanceKey, +}: { + namespace: string; + workerInstanceKey: string; +}) => { + const workerInstanceKeyEncoded = encodeURIForSvelte(workerInstanceKey); + return resolve('/namespaces/[namespace]/workers/[workerInstanceKey]', { + namespace, + workerInstanceKey: workerInstanceKeyEncoded, + }); +}; + export const routeForWorkerDeployment = ({ namespace, deployment, diff --git a/src/lib/utilities/screaming-enums.ts b/src/lib/utilities/screaming-enums.ts index 48b442f1fa..e11ef7ff63 100644 --- a/src/lib/utilities/screaming-enums.ts +++ b/src/lib/utilities/screaming-enums.ts @@ -1,8 +1,10 @@ +import type { WorkerStatus as ReadableWorkerStatus } from '$lib/models/worker-status'; import type { ArchivalState, CallbackState, NamespaceState, PendingNexusOperationState, + WorkerStatus, WorkflowExecutionStatus, } from '$lib/types'; import type { BatchOperationState, BatchOperationType } from '$lib/types/batch'; @@ -98,3 +100,13 @@ export const toCallbackStateReadable = ( if (!state) return state; return fromScreamingEnum(state, 'CallbackState'); }; + +export const toWorkerStatusReadable = ( + state?: WorkerStatus, +): ReadableWorkerStatus => { + if (!state) return 'Unspecified'; + return fromScreamingEnum( + state, + 'WorkerStatus', + ) as unknown as ReadableWorkerStatus; +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 97686cea2f..347647ab5f 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -35,6 +35,7 @@ routeForSchedules, routeForStandaloneActivities, routeForWorkerDeployments, + routeForWorkers, routeForWorkflows, } from '$lib/utilities/route-for'; import { minimumVersionRequired } from '$lib/utilities/version-check'; @@ -78,6 +79,7 @@ standaloneActivitiesRoute: routeForStandaloneActivities({ namespace }), schedulesRoute: routeForSchedules({ namespace }), batchOperationsRoute: routeForBatchOperations({ namespace }), + workersRoute: routeForWorkers({ namespace }), workerDeploymentsRoute: routeForWorkerDeployments({ namespace }), archivalRoute: routeForArchivalWorkflows({ namespace }), namespacesRoute: routeForNamespaces(), @@ -92,6 +94,7 @@ standaloneActivitiesRoute, schedulesRoute, batchOperationsRoute, + workersRoute, workerDeploymentsRoute, archivalRoute, namespacesRoute, @@ -102,6 +105,7 @@ standaloneActivitiesRoute: string; schedulesRoute: string; batchOperationsRoute: string; + workersRoute: string; workerDeploymentsRoute: string; archivalRoute: string; namespacesRoute: string; @@ -120,6 +124,7 @@ !path.includes(workflowsRoute) && !path.includes(schedulesRoute) && !path.includes(batchOperationsRoute) && + !path.includes(workersRoute) && !path.includes(workerDeploymentsRoute) && !path.includes(standaloneActivitiesRoute) && !path.includes(archivalRoute), @@ -152,11 +157,12 @@ isActive: (path) => path.includes(batchOperationsRoute), }, { - href: workerDeploymentsRoute, + href: workersRoute, icon: 'merge', label: translate('deployments.deployments'), tooltip: translate('deployments.worker-deployments'), - isActive: (path) => path.includes(workerDeploymentsRoute), + isActive: (path) => + path.includes(workersRoute) || path.includes(workerDeploymentsRoute), }, { href: nexusRoute, @@ -198,6 +204,7 @@ workflowsRoute, schedulesRoute, batchOperationsRoute, + workersRoute, workerDeploymentsRoute, archivalRoute, standaloneActivitiesRoute, @@ -206,6 +213,7 @@ [ workflowsRoute, schedulesRoute, + workersRoute, workerDeploymentsRoute, batchOperationsRoute, archivalRoute, @@ -228,8 +236,8 @@ fullRoute: routeForStandaloneActivities({ namespace }), }, { - subPath: 'worker-deployments', - fullRoute: routeForWorkerDeployments({ namespace }), + subPath: 'workers', + fullRoute: routeForWorkers({ namespace }), }, ]; diff --git a/src/routes/(app)/namespaces/[namespace]/task-queues/+layout.svelte b/src/routes/(app)/namespaces/[namespace]/task-queues/+layout.svelte index 45155471cd..78635ae367 100644 --- a/src/routes/(app)/namespaces/[namespace]/task-queues/+layout.svelte +++ b/src/routes/(app)/namespaces/[namespace]/task-queues/+layout.svelte @@ -1,14 +1,10 @@ -

- {translate('common.task-queue')} -

{@render children()} diff --git a/src/routes/(app)/namespaces/[namespace]/worker-deployments/+page.svelte b/src/routes/(app)/namespaces/[namespace]/worker-deployments/+page.svelte index da3d7ee384..266a412b77 100644 --- a/src/routes/(app)/namespaces/[namespace]/worker-deployments/+page.svelte +++ b/src/routes/(app)/namespaces/[namespace]/worker-deployments/+page.svelte @@ -1,9 +1,49 @@ - + +
+
+

+ {translate('deployments.worker-deployments')} +

+
+ + + + + + +
+ diff --git a/src/routes/(app)/namespaces/[namespace]/workers/+page.svelte b/src/routes/(app)/namespaces/[namespace]/workers/+page.svelte new file mode 100644 index 0000000000..ab82f1c928 --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/workers/+page.svelte @@ -0,0 +1,69 @@ + + + +
+
+

+ {translate('workers.workers')} +

+
+ + + + + + +
+ + + + diff --git a/src/routes/(app)/namespaces/[namespace]/workers/[workerInstanceKey]/+page.svelte b/src/routes/(app)/namespaces/[namespace]/workers/[workerInstanceKey]/+page.svelte new file mode 100644 index 0000000000..c9a9327069 --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/workers/[workerInstanceKey]/+page.svelte @@ -0,0 +1,38 @@ + + + + +
+
+
+ + {translate('workers.view-all-workers')} + +
+
+
+{#await describeWorker({ namespace, workerInstanceKey }) then data} + +{:catch error} + +{/await} diff --git a/src/routes/(app)/namespaces/[namespace]/workflows/[workflow]/[run]/workers/+page.svelte b/src/routes/(app)/namespaces/[namespace]/workflows/[workflow]/[run]/workers/+page.svelte index 44a0dc5f6a..a48be447a8 100644 --- a/src/routes/(app)/namespaces/[namespace]/workflows/[workflow]/[run]/workers/+page.svelte +++ b/src/routes/(app)/namespaces/[namespace]/workflows/[workflow]/[run]/workers/+page.svelte @@ -1,17 +1,17 @@ - + +