diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index 768ba38bc..821492684 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -27,6 +27,7 @@ import { PagerData } from "./components/shared/pager/pager"; import { PanelPosition, PanelStateChangedArgs } from "./components/panel/models"; import { RenderActivityPropsContext } from "./modules/workflow-definitions/components/models"; import { ActivityDriverRegistry } from "./services"; +import { JournalItemSelectedArgs } from "./modules/workflow-instances/events"; import { PublishClickedArgs } from "./modules/workflow-definitions/components/publish-button"; export namespace Components { interface ElsaActivityInputEditorDialogContent { @@ -320,7 +321,7 @@ export namespace Components { "workflowInstance": WorkflowInstance; } interface ElsaWorkflowJournal { - "getExecutionLogByWorkflowInstanceId": (activityId: string) => Promise; + "getExecutionLogByActivityId": (activityId: string) => Promise; "workflowDefinition": WorkflowDefinition; "workflowInstance": WorkflowInstance; } @@ -1196,7 +1197,7 @@ declare namespace LocalJSX { "workflowInstance"?: WorkflowInstance; } interface ElsaWorkflowJournal { - "onJournalItemStatusSelected"?: (event: ElsaWorkflowJournalCustomEvent) => void; + "onJournalItemSelected"?: (event: ElsaWorkflowJournalCustomEvent) => void; "workflowDefinition"?: WorkflowDefinition; "workflowInstance"?: WorkflowInstance; } diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/journal.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/journal.tsx index a447f80cb..4c1a3166a 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/journal.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/journal.tsx @@ -1,5 +1,5 @@ import {Component, h, Method, Prop, State, Watch, Event, EventEmitter} from "@stencil/core"; -import {Workflow, WorkflowExecutionLogRecord, WorkflowInstance} from "../../../models"; +import {Activity, Workflow, WorkflowExecutionLogRecord, WorkflowInstance, Container as ActivityContainer} from "../../../models"; import {Container} from "typedi"; import {ActivityIconRegistry, ActivityNode, createActivityNodeMap, flatten, walkActivities} from "../../../services"; import {durationToString, formatTime, getDuration, Hash, isNullOrWhitespace} from "../../../utils"; @@ -7,6 +7,7 @@ import {ActivityExecutionEventBlock} from "../models"; import {ActivityIconSize} from "../../../components/icons/activities"; import {WorkflowDefinition} from "../../workflow-definitions/models/entities"; import {WorkflowInstancesApi} from "../services/workflow-instances-api"; +import { JournalItemSelectedArgs } from "../events"; // TODO: Implement dynamic loading of records. const PAGE_SIZE: number = 10000; @@ -32,7 +33,8 @@ export class Journal { @State() workflowExecutionLogRecords: Array = []; @State() rootBlocks: Array = []; @State() expandedBlocks: Array = []; - @Event() journalItemStatusSelected: EventEmitter; + @State() journalActivityMap: Hash = {}; + @Event() journalItemSelected: EventEmitter; @Watch('workflowInstance') async onWorkflowInstanceChanged(value: string) { @@ -48,13 +50,14 @@ export class Journal { } @Method() - public async getExecutionLogByWorkflowInstanceId(activityId: string): Promise { + public async getExecutionLogByActivityId(activityId: string): Promise { const logRecords = this.workflowExecutionLogRecords.filter(r => r.activityId === activityId); return logRecords.find(r => r.eventName === "Faulted" || r.eventName === "Completed") ?? logRecords.find(r => r.eventName === "Started"); } async componentWillLoad(): Promise { this.createGraph(); + this.journalActivityMap = this.createActivityMapForJournal(); await this.loadJournalPage(0); } @@ -102,13 +105,17 @@ export class Journal { } private renderBlocks = (blocks: Array) => { - const nodeMap = this.nodeMap; + const journalActivityMap = this.journalActivityMap; const iconRegistry = this.iconRegistry; const expandedBlocks = this.expandedBlocks; - return blocks.map((block, index) => { + var sortedBlocks = this.sortByTimestamp(blocks); + return sortedBlocks.map((block, index) => { + const activity = journalActivityMap[block.activityId]; + + if(activity.type == "Elsa.Workflow" || activity.type == "Elsa.Flowchart"){ + return this.renderBlocks(block.children); + } - const activityNode = nodeMap[block.activityId]; - const activity = activityNode.activity; const activityMetadata = activity.metadata; const activityDisplayText = isNullOrWhitespace(activityMetadata.displayText) ? activity.id : activityMetadata.displayText; const duration = durationToString(block.duration); @@ -141,23 +148,23 @@ export class Journal { {toggleIcon} ) : undefined} - {formatTime(block.timestamp)} + + this.onJournalItemClick(e, block, activity)}>{formatTime(block.timestamp)} + - {block.children.length == 0 ? - ( this.onStatusClick(e, block)} class={`inline-flex rounded-full ${statusColor} px-2 text-xs font-semibold leading-5 text-green-800`}>{status}) - : {status}} + this.onJournalItemClick(e, block, activity)} class={`inline-flex rounded-full ${statusColor} px-2 text-xs font-semibold leading-5 text-green-800`}>{status} - {duration} + this.onJournalItemClick(e, block, activity)}>{duration} , expanded ? this.renderBlocks(block.children) : undefined] ); }); @@ -221,19 +228,13 @@ export class Journal { }); for (const block of blocks) { - // For now, only get child blocks if the associated activity actually has child nodes as well. - // If not, it means this is a composed activity for which we did not load it child nodes. - // This is something we might want to reconsider in a future iteration. - const activityNode = this.nodeMap[block.activityId]; - if (activityNode?.children.length > 0) - block.children = this.findChildBlocks(blocks, block.activityInstanceId); + block.children = this.findChildBlocks(blocks, block.activityInstanceId); } return blocks; }; private findChildBlocks = (blocks: Array, parentActivityInstanceId?: string): Array => { - if (blocks.length == 0) return []; @@ -249,8 +250,47 @@ export class Journal { this.expandedBlocks = existingBlock ? this.expandedBlocks.filter(x => x != existingBlock) : [...this.expandedBlocks, block]; }; - private onStatusClick = async (e: MouseEvent, block: ActivityExecutionEventBlock) => { + private onJournalItemClick = async (e: MouseEvent, block: ActivityExecutionEventBlock, activity: Activity) => { e.preventDefault(); - this.journalItemStatusSelected.emit(block.activityId); + this.journalItemSelected.emit({activity: activity, activityInstanceId: block.activityInstanceId}); }; -} + + private sortByTimestamp(blocks: ActivityExecutionEventBlock[]) { + return blocks.sort(function (x, y) { + if(x.timestamp > y.timestamp) + return 1; + return -1; + }); + } + + private createActivityMapForJournal() { + let allActivities = new Array(); + allActivities.push({id: 'Workflow1', type: 'Elsa.Workflow', version: null, metadata: {}, customProperties: null}); + + allActivities = this.walkActivitiesRecursive(this.workflowDefinition.root as ActivityContainer, allActivities); + + const map = {}; + for (const activity of allActivities) + map[activity.id] = activity; + return map; + } + + private walkActivitiesRecursive(activityContainer: ActivityContainer, allActivities: Array) { + allActivities.push(activityContainer as Activity); + if(activityContainer.activities == null || activityContainer.activities.length == 0) { + if(activityContainer.root != null) { + allActivities.push(activityContainer.root as Activity); + allActivities = this.walkActivitiesRecursive(activityContainer.root, allActivities); + } + } + else { + allActivities = allActivities.concat(activityContainer.activities); + activityContainer.activities.forEach(activity => { + if(activity.root != null){ + allActivities = this.walkActivitiesRecursive(activity.root, allActivities); + } + }); + } + return allActivities; + } +} \ No newline at end of file diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/viewer.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/viewer.tsx index 4e09a0dee..975cdaaae 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/viewer.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/viewer.tsx @@ -13,6 +13,7 @@ import {PluginRegistry, ActivityNameFormatter, ActivityDriverRegistry, EventBus} import {MonacoEditorSettings} from "../../../services/monaco-editor-settings"; import {WorkflowDefinition} from "../../workflow-definitions/models/entities"; import {WorkflowEditorEventTypes} from "../../workflow-definitions/models/ui"; +import { JournalItemSelectedArgs } from '../events'; import {JournalApi} from "../services/journal-api"; @Component({ @@ -27,7 +28,6 @@ export class WorkflowInstanceViewer { private flowchartElement: HTMLElsaFlowchartElement; private container: HTMLDivElement; private workflowJournalElement: HTMLElsaWorkflowJournalElement; - private activityPropertiesElement: HTMLElsaActivityPropertiesElement; constructor() { this.eventBus = Container.get(EventBus); @@ -44,7 +44,6 @@ export class WorkflowInstanceViewer { @State() private workflowInstanceState: WorkflowInstance; @State() private selectedActivity?: Activity; @State() private selectedActivityExecutionLog?: WorkflowExecutionLogRecord; - @State() private activityPropertyTabIndex?: number; @Watch('monacoLibPath') private handleMonacoLibPath(value: string) { @@ -86,22 +85,22 @@ export class WorkflowInstanceViewer { this.selectedActivityExecutionLog = await this.journalApi.getLastEntry({workflowInstanceId, activityId}); } - @Listen('journalItemStatusSelected') - private async handleJournalStatusSelected(e: CustomEvent) { + @Listen('journalItemSelected') + private async handleJournalItemSelected(e: CustomEvent) { const activityId = e.detail; const graph = await this.flowchartElement.getGraph(); - const node = graph.getNodes().find(n => n.id == activityId) + const node = graph.getNodes().find(n => n.id == e.detail.activity.id) if (node != null) { graph.resetSelection(node); this.selectedActivity = node.data; - this.selectedActivityExecutionLog = await this.workflowJournalElement.getExecutionLogByWorkflowInstanceId(this.selectedActivity.id); - if (this.activityPropertiesElement == null) { - this.activityPropertyTabIndex = 2; - } else { - await this.activityPropertiesElement.updateSelectedTab(2); - } } + else { + graph.resetSelection(); + this.selectedActivity = e.detail.activity; + } + + this.selectedActivityExecutionLog = await this.workflowJournalElement.getExecutionLogByActivityId(e.detail.activity.id); } @Listen('graphUpdated') @@ -157,7 +156,7 @@ export class WorkflowInstanceViewer { private renderSelectedObject = () => { const activity = this.selectedActivity; if (!!activity) - return this.activityPropertiesElement = el}/>; + return ; } private getWorkflowInternal = async (): Promise => { diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/events.ts b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/events.ts new file mode 100644 index 000000000..54e6a9b1d --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/events.ts @@ -0,0 +1,6 @@ +import { Activity } from "../../models"; + +export interface JournalItemSelectedArgs { + activity: Activity; + activityInstanceId: string; +} \ No newline at end of file diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/models.ts b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/models.ts index 8e0001602..efc3601c0 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/models.ts +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/models.ts @@ -49,4 +49,4 @@ export interface ActivityExecutionEventBlock { completedRecord?: WorkflowExecutionLogRecord; faultedRecord?: WorkflowExecutionLogRecord; children: Array; -} +} \ No newline at end of file