updated task-execution with all methods

This commit is contained in:
Lucio Lelii 2026-03-05 21:53:27 +01:00
parent b639133401
commit 4cedab52bc
8 changed files with 214 additions and 31 deletions

View File

@ -1,8 +1,7 @@
import { Component, computed, effect, inject, signal } from '@angular/core';
import { TaskExecution } from '@models/task-execution';
import { normalizeExecutionStatus, TaskExecution } from '@models/task-execution';
import {
TaskExecutionListItem,
TaskExecutionStatus,
TasksExecutionsListComponent
} from '@shared/tasks-executions-list/tasks-executions-list';
import { TaskExecutionViewerComponent } from '@shared/task-execution-viewer/task-execution-viewer';
@ -24,7 +23,7 @@ export class TasksExecutor {
id: execution.id,
title: execution.name,
flowName: execution.name,
status: this.toListStatus(execution.context.status),
status: normalizeExecutionStatus(execution.context.status),
startedAt: this.formatDateTime(execution.creationTime),
duration: this.formatDuration(execution.context.startTime ?? null, execution.context.endTime ?? null)
}))
@ -53,13 +52,6 @@ export class TasksExecutor {
this.selectedExecutionId.set(id);
}
private toListStatus(status: string): TaskExecutionStatus {
if (status === 'COMPLETED') return 'COMPLETED';
if (status === 'FAILED' || status === 'ERROR') return 'FAILED';
if (status === 'RUNNING' || status === 'WAITING' || status === 'WAITING_FOR_INPUT') return 'RUNNING';
return 'QUEUED';
}
private formatDateTime(timestamp: number): string {
const date = new Date(timestamp);
const yyyy = date.getFullYear();

View File

@ -1,6 +1,8 @@
import { FlowBlock, FlowPort } from './flow';
export type ExecutionStatus = 'RUNNING' | 'COMPLETED' | 'ERROR' | 'FAILED' | 'WAITING_FOR_INPUT' | string;
export type TaskExecutionStatus = 'CREATED' | 'READY' | 'RUNNING' | 'WAITING' | 'SUCCESS' | 'ERROR';
export type TaskExecutionStatusGroup = 'INIT' | 'RUNNING' | 'FINAL';
export type StepStatus = 'WAITING_FOR_INPUT' | 'FAILED' | 'COMPLETED' | 'RUNNING' | string;
export type TaskExecution = {
@ -18,7 +20,7 @@ export type TaskExecutionContext = {
errors: Record<string, string>;
warnings: Record<string, string>;
steps: Record<string, TaskExecutionStep>;
status: ExecutionStatus;
status: TaskExecutionStatus;
waitingSteps: string[];
executionResult: Record<string, unknown>;
};
@ -44,3 +46,34 @@ export type TaskExecutionStepOutput = {
descriptor: FlowPort;
connected: boolean;
};
export function getExecutionStatusGroup(status: string | null | undefined): TaskExecutionStatusGroup | null {
const normalized = String(status ?? '').toUpperCase();
if (!normalized) return null;
if (normalized === 'CREATED' || normalized === 'READY') return 'INIT';
if (
normalized === 'RUNNING' ||
normalized === 'WAITING'
) {
return 'RUNNING';
}
if (normalized === 'SUCCESS' || normalized === 'ERROR') {
return 'FINAL';
}
return null;
}
export function normalizeExecutionStatus(status: string | null | undefined): TaskExecutionStatus {
const normalized = String(status ?? '').toUpperCase();
if (normalized === 'CREATED') return 'CREATED';
if (normalized === 'READY') return 'READY';
if (normalized === 'RUNNING') return 'RUNNING';
if (normalized === 'WAITING' || normalized === 'WAITING_FOR_INPUT' || normalized === 'WAITING_FOR_INTERACTION') {
return 'WAITING';
}
if (normalized === 'SUCCESS' || normalized === 'COMPLETED') return 'SUCCESS';
if (normalized === 'ERROR' || normalized === 'FAILED') return 'ERROR';
return 'CREATED';
}

View File

@ -3,5 +3,18 @@ import { Observable } from 'rxjs';
export abstract class TaskExecutionsCallServiceBase {
abstract retrieveAllTaskExecutions(): Observable<TaskExecution[]>;
abstract createTaskExecution(flowId: string): Observable<TaskExecution>;
abstract startTaskExecution(executionId: string): Observable<TaskExecution>;
abstract prepareStringInput(
executionId: string,
nodeId: string,
inputName: string,
value: string
): Observable<TaskExecution>;
abstract prepareFileInput(
executionId: string,
nodeId: string,
inputName: string,
file: File
): Observable<TaskExecution>;
}

View File

@ -447,5 +447,65 @@ export class TaskExecutionsCallServiceFake extends TaskExecutionsCallServiceBase
override retrieveAllTaskExecutions(): Observable<TaskExecution[]> {
return of(this.data);
}
}
override createTaskExecution(flowId: string): Observable<TaskExecution> {
const execution: TaskExecution = {
id: crypto.randomUUID(),
name: flowId || 'Execution',
creationTime: Date.now(),
context: {
inputs: {},
result: {},
startTime: null,
endTime: null,
errors: {},
warnings: {},
steps: {},
status: 'CREATED',
waitingSteps: [],
executionResult: {}
}
};
this.data.unshift(execution);
return of(execution);
}
override startTaskExecution(executionId: string): Observable<TaskExecution> {
const execution = this.findExecution(executionId);
execution.context.status = 'RUNNING';
execution.context.startTime = execution.context.startTime ?? Date.now();
return of(execution);
}
override prepareStringInput(
executionId: string,
nodeId: string,
inputName: string,
value: string
): Observable<TaskExecution> {
const execution = this.findExecution(executionId);
execution.context.inputs[`${nodeId}:${inputName}`] = value;
execution.context.status = execution.context.waitingSteps.length ? 'WAITING' : execution.context.status;
return of(execution);
}
override prepareFileInput(
executionId: string,
nodeId: string,
inputName: string,
file: File
): Observable<TaskExecution> {
const execution = this.findExecution(executionId);
execution.context.inputs[`${nodeId}:${inputName}`] = file?.name ?? '';
execution.context.status = execution.context.waitingSteps.length ? 'WAITING' : execution.context.status;
return of(execution);
}
private findExecution(executionId: string): TaskExecution {
const execution = this.data.find((item) => item.id === executionId);
if (!execution) {
throw new Error(`Execution with id ${executionId} not found`);
}
return execution;
}
}

View File

@ -1,10 +1,46 @@
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { environment } from '@environment';
import { TaskExecution } from '@models/task-execution';
import { Observable } from 'rxjs';
import { TaskExecutionsCallServiceBase } from './task-executions-call.base';
export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase {
private readonly http = inject(HttpClient);
override retrieveAllTaskExecutions(): Observable<TaskExecution[]> {
throw new Error('Method not implemented.');
return this.http.get<TaskExecution[]>(`${environment.apiUrl}/executions`);
}
override createTaskExecution(flowId: string): Observable<TaskExecution> {
return this.http.post<TaskExecution>(`${environment.apiUrl}/executions`, flowId);
}
override startTaskExecution(executionId: string): Observable<TaskExecution> {
return this.http.put<TaskExecution>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/start`, null);
}
override prepareStringInput(
executionId: string,
nodeId: string,
inputName: string,
value: string
): Observable<TaskExecution> {
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/text`;
return this.http.put<TaskExecution>(url, value, {
headers: { 'Content-Type': 'text/plain' }
});
}
override prepareFileInput(
executionId: string,
nodeId: string,
inputName: string,
file: File
): Observable<TaskExecution> {
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/file`;
const formData = new FormData();
formData.append('file', file);
return this.http.put<TaskExecution>(url, formData);
}
}

View File

@ -1,6 +1,7 @@
import { Injectable, signal } from '@angular/core';
import { environment } from '@environment';
import { TaskExecution } from '@models/task-execution';
import { catchError, tap, throwError } from 'rxjs';
import { TaskExecutionsCallServiceBase } from './task-executions-call.base';
@Injectable({
@ -24,5 +25,44 @@ export class TaskExecutionsService {
this._taskExecutions.set(taskExecutions);
});
}
}
createExecution(flowId: string) {
return this.taskExecutionsCallService.createTaskExecution(flowId).pipe(
tap(() => this.refresh()),
catchError((err) => {
console.error('Create execution failed', err);
return throwError(() => err);
})
);
}
startExecution(executionId: string) {
return this.taskExecutionsCallService.startTaskExecution(executionId).pipe(
tap(() => this.refresh()),
catchError((err) => {
console.error('Start execution failed', err);
return throwError(() => err);
})
);
}
prepareStringInput(executionId: string, nodeId: string, inputName: string, value: string) {
return this.taskExecutionsCallService.prepareStringInput(executionId, nodeId, inputName, value).pipe(
tap(() => this.refresh()),
catchError((err) => {
console.error('Prepare string input failed', err);
return throwError(() => err);
})
);
}
prepareFileInput(executionId: string, nodeId: string, inputName: string, file: File) {
return this.taskExecutionsCallService.prepareFileInput(executionId, nodeId, inputName, file).pipe(
tap(() => this.refresh()),
catchError((err) => {
console.error('Prepare file input failed', err);
return throwError(() => err);
})
);
}
}

View File

@ -21,18 +21,18 @@
[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() === 'INIT'"
[class.btn-outline-primary]="filter() !== 'INIT'"
(click)="filter.set('INIT')">Init</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>
[class.btn-primary]="filter() === 'FINAL'"
[class.btn-outline-primary]="filter() !== 'FINAL'"
(click)="filter.set('FINAL')">Final</button>
</div>
</div>

View File

@ -1,10 +1,11 @@
import { CommonModule } from '@angular/common';
import { Component, computed, input, model, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { getExecutionStatusGroup, TaskExecutionStatus, TaskExecutionStatusGroup } from '@models/task-execution';
import { OrderEvent, OrderField, Ordering, orderDirType } from '@shared/ordering/ordering';
import { OrderViewState } from '@utilities/list-state-holder';
export type TaskExecutionStatus = 'RUNNING' | 'COMPLETED' | 'FAILED' | 'ERROR' | 'QUEUED';
export type TaskExecutionFilter = 'all' | TaskExecutionStatusGroup;
export type TaskExecutionListItem = {
id: string;
@ -26,7 +27,7 @@ export class TasksExecutionsListComponent {
readonly selectedExecutionId = input<string | null>(null);
readonly executionSelected = output<string>();
readonly searchTerm = model<string>('');
readonly filter = signal<TaskExecutionStatus | 'all'>('all');
readonly filter = signal<TaskExecutionFilter>('all');
readonly orderBy = signal<string | null>('startedAt');
readonly orderDir = signal<orderDirType>('desc');
@ -44,12 +45,12 @@ export class TasksExecutionsListComponent {
readonly filteredExecutions = computed(() => {
const term = this.searchTerm().trim().toLowerCase();
const status = this.filter();
const filter = 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 (!this.matchesFilter(execution.status, filter)) return false;
if (!term) return true;
return (
@ -90,9 +91,17 @@ export class TasksExecutionsListComponent {
}
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' || status === 'ERROR') return 'bg-rose-100 text-rose-700 border-rose-200';
const normalized = String(status).toUpperCase();
if (normalized === 'SUCCESS') return 'bg-emerald-100 text-emerald-700 border-emerald-200';
if (normalized === 'ERROR') return 'bg-rose-100 text-rose-700 border-rose-200';
if (normalized === 'WAITING') return 'bg-amber-100 text-amber-700 border-amber-200';
if (normalized === 'RUNNING') return 'bg-blue-100 text-blue-700 border-blue-200';
if (normalized === 'CREATED' || normalized === 'READY') return 'bg-slate-100 text-slate-700 border-slate-200';
return 'bg-slate-100 text-slate-700 border-slate-200';
}
private matchesFilter(status: TaskExecutionStatus, filter: TaskExecutionFilter): boolean {
if (filter === 'all') return true;
return getExecutionStatusGroup(status) === filter;
}
}