From 5815c89134b3d9c294a0bbef4f025eab3f41b457 Mon Sep 17 00:00:00 2001 From: Ross Nelson Date: Tue, 3 Mar 2026 14:32:48 -0500 Subject: [PATCH 1/7] feat: add serverless worker types and mock service Foundation for serverless workers prototype with types, CRUD operations, and async mock validators for Lambda ARN, IAM role, region, and task queue. --- src/lib/services/serverless-worker-service.ts | 182 ++++++++++++++++++ src/lib/types/serverless-workers.ts | 45 +++++ 2 files changed, 227 insertions(+) create mode 100644 src/lib/services/serverless-worker-service.ts create mode 100644 src/lib/types/serverless-workers.ts diff --git a/src/lib/services/serverless-worker-service.ts b/src/lib/services/serverless-worker-service.ts new file mode 100644 index 0000000000..ca2ddb34a8 --- /dev/null +++ b/src/lib/services/serverless-worker-service.ts @@ -0,0 +1,182 @@ +import type { + MockValidationResult, + ServerlessWorker, + ServerlessWorkerCreateInput, + ServerlessWorkerUpdateInput, +} from '$lib/types/serverless-workers'; + +const MOCK_TASK_QUEUES = [ + 'order-processing', + 'payment-tasks', + 'notification-queue', + 'etl-pipeline', +]; + +let mockWorkers: ServerlessWorker[] = [ + { + id: 'slw-a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'order-processor', + status: 'active', + lambdaArn: + 'arn:aws:lambda:us-east-1:123456789012:function:temporal-order-processor', + iamRoleArn: + 'arn:aws:iam::123456789012:role/temporal-order-processor-execution-role', + region: 'us-east-1', + taskQueue: 'order-processing', + maxWorkers: 10, + maxConcurrentActivities: 100, + maxTaskQueueActivitiesPerSecond: 50, + idleTimeoutSeconds: 300, + createdAt: '2024-11-01T09:00:00Z', + updatedAt: '2024-11-15T14:22:00Z', + }, + { + id: 'slw-b2c3d4e5-f6a7-8901-bcde-f12345678901', + name: 'payment-handler', + status: 'active', + lambdaArn: + 'arn:aws:lambda:us-west-2:123456789012:function:temporal-payment-handler', + iamRoleArn: + 'arn:aws:iam::123456789012:role/temporal-payment-handler-execution-role', + region: 'us-west-2', + taskQueue: 'payment-tasks', + maxWorkers: 5, + maxConcurrentActivities: 50, + maxTaskQueueActivitiesPerSecond: 25, + idleTimeoutSeconds: 120, + createdAt: '2024-10-15T11:30:00Z', + updatedAt: '2024-11-20T08:45:00Z', + }, + { + id: 'slw-c3d4e5f6-a7b8-9012-cdef-123456789012', + name: 'notification-sender', + status: 'degraded', + lambdaArn: + 'arn:aws:lambda:eu-west-1:123456789012:function:temporal-notification-sender', + iamRoleArn: + 'arn:aws:iam::123456789012:role/temporal-notification-sender-execution-role', + region: 'eu-west-1', + taskQueue: 'notification-queue', + maxWorkers: 20, + maxConcurrentActivities: 200, + maxTaskQueueActivitiesPerSecond: 100, + idleTimeoutSeconds: 600, + createdAt: '2024-09-20T16:00:00Z', + updatedAt: '2024-11-22T03:12:00Z', + }, + { + id: 'slw-d4e5f6a7-b8c9-0123-defa-234567890123', + name: 'data-pipeline', + status: 'provisioning', + lambdaArn: + 'arn:aws:lambda:ap-southeast-1:123456789012:function:temporal-data-pipeline', + iamRoleArn: + 'arn:aws:iam::123456789012:role/temporal-data-pipeline-execution-role', + region: 'ap-southeast-1', + taskQueue: 'etl-pipeline', + maxWorkers: 8, + maxConcurrentActivities: 80, + maxTaskQueueActivitiesPerSecond: 40, + idleTimeoutSeconds: 900, + createdAt: '2024-11-25T10:00:00Z', + updatedAt: '2024-11-25T10:00:00Z', + }, +]; + +const MOCK_LAMBDA_ARNS = mockWorkers.map((w) => w.lambdaArn); +const MOCK_IAM_ROLE_ARNS = mockWorkers.map((w) => w.iamRoleArn); + +export function getServerlessWorkers(): ServerlessWorker[] { + return mockWorkers; +} + +export function getServerlessWorker(id: string): ServerlessWorker | undefined { + return mockWorkers.find((w) => w.id === id); +} + +export function createServerlessWorker( + input: ServerlessWorkerCreateInput, +): ServerlessWorker { + const now = new Date().toISOString(); + const worker: ServerlessWorker = { + ...input, + id: crypto.randomUUID(), + status: 'provisioning', + createdAt: now, + updatedAt: now, + }; + mockWorkers = [...mockWorkers, worker]; + return worker; +} + +export function deleteServerlessWorker(id: string): boolean { + const index = mockWorkers.findIndex((w) => w.id === id); + if (index === -1) return false; + mockWorkers = mockWorkers.filter((w) => w.id !== id); + return true; +} + +export function updateServerlessWorker( + id: string, + input: ServerlessWorkerUpdateInput, +): ServerlessWorker | undefined { + const index = mockWorkers.findIndex((w) => w.id === id); + if (index === -1) return undefined; + const updated = { + ...mockWorkers[index], + ...input, + updatedAt: new Date().toISOString(), + }; + mockWorkers = mockWorkers.map((w) => (w.id === id ? updated : w)); + return updated; +} + +export async function validateLambdaArn( + arn: string, +): Promise { + await new Promise((resolve) => setTimeout(resolve, 500)); + if (!arn.startsWith('arn:aws:lambda:')) { + return { valid: false, message: 'Invalid Lambda ARN format' }; + } + if (MOCK_LAMBDA_ARNS.includes(arn)) { + return { valid: true, message: 'Lambda function verified' }; + } + return { valid: false, message: 'Function not found' }; +} + +export async function validateIamRole( + arn: string, +): Promise { + await new Promise((resolve) => setTimeout(resolve, 500)); + if (!arn.startsWith('arn:aws:iam::')) { + return { valid: false, message: 'Invalid IAM role ARN format' }; + } + if (MOCK_IAM_ROLE_ARNS.includes(arn)) { + return { valid: true, message: 'Permissions verified' }; + } + return { valid: false, message: 'Missing permissions' }; +} + +export async function validateRegion( + region: string, + namespaceRegion: string = 'us-west-2', +): Promise { + await new Promise((resolve) => setTimeout(resolve, 500)); + if (region === namespaceRegion) { + return { valid: true, message: 'Region matches namespace region' }; + } + return { + valid: true, + message: `Cross-region latency warning: worker in ${region}, namespace in ${namespaceRegion}`, + }; +} + +export async function validateTaskQueue( + name: string, +): Promise { + await new Promise((resolve) => setTimeout(resolve, 500)); + if (MOCK_TASK_QUEUES.includes(name)) { + return { valid: true, message: 'Task queue found' }; + } + return { valid: false, message: 'Task queue not found' }; +} diff --git a/src/lib/types/serverless-workers.ts b/src/lib/types/serverless-workers.ts new file mode 100644 index 0000000000..ae538dfc3f --- /dev/null +++ b/src/lib/types/serverless-workers.ts @@ -0,0 +1,45 @@ +import type { WorkerInfo } from '$lib/types'; + +export type ServerlessWorkerStatus = + | 'active' + | 'degraded' + | 'inactive' + | 'provisioning'; + +export type ServerlessWorker = { + id: string; + name: string; + status: ServerlessWorkerStatus; + lambdaArn: string; + iamRoleArn: string; + region: string; + taskQueue: string; + maxWorkers: number; + maxConcurrentActivities: number; + maxTaskQueueActivitiesPerSecond: number; + idleTimeoutSeconds: number; + createdAt: string; + updatedAt: string; +}; + +export type ServerlessWorkerCreateInput = Omit< + ServerlessWorker, + 'id' | 'status' | 'createdAt' | 'updatedAt' +>; + +export type ServerlessWorkerUpdateInput = Pick< + ServerlessWorker, + | 'maxWorkers' + | 'maxConcurrentActivities' + | 'maxTaskQueueActivitiesPerSecond' + | 'idleTimeoutSeconds' +>; + +export type MockValidationResult = { + valid: boolean; + message: string; +}; + +export type UnifiedWorkerRow = + | { type: 'traditional'; data: WorkerInfo } + | { type: 'serverless'; data: ServerlessWorker }; From e63b48543ce60edda6f595900a0a10b4eb6c20ac Mon Sep 17 00:00:00 2001 From: Ross Nelson Date: Tue, 3 Mar 2026 14:33:01 -0500 Subject: [PATCH 2/7] feat: add i18n keys and route helpers for serverless workers Adds 60+ translation keys for serverless worker management UI and route helpers for configuration, detail, and edit pages. --- src/lib/i18n/locales/en/workers.ts | 69 ++++++++++++++++++++++++++++++ src/lib/utilities/route-for.ts | 34 +++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/lib/i18n/locales/en/workers.ts b/src/lib/i18n/locales/en/workers.ts index 87ef81221b..b4a035a468 100644 --- a/src/lib/i18n/locales/en/workers.ts +++ b/src/lib/i18n/locales/en/workers.ts @@ -41,4 +41,73 @@ 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.', + configuration: 'Configuration', + type: 'Type', + 'type-traditional': 'Traditional', + 'type-serverless': 'Serverless', + 'serverless-workers': 'Serverless Workers', + 'serverless-workers-description': + 'Manage serverless worker configurations for this namespace.', + 'serverless-worker': 'Serverless Worker', + 'config-serverless': 'Serverless Workers', + 'config-general': 'General', + 'config-policies': 'Policies', + 'config-alerts': 'Alerts', + 'serverless-empty-state': 'No Serverless Workers Configured', + 'serverless-empty-state-description': + 'Create a serverless worker to get started.', + 'create-serverless-worker': 'Create Serverless Worker', + 'back-to-configuration': 'Back to Configuration', + 'serverless-detail-title': 'Serverless Worker Details', + 'lambda-arn': 'Lambda ARN', + 'iam-role-arn': 'IAM Role ARN', + region: 'Region', + 'max-workers': 'Max Workers', + 'max-concurrent-activities': 'Max Concurrent Activities', + 'max-task-queue-rate': 'Max Task Queue Rate', + 'idle-timeout': 'Idle Timeout', + 'created-at': 'Created', + 'updated-at': 'Last Updated', + 'edit-serverless-worker': 'Edit', + 'edit-serverless-worker-title': 'Edit Serverless Worker', + 'connection-details': 'Connection Details', + 'save-changes': 'Save Changes', + 'delete-serverless-worker': 'Delete Serverless Worker', + 'delete-confirm': 'Are you sure you want to delete this serverless worker?', + 'create-serverless-title': 'Create Serverless Worker', + 'name-label': 'Name', + 'name-hint': 'A unique name for this serverless worker configuration.', + 'name-placeholder': 'my-serverless-worker', + 'lambda-arn-label': 'Lambda ARN', + 'lambda-arn-hint': 'The ARN of the AWS Lambda function to invoke.', + 'lambda-arn-placeholder': + 'arn:aws:lambda:us-east-1:123456789:function:my-function', + 'iam-role-label': 'IAM Role ARN', + 'iam-role-hint': 'The ARN of the IAM role for Temporal to assume.', + 'iam-role-placeholder': + 'arn:aws:iam::123456789:role/temporal-serverless-role', + 'region-label': 'Region', + 'region-hint': 'The AWS region where the Lambda function is deployed.', + 'task-queue-label': 'Task Queue', + 'task-queue-hint': + 'The Temporal task queue this serverless worker will poll.', + 'task-queue-placeholder': 'my-task-queue', + 'advanced-config': 'Advanced Configuration', + 'max-workers-label': 'Max Workers', + 'max-workers-hint': 'Maximum number of concurrent worker instances.', + 'max-concurrent-label': 'Max Concurrent Activities', + 'max-concurrent-hint': 'Maximum concurrent activities per worker.', + 'max-rate-label': 'Max Task Queue Activities Per Second', + 'max-rate-hint': 'Rate limit for task queue polling.', + 'idle-timeout-label': 'Idle Timeout (seconds)', + 'idle-timeout-hint': 'Time before an idle worker is terminated.', + 'validation-checking': 'Checking...', + 'validation-function-found': 'Lambda function found', + 'validation-function-not-found': 'Lambda function not found', + 'validation-permissions-verified': 'Permissions verified', + 'validation-permissions-missing': 'Missing required permissions', + 'validation-region-match': 'Region matches namespace', + 'validation-region-mismatch': 'Cross-region: may increase latency', + 'validation-queue-available': 'Task queue name available', + 'validation-queue-exists': 'Task queue already in use', } as const; diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts index 7d8d49e94b..58f6e7cca7 100644 --- a/src/lib/utilities/route-for.ts +++ b/src/lib/utilities/route-for.ts @@ -369,6 +369,40 @@ export const routeForWorkerDeploymentVersion = ({ ); }; +export const routeForWorkerConfiguration = ({ + namespace, +}: { + namespace: string; +}): ResolvedPathname => { + return resolve('/namespaces/[namespace]/worker-configuration', { namespace }); +}; + +export const routeForServerlessWorker = ({ + namespace, + id, +}: { + namespace: string; + id: string; +}): ResolvedPathname => { + return resolve('/namespaces/[namespace]/serverless-workers/[id]', { + namespace, + id, + }); +}; + +export function routeForServerlessWorkerEdit({ + namespace, + id, +}: { + namespace: string; + id: string; +}): string { + return resolve('/namespaces/[namespace]/serverless-workers/[id]/edit', { + namespace, + id, + }); +} + export const routeForRelationships = ( parameters: WorkflowParameters, ): ResolvedPathname => { From c440ae083715feddcca37a996de8634c60b5b2ed Mon Sep 17 00:00:00 2001 From: Ross Nelson Date: Tue, 3 Mar 2026 14:33:14 -0500 Subject: [PATCH 3/7] feat: add Configuration tab and update nav for serverless workers Adds Configuration as third tab alongside Workers and Deployments. Updates sidebar nav isActive to include worker-configuration and serverless-workers routes. --- src/routes/(app)/+layout.svelte | 13 ++++++++++++- .../[namespace]/worker-deployments/+page.svelte | 10 ++++++++++ .../namespaces/[namespace]/workers/+page.svelte | 10 ++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 347647ab5f..6dfbaecf8b 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -34,6 +34,7 @@ routeForNexus, routeForSchedules, routeForStandaloneActivities, + routeForWorkerConfiguration, routeForWorkerDeployments, routeForWorkers, routeForWorkflows, @@ -80,6 +81,7 @@ schedulesRoute: routeForSchedules({ namespace }), batchOperationsRoute: routeForBatchOperations({ namespace }), workersRoute: routeForWorkers({ namespace }), + workerConfigurationRoute: routeForWorkerConfiguration({ namespace }), workerDeploymentsRoute: routeForWorkerDeployments({ namespace }), archivalRoute: routeForArchivalWorkflows({ namespace }), namespacesRoute: routeForNamespaces(), @@ -95,6 +97,7 @@ schedulesRoute, batchOperationsRoute, workersRoute, + workerConfigurationRoute, workerDeploymentsRoute, archivalRoute, namespacesRoute, @@ -106,6 +109,7 @@ schedulesRoute: string; batchOperationsRoute: string; workersRoute: string; + workerConfigurationRoute: string; workerDeploymentsRoute: string; archivalRoute: string; namespacesRoute: string; @@ -126,6 +130,8 @@ !path.includes(batchOperationsRoute) && !path.includes(workersRoute) && !path.includes(workerDeploymentsRoute) && + !path.includes(workerConfigurationRoute) && + !path.includes('/serverless-workers') && !path.includes(standaloneActivitiesRoute) && !path.includes(archivalRoute), }, @@ -162,7 +168,10 @@ label: translate('deployments.deployments'), tooltip: translate('deployments.worker-deployments'), isActive: (path) => - path.includes(workersRoute) || path.includes(workerDeploymentsRoute), + path.includes(workersRoute) || + path.includes(workerDeploymentsRoute) || + path.includes(workerConfigurationRoute) || + path.includes('/serverless-workers'), }, { href: nexusRoute, @@ -205,6 +214,7 @@ schedulesRoute, batchOperationsRoute, workersRoute, + workerConfigurationRoute, workerDeploymentsRoute, archivalRoute, standaloneActivitiesRoute, @@ -215,6 +225,7 @@ schedulesRoute, workersRoute, workerDeploymentsRoute, + workerConfigurationRoute, batchOperationsRoute, archivalRoute, standaloneActivitiesRoute, diff --git a/src/routes/(app)/namespaces/[namespace]/worker-deployments/+page.svelte b/src/routes/(app)/namespaces/[namespace]/worker-deployments/+page.svelte index 266a412b77..394566634e 100644 --- a/src/routes/(app)/namespaces/[namespace]/worker-deployments/+page.svelte +++ b/src/routes/(app)/namespaces/[namespace]/worker-deployments/+page.svelte @@ -8,6 +8,7 @@ import { translate } from '$lib/i18n/translate'; import WorkerDeployments from '$lib/pages/deployments.svelte'; import { + routeForWorkerConfiguration, routeForWorkerDeployments, routeForWorkers, } from '$lib/utilities/route-for'; @@ -16,6 +17,9 @@ const workersHref = $derived(routeForWorkers({ namespace })); const deploymentsHref = $derived(routeForWorkerDeployments({ namespace })); + const configurationHref = $derived( + routeForWorkerConfiguration({ namespace }), + ); + diff --git a/src/routes/(app)/namespaces/[namespace]/workers/+page.svelte b/src/routes/(app)/namespaces/[namespace]/workers/+page.svelte index 6d43a1e512..5b02aff7ac 100644 --- a/src/routes/(app)/namespaces/[namespace]/workers/+page.svelte +++ b/src/routes/(app)/namespaces/[namespace]/workers/+page.svelte @@ -8,6 +8,7 @@ import Tabs from '$lib/holocene/tab/tabs.svelte'; import { translate } from '$lib/i18n/translate'; import { + routeForWorkerConfiguration, routeForWorkerDeployments, routeForWorkers, } from '$lib/utilities/route-for'; @@ -16,6 +17,9 @@ const workersHref = $derived(routeForWorkers({ namespace })); const deploymentsHref = $derived(routeForWorkerDeployments({ namespace })); + const configurationHref = $derived( + routeForWorkerConfiguration({ namespace }), + ); @@ -39,6 +43,12 @@ href={deploymentsHref} active={false} /> + From 49825fec4e1dfa624a82fbcc1e8b96e8975ff189 Mon Sep 17 00:00:00 2001 From: Ross Nelson Date: Tue, 3 Mar 2026 14:33:26 -0500 Subject: [PATCH 4/7] feat: unified workers table with serverless rows and context menus Shows traditional and serverless workers in one table with Type column. Reorders columns to Instance, Task Queue, Status, Type. Adds three-dots context menu to serverless rows with View, Edit, and Delete actions. --- .../task-queue/worker-table-row.svelte | 5 ++ .../serverless-worker-table-row.svelte | 87 +++++++++++++++++++ .../workers/worker-search-table.svelte | 25 +++++- 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/lib/components/workers/serverless-worker-table-row.svelte diff --git a/src/lib/components/task-queue/worker-table-row.svelte b/src/lib/components/task-queue/worker-table-row.svelte index 686515c64d..6a9b79bcdc 100644 --- a/src/lib/components/task-queue/worker-table-row.svelte +++ b/src/lib/components/task-queue/worker-table-row.svelte @@ -1,4 +1,5 @@ + + + {#each columns as { label } (label)} + {#if label === translate('workers.type')} + {translate('workers.type-serverless')} + {:else if label === translate('workers.status')} + {worker.status} + {:else if label === translate('workers.instance')} + + + {worker.name} + + + {:else if label === translate('workers.task-queue')} + {worker.taskQueue} + {:else if label === translate('workers.identity')} + {worker.lambdaArn.split(':').pop()} + {:else if label === translate('workers.host-name')} + {worker.region} + {:else if label === ''} + + + + + + + + {translate('common.view')} + + + {translate('workers.edit-serverless-worker')} + + + {translate('common.delete')} + + + + + {:else} + — + {/if} + {/each} + diff --git a/src/lib/components/workers/worker-search-table.svelte b/src/lib/components/workers/worker-search-table.svelte index c0edded95f..43eaf7118b 100644 --- a/src/lib/components/workers/worker-search-table.svelte +++ b/src/lib/components/workers/worker-search-table.svelte @@ -5,25 +5,39 @@ import EmptyState from '$lib/holocene/empty-state.svelte'; import PaginatedTable from '$lib/holocene/table/paginated-table/api-paginated.svelte'; import { translate } from '$lib/i18n/translate'; + import { getServerlessWorkers } from '$lib/services/serverless-worker-service'; import { fetchPaginatedWorkers } from '$lib/services/worker-service'; + import ServerlessWorkerTableRow from './serverless-worker-table-row.svelte'; + let { namespace } = $props(); const query = $derived(page.url.searchParams.get('query') || ''); + const serverlessWorkers = getServerlessWorkers(); const columns = [ - { label: translate('workers.status') }, { label: translate('workers.instance') }, { label: translate('workers.task-queue') }, + { label: translate('workers.status') }, + { label: translate('workers.type') }, { label: translate('workers.identity') }, { label: translate('workers.host-name') }, { label: translate('workers.workflow-task-slots') }, { label: translate('workers.activity-task-slots') }, { label: translate('workers.nexus-task-slots') }, { label: translate('workers.sdk') }, + { label: '' }, ]; - const onFetch = $derived(() => fetchPaginatedWorkers({ namespace, query })); + const onFetch = $derived(() => + fetchPaginatedWorkers({ namespace, query }).then((fetcher) => { + return (pageSize: number, token: string) => + fetcher(pageSize, token).catch(() => ({ + items: [], + nextPageToken: '', + })); + }), + ); {#key query} @@ -49,9 +63,14 @@ {#each visibleItems as worker, i (i)} {/each} + {#each serverlessWorkers as sw (sw.id)} + + {/each} - + {#if serverlessWorkers.length === 0} + + {/if} {/key} From f651b125f34df3998edba83deffceb1d1dc57d4f Mon Sep 17 00:00:00 2001 From: Ross Nelson Date: Tue, 3 Mar 2026 14:33:44 -0500 Subject: [PATCH 5/7] feat: add worker configuration pages with VerticalNav layout Configuration tab uses side-nav with Serverless Workers active and placeholder items. Includes serverless workers list with context menus and create form with superforms validation. --- .../worker-configuration/+layout.svelte | 106 +++++++++++++++++ .../[namespace]/worker-configuration/+page.ts | 7 ++ .../serverless/+page.svelte | 109 ++++++++++++++++++ .../serverless/create/+page.svelte | 31 +++++ 4 files changed, 253 insertions(+) create mode 100644 src/routes/(app)/namespaces/[namespace]/worker-configuration/+layout.svelte create mode 100644 src/routes/(app)/namespaces/[namespace]/worker-configuration/+page.ts create mode 100644 src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte create mode 100644 src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/create/+page.svelte diff --git a/src/routes/(app)/namespaces/[namespace]/worker-configuration/+layout.svelte b/src/routes/(app)/namespaces/[namespace]/worker-configuration/+layout.svelte new file mode 100644 index 0000000000..ffcf456d52 --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/worker-configuration/+layout.svelte @@ -0,0 +1,106 @@ + + + +
+
+

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

+
+ + + + + + + +
+ +
+
+ + + + + + +
+
+ {@render children()} +
+
diff --git a/src/routes/(app)/namespaces/[namespace]/worker-configuration/+page.ts b/src/routes/(app)/namespaces/[namespace]/worker-configuration/+page.ts new file mode 100644 index 0000000000..b7d31a8e37 --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/worker-configuration/+page.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; + +import type { PageLoad } from './$types'; + +export const load: PageLoad = async function ({ url }) { + redirect(302, `${url.pathname}/serverless`); +}; diff --git a/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte b/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte new file mode 100644 index 0000000000..1dd45bbd7c --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte @@ -0,0 +1,109 @@ + + +
+
+

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

+

+ {translate('workers.serverless-workers-description')} +

+
+ +
+ +{#if workers.length === 0} + +

{translate('workers.serverless-empty-state-description')}

+
+{:else} + + + + + + + + + + + + + {#each workers as worker (worker.id)} + + + + + + + + + {/each} + +
{translate('workers.name-label')}{translate('workers.status')}{translate('workers.task-queue')}{translate('workers.region')}{translate('workers.lambda-arn')}
+ + {worker.name} + + + + {worker.status} + + {worker.taskQueue}{worker.region}{worker.lambdaArn} + + + + + + + {translate('common.view')} + + + {translate('workers.edit-serverless-worker')} + + + {translate('common.delete')} + + + +
+{/if} diff --git a/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/create/+page.svelte b/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/create/+page.svelte new file mode 100644 index 0000000000..632afd9654 --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/create/+page.svelte @@ -0,0 +1,31 @@ + + + + +
+ + {translate('workers.back-to-configuration')} + +

+ {translate('workers.create-serverless-title')} +

+
+ + goto(backHref)} /> From e20d669a7b5ae0bec411d1dc07e9f6be0086d03a Mon Sep 17 00:00:00 2001 From: Ross Nelson Date: Tue, 3 Mar 2026 14:34:02 -0500 Subject: [PATCH 6/7] feat: add serverless worker detail, edit, and create pages Detail page shows config and advanced settings with Edit/Delete actions. Edit page shows read-only connection details with editable advanced config. Create form uses superforms with zod validation and async mock checks. --- src/lib/pages/serverless-worker-create.svelte | 321 ++++++++++++++++++ src/lib/pages/serverless-worker-detail.svelte | 176 ++++++++++ src/lib/pages/serverless-worker-edit.svelte | 179 ++++++++++ .../serverless-workers/[id]/+page.svelte | 29 ++ .../serverless-workers/[id]/edit/+page.svelte | 29 ++ 5 files changed, 734 insertions(+) create mode 100644 src/lib/pages/serverless-worker-create.svelte create mode 100644 src/lib/pages/serverless-worker-detail.svelte create mode 100644 src/lib/pages/serverless-worker-edit.svelte create mode 100644 src/routes/(app)/namespaces/[namespace]/serverless-workers/[id]/+page.svelte create mode 100644 src/routes/(app)/namespaces/[namespace]/serverless-workers/[id]/edit/+page.svelte diff --git a/src/lib/pages/serverless-worker-create.svelte b/src/lib/pages/serverless-worker-create.svelte new file mode 100644 index 0000000000..b2a8c304ff --- /dev/null +++ b/src/lib/pages/serverless-worker-create.svelte @@ -0,0 +1,321 @@ + + +
+ + +
+ + {#if lambdaValidation.checking} + {translate('workers.validation-checking')} + {:else if lambdaValidation.result} + + {/if} +
+ +
+ + {#if iamValidation.checking} + {translate('workers.validation-checking')} + {:else if iamValidation.result} + + {/if} +
+ +
+ + + {$errors.region?.[0] || translate('workers.region-hint')} + {#if regionValidation.checking} + {translate('workers.validation-checking')} + {:else if regionValidation.result} + + {/if} +
+ +
+ + {#if taskQueueValidation.checking} + {translate('workers.validation-checking')} + {:else if taskQueueValidation.result} + + {/if} +
+ + +
+ + ($form.maxWorkers = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxWorkers" + name="maxWorkers" + label={translate('workers.max-workers-label')} + hintText={translate('workers.max-workers-hint')} + /> + + ($form.maxConcurrentActivities = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxConcurrentActivities" + name="maxConcurrentActivities" + label={translate('workers.max-concurrent-label')} + hintText={translate('workers.max-concurrent-hint')} + /> + + ($form.maxTaskQueueActivitiesPerSecond = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxRate" + name="maxRate" + label={translate('workers.max-rate-label')} + hintText={translate('workers.max-rate-hint')} + /> + + ($form.idleTimeoutSeconds = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="idleTimeout" + name="idleTimeout" + label={translate('workers.idle-timeout-label')} + hintText={translate('workers.idle-timeout-hint')} + /> +
+
+ +
+ + +
+
diff --git a/src/lib/pages/serverless-worker-detail.svelte b/src/lib/pages/serverless-worker-detail.svelte new file mode 100644 index 0000000000..a0c0c79aba --- /dev/null +++ b/src/lib/pages/serverless-worker-detail.svelte @@ -0,0 +1,176 @@ + + +{#if !worker} + + No serverless worker found with ID "{id}". + +{:else} +
+
+
+

{worker.name}

+ {worker.status} +
+
+ + +
+
+ +
+ +

Configuration

+
+
+ {translate('workers.lambda-arn')} + {worker.lambdaArn} +
+
+ {translate('workers.iam-role-arn')} + {worker.iamRoleArn} +
+
+ {translate('workers.region')} + {worker.region} +
+
+ {translate('workers.task-queue')} + {worker.taskQueue} +
+
+
+ + +

+ {translate('workers.advanced-config')} +

+
+
+ {translate('workers.max-workers')} + {worker.maxWorkers} +
+
+ {translate('workers.max-concurrent-activities')} + {worker.maxConcurrentActivities} +
+
+ {translate('workers.max-task-queue-rate')} + {worker.maxTaskQueueActivitiesPerSecond}/s +
+
+ {translate('workers.idle-timeout')} + {worker.idleTimeoutSeconds}s +
+
+ +
+
+
+ {translate('workers.created-at')} + {new Date(worker.createdAt).toLocaleString()} +
+
+ {translate('workers.updated-at')} + {new Date(worker.updatedAt).toLocaleString()} +
+
+
+
+
+
+ + (showDeleteModal = false)} + > +

{translate('workers.delete-serverless-worker')}

+

{translate('workers.delete-confirm')}

+
+{/if} diff --git a/src/lib/pages/serverless-worker-edit.svelte b/src/lib/pages/serverless-worker-edit.svelte new file mode 100644 index 0000000000..ae3f6872d1 --- /dev/null +++ b/src/lib/pages/serverless-worker-edit.svelte @@ -0,0 +1,179 @@ + + +{#if !worker} + + No serverless worker found with ID "{id}". + +{:else} +
+ +

+ {translate('workers.connection-details')} +

+
+
+ {translate('workers.name-label')} + {worker.name} +
+
+ {translate('workers.region')} + {worker.region} +
+
+ {translate('workers.lambda-arn')} + {worker.lambdaArn} +
+
+ {translate('workers.iam-role-arn')} + {worker.iamRoleArn} +
+
+ {translate('workers.task-queue')} + {worker.taskQueue} +
+
+
+ +
+

+ {translate('workers.advanced-config')} +

+ + ($form.maxWorkers = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxWorkers" + name="maxWorkers" + label={translate('workers.max-workers-label')} + hintText={$errors.maxWorkers?.[0] || + translate('workers.max-workers-hint')} + error={!!$errors.maxWorkers?.[0]} + /> + + ($form.maxConcurrentActivities = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxConcurrentActivities" + name="maxConcurrentActivities" + label={translate('workers.max-concurrent-label')} + hintText={$errors.maxConcurrentActivities?.[0] || + translate('workers.max-concurrent-hint')} + error={!!$errors.maxConcurrentActivities?.[0]} + /> + + ($form.maxTaskQueueActivitiesPerSecond = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxRate" + name="maxRate" + label={translate('workers.max-rate-label')} + hintText={$errors.maxTaskQueueActivitiesPerSecond?.[0] || + translate('workers.max-rate-hint')} + error={!!$errors.maxTaskQueueActivitiesPerSecond?.[0]} + /> + + ($form.idleTimeoutSeconds = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="idleTimeout" + name="idleTimeout" + label={translate('workers.idle-timeout-label')} + hintText={$errors.idleTimeoutSeconds?.[0] || + translate('workers.idle-timeout-hint')} + error={!!$errors.idleTimeoutSeconds?.[0]} + /> + +
+ + +
+
+
+{/if} diff --git a/src/routes/(app)/namespaces/[namespace]/serverless-workers/[id]/+page.svelte b/src/routes/(app)/namespaces/[namespace]/serverless-workers/[id]/+page.svelte new file mode 100644 index 0000000000..79746351ae --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/serverless-workers/[id]/+page.svelte @@ -0,0 +1,29 @@ + + + + +
+ + {translate('workers.back-to-workers')} + +
+ + diff --git a/src/routes/(app)/namespaces/[namespace]/serverless-workers/[id]/edit/+page.svelte b/src/routes/(app)/namespaces/[namespace]/serverless-workers/[id]/edit/+page.svelte new file mode 100644 index 0000000000..877e95302c --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/serverless-workers/[id]/edit/+page.svelte @@ -0,0 +1,29 @@ + + + + +
+ + {translate('workers.back-to-workers')} + +
+ + From d007bd94034195aa997905158130908454b82018 Mon Sep 17 00:00:00 2001 From: Ross Nelson Date: Wed, 4 Mar 2026 12:58:02 -0500 Subject: [PATCH 7/7] feat: improve serverless workers UX with guided setup and shared form Extract shared form component for create/edit, add provider picker, pre-setup guidance with CloudFormation/Terraform snippets, Combobox region selector, validation spinners, tooltip help, copyable ARNs with AWS console links, and educational empty state. --- src/lib/i18n/locales/en/workers.ts | 50 ++ src/lib/pages/serverless-worker-create.svelte | 325 +-------- src/lib/pages/serverless-worker-detail.svelte | 63 +- src/lib/pages/serverless-worker-edit.svelte | 170 +---- src/lib/pages/serverless-worker-form.svelte | 618 ++++++++++++++++++ src/lib/services/serverless-worker-service.ts | 18 +- .../serverless/+page.svelte | 27 +- 7 files changed, 795 insertions(+), 476 deletions(-) create mode 100644 src/lib/pages/serverless-worker-form.svelte diff --git a/src/lib/i18n/locales/en/workers.ts b/src/lib/i18n/locales/en/workers.ts index b4a035a468..ff3a4058ee 100644 --- a/src/lib/i18n/locales/en/workers.ts +++ b/src/lib/i18n/locales/en/workers.ts @@ -110,4 +110,54 @@ export const Strings = { 'validation-region-mismatch': 'Cross-region: may increase latency', 'validation-queue-available': 'Task queue name available', 'validation-queue-exists': 'Task queue already in use', + 'compute-provider': 'Compute Provider', + 'compute-provider-description': + 'Select how your worker activities will be executed.', + 'provider-lambda': 'AWS Lambda', + 'provider-lambda-description': + 'Run activities as serverless Lambda function invocations.', + 'provider-coming-soon': 'More providers coming soon', + 'provider-coming-soon-description': + 'Support for additional compute providers is planned.', + 'setup-guide-title': 'Prerequisites: AWS Setup Guide', + 'setup-guide-intro': + 'Before creating a serverless worker, you need a Lambda function and an IAM role in your AWS account.', + 'setup-guide-lambda-console': 'Open AWS Lambda Console', + 'setup-guide-iam-console': 'Open AWS IAM Console', + 'setup-guide-iam-note': + 'The IAM role must have a trust policy allowing Temporal to assume it. Use the templates below as a starting point.', + 'validation-checking-lambda': 'Validating Lambda function access...', + 'validation-checking-iam': 'Validating IAM role permissions...', + 'validation-checking-region': 'Checking region compatibility...', + 'validation-checking-queue': 'Checking task queue availability...', + 'validation-function-not-found-detail': + 'Lambda function not found. Verify the ARN and ensure the function exists in the specified region.', + 'validation-permissions-missing-detail': + 'IAM role lacks required permissions. Ensure the role has a trust policy allowing Temporal to assume it.', + 'validation-queue-new': + 'This will create a new task queue. Serverless workers require a dedicated task queue.', + 'serverless-empty-title': 'Get Started with Serverless Workers', + 'serverless-empty-description': + 'Serverless workers execute your Temporal activities as AWS Lambda function invocations, eliminating the need to manage worker infrastructure.', + 'serverless-empty-prereq-title': 'What you need:', + 'serverless-empty-prereq-lambda': + 'An AWS Lambda function with your activity code', + 'serverless-empty-prereq-iam': + 'An IAM role allowing Temporal to invoke your function', + 'serverless-empty-prereq-queue': + 'A task queue name for your serverless worker', + 'serverless-docs-link': 'Learn more about serverless workers', + 'open-lambda-console': 'Open in Lambda Console', + 'open-iam-console': 'Open in IAM Console', + 'copy-arn': 'Copy ARN', + 'copy-task-queue': 'Copy task queue name', + copied: 'Copied!', + 'lambda-arn-help': + 'The Amazon Resource Name of your Lambda function. Use an unqualified ARN (no version/alias suffix).', + 'iam-role-help': + 'Temporal assumes this role to invoke your Lambda function. The role needs a trust policy for Temporal and permission to invoke the function.', + 'task-queue-help': + 'Serverless workers require a dedicated task queue. You cannot share a task queue between serverless and traditional workers.', + 'region-help': + 'Choose the AWS region where your Lambda function is deployed. For best performance, match your Temporal namespace region.', } as const; diff --git a/src/lib/pages/serverless-worker-create.svelte b/src/lib/pages/serverless-worker-create.svelte index b2a8c304ff..c98fd73197 100644 --- a/src/lib/pages/serverless-worker-create.svelte +++ b/src/lib/pages/serverless-worker-create.svelte @@ -1,321 +1,24 @@ -
- - -
- - {#if lambdaValidation.checking} - {translate('workers.validation-checking')} - {:else if lambdaValidation.result} - - {/if} -
- -
- - {#if iamValidation.checking} - {translate('workers.validation-checking')} - {:else if iamValidation.result} - - {/if} -
- -
- - - {$errors.region?.[0] || translate('workers.region-hint')} - {#if regionValidation.checking} - {translate('workers.validation-checking')} - {:else if regionValidation.result} - - {/if} -
- -
- - {#if taskQueueValidation.checking} - {translate('workers.validation-checking')} - {:else if taskQueueValidation.result} - - {/if} -
- - -
- - ($form.maxWorkers = Number( - (e.currentTarget as HTMLInputElement).value, - ))} - id="maxWorkers" - name="maxWorkers" - label={translate('workers.max-workers-label')} - hintText={translate('workers.max-workers-hint')} - /> - - ($form.maxConcurrentActivities = Number( - (e.currentTarget as HTMLInputElement).value, - ))} - id="maxConcurrentActivities" - name="maxConcurrentActivities" - label={translate('workers.max-concurrent-label')} - hintText={translate('workers.max-concurrent-hint')} - /> - - ($form.maxTaskQueueActivitiesPerSecond = Number( - (e.currentTarget as HTMLInputElement).value, - ))} - id="maxRate" - name="maxRate" - label={translate('workers.max-rate-label')} - hintText={translate('workers.max-rate-hint')} - /> - - ($form.idleTimeoutSeconds = Number( - (e.currentTarget as HTMLInputElement).value, - ))} - id="idleTimeout" - name="idleTimeout" - label={translate('workers.idle-timeout-label')} - hintText={translate('workers.idle-timeout-hint')} - /> -
-
- -
- - -
-
+ { + createServerlessWorker(data as unknown as ServerlessWorkerCreateInput); + onSuccess(); + }} +/> diff --git a/src/lib/pages/serverless-worker-detail.svelte b/src/lib/pages/serverless-worker-detail.svelte index a0c0c79aba..6bd2e67927 100644 --- a/src/lib/pages/serverless-worker-detail.svelte +++ b/src/lib/pages/serverless-worker-detail.svelte @@ -5,12 +5,15 @@ import Badge from '$lib/holocene/badge.svelte'; import Button from '$lib/holocene/button.svelte'; import Card from '$lib/holocene/card.svelte'; + import CopyButton from '$lib/holocene/copyable/button.svelte'; + import Link from '$lib/holocene/link.svelte'; import Modal from '$lib/holocene/modal.svelte'; import { translate } from '$lib/i18n/translate'; import { deleteServerlessWorker, getServerlessWorker, } from '$lib/services/serverless-worker-service'; + import { copyToClipboard } from '$lib/utilities/copy-to-clipboard'; import { routeForServerlessWorkerEdit, routeForWorkers, @@ -22,6 +25,20 @@ const worker = $derived(getServerlessWorker(id)); let showDeleteModal = $state(false); + const { copy: copyLambda, copied: lambdaCopied } = copyToClipboard(); + const { copy: copyIam, copied: iamCopied } = copyToClipboard(); + const { copy: copyTaskQueue, copied: taskQueueCopied } = copyToClipboard(); + + function parseLambdaArn(arn: string) { + const parts = arn.split(':'); + return { region: parts[3], functionName: parts[6] }; + } + + function parseIamRoleArn(arn: string) { + const parts = arn.split('/'); + return { roleName: parts[parts.length - 1] }; + } + const statusBadgeType = $derived.by(() => { if (!worker) return 'default'; switch (worker.status) { @@ -73,17 +90,45 @@ {translate('workers.lambda-arn')} - {worker.lambdaArn} + {worker.lambdaArn} + copyLambda(e, worker.lambdaArn)} + /> + + + {translate('workers.open-lambda-console')} +
{translate('workers.iam-role-arn')} - {worker.iamRoleArn} + {worker.iamRoleArn} + copyIam(e, worker.iamRoleArn)} + /> +
+ + {translate('workers.open-iam-console')} +
{translate('workers.task-queue')} - {worker.taskQueue} +
+ {worker.taskQueue} + copyTaskQueue(e, worker.taskQueue)} + /> +
diff --git a/src/lib/pages/serverless-worker-edit.svelte b/src/lib/pages/serverless-worker-edit.svelte index ae3f6872d1..25a257dfd4 100644 --- a/src/lib/pages/serverless-worker-edit.svelte +++ b/src/lib/pages/serverless-worker-edit.svelte @@ -1,19 +1,14 @@ {#if !worker} @@ -66,114 +23,17 @@ No serverless worker found with ID "{id}". {:else} -
- -

- {translate('workers.connection-details')} -

-
-
- {translate('workers.name-label')} - {worker.name} -
-
- {translate('workers.region')} - {worker.region} -
-
- {translate('workers.lambda-arn')} - {worker.lambdaArn} -
-
- {translate('workers.iam-role-arn')} - {worker.iamRoleArn} -
-
- {translate('workers.task-queue')} - {worker.taskQueue} -
-
-
- -
-

- {translate('workers.advanced-config')} -

- - ($form.maxWorkers = Number( - (e.currentTarget as HTMLInputElement).value, - ))} - id="maxWorkers" - name="maxWorkers" - label={translate('workers.max-workers-label')} - hintText={$errors.maxWorkers?.[0] || - translate('workers.max-workers-hint')} - error={!!$errors.maxWorkers?.[0]} - /> - - ($form.maxConcurrentActivities = Number( - (e.currentTarget as HTMLInputElement).value, - ))} - id="maxConcurrentActivities" - name="maxConcurrentActivities" - label={translate('workers.max-concurrent-label')} - hintText={$errors.maxConcurrentActivities?.[0] || - translate('workers.max-concurrent-hint')} - error={!!$errors.maxConcurrentActivities?.[0]} - /> - - ($form.maxTaskQueueActivitiesPerSecond = Number( - (e.currentTarget as HTMLInputElement).value, - ))} - id="maxRate" - name="maxRate" - label={translate('workers.max-rate-label')} - hintText={$errors.maxTaskQueueActivitiesPerSecond?.[0] || - translate('workers.max-rate-hint')} - error={!!$errors.maxTaskQueueActivitiesPerSecond?.[0]} - /> - - ($form.idleTimeoutSeconds = Number( - (e.currentTarget as HTMLInputElement).value, - ))} - id="idleTimeout" - name="idleTimeout" - label={translate('workers.idle-timeout-label')} - hintText={$errors.idleTimeoutSeconds?.[0] || - translate('workers.idle-timeout-hint')} - error={!!$errors.idleTimeoutSeconds?.[0]} - /> - -
- - -
-
-
+ { + updateServerlessWorker( + id, + data as unknown as ServerlessWorkerUpdateInput, + ); + goto(detailHref); + }} + /> {/if} diff --git a/src/lib/pages/serverless-worker-form.svelte b/src/lib/pages/serverless-worker-form.svelte new file mode 100644 index 0000000000..b8f1e6814c --- /dev/null +++ b/src/lib/pages/serverless-worker-form.svelte @@ -0,0 +1,618 @@ + + +{#if isEditMode} +
+ +

+ {translate('workers.connection-details')} +

+
+
+ {translate('workers.name-label')} + {worker!.name} +
+
+ {translate('workers.region')} + {worker!.region} +
+
+ {translate('workers.lambda-arn')} + {worker!.lambdaArn} +
+
+ {translate('workers.iam-role-arn')} + {worker!.iamRoleArn} +
+
+ {translate('workers.task-queue')} + {worker!.taskQueue} +
+
+
+ +
+

+ {translate('workers.advanced-config')} +

+ + ($form.maxWorkers = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxWorkers" + name="maxWorkers" + label={translate('workers.max-workers-label')} + hintText={$errors.maxWorkers?.[0] || + translate('workers.max-workers-hint')} + error={!!$errors.maxWorkers?.[0]} + /> + + ($form.maxConcurrentActivities = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxConcurrentActivities" + name="maxConcurrentActivities" + label={translate('workers.max-concurrent-label')} + hintText={$errors.maxConcurrentActivities?.[0] || + translate('workers.max-concurrent-hint')} + error={!!$errors.maxConcurrentActivities?.[0]} + /> + + ($form.maxTaskQueueActivitiesPerSecond = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxRate" + name="maxRate" + label={translate('workers.max-rate-label')} + hintText={$errors.maxTaskQueueActivitiesPerSecond?.[0] || + translate('workers.max-rate-hint')} + error={!!$errors.maxTaskQueueActivitiesPerSecond?.[0]} + /> + + ($form.idleTimeoutSeconds = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="idleTimeout" + name="idleTimeout" + label={translate('workers.idle-timeout-label')} + hintText={$errors.idleTimeoutSeconds?.[0] || + translate('workers.idle-timeout-hint')} + error={!!$errors.idleTimeoutSeconds?.[0]} + /> + +
+ + +
+
+
+{:else} +
+
+

+ {translate('workers.compute-provider')} +

+ + + + +
+ + +
+

+ {translate('workers.setup-guide-intro')} +

+
+ + {translate('workers.setup-guide-lambda-console')} + + + {translate('workers.setup-guide-iam-console')} + +
+

+ {translate('workers.setup-guide-iam-note')} +

+ +
+
+ +
+ + +
+
+ + + + +
+ + {#if lambdaValidation.checking} +
+ + {translate('workers.validation-checking-lambda')} +
+ {:else if lambdaValidation.result} + + {/if} +
+ +
+
+ + + + +
+ + {#if iamValidation.checking} +
+ + {translate('workers.validation-checking-iam')} +
+ {:else if iamValidation.result} + + {/if} +
+ +
+
+ + + + +
+ + {#if regionValidation.checking} +
+ + {translate('workers.validation-checking-region')} +
+ {:else if regionValidation.result} + + {/if} +
+ +
+
+ + + + +
+ + {#if taskQueueValidation.checking} +
+ + {translate('workers.validation-checking-queue')} +
+ {:else if taskQueueValidation.result} + + {/if} +
+ + +
+ + ($form.maxWorkers = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxWorkers" + name="maxWorkers" + label={translate('workers.max-workers-label')} + hintText={translate('workers.max-workers-hint')} + /> + + ($form.maxConcurrentActivities = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxConcurrentActivities" + name="maxConcurrentActivities" + label={translate('workers.max-concurrent-label')} + hintText={translate('workers.max-concurrent-hint')} + /> + + ($form.maxTaskQueueActivitiesPerSecond = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="maxRate" + name="maxRate" + label={translate('workers.max-rate-label')} + hintText={translate('workers.max-rate-hint')} + /> + + ($form.idleTimeoutSeconds = Number( + (e.currentTarget as HTMLInputElement).value, + ))} + id="idleTimeout" + name="idleTimeout" + label={translate('workers.idle-timeout-label')} + hintText={translate('workers.idle-timeout-hint')} + /> +
+
+ +
+ + +
+
+
+{/if} diff --git a/src/lib/services/serverless-worker-service.ts b/src/lib/services/serverless-worker-service.ts index ca2ddb34a8..02f65b107d 100644 --- a/src/lib/services/serverless-worker-service.ts +++ b/src/lib/services/serverless-worker-service.ts @@ -141,7 +141,11 @@ export async function validateLambdaArn( if (MOCK_LAMBDA_ARNS.includes(arn)) { return { valid: true, message: 'Lambda function verified' }; } - return { valid: false, message: 'Function not found' }; + return { + valid: false, + message: + 'Lambda function not found. Verify the ARN and ensure the function exists in the specified region.', + }; } export async function validateIamRole( @@ -154,7 +158,11 @@ export async function validateIamRole( if (MOCK_IAM_ROLE_ARNS.includes(arn)) { return { valid: true, message: 'Permissions verified' }; } - return { valid: false, message: 'Missing permissions' }; + return { + valid: false, + message: + 'IAM role lacks required permissions. Ensure the role has a trust policy allowing Temporal to assume it.', + }; } export async function validateRegion( @@ -178,5 +186,9 @@ export async function validateTaskQueue( if (MOCK_TASK_QUEUES.includes(name)) { return { valid: true, message: 'Task queue found' }; } - return { valid: false, message: 'Task queue not found' }; + return { + valid: true, + message: + 'This will create a new task queue. Serverless workers require a dedicated task queue.', + }; } diff --git a/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte b/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte index 1dd45bbd7c..eb3b34a155 100644 --- a/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte +++ b/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte @@ -5,6 +5,7 @@ import Button from '$lib/holocene/button.svelte'; import EmptyState from '$lib/holocene/empty-state.svelte'; import Icon from '$lib/holocene/icon/icon.svelte'; + import Link from '$lib/holocene/link.svelte'; import { Menu, MenuButton, @@ -38,8 +39,30 @@ {#if workers.length === 0} - -

{translate('workers.serverless-empty-state-description')}

+ +
+

+ {translate('workers.serverless-empty-description')} +

+
+

+ {translate('workers.serverless-empty-prereq-title')} +

+
    +
  • {translate('workers.serverless-empty-prereq-lambda')}
  • +
  • {translate('workers.serverless-empty-prereq-iam')}
  • +
  • {translate('workers.serverless-empty-prereq-queue')}
  • +
+
+
+ + + {translate('workers.serverless-docs-link')} + +
+
{:else}