From 7b141a3a648d960f6fc4483fef7623ed22eb979c Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 28 May 2026 11:49:49 -0600 Subject: [PATCH 1/4] feat(nexus-operations): add standalone nexus operations list page - Add list page route at namespaces/[namespace]/nexus-operations - Add page component with status count filters and configurable table - Add filter bar with search attribute support - Add configurable table with header/body cells and empty state - Add get-nexus-operation-status-and-count utility and tests - Add nexus operation search attributes to search-attributes store - Wire nav layout item for standalone nexus operations - Add i18n strings for nexus operations page --- ...erations-summary-configurable-table.svelte | 87 +++++++++++++ .../filterable-table-cell.svelte | 71 +++++++++++ .../table-body-cell.svelte | 105 ++++++++++++++++ .../table-empty-state.svelte | 71 +++++++++++ .../table-header-cell.svelte | 15 +++ .../table-header-row.svelte | 12 ++ .../table-row.svelte | 22 ++++ .../filter-bar.svelte | 28 +++++ src/lib/i18n/locales/en/common.ts | 1 + .../locales/en/standalone-nexus-operations.ts | 15 +++ .../pages/standalone-nexus-operations.svelte | 116 ++++++++++++++++++ src/lib/services/nexus-operation-counts.ts | 2 +- src/lib/stores/search-attributes.ts | 36 ++++++ ...t-nexus-operation-status-and-count.test.ts | 104 ++++++++++++++++ .../get-nexus-operation-status-and-count.ts | 58 +++++++++ src/routes/(app)/+layout.svelte | 21 ++++ .../nexus-operations/+layout.svelte | 20 +++ .../[namespace]/nexus-operations/+page.svelte | 18 +++ 18 files changed, 801 insertions(+), 1 deletion(-) create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table.svelte create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/filterable-table-cell.svelte create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-empty-state.svelte create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-header-cell.svelte create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-header-row.svelte create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-row.svelte create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/filter-bar.svelte create mode 100644 src/lib/pages/standalone-nexus-operations.svelte create mode 100644 src/lib/utilities/get-nexus-operation-status-and-count.test.ts create mode 100644 src/lib/utilities/get-nexus-operation-status-and-count.ts create mode 100644 src/routes/(app)/namespaces/[namespace]/nexus-operations/+layout.svelte create mode 100644 src/routes/(app)/namespaces/[namespace]/nexus-operations/+page.svelte diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table.svelte new file mode 100644 index 0000000000..e117a39420 --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table.svelte @@ -0,0 +1,87 @@ + + +{#key [namespace, query, $nexusOperationRefresh]} + + + {translate('standalone-nexus-operations.nexus-operations-table')} + + + + {#each columns as column (column.label)} + + {/each} + + {#each visibleItems as operation (operation.operationId + '-' + operation.runId)} + + {#each columns as column (column.label)} + + {/each} + + {/each} + + + + + + + + + +{/key} diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/filterable-table-cell.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/filterable-table-cell.svelte new file mode 100644 index 0000000000..2f8e9d3490 --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/filterable-table-cell.svelte @@ -0,0 +1,71 @@ + + +{#if href} + {value} +{:else} + {value} +{/if} + filter.attribute === attribute && filter.value === value, + )} +/> diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte new file mode 100644 index 0000000000..7fdca9bae3 --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte @@ -0,0 +1,105 @@ + + +{#if filterableLabels.includes(label)} + + {#if label === 'Operation ID'} + + {:else if label === 'Endpoint'} + + {:else if label === 'Service'} + + {:else if label === 'Operation'} + + {/if} + +{:else} + + {#if label === 'Status'} + + {:else if label === 'Run ID'} + {operation.runId ?? ''} + {:else if label === 'Schedule Time'} + + {:else if label === 'Close Time'} + + {:else if label === 'Execution Duration'} + {#if operation.executionDuration} + {formatDistance({ + start: operation.scheduleTime, + end: operation.closeTime, + includeMillisecondsForUnderSecond: true, + })} + {/if} + {:else if label === 'State Transitions'} + {operation.stateTransitionCount ?? ''} + {/if} + +{/if} diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-empty-state.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-empty-state.svelte new file mode 100644 index 0000000000..cfa2d45a9c --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-empty-state.svelte @@ -0,0 +1,71 @@ + + +{#if query} +
+
+

+ {#if $nexusOperationError} + {translate( + 'standalone-nexus-operations.nexus-operation-query-error-state', + )} + {:else} + {translate('standalone-nexus-operations.empty-state-title')} + {/if} +

+

+ {#if $nexusOperationError} + {$nexusOperationError} + {:else} + {translate('standalone-nexus-operations.empty-state-description')} + {/if} +

+ +
+
+{:else} +
+
+

+ {translate('standalone-nexus-operations.empty-state-title')} +

+ {#if $nexusOperationError} + + {$nexusOperationError} + + {:else} +

+ {translate('standalone-nexus-operations.empty-state-description')} +

+ {/if} +
+
+
+ +
+
+
+
+{/if} diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-header-cell.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-header-cell.svelte new file mode 100644 index 0000000000..28cf002479 --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-header-cell.svelte @@ -0,0 +1,15 @@ + + + + {label} + diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-header-row.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-header-row.svelte new file mode 100644 index 0000000000..315563430c --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-header-row.svelte @@ -0,0 +1,12 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-row.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-row.svelte new file mode 100644 index 0000000000..7aae83946b --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-row.svelte @@ -0,0 +1,22 @@ + + + + + {@render children?.()} + diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/filter-bar.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/filter-bar.svelte new file mode 100644 index 0000000000..f5ee146f8d --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/filter-bar.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/src/lib/i18n/locales/en/common.ts b/src/lib/i18n/locales/en/common.ts index 655ed255ac..c5cd0039c8 100644 --- a/src/lib/i18n/locales/en/common.ts +++ b/src/lib/i18n/locales/en/common.ts @@ -138,6 +138,7 @@ export const Strings = { 'copy-success-icon-title': 'Content copied to clipboard', 'filter-workflows': 'Filter workflows', 'filter-activities': 'Filter activities', + 'filter-nexus-operations': 'Filter nexus operations', 'event-category-filter-label': 'Open event category filter menu', 'event-date-filter-label': 'Open event date filter menu', 'date-time-menu-label': 'Open time format menu', diff --git a/src/lib/i18n/locales/en/standalone-nexus-operations.ts b/src/lib/i18n/locales/en/standalone-nexus-operations.ts index 617f9510ed..d7841c7c39 100644 --- a/src/lib/i18n/locales/en/standalone-nexus-operations.ts +++ b/src/lib/i18n/locales/en/standalone-nexus-operations.ts @@ -8,4 +8,19 @@ export const Strings = { 'If you want to enable Standalone Nexus Operations globally, please ensure the following values are set in Dynamic Config.', 'standalone-nexus-operations-enablement-per-namespace': 'If you want to enable Standalone Nexus Operations for this namespace only, include the following', + 'nexus-operations-plural_one': 'Standalone Nexus Operation', + 'nexus-operations-plural_other': 'Standalone Nexus Operations', + 'recent-nexus-operations': 'Recent Standalone Nexus Operations', + 'nexus-operations-error-querying': 'Error querying nexus operations', + 'nexus-operation-query-error-state': 'Error Filtering Nexus Operations', + 'all-nexus-operations': 'All', + 'running-nexus-operations': 'Running', + 'completed-nexus-operations': 'Completed', + 'failed-nexus-operations': 'Failed', + 'canceled-nexus-operations': 'Canceled', + 'terminated-nexus-operations': 'Terminated', + 'timed-out-nexus-operations': 'Timed Out', + 'empty-state-title': 'No Nexus Operations Found', + 'empty-state-description': 'No nexus operations match the current filters.', + 'nexus-operations-table': 'Standalone Nexus Operations Table', } as const; diff --git a/src/lib/pages/standalone-nexus-operations.svelte b/src/lib/pages/standalone-nexus-operations.svelte new file mode 100644 index 0000000000..bfaca45d7b --- /dev/null +++ b/src/lib/pages/standalone-nexus-operations.svelte @@ -0,0 +1,116 @@ + + +
+
+
+
+

+ {#if $supportsAdvancedVisibility} + {$nexusOperationCount.count.toLocaleString()} + + {:else} + + {/if} +

+

+ {refreshTimeFormatted} +

+
+ + +
+
+
+ + + + diff --git a/src/lib/services/nexus-operation-counts.ts b/src/lib/services/nexus-operation-counts.ts index a5a0690edb..a2e2c59ec1 100644 --- a/src/lib/services/nexus-operation-counts.ts +++ b/src/lib/services/nexus-operation-counts.ts @@ -19,7 +19,7 @@ export const fetchNexusOperationCount = async ( request, }); count = parseInt(result?.count || '0'); - } catch (e) { + } catch { // Don't fail the nexus operations call due to count } diff --git a/src/lib/stores/search-attributes.ts b/src/lib/stores/search-attributes.ts index a4ed03b6f1..384bbe68ee 100644 --- a/src/lib/stores/search-attributes.ts +++ b/src/lib/stores/search-attributes.ts @@ -264,3 +264,39 @@ export const activitySearchAttributeOptions: Readable = return 0; }); }); + +export const nexusOperationSearchAttributes: Readable = + derived(customSearchAttributes, ($customSearchAttributes) => ({ + ExecutionStatus: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + OperationId: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + RunId: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + Endpoint: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + Service: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + Operation: SEARCH_ATTRIBUTE_TYPE.KEYWORD, + ScheduleTime: SEARCH_ATTRIBUTE_TYPE.DATETIME, + CloseTime: SEARCH_ATTRIBUTE_TYPE.DATETIME, + ExecutionDuration: SEARCH_ATTRIBUTE_TYPE.INT, + StateTransitionCount: SEARCH_ATTRIBUTE_TYPE.INT, + ...$customSearchAttributes, + })); + +export const nexusOperationSearchAttributeOptions: Readable< + SearchAttributeOption[] +> = derived( + nexusOperationSearchAttributes, + ($nexusOperationSearchAttributes) => { + return Object.entries($nexusOperationSearchAttributes) + .map(([key, value]) => { + return { + label: key, + value: key, + type: value, + }; + }) + .sort((a, b) => { + if (a.label < b.label) return -1; + if (a.label > b.label) return 1; + return 0; + }); + }, +); diff --git a/src/lib/utilities/get-nexus-operation-status-and-count.test.ts b/src/lib/utilities/get-nexus-operation-status-and-count.test.ts new file mode 100644 index 0000000000..7d42e7e13f --- /dev/null +++ b/src/lib/utilities/get-nexus-operation-status-and-count.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + getNexusOperationStatusAndCountOfGroup, + nexusOperationStatuses, + toNexusOperationStatus, +} from './get-nexus-operation-status-and-count'; + +vi.mock('$lib/utilities/decode-payload', () => ({ + parseRawPayloadToJSON: vi.fn((payload) => payload), +})); + +describe('toNexusOperationStatus', () => { + it('maps RUNNING to Running', () => { + expect( + toNexusOperationStatus('NEXUS_OPERATION_EXECUTION_STATUS_RUNNING'), + ).toBe('Running'); + }); + + it('maps UNSPECIFIED to Running', () => { + expect( + toNexusOperationStatus('NEXUS_OPERATION_EXECUTION_STATUS_UNSPECIFIED'), + ).toBe('Running'); + }); + + it('maps COMPLETED to Completed', () => { + expect( + toNexusOperationStatus('NEXUS_OPERATION_EXECUTION_STATUS_COMPLETED'), + ).toBe('Completed'); + }); + + it('maps FAILED to Failed', () => { + expect( + toNexusOperationStatus('NEXUS_OPERATION_EXECUTION_STATUS_FAILED'), + ).toBe('Failed'); + }); + + it('maps CANCELED to Canceled', () => { + expect( + toNexusOperationStatus('NEXUS_OPERATION_EXECUTION_STATUS_CANCELED'), + ).toBe('Canceled'); + }); + + it('maps TERMINATED to Terminated', () => { + expect( + toNexusOperationStatus('NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED'), + ).toBe('Terminated'); + }); + + it('maps TIMED_OUT to TimedOut', () => { + expect( + toNexusOperationStatus('NEXUS_OPERATION_EXECUTION_STATUS_TIMED_OUT'), + ).toBe('TimedOut'); + }); +}); + +describe('nexusOperationStatuses', () => { + it('contains all expected statuses in order', () => { + expect(nexusOperationStatuses).toEqual([ + 'Running', + 'Completed', + 'Failed', + 'Canceled', + 'Terminated', + 'TimedOut', + ]); + }); +}); + +describe('getNexusOperationStatusAndCountOfGroup', () => { + it('returns empty array for undefined groups', () => { + expect(getNexusOperationStatusAndCountOfGroup(undefined)).toEqual([]); + }); + + it('returns empty array for empty groups', () => { + expect(getNexusOperationStatusAndCountOfGroup([])).toEqual([]); + }); + + it('parses a single group', () => { + const groups = [{ groupValues: ['Running'], count: '5' }]; + const result = getNexusOperationStatusAndCountOfGroup(groups); + expect(result).toEqual([{ status: 'Running', count: 5 }]); + }); + + it('sorts groups by nexusOperationStatuses order', () => { + const groups = [ + { groupValues: ['Failed'], count: '3' }, + { groupValues: ['Running'], count: '10' }, + { groupValues: ['Completed'], count: '7' }, + ]; + const result = getNexusOperationStatusAndCountOfGroup(groups); + expect(result).toEqual([ + { status: 'Running', count: 10 }, + { status: 'Completed', count: 7 }, + { status: 'Failed', count: 3 }, + ]); + }); + + it('parses count as integer', () => { + const groups = [{ groupValues: ['TimedOut'], count: '42' }]; + const result = getNexusOperationStatusAndCountOfGroup(groups); + expect(result[0].count).toBe(42); + }); +}); diff --git a/src/lib/utilities/get-nexus-operation-status-and-count.ts b/src/lib/utilities/get-nexus-operation-status-and-count.ts new file mode 100644 index 0000000000..c6195cddab --- /dev/null +++ b/src/lib/utilities/get-nexus-operation-status-and-count.ts @@ -0,0 +1,58 @@ +import type { NexusOperationExecutionStatus } from '$lib/types/nexus-operation-execution'; +import type { CountWorkflowExecutionsResponse } from '$lib/types/workflows'; +import { parseRawPayloadToJSON } from '$lib/utilities/decode-payload'; + +export type NexusOperationStatus = + | 'Running' + | 'Completed' + | 'Failed' + | 'Canceled' + | 'Terminated' + | 'TimedOut'; + +export const nexusOperationStatuses: NexusOperationStatus[] = [ + 'Running', + 'Completed', + 'Failed', + 'Canceled', + 'Terminated', + 'TimedOut', +]; + +const executionStatusToNexusOperationStatus: Record< + NexusOperationExecutionStatus, + NexusOperationStatus +> = { + NEXUS_OPERATION_EXECUTION_STATUS_UNSPECIFIED: 'Running', + NEXUS_OPERATION_EXECUTION_STATUS_RUNNING: 'Running', + NEXUS_OPERATION_EXECUTION_STATUS_COMPLETED: 'Completed', + NEXUS_OPERATION_EXECUTION_STATUS_FAILED: 'Failed', + NEXUS_OPERATION_EXECUTION_STATUS_CANCELED: 'Canceled', + NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED: 'Terminated', + NEXUS_OPERATION_EXECUTION_STATUS_TIMED_OUT: 'TimedOut', +}; + +export const toNexusOperationStatus = ( + status: NexusOperationExecutionStatus, +): NexusOperationStatus => { + return executionStatusToNexusOperationStatus[status] ?? 'Running'; +}; + +export const getNexusOperationStatusAndCountOfGroup = ( + groups: CountWorkflowExecutionsResponse['groups'] = [], +): { status: NexusOperationStatus; count: number }[] => { + return groups + .map((group) => { + const rawStatus = parseRawPayloadToJSON( + group?.groupValues[0], + ) as unknown as NexusOperationStatus; + const count = parseInt(group.count); + return { status: rawStatus, count }; + }) + .sort((a, b) => { + return ( + nexusOperationStatuses.indexOf(a.status) - + nexusOperationStatuses.indexOf(b.status) + ); + }); +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 83a41e017f..c4ad782212 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -35,6 +35,7 @@ routeForNexus, routeForSchedules, routeForStandaloneActivities, + routeForStandaloneNexusOperations, routeForWorkerDeployments, routeForWorkers, routeForWorkflows, @@ -78,6 +79,9 @@ return { workflowsRoute: routeForWorkflows({ namespace }), standaloneActivitiesRoute: routeForStandaloneActivities({ namespace }), + standaloneNexusOperationsRoute: routeForStandaloneNexusOperations({ + namespace, + }), schedulesRoute: routeForSchedules({ namespace }), batchOperationsRoute: routeForBatchOperations({ namespace }), workersRoute: routeForWorkers({ namespace }), @@ -93,6 +97,7 @@ { workflowsRoute, standaloneActivitiesRoute, + standaloneNexusOperationsRoute, schedulesRoute, batchOperationsRoute, workersRoute, @@ -103,6 +108,7 @@ }: { workflowsRoute: string; standaloneActivitiesRoute: string; + standaloneNexusOperationsRoute: string; schedulesRoute: string; batchOperationsRoute: string; workersRoute: string; @@ -142,6 +148,15 @@ isActive: (path) => path.includes(standaloneActivitiesRoute), hidden: !minimumVersionRequired('1.30.0', $temporalVersion), }, + { + href: standaloneNexusOperationsRoute, + icon: 'nexus', + label: translate( + 'standalone-nexus-operations.standalone-nexus-operations', + ), + isActive: (path) => path.includes(standaloneNexusOperationsRoute), + hidden: !minimumVersionRequired('1.31.0', $temporalVersion), + }, { href: schedulesRoute, icon: 'schedules', @@ -231,6 +246,7 @@ workerDeploymentsRoute, archivalRoute, standaloneActivitiesRoute, + standaloneNexusOperationsRoute, } = $derived(routes); let showNamespacePicker = $derived( [ @@ -241,6 +257,7 @@ batchOperationsRoute, archivalRoute, standaloneActivitiesRoute, + standaloneNexusOperationsRoute, ].some((route) => page.url.href.includes(route)), ); @@ -258,6 +275,10 @@ subPath: 'activities', fullRoute: routeForStandaloneActivities({ namespace }), }, + { + subPath: 'nexus-operations', + fullRoute: routeForStandaloneNexusOperations({ namespace }), + }, { subPath: 'workers/deployments', fullRoute: routeForWorkerDeployments({ namespace }), diff --git a/src/routes/(app)/namespaces/[namespace]/nexus-operations/+layout.svelte b/src/routes/(app)/namespaces/[namespace]/nexus-operations/+layout.svelte new file mode 100644 index 0000000000..b3e1371afc --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/nexus-operations/+layout.svelte @@ -0,0 +1,20 @@ + + + + {@render children()} + {#snippet fallback()} + + {/snippet} + diff --git a/src/routes/(app)/namespaces/[namespace]/nexus-operations/+page.svelte b/src/routes/(app)/namespaces/[namespace]/nexus-operations/+page.svelte new file mode 100644 index 0000000000..03a8c8af9d --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/nexus-operations/+page.svelte @@ -0,0 +1,18 @@ + + + + + From d568da28acabc5b75c21ce38322328cc661e376a Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 28 May 2026 13:30:55 -0600 Subject: [PATCH 2/4] feat(nexus-operations): add saved views panel, CTA button, and rich empty state Match Figma design for standalone nexus operations list page: - Add saved views panel with system/user views, expand/collapse, save/edit/delete/share - Add 'Start a Standalone Nexus Operation' CTA button via headerActions snippet - Add rich empty state with value proposition, docs links, and GitHub code samples - Add i18n strings for all new UI copy - Add savedNexusQueryNavOpen persist store and savedNexusQueries/systemNexusViews stores --- .../table-empty-state.svelte | 77 ++- .../edit-view-modal.svelte | 29 + .../save-view-modal.svelte | 13 + .../saved-views.svelte | 509 ++++++++++++++++++ .../locales/en/standalone-nexus-operations.ts | 14 + .../pages/standalone-nexus-operations.svelte | 33 +- src/lib/stores/nav-open.ts | 5 + src/lib/stores/saved-queries.ts | 35 ++ .../[namespace]/nexus-operations/+page.svelte | 12 +- 9 files changed, 718 insertions(+), 9 deletions(-) create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/edit-view-modal.svelte create mode 100644 src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/save-view-modal.svelte create mode 100644 src/lib/components/standalone-nexus-operations/saved-views.svelte diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-empty-state.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-empty-state.svelte index cfa2d45a9c..358fae2736 100644 --- a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-empty-state.svelte +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-empty-state.svelte @@ -3,11 +3,36 @@ import NoQueryResults from '$lib/components/empty-states/no-query-results.svelte'; import Alert from '$lib/holocene/alert.svelte'; + import Icon from '$lib/holocene/icon/icon.svelte'; + import Link from '$lib/holocene/link.svelte'; import { translate } from '$lib/i18n/translate'; import { nexusOperationError } from '$lib/stores/nexus-operations'; import noResultsImages from '$lib/vendor/empty-state.svg'; let query = $derived(page.url.searchParams.get('query')); + + const codeSamples = [ + { + label: 'samples-go', + href: 'https://github.com/temporalio/samples-go', + }, + { + label: 'samples-java', + href: 'https://github.com/temporalio/samples-java', + }, + { + label: 'samples-typescript', + href: 'https://github.com/temporalio/samples-typescript', + }, + { + label: 'samples-python', + href: 'https://github.com/temporalio/samples-python', + }, + { + label: 'samples-dotnet', + href: 'https://github.com/temporalio/samples-dotnet', + }, + ]; {#if query} @@ -41,10 +66,10 @@ aria-live="polite" >

- {translate('standalone-nexus-operations.empty-state-title')} + {translate('standalone-nexus-operations.empty-state-no-data-title')}

{#if $nexusOperationError} {:else} -

- {translate('standalone-nexus-operations.empty-state-description')} -

+
+

+ {translate( + 'standalone-nexus-operations.empty-state-value-proposition', + )} +

+

+ {translate( + 'standalone-nexus-operations.empty-state-read-docs-prefix', + )} + + {translate( + 'standalone-nexus-operations.empty-state-nexus-docs-link', + )} + + {translate( + 'standalone-nexus-operations.empty-state-read-docs-middle', + )} + + {translate( + 'standalone-nexus-operations.empty-state-nexus-eval-guide-link', + )} + + {translate( + 'standalone-nexus-operations.empty-state-read-docs-suffix', + )} +

+
+
+

+ {translate( + 'standalone-nexus-operations.empty-state-code-samples-title', + )} +

+
    + {#each codeSamples as sample (sample.label)} +
  • + + + {sample.label} + +
  • + {/each} +
+
{/if}
diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/edit-view-modal.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/edit-view-modal.svelte new file mode 100644 index 0000000000..b9a9124ced --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/edit-view-modal.svelte @@ -0,0 +1,29 @@ + + + diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/save-view-modal.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/save-view-modal.svelte new file mode 100644 index 0000000000..f29e5c0b87 --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-filter-bar/save-view-modal.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/standalone-nexus-operations/saved-views.svelte b/src/lib/components/standalone-nexus-operations/saved-views.svelte new file mode 100644 index 0000000000..925d55cc46 --- /dev/null +++ b/src/lib/components/standalone-nexus-operations/saved-views.svelte @@ -0,0 +1,509 @@ + + +
+
+
+ {#if $savedNexusQueryNavOpen} + + {/if} +

Saved Views

+ +
+
+ +
+
+
+ {#each systemNexusViews as view (view.id)} + {@render queryButton({ + ...view, + active: query === view.query, + })} + {/each} +
+
+ + {#if $savedNexusQueryNavOpen} + + {/if} + +
+ + {#if unsavedQuery} + {@render queryButton(unsaveView)} + {/if} + + {#if namespaceSavedQueries.length > 0} +
+
+ {#each namespaceSavedQueries as savedQuery (savedQuery.id)} + {@render queryButton({ + ...savedQuery, + active: savedQuery.id === activeQueryView?.id, + badge: + savedQuery.id === activeQueryView?.id && + savedQuery.query !== query + ? 'Unsaved' + : undefined, + })} + {/each} +
+
+ {/if} + + {#if namespaceSavedQueries.length === 0 && !unsavedQuery} +

+ No Views +

+ {/if} +
+
+ + + +{#snippet queryButton(view: SavedQuery)} + +{/snippet} + +{#snippet queryBadge({ + className, + content, + iconClass, + icon, +}: { + className?: ClassNameValue; + content: string | number; + iconClass?: ClassNameValue; + icon?: IconName; +})} + +{/snippet} + +{#if showTooltip && tooltipText} + +{/if} diff --git a/src/lib/i18n/locales/en/standalone-nexus-operations.ts b/src/lib/i18n/locales/en/standalone-nexus-operations.ts index d7841c7c39..7a325ba742 100644 --- a/src/lib/i18n/locales/en/standalone-nexus-operations.ts +++ b/src/lib/i18n/locales/en/standalone-nexus-operations.ts @@ -23,4 +23,18 @@ export const Strings = { 'empty-state-title': 'No Nexus Operations Found', 'empty-state-description': 'No nexus operations match the current filters.', 'nexus-operations-table': 'Standalone Nexus Operations Table', + 'empty-state-no-data-title': + "This namespace doesn't have any Standalone Nexus Operations yet.", + 'empty-state-value-proposition': + 'Standalone Nexus Operations can call any Temporal-hosted endpoint from existing code using a simple SDK client call - no need to set up a Workflow or run a worker. Get all the durable execution benefits like automatic retries, timeouts, and at-least-once execution guarantees out of the box.', + 'empty-state-read-docs-prefix': 'Read the', + 'empty-state-nexus-docs-link': 'Nexus Docs', + 'empty-state-read-docs-middle': 'and check out the', + 'empty-state-nexus-eval-guide-link': 'Nexus Evaluation Guide', + 'empty-state-read-docs-suffix': + 'to learn how Nexus can fit into your operations.', + 'empty-state-code-samples-title': + 'Explore code samples for Standalone Nexus Operations', + 'start-standalone-nexus-operation': 'Start a Standalone Nexus Operation', + 'custom-views': 'Custom Views', } as const; diff --git a/src/lib/pages/standalone-nexus-operations.svelte b/src/lib/pages/standalone-nexus-operations.svelte index bfaca45d7b..6a0380f175 100644 --- a/src/lib/pages/standalone-nexus-operations.svelte +++ b/src/lib/pages/standalone-nexus-operations.svelte @@ -1,11 +1,14 @@ @@ -15,4 +17,12 @@ url={page.url.href} /> - + + {#snippet headerActions()} + + {/snippet} + From eab11509f321ecd5061b24d5b5ecac80ffcc122d Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 28 May 2026 13:42:37 -0600 Subject: [PATCH 3/4] Update api version to v1.62.12 for SANO APIs and capability --- server/go.mod | 2 +- server/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/go.mod b/server/go.mod index 845113dc70..320ae3a546 100644 --- a/server/go.mod +++ b/server/go.mod @@ -11,7 +11,7 @@ require ( github.com/labstack/echo/v4 v4.13.4 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.3.0 - go.temporal.io/api v1.62.8 + go.temporal.io/api v1.62.12 golang.org/x/net v0.54.0 golang.org/x/oauth2 v0.34.0 google.golang.org/grpc v1.79.3 diff --git a/server/go.sum b/server/go.sum index 844b55b5bc..8e5efd6ec1 100644 --- a/server/go.sum +++ b/server/go.sum @@ -88,8 +88,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= -go.temporal.io/api v1.62.8 h1:g8RAZmdebYODoNa2GLA4M4TsXNe1096WV3n26C4+fdw= -go.temporal.io/api v1.62.8/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.12 h1:627rVnItegQmrszg1bH4vfyc/1uNo5qCereCNkvZefw= +go.temporal.io/api v1.62.12/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= From ff2e82f68ef913cdf1b9c129c094e6701e150910 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 28 May 2026 14:53:58 -0600 Subject: [PATCH 4/4] fix capability name --- .../standalone-nexus-operations-guard.svelte | 2 +- src/lib/types/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/standalone-nexus-operations/standalone-nexus-operations-guard.svelte b/src/lib/components/standalone-nexus-operations/standalone-nexus-operations-guard.svelte index 1310d3e33f..7a85741177 100644 --- a/src/lib/components/standalone-nexus-operations/standalone-nexus-operations-guard.svelte +++ b/src/lib/components/standalone-nexus-operations/standalone-nexus-operations-guard.svelte @@ -17,7 +17,7 @@ let { namespace, children, fallback }: Props = $props(); -{#if (namespace.namespaceInfo?.capabilities as NamespaceCapabilities)?.standaloneNexusOperations && minimumVersionRequired('1.31.0', $temporalVersion)} +{#if (namespace.namespaceInfo?.capabilities as NamespaceCapabilities)?.standaloneNexusOperation && minimumVersionRequired('1.31.0', $temporalVersion)} {@render children()} {:else if fallback} {@render fallback()} diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 2fcdeb6e61..8e89870e0f 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -20,7 +20,7 @@ export type Capabilities = export type NamespaceCapabilities = NonNullable< NonNullable['capabilities'] > & { - standaloneNexusOperations?: boolean | null; + standaloneNexusOperation?: boolean | null; }; export type GetWorkflowExecutionHistoryResponse = temporal.api.workflowservice.v1.IGetWorkflowExecutionHistoryResponse;