diff --git a/src/lib/i18n/locales/en/common.ts b/src/lib/i18n/locales/en/common.ts index 8b91707b05..79e4345ab8 100644 --- a/src/lib/i18n/locales/en/common.ts +++ b/src/lib/i18n/locales/en/common.ts @@ -68,6 +68,9 @@ export const Strings = { 'workflow-type': 'Workflow Type', 'workflow-id': 'Workflow ID', 'run-id': 'Run ID', + 'event-id': 'Event ID', + 'event-type': 'Event Type', + 'request-id': 'Request ID', 'task-queue': 'Task Queue', preview: 'Preview', status: 'Status', diff --git a/src/lib/i18n/locales/en/nexus.ts b/src/lib/i18n/locales/en/nexus.ts index 966026636f..0a3c781d0c 100644 --- a/src/lib/i18n/locales/en/nexus.ts +++ b/src/lib/i18n/locales/en/nexus.ts @@ -60,7 +60,19 @@ export const Strings = { 'last-attempt-failure': 'Last Attempt Failure', 'blocked-reason': 'Blocked Reason', link: 'Link', + 'links-empty-state': 'No Nexus Links found.', 'link-namespace': 'Link Namespace', + 'nexus-service': 'Nexus Service', + 'nexus-endpoint-simple': 'Nexus Endpoint', + 'nexus-operation': 'Nexus Operation', + 'caller-event': 'Caller Event', + 'caller-link': 'Caller Link', + 'caller-workflow': 'Caller Workflow', + 'source-event': 'Source Event', + 'caller-namespace': 'Caller Namespace', + 'handler-namespace': 'Handler Namespace', + 'handler-workflow': 'Handler Workflow', + 'handler-event': 'Handler Event', service: 'Service', operation: 'Operation', 'operation-token': 'Operation Token', diff --git a/src/lib/i18n/locales/en/workflows.ts b/src/lib/i18n/locales/en/workflows.ts index e93ee6525d..48f4c151f6 100644 --- a/src/lib/i18n/locales/en/workflows.ts +++ b/src/lib/i18n/locales/en/workflows.ts @@ -125,6 +125,7 @@ export const Strings = { 'workflow-history': 'Workflow History', 'workers-tab': 'Workers', 'pending-activities-tab': 'Pending Activities', + 'nexus-links-tab': 'Nexus Links', 'call-stack-tab': 'Call Stack', 'queries-tab': 'Queries', 'metadata-tab': 'Metadata', diff --git a/src/lib/layouts/workflow-header.svelte b/src/lib/layouts/workflow-header.svelte index f7e7a83611..3b128d5b8d 100644 --- a/src/lib/layouts/workflow-header.svelte +++ b/src/lib/layouts/workflow-header.svelte @@ -18,6 +18,7 @@ import Tab from '$lib/holocene/tab/tab.svelte'; import Tabs from '$lib/holocene/tab/tabs.svelte'; import { translate } from '$lib/i18n/translate'; + import { getInboundNexusLinkEvents } from '$lib/runes/inbound-nexus-links.svelte'; import { getWorkflowPollersWithVersions } from '$lib/runes/workflow-versions.svelte'; import { fullEventHistory } from '$lib/stores/events'; import { namespaces } from '$lib/stores/namespaces'; @@ -25,11 +26,15 @@ import { workflowRun } from '$lib/stores/workflow-run'; import { workflowsSearchParams } from '$lib/stores/workflows'; import { isCancelInProgress } from '$lib/utilities/cancel-in-progress'; - import { getWorkflowRelationships } from '$lib/utilities/get-workflow-relationships'; + import { + getWorkflowNexusLinksFromHistory, + getWorkflowRelationships, + } from '$lib/utilities/get-workflow-relationships'; import { pathMatches } from '$lib/utilities/path-matches'; import { routeForCallStack, routeForEventHistory, + routeForNexusLinks, routeForPendingActivities, routeForRelationships, routeForWorkers, @@ -64,6 +69,10 @@ $fullEventHistory, $namespaces, ); + $: outboundLinks = + getWorkflowNexusLinksFromHistory($fullEventHistory)?.length || 0; + $: inboundLinks = getInboundNexusLinkEvents($fullEventHistory)?.length || 0; + $: linkCount = outboundLinks + inboundLinks;
@@ -204,6 +213,21 @@ {workflowRelationships.relationshipCount} + {#if linkCount > 0} + + + {linkCount} + + + {/if}
- {#if activitiesCanceled} + {#if activitiesCanceled} + {/if} {workflow?.pendingActivities?.length}
diff --git a/src/lib/models/event-groups/event-groups.d.ts b/src/lib/models/event-groups/event-groups.d.ts index 2e3eb402dc..fb787e96a1 100644 --- a/src/lib/models/event-groups/event-groups.d.ts +++ b/src/lib/models/event-groups/event-groups.d.ts @@ -1,6 +1,5 @@ -import type { Payload } from '$lib/types'; +import type { EventLink, Payload } from '$lib/types'; import type { - EventLink, PendingActivity, PendingNexusOperation, WorkflowEvent, diff --git a/src/lib/pages/workflow-history-event.svelte b/src/lib/pages/workflow-history-event.svelte index 82b1a27db9..f4a7a60719 100644 --- a/src/lib/pages/workflow-history-event.svelte +++ b/src/lib/pages/workflow-history-event.svelte @@ -1,5 +1,7 @@
+ import { page } from '$app/state'; + + import Link from '$lib/holocene/link.svelte'; + import TableHeaderRow from '$lib/holocene/table/table-header-row.svelte'; + import TableRow from '$lib/holocene/table/table-row.svelte'; + import Table from '$lib/holocene/table/table.svelte'; + import { translate } from '$lib/i18n/translate'; + import { groupEvents } from '$lib/models/event-groups'; + import { + getInboundLinkForEvent, + getInboundNexusLinkEvents, + } from '$lib/runes/inbound-nexus-links.svelte'; + import { fullEventHistory } from '$lib/stores/events'; + import { workflowRun } from '$lib/stores/workflow-run'; + import { getEventLinkHref } from '$lib/utilities/event-link-href'; + import { isNexusOperationScheduledEvent } from '$lib/utilities/is-event-type'; + import { + routeForEventHistory, + routeForEventHistoryEvent, + routeForNamespace, + } from '$lib/utilities/route-for'; + import { fromScreamingEnum } from '$lib/utilities/screaming-enums'; + + const { namespace, workflow: workflowId, run } = $derived(page.params); + const { workflow } = $derived($workflowRun); + const pendingActivities = $derived(workflow?.pendingActivities); + const pendingNexusOperations = $derived(workflow?.pendingNexusOperations); + + const groups = $derived( + groupEvents( + $fullEventHistory, + 'ascending', + pendingActivities, + pendingNexusOperations, + ), + ); + const nexusGroups = $derived( + groups.filter((group) => group.category === 'nexus' && group.links?.length), + ); + const inboundLinkEvents = $derived( + getInboundNexusLinkEvents($fullEventHistory), + ); + + +
+

+ {translate('workflows.nexus-links-tab')} +

+ {#if !inboundLinkEvents?.length && !nexusGroups.length} +

+ {translate('nexus.links-empty-state')} +

+ {/if} + {#if inboundLinkEvents?.length} +

Inbound

+ + + + + + + + + {#each inboundLinkEvents as event} + {@const link = getInboundLinkForEvent(event)} + + + + + + + {/each} +
{translate('workflows.workers-tab')}
{translate('nexus.caller-event')}{translate('nexus.caller-workflow')}{translate('nexus.caller-namespace')}{translate('nexus.handler-event')} + {#if link?.workflowEvent} + + {link.workflowEvent?.eventRef?.eventId} + + {/if} + + {#if link?.workflowEvent} + {link.workflowEvent.workflowId} + {/if} + + {#if link?.workflowEvent} + {link.workflowEvent.namespace} + {/if} + + {event.name} + ({event.id}) + +
+ {/if} + {#if nexusGroups.length} +

Outbound

+ + + + + + + + + + + + {#each nexusGroups as group} + {@const link = group.links?.[0]} + {@const scheduledEvent = group.eventList.find((e) => + isNexusOperationScheduledEvent(e), + )} + + + + + + + + + + {/each} +
{translate('workflows.workers-tab')}
{translate('nexus.source-event')}{translate('nexus.nexus-endpoint-simple')}{translate('nexus.nexus-service')}{translate('nexus.nexus-operation')}{translate('nexus.handler-namespace')}{translate('nexus.handler-workflow')}{translate('nexus.handler-event')} + {scheduledEvent.id} + + {scheduledEvent?.nexusOperationScheduledEventAttributes?.endpoint} + + {scheduledEvent?.nexusOperationScheduledEventAttributes?.service} + + {scheduledEvent?.nexusOperationScheduledEventAttributes?.operation} + + {#if link?.workflowEvent} + {link?.workflowEvent?.namespace} + {/if} + + {#if link?.workflowEvent} + {link.workflowEvent.workflowId} + {/if} + + {#if link?.workflowEvent} + {fromScreamingEnum( + link.workflowEvent?.eventRef?.eventType || + link.workflowEvent?.requestIdRef?.eventType, + 'EventType', + )} + {#if link.workflowEvent?.eventRef?.eventId} + ({link.workflowEvent.eventRef.eventId}) + {/if} + + {/if} +
+ {/if} +
diff --git a/src/lib/runes/inbound-nexus-links.svelte.ts b/src/lib/runes/inbound-nexus-links.svelte.ts new file mode 100644 index 0000000000..6b364109b2 --- /dev/null +++ b/src/lib/runes/inbound-nexus-links.svelte.ts @@ -0,0 +1,34 @@ +import type { WorkflowEvent } from '$lib/types/events'; +import { + isWorkflowExecutionOptionsUpdatedEvent, + isWorkflowExecutionStartedEvent, +} from '$lib/utilities/is-event-type'; + +export const getInboundNexusLinkEvents = (history: WorkflowEvent[]) => { + try { + const workflowExecutionStartedEvent = $derived( + history.find((event) => isWorkflowExecutionStartedEvent(event)), + ); + const workflowExecutionOptionsUpdatedEvents = $derived( + history.filter((event) => isWorkflowExecutionOptionsUpdatedEvent(event)), + ); + const matchingEvents = $derived([ + workflowExecutionStartedEvent, + ...workflowExecutionOptionsUpdatedEvents, + ]); + + return matchingEvents.filter(getInboundLinkForEvent); + } catch (error) { + return []; + } +}; + +export const getInboundLinkForEvent = (event: WorkflowEvent) => { + return ( + event?.links?.[0] || + (isWorkflowExecutionOptionsUpdatedEvent(event) && + event?.attributes?.attachedCompletionCallbacks?.[0]?.links?.[0]) || + (isWorkflowExecutionStartedEvent(event) && + event?.attributes?.completionCallbacks?.[0]?.links?.[0]) + ); +}; diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 3f82d35f97..1016906d0c 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -56,10 +56,7 @@ export type PendingActivityState = | 'CancelRequested'; export type PendingChildren = import('$lib/types').PendingChildrenInfo; -export type PendingNexusOperation = import('$lib/types').PendingNexusInfo & { - scheduledEventId: string; - scheduleToCloseTimeout: string; -}; +export type PendingNexusOperation = import('$lib/types').PendingNexusInfo; export type Callbacks = import('$lib/types').CallbackInfo[]; export type EventRequestMetadata = { @@ -111,7 +108,8 @@ export type CommonEventKey = | 'classification' | 'category' | 'workerMayIgnore' - | 'name'; + | 'name' + | 'links'; export type CommonHistoryEvent = Pick; diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index ca0014b768..ec32468af9 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -41,6 +41,10 @@ export type UpdateWorkflowRequest = temporal.api.workflowservice.v1.IUpdateWorkflowExecutionRequest; export type UpdateWorkflowResponse = temporal.api.workflowservice.v1.IUpdateWorkflowExecutionResponse; +export type PendingWorkflowTaskInfo = + temporal.api.workflow.v1.IPendingWorkflowTaskInfo; +export type WorkflowExtendedInfo = + temporal.api.workflow.v1.IWorkflowExecutionExtendedInfo; // api.history @@ -168,8 +172,6 @@ export type TaskReachability = temporal.api.enums.v1.TaskReachability; export type PendingNexusOperationState = temporal.api.enums.v1.PendingNexusOperationState; export type CallbackState = temporal.api.enums.v1.CallbackState; -export type PendingWorkflowTaskInfo = - temporal.api.workflow.v1.IPendingWorkflowTaskInfo; export type VersioningBehavior = temporal.api.enums.v1.VersioningBehavior; export type EventType = temporal.api.enums.v1.EventType; diff --git a/src/lib/types/workflows.ts b/src/lib/types/workflows.ts index 02705732aa..361885efa8 100644 --- a/src/lib/types/workflows.ts +++ b/src/lib/types/workflows.ts @@ -3,12 +3,13 @@ import type { Payloads, PendingWorkflowTaskInfo, WorkflowExecutionStatus, + WorkflowExtendedInfo, WorkflowVersionTimpstamp, } from '$lib/types'; +import type { Callback } from '$lib/types/nexus'; import type { VersioningInfo } from './deployments'; import type { - Callbacks, Payload, PendingActivity, PendingActivityInfo, @@ -78,22 +79,13 @@ export type UserMetadata = { details?: Payload; }; -export type WorkflowExecutionConfigWithMetadata = WorkflowExecutionConfig & { - userMetadata?: UserMetadata; -}; - -export type WorkflowExtendedInfo = { - resetRunId?: string; - originalStartTime?: string; -}; - export type WorkflowExecutionAPIResponse = Optional<{ workflowExecutionInfo: WorkflowExecutionInfo; pendingActivities: PendingActivityInfo[]; pendingChildren: PendingChildren[]; pendingNexusOperations: PendingNexusOperation[]; - executionConfig: WorkflowExecutionConfigWithMetadata; - callbacks: Callbacks; + executionConfig: WorkflowExecutionConfig; + callbacks: Callback[]; pendingWorkflowTask: PendingWorkflowTaskInfo; workflowExtendedInfo: WorkflowExtendedInfo; }>; @@ -188,7 +180,7 @@ export type WorkflowExecution = { isRunning: boolean; defaultWorkflowTaskTimeout: Duration; canBeTerminated: boolean; - callbacks: Callbacks; + callbacks: Callback[]; versioningInfo?: VersioningInfo; summary?: Payload; details?: Payload; diff --git a/src/lib/utilities/get-workflow-relationships.ts b/src/lib/utilities/get-workflow-relationships.ts index e8498540d6..23a738e0a7 100644 --- a/src/lib/utilities/get-workflow-relationships.ts +++ b/src/lib/utilities/get-workflow-relationships.ts @@ -1,4 +1,4 @@ -import type { DescribeNamespaceResponse } from '$lib/types'; +import type { DescribeNamespaceResponse, EventLink } from '$lib/types'; import type { ChildWorkflowExecutionCanceledEvent, ChildWorkflowExecutionCompletedEvent, @@ -134,3 +134,22 @@ export const getWorkflowRelationships = ( relationshipCount, }; }; + +export const getWorkflowNexusLinksFromHistory = ( + history: WorkflowEvents, +): EventLink[] => { + try { + const links = new Set(); + for (const event of history) { + if (event.category === 'nexus' && event.links && event.links.length > 0) { + for (const link of event.links) { + links.add(link); + } + } + } + + return Array.from(links); + } catch (error) { + return []; + } +}; diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts index e0559937d9..9cfaa4a5f4 100644 --- a/src/lib/utilities/route-for.ts +++ b/src/lib/utilities/route-for.ts @@ -263,6 +263,10 @@ export const routeForPendingActivities = ( return `${routeForWorkflow(parameters)}/pending-activities`; }; +export const routeForNexusLinks = (parameters: WorkflowParameters): string => { + return `${routeForWorkflow(parameters)}/nexus-links`; +}; + export const routeForAuthentication = ( parameters: AuthenticationParameters, ): string => { diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 862c6b2dc9..cd6b4e0164 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -158,7 +158,10 @@ icon: 'nexus', label: translate('nexus.nexus'), hidden: !$page.data?.systemInfo?.capabilities?.nexus, - isActive: (path) => path.includes(nexusRoute), + isActive: (path) => { + const match = path.split('/').find((segment) => segment === 'nexus'); + return !!match; + }, }, { href: historyImportRoute, diff --git a/src/routes/(app)/namespaces/[namespace]/workflows/[workflow]/[run]/nexus-links/+page.svelte b/src/routes/(app)/namespaces/[namespace]/workflows/[workflow]/[run]/nexus-links/+page.svelte new file mode 100644 index 0000000000..6b6820596c --- /dev/null +++ b/src/routes/(app)/namespaces/[namespace]/workflows/[workflow]/[run]/nexus-links/+page.svelte @@ -0,0 +1,18 @@ + + + + + +