added task layout
This commit is contained in:
parent
e3446e7aac
commit
4504ec0087
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<div class="flex flex-col flex-1 h-full">
|
||||
<div class="flex flex-row flex-1 overflow-hidden gap-2">
|
||||
<app-tasks-executions-list
|
||||
class="w-[320px] shrink-0"
|
||||
[executions]="executions()"
|
||||
[selectedExecutionId]="selectedExecutionId()"
|
||||
(executionSelected)="selectExecution($event)">
|
||||
</app-tasks-executions-list>
|
||||
|
||||
<main class="flex flex-col w-full bg-gray-100 overflow-hidden rounded-md">
|
||||
<app-task-execution-viewer [execution]="selectedExecution()"></app-task-execution-viewer>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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<Tasks>;
|
||||
describe('TasksExecutor', () => {
|
||||
let component: TasksExecutor;
|
||||
let fixture: ComponentFixture<TasksExecutor>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Tasks]
|
||||
imports: [TasksExecutor]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Tasks);
|
||||
fixture = TestBed.createComponent(TasksExecutor);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
|
@ -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<TaskExecutionListItem[]>([
|
||||
{
|
||||
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<string | null>(this.executions()[0]?.id ?? null);
|
||||
|
||||
readonly selectedExecution = () =>
|
||||
this.executions().find((execution) => execution.id === this.selectedExecutionId()) ?? null;
|
||||
|
||||
selectExecution(id: string) {
|
||||
this.selectedExecutionId.set(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
import { GetSchemes, ClassicPreset } from "rete";
|
||||
import { FlowBlock } from "./flow";
|
||||
|
||||
export type HFNodeData = FlowBlock & {
|
||||
deleteNode?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type HFNode = ClassicPreset.Node & {
|
||||
data?: FlowBlock;
|
||||
data?: HFNodeData;
|
||||
};
|
||||
|
||||
export type HFConnection = ClassicPreset.Connection<HFNode, HFNode>;
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<p>tasks works!</p>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
imports: [],
|
||||
templateUrl: './tasks.html',
|
||||
styleUrl: './tasks.css',
|
||||
})
|
||||
export class Tasks {
|
||||
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -12,19 +12,24 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (missingRequiredParams.length) {
|
||||
<div class="hi-warning-wrap">
|
||||
<div class="hi-warning">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<div class="hi-warning-tooltip">
|
||||
<div class="hi-warning-title">Missing required fields</div>
|
||||
@for (missing of missingRequiredParams; track missing) {
|
||||
<div class="hi-warning-item">{{ missing }}</div>
|
||||
}
|
||||
<div class="hi-header-actions">
|
||||
@if (missingRequiredParams.length) {
|
||||
<div class="hi-warning-wrap">
|
||||
<div class="hi-warning">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<div class="hi-warning-tooltip">
|
||||
<div class="hi-warning-title">Missing required fields</div>
|
||||
@for (missing of missingRequiredParams; track missing) {
|
||||
<div class="hi-warning-item">{{ missing }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="hi-delete-btn" title="Delete node" (pointerdown)="$event.stopPropagation()" (click)="confirmDelete($event)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="hi-body">
|
||||
|
|
|
|||
|
|
@ -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<string, any> {
|
||||
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<string, any>, 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.
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
<div class="cursor-move user-select-none rounded-xl bg-emerald-100 shadow-lg border border-gray-200 overflow-visible">
|
||||
<!-- header/row relativa -->
|
||||
<div class="relative w-full p-3 pt-0 flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
class="node-delete-btn"
|
||||
title="Delete node"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="confirmDelete($event)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<div class="grid grid-cols-4 h-full">
|
||||
<div class="col-span-4 text-ls mb-2 p-1 rounded-md text-green-700 font-bold">
|
||||
{{ data.label }}
|
||||
|
|
@ -20,4 +28,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,4 +35,25 @@
|
|||
|
||||
.input-socket {
|
||||
text-align: left; margin-left: -1px; display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -12,19 +12,24 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (missingRequiredParams.length) {
|
||||
<div class="llm-warning-wrap">
|
||||
<div class="llm-warning">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<div class="llm-warning-tooltip">
|
||||
<div class="llm-warning-title">Missing required fields</div>
|
||||
@for (missing of missingRequiredParams; track missing) {
|
||||
<div class="llm-warning-item">{{ missing }}</div>
|
||||
}
|
||||
<div class="llm-header-actions">
|
||||
@if (missingRequiredParams.length) {
|
||||
<div class="llm-warning-wrap">
|
||||
<div class="llm-warning">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<div class="llm-warning-tooltip">
|
||||
<div class="llm-warning-title">Missing required fields</div>
|
||||
@for (missing of missingRequiredParams; track missing) {
|
||||
<div class="llm-warning-item">{{ missing }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="llm-delete-btn" title="Delete node" (pointerdown)="$event.stopPropagation()" (click)="confirmDelete($event)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="llm-body">
|
||||
|
|
|
|||
|
|
@ -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<string, any> | 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<string, any>, 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.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
<div class="cursor-move user-select-none rounded-xl bg-red-100 shadow-lg border border-gray-200 overflow-visible">
|
||||
<!-- header/row relativa -->
|
||||
<div class="relative w-full p-3 pt-0 flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
class="node-delete-btn"
|
||||
title="Delete node"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="confirmDelete($event)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<div class="grid grid-cols-4 h-full">
|
||||
<div class="col-span-4 text-ls mb-2 p-1 rounded-md text-red-700 font-bold">
|
||||
{{ data.label }}
|
||||
|
|
@ -23,4 +31,3 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<div class="flex items-center w-full justify-between gap-2">
|
||||
<div class="form-floating">
|
||||
<div class="form-floating flex-1">
|
||||
<select id="orderSelect"
|
||||
class="form-select form-select-sm text-sm w-auto"
|
||||
class="form-select form-select-sm text-sm w-full"
|
||||
[(ngModel)]="orderBy"
|
||||
placeholder="Order By"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<section class="h-full p-4">
|
||||
@if (execution()) {
|
||||
<div class="h-full bg-white border border-slate-200 rounded-md flex flex-col overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200">
|
||||
<h2 class="text-base font-semibold text-slate-900">{{ execution()!.title }}</h2>
|
||||
<p class="text-sm text-slate-500">Execution ID: {{ execution()!.id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="p-5 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-md p-3">
|
||||
<div class="text-xs text-slate-500">Flow</div>
|
||||
<div class="text-sm font-medium text-slate-800">{{ execution()!.flowName }}</div>
|
||||
</div>
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-md p-3">
|
||||
<div class="text-xs text-slate-500">Status</div>
|
||||
<div class="text-sm font-medium text-slate-800">{{ execution()!.status }}</div>
|
||||
</div>
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-md p-3">
|
||||
<div class="text-xs text-slate-500">Duration</div>
|
||||
<div class="text-sm font-medium text-slate-800">{{ execution()!.duration || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-5 pt-0 overflow-auto">
|
||||
<div class="h-full rounded-md border border-dashed border-slate-300 bg-slate-50 text-slate-500 text-sm flex items-center justify-center">
|
||||
Execution details viewer placeholder
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="h-full bg-white border border-slate-200 rounded-md text-slate-400 text-center flex items-center justify-center">
|
||||
Select an execution from the left panel
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { TaskExecutionListItem } from '@shared/tasks-executions-list/tasks-executions-list';
|
||||
|
||||
@Component({
|
||||
selector: 'app-task-execution-viewer',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './task-execution-viewer.html',
|
||||
styleUrl: './task-execution-viewer.css',
|
||||
})
|
||||
export class TaskExecutionViewerComponent {
|
||||
readonly execution = input<TaskExecutionListItem | null>(null);
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<aside class="h-full bg-white border border-slate-200 rounded-md flex flex-col overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-slate-200">
|
||||
<h2 class="text-sm font-semibold text-slate-800">Task Executions</h2>
|
||||
<p class="text-xs text-slate-500">Running and completed runs</p>
|
||||
</div>
|
||||
|
||||
<div class="px-3 pt-2">
|
||||
<div class="relative flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||
class="absolute w-5 h-5 top-2.5 left-2.5 text-slate-600">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<input
|
||||
class="w-full bg-transparent placeholder:text-slate-400 text-slate-700 text-sm border border-slate-200 rounded-md pl-10 pr-3 py-2 transition duration-300 ease focus:outline-none focus:border-slate-400 hover:border-slate-300 shadow-sm focus:shadow"
|
||||
placeholder="Search executions..."
|
||||
[(ngModel)]="searchTerm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 pt-2">
|
||||
<div class="btn-group w-full h-8" role="group">
|
||||
<button type="button" class="btn btn-sm w-full transition"
|
||||
[class.btn-primary]="filter() === 'all'"
|
||||
[class.btn-outline-primary]="filter() !== 'all'"
|
||||
(click)="filter.set('all')">All</button>
|
||||
<button type="button" class="btn btn-sm w-full transition"
|
||||
[class.btn-primary]="filter() === 'RUNNING'"
|
||||
[class.btn-outline-primary]="filter() !== 'RUNNING'"
|
||||
(click)="filter.set('RUNNING')">Running</button>
|
||||
<button type="button" class="btn btn-sm w-full transition"
|
||||
[class.btn-primary]="filter() === 'COMPLETED'"
|
||||
[class.btn-outline-primary]="filter() !== 'COMPLETED'"
|
||||
(click)="filter.set('COMPLETED')">Completed</button>
|
||||
<button type="button" class="btn btn-sm w-full transition"
|
||||
[class.btn-primary]="filter() === 'FAILED'"
|
||||
[class.btn-outline-primary]="filter() !== 'FAILED'"
|
||||
(click)="filter.set('FAILED')">Failed</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 py-2">
|
||||
<app-ordering
|
||||
[orderFields]="orderFields"
|
||||
[orderView]="orderView"
|
||||
(orderChanged)="onOrderChanged($event)">
|
||||
</app-ordering>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-2 space-y-2">
|
||||
@if (!filteredExecutions().length) {
|
||||
<div class="text-xs text-slate-500 px-2 py-4 text-center">No executions available</div>
|
||||
} @else {
|
||||
@for (execution of filteredExecutions(); track execution.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left p-3 rounded-md border transition-colors"
|
||||
[class.border-blue-300]="execution.id === selectedExecutionId()"
|
||||
[class.bg-blue-50]="execution.id === selectedExecutionId()"
|
||||
[class.border-slate-200]="execution.id !== selectedExecutionId()"
|
||||
[class.bg-white]="execution.id !== selectedExecutionId()"
|
||||
(click)="selectExecution(execution.id)">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium text-slate-800 truncate">{{ execution.title }}</div>
|
||||
<div class="text-xs text-slate-500 truncate">{{ execution.flowName }}</div>
|
||||
</div>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full border font-semibold"
|
||||
[ngClass]="statusBadgeClass(execution.status)">
|
||||
{{ execution.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-slate-500 flex justify-between gap-2">
|
||||
<span>{{ execution.startedAt }}</span>
|
||||
<span>{{ execution.duration || '-' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
|
|
@ -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<TaskExecutionListItem[]>([]);
|
||||
readonly selectedExecutionId = input<string | null>(null);
|
||||
readonly executionSelected = output<string>();
|
||||
readonly searchTerm = model<string>('');
|
||||
readonly filter = signal<TaskExecutionStatus | 'all'>('all');
|
||||
readonly orderBy = signal<string | null>('startedAt');
|
||||
readonly orderDir = signal<orderDirType>('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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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")));
|
||||
|
|
|
|||
Loading…
Reference in New Issue