Add suspended execution resume support and production Docker setup
This commit is contained in:
parent
447b01786d
commit
fad93a3eca
|
|
@ -0,0 +1,8 @@
|
|||
node_modules
|
||||
dist
|
||||
.angular
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
README.md
|
||||
npm-debug.log
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/dist/humainFlow-gui-a21/browser /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -43,13 +43,13 @@
|
|||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
"maximumWarning": "1.75MB",
|
||||
"maximumError": "2MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
"maximumWarning": "12kB",
|
||||
"maximumError": "16kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
|
|
@ -23,17 +23,17 @@
|
|||
"private": true,
|
||||
"packageManager": "npm@10.8.2",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.0.4",
|
||||
"@angular/cdk": "^21.0.5",
|
||||
"@angular/common": "^21.0.4",
|
||||
"@angular/compiler": "^21.0.4",
|
||||
"@angular/core": "^21.0.4",
|
||||
"@angular/elements": "^21.0.8",
|
||||
"@angular/forms": "^21.0.4",
|
||||
"@angular/material": "^21.0.5",
|
||||
"@angular/platform-browser": "^21.0.4",
|
||||
"@angular/platform-browser-dynamic": "^21.0.4",
|
||||
"@angular/router": "^21.0.0",
|
||||
"@angular/animations": "21.0.5",
|
||||
"@angular/cdk": "21.0.5",
|
||||
"@angular/common": "21.0.5",
|
||||
"@angular/compiler": "21.0.5",
|
||||
"@angular/core": "21.0.5",
|
||||
"@angular/elements": "21.0.5",
|
||||
"@angular/forms": "21.0.5",
|
||||
"@angular/material": "21.0.5",
|
||||
"@angular/platform-browser": "21.0.5",
|
||||
"@angular/platform-browser-dynamic": "21.0.5",
|
||||
"@angular/router": "21.0.5",
|
||||
"@fontsource/material-icons-outlined": "^5.2.6",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
|
|
@ -50,9 +50,9 @@
|
|||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^21.0.3",
|
||||
"@angular/cli": "^21.0.3",
|
||||
"@angular/compiler-cli": "^21.0.0",
|
||||
"@angular/build": "21.0.3",
|
||||
"@angular/cli": "21.0.3",
|
||||
"@angular/compiler-cli": "21.0.5",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"jsdom": "^27.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { FlowBlock, FlowBlockConnection, FlowContainer, FlowNode, FlowPort } from './flow';
|
||||
|
||||
export type TaskExecutionStatus = 'CREATED' | 'READY' | 'RUNNING' | 'WAITING' | 'SUCCESS' | 'ERROR' | 'CANCELLED';
|
||||
export type TaskExecutionStatusGroup = 'INIT' | 'RUNNING' | 'FINAL';
|
||||
export type TaskExecutionStatus = 'CREATED' | 'READY' | 'RUNNING' | 'WAITING' | 'SUSPENDED' | 'SUCCESS' | 'ERROR' | 'CANCELLED';
|
||||
export type TaskExecutionStatusGroup = 'INIT' | 'RUNNING' | 'PAUSED' | 'FINAL';
|
||||
|
||||
export type StepStatus = 'WAITING_FOR_INPUT' | 'FAILED' | 'COMPLETED' | 'RUNNING' | string;
|
||||
|
||||
|
|
@ -73,6 +73,9 @@ export function getExecutionStatusGroup(status: string | null | undefined): Task
|
|||
) {
|
||||
return 'RUNNING';
|
||||
}
|
||||
if (normalized === 'SUSPENDED') {
|
||||
return 'PAUSED';
|
||||
}
|
||||
if (normalized === 'SUCCESS' || normalized === 'ERROR' || normalized === 'CANCELLED') {
|
||||
return 'FINAL';
|
||||
}
|
||||
|
|
@ -88,6 +91,7 @@ export function normalizeExecutionStatus(status: string | null | undefined): Tas
|
|||
if (normalized === 'WAITING' || normalized === 'WAITING_FOR_INPUT' || normalized === 'WAITING_FOR_INTERACTION') {
|
||||
return 'WAITING';
|
||||
}
|
||||
if (normalized === 'SUSPENDED') return 'SUSPENDED';
|
||||
if (normalized === 'CANCELLED') return 'CANCELLED';
|
||||
if (normalized === 'SUCCESS' || normalized === 'COMPLETED') return 'SUCCESS';
|
||||
if (normalized === 'ERROR' || normalized === 'FAILED') return 'ERROR';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { form, Field, minLength, email, required, validate, maxLength, disabled } from '@angular/forms/signals'
|
||||
import { Field, form, minLength, email, required, validate, maxLength, disabled } from '@angular/forms/signals'
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { UserRegistration } from '@models/user';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
|
|
@ -13,7 +13,7 @@ import { FormUtility } from '@utilities/form-utility';
|
|||
|
||||
@Component({
|
||||
selector: 'app-signup',
|
||||
imports: [FormsModule, Field, RouterLink, MatButtonModule, MatCardModule, MatFormFieldModule, MatInputModule],
|
||||
imports: [FormsModule, RouterLink, Field, MatButtonModule, MatCardModule, MatFormFieldModule, MatInputModule],
|
||||
templateUrl: './signup.html',
|
||||
styleUrl: './signup.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export abstract class TaskExecutionsCallServiceBase {
|
|||
abstract deleteTaskExecution(executionId: string): Observable<void>;
|
||||
abstract startTaskExecution(executionId: string): Observable<TaskExecution>;
|
||||
abstract cancelTaskExecution(executionId: string): Observable<TaskExecution>;
|
||||
abstract resumeTaskExecution(executionId: string): Observable<TaskExecution>;
|
||||
abstract prepareStringInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
|
|
|
|||
|
|
@ -516,6 +516,24 @@ export class TaskExecutionsCallServiceFake extends TaskExecutionsCallServiceBase
|
|||
return of(execution);
|
||||
}
|
||||
|
||||
override resumeTaskExecution(executionId: string): Observable<TaskExecution> {
|
||||
const execution = this.findExecution(executionId);
|
||||
const hasWaitingSteps = (execution.context.waitingSteps?.length ?? 0) > 0;
|
||||
execution.context.status = hasWaitingSteps ? 'WAITING' : 'RUNNING';
|
||||
execution.context.endTime = null;
|
||||
|
||||
for (const stepId of execution.context.waitingSteps ?? []) {
|
||||
const step = execution.context.steps?.[stepId];
|
||||
if (!step) continue;
|
||||
const status = String(step.status ?? '').toUpperCase();
|
||||
if (status === 'SUSPENDED' || status === 'WAITING' || status === 'WAITING_FOR_INPUT' || status === 'WAITING_FOR_INTERACTION') {
|
||||
step.status = 'WAITING_FOR_INTERACTION';
|
||||
}
|
||||
}
|
||||
|
||||
return of(execution);
|
||||
}
|
||||
|
||||
override prepareStringInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase {
|
|||
return this.http.put<TaskExecution>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/cancel`, null);
|
||||
}
|
||||
|
||||
override resumeTaskExecution(executionId: string): Observable<TaskExecution> {
|
||||
return this.http.put<TaskExecution>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/resume`, null);
|
||||
}
|
||||
|
||||
override prepareStringInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,22 @@ export class TaskExecutionsService {
|
|||
);
|
||||
}
|
||||
|
||||
resumeExecution(executionId: string) {
|
||||
const execution = this._taskExecutions().find((item) => item.id === executionId);
|
||||
const status = String(execution?.context.status ?? '').toUpperCase();
|
||||
if (status !== 'SUSPENDED') {
|
||||
return throwError(() => new Error('Resume is only supported for suspended executions.'));
|
||||
}
|
||||
|
||||
return this.taskExecutionsCallService.resumeTaskExecution(executionId).pipe(
|
||||
tap(() => this.refresh()),
|
||||
catchError((err) => {
|
||||
console.error('Resume 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()),
|
||||
|
|
|
|||
|
|
@ -366,6 +366,7 @@ export class TaskStepNodeComponent {
|
|||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (this.interactionSubmitting) return;
|
||||
if (this.executionStatus() === 'SUSPENDED') return;
|
||||
|
||||
const executionId = this.executionId();
|
||||
const executionNodeId = this.executionNodeId();
|
||||
|
|
@ -418,6 +419,11 @@ export class TaskStepNodeComponent {
|
|||
return typeof value === 'string' && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
private executionStatus(): string {
|
||||
const value = this.blockConfiguration?.['__executionStatus'];
|
||||
return typeof value === 'string' ? value.toUpperCase() : '';
|
||||
}
|
||||
|
||||
private getExecutionMessages(key: '__executionErrors' | '__executionWarnings'): string[] {
|
||||
const values = this.blockConfiguration?.[key];
|
||||
if (!Array.isArray(values)) return [];
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@
|
|||
<p class="text-sm text-slate-500">Execution ID: {{ execution()!.id }}</p>
|
||||
</div>
|
||||
<div class="flex items-stretch gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="self-stretch h-full rounded-md border border-violet-300 bg-violet-50 px-3 text-violet-700 hover:bg-violet-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
matTooltip="Resume execution"
|
||||
aria-label="Resume execution"
|
||||
[disabled]="!canResumeExecution()"
|
||||
(click)="resumeExecution()">
|
||||
Resume
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="self-stretch h-full w-10 rounded-md border border-rose-300 bg-rose-50 text-rose-600 hover:bg-rose-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
|
|
@ -58,6 +67,11 @@
|
|||
Execution cancelled. This execution is closed. Start a new execution to continue.
|
||||
</div>
|
||||
}
|
||||
@if (execution()!.context.status === 'SUSPENDED') {
|
||||
<div class="mx-1 mt-1 rounded-md border border-violet-200 bg-violet-50 px-4 py-3 text-sm text-violet-900">
|
||||
Execution suspended after service restart. Resume the execution to continue.
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-1 min-h-0 flex gap-2 p-1">
|
||||
<div class="flex-1 border border-slate-200 rounded-md bg-slate-50 min-h-0 overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
readonly activeAsideTab = signal<'inputs' | 'output'>('inputs');
|
||||
readonly startInProgress = signal(false);
|
||||
readonly cancelInProgress = signal(false);
|
||||
readonly resumeInProgress = signal(false);
|
||||
readonly savingInputs = signal<Record<string, boolean>>({});
|
||||
readonly savingErrors = signal<Record<string, string>>({});
|
||||
readonly pendingTextInputs = signal<Record<string, string | string[]>>({});
|
||||
|
|
@ -108,7 +109,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
if (dialogState.executionId !== execution.id || !dialogState.nodeId) return;
|
||||
|
||||
const executionStatus = String(execution.context.status ?? '').toUpperCase();
|
||||
if (executionStatus === 'CANCELLED') {
|
||||
if (executionStatus === 'CANCELLED' || executionStatus === 'SUSPENDED') {
|
||||
this.humanInteractionDialog.close(null);
|
||||
return;
|
||||
}
|
||||
|
|
@ -212,6 +213,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
...(stepNode.specificConfiguration ?? {}),
|
||||
__executionId: this.execution()?.id ?? null,
|
||||
__executionNodeId: step.id,
|
||||
__executionStatus: this.execution()?.context.status ?? null,
|
||||
__stepStatus: step.status,
|
||||
__executionStatusGroup: executionStatusGroup,
|
||||
__isWaitingStep: waitingSteps.includes(step.id),
|
||||
|
|
@ -287,6 +289,11 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
return !this.cancelInProgress() && (status === 'RUNNING' || status === 'WAITING');
|
||||
});
|
||||
|
||||
readonly canResumeExecution = computed(() => {
|
||||
const status = String(this.execution()?.context.status ?? '').toUpperCase();
|
||||
return !this.resumeInProgress() && status === 'SUSPENDED';
|
||||
});
|
||||
|
||||
readonly canStartExecution = computed(() => {
|
||||
const execution = this.execution();
|
||||
if (!execution) return false;
|
||||
|
|
@ -400,6 +407,17 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
resumeExecution() {
|
||||
const executionId = this.execution()?.id;
|
||||
if (!executionId || !this.canResumeExecution()) return;
|
||||
|
||||
this.resumeInProgress.set(true);
|
||||
this.taskExecutionsService.resumeExecution(executionId).subscribe({
|
||||
next: () => this.resumeInProgress.set(false),
|
||||
error: () => this.resumeInProgress.set(false)
|
||||
});
|
||||
}
|
||||
|
||||
onTextInputChange(input: EditableExecutionInput, value: string | string[]) {
|
||||
if (this.inputsReadOnly()) return;
|
||||
const executionId = this.execution()?.id;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<mat-button-toggle value="all">All</mat-button-toggle>
|
||||
<mat-button-toggle value="INIT">Init</mat-button-toggle>
|
||||
<mat-button-toggle value="RUNNING">Running</mat-button-toggle>
|
||||
<mat-button-toggle value="PAUSED">Paused</mat-button-toggle>
|
||||
<mat-button-toggle value="FINAL">Final</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export class TasksExecutionsListComponent {
|
|||
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 === 'CANCELLED') return 'bg-slate-200 text-slate-700 border-slate-300';
|
||||
if (normalized === 'SUSPENDED') return 'bg-violet-100 text-violet-700 border-violet-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';
|
||||
|
|
|
|||
Loading…
Reference in New Issue