From cce9c79d1658f7cab69e1b1b959166258c3429eb Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Fri, 20 Dec 2024 18:46:15 +0100 Subject: [PATCH 01/68] Add basic components for the workflow editor --- src/app/app.module.ts | 2 + .../default-layout.component.html | 7 ++ .../workflow-session.component.html | 2 + .../workflow-session.component.scss | 0 .../workflow-session.component.ts | 41 ++++++ .../workflow-viewer.component.html | 1 + .../workflow-viewer.component.scss | 0 .../workflow-viewer.component.ts | 12 ++ .../workflows-dashboard.component.html | 66 ++++++++++ .../workflows-dashboard.component.scss | 0 .../workflows-dashboard.component.ts | 62 ++++++++++ .../workflows/models/workflows.model.ts | 117 ++++++++++++++++++ .../workflows/services/workflows.service.ts | 69 +++++++++++ src/app/plugins/workflows/workflows.module.ts | 34 +++++ src/app/services/webui-settings.service.ts | 4 + src/app/views/views-routing.module.ts | 10 ++ 16 files changed, 427 insertions(+) create mode 100644 src/app/plugins/workflows/components/workflow-session/workflow-session.component.html create mode 100644 src/app/plugins/workflows/components/workflow-session/workflow-session.component.scss create mode 100644 src/app/plugins/workflows/components/workflow-session/workflow-session.component.ts create mode 100644 src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html create mode 100644 src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss create mode 100644 src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts create mode 100644 src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html create mode 100644 src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.scss create mode 100644 src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.ts create mode 100644 src/app/plugins/workflows/models/workflows.model.ts create mode 100644 src/app/plugins/workflows/services/workflows.service.ts create mode 100644 src/app/plugins/workflows/workflows.module.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f6daff46..e9e70fb0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -77,6 +77,7 @@ import {ModalModule} from 'ngx-bootstrap/modal'; import {NgxJsonViewerModule} from 'ngx-json-viewer'; import {NotebooksModule} from './plugins/notebooks/notebooks.module'; import {IconDirective} from '@coreui/icons-angular'; +import {WorkflowsModule} from './plugins/workflows/workflows.module'; @NgModule({ @@ -90,6 +91,7 @@ import {IconDirective} from '@coreui/icons-angular'; NgChartsModule, // plugins NotebooksModule, + WorkflowsModule, ToastComponent, NgChartsModule, ToasterComponent, diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index 68064b21..624a34a6 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -75,6 +75,13 @@ + + + + Workflows + + + diff --git a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html new file mode 100644 index 00000000..9b2766fd --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html @@ -0,0 +1,2 @@ +

Session ID: {{sessionId}}

+

{{session | json}}

\ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.scss b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.ts b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.ts new file mode 100644 index 00000000..c96a75d7 --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.ts @@ -0,0 +1,41 @@ +import {Component, inject, OnDestroy, OnInit} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; +import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; +import {LeftSidebarService} from '../../../../components/left-sidebar/left-sidebar.service'; +import {WorkflowsService} from '../../services/workflows.service'; +import {SessionModel} from '../../models/workflows.model'; + +@Component({ + selector: 'app-workflow-session', + templateUrl: './workflow-session.component.html', + styleUrl: './workflow-session.component.scss' +}) +export class WorkflowSessionComponent implements OnInit, OnDestroy { + private readonly _route = inject(ActivatedRoute); + private readonly _router = inject(Router); + private readonly _toast = inject(ToasterService); + private readonly _sidebar = inject(LeftSidebarService); + private readonly _workflows = inject(WorkflowsService); + + sessionId: string; + session: SessionModel; + + ngOnInit(): void { + this._sidebar.hide(); + + this._route.paramMap.subscribe(params => { + this.sessionId = params.get('sessionId'); + if (this.sessionId) { + this._workflows.getSession(this.sessionId).subscribe({ + next: res => this.session = res, + error: e => this._router.navigate(['./../'], {relativeTo: this._route}) + }); + } + }); + + this.sessionId = this._route.snapshot.paramMap.get('sessionId'); + } + + ngOnDestroy(): void { + } +} diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html new file mode 100644 index 00000000..f3a83729 --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html @@ -0,0 +1 @@ +

workflow-viewer works!

diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts new file mode 100644 index 00000000..053245ef --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts @@ -0,0 +1,12 @@ +import {Component, inject} from '@angular/core'; +import {WorkflowsService} from '../../services/workflows.service'; + +@Component({ + selector: 'app-workflow-viewer', + templateUrl: './workflow-viewer.component.html', + styleUrl: './workflow-viewer.component.scss' +}) +export class WorkflowViewerComponent { + private readonly _workflows = inject(WorkflowsService); + +} diff --git a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html new file mode 100644 index 00000000..4def5cbf --- /dev/null +++ b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html @@ -0,0 +1,66 @@ +

Workflow Dashboard

+ +

Stored Workflows

+ + + + + + + + +
+
+ + +
+ +
+
+ Description: {{ workflowDef.value.versions[selectedVersion[workflowDef.key]].description }} +
+
+ Creation Time: {{ workflowDef.value.versions[selectedVersion[workflowDef.key]].creationTime | date:'short' }} +
+ + +
+ +
+
+
+
+
+ +

Active Sessions

+
    + +
  • +
    +
    + + {{workflowDefs[session.value.workflowId].name}} + v{{session.value.version}} + + + {{session.value.type}} + + +
    + SessionId: {{ session.value.sessionId }}
    + Connected Users: {{ session.value.connectionCount }}
    +
    +
    + +
    +
  • +
    +
diff --git a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.scss b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.ts b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.ts new file mode 100644 index 00000000..41a5873a --- /dev/null +++ b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.ts @@ -0,0 +1,62 @@ +import {Component, inject, OnDestroy, OnInit} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; +import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; +import {LeftSidebarService} from '../../../../components/left-sidebar/left-sidebar.service'; +import {WorkflowsService} from '../../services/workflows.service'; +import {SessionModel, WorkflowDefModel} from '../../models/workflows.model'; + +@Component({ + selector: 'app-workflows-dashboard', + templateUrl: './workflows-dashboard.component.html', + styleUrl: './workflows-dashboard.component.scss' +}) +export class WorkflowsDashboardComponent implements OnInit, OnDestroy { + private readonly _route = inject(ActivatedRoute); + private readonly _router = inject(Router); + private readonly _toast = inject(ToasterService); + private readonly _sidebar = inject(LeftSidebarService); + private readonly _workflows = inject(WorkflowsService); + + workflowDefs: Record; + sessions: Record; + selectedVersion: Record = {}; + + ngOnInit(): void { + this._sidebar.hide(); + + this._workflows.getSessions().subscribe({ + next: res => this.sessions = res + }); + this._workflows.getWorkflowDefs().subscribe({ + next: res => { + console.log(res); + this.workflowDefs = res; + for (const version in this.workflowDefs) { + + } + Object.entries(res).forEach(([key, value]) => { + this.selectedVersion[key] = Math.max(...Object.keys(value.versions).map(versionId => parseInt(versionId, 10))); + }); + } + }); + } + + ngOnDestroy(): void { + } + + onVersionChange(workflowName: string) { + // Logic when a version is selected (optional) + console.log('Selected version for', workflowName, this.selectedVersion[workflowName]); + } + + openVersion(key: string) { + console.log('Opening Version'); + this._workflows.openWorkflow(key, this.selectedVersion[key]).subscribe({ + next: sessionId => this.openSession(sessionId) + }); + } + + openSession(sessionId: string) { + this._router.navigate([`./${sessionId}`], {relativeTo: this._route}); + } +} diff --git a/src/app/plugins/workflows/models/workflows.model.ts b/src/app/plugins/workflows/models/workflows.model.ts new file mode 100644 index 00000000..5951da48 --- /dev/null +++ b/src/app/plugins/workflows/models/workflows.model.ts @@ -0,0 +1,117 @@ +import {DataModel} from '../../../models/ui-request.model'; +import {FieldDefinition} from '../../../components/data-view/models/result-set.model'; + +export enum EdgeState { + IDLE = 'IDLE', + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE' +} + +export enum CommonType { + NONE = 'NONE', + EXTRACT = 'EXTRACT', + LOAD = 'LOAD' +} + +export enum ControlStateMerger { + AND_OR = 'AND_OR', + AND_AND = 'AND_AND' +} + +export enum ActivityState { + IDLE = 'IDLE', + QUEUED = 'QUEUED', + EXECUTING = 'EXECUTING', + SKIPPED = 'SKIPPED', + FAILED = 'FAILED', + FINISHED = 'FINISHED', + SAVED = 'SAVED' +} + +export enum WorkflowState { + IDLE = 'IDLE', + EXECUTING = 'EXECUTING', + INTERRUPTED = 'INTERRUPTED' +} + +export enum SessionModelType { + USER_SESSION = 'USER_SESSION', + API_SESSION = 'API_SESSION', + JOB_SESSION = 'JOB_SESSION' +} + +export type Settings = Record; + +export interface EdgeModel { + fromId: string; + toId: string; + fromPort: number; + toPort: number; + isControl: boolean; + state?: EdgeState; // only non-null when receiving, not when referencing an edge from the frontend +} + +export interface ActivityConfigModel { + enforceCheckpoint: boolean; + preferredStores: string[]; + commonType: CommonType; + controlStateMerger: ControlStateMerger; +} + +export interface RenderModel { + posX: number; + posY: number; + name: string; + notes: string; +} + +export interface TypePreviewModel { + dataModel: DataModel; + fields: FieldDefinition[]; +} + +export interface ActivityModel { + type: string; + id: string; + settings: Settings; + config: ActivityConfigModel; + rendering: RenderModel; + state?: ActivityState; + inTypePreview?: TypePreviewModel[]; + invalidReason?: string; +} + +export interface SessionModel { + type: SessionModelType; + sessionId: string; + connectionCount: number; + workflowId?: string; // Only for USER_SESSION + version?: number; // Only for USER_SESSION +} + +export interface WorkflowDefModel { + name: string; + versions: Record; +} + +export interface VersionInfo { + description: string; + creationTime: Date; +} + +export interface WorkflowModel { + format_version: string; + activities: ActivityModel[]; + edges: EdgeModel[]; + config: WorkflowConfigModel; + variables: Settings; + state?: WorkflowState; +} + +export interface WorkflowConfigModel { + preferredStores: Record; + fusionEnabled: boolean; + pipelineEnabled: boolean; + maxWorkers: number; + pipelineQueueCapacity: number; +} diff --git a/src/app/plugins/workflows/services/workflows.service.ts b/src/app/plugins/workflows/services/workflows.service.ts new file mode 100644 index 00000000..5eac819d --- /dev/null +++ b/src/app/plugins/workflows/services/workflows.service.ts @@ -0,0 +1,69 @@ +import {Injectable} from '@angular/core'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {WebuiSettingsService} from '../../../services/webui-settings.service'; +import {ActivityModel, SessionModel, WorkflowConfigModel, WorkflowDefModel, WorkflowModel} from '../models/workflows.model'; + +class JsonNode { +} + +@Injectable({ + providedIn: 'root' +}) +export class WorkflowsService { + + constructor(private _http: HttpClient, private _settings: WebuiSettingsService) { + } + + private httpUrl = this._settings.getConnection('workflows.rest'); + private httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; + + getSessions() { + return this._http.get>(`${this.httpUrl}/sessions`, this.httpOptions); + } + + getSession(sessionId: string) { + return this._http.get(`${this.httpUrl}/sessions/${sessionId}`, this.httpOptions); + } + + getActiveWorkflow(sessionId: string) { + return this._http.get(`${this.httpUrl}/sessions/${sessionId}/workflow`, this.httpOptions); + } + + getWorkflowConfig(sessionId: string) { + return this._http.get(`${this.httpUrl}/sessions/${sessionId}/workflow/config`, this.httpOptions); + } + + getActivity(sessionId: string, activityId: string) { + return this._http.get(`${this.httpUrl}/sessions/${sessionId}/workflow/${activityId}`, this.httpOptions); + } + + getIntermediaryResult(sessionId: string, activityId: string, outIndex: number) { + return this._http.get(`${this.httpUrl}/sessions/${sessionId}/workflow/${activityId}/${outIndex}`, this.httpOptions); + } + + getWorkflowDefs() { + return this._http.get>(`${this.httpUrl}/workflows`, this.httpOptions); + } + + createSession(workflowName: string) { + const json = { + name: workflowName + }; + return this._http.post(`${this.httpUrl}/sessions`, json, this.httpOptions); + } + + openWorkflow(workflowId: string, version: number) { + return this._http.post(`${this.httpUrl}/workflows/${workflowId}/${version}`, {}, this.httpOptions); + } + + saveSession(sessionId: string, saveMessage: string) { + const json = { + message: saveMessage + }; + return this._http.post(`${this.httpUrl}/sessions/${sessionId}`, json, this.httpOptions); + } + + terminateSession(sessionId: string) { + return this._http.delete(`${this.httpUrl}/sessions/${sessionId}`, this.httpOptions); + } +} diff --git a/src/app/plugins/workflows/workflows.module.ts b/src/app/plugins/workflows/workflows.module.ts new file mode 100644 index 00000000..afd0e23e --- /dev/null +++ b/src/app/plugins/workflows/workflows.module.ts @@ -0,0 +1,34 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {WorkflowViewerComponent} from './components/workflow-viewer/workflow-viewer.component'; +import {ReteModule} from 'rete-angular-plugin/17'; +import {WorkflowsDashboardComponent} from './components/workflows-dashboard/workflows-dashboard.component'; +import {WorkflowSessionComponent} from './components/workflow-session/workflow-session.component'; +import {AccordionButtonDirective, AccordionComponent, AccordionItemComponent, ListGroupDirective, ListGroupItemDirective, TemplateIdDirective} from '@coreui/angular'; +import {FormsModule} from '@angular/forms'; + + +@NgModule({ + imports: [ + CommonModule, + ReteModule, + AccordionComponent, + AccordionItemComponent, + AccordionButtonDirective, + TemplateIdDirective, + FormsModule, + ListGroupDirective, + ListGroupItemDirective, + ], + declarations: [ + WorkflowViewerComponent, + WorkflowsDashboardComponent, + WorkflowSessionComponent + ], + exports: [ + WorkflowViewerComponent, + WorkflowsDashboardComponent + ] +}) +export class WorkflowsModule { +} diff --git a/src/app/services/webui-settings.service.ts b/src/app/services/webui-settings.service.ts index 561f9fd5..1421d62a 100644 --- a/src/app/services/webui-settings.service.ts +++ b/src/app/services/webui-settings.service.ts @@ -42,6 +42,10 @@ export class WebuiSettingsService { 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/notebooks/webSocket'); this.connections.set('notebooks.file', 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/notebooks/file'); + this.connections.set('workflows.rest', + 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/workflows'); + this.connections.set('workflows.socket', + 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/workflows/webSocket'); } diff --git a/src/app/views/views-routing.module.ts b/src/app/views/views-routing.module.ts index e40afa30..0ea88751 100644 --- a/src/app/views/views-routing.module.ts +++ b/src/app/views/views-routing.module.ts @@ -13,6 +13,8 @@ import {QueryInterfacesComponent} from './query-interfaces/query-interfaces.comp import {NotebooksComponent} from '../plugins/notebooks/components/notebooks.component'; import {UnsavedChangesGuard} from '../plugins/notebooks/services/unsaved-changes.guard'; import {DockerconfigComponent} from './dockerconfig/dockerconfig.component'; +import {WorkflowsDashboardComponent} from '../plugins/workflows/components/workflows-dashboard/workflows-dashboard.component'; +import {WorkflowSessionComponent} from '../plugins/workflows/components/workflow-session/workflow-session.component'; const routes: Routes = [ { @@ -181,6 +183,14 @@ const routes: Routes = [ } ] }, + { + path: 'workflows', + component: WorkflowsDashboardComponent, + }, + { + path: 'workflows/:sessionId', + component: WorkflowSessionComponent + } ]; @NgModule({ From bba7b88ca85ebce9bce62426c05d70c47668e899 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Mon, 23 Dec 2024 12:56:00 +0100 Subject: [PATCH 02/68] Add workflow editor components --- .../polyalg-viewer/panning-boundary/index.ts | 5 +- .../workflow-session.component.html | 5 +- .../activity-port.component.scss | 18 +++ .../activity-port/activity-port.component.ts | 39 +++++++ .../editor/activity/activity.component.html | 53 +++++++++ .../editor/activity/activity.component.scss | 73 ++++++++++++ .../editor/activity/activity.component.ts | 49 ++++++++ .../editor/edge/edge.component.html | 3 + .../editor/edge/edge.component.scss | 12 ++ .../editor/edge/edge.component.ts | 23 ++++ .../workflow-viewer/editor/vars.scss | 3 + .../editor/workflow-editor-utils.ts | 54 +++++++++ .../workflow-viewer/editor/workflow-editor.ts | 110 ++++++++++++++++++ .../workflow-viewer.component.html | 6 + .../workflow-viewer.component.scss | 4 + .../workflow-viewer.component.ts | 39 ++++++- .../workflows-dashboard.component.html | 9 +- .../workflows-dashboard.component.ts | 23 ++-- .../workflows/models/workflows.model.ts | 2 +- src/app/plugins/workflows/workflows.module.ts | 11 +- 20 files changed, 523 insertions(+), 18 deletions(-) create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/activity-port/activity-port.component.scss create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/activity-port/activity-port.component.ts create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.html create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.scss create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.ts create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.html create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.scss create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.ts create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/vars.scss create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor-utils.ts create mode 100644 src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor.ts diff --git a/src/app/components/polyalg/polyalg-viewer/panning-boundary/index.ts b/src/app/components/polyalg/polyalg-viewer/panning-boundary/index.ts index d60638a5..85cd9318 100644 --- a/src/app/components/polyalg/polyalg-viewer/panning-boundary/index.ts +++ b/src/app/components/polyalg/polyalg-viewer/panning-boundary/index.ts @@ -1,11 +1,10 @@ -import {NodeEditor} from 'rete'; +import {GetSchemes, NodeEditor} from 'rete'; import {AreaExtensions, AreaPlugin} from 'rete-area-plugin'; import {getFrameWeight} from './frame'; import {animate, watchPointerMove} from './utils'; -import {Schemes} from '../alg-editor'; interface Props { - area: AreaPlugin; + area: AreaPlugin, any>; selector: AreaExtensions.Selector; intensity?: number; padding?: number; diff --git a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html index 9b2766fd..d6a4546c 100644 --- a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html +++ b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html @@ -1,2 +1,5 @@

Session ID: {{sessionId}}

-

{{session | json}}

\ No newline at end of file + + + + \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/activity-port/activity-port.component.scss b/src/app/plugins/workflows/components/workflow-viewer/editor/activity-port/activity-port.component.scss new file mode 100644 index 00000000..e527fd90 --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/activity-port/activity-port.component.scss @@ -0,0 +1,18 @@ +@use "sass:math"; +@import "../vars"; + +:host { + display: inline-block; + cursor: pointer; + border: 1px solid grey; + width: $socket-size; + height: $socket-size * 2; + vertical-align: middle; + background: #fff; + z-index: 2; + box-sizing: border-box; + + &:hover { + background: #ddd; + } +} \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/activity-port/activity-port.component.ts b/src/app/plugins/workflows/components/workflow-viewer/editor/activity-port/activity-port.component.ts new file mode 100644 index 00000000..b24ccc8b --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/activity-port/activity-port.component.ts @@ -0,0 +1,39 @@ +import {ChangeDetectorRef, Component, HostBinding, Input, OnChanges} from '@angular/core'; +import {ClassicPreset} from 'rete'; + +@Component({ + selector: 'app-activity-port', + standalone: true, + imports: [], + template: ``, + styleUrl: './activity-port.component.scss' +}) +export class ActivityPortComponent implements OnChanges { + @Input() data!: ActivityPort; + @Input() rendered!: any; + + @HostBinding('title') get title() { + return this.data.name; + } + + constructor(private cdr: ChangeDetectorRef) { + this.cdr.detach(); + } + + ngOnChanges(): void { + this.cdr.detectChanges(); + requestAnimationFrame(() => this.rendered()); + } + +} + +export class ActivityPort extends ClassicPreset.Socket { + + constructor() { + super(''); + } + + isCompatibleWith(socket: ActivityPort) { + return true; // TODO: change to computation based on porttype + } +} \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.html b/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.html new file mode 100644 index 00000000..a3573b12 --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.html @@ -0,0 +1,53 @@ +
{{data.label}}
+
+
+ {{output.value?.label}} +
+
+
+
+
+
+
+ {{input.value?.label}} +
+
+
diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.scss b/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.scss new file mode 100644 index 00000000..f6ba4aa5 --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.scss @@ -0,0 +1,73 @@ +@use "sass:math"; +@import "../vars"; + +:host { + display: block; + background: black; + border: 2px solid grey; + border-radius: 10px; + cursor: pointer; + box-sizing: border-box; + width: $node-width; + height: auto; + padding-bottom: 6px; + position: relative; + user-select: none; + + &:hover { + background: #333; + } + + &.selected { + border-color: red; + } + + .title { + color: white; + font-family: sans-serif; + font-size: 18px; + padding: 8px; + } + + .output { + text-align: right; + } + + .input { + text-align: left; + } + + .output-socket { + text-align: right; + margin-right: -1px; + display: inline-block; + } + + .input-socket { + text-align: left; + margin-left: -1px; + display: inline-block; + } + + .input-title, + .output-title { + vertical-align: middle; + color: white; + display: inline-block; + font-family: sans-serif; + font-size: 14px; + margin: $socket-margin; + line-height: $socket-size; + } + + .input-control { + z-index: 1; + width: calc(100% - #{$socket-size + 2*$socket-margin}); + vertical-align: middle; + display: inline-block; + } + + .control { + padding: $socket-margin math.div($socket-size, 2) + $socket-margin; + } +} \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.ts b/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.ts new file mode 100644 index 00000000..b12648aa --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/activity/activity.component.ts @@ -0,0 +1,49 @@ +import {ChangeDetectorRef, Component, HostBinding, Input, OnChanges} from '@angular/core'; +import {ClassicPreset} from 'rete'; +import {KeyValue} from '@angular/common'; + +@Component({ + selector: 'app-activity', + templateUrl: './activity.component.html', + styleUrl: './activity.component.scss' +}) +export class ActivityComponent implements OnChanges { + @Input() data!: ActivityNode; + @Input() emit!: (data: any) => void; + @Input() rendered!: () => void; + seed = 0; + + @HostBinding('class.selected') get selected() { + return this.data.selected; + } + + constructor(private cdr: ChangeDetectorRef) { + this.cdr.detach(); + } + + ngOnChanges(): void { + this.cdr.detectChanges(); + requestAnimationFrame(() => this.rendered()); + this.seed++; // force render sockets + } + + sortByIndex< + N extends object, + T extends KeyValue + >(a: T, b: T) { + const ai = a.value.index || 0; + const bi = b.value.index || 0; + + return ai - bi; + } + +} + +export class ActivityNode extends ClassicPreset.Node { + width = 100; + height = 100; // TODO: change dimensions + + constructor() { + super('ActivityNode Sample Label'); + } +} diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.html b/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.html new file mode 100644 index 00000000..e53abb7f --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.scss b/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.scss new file mode 100644 index 00000000..a4c39158 --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.scss @@ -0,0 +1,12 @@ +svg { + overflow: visible; + position: absolute; + pointer-events: none; + + path { + fill: none; + stroke-width: 5px; + stroke: green; + pointer-events: auto; + } +} \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.ts b/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.ts new file mode 100644 index 00000000..d87ec723 --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/edge/edge.component.ts @@ -0,0 +1,23 @@ +import {Component, Input} from '@angular/core'; +import {ClassicPreset} from 'rete'; +import {ActivityNode} from '../activity/activity.component'; + +@Component({ + selector: 'app-edge', + templateUrl: './edge.component.html', + styleUrl: './edge.component.scss' +}) +export class EdgeComponent { + @Input() data!: Edge; + @Input() start: any; + @Input() end: any; + @Input() path: string; +} + +export class Edge extends ClassicPreset.Connection { + isMagnetic = false; // TODO: why is this required? + + constructor(source: N, sourceOutput: keyof N['outputs'], target: N, targetInput: keyof N['inputs']) { + super(source, sourceOutput, target, targetInput); + } +} diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/vars.scss b/src/app/plugins/workflows/components/workflow-viewer/editor/vars.scss new file mode 100644 index 00000000..475c764c --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/vars.scss @@ -0,0 +1,3 @@ +$node-width: 200px; +$socket-margin: 6px; +$socket-size: 16px; \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor-utils.ts b/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor-utils.ts new file mode 100644 index 00000000..4561e8cf --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor-utils.ts @@ -0,0 +1,54 @@ +import {NodeEditor} from 'rete'; +import {SocketData} from 'rete-connection-plugin'; +import {Position} from 'rete-angular-plugin/17/types'; +import {Schemes} from './workflow-editor'; +import {Edge} from './edge/edge.component'; + + +// TODO: implement workflow-editor specific changes +export function getMagneticConnectionProps(editor: NodeEditor) { + return { + async createConnection(from: SocketData, to: SocketData) { + if (from.side === to.side) { + return; + } + const [source, target] = from.side === 'output' ? [from, to] : [to, from]; + const sourceNode = editor.getNode(source.nodeId); + const targetNode = editor.getNode(target.nodeId); + + const connection = new Edge( + sourceNode, + source.key as never, + targetNode, + target.key as never + ); + + /*if (!canCreateConnection(editor, connection)) { + return; + } + + const connectionsToRemove = editor.getConnections().filter(c => { + return (c.target === targetNode.id && c.targetInput === target.key && !targetNode.hasVariableInputs) || (c.source === sourceNode.id); + }); + + for (const c of connectionsToRemove) { + await editor.removeConnection(c.id); + }*/ + + await editor.addConnection( + connection + ); + }, + display(from: SocketData, to: SocketData) { + return from.side !== to.side; //&& areSocketsCompatible(editor, from, to); + }, + offset(socket: SocketData, position: Position) { + + return { + x: position.x + (socket.side === 'input' ? 3 : -3), + y: position.y + (socket.side === 'input' ? 12 : -12) + }; + }, + distance: 75 + }; +} diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor.ts b/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor.ts new file mode 100644 index 00000000..079bb8dd --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor.ts @@ -0,0 +1,110 @@ +import {Injector} from '@angular/core'; +import {ClassicPreset, GetSchemes, NodeEditor} from 'rete'; +import {AreaExtensions, AreaPlugin} from 'rete-area-plugin'; +import {ConnectionPlugin, Presets as ConnectionPresets} from 'rete-connection-plugin'; +import {AngularArea2D, AngularPlugin, Presets} from 'rete-angular-plugin/15'; +import {WorkflowModel} from '../../../models/workflows.model'; +import {ActivityComponent, ActivityNode} from './activity/activity.component'; +import {Edge, EdgeComponent} from './edge/edge.component'; +import {ActivityPort, ActivityPortComponent} from './activity-port/activity-port.component'; +import {addCustomBackground} from '../../../../../components/polyalg/polyalg-viewer/background'; +import {ReadonlyPlugin} from 'rete-readonly-plugin'; +import {useMagneticConnection} from '../../../../../components/polyalg/polyalg-viewer/magnetic-connection'; +import {setupPanningBoundary} from '../../../../../components/polyalg/polyalg-viewer/panning-boundary'; +import {MagneticConnectionComponent} from '../../../../../components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component'; +import {getMagneticConnectionProps} from './workflow-editor-utils'; + +export type Schemes = GetSchemes>; +type AreaExtra = AngularArea2D; + +export class WorkflowEditor { + private readonly editor: NodeEditor = new NodeEditor(); + private readonly connection: ConnectionPlugin = new ConnectionPlugin(); + private readonly readonlyPlugin = new ReadonlyPlugin(); + private readonly area: AreaPlugin; + private readonly render: AngularPlugin; + + private readonly socket = new ActivityPort(); + + constructor(private injector: Injector, container: HTMLElement, isReadOnly: boolean) { + this.area = new AreaPlugin(container); + this.render = new AngularPlugin({injector}); + + const selector = AreaExtensions.selector(); + AreaExtensions.selectableNodes(this.area, selector, { + accumulating: AreaExtensions.accumulateOnCtrl(), + }); + + this.render.addPreset( + Presets.classic.setup({ + customize: { + node() { + return ActivityComponent; + }, + connection(data) { + if (data.payload.isMagnetic) { + return MagneticConnectionComponent; + } + return EdgeComponent; + }, + socket() { + return ActivityPortComponent; + }, + }, + }) + ); + + this.connection.addPreset(ConnectionPresets.classic.setup()); + + + // Attach plugins + this.editor.use(this.readonlyPlugin.root); + this.editor.use(this.area); + this.area.use(this.connection); + this.area.use(this.render); + this.area.use(this.readonlyPlugin.area); + + + let panningBoundary = null; + if (!isReadOnly) { + this.area.use(this.connection); // make connections editable + //this.area.use(this.contextMenu); // add context menu + useMagneticConnection(this.connection, getMagneticConnectionProps(this.editor)); + panningBoundary = setupPanningBoundary({area: this.area, selector, padding: 40, intensity: 2}); + } + + AreaExtensions.restrictor(this.area, {scaling: {min: 0.03, max: 5}}); // Restrict Zoom + + this.area.addPipe((c) => { + if (c.type === 'render') { + console.log(c.data); + } + return c; + }); + AreaExtensions.simpleNodesOrder(this.area); + addCustomBackground(this.area); + } + + async initialize(workflow: WorkflowModel): Promise { + + // Create nodes and connections + const a = new ActivityNode(); + a.addControl('a', new ClassicPreset.InputControl('text', {initial: 'hello'})); + a.addOutput('a', new ClassicPreset.Output(this.socket)); + await this.editor.addNode(a); + + const b = new ActivityNode(); + b.addControl('b', new ClassicPreset.InputControl('text', {initial: 'hello'})); + b.addInput('b', new ClassicPreset.Input(this.socket)); + await this.editor.addNode(b); + + await this.area.translate(b.id, {x: 320, y: 0}); + await this.editor.addConnection(new Edge(a, 'a', b, 'b')); + + AreaExtensions.zoomAt(this.area, this.editor.getNodes()); + } + + destroy(): void { + this.area.destroy(); + } +} diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html index f3a83729..bf134e83 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html @@ -1 +1,7 @@

workflow-viewer works!

+Session id: {{sessionId}} + +{{workflow | json}} +
+
+
\ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss index e69de29b..748da1b3 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss @@ -0,0 +1,4 @@ +.rete { + min-height: 800px; + border: 2px solid; +} \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts index 053245ef..29b29073 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts @@ -1,12 +1,47 @@ -import {Component, inject} from '@angular/core'; +import {Component, ElementRef, inject, Injector, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {WorkflowsService} from '../../services/workflows.service'; +import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; +import {WorkflowModel} from '../../models/workflows.model'; +import {WorkflowEditor} from './editor/workflow-editor'; @Component({ selector: 'app-workflow-viewer', templateUrl: './workflow-viewer.component.html', styleUrl: './workflow-viewer.component.scss' }) -export class WorkflowViewerComponent { +export class WorkflowViewerComponent implements OnInit, OnDestroy { + @Input() sessionId: string; + @Input() isEditable: boolean; + + @ViewChild('rete') container!: ElementRef; + private readonly _workflows = inject(WorkflowsService); + private readonly _toast = inject(ToasterService); + private editor: WorkflowEditor; + workflow: WorkflowModel; + + constructor(private injector: Injector) { + } + + ngOnInit(): void { + } + + ngAfterViewInit(): void { + const el = this.container.nativeElement; + + if (el) { + this.editor = new WorkflowEditor(this.injector, el, false); + } + this._workflows.getActiveWorkflow(this.sessionId).subscribe({ + next: res => { + this.workflow = res; + this.editor.initialize(res); + } + }); + } + + ngOnDestroy(): void { + this.editor?.destroy(); + } } diff --git a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html index 4def5cbf..f75bfff7 100644 --- a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html +++ b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html @@ -1,5 +1,12 @@

Workflow Dashboard

+ + + + +

Stored Workflows

@@ -39,7 +46,7 @@

{{ workflowDef.value.name }}

Active Sessions

-
@@ -55,13 +57,6 @@

{{ activity().displayName() }}

@switch (activeTab()) { @case ("settings") { - - } @case ("variables") { diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html index c79299e7..eb81e6c6 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html @@ -44,12 +44,13 @@ - + {{ workflow.state() }} - + diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts index 7950b230..6941b34c 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts @@ -1,4 +1,4 @@ -import {Component, computed, effect, ElementRef, EventEmitter, Injector, Input, OnDestroy, OnInit, Output, signal, Signal, ViewChild} from '@angular/core'; +import {Component, computed, ElementRef, EventEmitter, Injector, Input, OnDestroy, OnInit, Output, signal, Signal, ViewChild} from '@angular/core'; import {WorkflowsService} from '../../services/workflows.service'; import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; import {WorkflowEditor} from './editor/workflow-editor'; @@ -52,9 +52,6 @@ export class WorkflowViewerComponent implements OnInit, OnDestroy { private readonly _toast: ToasterService, private readonly _websocket: WorkflowsWebSocketService, private injector: Injector) { - effect(() => { - console.log('edited changed', this.editedVariables()); - }); } ngOnInit(): void { diff --git a/src/app/plugins/workflows/models/activity-registry.model.ts b/src/app/plugins/workflows/models/activity-registry.model.ts index 34aecde3..001632be 100644 --- a/src/app/plugins/workflows/models/activity-registry.model.ts +++ b/src/app/plugins/workflows/models/activity-registry.model.ts @@ -9,11 +9,18 @@ export const DEFAULT_SUBGROUP = ''; export class ActivityRegistry { private readonly registry: Map; + public readonly categories: string[] = []; constructor(data: Record) { this.registry = new Map(Object.entries(data).map( ([key, model]) => [key, new ActivityDef(model)] )); + const categories = new Set(); + this.registry.forEach((def, type) => { + def.categories.forEach(c => categories.add(c)); + }); + this.categories.push(...categories); + this.categories.sort((a, b) => a.localeCompare(b)); } public getDef(activityType: string) { diff --git a/src/app/plugins/workflows/workflows.module.ts b/src/app/plugins/workflows/workflows.module.ts index 52df5f1f..9a7a2238 100644 --- a/src/app/plugins/workflows/workflows.module.ts +++ b/src/app/plugins/workflows/workflows.module.ts @@ -72,6 +72,7 @@ import {ActivitySettingsComponent} from './components/workflow-viewer/right-menu import {IntSettingComponent} from './components/workflow-viewer/right-menu/activity-settings/int-setting/int-setting.component'; import {StringSettingComponent} from './components/workflow-viewer/right-menu/activity-settings/string-setting/string-setting.component'; import {CdkDrag, CdkDragPreview, CdkDropList} from '@angular/cdk/drag-drop'; +import {AngularMultiSelectModule} from 'angular2-multiselect-dropdown'; @NgModule({ imports: [ @@ -135,7 +136,8 @@ import {CdkDrag, CdkDragPreview, CdkDropList} from '@angular/cdk/drag-drop'; CollapseDirective, CdkDrag, CdkDropList, - CdkDragPreview + CdkDragPreview, + AngularMultiSelectModule, ], declarations: [ WorkflowViewerComponent, diff --git a/src/scss/angular2-multiselect-dropdown.scss b/src/scss/angular2-multiselect-dropdown.scss new file mode 100644 index 00000000..e935208d --- /dev/null +++ b/src/scss/angular2-multiselect-dropdown.scss @@ -0,0 +1,188 @@ +// An edited copy of node_modules/angular2-multiselect-dropdown to match the CoreUI theme +$default-color: #ffffff; +$base-color: var(--cui-primary); +$btn-background: #fff; +$btn-border: var(--cui-border-color); +$btn-text-color: #333; +$btn-arrow: #333; +$border-radius: var(--cui-border-radius-sm); + + +$token-background: $base-color; +$token-text-color: #fff; +$token-remove-color: #fff; + +$box-shadow-color: #959595; +$list-hover-background: #f5f5f5; +$label-color: #000; +$selected-background: #e9f4ff; + + +.mat-toolbar { + background: $default-color; +} + +.c-btn { + background: $btn-background; + border: 1px solid $btn-border; + border-radius: $border-radius; + color: $btn-text-color; +} + +.selected-list { + .c-list { + .c-token { + background: $token-background; + + .c-label { + color: $token-text-color; + } + + .c-remove { + svg { + fill: $token-remove-color; + } + } + + } + } + + .c-angle-down, .c-angle-up { + svg { + fill: $btn-arrow; + } + } +} + +.dropdown-list { + ul { + li:hover { + background: $list-hover-background; + } + } +} + +.arrow-up, .arrow-down { + border-bottom: 15px solid #fff; +} + +.arrow-2 { + border-bottom: 15px solid #ccc; +} + +.list-area { + border: 1px solid #ccc; + border-radius: $border-radius; + background: #fff; + box-shadow: 0px 1px 5px $box-shadow-color; +} + +.select-all { + border-bottom: 1px solid #ccc; +} + +.list-filter { + border-bottom: 1px solid #ccc; + + .c-search { + svg { + fill: #888; + } + } + + .c-clear { + svg { + fill: #888; + } + } +} + +.pure-checkbox { + input[type="checkbox"]:focus + label:before, input[type="checkbox"]:hover + label:before { + border-color: $base-color; + background-color: #f2f2f2; + } + + input[type="checkbox"] + label { + color: $label-color; + } + + input[type="checkbox"] + label:before { + color: $base-color; + border: 1px solid $base-color; + } + + input[type="checkbox"] + label:after { + background-color: $base-color; + } + + input[type="checkbox"]:disabled + label:before { + border-color: #cccccc; + } + + input[type="checkbox"]:disabled:checked + label:before { + background-color: #cccccc; + } + + input[type="checkbox"] + label:after { + border-color: #ffffff; + } + + input[type="radio"]:checked + label:before { + background-color: white; + } + + input[type="checkbox"]:checked + label:before { + background: $base-color; + } +} + +.single-select-mode .pure-checkbox { + input[type="checkbox"]:focus + label:before, input[type="checkbox"]:hover + label:before { + border-color: $base-color; + background-color: #f2f2f2; + } + + input[type="checkbox"] + label { + color: $label-color; + } + + input[type="checkbox"] + label:before { + color: transparent !important; + border: 0px solid $base-color; + } + + input[type="checkbox"] + label:after { + background-color: transparent !important; + } + + input[type="checkbox"]:disabled + label:before { + border-color: #cccccc; + } + + input[type="checkbox"]:disabled:checked + label:before { + background-color: #cccccc; + } + + input[type="checkbox"] + label:after { + border-color: $base-color; + } + + input[type="radio"]:checked + label:before { + background-color: white; + } + + input[type="checkbox"]:checked + label:before { + background: none !important; + } +} + +.selected-item { + background: $selected-background; +} + +.btn-iceblue { + background: $base-color; + border: 1px solid $btn-border; + color: #fff; +} \ No newline at end of file From 884f8d487070a2579efb99e714d5b22995db3b5a Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Mon, 13 Jan 2025 19:05:45 +0100 Subject: [PATCH 17/68] Add component for showing checkpoints --- .../data-template/data-template.component.ts | 17 +----- .../left-menu/left-menu.component.html | 2 +- .../checkpoint-viewer.component.html | 27 +++++++++ .../checkpoint-viewer.component.scss | 0 .../checkpoint-viewer.component.ts | 37 ++++++++++++ .../right-menu/right-menu.component.html | 2 +- .../workflow-viewer.component.html | 22 +++++++ .../workflow-viewer.component.scss | 2 +- .../workflow-viewer.component.ts | 6 +- .../workflows/models/ws-response.model.ts | 18 +++--- .../services/checkpoint-viewer.service.ts | 59 +++++++++++++++++++ .../services/workflows-websocket.service.ts | 12 ++++ src/app/plugins/workflows/workflows.module.ts | 4 +- 13 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.html create mode 100644 src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.scss create mode 100644 src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.ts create mode 100644 src/app/plugins/workflows/services/checkpoint-viewer.service.ts diff --git a/src/app/components/data-view/data-template/data-template.component.ts b/src/app/components/data-view/data-template/data-template.component.ts index c522fe49..f83ae226 100644 --- a/src/app/components/data-view/data-template/data-template.component.ts +++ b/src/app/components/data-view/data-template/data-template.component.ts @@ -1,17 +1,4 @@ -import { - Component, - computed, - effect, - EventEmitter, - inject, - Input, - OnDestroy, - OnInit, - Signal, - signal, - untracked, - WritableSignal -} from '@angular/core'; +import {Component, computed, effect, EventEmitter, inject, Input, OnDestroy, OnInit, Signal, signal, untracked, WritableSignal} from '@angular/core'; import {RelationalResult, Result, UiColumnDefinition} from '../models/result-set.model'; import {WebuiSettingsService} from '../../../services/webui-settings.service'; import {CatalogService} from '../../../services/catalog.service'; @@ -132,7 +119,7 @@ export abstract class DataTemplateComponent implements OnInit, OnDestroy { } ngOnInit() { - this._sidebar.open(); + //this._sidebar.open(); //listen to results this.initWebsocket(); diff --git a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html index b30c8486..701ed3cc 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html +++ b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html @@ -4,7 +4,7 @@ [scroll]="true" placement="start" #offcanvas - [visible]="true" + [visible]="false" class="no-select" [ngClass]="openedActivityDef ? 'info-visible' : 'info-hidden'" > diff --git a/src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.html b/src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.html new file mode 100644 index 00000000..78f44cf8 --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.html @@ -0,0 +1,27 @@ +@if (activity() && isExecuted()) { + + @if (activity().def.outPorts.length > 0) { + + @if (isFinished()) { +

The outputs of this activity are currently not materialized

+ + } @else { +
Outputs
+
    + @for (output of activity().def.outPorts; track $index) { +
  • + +
  • + } +
+ } + } @else { +

This activity has no outputs.

+ } + + +} @else { +

Execute the activity to see its outputs.

+} diff --git a/src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.scss b/src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.ts b/src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.ts new file mode 100644 index 00000000..5284970a --- /dev/null +++ b/src/app/plugins/workflows/components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component.ts @@ -0,0 +1,37 @@ +import {Component, computed, input, OnInit, Signal} from '@angular/core'; +import {Activity} from '../../workflow'; +import {CheckpointViewerService} from '../../../../services/checkpoint-viewer.service'; + +@Component({ + selector: 'app-checkpoint-viewer', + templateUrl: './checkpoint-viewer.component.html', + styleUrl: './checkpoint-viewer.component.scss' +}) +export class CheckpointViewerComponent implements OnInit { + + activity = input.required(); + isQueryable = input.required(); + + isExecuted: Signal; + isFinished: Signal; + selectedOutput = 0; + + constructor(readonly _checkpoint: CheckpointViewerService) { + } + + ngOnInit(): void { + this.isExecuted = computed(() => this.activity().state() === 'FINISHED' || this.activity().state() === 'SAVED'); + this.isFinished = computed(() => this.activity().state() === 'FINISHED'); + } + + + showOutput(i: number) { + this.selectedOutput = i; + this._checkpoint.setModal(true); + this._checkpoint.getCheckpoint(this.activity(), this.selectedOutput); + } + + materializeCheckpoints() { + this._checkpoint.materializeCheckpoints(this.activity().id); + } +} diff --git a/src/app/plugins/workflows/components/workflow-viewer/right-menu/right-menu.component.html b/src/app/plugins/workflows/components/workflow-viewer/right-menu/right-menu.component.html index cebc1500..1d9cfaa2 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/right-menu/right-menu.component.html +++ b/src/app/plugins/workflows/components/workflow-viewer/right-menu/right-menu.component.html @@ -68,7 +68,7 @@
{{ entry.key }}
} } @case ("outputs") { -
outputs
+ } @case ("execution") {
Config
diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html index eb81e6c6..bf82d9d7 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.html @@ -114,3 +114,25 @@
Workflow Variables
+ + + +
{{ _checkpoint.selectedActivity().displayName() }}: {{ _checkpoint.selectedOutput() }}
+ +
+ +
+ + +

Only the first {{ _checkpoint.limit() }} of {{ _checkpoint.totalCount() }} elements are shown.

+
+
+ +
+
+ + + +
diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss index 72109bdc..626af882 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.scss @@ -4,4 +4,4 @@ .editor-container { border-top: 1px lightgray solid; -} +} \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts index 6941b34c..12ac23e1 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts +++ b/src/app/plugins/workflows/components/workflow-viewer/workflow-viewer.component.ts @@ -12,12 +12,13 @@ import {switchMap, tap} from 'rxjs/operators'; import {WorkflowConfigEditorComponent} from './workflow-config-editor/workflow-config-editor.component'; import {WorkflowsWebSocketService} from '../../services/workflows-websocket.service'; import {JsonEditorComponent} from '../../../../components/json/json-editor.component'; +import {CheckpointViewerService} from '../../services/checkpoint-viewer.service'; @Component({ selector: 'app-workflow-viewer', templateUrl: './workflow-viewer.component.html', styleUrl: './workflow-viewer.component.scss', - providers: [WorkflowsWebSocketService] + providers: [WorkflowsWebSocketService, CheckpointViewerService] }) export class WorkflowViewerComponent implements OnInit, OnDestroy { @Input() sessionId: string; @@ -51,6 +52,7 @@ export class WorkflowViewerComponent implements OnInit, OnDestroy { private readonly _workflows: WorkflowsService, private readonly _toast: ToasterService, private readonly _websocket: WorkflowsWebSocketService, + readonly _checkpoint: CheckpointViewerService, private injector: Injector) { } @@ -177,6 +179,8 @@ export class WorkflowViewerComponent implements OnInit, OnDestroy { this._toast.error(errorResponse.reason + cause, errorResponse.parentType + ' was unsuccessful'); } break; + case ResponseType.CHECKPOINT_DATA: + break; // handled by checkpoint service default: console.warn('unhandled websocket response', response); } diff --git a/src/app/plugins/workflows/models/ws-response.model.ts b/src/app/plugins/workflows/models/ws-response.model.ts index 8aa852c2..2bcaa257 100644 --- a/src/app/plugins/workflows/models/ws-response.model.ts +++ b/src/app/plugins/workflows/models/ws-response.model.ts @@ -1,11 +1,12 @@ -import {ActivityModel, ActivityState, EdgeModel, RenderModel, WorkflowModel, WorkflowState} from './workflows.model'; +import {ActivityModel, ActivityState, EdgeModel, RenderModel, WorkflowState} from './workflows.model'; +import {Result} from '../../../components/data-view/models/result-set.model'; export enum ResponseType { - WORKFLOW_UPDATE = 'WORKFLOW_UPDATE', ACTIVITY_UPDATE = 'ACTIVITY_UPDATE', RENDERING_UPDATE = 'RENDERING_UPDATE', STATE_UPDATE = 'STATE_UPDATE', PROGRESS_UPDATE = 'PROGRESS_UPDATE', + CHECKPOINT_DATA = 'CHECKPOINT_DATA', ERROR = 'ERROR' } @@ -21,6 +22,7 @@ export enum RequestType { // no specific interfaces for requests are required, s RESET = 'RESET', UPDATE_CONFIG = 'UPDATE_CONFIG', UPDATE_VARIABLES = 'UPDATE_VARIABLES', + GET_CHECKPOINT = 'GET_CHECKPOINT' } export interface WsResponse { @@ -29,11 +31,6 @@ export interface WsResponse { parentId?: string; } -export interface WorkflowUpdateResponse extends WsResponse { - type: ResponseType.WORKFLOW_UPDATE; - workflow: WorkflowModel; -} - export interface ActivityUpdateResponse extends WsResponse { type: ResponseType.ACTIVITY_UPDATE; activity: ActivityModel; @@ -63,3 +60,10 @@ export interface ErrorResponse extends WsResponse { cause?: string; parentType: RequestType; } + +export interface CheckpointDataResponse extends WsResponse { + type: ResponseType.CHECKPOINT_DATA; + result: Result; + limit: number; + totalCount: number; +} diff --git a/src/app/plugins/workflows/services/checkpoint-viewer.service.ts b/src/app/plugins/workflows/services/checkpoint-viewer.service.ts new file mode 100644 index 00000000..9dbb4c4d --- /dev/null +++ b/src/app/plugins/workflows/services/checkpoint-viewer.service.ts @@ -0,0 +1,59 @@ +import {computed, Injectable, signal, WritableSignal} from '@angular/core'; +import {WorkflowsWebSocketService} from './workflows-websocket.service'; +import {CheckpointDataResponse, ResponseType, WsResponse} from '../models/ws-response.model'; +import {Result} from '../../../components/data-view/models/result-set.model'; +import {Activity} from '../components/workflow-viewer/workflow'; + +@Injectable() +export class CheckpointViewerService { + readonly showModal = signal(false); + + readonly selectedActivity = signal(undefined); + readonly selectedOutput = signal(0); + readonly isLoading = signal(false); + result: WritableSignal> = signal(null); + readonly limit = signal(0); + readonly totalCount = signal(0); + readonly isLimited = computed(() => this.totalCount() > this.limit()); + + constructor(private readonly _websocket: WorkflowsWebSocketService) { + this._websocket.onMessage().subscribe(msg => this.handleWsMsg(msg)); + + } + + toggleModal() { + this.setModal(!this.showModal()); + } + + setModal(visible: boolean) { + this.showModal.set(visible); + if (!visible) { + this.selectedActivity.set(null); + this.result.set(null); + } + } + + getCheckpoint(activity: Activity, outputIndex: number) { + this.selectedActivity.set(activity); + this.selectedOutput.set(outputIndex); + this.isLoading.set(true); + this._websocket.getCheckpoint(activity.id, outputIndex); + + } + + private handleWsMsg(msg: { response: WsResponse; isDirect: boolean }) { + const {response, isDirect} = msg; + if (response.type === ResponseType.CHECKPOINT_DATA) { + const r = (response as CheckpointDataResponse); + console.log(r); + this.result.set(r.result); + this.limit.set(r.limit); + this.totalCount.set(r.totalCount); + this.isLoading.set(false); + } + } + + materializeCheckpoints(activityId: string) { + this._websocket.execute(activityId); + } +} diff --git a/src/app/plugins/workflows/services/workflows-websocket.service.ts b/src/app/plugins/workflows/services/workflows-websocket.service.ts index 2b8a14b3..a612f822 100644 --- a/src/app/plugins/workflows/services/workflows-websocket.service.ts +++ b/src/app/plugins/workflows/services/workflows-websocket.service.ts @@ -203,6 +203,18 @@ export class WorkflowsWebSocketService { return id; } + getCheckpoint(activityId: string, outputIndex: number): string { + const id = uuid.v4(); + const msg = { + type: RequestType.GET_CHECKPOINT, + msgId: id, + activityId, + outputIndex + }; + this.sendMessage(msg); + return id; + } + onMessage(): Observable<{ response: WsResponse, isDirect: boolean }> { return this.msgSubject.asObservable(); } diff --git a/src/app/plugins/workflows/workflows.module.ts b/src/app/plugins/workflows/workflows.module.ts index 9a7a2238..b4f4be30 100644 --- a/src/app/plugins/workflows/workflows.module.ts +++ b/src/app/plugins/workflows/workflows.module.ts @@ -73,6 +73,7 @@ import {IntSettingComponent} from './components/workflow-viewer/right-menu/activ import {StringSettingComponent} from './components/workflow-viewer/right-menu/activity-settings/string-setting/string-setting.component'; import {CdkDrag, CdkDragPreview, CdkDropList} from '@angular/cdk/drag-drop'; import {AngularMultiSelectModule} from 'angular2-multiselect-dropdown'; +import {CheckpointViewerComponent} from './components/workflow-viewer/right-menu/checkpoint-viewer/checkpoint-viewer.component'; @NgModule({ imports: [ @@ -151,7 +152,8 @@ import {AngularMultiSelectModule} from 'angular2-multiselect-dropdown'; ActivityHelpComponent, ActivitySettingsComponent, IntSettingComponent, - StringSettingComponent + StringSettingComponent, + CheckpointViewerComponent ], exports: [ WorkflowViewerComponent, From c5034346945c4cc412a75aa0e7dbd66301709f93 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Tue, 14 Jan 2025 16:33:57 +0100 Subject: [PATCH 18/68] Improve workflow management --- .../left-sidebar/left-sidebar.component.ts | 2 +- .../workflow-session.component.html | 15 +- .../workflow-session.component.ts | 9 +- .../left-menu/left-menu.component.ts | 2 +- .../workflows-dashboard.component.html | 256 ++++++++++++------ .../workflows-dashboard.component.scss | 3 + .../workflows-dashboard.component.ts | 147 +++++++++- .../workflows/models/workflows.model.ts | 2 + .../workflows/services/workflows.service.ts | 21 +- src/app/plugins/workflows/workflows.module.ts | 11 +- src/app/views/views-routing.module.ts | 15 +- 11 files changed, 381 insertions(+), 102 deletions(-) diff --git a/src/app/components/left-sidebar/left-sidebar.component.ts b/src/app/components/left-sidebar/left-sidebar.component.ts index adf41f47..69abefca 100644 --- a/src/app/components/left-sidebar/left-sidebar.component.ts +++ b/src/app/components/left-sidebar/left-sidebar.component.ts @@ -64,7 +64,7 @@ export class LeftSidebarComponent implements OnInit, AfterViewInit { } static readonly EXPAND_SHOWN_ROUTES: String[] = [ - '/views/monitoring', '/views/config', '/views/uml', '/views/querying/console', '/views/notebooks']; + '/views/monitoring', '/views/config', '/views/uml', '/views/querying/console', '/views/notebooks', '/views/workflows']; private readonly _router = inject(Router); public readonly _sidebar = inject(LeftSidebarService); diff --git a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html index 0addae13..cdf74fe7 100644 --- a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html +++ b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.html @@ -1,9 +1,16 @@
- + + + + +

{{ session.workflowDef.name }} v{{ session.version }}

Session ID: {{ sessionId }}
diff --git a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.ts b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.ts index 7ecb43cc..73c395e7 100644 --- a/src/app/plugins/workflows/components/workflow-session/workflow-session.component.ts +++ b/src/app/plugins/workflows/components/workflow-session/workflow-session.component.ts @@ -47,7 +47,7 @@ export class WorkflowSessionComponent implements OnInit, OnDestroy { } backToDashboard() { - this._router.navigate(['./../'], {relativeTo: this._route}); + this._router.navigate(['/views/workflows/dashboard']); } saveSession(message: string) { @@ -60,6 +60,13 @@ export class WorkflowSessionComponent implements OnInit, OnDestroy { }); } + terminateSession() { + this._workflows.terminateSession(this.sessionId).subscribe({ + next: () => this.backToDashboard(), + error: e => this._toast.error(e.error, 'Unable to terminate session'), + }); + } + private initMarkdown() { const renderer = new MarkedRenderer(); renderer.blockquote = (text: string) => { diff --git a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.ts b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.ts index beff9e6d..395a8d0f 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.ts +++ b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.ts @@ -88,7 +88,7 @@ export class LeftMenuComponent { return false; } return this.selectedCategories.length === 0 || - this.selectedCategories.find(item => def.categories.includes(item.itemName)); + !this.selectedCategories.some(item => !def.categories.includes(item.itemName)); }).map(type => this.registry.getDef(type)); } diff --git a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html index 4321b572..aec9b148 100644 --- a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html +++ b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.html @@ -1,87 +1,189 @@ -

Workflow Dashboard

+@switch (route()) { + @case ('dashboard') { +

Workflow Dashboard

- - - - + + + + + Create Workflow + {{ newWorkflowName }} + {{ newWorkflowGroup }} + + + + Name + + + + Folder + + + + + + + + + -

Stored Workflows

+

Stored Workflows ({{ workflowDefsCount }})

- - - - - - -
- - - -
- +
-
-
- Version Description: {{ entry.value.versions[selectedVersion[entry.key]].description }} -
-
- Creation Time: {{ entry.value.versions[selectedVersion[entry.key]].creationTime | date:'short' }} -
- +
+ + @for (def of group.defs; track def.id) { + + + + +
+
+ +
+ +
+ + + + + + -
-
- - - + + + + + + + + + + -

Active Sessions

-
    - -
  • -
    -
    - - {{workflowDefs[session.value.workflowId].name}} - v{{session.value.version}} - - - {{session.value.type}} - + -
    - SessionId: {{ session.value.sessionId }}
    - Connected Users: {{ session.value.connectionCount }}
    -
    -
    - + + + + + + + + + + + @for (version of def.def.versions | keyvalue; track version.key) { + + + + + + + } + +
    VersionCreation TimeCommentDelete Version
    v{{ version.key }}{{ version.value.creationTime | date:'short' }}{{ version.value.description }} + +
    +
    + + + } + -
  • -
    -
+ } + } + @case ('sessions') { + +

Active Sessions

+
    + +
  • +
    +
    + + {{ workflowDefs[session.value.workflowId].name }} + v{{ session.value.version }} + + + {{ session.value.type }} + + +
    + SessionId: {{ session.value.sessionId }}
    + State: {{ session.value.state }}
    + Connected Users: {{ session.value.connectionCount }}
    +
    + +
    + +
    +
  • +
    +
+ } + @case ('jobs') { +

Jobs coming soon...

+ } + @case ('api') { +

API coming soon...

+ } +} + + + +
Delete Workflow
+ +
+ + Do you really want to delete all versions of workflow "{{ workflowToDelete.def.name }}"? + This operation cannot be undone. + + + +
+ +
+
+
\ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.scss b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.scss index e69de29b..3539a147 100644 --- a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.scss +++ b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.scss @@ -0,0 +1,3 @@ +.col-md-6.fixed-width { + max-width: 400px; +} \ No newline at end of file diff --git a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.ts b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.ts index 8ff43723..25ace7a1 100644 --- a/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.ts +++ b/src/app/plugins/workflows/components/workflows-dashboard/workflows-dashboard.component.ts @@ -1,9 +1,10 @@ -import {Component, inject, OnDestroy, OnInit} from '@angular/core'; +import {Component, effect, inject, OnDestroy, OnInit, signal, WritableSignal} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; import {LeftSidebarService} from '../../../../components/left-sidebar/left-sidebar.service'; import {WorkflowsService} from '../../services/workflows.service'; import {SessionModel, WorkflowDefModel} from '../../models/workflows.model'; +import {SidebarNode} from '../../../../models/sidebar-node.model'; @Component({ selector: 'app-workflows-dashboard', @@ -17,35 +18,96 @@ export class WorkflowsDashboardComponent implements OnInit, OnDestroy { private readonly _sidebar = inject(LeftSidebarService); private readonly _workflows = inject(WorkflowsService); + public route = signal(null); + workflowDefs: Record; - sortedWorkflowDefs: { key: string; value: WorkflowDefModel }[]; + workflowDefsCount = 0; + sortedGroups: { groupName: string, defs: { id: string, def: WorkflowDefModel }[] }[]; + visibleGroups: Record> = {}; sessions: Record; selectedVersion: Record = {}; newWorkflowName = ''; + newWorkflowGroup = ''; + showDeleteModal = signal(false); + workflowToDelete: { id: string, def: WorkflowDefModel } = null; + + constructor() { + effect(() => { + if (this.route()) { + console.log('updating defs'); + this.getWorkflowDefs(); + this.getSessions(); + } + }); + } ngOnInit(): void { + this.getRoute(); + this.initSidebar(); + } + + ngOnDestroy(): void { this._sidebar.hide(); + } - // TODO: add ability to update sessions and workflowdefs + private getRoute() { + this.route.set(this._route.snapshot.paramMap.get('route')); + this._route.params.subscribe(params => { + this.route.set(params['route']); + }); + } + + private getWorkflowDefs() { this._workflows.getWorkflowDefs().subscribe({ next: res => { this.workflowDefs = res; + this.workflowDefsCount = Object.keys(res).length; + + const groups = new Map(); + //const groups: {group: string, defs: {id: string, def: WorkflowDefModel}[]}[] = []; - this.sortedWorkflowDefs = Object.entries(res) - .map(([key, value]) => ({key, value})) - .sort((a, b) => a.value.name.localeCompare(b.value.name)); + Object.entries(res).forEach(([id, def]) => { + const groupName = def.group || ''; + const group = groups.get(groupName) || []; + group.push({id, def}); + groups.set(groupName, group); + }); + + const groupsArray: { groupName: string, defs: { id: string, def: WorkflowDefModel }[] }[] = []; + const visibleGroups: Record> = {}; + groups.forEach((defs, groupName) => { + defs.sort((a, b) => a.def.name.localeCompare(b.def.name)); + groupsArray.push({groupName, defs}); + visibleGroups[groupName] = signal(this.visibleGroups[groupName] ? this.visibleGroups[groupName]() : true); + }); + this.sortedGroups = groupsArray.sort((a, b) => a.groupName.localeCompare(b.groupName)); + this.visibleGroups = visibleGroups; + const selectedVersion = {}; Object.entries(res).forEach(([key, value]) => { - this.selectedVersion[key] = Math.max(...Object.keys(value.versions).map(versionId => parseInt(versionId, 10))); // TODO: order by creation date + selectedVersion[key] = Math.max(...Object.keys(value.versions).map(versionId => parseInt(versionId, 10))); // TODO: order by creation date }); + console.log('selectedVersions', this.selectedVersion); + this.selectedVersion = selectedVersion; } }); + } + + private getSessions() { this._workflows.getSessions().subscribe({ next: res => this.sessions = res }); } - ngOnDestroy(): void { + private initSidebar() { + const sidebarNodes: SidebarNode[] = [ + new SidebarNode(0, 'Dashboard', null, '/views/workflows/dashboard'), + new SidebarNode(1, 'Sessions', null, '/views/workflows/sessions'), + //new SidebarNode(2, 'Jobs', null, '/views/workflows/jobs'), + //new SidebarNode(3, 'API', null, '/views/workflows/api') + ]; + this._sidebar.setNodes(sidebarNodes); + this._sidebar.open(); } openVersion(key: string) { @@ -56,13 +118,78 @@ export class WorkflowsDashboardComponent implements OnInit, OnDestroy { } createAndOpenWorkflow() { - this._workflows.createSession(this.newWorkflowName).subscribe({ + this._workflows.createSession(this.newWorkflowName, this.newWorkflowGroup).subscribe({ next: sessionId => this.openSession(sessionId), error: err => this._toast.error(err.error) }); } openSession(sessionId: string) { - this._router.navigate([`./${sessionId}`], {relativeTo: this._route}); + this._router.navigate([`/views/workflows/sessions/${sessionId}`]); + } + + terminateSession(sessionId: string) { + this._workflows.terminateSession(sessionId).subscribe({ + next: () => { + this._toast.success('Successfully terminated session', 'Terminate session'); + this.getSessions(); + }, + error: e => this._toast.error(e, 'Unable to terminate session'), + }); + + } + + toggleCollapse(groupName: string) { + this.visibleGroups[groupName].update(b => !b); + } + + confirmDelete(workflowId: string) { + this.workflowToDelete = {id: workflowId, def: this.workflowDefs[workflowId]}; + this.showDeleteModal.set(true); + } + + toggleDeleteModal() { + this.showDeleteModal.update(b => !b); + } + + deleteVersion(workflowId: string, version: string) { + this._workflows.deleteVersion(workflowId, parseInt(version, 10)).subscribe({ + next: () => { + this._toast.success('Successfully deleted workflow version ' + version, 'Delete Version'); + this.getWorkflowDefs(); + }, + error: e => this._toast.error(e.error, 'Unable to delete version') + }); + } + + deleteWorkflow(workflowId: string) { + this.showDeleteModal.set(false); + this._workflows.deleteWorkflow(workflowId).subscribe({ + next: () => { + this._toast.success('Successfully deleted workflow', 'Delete workflow'); + this.getWorkflowDefs(); + }, + error: e => this._toast.error(e.error, 'Unable to delete workflow') + }); + } + + changeName(workflowId: string, name: string) { + this._workflows.renameWorkflow(workflowId, name).subscribe({ + next: () => { + this._toast.success('Successfully renamed workflow ', 'Rename Workflow'); + this.getWorkflowDefs(); + }, + error: e => this._toast.error(e.error, 'Unable to rename workflow') + }); + } + + changeGroup(workflowId: string, groupName: string) { + this._workflows.renameWorkflow(workflowId, null, groupName).subscribe({ + next: () => { + this._toast.success('Successfully changed workflow group', 'Change Workflow Group'); + this.getWorkflowDefs(); + }, + error: e => this._toast.error(e.error, 'Unable to change workflow group') + }); } } diff --git a/src/app/plugins/workflows/models/workflows.model.ts b/src/app/plugins/workflows/models/workflows.model.ts index b711239c..af4066c4 100644 --- a/src/app/plugins/workflows/models/workflows.model.ts +++ b/src/app/plugins/workflows/models/workflows.model.ts @@ -93,11 +93,13 @@ export interface SessionModel { workflowId?: string; version?: number; workflowDef?: WorkflowDefModel; + state?: WorkflowState; } export interface WorkflowDefModel { name: string; versions: Record; + group: string; } export interface VersionInfo { diff --git a/src/app/plugins/workflows/services/workflows.service.ts b/src/app/plugins/workflows/services/workflows.service.ts index 47691b67..3afe002a 100644 --- a/src/app/plugins/workflows/services/workflows.service.ts +++ b/src/app/plugins/workflows/services/workflows.service.ts @@ -59,9 +59,10 @@ export class WorkflowsService { return this._http.get>(`${this.httpUrl}/workflows`, this.httpOptions); } - createSession(workflowName: string) { + createSession(workflowName: string, group: string) { const json = { - name: workflowName + name: workflowName, + group: group }; return this._http.post(`${this.httpUrl}/sessions`, json, this.httpOptions); } @@ -70,6 +71,22 @@ export class WorkflowsService { return this._http.post(`${this.httpUrl}/workflows/${workflowId}/${version}`, {}, this.httpOptions); } + renameWorkflow(workflowId: string, newName: string = null, newGroup: string = null) { + const json = { + name: newName, + group: newGroup + }; + return this._http.patch(`${this.httpUrl}/workflows/${workflowId}`, json, this.httpOptions); + } + + deleteWorkflow(workflowId: string) { + return this._http.delete(`${this.httpUrl}/workflows/${workflowId}`, this.httpOptions); + } + + deleteVersion(workflowId: string, version: number) { + return this._http.delete(`${this.httpUrl}/workflows/${workflowId}/${version}`, this.httpOptions); + } + saveSession(sessionId: string, saveMessage: string) { const json = { message: saveMessage diff --git a/src/app/plugins/workflows/workflows.module.ts b/src/app/plugins/workflows/workflows.module.ts index b4f4be30..ff8b098a 100644 --- a/src/app/plugins/workflows/workflows.module.ts +++ b/src/app/plugins/workflows/workflows.module.ts @@ -20,6 +20,7 @@ import { CardTextDirective, CardTitleDirective, ColComponent, + ColDirective, CollapseDirective, FormCheckComponent, FormCheckInputDirective, @@ -47,6 +48,7 @@ import { OffcanvasTitleDirective, OffcanvasToggleDirective, RowComponent, + RowDirective, SidebarBrandComponent, SidebarComponent, SidebarFooterComponent, @@ -55,10 +57,12 @@ import { SidebarToggleDirective, TabContentComponent, TabContentRefDirective, + TableColorDirective, + TableDirective, TabPaneComponent, TemplateIdDirective } from '@coreui/angular'; -import {FormsModule} from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {ActivityComponent} from './components/workflow-viewer/editor/activity/activity.component'; import {EdgeComponent} from './components/workflow-viewer/editor/edge/edge.component'; import {RightMenuComponent} from './components/workflow-viewer/right-menu/right-menu.component'; @@ -139,6 +143,11 @@ import {CheckpointViewerComponent} from './components/workflow-viewer/right-menu CdkDropList, CdkDragPreview, AngularMultiSelectModule, + ColDirective, + ReactiveFormsModule, + RowDirective, + TableDirective, + TableColorDirective, ], declarations: [ WorkflowViewerComponent, diff --git a/src/app/views/views-routing.module.ts b/src/app/views/views-routing.module.ts index 477fe360..32c960d5 100644 --- a/src/app/views/views-routing.module.ts +++ b/src/app/views/views-routing.module.ts @@ -185,18 +185,23 @@ const routes: Routes = [ }, { path: 'workflows', - component: WorkflowsDashboardComponent, - data: { - title: 'Workflows Dashboard' - } + redirectTo: 'workflows/dashboard', + pathMatch: 'full' }, { - path: 'workflows/:sessionId', + path: 'workflows/sessions/:sessionId', component: WorkflowSessionComponent, data: { title: 'Workflow Session', isFullWidth: true } + }, + { + path: 'workflows/:route', + component: WorkflowsDashboardComponent, + data: { + title: 'Workflows Dashboard' + } } ]; From 8e3321db45b64f330a9fe6c7927c698f1e5b677e Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Tue, 14 Jan 2025 17:59:00 +0100 Subject: [PATCH 19/68] Fix scaling issue of left and right menu --- .../activity-help.component.html | 0 .../activity-help.component.scss | 0 .../activity-help/activity-help.component.ts | 2 +- .../left-menu/left-menu.component.html | 126 +++++++++--------- .../left-menu/left-menu.component.scss | 18 ++- .../left-menu/left-menu.component.ts | 22 +-- .../right-menu/right-menu.component.html | 25 ++-- .../right-menu/right-menu.component.scss | 17 +-- .../right-menu/right-menu.component.ts | 15 +-- .../workflow-viewer.component.html | 4 +- .../workflow-viewer.component.ts | 2 +- src/app/plugins/workflows/workflows.module.ts | 2 +- src/scss/angular2-multiselect-dropdown.scss | 6 +- 13 files changed, 99 insertions(+), 140 deletions(-) rename src/app/plugins/workflows/components/workflow-viewer/{right-menu => }/activity-help/activity-help.component.html (100%) rename src/app/plugins/workflows/components/workflow-viewer/{right-menu => }/activity-help/activity-help.component.scss (100%) rename src/app/plugins/workflows/components/workflow-viewer/{right-menu => }/activity-help/activity-help.component.ts (96%) diff --git a/src/app/plugins/workflows/components/workflow-viewer/right-menu/activity-help/activity-help.component.html b/src/app/plugins/workflows/components/workflow-viewer/activity-help/activity-help.component.html similarity index 100% rename from src/app/plugins/workflows/components/workflow-viewer/right-menu/activity-help/activity-help.component.html rename to src/app/plugins/workflows/components/workflow-viewer/activity-help/activity-help.component.html diff --git a/src/app/plugins/workflows/components/workflow-viewer/right-menu/activity-help/activity-help.component.scss b/src/app/plugins/workflows/components/workflow-viewer/activity-help/activity-help.component.scss similarity index 100% rename from src/app/plugins/workflows/components/workflow-viewer/right-menu/activity-help/activity-help.component.scss rename to src/app/plugins/workflows/components/workflow-viewer/activity-help/activity-help.component.scss diff --git a/src/app/plugins/workflows/components/workflow-viewer/right-menu/activity-help/activity-help.component.ts b/src/app/plugins/workflows/components/workflow-viewer/activity-help/activity-help.component.ts similarity index 96% rename from src/app/plugins/workflows/components/workflow-viewer/right-menu/activity-help/activity-help.component.ts rename to src/app/plugins/workflows/components/workflow-viewer/activity-help/activity-help.component.ts index c8963826..a175a726 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/right-menu/activity-help/activity-help.component.ts +++ b/src/app/plugins/workflows/components/workflow-viewer/activity-help/activity-help.component.ts @@ -1,5 +1,5 @@ import {Component, computed, effect, input, OnInit, signal} from '@angular/core'; -import {ActivityDef, GroupDef, portTypeToDataModel} from '../../../../models/activity-registry.model'; +import {ActivityDef, GroupDef, portTypeToDataModel} from '../../../models/activity-registry.model'; import {KatexOptions} from 'ngx-markdown'; @Component({ diff --git a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html index 701ed3cc..d0e670f2 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html +++ b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html @@ -1,69 +1,67 @@ - -
-
- -

Add Activity

-
-
- - - - - - -
- -
    - - @for (activity of filteredList; track activity.type) { -
  • - - -
    -
    -
    {{ activity.displayName }}
    -
    -
    - - -
    -
    -
    -
    - -
  • - } -
-
- +
+
+ -
- -

{{ openedActivityDef.displayName }}

- -
- - - +
+ + + + + + + + {{ item.itemName }} + + + + + + +
+
    + @for (activity of filteredList; track activity.type) { +
  • + + +
    +
    +
    {{ activity.displayName }}
    + {{ activity.shortDescription }} +
    +
    + + +
    +
    +
    +
    + +
  • + } +
- +
+ +
+ +
+
+
diff --git a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.scss b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.scss index 501840d1..7295712f 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.scss +++ b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.scss @@ -1,8 +1,6 @@ -#Offcanvas { - height: 100%; - position: relative; - max-height: inherit; +[hidden] { + display: none !important; } .info-visible { @@ -20,19 +18,19 @@ .info-col { max-width: 400px; - .offcanvas-header { - min-height: 59px; // same height as Add Activity + .menu-header { + min-height: 51px; // same height as Add Activity } } .activity-list { overflow-y: auto; - max-height: calc(100vh - 400px); // TODO: find solution without hardcoded height, or at least consider activity name length + flex-basis: 0; } .activity-info { overflow-y: auto; - max-height: calc(100vh - 300px); // TODO: find solution without hardcoded height, or at least consider activity name length + flex-basis: 0; } .no-select { @@ -43,7 +41,7 @@ } .cdk-drag-preview { - z-index: 1100 !important; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } .draggable { @@ -56,7 +54,7 @@ .activity-card:hover { - .info-button { + .info-button:not(.text-primary) { color: var(--cui-dark) !important; } } diff --git a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.ts b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.ts index 395a8d0f..f039129c 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.ts +++ b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.ts @@ -1,5 +1,4 @@ -import {Component, EventEmitter, input, Output, ViewChild} from '@angular/core'; -import {OffcanvasComponent} from '@coreui/angular'; +import {Component, EventEmitter, input, Output, signal} from '@angular/core'; import {WorkflowsService} from '../../../services/workflows.service'; import {CdkDragDrop} from '@angular/cdk/drag-drop'; import {ActivityDef} from '../../../models/activity-registry.model'; @@ -14,8 +13,7 @@ export class LeftMenuComponent { @Output() create = new EventEmitter(); @Output() createAt = new EventEmitter<[string, { x: number, y: number }]>(); - - @ViewChild('offcanvas') menu: OffcanvasComponent; + visible = signal(true); private readonly registry = this._workflows.getRegistry(); readonly activityTypes = this.registry.getTypes(); @@ -24,6 +22,7 @@ export class LeftMenuComponent { readonly dropdownSettings = { singleSelection: false, text: 'Filter by category', + noDataLabel: 'No categories found', enableSearchFilter: true, enableCheckAll: false, enableFilterSelectAll: false, @@ -32,6 +31,7 @@ export class LeftMenuComponent { filterText: string; selectedCategories = []; filteredList: ActivityDef[]; + showDescription = false; openedActivityDef: ActivityDef; // for info @@ -47,19 +47,7 @@ export class LeftMenuComponent { } toggleMenu() { - this.menu.visible = !this.menu.visible; - } - - showMenu() { - this.menu.visible = true; - } - - hideMenu() { - this.menu.visible = false; - } - - isVisible() { - return this.menu?.visible; + this.visible.update(b => !b); } onDragDropped($event: CdkDragDrop) { diff --git a/src/app/plugins/workflows/components/workflow-viewer/right-menu/right-menu.component.html b/src/app/plugins/workflows/components/workflow-viewer/right-menu/right-menu.component.html index 1d9cfaa2..048e34df 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/right-menu/right-menu.component.html +++ b/src/app/plugins/workflows/components/workflow-viewer/right-menu/right-menu.component.html @@ -1,17 +1,8 @@ - - +