From 4504ec00876092e274540ad1ecafd80e6ddd3796 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Tue, 3 Mar 2026 20:01:03 +0100 Subject: [PATCH] added task layout --- src/app/app.routes.ts | 4 +- .../layouts/tasks-executor/tasks-executor.css | 4 + .../tasks-executor/tasks-executor.html | 14 +++ .../tasks-executor/tasks-executor.spec.ts} | 12 +-- .../layouts/tasks-executor/tasks-executor.ts | 47 +++++++++ src/app/models/nodes.ts | 6 +- src/app/pages/main/tasks/tasks.css | 0 src/app/pages/main/tasks/tasks.html | 1 - src/app/pages/main/tasks/tasks.ts | 11 --- src/app/services/blocks/blocks-call.fake.ts | 8 ++ src/app/shared/custom-socket/custom-socket.ts | 18 ++-- .../human-interaction/human-interaction.css | 27 ++++- .../human-interaction/human-interaction.html | 27 ++--- .../human-interaction/human-interaction.ts | 28 +++++- .../nodes/input/input-node.component.html | 10 +- .../nodes/input/input-node.component.scss | 23 ++++- .../nodes/input/input-node.component.ts | 13 +++ src/app/shared/nodes/llm/llm.css | 27 ++++- src/app/shared/nodes/llm/llm.html | 27 ++--- src/app/shared/nodes/llm/llm.ts | 31 +++++- .../nodes/output/output-node.component.html | 9 +- .../nodes/output/output-node.component.scss | 21 ++++ .../nodes/output/output-node.component.ts | 13 +++ src/app/shared/ordering/ordering.css | 4 + src/app/shared/ordering/ordering.html | 4 +- .../task-execution-viewer.css | 4 + .../task-execution-viewer.html | 35 +++++++ .../task-execution-viewer.ts | 13 +++ .../tasks-executions-list.css | 4 + .../tasks-executions-list.html | 82 ++++++++++++++++ .../tasks-executions-list.ts | 98 +++++++++++++++++++ src/app/utilities/rete-editor.ts | 15 ++- 32 files changed, 578 insertions(+), 62 deletions(-) create mode 100644 src/app/layouts/tasks-executor/tasks-executor.css create mode 100644 src/app/layouts/tasks-executor/tasks-executor.html rename src/app/{pages/main/tasks/tasks.spec.ts => layouts/tasks-executor/tasks-executor.spec.ts} (57%) create mode 100644 src/app/layouts/tasks-executor/tasks-executor.ts delete mode 100644 src/app/pages/main/tasks/tasks.css delete mode 100644 src/app/pages/main/tasks/tasks.html delete mode 100644 src/app/pages/main/tasks/tasks.ts create mode 100644 src/app/shared/task-execution-viewer/task-execution-viewer.css create mode 100644 src/app/shared/task-execution-viewer/task-execution-viewer.html create mode 100644 src/app/shared/task-execution-viewer/task-execution-viewer.ts create mode 100644 src/app/shared/tasks-executions-list/tasks-executions-list.css create mode 100644 src/app/shared/tasks-executions-list/tasks-executions-list.html create mode 100644 src/app/shared/tasks-executions-list/tasks-executions-list.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index f01e580..0888ed3 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,7 +4,7 @@ import { Signup } from '@pages/auth/signup/signup'; import { AppLayout } from '@layouts/app-layout/app-layout'; import { authGuard } from '@guards/auth-guard'; import { FlowEditor } from '@layouts/flow-editor/flow-editor'; -import { Tasks } from '@pages/main/tasks/tasks'; +import { TasksExecutor } from '@layouts/tasks-executor/tasks-executor'; export const routes: Routes = [ { @@ -22,7 +22,7 @@ export const routes: Routes = [ }, { path: 'tasks', - component: Tasks + component: TasksExecutor } ], canActivate: [authGuard] diff --git a/src/app/layouts/tasks-executor/tasks-executor.css b/src/app/layouts/tasks-executor/tasks-executor.css new file mode 100644 index 0000000..478abc9 --- /dev/null +++ b/src/app/layouts/tasks-executor/tasks-executor.css @@ -0,0 +1,4 @@ +:host { + display: block; + height: 100%; +} diff --git a/src/app/layouts/tasks-executor/tasks-executor.html b/src/app/layouts/tasks-executor/tasks-executor.html new file mode 100644 index 0000000..1dd5df5 --- /dev/null +++ b/src/app/layouts/tasks-executor/tasks-executor.html @@ -0,0 +1,14 @@ +
+
+ + + +
+ +
+
+
diff --git a/src/app/pages/main/tasks/tasks.spec.ts b/src/app/layouts/tasks-executor/tasks-executor.spec.ts similarity index 57% rename from src/app/pages/main/tasks/tasks.spec.ts rename to src/app/layouts/tasks-executor/tasks-executor.spec.ts index ae4a2fe..102831f 100644 --- a/src/app/pages/main/tasks/tasks.spec.ts +++ b/src/app/layouts/tasks-executor/tasks-executor.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Tasks } from './tasks'; +import { TasksExecutor } from './tasks-executor'; -describe('Tasks', () => { - let component: Tasks; - let fixture: ComponentFixture; +describe('TasksExecutor', () => { + let component: TasksExecutor; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Tasks] + imports: [TasksExecutor] }) .compileComponents(); - fixture = TestBed.createComponent(Tasks); + fixture = TestBed.createComponent(TasksExecutor); component = fixture.componentInstance; await fixture.whenStable(); }); diff --git a/src/app/layouts/tasks-executor/tasks-executor.ts b/src/app/layouts/tasks-executor/tasks-executor.ts new file mode 100644 index 0000000..803f00b --- /dev/null +++ b/src/app/layouts/tasks-executor/tasks-executor.ts @@ -0,0 +1,47 @@ +import { Component, signal } from '@angular/core'; +import { TasksExecutionsListComponent, TaskExecutionListItem } from '@shared/tasks-executions-list/tasks-executions-list'; +import { TaskExecutionViewerComponent } from '@shared/task-execution-viewer/task-execution-viewer'; + +@Component({ + selector: 'app-tasks-executor', + imports: [TasksExecutionsListComponent, TaskExecutionViewerComponent], + templateUrl: './tasks-executor.html', + styleUrl: './tasks-executor.css', +}) +export class TasksExecutor { + readonly executions = signal([ + { + id: 'run-001', + title: 'Customer onboarding flow', + flowName: 'Onboarding', + status: 'RUNNING', + startedAt: '2026-03-03 10:21', + duration: '00:02:13' + }, + { + id: 'run-002', + title: 'Weekly report generation', + flowName: 'Report Builder', + status: 'COMPLETED', + startedAt: '2026-03-03 09:15', + duration: '00:06:48' + }, + { + id: 'run-003', + title: 'Data enrichment pipeline', + flowName: 'Enricher', + status: 'FAILED', + startedAt: '2026-03-03 08:42', + duration: '00:01:59' + } + ]); + + readonly selectedExecutionId = signal(this.executions()[0]?.id ?? null); + + readonly selectedExecution = () => + this.executions().find((execution) => execution.id === this.selectedExecutionId()) ?? null; + + selectExecution(id: string) { + this.selectedExecutionId.set(id); + } +} diff --git a/src/app/models/nodes.ts b/src/app/models/nodes.ts index 49b9523..93b7349 100644 --- a/src/app/models/nodes.ts +++ b/src/app/models/nodes.ts @@ -1,8 +1,12 @@ import { GetSchemes, ClassicPreset } from "rete"; import { FlowBlock } from "./flow"; +export type HFNodeData = FlowBlock & { + deleteNode?: () => Promise; +}; + export type HFNode = ClassicPreset.Node & { - data?: FlowBlock; + data?: HFNodeData; }; export type HFConnection = ClassicPreset.Connection; diff --git a/src/app/pages/main/tasks/tasks.css b/src/app/pages/main/tasks/tasks.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/pages/main/tasks/tasks.html b/src/app/pages/main/tasks/tasks.html deleted file mode 100644 index 1c982ef..0000000 --- a/src/app/pages/main/tasks/tasks.html +++ /dev/null @@ -1 +0,0 @@ -

tasks works!

diff --git a/src/app/pages/main/tasks/tasks.ts b/src/app/pages/main/tasks/tasks.ts deleted file mode 100644 index 8ce1d5c..0000000 --- a/src/app/pages/main/tasks/tasks.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-tasks', - imports: [], - templateUrl: './tasks.html', - styleUrl: './tasks.css', -}) -export class Tasks { - -} diff --git a/src/app/services/blocks/blocks-call.fake.ts b/src/app/services/blocks/blocks-call.fake.ts index d235b94..6c3855a 100644 --- a/src/app/services/blocks/blocks-call.fake.ts +++ b/src/app/services/blocks/blocks-call.fake.ts @@ -51,6 +51,10 @@ export class BlocksCallServiceFake extends BlocksCallServiceBase { "LLMDescriptor": { "type": "object", "additionalProperties": false, + "required": [ + "provider", + "model" + ], "properties": { "provider": { "type": "string", @@ -111,6 +115,10 @@ export class BlocksCallServiceFake extends BlocksCallServiceBase { "LLMDescriptor": { "type": "object", "additionalProperties": false, + "required": [ + "provider", + "model" + ], "properties": { "provider": { "type": "string", diff --git a/src/app/shared/custom-socket/custom-socket.ts b/src/app/shared/custom-socket/custom-socket.ts index b1536da..69f93a5 100644 --- a/src/app/shared/custom-socket/custom-socket.ts +++ b/src/app/shared/custom-socket/custom-socket.ts @@ -15,20 +15,26 @@ export class CustomSocket { @HostBinding("style.width") w = "15px"; @HostBinding("style.height") h = "15px"; @HostBinding("style.display") d = "block"; - @HostBinding("style.borderRadius") br = "9999px"; - @HostBinding("style.border") border = "2px solid white"; + @HostBinding("style.borderRadius") br = "4px"; + @HostBinding("style.border") border = "1px solid rgba(255,255,255,0.9)"; @HostBinding("style.cursor") cursor = "crosshair"; @HostBinding("style.background") get bg() { - return this.data?.side === "input" ? "rgb(34,197,94)" : "rgb(99,102,241)"; + return this.socketSide === "input" + ? "linear-gradient(145deg, #4ade80 0%, #16a34a 100%)" + : "linear-gradient(145deg, #fb7185 0%, #dc2626 100%)"; } @HostBinding("style.boxShadow") get sh() { - console.log("CustomSocket sh data", this.data); - const c = this.data?.side === "input" ? "rgb(34,197,94)" : "rgb(99,102,241)"; - return `0 0 0 1px ${c}`; + const c = this.socketSide === "input" ? "rgba(22,163,74,0.45)" : "rgba(220,38,38,0.45)"; + return `0 2px 6px ${c}, 0 0 0 1px ${c}`; + } + + private get socketSide(): "input" | "output" { + const side = this.data?.__hfSide ?? this.data?.side; + return side === "output" ? "output" : "input"; } diff --git a/src/app/shared/nodes/human-interaction/human-interaction.css b/src/app/shared/nodes/human-interaction/human-interaction.css index cdd7f38..96f697e 100644 --- a/src/app/shared/nodes/human-interaction/human-interaction.css +++ b/src/app/shared/nodes/human-interaction/human-interaction.css @@ -47,6 +47,13 @@ min-width: 0; } +.hi-header-actions { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 6px; +} + .hi-warning { width: 24px; height: 24px; @@ -97,7 +104,25 @@ .hi-warning-wrap { position: relative; - margin-left: auto; + margin-left: 0; +} + +.hi-delete-btn { + width: 24px; + height: 24px; + border-radius: 999px; + border: 1px solid rgba(127, 29, 29, 0.45); + background: rgba(127, 29, 29, 0.2); + color: #fff1f2; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.15s ease; +} + +.hi-delete-btn:hover { + background: rgba(127, 29, 29, 0.35); } .hi-warning-tooltip { diff --git a/src/app/shared/nodes/human-interaction/human-interaction.html b/src/app/shared/nodes/human-interaction/human-interaction.html index 83c3f3f..58cf3ca 100644 --- a/src/app/shared/nodes/human-interaction/human-interaction.html +++ b/src/app/shared/nodes/human-interaction/human-interaction.html @@ -12,19 +12,24 @@ - @if (missingRequiredParams.length) { -
-
- -
-
-
Missing required fields
- @for (missing of missingRequiredParams; track missing) { -
{{ missing }}
- } +
+ @if (missingRequiredParams.length) { +
+
+ +
+
+
Missing required fields
+ @for (missing of missingRequiredParams; track missing) { +
{{ missing }}
+ } +
+ } +
- }
diff --git a/src/app/shared/nodes/human-interaction/human-interaction.ts b/src/app/shared/nodes/human-interaction/human-interaction.ts index 7d5e791..6d5377f 100644 --- a/src/app/shared/nodes/human-interaction/human-interaction.ts +++ b/src/app/shared/nodes/human-interaction/human-interaction.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, HostBinding, inject, Input } from '@angular/core'; +import { ChangeDetectorRef, Component, HostBinding, inject, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ClassicPreset } from 'rete'; import { ReteModule } from 'rete-angular-plugin/21'; @@ -24,6 +24,7 @@ export class HumanInteractionNodeComponent { private editorState = inject(EditorStateHolder); private fieldRetreiver = inject(FieldRetreiver); private blocksService = inject(BlocksService); + private cdr = inject(ChangeDetectorRef); @Input() data!: any; @Input() emit!: (data: any) => void; @@ -181,6 +182,19 @@ export class HumanInteractionNodeComponent { this.openSimpleParamEditor('name', event); } + async confirmDelete(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + + const confirmed = window.confirm('Do you want to delete this node from the flow?'); + if (!confirmed) return; + + const deleteNode = this.data?.data?.deleteNode; + if (typeof deleteNode === 'function') { + await deleteNode(); + } + } + private ensureBlockConfiguration(): Record { if (!this.data?.data) { this.data.data = {}; @@ -241,6 +255,8 @@ export class HumanInteractionNodeComponent { if (!this.refreshingConditionalRequirements) { void this.refreshConditionalRequirements(); } + + this.refreshView(); } private getByPath(source: Record, path: string): unknown { @@ -309,4 +325,14 @@ export class HumanInteractionNodeComponent { isConditionallyRequired(path: string) { return !!this.conditionalRequiredByPath.get(path); } + + private refreshView() { + queueMicrotask(() => { + try { + this.cdr.detectChanges(); + } catch { + // Node may have been removed while async validation was running. + } + }); + } } diff --git a/src/app/shared/nodes/input/input-node.component.html b/src/app/shared/nodes/input/input-node.component.html index 7ac936c..a2b4006 100644 --- a/src/app/shared/nodes/input/input-node.component.html +++ b/src/app/shared/nodes/input/input-node.component.html @@ -1,6 +1,14 @@
+
{{ data.label }} @@ -20,4 +28,4 @@
-
\ No newline at end of file +
diff --git a/src/app/shared/nodes/input/input-node.component.scss b/src/app/shared/nodes/input/input-node.component.scss index 37ec2c8..0edb29c 100644 --- a/src/app/shared/nodes/input/input-node.component.scss +++ b/src/app/shared/nodes/input/input-node.component.scss @@ -35,4 +35,25 @@ .input-socket { text-align: left; margin-left: -1px; display: inline-block; -} \ No newline at end of file +} + +.node-delete-btn { + position: absolute; + right: 4px; + top: 4px; + width: 22px; + height: 22px; + border: none; + border-radius: 999px; + background: rgba(185, 28, 28, 0.12); + color: #991b1b; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 20; +} + +.node-delete-btn:hover { + background: rgba(185, 28, 28, 0.2); +} diff --git a/src/app/shared/nodes/input/input-node.component.ts b/src/app/shared/nodes/input/input-node.component.ts index 430f97a..f2bd53f 100644 --- a/src/app/shared/nodes/input/input-node.component.ts +++ b/src/app/shared/nodes/input/input-node.component.ts @@ -42,4 +42,17 @@ export class InputNodeComponent { this.rendered(); } + async confirmDelete(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + + const confirmed = window.confirm('Do you want to delete this node from the flow?'); + if (!confirmed) return; + + const deleteNode = this.data?.data?.deleteNode; + if (typeof deleteNode === 'function') { + await deleteNode(); + } + } + } diff --git a/src/app/shared/nodes/llm/llm.css b/src/app/shared/nodes/llm/llm.css index ac28473..b13c443 100644 --- a/src/app/shared/nodes/llm/llm.css +++ b/src/app/shared/nodes/llm/llm.css @@ -52,6 +52,13 @@ min-width: 0; } +.llm-header-actions { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 6px; +} + .llm-warning { width: 24px; height: 24px; @@ -102,7 +109,25 @@ .llm-warning-wrap { position: relative; - margin-left: auto; + margin-left: 0; +} + +.llm-delete-btn { + width: 24px; + height: 24px; + border-radius: 999px; + border: 1px solid rgba(185, 28, 28, 0.45); + background: rgba(127, 29, 29, 0.2); + color: #fff1f2; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.15s ease; +} + +.llm-delete-btn:hover { + background: rgba(127, 29, 29, 0.35); } .llm-warning-tooltip { diff --git a/src/app/shared/nodes/llm/llm.html b/src/app/shared/nodes/llm/llm.html index 124c0c3..f6e897a 100644 --- a/src/app/shared/nodes/llm/llm.html +++ b/src/app/shared/nodes/llm/llm.html @@ -12,19 +12,24 @@
- @if (missingRequiredParams.length) { -
-
- -
-
-
Missing required fields
- @for (missing of missingRequiredParams; track missing) { -
{{ missing }}
- } +
+ @if (missingRequiredParams.length) { +
+
+ +
+
+
Missing required fields
+ @for (missing of missingRequiredParams; track missing) { +
{{ missing }}
+ } +
+ } +
- }
diff --git a/src/app/shared/nodes/llm/llm.ts b/src/app/shared/nodes/llm/llm.ts index fffb106..9d5d23f 100644 --- a/src/app/shared/nodes/llm/llm.ts +++ b/src/app/shared/nodes/llm/llm.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, HostBinding, inject, Input } from '@angular/core'; +import { ChangeDetectorRef, Component, HostBinding, inject, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ClassicPreset } from 'rete'; import { ReteModule } from 'rete-angular-plugin/21'; @@ -25,6 +25,7 @@ export class LLMNodeComponent { private editorState = inject(EditorStateHolder); private fieldRetreiver = inject(FieldRetreiver); private blocksService = inject(BlocksService); + private cdr = inject(ChangeDetectorRef); @Input() data!: any; @Input() emit!: (data: any) => void; @@ -79,8 +80,7 @@ export class LLMNodeComponent { this.inputs.push({ key: inKey, socket: (input as any).socket }); }); - const config = this.blockConfiguration; - if (!config) return; + const config = this.ensureBlockConfiguration(); this.name = this.toStringOrNull(config['name']) || this.name; @@ -211,6 +211,19 @@ export class LLMNodeComponent { this.openSimpleParamEditor('name', event); } + async confirmDelete(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + + const confirmed = window.confirm('Do you want to delete this node from the flow?'); + if (!confirmed) return; + + const deleteNode = this.data?.data?.deleteNode; + if (typeof deleteNode === 'function') { + await deleteNode(); + } + } + private get blockConfiguration(): Record | null { return this.data?.data?.specificConfiguration ?? null; } @@ -280,6 +293,8 @@ export class LLMNodeComponent { if (!this.refreshingConditionalRequirements) { void this.refreshConditionalRequirements(); } + + this.refreshView(); } private getByPath(source: Record, path: string): unknown { @@ -387,4 +402,14 @@ export class LLMNodeComponent { return match ? match[1] : token; } + private refreshView() { + queueMicrotask(() => { + try { + this.cdr.detectChanges(); + } catch { + // Node may have been removed while async validation was running. + } + }); + } + } diff --git a/src/app/shared/nodes/output/output-node.component.html b/src/app/shared/nodes/output/output-node.component.html index e21b973..3d7c70b 100644 --- a/src/app/shared/nodes/output/output-node.component.html +++ b/src/app/shared/nodes/output/output-node.component.html @@ -1,6 +1,14 @@
+
{{ data.label }} @@ -23,4 +31,3 @@
- diff --git a/src/app/shared/nodes/output/output-node.component.scss b/src/app/shared/nodes/output/output-node.component.scss index a6694b2..934b42b 100644 --- a/src/app/shared/nodes/output/output-node.component.scss +++ b/src/app/shared/nodes/output/output-node.component.scss @@ -20,3 +20,24 @@ color:white; font-weight: bolder; } + +.node-delete-btn { + position: absolute; + right: 4px; + top: 4px; + width: 22px; + height: 22px; + border: none; + border-radius: 999px; + background: rgba(185, 28, 28, 0.12); + color: #991b1b; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 20; +} + +.node-delete-btn:hover { + background: rgba(185, 28, 28, 0.2); +} diff --git a/src/app/shared/nodes/output/output-node.component.ts b/src/app/shared/nodes/output/output-node.component.ts index e003974..2a173a3 100644 --- a/src/app/shared/nodes/output/output-node.component.ts +++ b/src/app/shared/nodes/output/output-node.component.ts @@ -41,4 +41,17 @@ export class OutputNodeComponent { this.inputSocket = (input as any).socket; } + + async confirmDelete(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + + const confirmed = window.confirm('Do you want to delete this node from the flow?'); + if (!confirmed) return; + + const deleteNode = (this.data as any)?.data?.deleteNode; + if (typeof deleteNode === 'function') { + await deleteNode(); + } + } } diff --git a/src/app/shared/ordering/ordering.css b/src/app/shared/ordering/ordering.css index e69de29..fe8bbc5 100644 --- a/src/app/shared/ordering/ordering.css +++ b/src/app/shared/ordering/ordering.css @@ -0,0 +1,4 @@ +:host { + display: block; + width: 100%; +} diff --git a/src/app/shared/ordering/ordering.html b/src/app/shared/ordering/ordering.html index 2642b08..3eabf41 100644 --- a/src/app/shared/ordering/ordering.html +++ b/src/app/shared/ordering/ordering.html @@ -1,7 +1,7 @@
-
+
+
+
+ +
+
+ + + + +
+
+ +
+ + +
+ +
+ @if (!filteredExecutions().length) { +
No executions available
+ } @else { + @for (execution of filteredExecutions(); track execution.id) { + + } + } +
+ diff --git a/src/app/shared/tasks-executions-list/tasks-executions-list.ts b/src/app/shared/tasks-executions-list/tasks-executions-list.ts new file mode 100644 index 0000000..105a663 --- /dev/null +++ b/src/app/shared/tasks-executions-list/tasks-executions-list.ts @@ -0,0 +1,98 @@ +import { CommonModule } from '@angular/common'; +import { Component, computed, input, model, output, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { OrderEvent, OrderField, Ordering, orderDirType } from '@shared/ordering/ordering'; +import { OrderViewState } from '@utilities/list-state-holder'; + +export type TaskExecutionStatus = 'RUNNING' | 'COMPLETED' | 'FAILED' | 'QUEUED'; + +export type TaskExecutionListItem = { + id: string; + title: string; + flowName: string; + status: TaskExecutionStatus; + startedAt: string; + duration?: string; +}; + +@Component({ + selector: 'app-tasks-executions-list', + imports: [CommonModule, FormsModule, Ordering], + templateUrl: './tasks-executions-list.html', + styleUrl: './tasks-executions-list.css', +}) +export class TasksExecutionsListComponent { + readonly executions = input([]); + readonly selectedExecutionId = input(null); + readonly executionSelected = output(); + readonly searchTerm = model(''); + readonly filter = signal('all'); + readonly orderBy = signal('startedAt'); + readonly orderDir = signal('desc'); + + readonly orderView: OrderViewState = { + orderBy: this.orderBy(), + orderDir: this.orderDir() + }; + + readonly orderFields: OrderField[] = [ + { field: 'title', label: 'Title' }, + { field: 'flowName', label: 'Flow Name' }, + { field: 'startedAt', label: 'Started At' }, + { field: 'duration', label: 'Duration' } + ]; + + readonly filteredExecutions = computed(() => { + const term = this.searchTerm().trim().toLowerCase(); + const status = this.filter(); + const orderBy = this.orderBy(); + const orderDir = this.orderDir(); + + const filtered = this.executions().filter((execution) => { + if (status !== 'all' && execution.status !== status) return false; + if (!term) return true; + + return ( + execution.title.toLowerCase().includes(term) || + execution.flowName.toLowerCase().includes(term) || + execution.id.toLowerCase().includes(term) + ); + }); + + if (!orderBy) return filtered; + + return [...filtered].sort((a, b) => { + const aValue = (a as any)[orderBy]; + const bValue = (b as any)[orderBy]; + + if (aValue == null && bValue == null) return 0; + if (aValue == null) return orderDir === 'asc' ? -1 : 1; + if (bValue == null) return orderDir === 'asc' ? 1 : -1; + + const aComparable = typeof aValue === 'string' ? aValue.toLowerCase() : aValue; + const bComparable = typeof bValue === 'string' ? bValue.toLowerCase() : bValue; + + if (aComparable < bComparable) return orderDir === 'asc' ? -1 : 1; + if (aComparable > bComparable) return orderDir === 'asc' ? 1 : -1; + return 0; + }); + }); + + selectExecution(executionId: string) { + this.executionSelected.emit(executionId); + } + + onOrderChanged(event: OrderEvent) { + this.orderBy.set(event.orderBy); + this.orderDir.set(event.orderDir); + this.orderView.orderBy = event.orderBy; + this.orderView.orderDir = event.orderDir; + } + + statusBadgeClass(status: TaskExecutionStatus) { + if (status === 'RUNNING') return 'bg-blue-100 text-blue-700 border-blue-200'; + if (status === 'COMPLETED') return 'bg-emerald-100 text-emerald-700 border-emerald-200'; + if (status === 'FAILED') return 'bg-rose-100 text-rose-700 border-rose-200'; + return 'bg-slate-100 text-slate-700 border-slate-200'; + } +} diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index 22e88e1..c99e5e7 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -50,7 +50,14 @@ export async function createEditor( } return LLMNodeComponent; }, - socket() { + socket(context: any) { + // rete-angular passes only `payload` to socket component props, + // so we persist side on the payload to style input/output sockets. + const socketPayload = context?.payload; + const socketSide = context?.side; + if (socketPayload && (socketSide === "input" || socketSide === "output")) { + (socketPayload as any).__hfSide = socketSide; + } return CustomSocket; } }, @@ -131,7 +138,11 @@ export async function addBlockToEditor( position?: { x: number; y: number } ) { const node = new ClassicPreset.Node(toNodeLabel(block.typeName)) as HFNode; - node.data = { ...block, position: position ?? block.position }; + const removeNode = async () => { + if (!editor.getNode(node.id)) return; + await editor.removeNode(node.id); + }; + node.data = { ...block, position: position ?? block.position, deleteNode: removeNode }; for (const output of block.outputs ?? []) { node.addOutput(output.name, new ClassicPreset.Output(getSocket(editor, output.type ?? "ANY")));