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} diff --git a/src/lib/i18n/locales/en/workers.ts b/src/lib/i18n/locales/en/workers.ts index 87ef81221b..ff3a4058ee 100644 --- a/src/lib/i18n/locales/en/workers.ts +++ b/src/lib/i18n/locales/en/workers.ts @@ -41,4 +41,123 @@ 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', + '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 new file mode 100644 index 0000000000..c98fd73197 --- /dev/null +++ b/src/lib/pages/serverless-worker-create.svelte @@ -0,0 +1,24 @@ + + + { + 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 new file mode 100644 index 0000000000..6bd2e67927 --- /dev/null +++ b/src/lib/pages/serverless-worker-detail.svelte @@ -0,0 +1,229 @@ + + +{#if !worker} + + No serverless worker found with ID "{id}". + +{:else} +
+
+
+

{worker.name}

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

Configuration

+
+
+ {translate('workers.lambda-arn')} +
+ {worker.lambdaArn} + copyLambda(e, worker.lambdaArn)} + /> +
+ + {translate('workers.open-lambda-console')} + +
+
+ {translate('workers.iam-role-arn')} +
+ {worker.iamRoleArn} + copyIam(e, worker.iamRoleArn)} + /> +
+ + {translate('workers.open-iam-console')} + +
+
+ {translate('workers.region')} + {worker.region} +
+
+ {translate('workers.task-queue')} +
+ {worker.taskQueue} + copyTaskQueue(e, 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..25a257dfd4 --- /dev/null +++ b/src/lib/pages/serverless-worker-edit.svelte @@ -0,0 +1,39 @@ + + +{#if !worker} + + No serverless worker found with ID "{id}". + +{:else} + { + 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 new file mode 100644 index 0000000000..02f65b107d --- /dev/null +++ b/src/lib/services/serverless-worker-service.ts @@ -0,0 +1,194 @@ +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: + 'Lambda function not found. Verify the ARN and ensure the function exists in the specified region.', + }; +} + +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: + 'IAM role lacks required permissions. Ensure the role has a trust policy allowing Temporal to assume it.', + }; +} + +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: true, + message: + 'This will create a new task queue. Serverless workers require a dedicated task queue.', + }; +} 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 }; 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 => { 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]/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')} + +
+ + 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..eb3b34a155 --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/worker-configuration/serverless/+page.svelte @@ -0,0 +1,132 @@ + + +
+
+

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

+

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

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

+ {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} + + + + + + + + + + + + + {#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)} /> 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} /> +