more polish on workflows
Some checks failed
CI / Rustfmt (push) Failing after 25s
CI / Clippy (push) Failing after 2m3s
CI / Cargo Audit & Deny (push) Successful in 33s
CI / Web Blocking Checks (push) Failing after 26s
CI / Security Blocking Checks (push) Successful in 8s
CI / Security Advisory Checks (push) Has been cancelled
CI / Web Advisory Checks (push) Has been cancelled
CI / Tests (push) Has been cancelled
Some checks failed
CI / Rustfmt (push) Failing after 25s
CI / Clippy (push) Failing after 2m3s
CI / Cargo Audit & Deny (push) Successful in 33s
CI / Web Blocking Checks (push) Failing after 26s
CI / Security Blocking Checks (push) Successful in 8s
CI / Security Advisory Checks (push) Has been cancelled
CI / Web Advisory Checks (push) Has been cancelled
CI / Tests (push) Has been cancelled
This commit is contained in:
@@ -49,9 +49,6 @@ pub struct SaveWorkflowFileRequest {
|
|||||||
#[schema(example = json!(["deployment", "automation"]))]
|
#[schema(example = json!(["deployment", "automation"]))]
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Whether the workflow is enabled
|
|
||||||
#[schema(example = true)]
|
|
||||||
pub enabled: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request DTO for creating a new workflow
|
/// Request DTO for creating a new workflow
|
||||||
@@ -97,9 +94,6 @@ pub struct CreateWorkflowRequest {
|
|||||||
#[schema(example = json!(["incident", "slack", "approval"]))]
|
#[schema(example = json!(["incident", "slack", "approval"]))]
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Whether the workflow is enabled
|
|
||||||
#[schema(example = true)]
|
|
||||||
pub enabled: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request DTO for updating a workflow
|
/// Request DTO for updating a workflow
|
||||||
@@ -135,9 +129,6 @@ pub struct UpdateWorkflowRequest {
|
|||||||
#[schema(example = json!(["incident", "slack", "approval", "automation"]))]
|
#[schema(example = json!(["incident", "slack", "approval", "automation"]))]
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Whether the workflow is enabled
|
|
||||||
#[schema(example = true)]
|
|
||||||
pub enabled: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response DTO for workflow information
|
/// Response DTO for workflow information
|
||||||
@@ -187,10 +178,6 @@ pub struct WorkflowResponse {
|
|||||||
#[schema(example = json!(["incident", "slack", "approval"]))]
|
#[schema(example = json!(["incident", "slack", "approval"]))]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
/// Whether the workflow is enabled
|
|
||||||
#[schema(example = true)]
|
|
||||||
pub enabled: bool,
|
|
||||||
|
|
||||||
/// Creation timestamp
|
/// Creation timestamp
|
||||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
@@ -231,10 +218,6 @@ pub struct WorkflowSummary {
|
|||||||
#[schema(example = json!(["incident", "slack", "approval"]))]
|
#[schema(example = json!(["incident", "slack", "approval"]))]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
/// Whether the workflow is enabled
|
|
||||||
#[schema(example = true)]
|
|
||||||
pub enabled: bool,
|
|
||||||
|
|
||||||
/// Creation timestamp
|
/// Creation timestamp
|
||||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
@@ -259,7 +242,6 @@ impl From<attune_common::models::workflow::WorkflowDefinition> for WorkflowRespo
|
|||||||
out_schema: workflow.out_schema,
|
out_schema: workflow.out_schema,
|
||||||
definition: workflow.definition,
|
definition: workflow.definition,
|
||||||
tags: workflow.tags,
|
tags: workflow.tags,
|
||||||
enabled: workflow.enabled,
|
|
||||||
created: workflow.created,
|
created: workflow.created,
|
||||||
updated: workflow.updated,
|
updated: workflow.updated,
|
||||||
}
|
}
|
||||||
@@ -277,7 +259,6 @@ impl From<attune_common::models::workflow::WorkflowDefinition> for WorkflowSumma
|
|||||||
description: workflow.description,
|
description: workflow.description,
|
||||||
version: workflow.version,
|
version: workflow.version,
|
||||||
tags: workflow.tags,
|
tags: workflow.tags,
|
||||||
enabled: workflow.enabled,
|
|
||||||
created: workflow.created,
|
created: workflow.created,
|
||||||
updated: workflow.updated,
|
updated: workflow.updated,
|
||||||
}
|
}
|
||||||
@@ -291,10 +272,6 @@ pub struct WorkflowSearchParams {
|
|||||||
#[param(example = "incident,approval")]
|
#[param(example = "incident,approval")]
|
||||||
pub tags: Option<String>,
|
pub tags: Option<String>,
|
||||||
|
|
||||||
/// Filter by enabled status
|
|
||||||
#[param(example = true)]
|
|
||||||
pub enabled: Option<bool>,
|
|
||||||
|
|
||||||
/// Search term for label/description (case-insensitive)
|
/// Search term for label/description (case-insensitive)
|
||||||
#[param(example = "incident")]
|
#[param(example = "incident")]
|
||||||
pub search: Option<String>,
|
pub search: Option<String>,
|
||||||
@@ -320,7 +297,6 @@ mod tests {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: serde_json::json!({"tasks": []}),
|
definition: serde_json::json!({"tasks": []}),
|
||||||
tags: None,
|
tags: None,
|
||||||
enabled: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(req.validate().is_err());
|
assert!(req.validate().is_err());
|
||||||
@@ -338,7 +314,6 @@ mod tests {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: serde_json::json!({"tasks": []}),
|
definition: serde_json::json!({"tasks": []}),
|
||||||
tags: Some(vec!["test".to_string()]),
|
tags: Some(vec!["test".to_string()]),
|
||||||
enabled: Some(true),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(req.validate().is_ok());
|
assert!(req.validate().is_ok());
|
||||||
@@ -354,7 +329,6 @@ mod tests {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: None,
|
definition: None,
|
||||||
tags: None,
|
tags: None,
|
||||||
enabled: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Should be valid even with all None values
|
// Should be valid even with all None values
|
||||||
@@ -365,7 +339,6 @@ mod tests {
|
|||||||
fn test_workflow_search_params() {
|
fn test_workflow_search_params() {
|
||||||
let params = WorkflowSearchParams {
|
let params = WorkflowSearchParams {
|
||||||
tags: Some("incident,approval".to_string()),
|
tags: Some("incident,approval".to_string()),
|
||||||
enabled: Some(true),
|
|
||||||
search: Some("response".to_string()),
|
search: Some("response".to_string()),
|
||||||
pack_ref: Some("core".to_string()),
|
pack_ref: Some("core".to_string()),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ pub async fn list_workflows(
|
|||||||
let filters = WorkflowSearchFilters {
|
let filters = WorkflowSearchFilters {
|
||||||
pack: None,
|
pack: None,
|
||||||
pack_ref: search_params.pack_ref.clone(),
|
pack_ref: search_params.pack_ref.clone(),
|
||||||
enabled: search_params.enabled,
|
|
||||||
tags,
|
tags,
|
||||||
search: search_params.search.clone(),
|
search: search_params.search.clone(),
|
||||||
limit: pagination.limit(),
|
limit: pagination.limit(),
|
||||||
@@ -113,7 +112,6 @@ pub async fn list_workflows_by_pack(
|
|||||||
let filters = WorkflowSearchFilters {
|
let filters = WorkflowSearchFilters {
|
||||||
pack: None,
|
pack: None,
|
||||||
pack_ref: Some(pack_ref),
|
pack_ref: Some(pack_ref),
|
||||||
enabled: None,
|
|
||||||
tags: None,
|
tags: None,
|
||||||
search: None,
|
search: None,
|
||||||
limit: pagination.limit(),
|
limit: pagination.limit(),
|
||||||
@@ -208,7 +206,6 @@ pub async fn create_workflow(
|
|||||||
out_schema: request.out_schema.clone(),
|
out_schema: request.out_schema.clone(),
|
||||||
definition: request.definition,
|
definition: request.definition,
|
||||||
tags: request.tags.clone().unwrap_or_default(),
|
tags: request.tags.clone().unwrap_or_default(),
|
||||||
enabled: request.enabled.unwrap_or(true),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let workflow = WorkflowDefinitionRepository::create(&state.db, workflow_input).await?;
|
let workflow = WorkflowDefinitionRepository::create(&state.db, workflow_input).await?;
|
||||||
@@ -275,7 +272,6 @@ pub async fn update_workflow(
|
|||||||
out_schema: request.out_schema.clone(),
|
out_schema: request.out_schema.clone(),
|
||||||
definition: request.definition,
|
definition: request.definition,
|
||||||
tags: request.tags,
|
tags: request.tags,
|
||||||
enabled: request.enabled,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let workflow =
|
let workflow =
|
||||||
@@ -408,7 +404,6 @@ pub async fn save_workflow_file(
|
|||||||
out_schema: request.out_schema.clone(),
|
out_schema: request.out_schema.clone(),
|
||||||
definition: definition_json,
|
definition: definition_json,
|
||||||
tags: request.tags.clone().unwrap_or_default(),
|
tags: request.tags.clone().unwrap_or_default(),
|
||||||
enabled: request.enabled.unwrap_or(true),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let workflow = WorkflowDefinitionRepository::create(&state.db, workflow_input).await?;
|
let workflow = WorkflowDefinitionRepository::create(&state.db, workflow_input).await?;
|
||||||
@@ -489,7 +484,6 @@ pub async fn update_workflow_file(
|
|||||||
out_schema: request.out_schema.clone(),
|
out_schema: request.out_schema.clone(),
|
||||||
definition: Some(definition_json),
|
definition: Some(definition_json),
|
||||||
tags: request.tags,
|
tags: request.tags,
|
||||||
enabled: request.enabled,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let workflow =
|
let workflow =
|
||||||
@@ -647,7 +641,6 @@ fn build_action_yaml(pack_ref: &str, request: &SaveWorkflowFileRequest) -> Strin
|
|||||||
lines.push(format!("description: \"{}\"", desc.replace('"', "\\\"")));
|
lines.push(format!("description: \"{}\"", desc.replace('"', "\\\"")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.push("enabled: true".to_string());
|
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
"workflow_file: workflows/{}.workflow.yaml",
|
"workflow_file: workflows/{}.workflow.yaml",
|
||||||
request.name
|
request.name
|
||||||
|
|||||||
@@ -551,7 +551,6 @@ pub async fn create_test_workflow(
|
|||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
tags: vec!["test".to_string()],
|
tags: vec!["test".to_string()],
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(WorkflowDefinitionRepository::create(pool, input).await?)
|
Ok(WorkflowDefinitionRepository::create(pool, input).await?)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ ref: {}.example_workflow
|
|||||||
label: Example Workflow
|
label: Example Workflow
|
||||||
description: A test workflow for integration testing
|
description: A test workflow for integration testing
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
enabled: true
|
|
||||||
parameters:
|
parameters:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
@@ -46,7 +45,6 @@ ref: {}.another_workflow
|
|||||||
label: Another Workflow
|
label: Another Workflow
|
||||||
description: Second test workflow
|
description: Second test workflow
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
enabled: false
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: task1
|
- name: task1
|
||||||
action: core.noop
|
action: core.noop
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ async fn test_create_workflow_success() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"tags": ["test", "automation"],
|
"tags": ["test", "automation"]
|
||||||
"enabled": true
|
|
||||||
}),
|
}),
|
||||||
ctx.token(),
|
ctx.token(),
|
||||||
)
|
)
|
||||||
@@ -60,7 +59,6 @@ async fn test_create_workflow_success() {
|
|||||||
assert_eq!(body["data"]["ref"], "test-pack.test_workflow");
|
assert_eq!(body["data"]["ref"], "test-pack.test_workflow");
|
||||||
assert_eq!(body["data"]["label"], "Test Workflow");
|
assert_eq!(body["data"]["label"], "Test Workflow");
|
||||||
assert_eq!(body["data"]["version"], "1.0.0");
|
assert_eq!(body["data"]["version"], "1.0.0");
|
||||||
assert_eq!(body["data"]["enabled"], true);
|
|
||||||
assert!(body["data"]["tags"].as_array().unwrap().len() == 2);
|
assert!(body["data"]["tags"].as_array().unwrap().len() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +83,6 @@ async fn test_create_workflow_duplicate_ref() {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: json!({"tasks": []}),
|
definition: json!({"tasks": []}),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||||
.await
|
.await
|
||||||
@@ -152,7 +149,6 @@ async fn test_get_workflow_by_ref() {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: json!({"tasks": [{"name": "task1"}]}),
|
definition: json!({"tasks": [{"name": "task1"}]}),
|
||||||
tags: vec!["test".to_string()],
|
tags: vec!["test".to_string()],
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||||
.await
|
.await
|
||||||
@@ -206,7 +202,6 @@ async fn test_list_workflows() {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: json!({"tasks": []}),
|
definition: json!({"tasks": []}),
|
||||||
tags: vec!["test".to_string()],
|
tags: vec!["test".to_string()],
|
||||||
enabled: i % 2 == 1, // Odd ones enabled
|
|
||||||
};
|
};
|
||||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||||
.await
|
.await
|
||||||
@@ -256,7 +251,6 @@ async fn test_list_workflows_by_pack() {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: json!({"tasks": []}),
|
definition: json!({"tasks": []}),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||||
.await
|
.await
|
||||||
@@ -275,7 +269,6 @@ async fn test_list_workflows_by_pack() {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: json!({"tasks": []}),
|
definition: json!({"tasks": []}),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||||
.await
|
.await
|
||||||
@@ -308,14 +301,14 @@ async fn test_list_workflows_with_filters() {
|
|||||||
let pack_name = unique_pack_name();
|
let pack_name = unique_pack_name();
|
||||||
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
||||||
|
|
||||||
// Create workflows with different tags and enabled status
|
// Create workflows with different tags
|
||||||
let workflows = vec![
|
let workflows = vec![
|
||||||
("workflow1", vec!["incident", "approval"], true),
|
("workflow1", vec!["incident", "approval"]),
|
||||||
("workflow2", vec!["incident"], false),
|
("workflow2", vec!["incident"]),
|
||||||
("workflow3", vec!["automation"], true),
|
("workflow3", vec!["automation"]),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (ref_name, tags, enabled) in workflows {
|
for (ref_name, tags) in workflows {
|
||||||
let input = CreateWorkflowDefinitionInput {
|
let input = CreateWorkflowDefinitionInput {
|
||||||
r#ref: format!("test-pack.{}", ref_name),
|
r#ref: format!("test-pack.{}", ref_name),
|
||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
@@ -327,24 +320,12 @@ async fn test_list_workflows_with_filters() {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: json!({"tasks": []}),
|
definition: json!({"tasks": []}),
|
||||||
tags: tags.iter().map(|s| s.to_string()).collect(),
|
tags: tags.iter().map(|s| s.to_string()).collect(),
|
||||||
enabled,
|
|
||||||
};
|
};
|
||||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by enabled (and pack_ref for isolation)
|
|
||||||
let response = ctx
|
|
||||||
.get(
|
|
||||||
&format!("/api/v1/workflows?enabled=true&pack_ref={}", pack_name),
|
|
||||||
ctx.token(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let body: Value = response.json().await.unwrap();
|
|
||||||
assert_eq!(body["data"].as_array().unwrap().len(), 2);
|
|
||||||
|
|
||||||
// Filter by tag (and pack_ref for isolation)
|
// Filter by tag (and pack_ref for isolation)
|
||||||
let response = ctx
|
let response = ctx
|
||||||
.get(
|
.get(
|
||||||
@@ -387,7 +368,6 @@ async fn test_update_workflow() {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: json!({"tasks": []}),
|
definition: json!({"tasks": []}),
|
||||||
tags: vec!["test".to_string()],
|
tags: vec!["test".to_string()],
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||||
.await
|
.await
|
||||||
@@ -400,8 +380,7 @@ async fn test_update_workflow() {
|
|||||||
json!({
|
json!({
|
||||||
"label": "Updated Label",
|
"label": "Updated Label",
|
||||||
"description": "Updated description",
|
"description": "Updated description",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0"
|
||||||
"enabled": false
|
|
||||||
}),
|
}),
|
||||||
ctx.token(),
|
ctx.token(),
|
||||||
)
|
)
|
||||||
@@ -414,7 +393,6 @@ async fn test_update_workflow() {
|
|||||||
assert_eq!(body["data"]["label"], "Updated Label");
|
assert_eq!(body["data"]["label"], "Updated Label");
|
||||||
assert_eq!(body["data"]["description"], "Updated description");
|
assert_eq!(body["data"]["description"], "Updated description");
|
||||||
assert_eq!(body["data"]["version"], "1.1.0");
|
assert_eq!(body["data"]["version"], "1.1.0");
|
||||||
assert_eq!(body["data"]["enabled"], false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -455,7 +433,6 @@ async fn test_delete_workflow() {
|
|||||||
out_schema: None,
|
out_schema: None,
|
||||||
definition: json!({"tasks": []}),
|
definition: json!({"tasks": []}),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -86,9 +86,6 @@ struct ActionYaml {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tags: Option<Vec<String>>,
|
tags: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Whether the action is enabled
|
|
||||||
#[serde(default)]
|
|
||||||
enabled: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API DTOs ────────────────────────────────────────────────────────────
|
// ── API DTOs ────────────────────────────────────────────────────────────
|
||||||
@@ -109,8 +106,6 @@ struct SaveWorkflowFileRequest {
|
|||||||
out_schema: Option<serde_json::Value>,
|
out_schema: Option<serde_json::Value>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
tags: Option<Vec<String>>,
|
tags: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
enabled: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -127,7 +122,6 @@ struct WorkflowResponse {
|
|||||||
out_schema: Option<serde_json::Value>,
|
out_schema: Option<serde_json::Value>,
|
||||||
definition: serde_json::Value,
|
definition: serde_json::Value,
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
enabled: bool,
|
|
||||||
created: String,
|
created: String,
|
||||||
updated: String,
|
updated: String,
|
||||||
}
|
}
|
||||||
@@ -142,7 +136,6 @@ struct WorkflowSummary {
|
|||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
version: String,
|
version: String,
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
enabled: bool,
|
|
||||||
created: String,
|
created: String,
|
||||||
updated: String,
|
updated: String,
|
||||||
}
|
}
|
||||||
@@ -281,7 +274,6 @@ async fn handle_upload(
|
|||||||
param_schema: action.parameters.clone(),
|
param_schema: action.parameters.clone(),
|
||||||
out_schema: action.output.clone(),
|
out_schema: action.output.clone(),
|
||||||
tags: action.tags.clone(),
|
tags: action.tags.clone(),
|
||||||
enabled: action.enabled,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 6. Print progress ───────────────────────────────────────────────
|
// ── 6. Print progress ───────────────────────────────────────────────
|
||||||
@@ -357,7 +349,6 @@ async fn handle_upload(
|
|||||||
response.tags.join(", ")
|
response.tags.join(", ")
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("Enabled", output::format_bool(response.enabled)),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,7 +405,6 @@ async fn handle_list(
|
|||||||
"Pack",
|
"Pack",
|
||||||
"Label",
|
"Label",
|
||||||
"Version",
|
"Version",
|
||||||
"Enabled",
|
|
||||||
"Tags",
|
"Tags",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -426,7 +416,6 @@ async fn handle_list(
|
|||||||
wf.pack_ref.clone(),
|
wf.pack_ref.clone(),
|
||||||
output::truncate(&wf.label, 30),
|
output::truncate(&wf.label, 30),
|
||||||
wf.version.clone(),
|
wf.version.clone(),
|
||||||
output::format_bool(wf.enabled),
|
|
||||||
if wf.tags.is_empty() {
|
if wf.tags.is_empty() {
|
||||||
"-".to_string()
|
"-".to_string()
|
||||||
} else {
|
} else {
|
||||||
@@ -478,7 +467,6 @@ async fn handle_show(
|
|||||||
.unwrap_or_else(|| "-".to_string()),
|
.unwrap_or_else(|| "-".to_string()),
|
||||||
),
|
),
|
||||||
("Version", workflow.version.clone()),
|
("Version", workflow.version.clone()),
|
||||||
("Enabled", output::format_bool(workflow.enabled)),
|
|
||||||
(
|
(
|
||||||
"Tags",
|
"Tags",
|
||||||
if workflow.tags.is_empty() {
|
if workflow.tags.is_empty() {
|
||||||
|
|||||||
@@ -1385,7 +1385,6 @@ pub mod workflow {
|
|||||||
pub out_schema: Option<JsonSchema>,
|
pub out_schema: Option<JsonSchema>,
|
||||||
pub definition: JsonDict,
|
pub definition: JsonDict,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub enabled: bool,
|
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
pub updated: DateTime<Utc>,
|
pub updated: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1131,7 +1131,6 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
out_schema,
|
out_schema,
|
||||||
definition: Some(definition_json),
|
definition: Some(definition_json),
|
||||||
tags: Some(tags),
|
tags: Some(tags),
|
||||||
enabled: Some(true),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
WorkflowDefinitionRepository::update(self.pool, existing.id, update_input).await?;
|
WorkflowDefinitionRepository::update(self.pool, existing.id, update_input).await?;
|
||||||
@@ -1159,7 +1158,6 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
out_schema,
|
out_schema,
|
||||||
definition: definition_json,
|
definition: definition_json,
|
||||||
tags,
|
tags,
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let created = WorkflowDefinitionRepository::create(self.pool, create_input).await?;
|
let created = WorkflowDefinitionRepository::create(self.pool, create_input).await?;
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ pub struct WorkflowSearchFilters {
|
|||||||
pub pack: Option<Id>,
|
pub pack: Option<Id>,
|
||||||
/// Filter by pack reference
|
/// Filter by pack reference
|
||||||
pub pack_ref: Option<String>,
|
pub pack_ref: Option<String>,
|
||||||
/// Filter by enabled status
|
|
||||||
pub enabled: Option<bool>,
|
|
||||||
/// Filter by tags (OR across tags — matches if any tag is present)
|
/// Filter by tags (OR across tags — matches if any tag is present)
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
/// Text search across label and description (case-insensitive substring)
|
/// Text search across label and description (case-insensitive substring)
|
||||||
@@ -62,7 +60,6 @@ pub struct CreateWorkflowDefinitionInput {
|
|||||||
pub out_schema: Option<JsonSchema>,
|
pub out_schema: Option<JsonSchema>,
|
||||||
pub definition: JsonDict,
|
pub definition: JsonDict,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub enabled: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -74,7 +71,6 @@ pub struct UpdateWorkflowDefinitionInput {
|
|||||||
pub out_schema: Option<JsonSchema>,
|
pub out_schema: Option<JsonSchema>,
|
||||||
pub definition: Option<JsonDict>,
|
pub definition: Option<JsonDict>,
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
pub enabled: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -84,7 +80,7 @@ impl FindById for WorkflowDefinitionRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, WorkflowDefinition>(
|
sqlx::query_as::<_, WorkflowDefinition>(
|
||||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated
|
||||||
FROM workflow_definition
|
FROM workflow_definition
|
||||||
WHERE id = $1"
|
WHERE id = $1"
|
||||||
)
|
)
|
||||||
@@ -102,7 +98,7 @@ impl FindByRef for WorkflowDefinitionRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, WorkflowDefinition>(
|
sqlx::query_as::<_, WorkflowDefinition>(
|
||||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated
|
||||||
FROM workflow_definition
|
FROM workflow_definition
|
||||||
WHERE ref = $1"
|
WHERE ref = $1"
|
||||||
)
|
)
|
||||||
@@ -120,7 +116,7 @@ impl List for WorkflowDefinitionRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, WorkflowDefinition>(
|
sqlx::query_as::<_, WorkflowDefinition>(
|
||||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated
|
||||||
FROM workflow_definition
|
FROM workflow_definition
|
||||||
ORDER BY created DESC
|
ORDER BY created DESC
|
||||||
LIMIT 1000"
|
LIMIT 1000"
|
||||||
@@ -141,9 +137,9 @@ impl Create for WorkflowDefinitionRepository {
|
|||||||
{
|
{
|
||||||
sqlx::query_as::<_, WorkflowDefinition>(
|
sqlx::query_as::<_, WorkflowDefinition>(
|
||||||
"INSERT INTO workflow_definition
|
"INSERT INTO workflow_definition
|
||||||
(ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled)
|
(ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
RETURNING id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated"
|
RETURNING id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated"
|
||||||
)
|
)
|
||||||
.bind(&input.r#ref)
|
.bind(&input.r#ref)
|
||||||
.bind(input.pack)
|
.bind(input.pack)
|
||||||
@@ -155,7 +151,6 @@ impl Create for WorkflowDefinitionRepository {
|
|||||||
.bind(&input.out_schema)
|
.bind(&input.out_schema)
|
||||||
.bind(&input.definition)
|
.bind(&input.definition)
|
||||||
.bind(&input.tags)
|
.bind(&input.tags)
|
||||||
.bind(input.enabled)
|
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
@@ -219,20 +214,12 @@ impl Update for WorkflowDefinitionRepository {
|
|||||||
query.push("tags = ").push_bind(tags);
|
query.push("tags = ").push_bind(tags);
|
||||||
has_updates = true;
|
has_updates = true;
|
||||||
}
|
}
|
||||||
if let Some(enabled) = input.enabled {
|
|
||||||
if has_updates {
|
|
||||||
query.push(", ");
|
|
||||||
}
|
|
||||||
query.push("enabled = ").push_bind(enabled);
|
|
||||||
has_updates = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !has_updates {
|
if !has_updates {
|
||||||
return Self::get_by_id(executor, id).await;
|
return Self::get_by_id(executor, id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
query.push(", updated = NOW() WHERE id = ").push_bind(id);
|
query.push(", updated = NOW() WHERE id = ").push_bind(id);
|
||||||
query.push(" RETURNING id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated");
|
query.push(" RETURNING id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated");
|
||||||
|
|
||||||
query
|
query
|
||||||
.build_query_as::<WorkflowDefinition>()
|
.build_query_as::<WorkflowDefinition>()
|
||||||
@@ -269,7 +256,7 @@ impl WorkflowDefinitionRepository {
|
|||||||
where
|
where
|
||||||
E: Executor<'e, Database = Postgres> + Copy + 'e,
|
E: Executor<'e, Database = Postgres> + Copy + 'e,
|
||||||
{
|
{
|
||||||
let select_cols = "id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated";
|
let select_cols = "id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated";
|
||||||
|
|
||||||
let mut qb: QueryBuilder<'_, Postgres> =
|
let mut qb: QueryBuilder<'_, Postgres> =
|
||||||
QueryBuilder::new(format!("SELECT {select_cols} FROM workflow_definition"));
|
QueryBuilder::new(format!("SELECT {select_cols} FROM workflow_definition"));
|
||||||
@@ -301,9 +288,6 @@ impl WorkflowDefinitionRepository {
|
|||||||
if let Some(ref pack_ref) = filters.pack_ref {
|
if let Some(ref pack_ref) = filters.pack_ref {
|
||||||
push_condition!("pack_ref = ", pack_ref.clone());
|
push_condition!("pack_ref = ", pack_ref.clone());
|
||||||
}
|
}
|
||||||
if let Some(enabled) = filters.enabled {
|
|
||||||
push_condition!("enabled = ", enabled);
|
|
||||||
}
|
|
||||||
if let Some(ref tags) = filters.tags {
|
if let Some(ref tags) = filters.tags {
|
||||||
if !tags.is_empty() {
|
if !tags.is_empty() {
|
||||||
// Use PostgreSQL array overlap operator: tags && ARRAY[...]
|
// Use PostgreSQL array overlap operator: tags && ARRAY[...]
|
||||||
@@ -359,7 +343,7 @@ impl WorkflowDefinitionRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, WorkflowDefinition>(
|
sqlx::query_as::<_, WorkflowDefinition>(
|
||||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated
|
||||||
FROM workflow_definition
|
FROM workflow_definition
|
||||||
WHERE pack = $1
|
WHERE pack = $1
|
||||||
ORDER BY label"
|
ORDER BY label"
|
||||||
@@ -379,7 +363,7 @@ impl WorkflowDefinitionRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, WorkflowDefinition>(
|
sqlx::query_as::<_, WorkflowDefinition>(
|
||||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated
|
||||||
FROM workflow_definition
|
FROM workflow_definition
|
||||||
WHERE pack_ref = $1
|
WHERE pack_ref = $1
|
||||||
ORDER BY label"
|
ORDER BY label"
|
||||||
@@ -403,29 +387,13 @@ impl WorkflowDefinitionRepository {
|
|||||||
Ok(result.0)
|
Ok(result.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find all enabled workflows
|
|
||||||
pub async fn find_enabled<'e, E>(executor: E) -> Result<Vec<WorkflowDefinition>>
|
|
||||||
where
|
|
||||||
E: Executor<'e, Database = Postgres> + 'e,
|
|
||||||
{
|
|
||||||
sqlx::query_as::<_, WorkflowDefinition>(
|
|
||||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
|
||||||
FROM workflow_definition
|
|
||||||
WHERE enabled = true
|
|
||||||
ORDER BY label"
|
|
||||||
)
|
|
||||||
.fetch_all(executor)
|
|
||||||
.await
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find workflows by tag
|
/// Find workflows by tag
|
||||||
pub async fn find_by_tag<'e, E>(executor: E, tag: &str) -> Result<Vec<WorkflowDefinition>>
|
pub async fn find_by_tag<'e, E>(executor: E, tag: &str) -> Result<Vec<WorkflowDefinition>>
|
||||||
where
|
where
|
||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, WorkflowDefinition>(
|
sqlx::query_as::<_, WorkflowDefinition>(
|
||||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, created, updated
|
||||||
FROM workflow_definition
|
FROM workflow_definition
|
||||||
WHERE $1 = ANY(tags)
|
WHERE $1 = ANY(tags)
|
||||||
ORDER BY label"
|
ORDER BY label"
|
||||||
|
|||||||
@@ -379,7 +379,6 @@ impl WorkflowRegistrar {
|
|||||||
out_schema: workflow.output.clone(),
|
out_schema: workflow.output.clone(),
|
||||||
definition,
|
definition,
|
||||||
tags: workflow.tags.clone(),
|
tags: workflow.tags.clone(),
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let created = WorkflowDefinitionRepository::create(&self.pool, input).await?;
|
let created = WorkflowDefinitionRepository::create(&self.pool, input).await?;
|
||||||
@@ -411,7 +410,6 @@ impl WorkflowRegistrar {
|
|||||||
out_schema: workflow.output.clone(),
|
out_schema: workflow.output.clone(),
|
||||||
definition: Some(definition),
|
definition: Some(definition),
|
||||||
tags: Some(workflow.tags.clone()),
|
tags: Some(workflow.tags.clone()),
|
||||||
enabled: Some(true),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated = WorkflowDefinitionRepository::update(&self.pool, *workflow_id, input).await?;
|
let updated = WorkflowDefinitionRepository::update(&self.pool, *workflow_id, input).await?;
|
||||||
|
|||||||
@@ -286,21 +286,6 @@ impl ExecutionScheduler {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !workflow_def.enabled {
|
|
||||||
warn!(
|
|
||||||
"Workflow '{}' is disabled, failing execution {}",
|
|
||||||
workflow_def.r#ref, execution.id
|
|
||||||
);
|
|
||||||
let mut fail = execution.clone();
|
|
||||||
fail.status = ExecutionStatus::Failed;
|
|
||||||
fail.result = Some(serde_json::json!({
|
|
||||||
"error": format!("Workflow '{}' is disabled", workflow_def.r#ref),
|
|
||||||
"succeeded": false,
|
|
||||||
}));
|
|
||||||
ExecutionRepository::update(pool, fail.id, fail.into()).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse workflow definition JSON into the strongly-typed struct
|
// Parse workflow definition JSON into the strongly-typed struct
|
||||||
let definition: WorkflowDefinition =
|
let definition: WorkflowDefinition =
|
||||||
serde_json::from_value(workflow_def.definition.clone()).map_err(|e| {
|
serde_json::from_value(workflow_def.definition.clone()).map_err(|e| {
|
||||||
@@ -900,9 +885,25 @@ impl ExecutionScheduler {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancelled workflow: don't dispatch new tasks, but check whether all
|
let parent_execution = ExecutionRepository::find_by_id(pool, workflow_execution.execution)
|
||||||
// running children have now finished. When none remain, finalize the
|
.await?
|
||||||
// parent execution as Cancelled so it doesn't stay stuck in "Canceling".
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Parent execution {} not found for workflow_execution {}",
|
||||||
|
workflow_execution.execution,
|
||||||
|
workflow_execution_id
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Cancellation must be a hard stop for workflow orchestration. Once
|
||||||
|
// either the workflow record, the parent execution, or the completed
|
||||||
|
// child itself is in a cancellation state, do not evaluate transitions,
|
||||||
|
// release more with_items siblings, or dispatch any successor tasks.
|
||||||
|
if Self::should_halt_workflow_advancement(
|
||||||
|
workflow_execution.status,
|
||||||
|
parent_execution.status,
|
||||||
|
execution.status,
|
||||||
|
) {
|
||||||
if workflow_execution.status == ExecutionStatus::Cancelled {
|
if workflow_execution.status == ExecutionStatus::Cancelled {
|
||||||
let running = Self::count_running_workflow_children(
|
let running = Self::count_running_workflow_children(
|
||||||
pool,
|
pool,
|
||||||
@@ -926,11 +927,21 @@ impl ExecutionScheduler {
|
|||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"Cancelled workflow_execution {} still has {} running children, \
|
"Workflow_execution {} is cancelling/cancelled with {} running children, \
|
||||||
waiting for them to finish",
|
skipping advancement",
|
||||||
workflow_execution_id, running
|
workflow_execution_id, running
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"Workflow_execution {} advancement halted due to cancellation state \
|
||||||
|
(workflow: {:?}, parent: {:?}, child: {:?})",
|
||||||
|
workflow_execution_id,
|
||||||
|
workflow_execution.status,
|
||||||
|
parent_execution.status,
|
||||||
|
execution.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -1116,17 +1127,6 @@ impl ExecutionScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the parent execution for context
|
|
||||||
let parent_execution = ExecutionRepository::find_by_id(pool, workflow_execution.execution)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Parent execution {} not found for workflow_execution {}",
|
|
||||||
workflow_execution.execution,
|
|
||||||
workflow_execution_id
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Rebuild the WorkflowContext from persisted state + completed task
|
// Rebuild the WorkflowContext from persisted state + completed task
|
||||||
// results so that successor task inputs can be rendered.
|
// results so that successor task inputs can be rendered.
|
||||||
@@ -1414,6 +1414,23 @@ impl ExecutionScheduler {
|
|||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_halt_workflow_advancement(
|
||||||
|
workflow_status: ExecutionStatus,
|
||||||
|
parent_status: ExecutionStatus,
|
||||||
|
child_status: ExecutionStatus,
|
||||||
|
) -> bool {
|
||||||
|
matches!(
|
||||||
|
workflow_status,
|
||||||
|
ExecutionStatus::Canceling | ExecutionStatus::Cancelled
|
||||||
|
) || matches!(
|
||||||
|
parent_status,
|
||||||
|
ExecutionStatus::Canceling | ExecutionStatus::Cancelled
|
||||||
|
) || matches!(
|
||||||
|
child_status,
|
||||||
|
ExecutionStatus::Canceling | ExecutionStatus::Cancelled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Finalize a cancelled workflow by updating the parent `execution` record
|
/// Finalize a cancelled workflow by updating the parent `execution` record
|
||||||
/// to `Cancelled`. The `workflow_execution` record is already `Cancelled`
|
/// to `Cancelled`. The `workflow_execution` record is already `Cancelled`
|
||||||
/// (set by `cancel_workflow_children`); this only touches the parent.
|
/// (set by `cancel_workflow_children`); this only touches the parent.
|
||||||
@@ -1918,4 +1935,28 @@ mod tests {
|
|||||||
assert_eq!(update.status, Some(ExecutionStatus::Scheduled));
|
assert_eq!(update.status, Some(ExecutionStatus::Scheduled));
|
||||||
assert_eq!(update.worker, Some(99));
|
assert_eq!(update.worker, Some(99));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workflow_advancement_halts_for_any_cancellation_state() {
|
||||||
|
assert!(ExecutionScheduler::should_halt_workflow_advancement(
|
||||||
|
ExecutionStatus::Running,
|
||||||
|
ExecutionStatus::Canceling,
|
||||||
|
ExecutionStatus::Completed
|
||||||
|
));
|
||||||
|
assert!(ExecutionScheduler::should_halt_workflow_advancement(
|
||||||
|
ExecutionStatus::Cancelled,
|
||||||
|
ExecutionStatus::Running,
|
||||||
|
ExecutionStatus::Failed
|
||||||
|
));
|
||||||
|
assert!(ExecutionScheduler::should_halt_workflow_advancement(
|
||||||
|
ExecutionStatus::Running,
|
||||||
|
ExecutionStatus::Running,
|
||||||
|
ExecutionStatus::Cancelled
|
||||||
|
));
|
||||||
|
assert!(!ExecutionScheduler::should_halt_workflow_advancement(
|
||||||
|
ExecutionStatus::Running,
|
||||||
|
ExecutionStatus::Running,
|
||||||
|
ExecutionStatus::Failed
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,7 +379,6 @@ impl WorkflowRegistrar {
|
|||||||
out_schema: workflow.output.clone(),
|
out_schema: workflow.output.clone(),
|
||||||
definition,
|
definition,
|
||||||
tags: workflow.tags.clone(),
|
tags: workflow.tags.clone(),
|
||||||
enabled: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let created = WorkflowDefinitionRepository::create(&self.pool, input).await?;
|
let created = WorkflowDefinitionRepository::create(&self.pool, input).await?;
|
||||||
@@ -411,7 +410,6 @@ impl WorkflowRegistrar {
|
|||||||
out_schema: workflow.output.clone(),
|
out_schema: workflow.output.clone(),
|
||||||
definition: Some(definition),
|
definition: Some(definition),
|
||||||
tags: Some(workflow.tags.clone()),
|
tags: Some(workflow.tags.clone()),
|
||||||
enabled: Some(true),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated = WorkflowDefinitionRepository::update(&self.pool, *workflow_id, input).await?;
|
let updated = WorkflowDefinitionRepository::update(&self.pool, *workflow_id, input).await?;
|
||||||
|
|||||||
@@ -26,14 +26,12 @@ CREATE TABLE workflow_definition (
|
|||||||
out_schema JSONB,
|
out_schema JSONB,
|
||||||
definition JSONB NOT NULL,
|
definition JSONB NOT NULL,
|
||||||
tags TEXT[] DEFAULT '{}',
|
tags TEXT[] DEFAULT '{}',
|
||||||
enabled BOOLEAN DEFAULT true NOT NULL,
|
|
||||||
created TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
created TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||||
updated TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
updated TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
CREATE INDEX idx_workflow_def_pack ON workflow_definition(pack);
|
CREATE INDEX idx_workflow_def_pack ON workflow_definition(pack);
|
||||||
CREATE INDEX idx_workflow_def_enabled ON workflow_definition(enabled);
|
|
||||||
CREATE INDEX idx_workflow_def_ref ON workflow_definition(ref);
|
CREATE INDEX idx_workflow_def_ref ON workflow_definition(ref);
|
||||||
CREATE INDEX idx_workflow_def_tags ON workflow_definition USING gin(tags);
|
CREATE INDEX idx_workflow_def_tags ON workflow_definition USING gin(tags);
|
||||||
|
|
||||||
@@ -137,7 +135,6 @@ SELECT
|
|||||||
wd.ref as workflow_ref,
|
wd.ref as workflow_ref,
|
||||||
wd.label,
|
wd.label,
|
||||||
wd.version,
|
wd.version,
|
||||||
wd.enabled,
|
|
||||||
a.id as action_id,
|
a.id as action_id,
|
||||||
a.ref as action_ref,
|
a.ref as action_ref,
|
||||||
a.pack as pack_id,
|
a.pack as pack_id,
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ export type ApiResponse_WorkflowResponse = {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Workflow ID
|
* Workflow ID
|
||||||
*/
|
*/
|
||||||
@@ -72,4 +68,3 @@ export type ApiResponse_WorkflowResponse = {
|
|||||||
*/
|
*/
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ export type CreateWorkflowRequest = {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled?: boolean | null;
|
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
@@ -47,4 +43,3 @@ export type CreateWorkflowRequest = {
|
|||||||
*/
|
*/
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ export type PaginatedResponse_WorkflowSummary = {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Workflow ID
|
* Workflow ID
|
||||||
*/
|
*/
|
||||||
@@ -57,4 +53,3 @@ export type PaginatedResponse_WorkflowSummary = {
|
|||||||
*/
|
*/
|
||||||
pagination: PaginationMeta;
|
pagination: PaginationMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ export type UpdateWorkflowRequest = {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled?: boolean | null;
|
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
@@ -39,4 +35,3 @@ export type UpdateWorkflowRequest = {
|
|||||||
*/
|
*/
|
||||||
version?: string | null;
|
version?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ export type WorkflowResponse = {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Workflow ID
|
* Workflow ID
|
||||||
*/
|
*/
|
||||||
@@ -63,4 +59,3 @@ export type WorkflowResponse = {
|
|||||||
*/
|
*/
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ export type WorkflowSummary = {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Workflow ID
|
* Workflow ID
|
||||||
*/
|
*/
|
||||||
@@ -47,4 +43,3 @@ export type WorkflowSummary = {
|
|||||||
*/
|
*/
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export class WorkflowsService {
|
|||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
tags,
|
tags,
|
||||||
enabled,
|
|
||||||
search,
|
search,
|
||||||
packRef,
|
packRef,
|
||||||
}: {
|
}: {
|
||||||
@@ -73,10 +72,6 @@ export class WorkflowsService {
|
|||||||
* Filter by tag(s) - comma-separated list
|
* Filter by tag(s) - comma-separated list
|
||||||
*/
|
*/
|
||||||
tags?: string | null,
|
tags?: string | null,
|
||||||
/**
|
|
||||||
* Filter by enabled status
|
|
||||||
*/
|
|
||||||
enabled?: boolean | null,
|
|
||||||
/**
|
/**
|
||||||
* Search term for label/description (case-insensitive)
|
* Search term for label/description (case-insensitive)
|
||||||
*/
|
*/
|
||||||
@@ -93,7 +88,6 @@ export class WorkflowsService {
|
|||||||
'page': page,
|
'page': page,
|
||||||
'page_size': pageSize,
|
'page_size': pageSize,
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
'enabled': enabled,
|
|
||||||
'search': search,
|
'search': search,
|
||||||
'pack_ref': packRef,
|
'pack_ref': packRef,
|
||||||
},
|
},
|
||||||
@@ -125,10 +119,6 @@ export class WorkflowsService {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Workflow ID
|
* Workflow ID
|
||||||
*/
|
*/
|
||||||
@@ -216,10 +206,6 @@ export class WorkflowsService {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Workflow ID
|
* Workflow ID
|
||||||
*/
|
*/
|
||||||
@@ -308,10 +294,6 @@ export class WorkflowsService {
|
|||||||
* Workflow description
|
* Workflow description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
|
||||||
* Whether the workflow is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Workflow ID
|
* Workflow ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ const ARROW_LENGTH = 12;
|
|||||||
const ARROW_HALF_WIDTH = 5;
|
const ARROW_HALF_WIDTH = 5;
|
||||||
const ARROW_DIRECTION_LOOKBACK_PX = 10;
|
const ARROW_DIRECTION_LOOKBACK_PX = 10;
|
||||||
const ARROW_DIRECTION_SAMPLES = 48;
|
const ARROW_DIRECTION_SAMPLES = 48;
|
||||||
const ARROW_SHAFT_OVERLAP_PX = 2;
|
// Keep a small amount of shaft under the arrowhead so sample-based trimming
|
||||||
|
// does not leave a visible gap on simple bezier edges without waypoints.
|
||||||
|
const ARROW_SHAFT_OVERLAP_PX = 4;
|
||||||
|
|
||||||
/** Color for each edge type (alias for shared constant) */
|
/** Color for each edge type (alias for shared constant) */
|
||||||
const EDGE_COLORS = EDGE_TYPE_COLORS;
|
const EDGE_COLORS = EDGE_TYPE_COLORS;
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Pencil, Plus, X, LogIn, LogOut } from "lucide-react";
|
import {
|
||||||
|
Pencil,
|
||||||
|
Plus,
|
||||||
|
X,
|
||||||
|
LogIn,
|
||||||
|
LogOut,
|
||||||
|
SlidersHorizontal,
|
||||||
|
} from "lucide-react";
|
||||||
import SchemaBuilder from "@/components/common/SchemaBuilder";
|
import SchemaBuilder from "@/components/common/SchemaBuilder";
|
||||||
import type { ParamDefinition } from "@/types/workflow";
|
import type { CancellationPolicy, ParamDefinition } from "@/types/workflow";
|
||||||
|
import { CANCELLATION_POLICY_LABELS } from "@/types/workflow";
|
||||||
|
|
||||||
interface WorkflowInputsPanelProps {
|
interface WorkflowInputsPanelProps {
|
||||||
|
label: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
cancellationPolicy: CancellationPolicy;
|
||||||
parameters: Record<string, ParamDefinition>;
|
parameters: Record<string, ParamDefinition>;
|
||||||
output: Record<string, ParamDefinition>;
|
output: Record<string, ParamDefinition>;
|
||||||
|
onLabelChange: (label: string) => void;
|
||||||
|
onVersionChange: (version: string) => void;
|
||||||
|
onDescriptionChange: (description: string) => void;
|
||||||
|
onTagsChange: (tags: string[]) => void;
|
||||||
|
onCancellationPolicyChange: (policy: CancellationPolicy) => void;
|
||||||
onParametersChange: (parameters: Record<string, ParamDefinition>) => void;
|
onParametersChange: (parameters: Record<string, ParamDefinition>) => void;
|
||||||
onOutputChange: (output: Record<string, ParamDefinition>) => void;
|
onOutputChange: (output: Record<string, ParamDefinition>) => void;
|
||||||
}
|
}
|
||||||
@@ -82,8 +100,18 @@ function ParamSummaryList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function WorkflowInputsPanel({
|
export default function WorkflowInputsPanel({
|
||||||
|
label,
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
cancellationPolicy,
|
||||||
parameters,
|
parameters,
|
||||||
output,
|
output,
|
||||||
|
onLabelChange,
|
||||||
|
onVersionChange,
|
||||||
|
onDescriptionChange,
|
||||||
|
onTagsChange,
|
||||||
|
onCancellationPolicyChange,
|
||||||
onParametersChange,
|
onParametersChange,
|
||||||
onOutputChange,
|
onOutputChange,
|
||||||
}: WorkflowInputsPanelProps) {
|
}: WorkflowInputsPanelProps) {
|
||||||
@@ -123,8 +151,103 @@ export default function WorkflowInputsPanel({
|
|||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
<div className="flex flex-col h-full overflow-hidden">
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
||||||
{/* Input Parameters */}
|
|
||||||
<div>
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<SlidersHorizontal className="w-3.5 h-3.5 text-blue-500" />
|
||||||
|
<h4 className="text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
Workflow
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2.5 rounded-lg border border-gray-200 bg-white p-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||||
|
Label
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={label}
|
||||||
|
onChange={(e) => onLabelChange(e.target.value)}
|
||||||
|
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="Workflow Label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||||
|
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="Workflow description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||||
|
Version
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={version}
|
||||||
|
onChange={(e) => onVersionChange(e.target.value)}
|
||||||
|
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="1.0.0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||||
|
Tags
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={tags.join(", ")}
|
||||||
|
onChange={(e) =>
|
||||||
|
onTagsChange(
|
||||||
|
e.target.value
|
||||||
|
.split(",")
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="tag-one, tag-two"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-medium text-gray-600 mb-1">
|
||||||
|
Cancellation Policy
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={cancellationPolicy}
|
||||||
|
onChange={(e) =>
|
||||||
|
onCancellationPolicyChange(
|
||||||
|
e.target.value as CancellationPolicy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full px-2.5 py-2 border border-gray-300 rounded-md text-sm text-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
||||||
|
title="Controls how running tasks behave when the workflow is cancelled"
|
||||||
|
>
|
||||||
|
{Object.entries(CANCELLATION_POLICY_LABELS).map(
|
||||||
|
([value, optionLabel]) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{optionLabel}
|
||||||
|
</option>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Parameters */}
|
||||||
|
<div className="border-t border-gray-200 pt-3">
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<LogIn className="w-3.5 h-3.5 text-green-500" />
|
<LogIn className="w-3.5 h-3.5 text-green-500" />
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ interface WorkflowsQueryParams {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
packRef?: string;
|
packRef?: string;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
enabled?: boolean;
|
|
||||||
search?: string;
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ export function useWorkflows(params?: WorkflowsQueryParams) {
|
|||||||
page: params?.page || 1,
|
page: params?.page || 1,
|
||||||
pageSize: params?.pageSize || 50,
|
pageSize: params?.pageSize || 50,
|
||||||
tags: params?.tags,
|
tags: params?.tags,
|
||||||
enabled: params?.enabled,
|
|
||||||
search: params?.search,
|
search: params?.search,
|
||||||
packRef: params?.packRef,
|
packRef: params?.packRef,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Copy,
|
Copy,
|
||||||
Check,
|
Check,
|
||||||
|
PanelLeftClose,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import SearchableSelect from "@/components/common/SearchableSelect";
|
import SearchableSelect from "@/components/common/SearchableSelect";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
@@ -40,7 +41,6 @@ import type {
|
|||||||
WorkflowBuilderState,
|
WorkflowBuilderState,
|
||||||
PaletteAction,
|
PaletteAction,
|
||||||
TransitionPreset,
|
TransitionPreset,
|
||||||
CancellationPolicy,
|
|
||||||
} from "@/types/workflow";
|
} from "@/types/workflow";
|
||||||
import {
|
import {
|
||||||
generateUniqueTaskName,
|
generateUniqueTaskName,
|
||||||
@@ -54,7 +54,6 @@ import {
|
|||||||
removeTaskFromTransitions,
|
removeTaskFromTransitions,
|
||||||
renameTaskInTransitions,
|
renameTaskInTransitions,
|
||||||
findStartingTaskIds,
|
findStartingTaskIds,
|
||||||
CANCELLATION_POLICY_LABELS,
|
|
||||||
} from "@/types/workflow";
|
} from "@/types/workflow";
|
||||||
|
|
||||||
const INITIAL_STATE: WorkflowBuilderState = {
|
const INITIAL_STATE: WorkflowBuilderState = {
|
||||||
@@ -68,10 +67,13 @@ const INITIAL_STATE: WorkflowBuilderState = {
|
|||||||
vars: {},
|
vars: {},
|
||||||
tasks: [],
|
tasks: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
enabled: true,
|
|
||||||
cancellationPolicy: "allow_finish",
|
cancellationPolicy: "allow_finish",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ACTIONS_SIDEBAR_WIDTH = 256;
|
||||||
|
const WORKFLOW_OPTIONS_DEFAULT_WIDTH = 360;
|
||||||
|
const WORKFLOW_OPTIONS_STORAGE_KEY = "workflow-builder-options-width";
|
||||||
|
|
||||||
export default function WorkflowBuilderPage() {
|
export default function WorkflowBuilderPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { ref: editRef } = useParams<{ ref?: string }>();
|
const { ref: editRef } = useParams<{ ref?: string }>();
|
||||||
@@ -104,10 +106,19 @@ export default function WorkflowBuilderPage() {
|
|||||||
const [initialized, setInitialized] = useState(false);
|
const [initialized, setInitialized] = useState(false);
|
||||||
const [showYamlPreview, setShowYamlPreview] = useState(false);
|
const [showYamlPreview, setShowYamlPreview] = useState(false);
|
||||||
const [sidebarTab, setSidebarTab] = useState<"actions" | "inputs">("actions");
|
const [sidebarTab, setSidebarTab] = useState<"actions" | "inputs">("actions");
|
||||||
|
const [workflowOptionsWidth, setWorkflowOptionsWidth] = useState<number>(() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return WORKFLOW_OPTIONS_DEFAULT_WIDTH;
|
||||||
|
}
|
||||||
|
const saved = window.localStorage.getItem(WORKFLOW_OPTIONS_STORAGE_KEY);
|
||||||
|
const parsed = saved ? Number(saved) : NaN;
|
||||||
|
return Number.isFinite(parsed) ? parsed : WORKFLOW_OPTIONS_DEFAULT_WIDTH;
|
||||||
|
});
|
||||||
const [highlightedTransition, setHighlightedTransition] = useState<{
|
const [highlightedTransition, setHighlightedTransition] = useState<{
|
||||||
taskId: string;
|
taskId: string;
|
||||||
transitionIndex: number;
|
transitionIndex: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [isResizingSidebar, setIsResizingSidebar] = useState(false);
|
||||||
|
|
||||||
// Start-node warning toast state
|
// Start-node warning toast state
|
||||||
const [startWarningVisible, setStartWarningVisible] = useState(false);
|
const [startWarningVisible, setStartWarningVisible] = useState(false);
|
||||||
@@ -261,6 +272,71 @@ export default function WorkflowBuilderPage() {
|
|||||||
return null;
|
return null;
|
||||||
}, [state.tasks, startingTaskIds]);
|
}, [state.tasks, startingTaskIds]);
|
||||||
|
|
||||||
|
const getMaxWorkflowOptionsWidth = useCallback(() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return WORKFLOW_OPTIONS_DEFAULT_WIDTH;
|
||||||
|
}
|
||||||
|
return Math.max(
|
||||||
|
ACTIONS_SIDEBAR_WIDTH,
|
||||||
|
Math.floor(window.innerWidth * 0.5),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clampWorkflowOptionsWidth = useCallback(
|
||||||
|
(width: number) =>
|
||||||
|
Math.min(
|
||||||
|
Math.max(Math.round(width), ACTIONS_SIDEBAR_WIDTH),
|
||||||
|
getMaxWorkflowOptionsWidth(),
|
||||||
|
),
|
||||||
|
[getMaxWorkflowOptionsWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWorkflowOptionsWidth((prev) => clampWorkflowOptionsWidth(prev));
|
||||||
|
}, [clampWorkflowOptionsWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setWorkflowOptionsWidth((prev) => clampWorkflowOptionsWidth(prev));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
|
}, [clampWorkflowOptionsWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
WORKFLOW_OPTIONS_STORAGE_KEY,
|
||||||
|
String(workflowOptionsWidth),
|
||||||
|
);
|
||||||
|
}, [workflowOptionsWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isResizingSidebar) return;
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
setWorkflowOptionsWidth(clampWorkflowOptionsWidth(event.clientX));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsResizingSidebar(false);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.style.cursor = "col-resize";
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [isResizingSidebar, clampWorkflowOptionsWidth]);
|
||||||
|
|
||||||
// Render-phase state adjustment: detect warning key changes for immediate
|
// Render-phase state adjustment: detect warning key changes for immediate
|
||||||
// show/hide without refs or synchronous setState inside effects.
|
// show/hide without refs or synchronous setState inside effects.
|
||||||
const warningKey = startNodeWarning
|
const warningKey = startNodeWarning
|
||||||
@@ -475,7 +551,6 @@ export default function WorkflowBuilderPage() {
|
|||||||
out_schema:
|
out_schema:
|
||||||
Object.keys(state.output).length > 0 ? state.output : undefined,
|
Object.keys(state.output).length > 0 ? state.output : undefined,
|
||||||
tags: state.tags.length > 0 ? state.tags : undefined,
|
tags: state.tags.length > 0 ? state.tags : undefined,
|
||||||
enabled: state.enabled,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -493,7 +568,6 @@ export default function WorkflowBuilderPage() {
|
|||||||
out_schema:
|
out_schema:
|
||||||
Object.keys(state.output).length > 0 ? state.output : undefined,
|
Object.keys(state.output).length > 0 ? state.output : undefined,
|
||||||
tags: state.tags.length > 0 ? state.tags : undefined,
|
tags: state.tags.length > 0 ? state.tags : undefined,
|
||||||
enabled: state.enabled,
|
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await saveWorkflowFile.mutateAsync(fileData);
|
await saveWorkflowFile.mutateAsync(fileData);
|
||||||
@@ -635,6 +709,8 @@ export default function WorkflowBuilderPage() {
|
|||||||
|
|
||||||
const isSaving = saveWorkflowFile.isPending || updateWorkflowFile.isPending;
|
const isSaving = saveWorkflowFile.isPending || updateWorkflowFile.isPending;
|
||||||
const isExecuting = requestExecution.isPending;
|
const isExecuting = requestExecution.isPending;
|
||||||
|
const sidebarWidth =
|
||||||
|
sidebarTab === "inputs" ? workflowOptionsWidth : ACTIONS_SIDEBAR_WIDTH;
|
||||||
|
|
||||||
if (isEditing && workflowLoading) {
|
if (isEditing && workflowLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -692,24 +768,9 @@ export default function WorkflowBuilderPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="text-gray-400 text-lg font-light">—</span>
|
<span className="text-gray-400 text-lg font-light">—</span>
|
||||||
|
<span className="truncate text-sm text-gray-600">
|
||||||
{/* Label */}
|
{state.label || "Untitled workflow"}
|
||||||
<input
|
</span>
|
||||||
type="text"
|
|
||||||
value={state.label}
|
|
||||||
onChange={(e) => updateMetadata({ label: e.target.value })}
|
|
||||||
className="px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-1 min-w-[160px] max-w-[300px]"
|
|
||||||
placeholder="Workflow Label"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Version */}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={state.version}
|
|
||||||
onChange={(e) => updateMetadata({ version: e.target.value })}
|
|
||||||
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-20"
|
|
||||||
placeholder="1.0.0"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -819,59 +880,6 @@ export default function WorkflowBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description row (collapsible) */}
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={state.description}
|
|
||||||
onChange={(e) => updateMetadata({ description: e.target.value })}
|
|
||||||
className="flex-1 px-2 py-1 border border-gray-200 rounded text-xs text-gray-600 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
placeholder="Workflow description (optional)"
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={state.tags.join(", ")}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateMetadata({
|
|
||||||
tags: e.target.value
|
|
||||||
.split(",")
|
|
||||||
.map((t) => t.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="px-2 py-1 border border-gray-200 rounded text-xs text-gray-600 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 w-40"
|
|
||||||
placeholder="Tags (comma-sep)"
|
|
||||||
/>
|
|
||||||
<label className="flex items-center gap-1 text-xs text-gray-600">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={state.enabled}
|
|
||||||
onChange={(e) => updateMetadata({ enabled: e.target.checked })}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
Enabled
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={state.cancellationPolicy}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateMetadata({
|
|
||||||
cancellationPolicy: e.target.value as CancellationPolicy,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="px-2 py-1 border border-gray-200 rounded text-xs text-gray-600 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
|
||||||
title="Cancellation policy: controls how running tasks behave when the workflow is cancelled"
|
|
||||||
>
|
|
||||||
{Object.entries(CANCELLATION_POLICY_LABELS).map(
|
|
||||||
([value, label]) => (
|
|
||||||
<option key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Validation errors panel */}
|
{/* Validation errors panel */}
|
||||||
@@ -987,8 +995,11 @@ export default function WorkflowBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Left sidebar: tabbed Actions / Inputs */}
|
{/* Left sidebar: tabbed Actions / Workflow Options */}
|
||||||
<div className="w-64 border-r border-gray-200 bg-gray-50 flex flex-col h-full overflow-hidden">
|
<div
|
||||||
|
className="border-r border-gray-200 bg-gray-50 flex flex-col h-full overflow-hidden relative flex-shrink-0"
|
||||||
|
style={{ width: sidebarWidth }}
|
||||||
|
>
|
||||||
{/* Tab header */}
|
{/* Tab header */}
|
||||||
<div className="flex border-b border-gray-200 bg-white flex-shrink-0">
|
<div className="flex border-b border-gray-200 bg-white flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
@@ -1011,7 +1022,7 @@ export default function WorkflowBuilderPage() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Settings2 className="w-3.5 h-3.5" />
|
<Settings2 className="w-3.5 h-3.5" />
|
||||||
Inputs
|
Workflow Options
|
||||||
{Object.keys(state.parameters).length > 0 && (
|
{Object.keys(state.parameters).length > 0 && (
|
||||||
<span className="text-[10px] bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded-full">
|
<span className="text-[10px] bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded-full">
|
||||||
{Object.keys(state.parameters).length}
|
{Object.keys(state.parameters).length}
|
||||||
@@ -1029,8 +1040,22 @@ export default function WorkflowBuilderPage() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<WorkflowInputsPanel
|
<WorkflowInputsPanel
|
||||||
|
label={state.label}
|
||||||
|
version={state.version}
|
||||||
|
description={state.description}
|
||||||
|
tags={state.tags}
|
||||||
|
cancellationPolicy={state.cancellationPolicy}
|
||||||
parameters={state.parameters}
|
parameters={state.parameters}
|
||||||
output={state.output}
|
output={state.output}
|
||||||
|
onLabelChange={(label) => updateMetadata({ label })}
|
||||||
|
onVersionChange={(version) => updateMetadata({ version })}
|
||||||
|
onDescriptionChange={(description) =>
|
||||||
|
updateMetadata({ description })
|
||||||
|
}
|
||||||
|
onTagsChange={(tags) => updateMetadata({ tags })}
|
||||||
|
onCancellationPolicyChange={(cancellationPolicy) =>
|
||||||
|
updateMetadata({ cancellationPolicy })
|
||||||
|
}
|
||||||
onParametersChange={(parameters) =>
|
onParametersChange={(parameters) =>
|
||||||
setState((prev) => ({ ...prev, parameters }))
|
setState((prev) => ({ ...prev, parameters }))
|
||||||
}
|
}
|
||||||
@@ -1039,6 +1064,30 @@ export default function WorkflowBuilderPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{sidebarTab === "inputs" && (
|
||||||
|
<div
|
||||||
|
className={`absolute top-0 right-0 h-full w-2 translate-x-1/2 cursor-col-resize group ${
|
||||||
|
isResizingSidebar ? "z-30" : "z-10"
|
||||||
|
}`}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsResizingSidebar(true);
|
||||||
|
}}
|
||||||
|
title="Resize workflow options panel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`mx-auto h-full w-px transition-colors ${
|
||||||
|
isResizingSidebar
|
||||||
|
? "bg-blue-500"
|
||||||
|
: "bg-transparent group-hover:bg-blue-300"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="absolute top-3 right-0 -translate-y-1/2 translate-x-1/2 rounded-full border border-gray-200 bg-white p-1 text-gray-300 shadow-sm group-hover:text-blue-500">
|
||||||
|
<PanelLeftClose className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center: Canvas */}
|
{/* Center: Canvas */}
|
||||||
|
|||||||
@@ -224,8 +224,6 @@ export interface WorkflowBuilderState {
|
|||||||
tasks: WorkflowTask[];
|
tasks: WorkflowTask[];
|
||||||
/** Tags */
|
/** Tags */
|
||||||
tags: string[];
|
tags: string[];
|
||||||
/** Whether the workflow is enabled */
|
|
||||||
enabled: boolean;
|
|
||||||
/** Cancellation policy (default: allow_finish) */
|
/** Cancellation policy (default: allow_finish) */
|
||||||
cancellationPolicy: CancellationPolicy;
|
cancellationPolicy: CancellationPolicy;
|
||||||
}
|
}
|
||||||
@@ -285,7 +283,6 @@ export interface ActionYamlDefinition {
|
|||||||
ref: string;
|
ref: string;
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
enabled: boolean;
|
|
||||||
workflow_file: string;
|
workflow_file: string;
|
||||||
parameters?: Record<string, unknown>;
|
parameters?: Record<string, unknown>;
|
||||||
output?: Record<string, unknown>;
|
output?: Record<string, unknown>;
|
||||||
@@ -358,8 +355,6 @@ export interface SaveWorkflowFileRequest {
|
|||||||
out_schema?: Record<string, unknown>;
|
out_schema?: Record<string, unknown>;
|
||||||
/** Tags */
|
/** Tags */
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
/** Whether the workflow is enabled */
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An action summary used in the action palette */
|
/** An action summary used in the action palette */
|
||||||
@@ -581,7 +576,6 @@ export function builderStateToActionYaml(
|
|||||||
const action: ActionYamlDefinition = {
|
const action: ActionYamlDefinition = {
|
||||||
ref: `${state.packRef}.${state.name}`,
|
ref: `${state.packRef}.${state.name}`,
|
||||||
label: state.label,
|
label: state.label,
|
||||||
enabled: state.enabled,
|
|
||||||
workflow_file: `workflows/${state.name}.workflow.yaml`,
|
workflow_file: `workflows/${state.name}.workflow.yaml`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -751,7 +745,6 @@ export function definitionToBuilderState(
|
|||||||
vars: definition.vars || {},
|
vars: definition.vars || {},
|
||||||
tasks,
|
tasks,
|
||||||
tags: definition.tags || [],
|
tags: definition.tags || [],
|
||||||
enabled: true,
|
|
||||||
cancellationPolicy: definition.cancellation_policy || "allow_finish",
|
cancellationPolicy: definition.cancellation_policy || "allow_finish",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user