[WIP] Workflows

This commit is contained in:
2026-02-27 16:34:17 -06:00
parent 570c52e623
commit daeff10f18
96 changed files with 5889 additions and 2098 deletions

View File

@@ -6,57 +6,64 @@
* Response DTO for action information
*/
export type ActionResponse = {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
*/
runtime_version_constraint?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow definition ID (non-null if this action is a workflow)
*/
workflow_def?: number | null;
};

View File

@@ -6,41 +6,48 @@
* Simplified action response (for list endpoints)
*/
export type ActionSummary = {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Human-readable label
*/
label: string;
/**
* Pack reference
*/
pack_ref: string;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Human-readable label
*/
label: string;
/**
* Pack reference
*/
pack_ref: string;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Semver version constraint for the runtime
*/
runtime_version_constraint?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow definition ID (non-null if this action is a workflow)
*/
workflow_def?: number | null;
};

View File

@@ -6,66 +6,73 @@
* Standard API response wrapper
*/
export type ApiResponse_ActionResponse = {
/**
* Response DTO for action information
*/
data: {
/**
* Response DTO for action information
* Creation timestamp
*/
data: {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Last update timestamp
*/
updated: string;
};
created: string;
/**
* Optional message
* Action description
*/
message?: string | null;
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
*/
runtime_version_constraint?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow definition ID (non-null if this action is a workflow)
*/
workflow_def?: number | null;
};
/**
* Optional message
*/
message?: string | null;
};

View File

@@ -38,6 +38,10 @@ export type ApiResponse_EnforcementResponse = {
* Enforcement payload
*/
payload: Record<string, any>;
/**
* Timestamp when the enforcement was resolved (status changed from created to processed/disabled)
*/
resolved_at?: string | null;
rule?: (null | i64);
/**
* Rule reference
@@ -51,10 +55,6 @@ export type ApiResponse_EnforcementResponse = {
* Trigger reference
*/
trigger_ref: string;
/**
* Last update timestamp
*/
updated: string;
};
/**
* Optional message

View File

@@ -42,10 +42,6 @@ export type ApiResponse_EventResponse = {
* Trigger reference
*/
trigger_ref: string;
/**
* Last update timestamp
*/
updated: string;
};
/**
* Optional message

View File

@@ -2,63 +2,79 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ExecutionStatus } from './ExecutionStatus';
import type { ExecutionStatus } from "./ExecutionStatus";
/**
* Standard API response wrapper
*/
export type ApiResponse_ExecutionResponse = {
/**
* Response DTO for execution information
*/
data: {
/**
* Response DTO for execution information
* Action ID (optional, may be null for ad-hoc executions)
*/
data: {
/**
* Action ID (optional, may be null for ad-hoc executions)
*/
action?: number | null;
/**
* Action reference
*/
action_ref: string;
/**
* Execution configuration/parameters
*/
config: Record<string, any>;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID (rule enforcement that triggered this)
*/
enforcement?: number | null;
/**
* Executor ID (worker/executor that ran this)
*/
executor?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID (for nested/child executions)
*/
parent?: number | null;
/**
* Execution result/output
*/
result: Record<string, any>;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Last update timestamp
*/
updated: string;
};
action?: number | null;
/**
* Optional message
* Action reference
*/
message?: string | null;
action_ref: string;
/**
* Execution configuration/parameters
*/
config: Record<string, any>;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID (rule enforcement that triggered this)
*/
enforcement?: number | null;
/**
* Executor ID (worker/executor that ran this)
*/
executor?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID (for nested/child executions)
*/
parent?: number | null;
/**
* Execution result/output
*/
result: Record<string, any>;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow task metadata (only populated for workflow task executions)
*/
workflow_task?: {
workflow_execution: number;
task_name: string;
task_index?: number | null;
task_batch?: number | null;
retry_count: number;
max_retries: number;
next_retry_at?: string | null;
timeout_seconds?: number | null;
timed_out: boolean;
duration_ms?: number | null;
started_at?: string | null;
completed_at?: string | null;
} | null;
};
/**
* Optional message
*/
message?: string | null;
};

View File

@@ -22,6 +22,10 @@ export type ApiResponse_PackResponse = {
* Creation timestamp
*/
created: string;
/**
* Pack dependencies (refs of required packs)
*/
dependencies: Array<string>;
/**
* Pack description
*/
@@ -47,7 +51,7 @@ export type ApiResponse_PackResponse = {
*/
ref: string;
/**
* Runtime dependencies
* Runtime dependencies (e.g., shell, python, nodejs)
*/
runtime_deps: Array<string>;
/**

View File

@@ -11,9 +11,9 @@ export type ApiResponse_RuleResponse = {
*/
data: {
/**
* Action ID
* Action ID (null if the referenced action has been deleted)
*/
action: number;
action?: number | null;
/**
* Parameters to pass to the action when rule is triggered
*/
@@ -63,9 +63,9 @@ export type ApiResponse_RuleResponse = {
*/
ref: string;
/**
* Trigger ID
* Trigger ID (null if the referenced trigger has been deleted)
*/
trigger: number;
trigger?: number | null;
/**
* Parameters for trigger configuration and event filtering
*/

View File

@@ -43,7 +43,7 @@ export type ApiResponse_SensorResponse = {
*/
pack_ref?: string | null;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**

View File

@@ -47,7 +47,7 @@ export type ApiResponse_TriggerResponse = {
*/
pack_ref?: string | null;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**

View File

@@ -47,7 +47,7 @@ export type ApiResponse_WorkflowResponse = {
*/
pack_ref: string;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**

View File

@@ -19,7 +19,7 @@ export type CreateActionRequest = {
*/
label: string;
/**
* Output schema (JSON Schema) defining expected outputs
* Output schema (flat format) defining expected outputs with inline required/secret
*/
out_schema?: any | null;
/**
@@ -27,7 +27,7 @@ export type CreateActionRequest = {
*/
pack_ref: string;
/**
* Parameter schema (JSON Schema) defining expected inputs
* Parameter schema (StackStorm-style) defining expected inputs with inline required/secret
*/
param_schema?: any | null;
/**
@@ -38,5 +38,9 @@ export type CreateActionRequest = {
* Optional runtime ID for this action
*/
runtime?: number | null;
/**
* Optional semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
*/
runtime_version_constraint?: string | null;
};

View File

@@ -17,7 +17,7 @@ export type CreateInquiryRequest = {
*/
prompt: string;
/**
* Optional JSON schema for the expected response format
* Optional schema for the expected response format (flat format with inline required/secret)
*/
response_schema: Record<string, any>;
/**

View File

@@ -7,13 +7,17 @@
*/
export type CreatePackRequest = {
/**
* Configuration schema (JSON Schema)
* Configuration schema (flat format with inline required/secret per parameter)
*/
conf_schema?: Record<string, any>;
/**
* Pack configuration values
*/
config?: Record<string, any>;
/**
* Pack dependencies (refs of required packs)
*/
dependencies?: Array<string>;
/**
* Pack description
*/
@@ -35,7 +39,7 @@ export type CreatePackRequest = {
*/
ref: string;
/**
* Runtime dependencies (refs of required packs)
* Runtime dependencies (e.g., shell, python, nodejs)
*/
runtime_deps?: Array<string>;
/**

View File

@@ -31,7 +31,7 @@ export type CreateSensorRequest = {
*/
pack_ref: string;
/**
* Parameter schema (JSON Schema) for sensor configuration
* Parameter schema (flat format) for sensor configuration
*/
param_schema?: any | null;
/**

View File

@@ -19,7 +19,7 @@ export type CreateTriggerRequest = {
*/
label: string;
/**
* Output schema (JSON Schema) defining event data structure
* Output schema (flat format) defining event data structure with inline required/secret
*/
out_schema?: any | null;
/**
@@ -27,7 +27,7 @@ export type CreateTriggerRequest = {
*/
pack_ref?: string | null;
/**
* Parameter schema (JSON Schema) defining event payload structure
* Parameter schema (StackStorm-style) defining trigger configuration with inline required/secret
*/
param_schema?: any | null;
/**

View File

@@ -23,7 +23,7 @@ export type CreateWorkflowRequest = {
*/
label: string;
/**
* Output schema (JSON Schema) defining expected outputs
* Output schema (flat format) defining expected outputs with inline required/secret
*/
out_schema: Record<string, any>;
/**
@@ -31,7 +31,7 @@ export type CreateWorkflowRequest = {
*/
pack_ref: string;
/**
* Parameter schema (JSON Schema) defining expected inputs
* Parameter schema (StackStorm-style) defining expected inputs with inline required/secret
*/
param_schema: Record<string, any>;
/**

View File

@@ -34,6 +34,10 @@ export type EnforcementResponse = {
* Enforcement payload
*/
payload: Record<string, any>;
/**
* Timestamp when the enforcement was resolved (status changed from created to processed/disabled)
*/
resolved_at?: string | null;
rule?: (null | i64);
/**
* Rule reference
@@ -47,9 +51,5 @@ export type EnforcementResponse = {
* Trigger reference
*/
trigger_ref: string;
/**
* Last update timestamp
*/
updated: string;
};

View File

@@ -38,9 +38,5 @@ export type EventResponse = {
* Trigger reference
*/
trigger_ref: string;
/**
* Last update timestamp
*/
updated: string;
};

View File

@@ -2,54 +2,70 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ExecutionStatus } from './ExecutionStatus';
import type { ExecutionStatus } from "./ExecutionStatus";
/**
* Response DTO for execution information
*/
export type ExecutionResponse = {
/**
* Action ID (optional, may be null for ad-hoc executions)
*/
action?: number | null;
/**
* Action reference
*/
action_ref: string;
/**
* Execution configuration/parameters
*/
config: Record<string, any>;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID (rule enforcement that triggered this)
*/
enforcement?: number | null;
/**
* Executor ID (worker/executor that ran this)
*/
executor?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID (for nested/child executions)
*/
parent?: number | null;
/**
* Execution result/output
*/
result: Record<string, any>;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Last update timestamp
*/
updated: string;
/**
* Action ID (optional, may be null for ad-hoc executions)
*/
action?: number | null;
/**
* Action reference
*/
action_ref: string;
/**
* Execution configuration/parameters
*/
config: Record<string, any>;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID (rule enforcement that triggered this)
*/
enforcement?: number | null;
/**
* Executor ID (worker/executor that ran this)
*/
executor?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID (for nested/child executions)
*/
parent?: number | null;
/**
* Execution result/output
*/
result: Record<string, any>;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow task metadata (only populated for workflow task executions)
*/
workflow_task?: {
workflow_execution: number;
task_name: string;
task_index?: number | null;
task_batch?: number | null;
retry_count: number;
max_retries: number;
next_retry_at?: string | null;
timeout_seconds?: number | null;
timed_out: boolean;
duration_ms?: number | null;
started_at?: string | null;
completed_at?: string | null;
} | null;
};

View File

@@ -2,46 +2,62 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ExecutionStatus } from './ExecutionStatus';
import type { ExecutionStatus } from "./ExecutionStatus";
/**
* Simplified execution response (for list endpoints)
*/
export type ExecutionSummary = {
/**
* Action reference
*/
action_ref: string;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID
*/
enforcement?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID
*/
parent?: number | null;
/**
* Rule reference (if triggered by a rule)
*/
rule_ref?: string | null;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Trigger reference (if triggered by a trigger)
*/
trigger_ref?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Action reference
*/
action_ref: string;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID
*/
enforcement?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID
*/
parent?: number | null;
/**
* Rule reference (if triggered by a rule)
*/
rule_ref?: string | null;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Trigger reference (if triggered by a trigger)
*/
trigger_ref?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow task metadata (only populated for workflow task executions)
*/
workflow_task?: {
workflow_execution: number;
task_name: string;
task_index?: number | null;
task_batch?: number | null;
retry_count: number;
max_retries: number;
next_retry_at?: string | null;
timeout_seconds?: number | null;
timed_out: boolean;
duration_ms?: number | null;
started_at?: string | null;
completed_at?: string | null;
} | null;
};

View File

@@ -6,20 +6,21 @@
* Request DTO for installing a pack from remote source
*/
export type InstallPackRequest = {
/**
* Git branch, tag, or commit reference
*/
ref_spec?: string | null;
/**
* Skip dependency validation (not recommended)
*/
skip_deps?: boolean;
/**
* Skip running pack tests during installation
*/
skip_tests?: boolean;
/**
* Repository URL or source location
*/
source: string;
/**
* Git branch, tag, or commit reference
*/
ref_spec?: string | null;
/**
* Skip dependency validation (not recommended)
*/
skip_deps?: boolean;
/**
* Skip running pack tests during installation
*/
skip_tests?: boolean;
/**
* Repository URL or source location
*/
source: string;
};

View File

@@ -18,6 +18,10 @@ export type PackResponse = {
* Creation timestamp
*/
created: string;
/**
* Pack dependencies (refs of required packs)
*/
dependencies: Array<string>;
/**
* Pack description
*/
@@ -43,7 +47,7 @@ export type PackResponse = {
*/
ref: string;
/**
* Runtime dependencies
* Runtime dependencies (e.g., shell, python, nodejs)
*/
runtime_deps: Array<string>;
/**

View File

@@ -2,55 +2,62 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PaginationMeta } from './PaginationMeta';
import type { PaginationMeta } from "./PaginationMeta";
/**
* Paginated response wrapper
*/
export type PaginatedResponse_ActionSummary = {
/**
* The data items
*/
data: Array<{
/**
* The data items
* Creation timestamp
*/
data: Array<{
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Human-readable label
*/
label: string;
/**
* Pack reference
*/
pack_ref: string;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Last update timestamp
*/
updated: string;
}>;
created: string;
/**
* Pagination metadata
* Action description
*/
pagination: PaginationMeta;
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Human-readable label
*/
label: string;
/**
* Pack reference
*/
pack_ref: string;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Semver version constraint for the runtime
*/
runtime_version_constraint?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow definition ID (non-null if this action is a workflow)
*/
workflow_def?: number | null;
}>;
/**
* Pagination metadata
*/
pagination: PaginationMeta;
};

View File

@@ -2,56 +2,72 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ExecutionStatus } from './ExecutionStatus';
import type { PaginationMeta } from './PaginationMeta';
import type { ExecutionStatus } from "./ExecutionStatus";
import type { PaginationMeta } from "./PaginationMeta";
/**
* Paginated response wrapper
*/
export type PaginatedResponse_ExecutionSummary = {
/**
* The data items
*/
data: Array<{
/**
* The data items
* Action reference
*/
data: Array<{
/**
* Action reference
*/
action_ref: string;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID
*/
enforcement?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID
*/
parent?: number | null;
/**
* Rule reference (if triggered by a rule)
*/
rule_ref?: string | null;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Trigger reference (if triggered by a trigger)
*/
trigger_ref?: string | null;
/**
* Last update timestamp
*/
updated: string;
}>;
action_ref: string;
/**
* Pagination metadata
* Creation timestamp
*/
pagination: PaginationMeta;
created: string;
/**
* Enforcement ID
*/
enforcement?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID
*/
parent?: number | null;
/**
* Rule reference (if triggered by a rule)
*/
rule_ref?: string | null;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Trigger reference (if triggered by a trigger)
*/
trigger_ref?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow task metadata (only populated for workflow task executions)
*/
workflow_task?: {
workflow_execution: number;
task_name: string;
task_index?: number | null;
task_batch?: number | null;
retry_count: number;
max_retries: number;
next_retry_at?: string | null;
timeout_seconds?: number | null;
timed_out: boolean;
duration_ms?: number | null;
started_at?: string | null;
completed_at?: string | null;
} | null;
}>;
/**
* Pagination metadata
*/
pagination: PaginationMeta;
};

View File

@@ -7,9 +7,9 @@
*/
export type RuleResponse = {
/**
* Action ID
* Action ID (null if the referenced action has been deleted)
*/
action: number;
action?: number | null;
/**
* Parameters to pass to the action when rule is triggered
*/
@@ -59,9 +59,9 @@ export type RuleResponse = {
*/
ref: string;
/**
* Trigger ID
* Trigger ID (null if the referenced trigger has been deleted)
*/
trigger: number;
trigger?: number | null;
/**
* Parameters for trigger configuration and event filtering
*/

View File

@@ -39,7 +39,7 @@ export type SensorResponse = {
*/
pack_ref?: string | null;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**

View File

@@ -43,7 +43,7 @@ export type TriggerResponse = {
*/
pack_ref?: string | null;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**

View File

@@ -23,12 +23,16 @@ export type UpdateActionRequest = {
*/
out_schema: any | null;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Optional semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
*/
runtime_version_constraint?: string | null;
};

View File

@@ -14,6 +14,10 @@ export type UpdatePackRequest = {
* Pack configuration values
*/
config: any | null;
/**
* Pack dependencies (refs of required packs)
*/
dependencies?: any[] | null;
/**
* Pack description
*/
@@ -31,7 +35,7 @@ export type UpdatePackRequest = {
*/
meta: any | null;
/**
* Runtime dependencies
* Runtime dependencies (e.g., shell, python, nodejs)
*/
runtime_deps?: any[] | null;
/**

View File

@@ -23,7 +23,7 @@ export type UpdateSensorRequest = {
*/
label?: string | null;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
};

View File

@@ -23,7 +23,7 @@ export type UpdateTriggerRequest = {
*/
out_schema: any | null;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
};

View File

@@ -27,7 +27,7 @@ export type UpdateWorkflowRequest = {
*/
out_schema: any | null;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**

View File

@@ -43,7 +43,7 @@ export type WorkflowResponse = {
*/
pack_ref: string;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**

View File

@@ -2,432 +2,456 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CreateActionRequest } from '../models/CreateActionRequest';
import type { PaginatedResponse_ActionSummary } from '../models/PaginatedResponse_ActionSummary';
import type { SuccessResponse } from '../models/SuccessResponse';
import type { UpdateActionRequest } from '../models/UpdateActionRequest';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
import type { CreateActionRequest } from "../models/CreateActionRequest";
import type { PaginatedResponse_ActionSummary } from "../models/PaginatedResponse_ActionSummary";
import type { SuccessResponse } from "../models/SuccessResponse";
import type { UpdateActionRequest } from "../models/UpdateActionRequest";
import type { CancelablePromise } from "../core/CancelablePromise";
import { OpenAPI } from "../core/OpenAPI";
import { request as __request } from "../core/request";
export class ActionsService {
/**
* List all actions with pagination
* @returns PaginatedResponse_ActionSummary List of actions
* @throws ApiError
*/
public static listActions({
page,
pageSize,
}: {
/**
* List all actions with pagination
* @returns PaginatedResponse_ActionSummary List of actions
* @throws ApiError
* Page number (1-based)
*/
public static listActions({
page,
pageSize,
}: {
/**
* Page number (1-based)
*/
page?: number,
/**
* Number of items per page
*/
pageSize?: number,
}): CancelablePromise<PaginatedResponse_ActionSummary> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/actions',
query: {
'page': page,
'page_size': pageSize,
},
});
}
page?: number;
/**
* Create a new action
* @returns any Action created successfully
* @throws ApiError
* Number of items per page
*/
public static createAction({
requestBody,
}: {
requestBody: CreateActionRequest,
}): CancelablePromise<{
/**
* Response DTO for action information
*/
data: {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Last update timestamp
*/
updated: string;
};
/**
* Optional message
*/
message?: string | null;
}> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/actions',
body: requestBody,
mediaType: 'application/json',
errors: {
400: `Validation error`,
404: `Pack not found`,
409: `Action with same ref already exists`,
},
});
}
pageSize?: number;
}): CancelablePromise<PaginatedResponse_ActionSummary> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/actions",
query: {
page: page,
page_size: pageSize,
},
});
}
/**
* Create a new action
* @returns any Action created successfully
* @throws ApiError
*/
public static createAction({
requestBody,
}: {
requestBody: CreateActionRequest;
}): CancelablePromise<{
/**
* Get a single action by reference
* @returns any Action details
* @throws ApiError
* Response DTO for action information
*/
public static getAction({
ref,
}: {
/**
* Action reference identifier
*/
ref: string,
}): CancelablePromise<{
/**
* Response DTO for action information
*/
data: {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Last update timestamp
*/
updated: string;
};
/**
* Optional message
*/
message?: string | null;
}> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/actions/{ref}',
path: {
'ref': ref,
},
errors: {
404: `Action not found`,
},
});
}
data: {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
*/
runtime_version_constraint?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow definition ID (non-null if this action is a workflow)
*/
workflow_def?: number | null;
};
/**
* Update an existing action
* @returns any Action updated successfully
* @throws ApiError
* Optional message
*/
public static updateAction({
ref,
requestBody,
}: {
/**
* Action reference identifier
*/
ref: string,
requestBody: UpdateActionRequest,
}): CancelablePromise<{
/**
* Response DTO for action information
*/
data: {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Last update timestamp
*/
updated: string;
};
/**
* Optional message
*/
message?: string | null;
}> {
return __request(OpenAPI, {
method: 'PUT',
url: '/api/v1/actions/{ref}',
path: {
'ref': ref,
},
body: requestBody,
mediaType: 'application/json',
errors: {
400: `Validation error`,
404: `Action not found`,
},
});
}
message?: string | null;
}> {
return __request(OpenAPI, {
method: "POST",
url: "/api/v1/actions",
body: requestBody,
mediaType: "application/json",
errors: {
400: `Validation error`,
404: `Pack not found`,
409: `Action with same ref already exists`,
},
});
}
/**
* Get a single action by reference
* @returns any Action details
* @throws ApiError
*/
public static getAction({
ref,
}: {
/**
* Delete an action
* @returns SuccessResponse Action deleted successfully
* @throws ApiError
* Action reference identifier
*/
public static deleteAction({
ref,
}: {
/**
* Action reference identifier
*/
ref: string,
}): CancelablePromise<SuccessResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/api/v1/actions/{ref}',
path: {
'ref': ref,
},
errors: {
404: `Action not found`,
},
});
}
ref: string;
}): CancelablePromise<{
/**
* Get queue statistics for an action
* @returns any Queue statistics
* @throws ApiError
* Response DTO for action information
*/
public static getQueueStats({
ref,
}: {
/**
* Action reference identifier
*/
ref: string,
}): CancelablePromise<{
/**
* Response DTO for queue statistics
*/
data: {
/**
* Action ID
*/
action_id: number;
/**
* Action reference
*/
action_ref: string;
/**
* Number of currently running executions
*/
active_count: number;
/**
* Timestamp of last statistics update
*/
last_updated: string;
/**
* Maximum concurrent executions allowed
*/
max_concurrent: number;
/**
* Timestamp of oldest queued execution (if any)
*/
oldest_enqueued_at?: string | null;
/**
* Number of executions waiting in queue
*/
queue_length: number;
/**
* Total executions completed since queue creation
*/
total_completed: number;
/**
* Total executions enqueued since queue creation
*/
total_enqueued: number;
};
/**
* Optional message
*/
message?: string | null;
}> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/actions/{ref}/queue-stats',
path: {
'ref': ref,
},
errors: {
404: `Action not found or no queue statistics available`,
},
});
}
data: {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
*/
runtime_version_constraint?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow definition ID (non-null if this action is a workflow)
*/
workflow_def?: number | null;
};
/**
* List actions by pack reference
* @returns PaginatedResponse_ActionSummary List of actions for pack
* @throws ApiError
* Optional message
*/
public static listActionsByPack({
packRef,
page,
pageSize,
}: {
/**
* Pack reference identifier
*/
packRef: string,
/**
* Page number (1-based)
*/
page?: number,
/**
* Number of items per page
*/
pageSize?: number,
}): CancelablePromise<PaginatedResponse_ActionSummary> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/packs/{pack_ref}/actions',
path: {
'pack_ref': packRef,
},
query: {
'page': page,
'page_size': pageSize,
},
errors: {
404: `Pack not found`,
},
});
}
message?: string | null;
}> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/actions/{ref}",
path: {
ref: ref,
},
errors: {
404: `Action not found`,
},
});
}
/**
* Update an existing action
* @returns any Action updated successfully
* @throws ApiError
*/
public static updateAction({
ref,
requestBody,
}: {
/**
* Action reference identifier
*/
ref: string;
requestBody: UpdateActionRequest;
}): CancelablePromise<{
/**
* Response DTO for action information
*/
data: {
/**
* Creation timestamp
*/
created: string;
/**
* Action description
*/
description: string;
/**
* Entry point
*/
entrypoint: string;
/**
* Action ID
*/
id: number;
/**
* Whether this is an ad-hoc action (not from pack installation)
*/
is_adhoc: boolean;
/**
* Human-readable label
*/
label: string;
/**
* Output schema
*/
out_schema: any | null;
/**
* Pack ID
*/
pack: number;
/**
* Pack reference
*/
pack_ref: string;
/**
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**
* Unique reference identifier
*/
ref: string;
/**
* Runtime ID
*/
runtime?: number | null;
/**
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
*/
runtime_version_constraint?: string | null;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow definition ID (non-null if this action is a workflow)
*/
workflow_def?: number | null;
};
/**
* Optional message
*/
message?: string | null;
}> {
return __request(OpenAPI, {
method: "PUT",
url: "/api/v1/actions/{ref}",
path: {
ref: ref,
},
body: requestBody,
mediaType: "application/json",
errors: {
400: `Validation error`,
404: `Action not found`,
},
});
}
/**
* Delete an action
* @returns SuccessResponse Action deleted successfully
* @throws ApiError
*/
public static deleteAction({
ref,
}: {
/**
* Action reference identifier
*/
ref: string;
}): CancelablePromise<SuccessResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/api/v1/actions/{ref}",
path: {
ref: ref,
},
errors: {
404: `Action not found`,
},
});
}
/**
* Get queue statistics for an action
* @returns any Queue statistics
* @throws ApiError
*/
public static getQueueStats({
ref,
}: {
/**
* Action reference identifier
*/
ref: string;
}): CancelablePromise<{
/**
* Response DTO for queue statistics
*/
data: {
/**
* Action ID
*/
action_id: number;
/**
* Action reference
*/
action_ref: string;
/**
* Number of currently running executions
*/
active_count: number;
/**
* Timestamp of last statistics update
*/
last_updated: string;
/**
* Maximum concurrent executions allowed
*/
max_concurrent: number;
/**
* Timestamp of oldest queued execution (if any)
*/
oldest_enqueued_at?: string | null;
/**
* Number of executions waiting in queue
*/
queue_length: number;
/**
* Total executions completed since queue creation
*/
total_completed: number;
/**
* Total executions enqueued since queue creation
*/
total_enqueued: number;
};
/**
* Optional message
*/
message?: string | null;
}> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/actions/{ref}/queue-stats",
path: {
ref: ref,
},
errors: {
404: `Action not found or no queue statistics available`,
},
});
}
/**
* List actions by pack reference
* @returns PaginatedResponse_ActionSummary List of actions for pack
* @throws ApiError
*/
public static listActionsByPack({
packRef,
page,
pageSize,
}: {
/**
* Pack reference identifier
*/
packRef: string;
/**
* Page number (1-based)
*/
page?: number;
/**
* Number of items per page
*/
pageSize?: number;
}): CancelablePromise<PaginatedResponse_ActionSummary> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/packs/{pack_ref}/actions",
path: {
pack_ref: packRef,
},
query: {
page: page,
page_size: pageSize,
},
errors: {
404: `Pack not found`,
},
});
}
}

View File

@@ -2,92 +2,92 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiResponse_EventResponse } from "../models/ApiResponse_EventResponse";
import type { i64 } from "../models/i64";
import type { PaginatedResponse_EventSummary } from "../models/PaginatedResponse_EventSummary";
import type { CancelablePromise } from "../core/CancelablePromise";
import { OpenAPI } from "../core/OpenAPI";
import { request as __request } from "../core/request";
import type { ApiResponse_EventResponse } from '../models/ApiResponse_EventResponse';
import type { i64 } from '../models/i64';
import type { PaginatedResponse_EventSummary } from '../models/PaginatedResponse_EventSummary';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class EventsService {
/**
* List all events with pagination and optional filters
* @returns PaginatedResponse_EventSummary List of events
* @throws ApiError
*/
public static listEvents({
trigger,
triggerRef,
ruleRef,
source,
page,
perPage,
}: {
/**
* Filter by trigger ID
* List all events with pagination and optional filters
* @returns PaginatedResponse_EventSummary List of events
* @throws ApiError
*/
trigger?: null | i64;
public static listEvents({
trigger,
triggerRef,
ruleRef,
source,
page,
perPage,
}: {
/**
* Filter by trigger ID
*/
trigger?: (null | i64),
/**
* Filter by trigger reference
*/
triggerRef?: string | null,
/**
* Filter by rule reference
*/
ruleRef?: string | null,
/**
* Filter by source ID
*/
source?: (null | i64),
/**
* Page number (1-indexed)
*/
page?: number,
/**
* Items per page
*/
perPage?: number,
}): CancelablePromise<PaginatedResponse_EventSummary> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/events',
query: {
'trigger': trigger,
'trigger_ref': triggerRef,
'rule_ref': ruleRef,
'source': source,
'page': page,
'per_page': perPage,
},
errors: {
401: `Unauthorized`,
500: `Internal server error`,
},
});
}
/**
* Filter by trigger reference
* Get a single event by ID
* @returns ApiResponse_EventResponse Event details
* @throws ApiError
*/
triggerRef?: string | null;
/**
* Filter by rule reference
*/
ruleRef?: string | null;
/**
* Filter by source ID
*/
source?: null | i64;
/**
* Page number (1-indexed)
*/
page?: number;
/**
* Items per page
*/
perPage?: number;
}): CancelablePromise<PaginatedResponse_EventSummary> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/events",
query: {
trigger: trigger,
trigger_ref: triggerRef,
rule_ref: ruleRef,
source: source,
page: page,
per_page: perPage,
},
errors: {
401: `Unauthorized`,
500: `Internal server error`,
},
});
}
/**
* Get a single event by ID
* @returns ApiResponse_EventResponse Event details
* @throws ApiError
*/
public static getEvent({
id,
}: {
/**
* Event ID
*/
id: number;
}): CancelablePromise<ApiResponse_EventResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/events/{id}",
path: {
id: id,
},
errors: {
401: `Unauthorized`,
404: `Event not found`,
500: `Internal server error`,
},
});
}
public static getEvent({
id,
}: {
/**
* Event ID
*/
id: number,
}): CancelablePromise<ApiResponse_EventResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/events/{id}',
path: {
'id': id,
},
errors: {
401: `Unauthorized`,
404: `Event not found`,
500: `Internal server error`,
},
});
}
}

View File

@@ -2,260 +2,283 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ExecutionStatus } from '../models/ExecutionStatus';
import type { PaginatedResponse_ExecutionSummary } from '../models/PaginatedResponse_ExecutionSummary';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
import type { ExecutionStatus } from "../models/ExecutionStatus";
import type { PaginatedResponse_ExecutionSummary } from "../models/PaginatedResponse_ExecutionSummary";
import type { CancelablePromise } from "../core/CancelablePromise";
import { OpenAPI } from "../core/OpenAPI";
import { request as __request } from "../core/request";
export class ExecutionsService {
/**
* List all executions with pagination and optional filters
* @returns PaginatedResponse_ExecutionSummary List of executions
* @throws ApiError
*/
public static listExecutions({
status,
actionRef,
packName,
ruleRef,
triggerRef,
executor,
resultContains,
enforcement,
parent,
topLevelOnly,
page,
perPage,
}: {
/**
* List all executions with pagination and optional filters
* @returns PaginatedResponse_ExecutionSummary List of executions
* @throws ApiError
* Filter by execution status
*/
public static listExecutions({
status,
actionRef,
packName,
ruleRef,
triggerRef,
executor,
resultContains,
enforcement,
parent,
page,
perPage,
}: {
/**
* Filter by execution status
*/
status?: (null | ExecutionStatus),
/**
* Filter by action reference
*/
actionRef?: string | null,
/**
* Filter by pack name
*/
packName?: string | null,
/**
* Filter by rule reference
*/
ruleRef?: string | null,
/**
* Filter by trigger reference
*/
triggerRef?: string | null,
/**
* Filter by executor ID
*/
executor?: number | null,
/**
* Search in result JSON (case-insensitive substring match)
*/
resultContains?: string | null,
/**
* Filter by enforcement ID
*/
enforcement?: number | null,
/**
* Filter by parent execution ID
*/
parent?: number | null,
/**
* Page number (for pagination)
*/
page?: number,
/**
* Items per page (for pagination)
*/
perPage?: number,
}): CancelablePromise<PaginatedResponse_ExecutionSummary> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/executions',
query: {
'status': status,
'action_ref': actionRef,
'pack_name': packName,
'rule_ref': ruleRef,
'trigger_ref': triggerRef,
'executor': executor,
'result_contains': resultContains,
'enforcement': enforcement,
'parent': parent,
'page': page,
'per_page': perPage,
},
});
}
status?: null | ExecutionStatus;
/**
* List executions by enforcement ID
* @returns PaginatedResponse_ExecutionSummary List of executions for enforcement
* @throws ApiError
* Filter by action reference
*/
public static listExecutionsByEnforcement({
enforcementId,
page,
pageSize,
}: {
/**
* Enforcement ID
*/
enforcementId: number,
/**
* Page number (1-based)
*/
page?: number,
/**
* Number of items per page
*/
pageSize?: number,
}): CancelablePromise<PaginatedResponse_ExecutionSummary> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/executions/enforcement/{enforcement_id}',
path: {
'enforcement_id': enforcementId,
},
query: {
'page': page,
'page_size': pageSize,
},
errors: {
500: `Internal server error`,
},
});
}
actionRef?: string | null;
/**
* Get execution statistics
* @returns any Execution statistics
* @throws ApiError
* Filter by pack name
*/
public static getExecutionStats(): CancelablePromise<Record<string, any>> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/executions/stats',
errors: {
500: `Internal server error`,
},
});
}
packName?: string | null;
/**
* List executions by status
* @returns PaginatedResponse_ExecutionSummary List of executions with specified status
* @throws ApiError
* Filter by rule reference
*/
public static listExecutionsByStatus({
status,
page,
pageSize,
}: {
/**
* Execution status (requested, scheduling, scheduled, running, completed, failed, canceling, cancelled, timeout, abandoned)
*/
status: string,
/**
* Page number (1-based)
*/
page?: number,
/**
* Number of items per page
*/
pageSize?: number,
}): CancelablePromise<PaginatedResponse_ExecutionSummary> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/executions/status/{status}',
path: {
'status': status,
},
query: {
'page': page,
'page_size': pageSize,
},
errors: {
400: `Invalid status`,
500: `Internal server error`,
},
});
}
ruleRef?: string | null;
/**
* Get a single execution by ID
* @returns any Execution details
* @throws ApiError
* Filter by trigger reference
*/
public static getExecution({
id,
}: {
/**
* Execution ID
*/
id: number,
}): CancelablePromise<{
/**
* Response DTO for execution information
*/
data: {
/**
* Action ID (optional, may be null for ad-hoc executions)
*/
action?: number | null;
/**
* Action reference
*/
action_ref: string;
/**
* Execution configuration/parameters
*/
config: Record<string, any>;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID (rule enforcement that triggered this)
*/
enforcement?: number | null;
/**
* Executor ID (worker/executor that ran this)
*/
executor?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID (for nested/child executions)
*/
parent?: number | null;
/**
* Execution result/output
*/
result: Record<string, any>;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Last update timestamp
*/
updated: string;
};
/**
* Optional message
*/
message?: string | null;
}> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/executions/{id}',
path: {
'id': id,
},
errors: {
404: `Execution not found`,
},
});
}
triggerRef?: string | null;
/**
* Filter by executor ID
*/
executor?: number | null;
/**
* Search in result JSON (case-insensitive substring match)
*/
resultContains?: string | null;
/**
* Filter by enforcement ID
*/
enforcement?: number | null;
/**
* Filter by parent execution ID
*/
parent?: number | null;
/**
* If true, only return top-level executions (those without a parent)
*/
topLevelOnly?: boolean | null;
/**
* Page number (for pagination)
*/
page?: number;
/**
* Items per page (for pagination)
*/
perPage?: number;
}): CancelablePromise<PaginatedResponse_ExecutionSummary> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/executions",
query: {
status: status,
action_ref: actionRef,
pack_name: packName,
rule_ref: ruleRef,
trigger_ref: triggerRef,
executor: executor,
result_contains: resultContains,
enforcement: enforcement,
parent: parent,
top_level_only: topLevelOnly,
page: page,
per_page: perPage,
},
});
}
/**
* List executions by enforcement ID
* @returns PaginatedResponse_ExecutionSummary List of executions for enforcement
* @throws ApiError
*/
public static listExecutionsByEnforcement({
enforcementId,
page,
pageSize,
}: {
/**
* Enforcement ID
*/
enforcementId: number;
/**
* Page number (1-based)
*/
page?: number;
/**
* Number of items per page
*/
pageSize?: number;
}): CancelablePromise<PaginatedResponse_ExecutionSummary> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/executions/enforcement/{enforcement_id}",
path: {
enforcement_id: enforcementId,
},
query: {
page: page,
page_size: pageSize,
},
errors: {
500: `Internal server error`,
},
});
}
/**
* Get execution statistics
* @returns any Execution statistics
* @throws ApiError
*/
public static getExecutionStats(): CancelablePromise<Record<string, any>> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/executions/stats",
errors: {
500: `Internal server error`,
},
});
}
/**
* List executions by status
* @returns PaginatedResponse_ExecutionSummary List of executions with specified status
* @throws ApiError
*/
public static listExecutionsByStatus({
status,
page,
pageSize,
}: {
/**
* Execution status (requested, scheduling, scheduled, running, completed, failed, canceling, cancelled, timeout, abandoned)
*/
status: string;
/**
* Page number (1-based)
*/
page?: number;
/**
* Number of items per page
*/
pageSize?: number;
}): CancelablePromise<PaginatedResponse_ExecutionSummary> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/executions/status/{status}",
path: {
status: status,
},
query: {
page: page,
page_size: pageSize,
},
errors: {
400: `Invalid status`,
500: `Internal server error`,
},
});
}
/**
* Get a single execution by ID
* @returns any Execution details
* @throws ApiError
*/
public static getExecution({
id,
}: {
/**
* Execution ID
*/
id: number;
}): CancelablePromise<{
/**
* Response DTO for execution information
*/
data: {
/**
* Action ID (optional, may be null for ad-hoc executions)
*/
action?: number | null;
/**
* Action reference
*/
action_ref: string;
/**
* Execution configuration/parameters
*/
config: Record<string, any>;
/**
* Creation timestamp
*/
created: string;
/**
* Enforcement ID (rule enforcement that triggered this)
*/
enforcement?: number | null;
/**
* Executor ID (worker/executor that ran this)
*/
executor?: number | null;
/**
* Execution ID
*/
id: number;
/**
* Parent execution ID (for nested/child executions)
*/
parent?: number | null;
/**
* Execution result/output
*/
result: Record<string, any>;
/**
* Execution status
*/
status: ExecutionStatus;
/**
* Last update timestamp
*/
updated: string;
/**
* Workflow task metadata (only populated for workflow task executions)
*/
workflow_task?: {
workflow_execution: number;
task_name: string;
task_index?: number | null;
task_batch?: number | null;
retry_count: number;
max_retries: number;
next_retry_at?: string | null;
timeout_seconds?: number | null;
timed_out: boolean;
duration_ms?: number | null;
started_at?: string | null;
completed_at?: string | null;
} | null;
};
/**
* Optional message
*/
message?: string | null;
}> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/executions/{id}",
path: {
id: id,
},
errors: {
404: `Execution not found`,
},
});
}
}

View File

@@ -71,6 +71,10 @@ export class PacksService {
* Creation timestamp
*/
created: string;
/**
* Pack dependencies (refs of required packs)
*/
dependencies: Array<string>;
/**
* Pack description
*/
@@ -96,7 +100,7 @@ export class PacksService {
*/
ref: string;
/**
* Runtime dependencies
* Runtime dependencies (e.g., shell, python, nodejs)
*/
runtime_deps: Array<string>;
/**
@@ -145,7 +149,6 @@ export class PacksService {
mediaType: 'application/json',
errors: {
400: `Invalid request or tests failed`,
409: `Pack already exists`,
501: `Not implemented yet`,
},
});
@@ -200,6 +203,10 @@ export class PacksService {
* Creation timestamp
*/
created: string;
/**
* Pack dependencies (refs of required packs)
*/
dependencies: Array<string>;
/**
* Pack description
*/
@@ -225,7 +232,7 @@ export class PacksService {
*/
ref: string;
/**
* Runtime dependencies
* Runtime dependencies (e.g., shell, python, nodejs)
*/
runtime_deps: Array<string>;
/**
@@ -288,6 +295,10 @@ export class PacksService {
* Creation timestamp
*/
created: string;
/**
* Pack dependencies (refs of required packs)
*/
dependencies: Array<string>;
/**
* Pack description
*/
@@ -313,7 +324,7 @@ export class PacksService {
*/
ref: string;
/**
* Runtime dependencies
* Runtime dependencies (e.g., shell, python, nodejs)
*/
runtime_deps: Array<string>;
/**

View File

@@ -150,7 +150,7 @@ export class WorkflowsService {
*/
pack_ref: string;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**
@@ -241,7 +241,7 @@ export class WorkflowsService {
*/
pack_ref: string;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**
@@ -333,7 +333,7 @@ export class WorkflowsService {
*/
pack_ref: string;
/**
* Parameter schema
* Parameter schema (StackStorm-style with inline required/secret)
*/
param_schema: any | null;
/**

View File

@@ -0,0 +1,312 @@
import { useState, useMemo } from "react";
import { Link } from "react-router-dom";
import { formatDistanceToNow } from "date-fns";
import {
ChevronDown,
ChevronRight,
Workflow,
CheckCircle2,
XCircle,
Clock,
Loader2,
AlertTriangle,
Ban,
CircleDot,
RotateCcw,
} from "lucide-react";
import { useChildExecutions } from "@/hooks/useExecutions";
interface WorkflowTasksPanelProps {
/** The parent (workflow) execution ID */
parentExecutionId: number;
/** Whether the panel starts collapsed (default: false — open by default for workflows) */
defaultCollapsed?: boolean;
}
/** Format a duration in ms to a human-readable string. */
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
const secs = ms / 1000;
if (secs < 60) return `${secs.toFixed(1)}s`;
const mins = Math.floor(secs / 60);
const remainSecs = Math.round(secs % 60);
if (mins < 60) return `${mins}m ${remainSecs}s`;
const hrs = Math.floor(mins / 60);
const remainMins = mins % 60;
return `${hrs}h ${remainMins}m`;
}
function getStatusIcon(status: string) {
switch (status) {
case "completed":
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case "failed":
return <XCircle className="h-4 w-4 text-red-500" />;
case "running":
return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
case "requested":
case "scheduling":
case "scheduled":
return <Clock className="h-4 w-4 text-yellow-500" />;
case "timeout":
return <AlertTriangle className="h-4 w-4 text-orange-500" />;
case "canceling":
case "cancelled":
return <Ban className="h-4 w-4 text-gray-400" />;
case "abandoned":
return <AlertTriangle className="h-4 w-4 text-red-400" />;
default:
return <CircleDot className="h-4 w-4 text-gray-400" />;
}
}
function getStatusBadgeClasses(status: string): string {
switch (status) {
case "completed":
return "bg-green-100 text-green-800";
case "failed":
return "bg-red-100 text-red-800";
case "running":
return "bg-blue-100 text-blue-800";
case "requested":
case "scheduling":
case "scheduled":
return "bg-yellow-100 text-yellow-800";
case "timeout":
return "bg-orange-100 text-orange-800";
case "canceling":
case "cancelled":
return "bg-gray-100 text-gray-800";
case "abandoned":
return "bg-red-100 text-red-600";
default:
return "bg-gray-100 text-gray-800";
}
}
/**
* Panel that displays workflow task (child) executions for a parent
* workflow execution. Shows each task's name, action, status, and timing.
*/
export default function WorkflowTasksPanel({
parentExecutionId,
defaultCollapsed = false,
}: WorkflowTasksPanelProps) {
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const { data, isLoading, error } = useChildExecutions(parentExecutionId);
const tasks = useMemo(() => {
if (!data?.data) return [];
return data.data;
}, [data]);
const summary = useMemo(() => {
const total = tasks.length;
const completed = tasks.filter((t) => t.status === "completed").length;
const failed = tasks.filter((t) => t.status === "failed").length;
const running = tasks.filter(
(t) =>
t.status === "running" ||
t.status === "requested" ||
t.status === "scheduling" ||
t.status === "scheduled",
).length;
const other = total - completed - failed - running;
return { total, completed, failed, running, other };
}, [tasks]);
if (!isLoading && tasks.length === 0 && !error) {
// No child tasks — nothing to show
return null;
}
return (
<div className="bg-white shadow rounded-lg">
{/* Header */}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="w-full flex items-center justify-between p-6 text-left hover:bg-gray-50 rounded-lg transition-colors"
>
<div className="flex items-center gap-3">
{isCollapsed ? (
<ChevronRight className="h-5 w-5 text-gray-400" />
) : (
<ChevronDown className="h-5 w-5 text-gray-400" />
)}
<Workflow className="h-5 w-5 text-indigo-500" />
<h2 className="text-xl font-semibold">Workflow Tasks</h2>
{!isLoading && (
<span className="text-sm text-gray-500">
({summary.total} task{summary.total !== 1 ? "s" : ""})
</span>
)}
</div>
{/* Summary badges */}
{!isCollapsed || !isLoading ? (
<div className="flex items-center gap-2">
{summary.completed > 0 && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle2 className="h-3 w-3" />
{summary.completed}
</span>
)}
{summary.running > 0 && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<Loader2 className="h-3 w-3 animate-spin" />
{summary.running}
</span>
)}
{summary.failed > 0 && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<XCircle className="h-3 w-3" />
{summary.failed}
</span>
)}
{summary.other > 0 && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
{summary.other}
</span>
)}
</div>
) : null}
</button>
{/* Content */}
{!isCollapsed && (
<div className="px-6 pb-6">
{isLoading && (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
<span className="ml-2 text-sm text-gray-500">
Loading workflow tasks
</span>
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded text-sm">
Error loading workflow tasks:{" "}
{error instanceof Error ? error.message : "Unknown error"}
</div>
)}
{!isLoading && !error && tasks.length > 0 && (
<div className="space-y-2">
{/* Column headers */}
<div className="grid grid-cols-12 gap-3 px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-100">
<div className="col-span-1">#</div>
<div className="col-span-3">Task</div>
<div className="col-span-3">Action</div>
<div className="col-span-2">Status</div>
<div className="col-span-2">Duration</div>
<div className="col-span-1">Retry</div>
</div>
{/* Task rows */}
{tasks.map((task, idx) => {
const wt = task.workflow_task;
const taskName = wt?.task_name ?? `Task ${idx + 1}`;
const retryCount = wt?.retry_count ?? 0;
const maxRetries = wt?.max_retries ?? 0;
const timedOut = wt?.timed_out ?? false;
// Compute duration from created → updated (best available)
const created = new Date(task.created);
const updated = new Date(task.updated);
const durationMs =
wt?.duration_ms ??
(task.status === "completed" ||
task.status === "failed" ||
task.status === "timeout"
? updated.getTime() - created.getTime()
: null);
return (
<Link
key={task.id}
to={`/executions/${task.id}`}
className="grid grid-cols-12 gap-3 px-3 py-3 rounded-lg hover:bg-gray-50 transition-colors items-center group"
>
{/* Index */}
<div className="col-span-1 text-sm text-gray-400 font-mono">
{idx + 1}
</div>
{/* Task name */}
<div className="col-span-3 flex items-center gap-2 min-w-0">
{getStatusIcon(task.status)}
<span
className="text-sm font-medium text-gray-900 truncate group-hover:text-blue-600"
title={taskName}
>
{taskName}
</span>
{wt?.task_index != null && (
<span className="text-xs text-gray-400 flex-shrink-0">
[{wt.task_index}]
</span>
)}
</div>
{/* Action ref */}
<div className="col-span-3 min-w-0">
<span
className="text-sm text-gray-600 truncate block"
title={task.action_ref}
>
{task.action_ref}
</span>
</div>
{/* Status badge */}
<div className="col-span-2 flex items-center gap-1.5">
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${getStatusBadgeClasses(task.status)}`}
>
{task.status}
</span>
{timedOut && (
<span title="Timed out">
<AlertTriangle className="h-3.5 w-3.5 text-orange-500" />
</span>
)}
</div>
{/* Duration */}
<div className="col-span-2 text-sm text-gray-500">
{task.status === "running" ? (
<span className="text-blue-600">
{formatDistanceToNow(created, { addSuffix: false })}
</span>
) : durationMs != null && durationMs > 0 ? (
formatDuration(durationMs)
) : (
<span className="text-gray-300"></span>
)}
</div>
{/* Retry info */}
<div className="col-span-1 text-sm text-gray-500">
{maxRetries > 0 ? (
<span
className="inline-flex items-center gap-0.5"
title={`Attempt ${retryCount + 1} of ${maxRetries + 1}`}
>
<RotateCcw className="h-3 w-3" />
{retryCount}/{maxRetries}
</span>
) : (
<span className="text-gray-300"></span>
)}
</div>
</Link>
);
})}
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,297 @@
import { memo, useEffect } from "react";
import { Link } from "react-router-dom";
import { X, ExternalLink, Loader2 } from "lucide-react";
import { useExecution } from "@/hooks/useExecutions";
import { useExecutionStream } from "@/hooks/useExecutionStream";
import { formatDistanceToNow } from "date-fns";
import type { ExecutionStatus } from "@/api";
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
const secs = ms / 1000;
if (secs < 60) return `${secs.toFixed(1)}s`;
const mins = Math.floor(secs / 60);
const remainSecs = Math.round(secs % 60);
if (mins < 60) return `${mins}m ${remainSecs}s`;
const hrs = Math.floor(mins / 60);
const remainMins = mins % 60;
return `${hrs}h ${remainMins}m`;
}
const getStatusColor = (status: string) => {
switch (status) {
case "succeeded":
case "completed":
return "bg-green-100 text-green-800";
case "failed":
case "timeout":
return "bg-red-100 text-red-800";
case "running":
return "bg-blue-100 text-blue-800";
case "scheduled":
case "scheduling":
case "requested":
return "bg-yellow-100 text-yellow-800";
case "canceling":
case "cancelled":
return "bg-gray-100 text-gray-600";
default:
return "bg-gray-100 text-gray-800";
}
};
interface ExecutionPreviewPanelProps {
executionId: number;
onClose: () => void;
}
const ExecutionPreviewPanel = memo(function ExecutionPreviewPanel({
executionId,
onClose,
}: ExecutionPreviewPanelProps) {
const { data, isLoading, error } = useExecution(executionId);
const execution = data?.data;
// Subscribe to real-time updates for this execution
useExecutionStream({ executionId, enabled: true });
// Close on Escape key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
const isRunning =
execution?.status === "running" ||
execution?.status === "scheduling" ||
execution?.status === "scheduled" ||
execution?.status === "requested";
const created = execution ? new Date(execution.created) : null;
const updated = execution ? new Date(execution.updated) : null;
const durationMs =
created && updated && !isRunning
? updated.getTime() - created.getTime()
: null;
return (
<div className="border-l border-gray-200 bg-white flex flex-col h-full overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-gray-50 flex-shrink-0">
<div className="flex items-center gap-2 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 truncate">
Execution #{executionId}
</h3>
{execution && (
<span
className={`px-2 py-0.5 text-xs rounded-full font-medium flex-shrink-0 ${getStatusColor(execution.status)}`}
>
{execution.status}
</span>
)}
{isRunning && (
<Loader2 className="h-3.5 w-3.5 text-blue-500 animate-spin flex-shrink-0" />
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<Link
to={`/executions/${executionId}`}
className="p-1.5 text-gray-400 hover:text-blue-600 rounded hover:bg-gray-100 transition-colors"
title="Open full detail page"
>
<ExternalLink className="h-4 w-4" />
</Link>
<button
onClick={onClose}
className="p-1.5 text-gray-400 hover:text-gray-600 rounded hover:bg-gray-100 transition-colors"
title="Close preview (Esc)"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto">
{isLoading && (
<div className="flex items-center justify-center h-32">
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
</div>
)}
{error && !execution && (
<div className="p-4">
<div className="bg-red-50 border border-red-200 text-red-700 px-3 py-2 rounded text-sm">
Error: {(error as Error).message}
</div>
</div>
)}
{execution && (
<div className="divide-y divide-gray-100">
{/* Action */}
<div className="px-4 py-3">
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Action
</dt>
<dd className="mt-1">
<Link
to={`/actions/${execution.action_ref}`}
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
>
{execution.action_ref}
</Link>
</dd>
</div>
{/* Timing */}
<div className="px-4 py-3 space-y-2">
<div>
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Created
</dt>
<dd className="mt-0.5 text-sm text-gray-900">
{created!.toLocaleString()}
<span className="text-gray-400 ml-1.5 text-xs">
{formatDistanceToNow(created!, { addSuffix: true })}
</span>
</dd>
</div>
{durationMs != null && durationMs > 0 && (
<div>
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Duration
</dt>
<dd className="mt-0.5 text-sm text-gray-900">
{formatDuration(durationMs)}
</dd>
</div>
)}
{isRunning && (
<div>
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Elapsed
</dt>
<dd className="mt-0.5 text-sm text-blue-600 flex items-center gap-1.5">
<Loader2 className="h-3 w-3 animate-spin" />
{formatDistanceToNow(created!)}
</dd>
</div>
)}
</div>
{/* References */}
<div className="px-4 py-3 space-y-2">
{execution.parent && (
<div>
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Parent Execution
</dt>
<dd className="mt-0.5 text-sm">
<Link
to={`/executions/${execution.parent}`}
className="text-blue-600 hover:text-blue-800 font-mono"
>
#{execution.parent}
</Link>
</dd>
</div>
)}
{execution.enforcement && (
<div>
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Enforcement
</dt>
<dd className="mt-0.5 text-sm text-gray-900 font-mono">
#{execution.enforcement}
</dd>
</div>
)}
{execution.executor && (
<div>
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Executor
</dt>
<dd className="mt-0.5 text-sm text-gray-900 font-mono">
#{execution.executor}
</dd>
</div>
)}
{execution.workflow_task && (
<div>
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Workflow Task
</dt>
<dd className="mt-0.5 text-sm text-gray-900">
<span className="font-medium">
{execution.workflow_task.task_name}
</span>
{execution.workflow_task.task_index != null && (
<span className="text-gray-400 ml-1">
[{execution.workflow_task.task_index}]
</span>
)}
</dd>
</div>
)}
</div>
{/* Config / Parameters */}
{execution.config &&
Object.keys(execution.config).length > 0 && (
<div className="px-4 py-3">
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1.5">
Parameters
</dt>
<dd>
<pre className="bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto max-h-48 overflow-y-auto">
{JSON.stringify(execution.config, null, 2)}
</pre>
</dd>
</div>
)}
{/* Result */}
{execution.result &&
Object.keys(execution.result).length > 0 && (
<div className="px-4 py-3">
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1.5">
Result
</dt>
<dd>
<pre
className={`border rounded p-3 text-xs overflow-x-auto max-h-64 overflow-y-auto ${
execution.status === ("failed" as ExecutionStatus) ||
execution.status === ("timeout" as ExecutionStatus)
? "bg-red-50 border-red-200"
: "bg-gray-50 border-gray-200"
}`}
>
{JSON.stringify(execution.result, null, 2)}
</pre>
</dd>
</div>
)}
</div>
)}
</div>
{/* Footer */}
{execution && (
<div className="px-4 py-3 border-t border-gray-200 bg-gray-50 flex-shrink-0">
<Link
to={`/executions/${executionId}`}
className="block w-full text-center px-3 py-2 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors"
>
Open Full Details
</Link>
</div>
)}
</div>
);
});
export default ExecutionPreviewPanel;

View File

@@ -0,0 +1,78 @@
import { memo } from "react";
interface PaginationProps {
page: number;
setPage: (page: number) => void;
pageSize: number;
total: number;
}
function computeRange(page: number, pageSize: number, total: number) {
const start = (page - 1) * pageSize + 1;
const end = Math.min(page * pageSize, total);
return { start, end };
}
const Pagination = memo(function Pagination({
page,
setPage,
pageSize,
total,
}: PaginationProps) {
const totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1) return null;
const { start, end } = computeRange(page, pageSize, total);
return (
<div className="bg-gray-50 px-6 py-4 flex items-center justify-between border-t border-gray-200">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{start}</span> to{" "}
<span className="font-medium">{end}</span> of{" "}
<span className="font-medium">{total}</span> executions
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</nav>
</div>
</div>
</div>
);
});
Pagination.displayName = "Pagination";
export default Pagination;

View File

@@ -0,0 +1,618 @@
import { useState, useMemo, memo } from "react";
import { Link } from "react-router-dom";
import {
ChevronRight,
ChevronDown,
Workflow,
Loader2,
CheckCircle2,
XCircle,
Clock,
AlertTriangle,
Ban,
CircleDot,
RotateCcw,
} from "lucide-react";
import { useChildExecutions } from "@/hooks/useExecutions";
import type { ExecutionSummary } from "@/api";
import Pagination from "./Pagination";
// ─── Helpers ────────────────────────────────────────────────────────────────
function getStatusColor(status: string) {
switch (status) {
case "completed":
return "bg-green-100 text-green-800";
case "failed":
case "timeout":
return "bg-red-100 text-red-800";
case "running":
return "bg-blue-100 text-blue-800";
case "requested":
case "scheduling":
case "scheduled":
return "bg-yellow-100 text-yellow-800";
case "canceling":
case "cancelled":
return "bg-gray-100 text-gray-600";
default:
return "bg-gray-100 text-gray-800";
}
}
function getStatusIcon(status: string) {
switch (status) {
case "completed":
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case "failed":
return <XCircle className="h-4 w-4 text-red-500" />;
case "running":
return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
case "requested":
case "scheduling":
case "scheduled":
return <Clock className="h-4 w-4 text-yellow-500" />;
case "timeout":
return <AlertTriangle className="h-4 w-4 text-orange-500" />;
case "canceling":
case "cancelled":
return <Ban className="h-4 w-4 text-gray-400" />;
case "abandoned":
return <AlertTriangle className="h-4 w-4 text-red-400" />;
default:
return <CircleDot className="h-4 w-4 text-gray-400" />;
}
}
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
const secs = ms / 1000;
if (secs < 60) return `${secs.toFixed(1)}s`;
const mins = Math.floor(secs / 60);
const remainSecs = Math.round(secs % 60);
if (mins < 60) return `${mins}m ${remainSecs}s`;
const hrs = Math.floor(mins / 60);
const remainMins = mins % 60;
return `${hrs}h ${remainMins}m`;
}
// ─── Child execution row (recursive) ────────────────────────────────────────
interface ChildExecutionRowProps {
execution: ExecutionSummary;
depth: number;
selectedExecutionId: number | null;
onSelectExecution: (id: number) => void;
workflowActionRefs: Set<string>;
}
/**
* A single child-execution row inside the accordion. If it has its own
* children (nested workflow), it can be expanded recursively.
*/
const ChildExecutionRow = memo(function ChildExecutionRow({
execution,
depth,
selectedExecutionId,
onSelectExecution,
workflowActionRefs,
}: ChildExecutionRowProps) {
const isWorkflow = workflowActionRefs.has(execution.action_ref);
const [expanded, setExpanded] = useState(false);
// Only fetch children when expanded and this is a workflow action
const { data, isLoading } = useChildExecutions(
expanded && isWorkflow ? execution.id : undefined,
);
const children = useMemo(() => data?.data ?? [], [data]);
const hasChildren = expanded && children.length > 0;
const wt = execution.workflow_task;
const taskName = wt?.task_name;
const retryCount = wt?.retry_count ?? 0;
const maxRetries = wt?.max_retries ?? 0;
const created = new Date(execution.created);
const updated = new Date(execution.updated);
const durationMs =
wt?.duration_ms ??
(execution.status === "completed" ||
execution.status === "failed" ||
execution.status === "timeout"
? updated.getTime() - created.getTime()
: null);
const indent = 16 + depth * 24;
return (
<>
<tr
className={`hover:bg-gray-50/80 group border-t border-gray-100 cursor-pointer ${
selectedExecutionId === execution.id
? "bg-blue-50 hover:bg-blue-50"
: ""
}`}
onClick={() => onSelectExecution(execution.id)}
>
{/* Task name / expand toggle */}
<td className="py-3 pr-2" style={{ paddingLeft: indent }}>
<div className="flex items-center gap-1.5 min-w-0">
{isWorkflow && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setExpanded((prev) => !prev);
}}
className={`flex-shrink-0 p-0.5 rounded hover:bg-gray-200 transition-colors ${
expanded || isLoading
? "visible"
: "invisible group-hover:visible"
}`}
title={expanded ? "Collapse" : "Expand"}
>
{isLoading ? (
<Loader2 className="h-3.5 w-3.5 text-gray-400 animate-spin" />
) : expanded ? (
<ChevronDown className="h-3.5 w-3.5 text-gray-400" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-gray-400" />
)}
</button>
)}
{getStatusIcon(execution.status)}
{taskName && (
<span
className="text-sm font-medium text-gray-700 truncate"
title={taskName}
>
{taskName}
</span>
)}
{wt?.task_index != null && (
<span className="text-xs text-gray-400 flex-shrink-0">
[{wt.task_index}]
</span>
)}
</div>
</td>
{/* Exec ID */}
<td className="px-4 py-3 font-mono text-xs">
<Link
to={`/executions/${execution.id}`}
className="text-blue-600 hover:text-blue-800"
onClick={(e) => e.stopPropagation()}
>
#{execution.id}
</Link>
</td>
{/* Action */}
<td className="px-4 py-3">
<Link
to={`/executions/${execution.id}`}
className="text-sm text-blue-600 hover:text-blue-800 hover:underline truncate block"
title={execution.action_ref}
onClick={(e) => e.stopPropagation()}
>
{execution.action_ref}
</Link>
</td>
{/* Status */}
<td className="px-4 py-3">
<span
className={`px-2 py-0.5 text-xs rounded-full font-medium ${getStatusColor(execution.status)}`}
>
{execution.status}
</span>
</td>
{/* Duration */}
<td className="px-4 py-3 text-sm text-gray-500">
{execution.status === "running" ? (
<span className="text-blue-600 flex items-center gap-1">
<Loader2 className="h-3 w-3 animate-spin" />
running
</span>
) : durationMs != null && durationMs > 0 ? (
formatDuration(durationMs)
) : (
<span className="text-gray-300">&mdash;</span>
)}
</td>
{/* Retry */}
<td className="px-4 py-3 text-sm text-gray-500">
{maxRetries > 0 ? (
<span
className="inline-flex items-center gap-0.5"
title={`Attempt ${retryCount + 1} of ${maxRetries + 1}`}
>
<RotateCcw className="h-3 w-3" />
{retryCount}/{maxRetries}
</span>
) : (
<span className="text-gray-300">&mdash;</span>
)}
</td>
</tr>
{/* Nested children */}
{expanded &&
!isLoading &&
hasChildren &&
children.map((child: ExecutionSummary) => (
<ChildExecutionRow
key={child.id}
execution={child}
depth={depth + 1}
selectedExecutionId={selectedExecutionId}
onSelectExecution={onSelectExecution}
workflowActionRefs={workflowActionRefs}
/>
))}
</>
);
});
// ─── Top-level workflow row (accordion) ─────────────────────────────────────
interface WorkflowExecutionRowProps {
execution: ExecutionSummary;
workflowActionRefs: Set<string>;
selectedExecutionId: number | null;
onSelectExecution: (id: number) => void;
}
/**
* A top-level execution row with an expandable accordion for child tasks.
*/
const WorkflowExecutionRow = memo(function WorkflowExecutionRow({
execution,
workflowActionRefs,
selectedExecutionId,
onSelectExecution,
}: WorkflowExecutionRowProps) {
const isWorkflow = workflowActionRefs.has(execution.action_ref);
const [expanded, setExpanded] = useState(false);
const { data, isLoading } = useChildExecutions(
expanded && isWorkflow ? execution.id : undefined,
);
const children = useMemo(() => data?.data ?? [], [data]);
const summary = useMemo(() => {
const total = children.length;
const completed = children.filter(
(t: ExecutionSummary) => t.status === "completed",
).length;
const failed = children.filter(
(t: ExecutionSummary) => t.status === "failed" || t.status === "timeout",
).length;
const running = children.filter(
(t: ExecutionSummary) =>
t.status === "running" ||
t.status === "requested" ||
t.status === "scheduling" ||
t.status === "scheduled",
).length;
return { total, completed, failed, running };
}, [children]);
const hasWorkflowChildren = expanded && children.length > 0;
return (
<>
{/* Main execution row */}
<tr
className={`hover:bg-gray-50 border-b border-gray-200 cursor-pointer ${
selectedExecutionId === execution.id
? "bg-blue-50 hover:bg-blue-50"
: ""
}`}
onClick={() => onSelectExecution(execution.id)}
>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
{isWorkflow && (
<button
onClick={(e) => {
e.stopPropagation();
setExpanded((prev) => !prev);
}}
className="flex-shrink-0 p-0.5 rounded hover:bg-gray-200 transition-colors"
title={
expanded ? "Collapse workflow tasks" : "Expand workflow tasks"
}
>
{isLoading ? (
<Loader2 className="h-4 w-4 text-gray-400 animate-spin" />
) : expanded ? (
<ChevronDown className="h-4 w-4 text-gray-500" />
) : (
<ChevronRight className="h-4 w-4 text-gray-500" />
)}
</button>
)}
<Link
to={`/executions/${execution.id}`}
className="text-blue-600 hover:text-blue-800 font-mono text-sm"
onClick={(e) => e.stopPropagation()}
>
#{execution.id}
</Link>
</div>
</td>
<td className="px-6 py-4">
<span className="text-sm text-gray-900">{execution.action_ref}</span>
</td>
<td className="px-6 py-4">
{execution.rule_ref ? (
<span className="text-sm text-gray-700">{execution.rule_ref}</span>
) : (
<span className="text-sm text-gray-400 italic">-</span>
)}
</td>
<td className="px-6 py-4">
{execution.trigger_ref ? (
<span className="text-sm text-gray-700">
{execution.trigger_ref}
</span>
) : (
<span className="text-sm text-gray-400 italic">-</span>
)}
</td>
<td className="px-6 py-4">
<span
className={`px-2 py-1 text-xs rounded ${getStatusColor(execution.status)}`}
>
{execution.status}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-500">
{new Date(execution.created).toLocaleString()}
</td>
</tr>
{/* Expanded child-task section */}
{expanded && (
<tr>
<td colSpan={6} className="p-0">
<div className="bg-gray-50 border-b border-gray-200">
{/* Summary bar */}
{hasWorkflowChildren && (
<div className="flex items-center gap-3 px-8 py-2 border-b border-gray-200 bg-gray-100/60">
<Workflow className="h-4 w-4 text-indigo-500" />
<span className="text-xs font-medium text-gray-600">
{summary.total} task{summary.total !== 1 ? "s" : ""}
</span>
{summary.completed > 0 && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">
<CheckCircle2 className="h-3 w-3" />
{summary.completed}
</span>
)}
{summary.running > 0 && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
<Loader2 className="h-3 w-3 animate-spin" />
{summary.running}
</span>
)}
{summary.failed > 0 && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700">
<XCircle className="h-3 w-3" />
{summary.failed}
</span>
)}
</div>
)}
{/* Loading state */}
{isLoading && (
<div className="flex items-center gap-2 px-8 py-4">
<Loader2 className="h-4 w-4 animate-spin text-gray-400" />
<span className="text-sm text-gray-500">
Loading workflow tasks...
</span>
</div>
)}
{/* No children yet (workflow still starting) */}
{!isLoading && children.length === 0 && (
<div className="px-8 py-3 text-sm text-gray-400 italic">
No child tasks yet.
</div>
)}
{/* Children table */}
{hasWorkflowChildren && (
<table className="w-full">
<thead>
<tr className="text-xs font-medium text-gray-500 uppercase tracking-wider">
<th
className="py-2 pr-2 text-left"
style={{ paddingLeft: 40 }}
>
Task
</th>
<th className="px-4 py-2 text-left">ID</th>
<th className="px-4 py-2 text-left">Action</th>
<th className="px-4 py-2 text-left">Status</th>
<th className="px-4 py-2 text-left">Duration</th>
<th className="px-4 py-2 text-left">Retry</th>
</tr>
</thead>
<tbody>
{children.map((child: ExecutionSummary) => (
<ChildExecutionRow
key={child.id}
execution={child}
depth={0}
selectedExecutionId={selectedExecutionId}
onSelectExecution={onSelectExecution}
workflowActionRefs={workflowActionRefs}
/>
))}
</tbody>
</table>
)}
</div>
</td>
</tr>
)}
</>
);
});
// ─── Main tree table ────────────────────────────────────────────────────────
interface WorkflowExecutionTreeProps {
executions: ExecutionSummary[];
isLoading: boolean;
isFetching: boolean;
error: Error | null;
hasActiveFilters: boolean;
clearFilters: () => void;
page: number;
setPage: (page: number) => void;
pageSize: number;
total: number;
workflowActionRefs: Set<string>;
selectedExecutionId: number | null;
onSelectExecution: (id: number) => void;
}
/**
* Renders the executions list in "By Workflow" mode. Top-level executions
* are shown with the same columns as the "All" view, but each row is
* expandable to reveal the workflow's child task executions in an accordion.
* Nested workflows can be drilled into recursively.
*/
const WorkflowExecutionTree = memo(function WorkflowExecutionTree({
executions,
isLoading,
isFetching,
error,
hasActiveFilters,
clearFilters,
page,
setPage,
pageSize,
total,
workflowActionRefs,
selectedExecutionId,
onSelectExecution,
}: WorkflowExecutionTreeProps) {
// Initial load
if (isLoading && executions.length === 0) {
return (
<div className="bg-white shadow rounded-lg">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
</div>
</div>
);
}
// Error with no cached data
if (error && executions.length === 0) {
return (
<div className="bg-white shadow rounded-lg">
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
<p>Error: {error.message}</p>
</div>
</div>
);
}
// Empty
if (executions.length === 0) {
return (
<div className="bg-white p-12 text-center rounded-lg shadow">
<p>No executions found</p>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="mt-3 text-sm text-blue-600 hover:text-blue-800"
>
Clear filters
</button>
)}
</div>
);
}
return (
<div className="relative">
{/* Loading overlay */}
{isFetching && (
<div className="absolute inset-0 bg-white/60 z-10 flex items-center justify-center rounded-lg">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
</div>
)}
{/* Non-fatal error banner */}
{error && (
<div className="mb-4 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
<p>Error refreshing: {error.message}</p>
</div>
)}
<div className="bg-white shadow rounded-lg overflow-hidden">
<table className="min-w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Action
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Rule
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Trigger
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Created
</th>
</tr>
</thead>
<tbody className="bg-white">
{executions.map((exec: ExecutionSummary) => (
<WorkflowExecutionRow
key={exec.id}
execution={exec}
workflowActionRefs={workflowActionRefs}
selectedExecutionId={selectedExecutionId}
onSelectExecution={onSelectExecution}
/>
))}
</tbody>
</table>
</div>
<Pagination
page={page}
setPage={setPage}
pageSize={pageSize}
total={total}
/>
</div>
);
});
WorkflowExecutionTree.displayName = "WorkflowExecutionTree";
export default WorkflowExecutionTree;

View File

@@ -90,12 +90,6 @@ export function useEnforcementStream(
// Extract enforcement data from notification payload (flat structure)
const enforcementData = notification.payload as any;
// Invalidate history queries so the EntityHistoryPanel picks up new records
// (e.g. status changes recorded by the enforcement_history trigger)
queryClient.invalidateQueries({
queryKey: ["history", "enforcement", notification.entity_id],
});
// Update specific enforcement query if it exists
queryClient.setQueryData(
["enforcements", notification.entity_id],

View File

@@ -48,6 +48,22 @@ function stripNotificationMeta(payload: any): any {
function executionMatchesParams(execution: any, params: any): boolean {
if (!params) return true;
// Check topLevelOnly filter — child executions (with a parent) must not
// appear in top-level list queries.
if (params.topLevelOnly && execution.parent != null) {
return false;
}
// Check parent filter — child execution queries (keyed by { parent: id })
// should only receive notifications for executions belonging to that parent.
// Without this, every execution notification would match child queries since
// they have no other filter fields.
if (params.parent !== undefined) {
if (execution.parent !== params.parent) {
return false;
}
}
// Check status filter (from API query parameters)
if (params.status && execution.status !== params.status) {
return false;

View File

@@ -11,6 +11,7 @@ interface ExecutionsQueryParams {
ruleRef?: string;
triggerRef?: string;
executor?: number;
topLevelOnly?: boolean;
}
export function useExecutions(params?: ExecutionsQueryParams) {
@@ -21,7 +22,8 @@ export function useExecutions(params?: ExecutionsQueryParams) {
params?.packName ||
params?.ruleRef ||
params?.triggerRef ||
params?.executor;
params?.executor ||
params?.topLevelOnly;
return useQuery({
queryKey: ["executions", params],
@@ -35,6 +37,7 @@ export function useExecutions(params?: ExecutionsQueryParams) {
ruleRef: params?.ruleRef,
triggerRef: params?.triggerRef,
executor: params?.executor,
topLevelOnly: params?.topLevelOnly,
});
return response;
},
@@ -59,3 +62,37 @@ export function useExecution(id: number) {
staleTime: 30000, // 30 seconds - SSE handles real-time updates
});
}
/**
* Fetch child executions (workflow tasks) for a given parent execution ID.
*
* Enabled only when `parentId` is provided. Polls every 5 seconds while any
* child execution is still in a running/pending state so the UI stays current.
*/
export function useChildExecutions(parentId: number | undefined) {
return useQuery({
queryKey: ["executions", { parent: parentId }],
queryFn: async () => {
const response = await ExecutionsService.listExecutions({
parent: parentId,
perPage: 100,
});
return response;
},
enabled: !!parentId,
staleTime: 5000,
// Re-fetch periodically so in-progress tasks update
refetchInterval: (query) => {
const data = query.state.data;
if (!data) return false;
const hasActive = data.data.some(
(e) =>
e.status === "requested" ||
e.status === "scheduling" ||
e.status === "scheduled" ||
e.status === "running",
);
return hasActive ? 5000 : false;
},
});
}

View File

@@ -61,12 +61,20 @@ export function useFilterSuggestions() {
return [...new Set(refs)].sort();
}, [actionsData]);
const workflowActionRefs = useMemo(() => {
const refs =
actionsData?.data
?.filter((a) => a.workflow_def != null)
.map((a) => a.ref) || [];
return new Set(refs);
}, [actionsData]);
const triggerRefs = useMemo(() => {
const refs = triggersData?.data?.map((t) => t.ref) || [];
return [...new Set(refs)].sort();
}, [triggersData]);
return { packNames, ruleRefs, actionRefs, triggerRefs };
return { packNames, ruleRefs, actionRefs, triggerRefs, workflowActionRefs };
}
/**

View File

@@ -5,11 +5,7 @@ import { apiClient } from "@/lib/api-client";
* Supported entity types for history queries.
* Maps to the TimescaleDB history hypertables.
*/
export type HistoryEntityType =
| "execution"
| "worker"
| "enforcement"
| "event";
export type HistoryEntityType = "execution" | "worker";
/**
* A single history record from the API.
@@ -68,8 +64,6 @@ export interface HistoryQueryParams {
* Uses the entity-specific endpoints:
* - GET /api/v1/executions/:id/history
* - GET /api/v1/workers/:id/history
* - GET /api/v1/enforcements/:id/history
* - GET /api/v1/events/:id/history
*/
async function fetchEntityHistory(
entityType: HistoryEntityType,
@@ -79,8 +73,6 @@ async function fetchEntityHistory(
const pluralMap: Record<HistoryEntityType, string> = {
execution: "executions",
worker: "workers",
enforcement: "enforcements",
event: "events",
};
const queryParams: Record<string, string | number> = {};
@@ -143,23 +135,3 @@ export function useWorkerHistory(
) {
return useEntityHistory("worker", workerId, params);
}
/**
* Convenience hook for enforcement history.
*/
export function useEnforcementHistory(
enforcementId: number,
params: HistoryQueryParams = {},
) {
return useEntityHistory("enforcement", enforcementId, params);
}
/**
* Convenience hook for event history.
*/
export function useEventHistory(
eventId: number,
params: HistoryQueryParams = {},
) {
return useEntityHistory("event", eventId, params);
}

View File

@@ -2,7 +2,16 @@ import { Link, useParams, useNavigate } from "react-router-dom";
import { useActions, useAction, useDeleteAction } from "@/hooks/useActions";
import { useExecutions } from "@/hooks/useExecutions";
import { useState, useMemo } from "react";
import { ChevronDown, ChevronRight, Search, X, Play, Plus } from "lucide-react";
import {
ChevronDown,
ChevronRight,
Search,
X,
Play,
Plus,
GitBranch,
Pencil,
} from "lucide-react";
import ExecuteActionModal from "@/components/common/ExecuteActionModal";
import ErrorDisplay from "@/components/common/ErrorDisplay";
import { extractProperties } from "@/components/common/ParamSchemaForm";
@@ -177,7 +186,12 @@ export default function ActionsPage() {
: "border-2 border-transparent hover:bg-gray-50"
}`}
>
<div className="font-medium text-sm text-gray-900 truncate">
<div className="font-medium text-sm text-gray-900 truncate flex items-center gap-1.5">
{action.workflow_def && (
<span title="Workflow">
<GitBranch className="w-3.5 h-3.5 text-purple-500 flex-shrink-0" />
</span>
)}
{action.label}
</div>
<div className="font-mono text-xs text-gray-500 mt-1 truncate">
@@ -236,6 +250,7 @@ export default function ActionsPage() {
}
function ActionDetail({ actionRef }: { actionRef: string }) {
const navigate = useNavigate();
const { data: action, isLoading, error } = useAction(actionRef);
const { data: executionsData } = useExecutions({
actionRef: actionRef,
@@ -290,6 +305,17 @@ function ActionDetail({ actionRef }: { actionRef: string }) {
</h1>
</div>
<div className="flex gap-2">
{action.data?.workflow_def && (
<button
onClick={() =>
navigate(`/actions/workflows/${action.data!.ref}/edit`)
}
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 flex items-center gap-2"
>
<Pencil className="h-4 w-4" />
Edit Workflow
</button>
)}
<button
onClick={() => setShowExecuteModal(true)}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center gap-2"

View File

@@ -457,7 +457,7 @@ export default function WorkflowBuilderPage() {
},
});
} else {
await saveWorkflowFile.mutateAsync({
const fileData = {
name: state.name,
label: state.label,
description: state.description || undefined,
@@ -472,7 +472,30 @@ export default function WorkflowBuilderPage() {
Object.keys(state.output).length > 0 ? state.output : undefined,
tags: state.tags.length > 0 ? state.tags : undefined,
enabled: state.enabled,
});
};
try {
await saveWorkflowFile.mutateAsync(fileData);
} catch (createErr: unknown) {
const apiErr = createErr as { status?: number };
if (apiErr?.status === 409) {
// Workflow already exists — fall back to update
const workflowRef = `${state.packRef}.${state.name}`;
await updateWorkflowFile.mutateAsync({
workflowRef,
data: fileData,
});
} else {
throw createErr;
}
}
}
// After a successful first save, navigate to the edit URL so the
// page transitions into edit mode (locks ref, uses update on next save).
if (!isEditing) {
const newRef = `${state.packRef}.${state.name}`;
navigate(`/actions/workflows/${newRef}/edit`, { replace: true });
return;
}
setSaveSuccess(true);
@@ -490,6 +513,7 @@ export default function WorkflowBuilderPage() {
saveWorkflowFile,
updateWorkflowFile,
actionSchemaMap,
navigate,
]);
const handleSave = useCallback(() => {
@@ -540,9 +564,11 @@ export default function WorkflowBuilderPage() {
{/* Left section: Back + metadata */}
<div className="flex items-center gap-3 flex-1 min-w-0">
<button
onClick={() => navigate("/actions")}
onClick={() =>
navigate(isEditing ? `/actions/${editRef}` : "/actions")
}
className="p-1.5 rounded hover:bg-gray-100 text-gray-500 hover:text-gray-700 transition-colors flex-shrink-0"
title="Back to Actions"
title={isEditing ? "Back to Workflow" : "Back to Actions"}
>
<ArrowLeft className="w-5 h-5" />
</button>
@@ -558,6 +584,7 @@ export default function WorkflowBuilderPage() {
}))}
placeholder="Pack..."
className="max-w-[140px]"
disabled={isEditing}
/>
<span className="text-gray-400 text-lg font-light">/</span>
@@ -571,8 +598,9 @@ export default function WorkflowBuilderPage() {
name: e.target.value.replace(/[^a-zA-Z0-9_-]/g, "_"),
})
}
className="px-2 py-1.5 border border-gray-300 rounded text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-48"
className={`px-2 py-1.5 border border-gray-300 rounded text-sm font-mono w-48 ${isEditing ? "bg-gray-100 cursor-not-allowed text-gray-500" : "focus:ring-2 focus:ring-blue-500 focus:border-blue-500"}`}
placeholder="workflow_name"
disabled={isEditing}
/>
<span className="text-gray-400 text-lg font-light"></span>

View File

@@ -1,7 +1,6 @@
import { useParams, Link } from "react-router-dom";
import { useEnforcement } from "@/hooks/useEvents";
import { EnforcementStatus, EnforcementCondition } from "@/api";
import EntityHistoryPanel from "@/components/common/EntityHistoryPanel";
export default function EnforcementDetailPage() {
const { id } = useParams<{ id: string }>();
@@ -189,6 +188,18 @@ export default function EnforcementDetailPage() {
{formatDate(enforcement.created)}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">
Resolved At
</dt>
<dd className="mt-1 text-gray-900">
{enforcement.resolved_at ? (
formatDate(enforcement.resolved_at)
) : (
<span className="text-gray-500">Pending</span>
)}
</dd>
</div>
</dl>
</div>
</div>
@@ -331,6 +342,14 @@ export default function EnforcementDetailPage() {
{formatDate(enforcement.created)}
</dd>
</div>
{enforcement.resolved_at && (
<div>
<dt className="text-gray-500">Resolved</dt>
<dd className="text-gray-900">
{formatDate(enforcement.resolved_at)}
</dd>
</div>
)}
</dl>
</div>
</div>
@@ -377,15 +396,6 @@ export default function EnforcementDetailPage() {
</div>
</div>
</div>
{/* Change History */}
<div className="mt-6">
<EntityHistoryPanel
entityType="enforcement"
entityId={enforcement.id}
title="Enforcement History"
/>
</div>
</div>
);
}

View File

@@ -1,6 +1,5 @@
import { useParams, Link } from "react-router-dom";
import { useEvent } from "@/hooks/useEvents";
import EntityHistoryPanel from "@/components/common/EntityHistoryPanel";
export default function EventDetailPage() {
const { id } = useParams<{ id: string }>();
@@ -259,15 +258,6 @@ export default function EventDetailPage() {
</div>
</div>
</div>
{/* Change History */}
<div className="mt-6">
<EntityHistoryPanel
entityType="event"
entityId={event.id}
title="Event History"
/>
</div>
</div>
);
}

View File

@@ -22,6 +22,7 @@ import { useState, useMemo } from "react";
import { RotateCcw, Loader2 } from "lucide-react";
import ExecuteActionModal from "@/components/common/ExecuteActionModal";
import EntityHistoryPanel from "@/components/common/EntityHistoryPanel";
import WorkflowTasksPanel from "@/components/common/WorkflowTasksPanel";
const getStatusColor = (status: string) => {
switch (status) {
@@ -116,6 +117,9 @@ export default function ExecutionDetailPage() {
// Fetch the action so we can get param_schema for the re-run modal
const { data: actionData } = useAction(execution?.action_ref || "");
// Determine if this execution is a workflow (action has workflow_def)
const isWorkflow = !!actionData?.data?.workflow_def;
const [showRerunModal, setShowRerunModal] = useState(false);
// Fetch status history for the timeline
@@ -207,6 +211,11 @@ export default function ExecutionDetailPage() {
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<h1 className="text-3xl font-bold">Execution #{execution.id}</h1>
{isWorkflow && (
<span className="px-3 py-1 text-sm rounded-full bg-indigo-100 text-indigo-800">
Workflow
</span>
)}
<span
className={`px-3 py-1 text-sm rounded-full ${getStatusColor(execution.status)}`}
>
@@ -247,6 +256,25 @@ export default function ExecutionDetailPage() {
{execution.action_ref}
</Link>
</p>
{execution.workflow_task && (
<p className="text-sm text-indigo-600 mt-1 flex items-center gap-1.5">
<span className="text-gray-500">Task</span>{" "}
<span className="font-medium">
{execution.workflow_task.task_name}
</span>
{execution.parent && (
<>
<span className="text-gray-500">in workflow</span>
<Link
to={`/executions/${execution.parent}`}
className="text-indigo-600 hover:text-indigo-800 font-medium"
>
Execution #{execution.parent}
</Link>
</>
)}
</p>
)}
</div>
{/* Re-Run Modal */}
@@ -504,6 +532,13 @@ export default function ExecutionDetailPage() {
</div>
</div>
{/* Workflow Tasks (shown only for workflow executions) */}
{isWorkflow && (
<div className="mt-6">
<WorkflowTasksPanel parentExecutionId={execution.id} />
</div>
)}
{/* Change History */}
<div className="mt-6">
<EntityHistoryPanel

View File

@@ -3,13 +3,19 @@ import { useExecutions } from "@/hooks/useExecutions";
import { useExecutionStream } from "@/hooks/useExecutionStream";
import { ExecutionStatus } from "@/api";
import { useState, useMemo, memo, useCallback, useEffect } from "react";
import { Search, X } from "lucide-react";
import { Search, X, List, GitBranch } from "lucide-react";
import MultiSelect from "@/components/common/MultiSelect";
import AutocompleteInput from "@/components/common/AutocompleteInput";
import {
useFilterSuggestions,
useMergedSuggestions,
} from "@/hooks/useFilterSuggestions";
import WorkflowExecutionTree from "@/components/executions/WorkflowExecutionTree";
import ExecutionPreviewPanel from "@/components/executions/ExecutionPreviewPanel";
type ViewMode = "all" | "workflow";
const VIEW_MODE_STORAGE_KEY = "attune:executions:viewMode";
// Memoized filter input component for non-ref fields (e.g. Executor ID)
const FilterInput = memo(
@@ -87,6 +93,8 @@ const ExecutionsResultsTable = memo(
setPage,
pageSize,
total,
selectedExecutionId,
onSelectExecution,
}: {
executions: any[];
isLoading: boolean;
@@ -98,6 +106,8 @@ const ExecutionsResultsTable = memo(
setPage: (page: number) => void;
pageSize: number;
total: number;
selectedExecutionId: number | null;
onSelectExecution: (id: number) => void;
}) => {
const totalPages = Math.ceil(total / pageSize);
@@ -182,11 +192,20 @@ const ExecutionsResultsTable = memo(
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{executions.map((exec: any) => (
<tr key={exec.id} className="hover:bg-gray-50">
<tr
key={exec.id}
className={`hover:bg-gray-50 cursor-pointer ${
selectedExecutionId === exec.id
? "bg-blue-50 hover:bg-blue-50"
: ""
}`}
onClick={() => onSelectExecution(exec.id)}
>
<td className="px-6 py-4 font-mono text-sm">
<Link
to={`/executions/${exec.id}`}
className="text-blue-600 hover:text-blue-800"
onClick={(e) => e.stopPropagation()}
>
#{exec.id}
</Link>
@@ -294,6 +313,15 @@ ExecutionsResultsTable.displayName = "ExecutionsResultsTable";
export default function ExecutionsPage() {
const [searchParams] = useSearchParams();
// --- View mode toggle ---
const [viewMode, setViewMode] = useState<ViewMode>(() => {
const stored = localStorage.getItem(VIEW_MODE_STORAGE_KEY);
if (stored === "all" || stored === "workflow") return stored;
const param = searchParams.get("view");
if (param === "all" || param === "workflow") return param;
return "all";
});
// --- Filter input state (updates immediately on keystroke) ---
const [page, setPage] = useState(1);
const pageSize = 50;
@@ -342,8 +370,11 @@ export default function ExecutionsPage() {
if (debouncedStatuses.length === 1) {
params.status = debouncedStatuses[0] as ExecutionStatus;
}
if (viewMode === "workflow") {
params.topLevelOnly = true;
}
return params;
}, [page, pageSize, debouncedFilters, debouncedStatuses]);
}, [page, pageSize, debouncedFilters, debouncedStatuses, viewMode]);
const { data, isLoading, isFetching, error } = useExecutions(queryParams);
const { isConnected } = useExecutionStream({ enabled: true });
@@ -423,103 +454,181 @@ export default function ExecutionsPage() {
Object.values(searchFilters).some((v) => v !== "") ||
selectedStatuses.length > 0;
const [selectedExecutionId, setSelectedExecutionId] = useState<number | null>(
null,
);
const handleSelectExecution = useCallback((id: number) => {
setSelectedExecutionId((prev) => (prev === id ? null : id));
}, []);
const handleClosePreview = useCallback(() => {
setSelectedExecutionId(null);
}, []);
const handleViewModeChange = useCallback((mode: ViewMode) => {
setViewMode(mode);
localStorage.setItem(VIEW_MODE_STORAGE_KEY, mode);
setPage(1);
}, []);
return (
<div className="p-6">
{/* Header - always visible */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold">Executions</h1>
{isFetching && hasActiveFilters && (
<p className="text-sm text-gray-500 mt-1">
Searching executions...
</p>
)}
</div>
{isConnected && (
<div className="flex items-center gap-2 text-sm text-green-600">
<div className="h-2 w-2 rounded-full bg-green-600 animate-pulse" />
<span>Live Updates</span>
<div className="flex h-[calc(100vh-4rem)]">
{/* Main content area */}
<div
className={`flex-1 min-w-0 overflow-y-auto p-6 ${selectedExecutionId ? "mr-0" : ""}`}
>
{/* Header - always visible */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<h1 className="text-3xl font-bold">Executions</h1>
{isConnected && (
<div className="flex items-center gap-1.5 text-xs text-green-600 bg-green-50 border border-green-200 rounded-full px-2.5 py-1">
<div className="h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
<span>Live</span>
</div>
)}
{isFetching && hasActiveFilters && (
<p className="text-sm text-gray-500">Searching executions...</p>
)}
</div>
<div className="flex items-center gap-4">
{/* View mode toggle */}
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-sm">
<button
onClick={() => handleViewModeChange("all")}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-l-lg transition-colors ${
viewMode === "all"
? "bg-blue-600 text-white"
: "text-gray-600 hover:bg-gray-50"
}`}
>
<List className="h-4 w-4" />
All
</button>
<button
onClick={() => handleViewModeChange("workflow")}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-r-lg transition-colors ${
viewMode === "workflow"
? "bg-blue-600 text-white"
: "text-gray-600 hover:bg-gray-50"
}`}
>
<GitBranch className="h-4 w-4" />
By Workflow
</button>
</div>
</div>
</div>
{/* Filter section - always mounted, never unmounts during loading */}
<div className="bg-white shadow rounded-lg p-4 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Search className="h-5 w-5 text-gray-400" />
<h2 className="text-lg font-semibold">Filter Executions</h2>
</div>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900"
>
<X className="h-4 w-4" />
Clear Filters
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<AutocompleteInput
label="Pack"
value={searchFilters.pack}
onChange={(value) => handleFilterChange("pack", value)}
suggestions={packSuggestions}
placeholder="e.g., core"
/>
<AutocompleteInput
label="Rule"
value={searchFilters.rule}
onChange={(value) => handleFilterChange("rule", value)}
suggestions={ruleSuggestions}
placeholder="e.g., core.on_timer"
/>
<AutocompleteInput
label="Action"
value={searchFilters.action}
onChange={(value) => handleFilterChange("action", value)}
suggestions={actionSuggestions}
placeholder="e.g., core.echo"
/>
<AutocompleteInput
label="Trigger"
value={searchFilters.trigger}
onChange={(value) => handleFilterChange("trigger", value)}
suggestions={triggerSuggestions}
placeholder="e.g., core.timer"
/>
<FilterInput
label="Executor ID"
value={searchFilters.executor}
onChange={(value) => handleFilterChange("executor", value)}
placeholder="e.g., 1"
/>
<div>
<MultiSelect
label="Status"
options={STATUS_OPTIONS}
value={selectedStatuses}
onChange={setSelectedStatuses}
placeholder="All Statuses"
/>
</div>
</div>
</div>
{/* Results section - isolated from filter state, only depends on query results */}
{viewMode === "all" ? (
<ExecutionsResultsTable
executions={filteredExecutions}
isLoading={isLoading}
isFetching={isFetching}
error={error as Error | null}
hasActiveFilters={hasActiveFilters}
clearFilters={clearFilters}
page={page}
setPage={setPage}
pageSize={pageSize}
total={total}
selectedExecutionId={selectedExecutionId}
onSelectExecution={handleSelectExecution}
/>
) : (
<WorkflowExecutionTree
executions={filteredExecutions}
isLoading={isLoading}
isFetching={isFetching}
error={error as Error | null}
hasActiveFilters={hasActiveFilters}
clearFilters={clearFilters}
page={page}
setPage={setPage}
pageSize={pageSize}
total={total}
workflowActionRefs={baseSuggestions.workflowActionRefs}
selectedExecutionId={selectedExecutionId}
onSelectExecution={handleSelectExecution}
/>
)}
</div>
{/* Filter section - always mounted, never unmounts during loading */}
<div className="bg-white shadow rounded-lg p-4 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Search className="h-5 w-5 text-gray-400" />
<h2 className="text-lg font-semibold">Filter Executions</h2>
</div>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900"
>
<X className="h-4 w-4" />
Clear Filters
</button>
)}
{/* Right-side preview panel */}
{selectedExecutionId && (
<div className="w-[400px] flex-shrink-0 h-full">
<ExecutionPreviewPanel
executionId={selectedExecutionId}
onClose={handleClosePreview}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<AutocompleteInput
label="Pack"
value={searchFilters.pack}
onChange={(value) => handleFilterChange("pack", value)}
suggestions={packSuggestions}
placeholder="e.g., core"
/>
<AutocompleteInput
label="Rule"
value={searchFilters.rule}
onChange={(value) => handleFilterChange("rule", value)}
suggestions={ruleSuggestions}
placeholder="e.g., core.on_timer"
/>
<AutocompleteInput
label="Action"
value={searchFilters.action}
onChange={(value) => handleFilterChange("action", value)}
suggestions={actionSuggestions}
placeholder="e.g., core.echo"
/>
<AutocompleteInput
label="Trigger"
value={searchFilters.trigger}
onChange={(value) => handleFilterChange("trigger", value)}
suggestions={triggerSuggestions}
placeholder="e.g., core.timer"
/>
<FilterInput
label="Executor ID"
value={searchFilters.executor}
onChange={(value) => handleFilterChange("executor", value)}
placeholder="e.g., 1"
/>
<div>
<MultiSelect
label="Status"
options={STATUS_OPTIONS}
value={selectedStatuses}
onChange={setSelectedStatuses}
placeholder="All Statuses"
/>
</div>
</div>
</div>
{/* Results section - isolated from filter state, only depends on query results */}
<ExecutionsResultsTable
executions={filteredExecutions}
isLoading={isLoading}
isFetching={isFetching}
error={error as Error | null}
hasActiveFilters={hasActiveFilters}
clearFilters={clearFilters}
page={page}
setPage={setPage}
pageSize={pageSize}
total={total}
/>
)}
</div>
);
}