mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
Merge remote-tracking branch 'origin/main' into dieri/triggers-queue-mode
This commit is contained in:
@@ -82,6 +82,10 @@ jobs:
|
||||
run: |
|
||||
set -e # Exit on any command failure
|
||||
PR_NUMBER=${{ github.event.issue.number }}
|
||||
|
||||
# Set up error trap to comment on PR for any failure
|
||||
trap 'gh pr comment $PR_NUMBER --body "❌ SQLx update failed. Please check the workflow logs for details."' ERR
|
||||
|
||||
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
|
||||
echo "Checking out PR branch: $BRANCH_NAME"
|
||||
git checkout $BRANCH_NAME
|
||||
@@ -93,10 +97,8 @@ jobs:
|
||||
cd backend
|
||||
cargo install sqlx-cli --version 0.8.5
|
||||
sqlx migrate run
|
||||
if ! ./update_sqlx.sh --dir ./windmill-ee-private; then
|
||||
gh pr comment $PR_NUMBER --body "❌ SQLx update failed. Please check the workflow logs for details."
|
||||
exit 1
|
||||
fi
|
||||
./substitute_ee_code.sh --dir ./windmill-ee-private
|
||||
./update_sqlx.sh
|
||||
# Pass the branch name to the next step
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
@@ -216,11 +218,12 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ steps.app.outputs.token }}
|
||||
script: |
|
||||
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Starting ee ref update...'
|
||||
body: `Starting ee ref update...\n\n[View workflow run](${runUrl})`
|
||||
})
|
||||
|
||||
- name: Checkout repository
|
||||
@@ -249,7 +252,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app.outputs.token }}
|
||||
run: |
|
||||
set -e # Exit on any command failure
|
||||
PR_NUMBER=${{ github.event.issue.number }}
|
||||
|
||||
# Set up error trap to comment on PR for any failure
|
||||
trap 'gh pr comment $PR_NUMBER --body "❌ EE ref update failed. Please check the workflow logs for details."' ERR
|
||||
|
||||
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
|
||||
echo "Checking out PR branch: $BRANCH_NAME"
|
||||
git checkout $BRANCH_NAME
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
## [1.585.1](https://github.com/windmill-labs/windmill/compare/v1.585.0...v1.585.1) (2025-11-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clone group members on workspace forks ([#7232](https://github.com/windmill-labs/windmill/issues/7232)) ([faad000](https://github.com/windmill-labs/windmill/commit/faad0006b430262f1ab6b749ccf60a6860a54405))
|
||||
|
||||
## [1.585.0](https://github.com/windmill-labs/windmill/compare/v1.584.0...v1.585.0) (2025-11-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* flow for loop squashing ([#7107](https://github.com/windmill-labs/windmill/issues/7107)) ([93c34c6](https://github.com/windmill-labs/windmill/commit/93c34c6d769dd56da02064c0abdaa060efe0259d))
|
||||
* **frontend:** move app reports script to hub ([#7227](https://github.com/windmill-labs/windmill/issues/7227)) ([f8a0bfc](https://github.com/windmill-labs/windmill/commit/f8a0bfc20d1ccb6ce36e3adf4188533470270bdb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** improve back-compatibility with app.yaml with policy still present ([c0a92f8](https://github.com/windmill-labs/windmill/commit/c0a92f83b00341b0eee36e2dc88d301a75b6fdfb))
|
||||
* **frontend:** workspace color race condition for superadmins ([#7229](https://github.com/windmill-labs/windmill/issues/7229)) ([8922be1](https://github.com/windmill-labs/windmill/commit/8922be11a6935d200e1909f14aa410c6eff67a13))
|
||||
* workspace forks shouldn't inherit promotion mode repo ([#7223](https://github.com/windmill-labs/windmill/issues/7223)) ([f56f3b0](https://github.com/windmill-labs/windmill/commit/f56f3b0fe6185167e9c4c8541ddefba01c509b8d))
|
||||
|
||||
## [1.584.0](https://github.com/windmill-labs/windmill/compare/v1.583.3...v1.584.0) (2025-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **app:** add chat component ([#7199](https://github.com/windmill-labs/windmill/issues/7199)) ([c47669b](https://github.com/windmill-labs/windmill/commit/c47669be0bffb0821afe9d4182cc071cb3b675f0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** handle better public apps ([de7739a](https://github.com/windmill-labs/windmill/commit/de7739a3ee7ef76a8eb4fe6570ef0bdfbdb92293))
|
||||
* **frontend:** forking UI issues ([#7215](https://github.com/windmill-labs/windmill/issues/7215)) ([dccee1d](https://github.com/windmill-labs/windmill/commit/dccee1dba08b4c4b170336b3f3e959fbe7bca8ce))
|
||||
* **frontend:** improve preprocessor discoverability ([#7214](https://github.com/windmill-labs/windmill/issues/7214)) ([a2d3297](https://github.com/windmill-labs/windmill/commit/a2d3297343338ba0b559481723e64db5f4acf3a7))
|
||||
* **frontend:** loading saved/history inputs when json view is on ([#7217](https://github.com/windmill-labs/windmill/issues/7217)) ([e87f814](https://github.com/windmill-labs/windmill/commit/e87f81470904578467f42598cfce5fad2249f131))
|
||||
* **frontend:** show trigger table when no trigger selected ([#7219](https://github.com/windmill-labs/windmill/issues/7219)) ([c37dde0](https://github.com/windmill-labs/windmill/commit/c37dde096c301a502c6ff2c570b91153a5717fcd))
|
||||
* list scripts without 1000 limits ([897faf0](https://github.com/windmill-labs/windmill/commit/897faf085b49ada0648594797cfe627decb50952))
|
||||
|
||||
## [1.583.3](https://github.com/windmill-labs/windmill/compare/v1.583.2...v1.583.3) (2025-11-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** fix pushing non existing apps with cli ([29d8ad2](https://github.com/windmill-labs/windmill/commit/29d8ad2e5de1447f39252edd5ceea22034801547))
|
||||
* fix deadlock on oidc ([0e717c9](https://github.com/windmill-labs/windmill/commit/0e717c9588c1a3811cc4d438b53a93b4b13cf239))
|
||||
|
||||
## [1.583.2](https://github.com/windmill-labs/windmill/compare/v1.583.1...v1.583.2) (2025-11-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* listScripts include description with opt-out query arg ([#7210](https://github.com/windmill-labs/windmill/issues/7210)) ([80c7911](https://github.com/windmill-labs/windmill/commit/80c79116416db8629135a8853df645d9fff286dd))
|
||||
* **scim:** use value instead of display for group updates ([#7203](https://github.com/windmill-labs/windmill/issues/7203)) ([193efe3](https://github.com/windmill-labs/windmill/commit/193efe31c8aeab3a5e3e28278855d518c000bda8))
|
||||
|
||||
## [1.583.1](https://github.com/windmill-labs/windmill/compare/v1.583.0...v1.583.1) (2025-11-21)
|
||||
|
||||
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n workspace.id AS \"id!\",\n workspace.name AS \"name!\",\n workspace.owner AS \"owner!\",\n workspace.deleted AS \"deleted!\",\n workspace.premium AS \"premium!\",\n workspace_settings.color AS \"color\",\n workspace.parent_workspace_id AS \"parent_workspace_id\"\n FROM workspace\n LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id\n WHERE workspace.id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "owner!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "deleted!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "premium!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "parent_workspace_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "00c1dd0cfaf15aafdcfcabc1f123cebdf8d777f48e148bcb171fa15e8bf6f098"
|
||||
}
|
||||
+5
-5
@@ -46,11 +46,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1) as \"exists!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "086fdf726b88e9f4fd9750bf9dd7f49c589465194548d88e5ae30872846b70a9"
|
||||
}
|
||||
+1
-3
@@ -59,9 +59,7 @@
|
||||
"failure",
|
||||
"command",
|
||||
"approval",
|
||||
"preprocessor",
|
||||
"schedule_handler_old",
|
||||
"dynamic_skip"
|
||||
"preprocessor"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_status\n SET flow_status = jsonb_set(\n flow_status,\n '{memory_id}',\n to_jsonb($2::uuid)\n )\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "348d64dc9f81f04538c5fec98df51312794c22d6337be897bc618585fa5b27f3"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO usr_to_group (workspace_id, group_, usr)\n SELECT $2, group_, usr\n FROM usr_to_group\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3f9004b5542afaaef40942a3ca8889ea164d9cfb4962997650d922d82f000df9"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
Generated
+265
-316
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.583.1"
|
||||
version = "1.585.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.583.1"
|
||||
version = "1.585.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -65,15 +65,19 @@ const targets = [
|
||||
# -Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
|
||||
# Build all separately
|
||||
def 'main all' [ --no-opt(-n), --cli] {
|
||||
def 'main all' [ --no-opt(-n), --cli, --node ] {
|
||||
if ($no_opt) {
|
||||
if ($cli) {
|
||||
if ($node) {
|
||||
$targets | each { main $in.ident -n --node }
|
||||
} else if ($cli) {
|
||||
$targets | each { main $in.ident -n --cli }
|
||||
} else {
|
||||
$targets | each { main $in.ident -n }
|
||||
}
|
||||
} else {
|
||||
if ($cli) {
|
||||
if ($node) {
|
||||
$targets | each { main $in.ident --node }
|
||||
} else if ($cli) {
|
||||
$targets | each { main $in.ident --cli }
|
||||
} else {
|
||||
$targets | each { main $in.ident }
|
||||
@@ -86,6 +90,7 @@ def main [
|
||||
target: string, # Language, e.g.: py, ts, rust
|
||||
--no-opt(-n), # Compile in debug mode for dev
|
||||
--cli # Compile and place binaries in cli.
|
||||
--node
|
||||
] {
|
||||
let t = $targets | where ident == $target;
|
||||
|
||||
@@ -97,7 +102,10 @@ def main [
|
||||
mut profile = "";
|
||||
mut tar = "";
|
||||
|
||||
if ($cli) {
|
||||
if ($node) {
|
||||
$env.OUT_DIR = $"node-pkg-($t.ident)"
|
||||
$tar = "nodejs"
|
||||
} else if ($cli) {
|
||||
$env.OUT_DIR = $"../../../cli/wasm/($t.ident)"
|
||||
$tar = "deno"
|
||||
} else {
|
||||
|
||||
@@ -2316,13 +2316,14 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
|
||||
);
|
||||
|
||||
let error_message = format!(
|
||||
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?}).\nThis likely means that the job died on worker {}, OOM are a common reason for worker crashes.\nCheck the workers around the time of the last ping and the exit code if any.",
|
||||
job.last_ping.unwrap_or_default(),
|
||||
*ZOMBIE_JOB_TIMEOUT,
|
||||
error_kind.to_string(),
|
||||
job.worker.clone().unwrap_or_default(),
|
||||
);
|
||||
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?}).\nThis likely means that the job died on worker {}, OOM are a common reason for worker crashes.\nCheck the workers around the time of the last ping and the exit code if any.",
|
||||
job.last_ping.unwrap_or_default(),
|
||||
*ZOMBIE_JOB_TIMEOUT,
|
||||
error_kind.to_string(),
|
||||
job.worker.clone().unwrap_or_default(),
|
||||
);
|
||||
let memory_peak = job.memory_peak.unwrap_or(0);
|
||||
let (_, killpill_rx_never_used) = KillpillSender::new(1);
|
||||
let _ = handle_job_error(
|
||||
db,
|
||||
&client,
|
||||
@@ -2335,6 +2336,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
|
||||
"",
|
||||
node_name,
|
||||
send_result_never_used,
|
||||
&killpill_rx_never_used,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut windmill_common::bench::BenchmarkIter::new(),
|
||||
)
|
||||
|
||||
@@ -217,6 +217,7 @@ async fn test_deno_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
iterator: InputTransform::Javascript { expr: "result".to_string() },
|
||||
skip_failures: false,
|
||||
parallel: false,
|
||||
squash: None,
|
||||
parallelism: None,
|
||||
modules: vec![FlowModule {
|
||||
id: "c".to_string(),
|
||||
@@ -396,6 +397,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
iterator: InputTransform::Static { value: windmill_common::worker::to_raw_value(&[1, 2, 3]) },
|
||||
skip_failures: false,
|
||||
parallel: false,
|
||||
squash: None,
|
||||
parallelism: None,
|
||||
modules: vec![
|
||||
FlowModule {
|
||||
|
||||
@@ -2,15 +2,6 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Parse arguments
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dir) EE_DIR="$2"; shift ;;
|
||||
*) echo "Unknown parameter: $1"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Check if running on macOS
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
echo "Running on macOS - substituting samael..."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.583.1
|
||||
version: 1.585.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -688,6 +688,22 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserWorkspaceList"
|
||||
|
||||
/w/{workspace}/workspaces/get_as_superadmin:
|
||||
get:
|
||||
summary: get workspace as super admin (require to be super admin)
|
||||
operationId: getWorkspaceAsSuperAdmin
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: workspace
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Workspace"
|
||||
|
||||
/workspaces/list_as_superadmin:
|
||||
get:
|
||||
summary: list all workspaces as super admin (require to be super admin)
|
||||
@@ -7707,6 +7723,9 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
run_query_params:
|
||||
type: object
|
||||
description: Runnable query parameters
|
||||
required:
|
||||
- args
|
||||
- component
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use crate::{
|
||||
auth::OptTokened,
|
||||
db::{ApiAuthed, DB},
|
||||
jobs::RunJobQuery,
|
||||
resources::get_resource_value_interpolated_internal,
|
||||
users::{require_owner_of_path, OptAuthed},
|
||||
utils::{check_scopes, WithStarredInfoQuery},
|
||||
@@ -1654,6 +1655,8 @@ pub struct ExecuteApp {
|
||||
pub force_viewer_static_fields: Option<StaticFields>,
|
||||
pub force_viewer_one_of_fields: Option<OneOfFields>,
|
||||
pub force_viewer_allow_user_resources: Option<AllowUserResources>,
|
||||
/// Runnable query parameters (e.g., memory_id for chat-enabled flows)
|
||||
pub run_query_params: Option<RunJobQuery>,
|
||||
}
|
||||
|
||||
fn digest(code: &str) -> String {
|
||||
@@ -1894,6 +1897,12 @@ async fn execute_component(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let is_flow = payload
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|p| p.starts_with("flow/"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let (job_payload, tag, on_behalf_of) = match (payload.path, payload.raw_code, payload.id) {
|
||||
// flow or script:
|
||||
(Some(path), None, None) => get_payload_tag_from_prefixed_path(&path, &db, &w_id).await?,
|
||||
@@ -1920,7 +1929,7 @@ async fn execute_component(
|
||||
|
||||
let end_user_email = opt_authed.as_ref().map(|a| a.email.clone());
|
||||
|
||||
let (uuid, tx) = push(
|
||||
let (uuid, mut tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
&w_id,
|
||||
@@ -1954,6 +1963,14 @@ async fn execute_component(
|
||||
None
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Apply runnable query parameters if provided
|
||||
if let Some(ref run_query) = payload.run_query_params {
|
||||
if is_flow {
|
||||
crate::jobs::process_flow_run_query_params(&mut tx, uuid, run_query).await?;
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(uuid.to_string())
|
||||
|
||||
@@ -134,6 +134,7 @@ pub async fn get_or_create_conversation_with_id(
|
||||
} else {
|
||||
title.to_string()
|
||||
};
|
||||
|
||||
// Create new conversation with provided ID
|
||||
let conversation = sqlx::query_as!(
|
||||
FlowConversation,
|
||||
@@ -148,6 +149,7 @@ pub async fn get_or_create_conversation_with_id(
|
||||
)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(conversation)
|
||||
}
|
||||
|
||||
|
||||
@@ -1583,6 +1583,7 @@ mod tests {
|
||||
skip_failures: true,
|
||||
parallel: false,
|
||||
parallelism: None,
|
||||
squash: None,
|
||||
}),
|
||||
stop_after_if: Some(StopAfterIf {
|
||||
expr: "previous.isEmpty()".to_string(),
|
||||
|
||||
@@ -1741,7 +1741,7 @@ pub struct ListableCompletedJob {
|
||||
pub labels: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
pub struct RunJobQuery {
|
||||
pub scheduled_for: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub scheduled_in_secs: Option<i64>,
|
||||
@@ -3955,13 +3955,13 @@ async fn set_flow_memory_id(
|
||||
memory_id: Uuid,
|
||||
) -> error::Result<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE v2_job_status
|
||||
SET flow_status = jsonb_set(
|
||||
flow_status,
|
||||
'{memory_id}',
|
||||
to_jsonb($2::uuid)
|
||||
)
|
||||
WHERE id = $1",
|
||||
"UPDATE v2_job_status
|
||||
SET flow_status = jsonb_set(
|
||||
flow_status,
|
||||
'{memory_id}',
|
||||
to_jsonb($2::uuid)
|
||||
)
|
||||
WHERE id = $1",
|
||||
job_id,
|
||||
memory_id
|
||||
)
|
||||
@@ -3970,6 +3970,19 @@ async fn set_flow_memory_id(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply flow-specific query parameters after job creation
|
||||
pub async fn process_flow_run_query_params(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
job_id: Uuid,
|
||||
run_query: &RunJobQuery,
|
||||
) -> error::Result<()> {
|
||||
// Set memory_id if provided (for agent memory)
|
||||
if let Some(memory_id) = run_query.memory_id {
|
||||
set_flow_memory_id(tx, job_id, memory_id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_chat_conversation_messages(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
authed: &ApiAuthed,
|
||||
|
||||
@@ -41,11 +41,11 @@ use windmill_audit::ActionKind;
|
||||
use windmill_worker::{process_relative_imports, scoped_dependency_map::ScopedDependencyMap};
|
||||
|
||||
use windmill_common::{
|
||||
assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType},
|
||||
assets::{AssetUsageKind, AssetWithAltAccessType, clear_asset_usage, insert_asset_usage},
|
||||
error::to_anyhow,
|
||||
s3_helpers::upload_artifact_to_store,
|
||||
scripts::hash_script,
|
||||
utils::WarnAfterExt,
|
||||
utils::{WarnAfterExt, paginate_without_limits},
|
||||
worker::{CLOUD_HOSTED, MIN_VERSION_SUPPORTS_DEBOUNCING},
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ use windmill_common::{
|
||||
},
|
||||
users::username_to_permissioned_as,
|
||||
utils::{
|
||||
not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath,
|
||||
not_found_if_none, query_elems_from_hub, require_admin, Pagination, StripPath,
|
||||
},
|
||||
worker::to_raw_value,
|
||||
HUB_BASE_URL,
|
||||
@@ -193,7 +193,7 @@ async fn list_search_scripts(
|
||||
) -> JsonResult<Vec<SearchScript>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
#[cfg(feature = "enterprise")]
|
||||
let n = 1000;
|
||||
let n = 10000;
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
let n = 10;
|
||||
@@ -219,7 +219,7 @@ async fn list_scripts(
|
||||
Query(pagination): Query<Pagination>,
|
||||
Query(lq): Query<ListScriptQuery>,
|
||||
) -> JsonResult<Vec<ListableScript>> {
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
let (per_page, offset) = paginate_without_limits(pagination);
|
||||
let mut sqlb = SqlBuilder::select_from("script as o")
|
||||
.fields(&[
|
||||
"hash",
|
||||
|
||||
@@ -50,7 +50,7 @@ use windmill_common::{
|
||||
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
|
||||
utils::{paginate, rd_string, require_admin, Pagination},
|
||||
};
|
||||
use windmill_git_sync::{handle_fork_branch_creation, handle_deployment_metadata, DeployedObject};
|
||||
use windmill_git_sync::{handle_deployment_metadata, handle_fork_branch_creation, DeployedObject};
|
||||
use windmill_worker::scoped_dependency_map::{DependencyMap, ScopedDependencyMap};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -73,6 +73,7 @@ lazy_static::lazy_static! {
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
let router = Router::new()
|
||||
.route("/get_as_superadmin", get(get_workspace_as_superadmin))
|
||||
.route("/list_pending_invites", get(list_pending_invites))
|
||||
.route("/update", post(edit_workspace))
|
||||
.route("/archive", post(archive_workspace))
|
||||
@@ -163,7 +164,10 @@ pub fn workspaced_service() -> Router {
|
||||
post(acknowledge_all_critical_alerts),
|
||||
)
|
||||
.route("/critical_alerts/mute", post(mute_critical_alerts))
|
||||
.route("/create_workspace_fork_branch", post(create_workspace_fork_branch))
|
||||
.route(
|
||||
"/create_workspace_fork_branch",
|
||||
post(create_workspace_fork_branch),
|
||||
)
|
||||
.route("/operator_settings", post(update_operator_settings));
|
||||
|
||||
#[cfg(all(feature = "stripe", feature = "enterprise"))]
|
||||
@@ -706,7 +710,9 @@ async fn get_slack_oauth_config(
|
||||
.await?;
|
||||
|
||||
// Mask the secret if it exists
|
||||
let masked_secret = settings.slack_oauth_client_secret.map(|_| "***".to_string());
|
||||
let masked_secret = settings
|
||||
.slack_oauth_client_secret
|
||||
.map(|_| "***".to_string());
|
||||
|
||||
Ok(Json(GetSlackOAuthConfigResponse {
|
||||
slack_oauth_client_id: settings.slack_oauth_client_id,
|
||||
@@ -2227,6 +2233,35 @@ async fn get_used_triggers(
|
||||
Ok(Json(websocket_used))
|
||||
}
|
||||
|
||||
async fn get_workspace_as_superadmin(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Workspace> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let workspace = sqlx::query_as!(
|
||||
Workspace,
|
||||
"SELECT
|
||||
workspace.id AS \"id!\",
|
||||
workspace.name AS \"name!\",
|
||||
workspace.owner AS \"owner!\",
|
||||
workspace.deleted AS \"deleted!\",
|
||||
workspace.premium AS \"premium!\",
|
||||
workspace_settings.color AS \"color\",
|
||||
workspace.parent_workspace_id AS \"parent_workspace_id\"
|
||||
FROM workspace
|
||||
LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id
|
||||
WHERE workspace.id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let workspace = not_found_if_none(workspace, "workspace", w_id)?;
|
||||
|
||||
Ok(Json(workspace))
|
||||
}
|
||||
|
||||
async fn list_workspaces_as_super_admin(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -2310,6 +2345,13 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
};
|
||||
|
||||
pub static ref DISABLE_WORKSPACE_FORK: bool = {
|
||||
match std::env::var("DISABLE_WORKSPACE_FORK") {
|
||||
Ok(val) => val == "true",
|
||||
Err(_) => false,
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
async fn create_workspace_require_superadmin() -> String {
|
||||
@@ -2566,9 +2608,14 @@ async fn update_workspace_settings(
|
||||
WorkspaceGitSyncSettings::default()
|
||||
};
|
||||
|
||||
// We only keep the first git sync repo, since it is considered the main one
|
||||
// We only keep the first git sync repo that is sync mode (use_individual_branch = false), since it is considered the main one
|
||||
// Context: see WIN-1559
|
||||
git_sync_settings.repositories.truncate(1);
|
||||
git_sync_settings.repositories = git_sync_settings
|
||||
.repositories
|
||||
.into_iter()
|
||||
.filter(|r| !r.use_individual_branch.unwrap_or(false))
|
||||
.take(1)
|
||||
.collect();
|
||||
|
||||
let serialized_config = serde_json::to_value::<WorkspaceGitSyncSettings>(git_sync_settings)
|
||||
.map_err(|err| Error::internal_err(err.to_string()))?;
|
||||
@@ -2638,6 +2685,17 @@ async fn clone_groups(
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO usr_to_group (workspace_id, group_, usr)
|
||||
SELECT $2, group_, usr
|
||||
FROM usr_to_group
|
||||
WHERE workspace_id = $1",
|
||||
source_workspace_id,
|
||||
target_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3008,6 +3066,10 @@ async fn create_workspace_fork_branch(
|
||||
)));
|
||||
}
|
||||
|
||||
if *DISABLE_WORKSPACE_FORK {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
}
|
||||
|
||||
Ok(Json(
|
||||
handle_fork_branch_creation(&authed.email, &authed.username, &db, &w_id, &nw.id).await?,
|
||||
))
|
||||
@@ -3025,6 +3087,10 @@ async fn create_workspace_fork(
|
||||
)));
|
||||
}
|
||||
|
||||
if *DISABLE_WORKSPACE_FORK {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
}
|
||||
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
// Generate unique forked workspace ID with wm-fork prefix
|
||||
|
||||
@@ -15,6 +15,8 @@ pub enum MessageType {
|
||||
}
|
||||
|
||||
/// Add a message to a conversation using an existing transaction
|
||||
/// If the conversation doesn't exist, logs a warning and returns Ok (no error thrown)
|
||||
/// This allows memory_id to be used for agent memory without requiring a conversation
|
||||
pub async fn add_message_to_conversation_tx(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
conversation_id: Uuid,
|
||||
@@ -24,6 +26,23 @@ pub async fn add_message_to_conversation_tx(
|
||||
step_name: Option<&str>,
|
||||
success: bool,
|
||||
) -> Result<()> {
|
||||
// Check if conversation exists first
|
||||
let conversation_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1) as \"exists!\"",
|
||||
conversation_id
|
||||
)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?
|
||||
.exists;
|
||||
|
||||
if !conversation_exists {
|
||||
tracing::warn!(
|
||||
"Conversation {} does not exist. Skipping message insertion. This is expected when flows are called from apps (memory_id is used for agent memory only).",
|
||||
conversation_id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Insert the message
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id, step_name, success)
|
||||
|
||||
@@ -869,6 +869,8 @@ pub enum FlowModuleValue {
|
||||
parallel: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
parallelism: Option<InputTransform>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
squash: Option<bool>,
|
||||
},
|
||||
|
||||
/// While loop node
|
||||
@@ -878,6 +880,8 @@ pub enum FlowModuleValue {
|
||||
modules_node: Option<FlowNodeId>,
|
||||
#[serde(default = "default_false")]
|
||||
skip_failures: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
squash: Option<bool>,
|
||||
},
|
||||
|
||||
/// Branch-one node
|
||||
@@ -989,6 +993,7 @@ struct UntaggedFlowModuleValue {
|
||||
assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
tools: Option<Vec<AgentTool>>,
|
||||
pass_flow_input_directly: Option<bool>,
|
||||
squash: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
@@ -1027,6 +1032,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
skip_failures: untagged.skip_failures.unwrap_or(true),
|
||||
parallel: untagged.parallel.unwrap_or(false),
|
||||
parallelism: untagged.parallelism,
|
||||
squash: untagged.squash,
|
||||
}),
|
||||
"whileloopflow" => Ok(FlowModuleValue::WhileloopFlow {
|
||||
modules: untagged
|
||||
@@ -1034,6 +1040,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
.ok_or_else(|| serde::de::Error::missing_field("modules"))?,
|
||||
modules_node: untagged.modules_node,
|
||||
skip_failures: untagged.skip_failures.unwrap_or(false),
|
||||
squash: untagged.squash,
|
||||
}),
|
||||
"branchone" => Ok(FlowModuleValue::BranchOne {
|
||||
branches: untagged
|
||||
|
||||
@@ -24,6 +24,9 @@ use serde::{ser::SerializeMap, Serialize};
|
||||
use serde_json::{json, value::RawValue};
|
||||
use sqlx::{types::Json, Pool, Postgres, Transaction};
|
||||
use sqlx::{Encode, PgExecutor};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::{sync::RwLock, time::sleep};
|
||||
use ulid::Ulid;
|
||||
use uuid::Uuid;
|
||||
@@ -138,7 +141,7 @@ pub struct CanceledBy {
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct JobCompleted {
|
||||
pub job: MiniCompletedJob,
|
||||
pub preprocessed_args: Option<HashMap<String, Box<RawValue>>>,
|
||||
@@ -152,6 +155,10 @@ pub struct JobCompleted {
|
||||
pub duration: Option<i64>,
|
||||
pub has_stream: Option<bool>,
|
||||
pub from_cache: Option<bool>,
|
||||
#[serde(skip)]
|
||||
pub flow_runners: Option<Arc<FlowRunners>>,
|
||||
#[serde(skip)]
|
||||
pub done_tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
pub async fn cancel_single_job<'c>(
|
||||
@@ -2272,6 +2279,8 @@ pub struct JobAndPerms {
|
||||
pub parent_runnable_path: Option<String>,
|
||||
pub token: String,
|
||||
pub precomputed_agent_info: Option<PrecomputedAgentInfo>,
|
||||
#[serde(skip)]
|
||||
pub flow_runners: Option<Arc<FlowRunners>>,
|
||||
}
|
||||
impl PulledJob {
|
||||
pub async fn get_job_and_perms(self, db: &DB) -> JobAndPerms {
|
||||
@@ -2303,6 +2312,7 @@ impl PulledJob {
|
||||
parent_runnable_path: self.parent_runnable_path,
|
||||
token,
|
||||
precomputed_agent_info: None,
|
||||
flow_runners: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2493,6 +2503,8 @@ impl PulledJobResult {
|
||||
duration: None,
|
||||
has_stream: Some(false),
|
||||
from_cache: None,
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
}),
|
||||
),
|
||||
PulledJobResult { job: Some(job), error_while_preprocessing: Some(e), .. } => Err(
|
||||
@@ -2512,6 +2524,8 @@ impl PulledJobResult {
|
||||
duration: None,
|
||||
has_stream: Some(false),
|
||||
from_cache: None,
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
}),
|
||||
),
|
||||
PulledJobResult { job, .. } => Ok(job),
|
||||
@@ -5718,10 +5732,74 @@ async fn restarted_flows_resolution(
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Wrapper struct to send both job and optional flow_runners to dedicated workers
|
||||
pub struct DedicatedWorkerJob {
|
||||
pub job: Arc<MiniPulledJob>,
|
||||
pub flow_runners: Option<Arc<FlowRunners>>,
|
||||
pub done_tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FlowRunners {
|
||||
pub runners: HashMap<String, Sender<DedicatedWorkerJob>>,
|
||||
pub handles: Vec<JoinHandle<()>>,
|
||||
pub job_id: Uuid,
|
||||
}
|
||||
|
||||
impl Drop for FlowRunners {
|
||||
fn drop(&mut self) {
|
||||
let total_runners = self.handles.len();
|
||||
tracing::info!("dropping {} flow runners for job {}", total_runners, self.job_id);
|
||||
|
||||
// First, drop all senders to signal workers to stop gracefully
|
||||
self.runners.clear();
|
||||
|
||||
// Spawn a background task to wait with timeout and abort if needed
|
||||
let handles = std::mem::take(&mut self.handles);
|
||||
let job_id = self.job_id;
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Extract abort handles before consuming the join handles
|
||||
let abort_handles: Vec<_> = handles.iter().map(|h| h.abort_handle()).collect();
|
||||
|
||||
// Wait up to 5 seconds for natural termination
|
||||
let timeout_result = tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(5),
|
||||
futures::future::join_all(handles)
|
||||
).await;
|
||||
|
||||
match timeout_result {
|
||||
Ok(_) => {
|
||||
tracing::info!("all {} flow runners for job {} terminated gracefully", total_runners, job_id);
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout reached, abort only the handles that haven't finished
|
||||
let mut aborted = 0;
|
||||
for abort_handle in abort_handles {
|
||||
if !abort_handle.is_finished() {
|
||||
abort_handle.abort();
|
||||
aborted += 1;
|
||||
}
|
||||
}
|
||||
let graceful = total_runners - aborted;
|
||||
tracing::warn!(
|
||||
"flow runners for job {}: {} terminated gracefully, {} aborted after 5s timeout",
|
||||
job_id, graceful, aborted
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SameWorkerPayload {
|
||||
pub job_id: Uuid,
|
||||
pub recoverable: bool,
|
||||
#[serde(skip)]
|
||||
pub flow_runners: Option<Arc<FlowRunners>>,
|
||||
}
|
||||
|
||||
pub async fn get_same_worker_job(
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
mod jobs;
|
||||
pub mod jobs;
|
||||
#[cfg(feature = "private")]
|
||||
pub mod jobs_ee;
|
||||
pub mod jobs_oss;
|
||||
|
||||
@@ -529,6 +529,7 @@ async fn execute_windmill_tool(
|
||||
&mut occupancy_metrics_spawn,
|
||||
&mut killpill_rx_spawn,
|
||||
None,
|
||||
None,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench_spawn,
|
||||
)
|
||||
@@ -656,8 +657,9 @@ async fn handle_tool_execution_success(
|
||||
..
|
||||
}) = send_result.as_ref()
|
||||
{
|
||||
let result = result.clone();
|
||||
ctx.job_completed_tx
|
||||
.send(send_result.as_ref().unwrap().result.clone(), true)
|
||||
.send(send_result.unwrap().result, true)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
result
|
||||
|
||||
@@ -9,10 +9,8 @@ use serde_json::value::RawValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
use windmill_parser_ts::remove_pinned_imports;
|
||||
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob, PrecomputedAgentInfo};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::common::build_envs_map;
|
||||
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob, PrecomputedAgentInfo};
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
@@ -38,12 +36,6 @@ use tokio::{fs::File, process::Command};
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::variables;
|
||||
|
||||
use windmill_common::{
|
||||
error::{self, Result},
|
||||
get_latest_hash_for_path,
|
||||
@@ -1567,10 +1559,18 @@ pub async fn get_common_bun_proc_envs(base_internal_url: Option<&str>) -> HashMa
|
||||
return bun_envs;
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::{dedicated_worker::handle_dedicated_process, JobCompletedSender};
|
||||
#[cfg(feature = "private")]
|
||||
use crate::{
|
||||
common::build_envs_map, dedicated_worker_oss::handle_dedicated_process, JobCompletedSender,
|
||||
};
|
||||
#[cfg(feature = "private")]
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
#[cfg(feature = "private")]
|
||||
use windmill_common::variables;
|
||||
#[cfg(feature = "private")]
|
||||
use windmill_queue::DedicatedWorkerJob;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "private")]
|
||||
pub async fn start_worker(
|
||||
requirements_o: Option<String>,
|
||||
codebase: Option<String>,
|
||||
@@ -1584,8 +1584,9 @@ pub async fn start_worker(
|
||||
script_path: &str,
|
||||
token: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
jobs_rx: Receiver<std::sync::Arc<MiniPulledJob>>,
|
||||
jobs_rx: Receiver<DedicatedWorkerJob>,
|
||||
killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
client: windmill_common::client::AuthedClient,
|
||||
) -> Result<()> {
|
||||
let mut logs = "".to_string();
|
||||
let mut mem_peak: i32 = 0;
|
||||
@@ -1737,6 +1738,12 @@ BigInt.prototype.toJSON = function () {{
|
||||
|
||||
console.log('start');
|
||||
|
||||
function getArgs(line) {{
|
||||
let {{ {spread} }} = JSON.parse(line)
|
||||
{dates}
|
||||
return [ {spread} ];
|
||||
}}
|
||||
|
||||
for await (const line of Readline.createInterface({{ input: process.stdin }})) {{
|
||||
{print_lines}
|
||||
|
||||
@@ -1744,9 +1751,8 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
|
||||
process.exit(0);
|
||||
}}
|
||||
try {{
|
||||
let {{ {spread} }} = JSON.parse(line)
|
||||
{dates}
|
||||
let res = await Main.main(...[ {spread} ]);
|
||||
const args = getArgs(line);
|
||||
const res = await Main.main(...args);
|
||||
console.log("wm_res[success]:" + JSON.stringify(res ?? null, (key, value) => typeof value === 'undefined' ? null : value));
|
||||
}} catch (e) {{
|
||||
console.log("wm_res[error]:" + JSON.stringify({{ message: e.message, name: e.name, stack: e.stack, line: line }}));
|
||||
@@ -1812,6 +1818,7 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
|
||||
db,
|
||||
&script_path,
|
||||
"nodejs",
|
||||
client,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
@@ -1838,6 +1845,7 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
|
||||
db,
|
||||
script_path,
|
||||
"bun",
|
||||
client,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,759 +0,0 @@
|
||||
// use tokio::sync::mpsc;
|
||||
|
||||
// pub fn create_dedicated_worker() {
|
||||
// let (job_completed_tx, mut new_job) = mpsc::channel::<JobCompleted>(100);
|
||||
// }
|
||||
|
||||
use async_recursion::async_recursion;
|
||||
use std::{collections::VecDeque, process::Stdio, sync::Arc};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::Command,
|
||||
task::JoinHandle,
|
||||
};
|
||||
use windmill_common::error::Error;
|
||||
use windmill_common::flows::FlowValue;
|
||||
use windmill_common::worker::WORKER_CONFIG;
|
||||
use windmill_common::KillpillSender;
|
||||
use windmill_common::{
|
||||
cache, error,
|
||||
flows::{FlowModule, FlowModuleValue},
|
||||
scripts::{ScriptHash, ScriptLang},
|
||||
variables,
|
||||
worker::to_raw_value,
|
||||
DB,
|
||||
};
|
||||
use windmill_queue::append_logs;
|
||||
use windmill_queue::MiniPulledJob;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::{common::start_child_process, JobCompletedSender, MAX_BUFFERED_DEDICATED_JOBS};
|
||||
|
||||
use futures::{future, Future};
|
||||
use std::{collections::HashMap, task::Poll};
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
|
||||
fn conditional_polling<T>(
|
||||
fut: impl Future<Output = T>,
|
||||
predicate: bool,
|
||||
) -> impl Future<Output = T> {
|
||||
let mut fut = Box::pin(fut);
|
||||
future::poll_fn(move |cx| {
|
||||
if predicate {
|
||||
fut.as_mut().poll(cx)
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_stdin(stdin: &mut tokio::process::ChildStdin, s: &str) -> error::Result<()> {
|
||||
let _ = &stdin.write_all(format!("{s}\n").as_bytes()).await?;
|
||||
stdin.flush().await.context("stdin flush")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn handle_dedicated_process(
|
||||
command_path: &String,
|
||||
job_dir: &str,
|
||||
context_envs: HashMap<String, String>,
|
||||
envs: HashMap<String, String>,
|
||||
reserved_variables: Vec<variables::ContextualVariable>,
|
||||
common_bun_proc_envs: HashMap<String, String>,
|
||||
args: Vec<&str>,
|
||||
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
token: &str,
|
||||
mut jobs_rx: Receiver<std::sync::Arc<MiniPulledJob>>,
|
||||
worker_name: &str,
|
||||
db: &DB,
|
||||
script_path: &str,
|
||||
mode: &str,
|
||||
) -> std::result::Result<(), error::Error> {
|
||||
//do not cache local dependencies
|
||||
|
||||
use windmill_queue::{JobCompleted, MiniCompletedJob};
|
||||
|
||||
use crate::{handle_child::process_status, PROXY_ENVS};
|
||||
let cmd_name = format!("dedicated {command_path}");
|
||||
let mut child = {
|
||||
let mut cmd = Command::new(command_path);
|
||||
cmd.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(context_envs)
|
||||
.envs(envs)
|
||||
.envs(PROXY_ENVS.clone())
|
||||
.envs(
|
||||
reserved_variables
|
||||
.iter()
|
||||
.map(|x| (x.name.clone(), x.value.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.envs(common_bun_proc_envs)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
start_child_process(cmd, command_path, false).await?
|
||||
};
|
||||
|
||||
let stdout = child
|
||||
.stdout()
|
||||
.take()
|
||||
.expect("child did not have a handle to stdout");
|
||||
|
||||
let stderr = child
|
||||
.stderr()
|
||||
.take()
|
||||
.expect("child did not have a handle to stderr");
|
||||
|
||||
let mut reader = BufReader::new(stdout).lines();
|
||||
|
||||
let mut err_reader = BufReader::new(stderr).lines();
|
||||
|
||||
let mut stdin = child
|
||||
.stdin()
|
||||
.take()
|
||||
.expect("child did not have a handle to stdin");
|
||||
|
||||
// Ensure the child process is spawned in the runtime so it can
|
||||
// make progress on its own while we await for any output.
|
||||
let child = tokio::spawn(async move {
|
||||
let status = Box::into_pin(child.wait())
|
||||
.await
|
||||
.expect("child process encountered an error");
|
||||
if let Err(e) = process_status(&cmd_name, status, vec![]) {
|
||||
tracing::error!("child exit status was not success: {e:#}");
|
||||
} else {
|
||||
tracing::info!("child exit status was success");
|
||||
}
|
||||
});
|
||||
|
||||
let mut jobs: VecDeque<MiniCompletedJob> = VecDeque::with_capacity(MAX_BUFFERED_DEDICATED_JOBS);
|
||||
// let mut i = 0;
|
||||
// let mut j = 0;
|
||||
let mut alive = true;
|
||||
|
||||
let init_log = format!("dedicated worker {mode}: {worker_name}\n\n");
|
||||
let mut logs = init_log.clone();
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv(), if alive => {
|
||||
println!("received killpill for dedicated worker");
|
||||
alive = false;
|
||||
if let Err(e) = write_stdin(&mut stdin, "end").await {
|
||||
tracing::info!("Could not write end message to stdin: {e:?}")
|
||||
}
|
||||
stdin.flush().await.context("stdin flush")?;
|
||||
},
|
||||
line = err_reader.next_line() => {
|
||||
if let Some(line) = line.expect("line is ok") {
|
||||
tracing::error!("stderr dedicated worker: {line}");
|
||||
logs.push_str("[stderr] ");
|
||||
logs.push_str(&line);
|
||||
logs.push_str("\n");
|
||||
} else {
|
||||
tracing::info!("dedicated worker process exited {script_path}");
|
||||
let mut last_stdout = "".to_string();
|
||||
while let Some(line) = reader.next_line().await.ok().flatten() {
|
||||
last_stdout = line;
|
||||
last_stdout.push_str("\n");
|
||||
}
|
||||
tracing::info!("Last stdout for {script_path}: {last_stdout}");
|
||||
break;
|
||||
}
|
||||
},
|
||||
line = reader.next_line() => {
|
||||
// j += 1;
|
||||
|
||||
if let Some(line) = line.expect("line is ok") {
|
||||
if line == "start" {
|
||||
tracing::info!("dedicated worker process started {script_path}");
|
||||
continue;
|
||||
}
|
||||
tracing::debug!("processed job: |{line}|");
|
||||
if line.starts_with("wm_res[") {
|
||||
let job = jobs.pop_front().expect("pop");
|
||||
tracing::info!("job completed on dedicated worker {script_path}: {}", job.id);
|
||||
match serde_json::from_str::<Box<serde_json::value::RawValue>>(&line.replace("wm_res[success]:", "").replace("wm_res[error]:", "")) {
|
||||
Ok(result) => {
|
||||
let result = Arc::new(result);
|
||||
append_logs(&job.id, &job.workspace_id, logs.clone(), &db.into()).await;
|
||||
if line.starts_with("wm_res[success]:") {
|
||||
job_completed_tx.send_job(JobCompleted { job , result, result_columns: None, mem_peak: 0, canceled_by: None, success: true, cached_res_path: None, token: token.to_string(), duration: None, preprocessed_args: None, has_stream: Some(false), from_cache: None }, true).await.unwrap()
|
||||
} else {
|
||||
job_completed_tx.send_job(JobCompleted { job , result, result_columns: None, mem_peak: 0, canceled_by: None, success: false, cached_res_path: None, token: token.to_string(), duration: None, preprocessed_args: None, has_stream: Some(false), from_cache: None }, true).await.unwrap()
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Could not deserialize job result `{line}`: {e:?}");
|
||||
job_completed_tx.send_job(JobCompleted { job , result: Arc::new(to_raw_value(&serde_json::json!({"error": format!("Could not deserialize job result `{line}`: {e:?}")}))), result_columns: None, mem_peak: 0, canceled_by: None, success: false, cached_res_path: None, token: token.to_string(), duration: None, preprocessed_args: None, has_stream: Some(false), from_cache: None }, true).await.unwrap();
|
||||
},
|
||||
};
|
||||
logs = init_log.clone();
|
||||
} else {
|
||||
logs.push_str(&line);
|
||||
logs.push_str("\n");
|
||||
}
|
||||
} else {
|
||||
tracing::info!("dedicated worker {script_path} process exited");
|
||||
let mut last_stderr = "".to_string();
|
||||
while let Some(line) = err_reader.next_line().await.ok().flatten() {
|
||||
last_stderr = line;
|
||||
last_stderr.push_str("\n");
|
||||
}
|
||||
tracing::info!("Last stderr for {script_path}: {last_stderr}");
|
||||
break;
|
||||
}
|
||||
},
|
||||
job = conditional_polling(jobs_rx.recv(), alive && jobs.len() < MAX_BUFFERED_DEDICATED_JOBS) => {
|
||||
// i += 1;
|
||||
if let Some(job) = job {
|
||||
let id = job.id;
|
||||
let args = serde_json::to_string(&job.args).expect("serialize");
|
||||
jobs.push_back(MiniCompletedJob::from(job));
|
||||
tracing::info!("received job and adding to queue on dedicated worker for {script_path}: {} (queue_size: {})", id, jobs.len());
|
||||
|
||||
// write_stdin(&mut stdin, &serde_json::to_string(&job.args.unwrap_or_else(|| serde_json::json!({"x": job.id}))).expect("serialize")).await?;
|
||||
write_stdin(&mut stdin, &args).await?;
|
||||
stdin.flush().await.context("stdin flush")?;
|
||||
// tracing::info!("wrote job to stdin for {script_path}: {} (queue_size: {})", id, jobs.len());
|
||||
} else {
|
||||
tracing::debug!("job channel closed");
|
||||
alive = false;
|
||||
if let Err(e) = write_stdin(&mut stdin, "end").await {
|
||||
tracing::error!("Could not write end message to stdin: {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("child process {script_path} encountered an error: {e:#}"))?;
|
||||
|
||||
tracing::info!("dedicated worker {script_path} child process exited successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type DedicatedWorker = (
|
||||
String,
|
||||
Sender<std::sync::Arc<MiniPulledJob>>,
|
||||
Option<JoinHandle<()>>,
|
||||
);
|
||||
|
||||
// spawn one dedicated worker per compatible steps of the flow, associating the node id to the dedicated worker channel send
|
||||
#[async_recursion]
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn spawn_dedicated_workers_for_flow(
|
||||
modules: &Vec<FlowModule>,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
killpill_tx: KillpillSender,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
db: &DB,
|
||||
worker_dir: &str,
|
||||
base_internal_url: &str,
|
||||
worker_name: &str,
|
||||
job_completed_tx: &JobCompletedSender,
|
||||
) -> Vec<DedicatedWorker> {
|
||||
let mut workers = vec![];
|
||||
let mut script_path_to_worker: HashMap<String, Sender<std::sync::Arc<MiniPulledJob>>> =
|
||||
HashMap::new();
|
||||
for module in modules.iter() {
|
||||
let value = module.get_value();
|
||||
if let Ok(value) = value {
|
||||
match &value {
|
||||
FlowModuleValue::Script { path, hash, .. } => {
|
||||
let key = format!(
|
||||
"{}:{}",
|
||||
path,
|
||||
hash.clone()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
);
|
||||
if let Some(sender) = script_path_to_worker.get(&key) {
|
||||
workers.push((module.id.clone(), sender.clone(), None));
|
||||
} else {
|
||||
if let Some(dedi_w) = spawn_dedicated_worker(
|
||||
SpawnWorker::Script { path: path.to_string(), hash: hash.clone() },
|
||||
w_id,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
Some(module.id.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
script_path_to_worker.insert(key, dedi_w.1.clone());
|
||||
workers.push(dedi_w);
|
||||
}
|
||||
}
|
||||
}
|
||||
FlowModuleValue::ForloopFlow { modules, .. } => {
|
||||
let w = spawn_dedicated_workers_for_flow(
|
||||
&modules,
|
||||
w_id,
|
||||
path,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await;
|
||||
workers.extend(w);
|
||||
}
|
||||
FlowModuleValue::WhileloopFlow { modules, .. } => {
|
||||
let w = spawn_dedicated_workers_for_flow(
|
||||
&modules,
|
||||
w_id,
|
||||
path,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await;
|
||||
workers.extend(w);
|
||||
}
|
||||
FlowModuleValue::BranchOne { branches, default, .. } => {
|
||||
for modules in branches
|
||||
.iter()
|
||||
.map(|x| &x.modules)
|
||||
.chain(std::iter::once(default))
|
||||
{
|
||||
let w = spawn_dedicated_workers_for_flow(
|
||||
&modules,
|
||||
w_id,
|
||||
path,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await;
|
||||
workers.extend(w);
|
||||
}
|
||||
}
|
||||
FlowModuleValue::BranchAll { branches, .. } => {
|
||||
for branch in branches {
|
||||
let w = spawn_dedicated_workers_for_flow(
|
||||
&branch.modules,
|
||||
w_id,
|
||||
path,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await;
|
||||
workers.extend(w);
|
||||
}
|
||||
}
|
||||
FlowModuleValue::RawScript { content, lock, path: spath, language, .. } => {
|
||||
if let Some(dedi_w) = spawn_dedicated_worker(
|
||||
SpawnWorker::RawScript {
|
||||
path: spath.clone().unwrap_or(path.to_string()),
|
||||
content: content.to_string(),
|
||||
lock: lock.clone(),
|
||||
lang: language.clone(),
|
||||
},
|
||||
w_id,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
Some(module.id.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
workers.push(dedi_w);
|
||||
}
|
||||
}
|
||||
FlowModuleValue::FlowScript { id, language, .. } => {
|
||||
let spawn = cache::flow::fetch_script(
|
||||
&windmill_common::worker::Connection::Sql(db.clone()),
|
||||
*id,
|
||||
)
|
||||
.await
|
||||
.map(|data| SpawnWorker::RawScript {
|
||||
path: "".to_string(),
|
||||
content: data.code.clone(),
|
||||
lock: data.lock.clone(),
|
||||
lang: *language,
|
||||
});
|
||||
match spawn {
|
||||
Ok(spawn) => {
|
||||
if let Some(dedi_w) = spawn_dedicated_worker(
|
||||
spawn,
|
||||
w_id,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
Some(module.id.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
workers.push(dedi_w);
|
||||
}
|
||||
}
|
||||
Err(err) => tracing::error!(
|
||||
"failed to get script for module: {:?}, err: {:?}",
|
||||
module,
|
||||
err
|
||||
),
|
||||
}
|
||||
}
|
||||
FlowModuleValue::Flow { .. } => (),
|
||||
FlowModuleValue::Identity => (),
|
||||
FlowModuleValue::AIAgent { .. } => (),
|
||||
}
|
||||
} else {
|
||||
tracing::error!("failed to get value for module: {:?}", module);
|
||||
}
|
||||
}
|
||||
workers
|
||||
}
|
||||
|
||||
pub async fn create_dedicated_worker_map(
|
||||
killpill_tx: &KillpillSender,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
db: &DB,
|
||||
worker_dir: &str,
|
||||
base_internal_url: &str,
|
||||
worker_name: &str,
|
||||
job_completed_tx: &JobCompletedSender,
|
||||
) -> (
|
||||
HashMap<String, Sender<std::sync::Arc<MiniPulledJob>>>,
|
||||
bool,
|
||||
Vec<JoinHandle<()>>,
|
||||
) {
|
||||
let mut dedicated_handles = vec![];
|
||||
if let Some(_wp) = WORKER_CONFIG.read().await.dedicated_worker.clone() {
|
||||
let mut hm = HashMap::new();
|
||||
let is_flow_worker;
|
||||
if let Some(flow_path) = _wp.path.strip_prefix("flow/") {
|
||||
is_flow_worker = true;
|
||||
let value = sqlx::query_scalar!(
|
||||
"SELECT flow_version.value AS \"value!: sqlx::types::Json<Box<sqlx::types::JsonRawValue>>\"
|
||||
FROM flow
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2",
|
||||
flow_path,
|
||||
_wp.workspace_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await;
|
||||
if let Ok(v) = value {
|
||||
if let Some(v) = v {
|
||||
let value = serde_json::from_str::<FlowValue>(v.get()).map_err(|err| {
|
||||
Error::internal_err(format!(
|
||||
"could not convert json to flow for {flow_path}: {err:?}"
|
||||
))
|
||||
});
|
||||
if let Ok(flow) = value {
|
||||
let workers = spawn_dedicated_workers_for_flow(
|
||||
&flow.modules,
|
||||
&_wp.workspace_id,
|
||||
&_wp.path,
|
||||
killpill_tx.clone(),
|
||||
&killpill_rx,
|
||||
db,
|
||||
&worker_dir,
|
||||
base_internal_url,
|
||||
&worker_name,
|
||||
&job_completed_tx,
|
||||
)
|
||||
.await;
|
||||
workers.into_iter().for_each(|(path, sender, handle)| {
|
||||
tracing::info!("spawned dedicated worker for flow: {}", path.as_str());
|
||||
if let Some(h) = handle {
|
||||
dedicated_handles.push(h);
|
||||
}
|
||||
hm.insert(path, sender);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
tracing::error!(
|
||||
"flow present but value not found for dedicated worker. {}",
|
||||
flow_path
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::error!("flow not found for dedicated worker: {}. Waiting for dependency job and expected to restart.", flow_path);
|
||||
}
|
||||
} else {
|
||||
is_flow_worker = false;
|
||||
if let Some((path, sender, handle)) = spawn_dedicated_worker(
|
||||
SpawnWorker::Script { path: _wp.path.clone(), hash: None },
|
||||
&_wp.workspace_id,
|
||||
killpill_tx.clone(),
|
||||
&killpill_rx,
|
||||
db,
|
||||
&worker_dir,
|
||||
base_internal_url,
|
||||
&worker_name,
|
||||
&job_completed_tx,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(h) = handle {
|
||||
dedicated_handles.push(h);
|
||||
}
|
||||
hm.insert(path, sender);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"failed to spawn dedicated worker for {}, script not found",
|
||||
_wp.path
|
||||
);
|
||||
}
|
||||
}
|
||||
(hm, is_flow_worker, dedicated_handles)
|
||||
} else {
|
||||
(HashMap::new(), false, dedicated_handles)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SpawnWorker {
|
||||
Script { path: String, hash: Option<ScriptHash> },
|
||||
RawScript { path: String, content: String, lock: Option<String>, lang: ScriptLang },
|
||||
}
|
||||
|
||||
// spawn one dedicated worker and return the key, the channel sender and the join handle
|
||||
// note that for it will return none for language that do not support dedicated workers
|
||||
// note that go using cache binary does not need dedicated workers so all languages are supported
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn spawn_dedicated_worker(
|
||||
sw: SpawnWorker,
|
||||
w_id: &str,
|
||||
killpill_tx: KillpillSender,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
db: &DB,
|
||||
worker_dir: &str,
|
||||
base_internal_url: &str,
|
||||
worker_name: &str,
|
||||
job_completed_tx: &JobCompletedSender,
|
||||
node_id: Option<String>,
|
||||
) -> Option<DedicatedWorker> {
|
||||
use windmill_common::{
|
||||
error::Error,
|
||||
scripts::{ScriptHash, ScriptLang},
|
||||
utils::rd_string,
|
||||
};
|
||||
use windmill_queue::MiniPulledJob;
|
||||
|
||||
use crate::{build_envs, get_script_content_by_hash, ContentReqLangEnvs};
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
tracing::error!("Dedicated worker is an enterprise feature");
|
||||
killpill_tx.send(()).expect("send");
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
let (dedicated_worker_tx, dedicated_worker_rx) = tokio::sync::mpsc::channel::<
|
||||
std::sync::Arc<MiniPulledJob>,
|
||||
>(MAX_BUFFERED_DEDICATED_JOBS);
|
||||
let killpill_rx = killpill_rx.resubscribe();
|
||||
let db2 = db.clone();
|
||||
let base_internal_url = base_internal_url.to_string();
|
||||
let worker_name = worker_name.to_string();
|
||||
let job_completed_tx = job_completed_tx.clone();
|
||||
let job_dir = format!(
|
||||
"{}/dedicated{}",
|
||||
worker_dir,
|
||||
node_id
|
||||
.as_ref()
|
||||
.map(|x| format!("-{x}"))
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
);
|
||||
tokio::fs::create_dir_all(&job_dir)
|
||||
.await
|
||||
.expect("create dir");
|
||||
|
||||
let path = match &sw {
|
||||
SpawnWorker::RawScript { path, .. } => path.to_string(),
|
||||
SpawnWorker::Script { path, .. } => path.to_string(),
|
||||
};
|
||||
|
||||
let path2 = path.clone();
|
||||
let w_id = w_id.to_string();
|
||||
|
||||
let (content, lock, language, envs, codebase) = match sw.clone() {
|
||||
SpawnWorker::Script { path, hash } => {
|
||||
let q = if let Some(hash) = hash {
|
||||
get_script_content_by_hash(&hash, &w_id, &db2.into())
|
||||
.await
|
||||
.map(|r: ContentReqLangEnvs| {
|
||||
Some((r.content, r.lockfile, r.language, r.envs, r.codebase))
|
||||
})
|
||||
} else {
|
||||
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>, bool, Option<ScriptHash>)>(
|
||||
"SELECT content, lock, language, envs, codebase IS NOT NULL, hash FROM script WHERE path = $1 AND workspace_id = $2 AND
|
||||
archived = false AND lock IS not NULL AND lock_error_logs IS NULL ORDER BY created_at DESC LIMIT 1",
|
||||
)
|
||||
.bind(&path)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&db2)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("expected content and lock: {e:#}")))
|
||||
.map(|x| x.map(|y| (y.0, y.1, y.2, y.3, if y.4 { y.5.map(|z| z.to_string()) } else { None })))
|
||||
};
|
||||
if let Ok(q) = q {
|
||||
if let Some(wp) = q {
|
||||
wp
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to fetch script `{}` in workspace {} for dedicated worker.",
|
||||
path,
|
||||
w_id
|
||||
);
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Failed to fetch script for dedicated worker");
|
||||
killpill_tx.send();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
SpawnWorker::RawScript { content, lock, lang, .. } => {
|
||||
(content, lock, Some(lang), None, None)
|
||||
}
|
||||
};
|
||||
|
||||
match language {
|
||||
Some(ScriptLang::Python3) | Some(ScriptLang::Bun) | Some(ScriptLang::Deno) => {}
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
let db = db.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let token = {
|
||||
let token = rd_string(32);
|
||||
if let Err(e) = sqlx::query_scalar!(
|
||||
"INSERT INTO token
|
||||
(token, label, super_admin, email)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
token,
|
||||
"dedicated_worker",
|
||||
true,
|
||||
"dedicated_worker@windmill.dev"
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("failed to create token for dedicated worker: {:?}", e);
|
||||
killpill_tx.clone().send();
|
||||
};
|
||||
token
|
||||
};
|
||||
|
||||
let worker_envs = build_envs(envs.as_ref()).expect("failed to build envs");
|
||||
|
||||
if let Err(e) = match language {
|
||||
Some(ScriptLang::Python3) => {
|
||||
#[cfg(not(feature = "python"))]
|
||||
{
|
||||
tracing::error!("Python requires the python feature to be enabled");
|
||||
killpill_tx.send();
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
crate::python_executor::start_worker(
|
||||
lock.as_ref(),
|
||||
&db,
|
||||
&content,
|
||||
&base_internal_url,
|
||||
&job_dir,
|
||||
&worker_name,
|
||||
worker_envs,
|
||||
&w_id,
|
||||
&path,
|
||||
&token,
|
||||
job_completed_tx,
|
||||
dedicated_worker_rx,
|
||||
killpill_rx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(ScriptLang::Bun) => {
|
||||
crate::bun_executor::start_worker(
|
||||
lock,
|
||||
codebase,
|
||||
&db,
|
||||
&content,
|
||||
&base_internal_url,
|
||||
&job_dir,
|
||||
&worker_name,
|
||||
worker_envs,
|
||||
&w_id,
|
||||
&path,
|
||||
&token,
|
||||
job_completed_tx,
|
||||
dedicated_worker_rx,
|
||||
killpill_rx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(ScriptLang::Deno) => {
|
||||
crate::deno_executor::start_worker(
|
||||
&content,
|
||||
&base_internal_url,
|
||||
&job_dir,
|
||||
&worker_name,
|
||||
worker_envs,
|
||||
&w_id,
|
||||
&path,
|
||||
&token,
|
||||
job_completed_tx,
|
||||
dedicated_worker_rx,
|
||||
killpill_rx,
|
||||
&db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => unreachable!("Non supported language for dedicated worker"),
|
||||
} {
|
||||
tracing::error!("error in dedicated worker for {sw:#?}: {:?}", e);
|
||||
};
|
||||
killpill_tx.clone().send();
|
||||
});
|
||||
return Some((node_id.unwrap_or(path2), dedicated_worker_tx, Some(handle)));
|
||||
// (Some(dedi_path), Some(dedicated_worker_tx), Some(handle))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#[cfg(feature = "private")]
|
||||
// #[allow(unused)]
|
||||
pub use crate::dedicated_worker_ee::*;
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub async fn spawn_flow_module_runners(
|
||||
_job: &windmill_queue::MiniPulledJob,
|
||||
_module: &windmill_common::flows::FlowModule,
|
||||
_failure_module: Option<&Box<windmill_common::flows::FlowModule>>,
|
||||
_killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
_db: &windmill_common::db::DB,
|
||||
_worker_dir: &str,
|
||||
_base_internal_url: &str,
|
||||
_worker_name: &str,
|
||||
_job_completed_tx: &crate::JobCompletedSender,
|
||||
) -> windmill_common::error::Result<(
|
||||
std::collections::HashMap<
|
||||
String,
|
||||
tokio::sync::mpsc::Sender<windmill_queue::DedicatedWorkerJob>,
|
||||
>,
|
||||
Vec<tokio::task::JoinHandle<()>>,
|
||||
)> {
|
||||
Err(windmill_common::error::Error::internal_err(
|
||||
"Squashing flow loops is not available in OSS".to_string(),
|
||||
))
|
||||
}
|
||||
@@ -514,13 +514,14 @@ async fn build_import_map(
|
||||
Ok(()) as error::Result<()>
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::{dedicated_worker::handle_dedicated_process, JobCompletedSender};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "private")]
|
||||
use crate::{dedicated_worker_oss::handle_dedicated_process, JobCompletedSender};
|
||||
#[cfg(feature = "private")]
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
#[cfg(feature = "private")]
|
||||
use windmill_queue::DedicatedWorkerJob;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "private")]
|
||||
pub async fn start_worker(
|
||||
inner_content: &str,
|
||||
base_internal_url: &str,
|
||||
@@ -531,9 +532,10 @@ pub async fn start_worker(
|
||||
script_path: &str,
|
||||
token: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
jobs_rx: Receiver<std::sync::Arc<MiniPulledJob>>,
|
||||
jobs_rx: Receiver<DedicatedWorkerJob>,
|
||||
killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
client: windmill_common::client::AuthedClient,
|
||||
) -> Result<()> {
|
||||
use windmill_common::variables;
|
||||
|
||||
@@ -654,6 +656,7 @@ for await (const chunk of Deno.stdin.readable) {{
|
||||
db,
|
||||
script_path,
|
||||
"deno",
|
||||
client,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ mod bun_executor;
|
||||
pub mod common;
|
||||
mod config;
|
||||
mod csharp_executor;
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod dedicated_worker;
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
mod dedicated_worker_ee;
|
||||
mod dedicated_worker_oss;
|
||||
mod deno_executor;
|
||||
#[cfg(feature = "duckdb")]
|
||||
mod duckdb_executor;
|
||||
|
||||
@@ -18,6 +18,8 @@ use tokio::{
|
||||
sync::Semaphore,
|
||||
task,
|
||||
};
|
||||
use windmill_queue::MiniPulledJob;
|
||||
|
||||
use uuid::Uuid;
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
use windmill_common::ee_oss::{get_license_plan, LicensePlan};
|
||||
@@ -546,7 +548,6 @@ pub async fn handle_python_job(
|
||||
precomputed_agent_info: Option<PrecomputedAgentInfo>,
|
||||
has_stream: &mut bool,
|
||||
) -> windmill_common::error::Result<Box<RawValue>> {
|
||||
|
||||
let script_path = crate::common::use_flow_root_path(job.runnable_path());
|
||||
|
||||
let annotations = PythonAnnotations::parse(inner_content);
|
||||
@@ -2130,16 +2131,16 @@ fn get_result_postprocessor<'a>(skip: bool) -> &'a str {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "private")]
|
||||
use crate::JobCompletedSender;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::{common::build_envs_map, dedicated_worker::handle_dedicated_process};
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "private")]
|
||||
use crate::{common::build_envs_map, dedicated_worker_oss::handle_dedicated_process};
|
||||
#[cfg(feature = "private")]
|
||||
use windmill_common::variables;
|
||||
#[cfg(feature = "private")]
|
||||
use windmill_queue::DedicatedWorkerJob;
|
||||
|
||||
use windmill_queue::MiniPulledJob;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "private")]
|
||||
pub async fn start_worker(
|
||||
requirements_o: Option<&String>,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
@@ -2152,10 +2153,12 @@ pub async fn start_worker(
|
||||
script_path: &str,
|
||||
token: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
jobs_rx: tokio::sync::mpsc::Receiver<std::sync::Arc<MiniPulledJob>>,
|
||||
jobs_rx: tokio::sync::mpsc::Receiver<DedicatedWorkerJob>,
|
||||
killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
client: windmill_common::client::AuthedClient,
|
||||
) -> error::Result<()> {
|
||||
use crate::{PyV, PyVAlias};
|
||||
tracing::info!("script path: {}", script_path);
|
||||
|
||||
let mut mem_peak: i32 = 0;
|
||||
let mut canceled_by: Option<CanceledBy> = None;
|
||||
@@ -2346,6 +2349,7 @@ for line in sys.stdin:
|
||||
db,
|
||||
script_path,
|
||||
"python",
|
||||
client,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ use windmill_common::{
|
||||
use windmill_common::bench::{BenchmarkInfo, BenchmarkIter};
|
||||
|
||||
use windmill_queue::{
|
||||
append_logs, get_mini_completed_job, CanceledBy, JobCompleted, MiniCompletedJob, MiniPulledJob,
|
||||
ValidableJson, WrappedError, INIT_SCRIPT_TAG,
|
||||
append_logs, get_mini_completed_job, CanceledBy, FlowRunners, JobCompleted, MiniCompletedJob,
|
||||
MiniPulledJob, ValidableJson, WrappedError, INIT_SCRIPT_TAG,
|
||||
};
|
||||
|
||||
use serde_json::{json, value::RawValue, Value};
|
||||
@@ -66,6 +66,7 @@ async fn process_jc(
|
||||
same_worker_tx: Option<&SameWorkerSender>,
|
||||
job_completed_sender: &JobCompletedSender,
|
||||
stats_map: &JobStatsMap,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) {
|
||||
let success: bool = jc.success;
|
||||
@@ -155,6 +156,7 @@ async fn process_jc(
|
||||
same_worker_tx,
|
||||
&worker_name,
|
||||
job_completed_sender.clone(),
|
||||
killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
@@ -285,6 +287,7 @@ pub fn start_background_processor(
|
||||
Some(&same_worker_tx),
|
||||
&job_completed_sender,
|
||||
&stats_map,
|
||||
&killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench,
|
||||
)
|
||||
@@ -350,6 +353,8 @@ pub fn start_background_processor(
|
||||
stop_early_override,
|
||||
&worker_name,
|
||||
job_completed_sender.clone(),
|
||||
None,
|
||||
&killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench,
|
||||
)
|
||||
@@ -413,6 +418,7 @@ pub async fn process_result(
|
||||
conn: &Connection,
|
||||
duration: Option<i64>,
|
||||
has_stream: bool,
|
||||
flow_runners: Option<Arc<FlowRunners>>,
|
||||
) -> error::Result<bool> {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
@@ -431,6 +437,8 @@ pub async fn process_result(
|
||||
duration,
|
||||
has_stream: Some(has_stream),
|
||||
from_cache: None,
|
||||
flow_runners,
|
||||
done_tx: None,
|
||||
},
|
||||
)
|
||||
.with_context(windmill_common::otel_oss::otel_ctx())
|
||||
@@ -495,6 +503,8 @@ pub async fn process_result(
|
||||
duration,
|
||||
has_stream: Some(has_stream),
|
||||
from_cache: None,
|
||||
flow_runners,
|
||||
done_tx: None,
|
||||
},
|
||||
)
|
||||
.with_context(windmill_common::otel_oss::otel_ctx())
|
||||
@@ -512,6 +522,7 @@ pub async fn handle_receive_completed_job(
|
||||
same_worker_tx: Option<&SameWorkerSender>,
|
||||
worker_name: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) -> Option<Arc<MiniPulledJob>> {
|
||||
let token = jc.token.clone();
|
||||
@@ -529,6 +540,7 @@ pub async fn handle_receive_completed_job(
|
||||
same_worker_tx.clone(),
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
@@ -548,6 +560,7 @@ pub async fn handle_receive_completed_job(
|
||||
&worker_dir,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
@@ -571,6 +584,8 @@ pub async fn process_completed_job(
|
||||
preprocessed_args,
|
||||
has_stream,
|
||||
from_cache,
|
||||
flow_runners,
|
||||
done_tx,
|
||||
..
|
||||
}: JobCompleted,
|
||||
client: &AuthedClient,
|
||||
@@ -579,6 +594,7 @@ pub async fn process_completed_job(
|
||||
same_worker_tx: Option<&SameWorkerSender>,
|
||||
worker_name: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) -> error::Result<Option<Arc<MiniPulledJob>>> {
|
||||
if success {
|
||||
@@ -661,12 +677,19 @@ pub async fn process_completed_job(
|
||||
None,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
flow_runners,
|
||||
killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
add_time!(bench, "updated flow status END");
|
||||
if let Some(done_tx) = done_tx {
|
||||
done_tx
|
||||
.send(())
|
||||
.expect("done receiver should still be alive");
|
||||
}
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
@@ -707,11 +730,18 @@ pub async fn process_completed_job(
|
||||
None,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
flow_runners,
|
||||
killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
if let Some(done_tx) = done_tx {
|
||||
done_tx
|
||||
.send(())
|
||||
.expect("done receiver should still be alive");
|
||||
}
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
@@ -761,6 +791,7 @@ pub async fn handle_job_error(
|
||||
worker_dir: &str,
|
||||
worker_name: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) {
|
||||
let err_string = format!("{}: {}", err.name(), err.to_string());
|
||||
@@ -809,6 +840,8 @@ pub async fn handle_job_error(
|
||||
None,
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
None,
|
||||
killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
use anyhow::anyhow;
|
||||
use futures::TryFutureExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::timeout;
|
||||
use windmill_common::client::AuthedClient;
|
||||
@@ -59,6 +60,8 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use windmill_parser::MainArgSignature;
|
||||
use windmill_queue::DedicatedWorkerJob;
|
||||
use windmill_queue::FlowRunners;
|
||||
use windmill_queue::MiniCompletedJob;
|
||||
use windmill_queue::PulledJobResultToJobErr;
|
||||
|
||||
@@ -167,8 +170,8 @@ use crate::duckdb_executor::do_duckdb;
|
||||
#[cfg(all(feature = "enterprise", feature = "oracledb"))]
|
||||
use crate::oracledb_executor::do_oracledb;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::dedicated_worker::create_dedicated_worker_map;
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
use crate::dedicated_worker_oss::create_dedicated_worker_map;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::snowflake_executor::do_snowflake;
|
||||
@@ -532,6 +535,8 @@ lazy_static::lazy_static! {
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<i64>().ok())
|
||||
.unwrap_or(1000);
|
||||
|
||||
pub static ref FLOW_RUNNER_RUNNING: Mutex<bool> = Mutex::new(false);
|
||||
}
|
||||
|
||||
type Envs = Vec<(String, String)>;
|
||||
@@ -558,14 +563,14 @@ lazy_static::lazy_static! {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NextJob {
|
||||
Sql(PulledJob),
|
||||
Sql { job: PulledJob, flow_runners: Option<Arc<FlowRunners>> },
|
||||
Http(JobAndPerms),
|
||||
}
|
||||
|
||||
impl NextJob {
|
||||
pub fn job(self) -> MiniPulledJob {
|
||||
match self {
|
||||
NextJob::Sql(job) => job.job,
|
||||
NextJob::Sql { job, .. } => job.job,
|
||||
NextJob::Http(job) => job.job,
|
||||
}
|
||||
}
|
||||
@@ -575,7 +580,7 @@ impl std::ops::Deref for NextJob {
|
||||
type Target = MiniPulledJob;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
NextJob::Sql(job) => &job.job,
|
||||
NextJob::Sql { job, .. } => &job.job,
|
||||
NextJob::Http(job) => &job.job,
|
||||
}
|
||||
}
|
||||
@@ -733,10 +738,10 @@ impl JobCompletedSender {
|
||||
impl SameWorkerSender {
|
||||
pub async fn send(
|
||||
&self,
|
||||
payload: SameWorkerPayload,
|
||||
message: SameWorkerPayload,
|
||||
) -> Result<(), tokio::sync::mpsc::error::SendError<SameWorkerPayload>> {
|
||||
self.1.fetch_add(1, Ordering::Relaxed);
|
||||
self.0.send(payload).await
|
||||
self.0.send(message).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -829,18 +834,36 @@ fn add_outstanding_wait_time(
|
||||
|
||||
async fn extract_job_and_perms(job: NextJob, conn: &Connection) -> JobAndPerms {
|
||||
match (job, conn) {
|
||||
(NextJob::Sql(job), Connection::Sql(db)) => job.get_job_and_perms(db).await,
|
||||
(NextJob::Sql(_), Connection::Http(_)) => panic!("sql job on http connection"),
|
||||
(NextJob::Sql { job, flow_runners, .. }, Connection::Sql(db)) => {
|
||||
JobAndPerms { flow_runners, ..job.get_job_and_perms(db).await }
|
||||
}
|
||||
(NextJob::Sql { .. }, Connection::Http(_)) => panic!("sql job on http connection"),
|
||||
(NextJob::Http(job), _) => job,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_span(arc_job: &Arc<MiniPulledJob>, worker_name: &str, hostname: &str) -> Span {
|
||||
let span = tracing::span!(tracing::Level::INFO, "job",
|
||||
job_id = %arc_job.id, root_job = field::Empty, workspace_id = %arc_job.workspace_id, worker = %worker_name, hostname = %hostname, tag = %arc_job.tag,
|
||||
pub fn create_span_with_name(
|
||||
arc_job: &MiniPulledJob,
|
||||
worker_name: &str,
|
||||
hostname: Option<&str>,
|
||||
span_name: &str,
|
||||
) -> Span {
|
||||
// The span macro requires a literal, so we use a fixed name and set otel.name dynamically
|
||||
let span = tracing::span!(
|
||||
tracing::Level::INFO,
|
||||
"job",
|
||||
job_id = %arc_job.id,
|
||||
root_job = field::Empty,
|
||||
workspace_id = %arc_job.workspace_id,
|
||||
worker = %worker_name,
|
||||
hostname = field::Empty,
|
||||
tag = %arc_job.tag,
|
||||
language = field::Empty,
|
||||
script_path = field::Empty, flow_step_id = field::Empty, parent_job = field::Empty,
|
||||
otel.name = field::Empty);
|
||||
script_path = field::Empty,
|
||||
flow_step_id = field::Empty,
|
||||
parent_job = field::Empty,
|
||||
otel.name = field::Empty
|
||||
);
|
||||
|
||||
let rj = arc_job.flow_innermost_root_job.unwrap_or(arc_job.id);
|
||||
|
||||
@@ -848,10 +871,10 @@ fn create_span(arc_job: &Arc<MiniPulledJob>, worker_name: &str, hostname: &str)
|
||||
span.record("language", lg.as_str());
|
||||
}
|
||||
if let Some(step_id) = arc_job.flow_step_id.as_ref() {
|
||||
span.record("otel.name", format!("job {}", step_id).as_str());
|
||||
span.record("otel.name", format!("{} {}", span_name, step_id).as_str());
|
||||
span.record("flow_step_id", step_id.as_str());
|
||||
} else {
|
||||
span.record("otel.name", "job");
|
||||
span.record("otel.name", span_name);
|
||||
}
|
||||
if let Some(parent_job) = arc_job.parent_job.as_ref() {
|
||||
span.record("parent_job", parent_job.to_string().as_str());
|
||||
@@ -862,6 +885,9 @@ fn create_span(arc_job: &Arc<MiniPulledJob>, worker_name: &str, hostname: &str)
|
||||
if let Some(root_job) = arc_job.flow_innermost_root_job.as_ref() {
|
||||
span.record("root_job", root_job.to_string().as_str());
|
||||
}
|
||||
if let Some(hostname) = hostname {
|
||||
span.record("hostname", hostname);
|
||||
}
|
||||
|
||||
windmill_common::otel_oss::set_span_parent(&span, &rj);
|
||||
span
|
||||
@@ -876,6 +902,7 @@ pub async fn handle_all_job_kind_error(
|
||||
worker_dir: &str,
|
||||
worker_name: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) {
|
||||
match conn {
|
||||
@@ -892,6 +919,7 @@ pub async fn handle_all_job_kind_error(
|
||||
&worker_dir,
|
||||
&worker_name,
|
||||
job_completed_tx.clone(),
|
||||
&killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
@@ -915,6 +943,8 @@ pub async fn handle_all_job_kind_error(
|
||||
duration: None,
|
||||
has_stream: Some(false),
|
||||
from_cache: None,
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
},
|
||||
false,
|
||||
)
|
||||
@@ -961,27 +991,21 @@ pub fn start_interactive_worker_shell(
|
||||
.await;
|
||||
|
||||
match job {
|
||||
Ok(j) => match j.to_pulled_job() {
|
||||
Ok(j) => Ok(j.clone().map(NextJob::Sql)),
|
||||
ref e @ (Err(PulledJobResultToJobErr::MissingConcurrencyKey(
|
||||
ref jc,
|
||||
))
|
||||
| Err(PulledJobResultToJobErr::ErrorWhilePreprocessing(
|
||||
ref jc,
|
||||
))) => {
|
||||
if let Err(err) =
|
||||
job_completed_tx.send_job(jc.clone(), true).await
|
||||
{
|
||||
let e_fmt = match e {
|
||||
Ok(_) => "unknown error".to_owned(),
|
||||
Err(e) => e.to_string(),
|
||||
};
|
||||
|
||||
tracing::error!("An error occurred while sending job completed ({e_fmt}): {:#?}", err)
|
||||
Ok(j) => {
|
||||
match j.to_pulled_job() {
|
||||
Ok(j) => Ok(j
|
||||
.clone()
|
||||
.map(|job| NextJob::Sql { flow_runners: None, job })),
|
||||
Err(PulledJobResultToJobErr::MissingConcurrencyKey(jc))
|
||||
| Err(PulledJobResultToJobErr::ErrorWhilePreprocessing(jc)) => {
|
||||
if let Err(err) = job_completed_tx.send_job(jc, true).await
|
||||
{
|
||||
tracing::error!("An error occurred while sending job completed: {:#?}", err)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@@ -1010,6 +1034,7 @@ pub fn start_interactive_worker_shell(
|
||||
parent_runnable_path,
|
||||
token,
|
||||
precomputed_agent_info: precomputed_bundle,
|
||||
flow_runners,
|
||||
} = extract_job_and_perms(job, &conn).await;
|
||||
|
||||
let authed_client = AuthedClient::new(
|
||||
@@ -1039,6 +1064,7 @@ pub fn start_interactive_worker_shell(
|
||||
&mut occupancy_metrics,
|
||||
&mut killpill_rx,
|
||||
precomputed_bundle,
|
||||
flow_runners,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench,
|
||||
)
|
||||
@@ -1476,9 +1502,9 @@ pub async fn run_worker(
|
||||
// Option<Sender<Arc<QueuedJob>>>,
|
||||
// Option<JoinHandle<()>>,
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
let (dedicated_workers, is_flow_worker, dedicated_handles): (
|
||||
HashMap<String, Sender<Arc<MiniPulledJob>>>,
|
||||
HashMap<String, Sender<DedicatedWorkerJob>>,
|
||||
bool,
|
||||
Vec<JoinHandle<()>>,
|
||||
) = match conn {
|
||||
@@ -1497,9 +1523,9 @@ pub async fn run_worker(
|
||||
Connection::Http(_) => (HashMap::new(), false, vec![]),
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
#[cfg(any(not(feature = "private"), not(feature = "enterprise")))]
|
||||
let (dedicated_workers, is_flow_worker, dedicated_handles): (
|
||||
HashMap<String, Sender<Arc<MiniPulledJob>>>,
|
||||
HashMap<String, Sender<DedicatedWorkerJob>>,
|
||||
bool,
|
||||
Vec<JoinHandle<()>>,
|
||||
) = (HashMap::new(), false, vec![]);
|
||||
@@ -1656,7 +1682,7 @@ pub async fn run_worker(
|
||||
|
||||
if let Ok(same_worker_job) = same_worker_rx.try_recv() {
|
||||
same_worker_queue_size.fetch_sub(1, Ordering::SeqCst);
|
||||
tracing::debug!(
|
||||
tracing::info!(
|
||||
worker = %worker_name, hostname = %hostname,
|
||||
"received {} from same worker channel",
|
||||
same_worker_job.job_id
|
||||
@@ -1676,7 +1702,12 @@ pub async fn run_worker(
|
||||
.expect("send kill to job completed tx");
|
||||
break;
|
||||
} else {
|
||||
job.map(|x| x.map(NextJob::Sql))
|
||||
job.map(|x| {
|
||||
x.map(|job| NextJob::Sql {
|
||||
flow_runners: same_worker_job.flow_runners,
|
||||
job,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Connection::Http(client) => client
|
||||
@@ -1827,22 +1858,14 @@ pub async fn run_worker(
|
||||
}
|
||||
match job {
|
||||
Ok(pulled_job_result) => match pulled_job_result.to_pulled_job() {
|
||||
Ok(j) => Ok(j.map(NextJob::Sql)),
|
||||
ref e @ (Err(PulledJobResultToJobErr::MissingConcurrencyKey(
|
||||
ref jc,
|
||||
))
|
||||
| Err(PulledJobResultToJobErr::ErrorWhilePreprocessing(
|
||||
ref jc,
|
||||
))) => {
|
||||
if let Err(err) =
|
||||
job_completed_tx.send_job(jc.clone(), true).await
|
||||
{
|
||||
let e_fmt = match e {
|
||||
Ok(_) => "unknown error".to_owned(),
|
||||
Err(e) => e.to_string(),
|
||||
};
|
||||
|
||||
tracing::error!("An error occurred while sending job completed ({e_fmt}): {:#?}", err)
|
||||
Ok(j) => Ok(j.map(|job| NextJob::Sql { flow_runners: None, job })),
|
||||
Err(PulledJobResultToJobErr::MissingConcurrencyKey(jc))
|
||||
| Err(PulledJobResultToJobErr::ErrorWhilePreprocessing(jc)) => {
|
||||
if let Err(err) = job_completed_tx.send_job(jc, true).await {
|
||||
tracing::error!(
|
||||
"An error occurred while sending job completed: {:#?}",
|
||||
err
|
||||
)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1874,7 +1897,10 @@ pub async fn run_worker(
|
||||
|
||||
tracing::debug!(worker = %worker_name, hostname = %hostname, "started handling of job {}", job.id);
|
||||
|
||||
if matches!(job.kind, JobKind::Script | JobKind::Preview) {
|
||||
if matches!(
|
||||
job.kind,
|
||||
JobKind::Script | JobKind::Preview | JobKind::FlowScript
|
||||
) {
|
||||
if !dedicated_workers.is_empty() {
|
||||
let key_o = if is_flow_worker {
|
||||
job.flow_step_id.as_ref().map(|x| x.to_string())
|
||||
@@ -1883,8 +1909,12 @@ pub async fn run_worker(
|
||||
};
|
||||
if let Some(key) = key_o {
|
||||
if let Some(dedicated_worker_tx) = dedicated_workers.get(&key) {
|
||||
if let Err(e) = dedicated_worker_tx.send(Arc::new(job.job())).await
|
||||
{
|
||||
let dedicated_job = DedicatedWorkerJob {
|
||||
job: Arc::new(job.job()),
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
};
|
||||
if let Err(e) = dedicated_worker_tx.send(dedicated_job).await {
|
||||
tracing::info!("failed to send jobs to dedicated workers. Likely dedicated worker has been shut down. This is normal: {e:?}");
|
||||
}
|
||||
|
||||
@@ -1898,6 +1928,77 @@ pub async fn run_worker(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract flow_runners early to use in both dedicated workers and flow runners
|
||||
let flow_runners = match &job {
|
||||
NextJob::Sql { flow_runners, .. } => flow_runners.clone(),
|
||||
NextJob::Http(_) => None,
|
||||
};
|
||||
|
||||
if let Some(flow_runners) = flow_runners {
|
||||
let key_o = job.flow_step_id.as_ref().map(|x| x.to_string());
|
||||
if let Some(key) = key_o {
|
||||
if let Some(flow_runner_tx) = flow_runners.runners.get(&key) {
|
||||
tracing::info!(
|
||||
"sending job {} to flow runner step {}",
|
||||
job.id,
|
||||
key
|
||||
);
|
||||
let (done_tx, done_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let flow_runners = flow_runners.clone();
|
||||
|
||||
let job = job.job();
|
||||
|
||||
let dedicated_job = DedicatedWorkerJob {
|
||||
job: Arc::new(job.clone()),
|
||||
flow_runners: Some(flow_runners),
|
||||
done_tx: Some(done_tx),
|
||||
};
|
||||
|
||||
if let Err(e) = flow_runner_tx.send(dedicated_job).await {
|
||||
let token = match &conn {
|
||||
Connection::Sql(db) => {
|
||||
windmill_queue::jobs::create_token(db, &job, None).await
|
||||
}
|
||||
_ => "".to_string(),
|
||||
};
|
||||
handle_all_job_kind_error(
|
||||
&conn,
|
||||
&AuthedClient::new(
|
||||
base_internal_url.to_owned(),
|
||||
job.workspace_id.clone(),
|
||||
token,
|
||||
None,
|
||||
),
|
||||
MiniCompletedJob::from(job),
|
||||
error::Error::InternalErr(format!(
|
||||
"failed to send jobs to flow runners: {e:?}"
|
||||
)),
|
||||
Some(&same_worker_tx),
|
||||
&worker_dir,
|
||||
&worker_name,
|
||||
job_completed_tx.clone(),
|
||||
&killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
if let Err(err) = done_rx.await {
|
||||
tracing::error!("Flow runner done channel has been dropped without being received: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "benchmark")]
|
||||
{
|
||||
add_time!(bench, "sent to flow runner");
|
||||
infos.add_iter(bench, true);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(job.kind, JobKind::Noop) {
|
||||
@@ -1917,6 +2018,8 @@ pub async fn run_worker(
|
||||
duration: None,
|
||||
has_stream: Some(false),
|
||||
from_cache: None,
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
},
|
||||
true,
|
||||
)
|
||||
@@ -2048,6 +2151,7 @@ pub async fn run_worker(
|
||||
parent_runnable_path,
|
||||
token,
|
||||
precomputed_agent_info: precomputed_bundle,
|
||||
flow_runners,
|
||||
} = extract_job_and_perms(job, &conn).await;
|
||||
|
||||
let authed_client = AuthedClient::new(
|
||||
@@ -2059,7 +2163,7 @@ pub async fn run_worker(
|
||||
|
||||
let arc_job = Arc::new(job);
|
||||
|
||||
let span = create_span(&arc_job, &worker_name, hostname);
|
||||
let span = create_span_with_name(&arc_job, &worker_name, Some(hostname), "job");
|
||||
|
||||
let job_result = handle_queued_job(
|
||||
arc_job.clone(),
|
||||
@@ -2079,6 +2183,7 @@ pub async fn run_worker(
|
||||
&mut occupancy_metrics,
|
||||
&mut killpill_rx2,
|
||||
precomputed_bundle,
|
||||
flow_runners,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench,
|
||||
)
|
||||
@@ -2138,6 +2243,7 @@ pub async fn run_worker(
|
||||
&worker_dir,
|
||||
&worker_name,
|
||||
job_completed_tx.clone(),
|
||||
&killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench,
|
||||
)
|
||||
@@ -2302,7 +2408,7 @@ async fn queue_init_bash_maybe<'c>(
|
||||
};
|
||||
if let Some((uuid, content)) = uuid_content {
|
||||
same_worker_tx
|
||||
.send(SameWorkerPayload { job_id: uuid, recoverable: false })
|
||||
.send(SameWorkerPayload { job_id: uuid, recoverable: false, flow_runners: None })
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
tracing::info!("Creating initial job {uuid} from initial script script: {content}");
|
||||
@@ -2420,7 +2526,7 @@ async fn queue_periodic_script_bash_maybe<'c>(
|
||||
};
|
||||
|
||||
same_worker_tx
|
||||
.send(SameWorkerPayload { job_id: uuid, recoverable: false })
|
||||
.send(SameWorkerPayload { job_id: uuid, recoverable: false, flow_runners: None })
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
tracing::info!("Creating periodic script job {uuid} from periodic script: {content}");
|
||||
@@ -2432,7 +2538,6 @@ pub struct SendResult {
|
||||
pub time: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SendResultPayload {
|
||||
JobCompleted(JobCompleted),
|
||||
UpdateFlow(UpdateFlow),
|
||||
@@ -2519,6 +2624,7 @@ pub async fn handle_queued_job(
|
||||
occupancy_metrics: &mut OccupancyMetrics,
|
||||
killpill_rx: &mut tokio::sync::broadcast::Receiver<()>,
|
||||
precomputed_agent_info: Option<PrecomputedAgentInfo>,
|
||||
flow_runners: Option<Arc<FlowRunners>>,
|
||||
#[cfg(feature = "benchmark")] _bench: &mut BenchmarkIter,
|
||||
) -> windmill_common::error::Result<bool> {
|
||||
// Extract the active span from the context
|
||||
@@ -2646,6 +2752,8 @@ pub async fn handle_queued_job(
|
||||
duration: None,
|
||||
has_stream: Some(false),
|
||||
from_cache: Some(true),
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
},
|
||||
true,
|
||||
)
|
||||
@@ -2681,6 +2789,8 @@ pub async fn handle_queued_job(
|
||||
worker_dir,
|
||||
job_completed_tx.clone(),
|
||||
worker_name,
|
||||
flow_runners,
|
||||
&killpill_rx,
|
||||
))
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
@@ -2911,6 +3021,7 @@ pub async fn handle_queued_job(
|
||||
conn,
|
||||
Some(started.elapsed().as_millis() as i64),
|
||||
has_stream,
|
||||
flow_runners,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ use windmill_common::{
|
||||
use windmill_queue::schedule::get_schedule_opt;
|
||||
use windmill_queue::{
|
||||
add_completed_job, add_completed_job_error, append_logs, get_mini_pulled_job,
|
||||
handle_maybe_scheduled_job, insert_concurrency_key, interpolate_args, CanceledBy,
|
||||
handle_maybe_scheduled_job, insert_concurrency_key, interpolate_args, CanceledBy, FlowRunners,
|
||||
MiniCompletedJob, MiniPulledJob, PushArgs, PushIsolationLevel, SameWorkerPayload, WrappedError,
|
||||
};
|
||||
|
||||
@@ -88,6 +88,8 @@ pub async fn update_flow_status_after_job_completion(
|
||||
stop_early_override: Option<bool>,
|
||||
worker_name: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
flow_runners: Option<Arc<FlowRunners>>,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) -> error::Result<Option<Arc<MiniPulledJob>>> {
|
||||
// this is manual tailrecursion because async_recursion blows up the stack
|
||||
@@ -121,6 +123,8 @@ pub async fn update_flow_status_after_job_completion(
|
||||
rec.has_triggered_error_handler,
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
flow_runners.clone(),
|
||||
killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
))
|
||||
@@ -147,6 +151,8 @@ pub async fn update_flow_status_after_job_completion(
|
||||
rec.has_triggered_error_handler,
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
flow_runners.clone(),
|
||||
killpill_rx,
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
))
|
||||
@@ -285,6 +291,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
has_triggered_error_handler: bool,
|
||||
worker_name: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
flow_runners: Option<Arc<FlowRunners>>,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) -> error::Result<UpdateFlowStatusAfterJobCompletion> {
|
||||
let mut has_triggered_error_handler = has_triggered_error_handler;
|
||||
@@ -434,7 +442,6 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
args
|
||||
}));
|
||||
|
||||
|
||||
let from_result_to_args = |args: &Result<HashMap<String, Box<RawValue>>, sqlx::Error>| {
|
||||
let args = args
|
||||
.as_ref()
|
||||
@@ -473,7 +480,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
_ => None,
|
||||
};
|
||||
let args = from_result_to_args(args.as_ref().await.get_ref())?;
|
||||
|
||||
|
||||
compute_bool_from_expr(
|
||||
&expr,
|
||||
Marc::new(args),
|
||||
@@ -742,7 +749,6 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
&mut stop_early_err_msg,
|
||||
&mut nresult,
|
||||
args,
|
||||
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1565,6 +1571,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
worker_dir,
|
||||
job_completed_tx,
|
||||
worker_name,
|
||||
flow_runners,
|
||||
&killpill_rx,
|
||||
))
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
@@ -2044,6 +2052,8 @@ pub async fn handle_flow(
|
||||
worker_dir: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
worker_name: &str,
|
||||
flow_runners: Option<Arc<FlowRunners>>,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
let flow = flow_data.value();
|
||||
let status = flow_job
|
||||
@@ -2101,6 +2111,9 @@ pub async fn handle_flow(
|
||||
same_worker_tx,
|
||||
worker_dir,
|
||||
worker_name,
|
||||
flow_runners.clone(),
|
||||
job_completed_tx.clone(),
|
||||
&killpill_rx,
|
||||
))
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
@@ -2185,6 +2198,7 @@ struct PushNextFlowJobRec {
|
||||
flow_job: Arc<MiniPulledJob>,
|
||||
status: FlowStatus,
|
||||
}
|
||||
|
||||
// #[async_recursion]
|
||||
// #[instrument(level = "trace", skip_all)]
|
||||
async fn push_next_flow_job(
|
||||
@@ -2197,6 +2211,9 @@ async fn push_next_flow_job(
|
||||
same_worker_tx: &SameWorkerSender,
|
||||
worker_dir: &str,
|
||||
worker_name: &str,
|
||||
flow_runners: Option<Arc<FlowRunners>>,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
) -> error::Result<PushNextFlowJob> {
|
||||
let job_root = flow_job
|
||||
.flow_innermost_root_job
|
||||
@@ -3028,6 +3045,22 @@ async fn push_next_flow_job(
|
||||
}
|
||||
};
|
||||
|
||||
// only start runners if we're not already in a squash for loop
|
||||
let start_runners = flow_runners.is_none()
|
||||
&& matches!(
|
||||
next_status,
|
||||
NextStatus::NextLoopIteration { start_runners: true, .. }
|
||||
);
|
||||
|
||||
let do_not_pass_runners = matches!(next_status, NextStatus::NextStep { .. })
|
||||
&& flow_runners
|
||||
.as_ref()
|
||||
.is_some_and(|fr| fr.job_id == flow_job.id);
|
||||
|
||||
let continue_with_runners = (start_runners || (flow_runners.is_some() && !do_not_pass_runners))
|
||||
&& module.suspend.is_none()
|
||||
&& module.sleep.is_none();
|
||||
|
||||
// Also check `flow_job.same_worker` for [`JobKind::Flow`] jobs as it's no
|
||||
// more reflected to the flow value on push.
|
||||
let job_same_worker = flow_job.same_worker
|
||||
@@ -3083,6 +3116,7 @@ async fn push_next_flow_job(
|
||||
NextStatus::NextLoopIteration {
|
||||
next: ForloopNextIteration { new_args, .. },
|
||||
simple_input_transforms,
|
||||
..
|
||||
} => {
|
||||
let mut args = if let Ok(args) = nargs {
|
||||
args.as_ref().clone()
|
||||
@@ -3300,7 +3334,7 @@ async fn push_next_flow_job(
|
||||
Some(module.id.clone()),
|
||||
new_job_priority_override,
|
||||
job_perms.as_ref(),
|
||||
false,
|
||||
continue_with_runners,
|
||||
None,
|
||||
None,
|
||||
None
|
||||
@@ -3308,7 +3342,7 @@ async fn push_next_flow_job(
|
||||
.warn_after_seconds(2)
|
||||
.await?;
|
||||
|
||||
if continue_on_same_worker {
|
||||
if continue_on_same_worker || continue_with_runners {
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE v2_job_queue SET worker = $2 WHERE id = $1",
|
||||
uuid,
|
||||
@@ -3583,7 +3617,7 @@ async fn push_next_flow_job(
|
||||
.warn_after_seconds(3)
|
||||
.await?;
|
||||
|
||||
if continue_on_same_worker {
|
||||
if continue_on_same_worker || continue_with_runners {
|
||||
if !is_one_uuid {
|
||||
return Err(Error::BadRequest(
|
||||
"Cannot continue on same worker with multiple jobs, parallel cannot be used in conjunction with same_worker".to_string(),
|
||||
@@ -3593,9 +3627,43 @@ async fn push_next_flow_job(
|
||||
tx.commit().warn_after_seconds(3).await?;
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, "all next flow jobs pushed: {uuids:?}");
|
||||
|
||||
if continue_on_same_worker {
|
||||
if continue_on_same_worker || continue_with_runners {
|
||||
let flow_runners = if start_runners {
|
||||
tracing::info!(id = %flow_job.id, "starting flow runners for module {}", module.id);
|
||||
let (new_flow_runners, new_flow_runner_handles) =
|
||||
crate::dedicated_worker_oss::spawn_flow_module_runners(
|
||||
&flow_job,
|
||||
module,
|
||||
flow.failure_module.as_ref(),
|
||||
&killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
&client.base_internal_url,
|
||||
worker_name,
|
||||
&job_completed_tx,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to spawn flow module runners for job: {}",
|
||||
flow_job.id
|
||||
)
|
||||
})?;
|
||||
|
||||
let flow_runners = FlowRunners {
|
||||
runners: new_flow_runners,
|
||||
handles: new_flow_runner_handles,
|
||||
job_id: flow_job.id,
|
||||
};
|
||||
Some(Arc::new(flow_runners))
|
||||
} else if !do_not_pass_runners {
|
||||
flow_runners
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
same_worker_tx
|
||||
.send(SameWorkerPayload { job_id: first_uuid, recoverable: true })
|
||||
.send(SameWorkerPayload { job_id: first_uuid, recoverable: true, flow_runners })
|
||||
.warn_after_seconds(3)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
@@ -3702,6 +3770,7 @@ enum NextStatus {
|
||||
NextLoopIteration {
|
||||
next: ForloopNextIteration,
|
||||
simple_input_transforms: Option<HashMap<String, InputTransform>>,
|
||||
start_runners: bool,
|
||||
},
|
||||
AllFlowJobs {
|
||||
branchall: Option<BranchAllStatus>,
|
||||
@@ -3943,7 +4012,7 @@ async fn compute_next_flow_transform(
|
||||
NextStatus::NextStep,
|
||||
))
|
||||
}
|
||||
FlowModuleValue::WhileloopFlow { modules, modules_node, .. } => {
|
||||
FlowModuleValue::WhileloopFlow { modules, modules_node, squash, .. } => {
|
||||
// if it's a simple single step flow, we will collapse it as an optimization and need to pass flow_input as an arg
|
||||
let is_simple = is_simple_modules(&modules, flow.failure_module.as_ref());
|
||||
let (flow_jobs, flow_jobs_success, flow_jobs_duration) = match status_module {
|
||||
@@ -3960,6 +4029,8 @@ async fn compute_next_flow_transform(
|
||||
_ => (vec![], Some(vec![]), Some(FlowJobsDuration::new(0))),
|
||||
};
|
||||
let next_loop_idx = flow_jobs.len();
|
||||
|
||||
let start_runners = next_loop_idx == 0 && squash.unwrap_or(false);
|
||||
next_loop_iteration(
|
||||
flow,
|
||||
status,
|
||||
@@ -3982,11 +4053,14 @@ async fn compute_next_flow_transform(
|
||||
db,
|
||||
module,
|
||||
delete_after_use,
|
||||
start_runners,
|
||||
)
|
||||
.await
|
||||
}
|
||||
/* forloop modules are expected set `iter: { value: Value, index: usize }` as job arguments */
|
||||
FlowModuleValue::ForloopFlow { modules, modules_node, iterator, parallel, .. } => {
|
||||
FlowModuleValue::ForloopFlow {
|
||||
modules, modules_node, iterator, parallel, squash, ..
|
||||
} => {
|
||||
// if it's a simple single step flow, we will collapse it as an optimization and need to pass flow_input as an arg
|
||||
let is_simple = !matches!(flow_job.kind, JobKind::FlowPreview)
|
||||
&& !parallel
|
||||
@@ -4025,6 +4099,7 @@ async fn compute_next_flow_transform(
|
||||
Ok(NextFlowTransform::EmptyInnerFlows { branch_chosen: None })
|
||||
}
|
||||
ForLoopStatus::NextIteration(ns) => {
|
||||
let start_runners = ns.index == 0 && squash.unwrap_or(false);
|
||||
next_loop_iteration(
|
||||
flow,
|
||||
status,
|
||||
@@ -4036,6 +4111,7 @@ async fn compute_next_flow_transform(
|
||||
db,
|
||||
module,
|
||||
delete_after_use,
|
||||
start_runners,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -4300,6 +4376,7 @@ async fn next_loop_iteration(
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
module: &FlowModule,
|
||||
delete_after_use: bool,
|
||||
start_runners: bool,
|
||||
) -> Result<NextFlowTransform, Error> {
|
||||
let inner_path = || format!("{}/loop-{}", flow_job.runnable_path(), ns.index);
|
||||
if is_simple {
|
||||
@@ -4317,7 +4394,7 @@ async fn next_loop_iteration(
|
||||
ContinuePayload::SingleJob(
|
||||
payload_from_simple_module(value, db, flow_job, module, inner_path()).await?,
|
||||
),
|
||||
NextStatus::NextLoopIteration { next: ns, simple_input_transforms },
|
||||
NextStatus::NextLoopIteration { next: ns, simple_input_transforms, start_runners },
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4341,7 +4418,7 @@ async fn next_loop_iteration(
|
||||
timeout: None,
|
||||
on_behalf_of: None,
|
||||
}),
|
||||
NextStatus::NextLoopIteration { next: ns, simple_input_transforms: None },
|
||||
NextStatus::NextLoopIteration { next: ns, simple_input_transforms: None, start_runners },
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -1193,6 +1193,7 @@ async fn lock_modules<'c>(
|
||||
skip_failures,
|
||||
parallel,
|
||||
parallelism,
|
||||
squash,
|
||||
} => {
|
||||
let nmodules;
|
||||
(nmodules, tx, nmodified_ids, nerrors) = Box::pin(lock_modules(
|
||||
@@ -1222,6 +1223,7 @@ async fn lock_modules<'c>(
|
||||
skip_failures,
|
||||
parallel,
|
||||
parallelism,
|
||||
squash,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -1258,7 +1260,7 @@ async fn lock_modules<'c>(
|
||||
}
|
||||
e.value = FlowModuleValue::BranchAll { branches: nbranches, parallel }.into()
|
||||
}
|
||||
FlowModuleValue::WhileloopFlow { modules, modules_node, skip_failures } => {
|
||||
FlowModuleValue::WhileloopFlow { modules, modules_node, skip_failures, squash } => {
|
||||
let nmodules;
|
||||
(nmodules, tx, nmodified_ids, nerrors) = Box::pin(lock_modules(
|
||||
modules,
|
||||
@@ -1284,6 +1286,7 @@ async fn lock_modules<'c>(
|
||||
modules: nmodules,
|
||||
modules_node,
|
||||
skip_failures,
|
||||
squash,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.583.1";
|
||||
export const VERSION = "v1.585.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -18,12 +18,16 @@ import { readInlinePathSync } from "../../utils/utils.ts";
|
||||
|
||||
export interface AppFile {
|
||||
value: any;
|
||||
public?: boolean;
|
||||
summary: string;
|
||||
policy: Policy;
|
||||
}
|
||||
|
||||
const alreadySynced: string[] = [];
|
||||
|
||||
export function isExecutionModeAnonymous(app: any) {
|
||||
return app?.["policy"]?.["execution_mode"] == "anonymous";
|
||||
}
|
||||
export async function pushApp(
|
||||
workspace: string,
|
||||
remotePath: string,
|
||||
@@ -45,7 +49,13 @@ export async function pushApp(
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
app.policy = undefined;
|
||||
if (isExecutionModeAnonymous(app)) {
|
||||
app.public = true;
|
||||
}
|
||||
// console.log(app);
|
||||
if (app) {
|
||||
app.policy = undefined;
|
||||
}
|
||||
|
||||
if (!localPath.endsWith(SEP)) {
|
||||
localPath += SEP;
|
||||
@@ -78,7 +88,9 @@ export async function pushApp(
|
||||
}
|
||||
|
||||
replaceInlineScripts(localApp.value);
|
||||
await generatingPolicy(localApp, remotePath);
|
||||
// console.log(localApp, localApp?.["policy"]);
|
||||
await generatingPolicy(localApp, remotePath, localApp?.["public"] ?? (localApp.policy ? isExecutionModeAnonymous(localApp) : false));
|
||||
// console.log(localApp, localApp?.["policy"]);
|
||||
if (app) {
|
||||
if (isSuperset(localApp, app)) {
|
||||
log.info(colors.green(`App ${remotePath} is up to date`));
|
||||
@@ -107,11 +119,11 @@ export async function pushApp(
|
||||
}
|
||||
}
|
||||
|
||||
async function generatingPolicy(app: any, path: string) {
|
||||
async function generatingPolicy(app: any, path: string, publicApp: boolean) {
|
||||
log.info(colors.gray(`Generating fresh policy for app ${path}...`));
|
||||
try {
|
||||
app.policy = await windmillUtils.updatePolicy(app.value, undefined);
|
||||
app.policy.execution_mode = "publisher";
|
||||
app.policy.execution_mode = publicApp ? "anonymous" : "publisher";
|
||||
} catch (e) {
|
||||
log.error(colors.red(`Error generating policy for app ${path}: ${e}`));
|
||||
throw e;
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
PathAssigner,
|
||||
} from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts";
|
||||
import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
import { isExecutionModeAnonymous } from "../app/apps.ts";
|
||||
|
||||
// Merge CLI options with effective settings, preserving CLI flags as overrides
|
||||
function mergeCliWithEffectiveOptions<
|
||||
@@ -415,6 +416,9 @@ function ZipFSElement(
|
||||
};
|
||||
}
|
||||
|
||||
if (isExecutionModeAnonymous(app)) {
|
||||
app.public = true;
|
||||
}
|
||||
app.policy = undefined;
|
||||
yield {
|
||||
isDirectory: false,
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.583.1";
|
||||
export const VERSION = "1.585.1";
|
||||
|
||||
export const WM_FORK_PREFIX = "wm-fork";
|
||||
|
||||
|
||||
Generated
+85
-96
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.583.1",
|
||||
"version": "1.585.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.583.1",
|
||||
"version": "1.585.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -142,7 +142,7 @@
|
||||
"tar": "^7.4.3",
|
||||
"tslib": "^2.6.1",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite": "npm:rolldown-vite@7.2.8",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-mkcert": "^1.17.5",
|
||||
"yootils": "^0.3.1"
|
||||
@@ -1904,9 +1904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
||||
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
||||
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -1916,9 +1916,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
|
||||
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
||||
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -2603,9 +2603,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/runtime": {
|
||||
"version": "0.92.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.92.0.tgz",
|
||||
"integrity": "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==",
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.99.0.tgz",
|
||||
"integrity": "sha512-8iE5/4OK0SLHqWzRxSvI1gjFPmIH6718s8iwkuco95rBZsCZIHq+5wy4lYsASxnH+8FOhbGndiUrcwsVG5i2zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2613,9 +2613,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.94.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.94.0.tgz",
|
||||
"integrity": "sha512-+UgQT/4o59cZfH6Cp7G0hwmqEQ0wE+AdIwhikdwnhWI9Dp8CgSY081+Q3O67/wq3VJu8mgUEB93J9EHHn70fOw==",
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.99.0.tgz",
|
||||
"integrity": "sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -2679,9 +2679,9 @@
|
||||
"license": "SEE LICENSE IN LICENSE"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-W5ZKF3TP3bOWuBfotAGp+UGjxOkGV7jRmIRbBA7NFjggx7Oi6vOmGDqpHEIX7kDCiry1cnIsWQaxNvWbMdkvzQ==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-MBGIgysimZPqTDcLXI+i9VveijkP5C3EAncEogXhqfax6YXj1Tr2LY3DVuEOMIjWfMPMhtQSPup4fSTAmgjqIw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2696,9 +2696,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-abw/wtgJA8OCgaTlL+xJxnN/Z01BwV1rfzIp5Hh9x+IIO6xOBfPsQ0nzi0+rWx3TyZ9FZXyC7bbC+5NpQ9EaXQ==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-MmKeoLnKu1d9j6r19K8B+prJnIZ7u+zQ+zGQ3YHXGnr41rzE3eqQLovlkvoZnRoxDGPA4ps0pGiwXy6YE3lJyg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2713,9 +2713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-Y/UrZIRVr8CvXVEB88t6PeC46r1K9/QdPEo2ASE/b/KBEyXIx+QbM6kv9QfQVWU2Atly2+SVsQzxQsIvuk3lZQ==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-qpHedvQBmIjT8zdnjN3nWPR2qjQyJttbXniCEKKdHeAbZG9HyNPBUzQF7AZZGwmS9coQKL+hWg9FhWzh2dZ2IA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2730,9 +2730,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-zRM0oOk7BZiy6DoWBvdV4hyEg+j6+WcBZIMHVirMEZRu8hd18kZdJkg+bjVMfCEhwpWeFUfBfZ1qcaZ5UdYzlQ==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-dDp7WbPapj/NVW0LSiH/CLwMhmLwwKb3R7mh2kWX+QW85X1DGVnIEyKh9PmNJjB/+suG1dJygdtdNPVXK1hylg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2747,9 +2747,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-6RjFaC52QNwo7ilU8C5H7swbGlgfTkG9pudXwzr3VYyT18s0C9gLg3mvc7OMPIGqNxnQ0M5lU8j6aQCk2DTRVg==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-9e4l6vy5qNSliDPqNfR6CkBOAx6PH7iDV4OJiEJzajajGrVy8gc/IKKJUsoE52G8ud8MX6r3PMl97NfwgOzB7g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2764,9 +2764,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-LMYHM5Sf6ROq+VUwHMDVX2IAuEsWTv4SnlFEedBnMGpvRuQ14lCmD4m5Q8sjyAQCgyha9oghdGoK8AEg1sXZKg==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-V48oDR84feRU2KRuzpALp594Uqlx27+zFsT6+BgTcXOtu7dWy350J1G28ydoCwKB+oxwsRPx2e7aeQnmd3YJbQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2781,9 +2781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-/bNTYb9aKNhzdbPn3O4MK2aLv55AlrkUKPE4KNfBYjkoZUfDr4jWp7gsSlvTc5A/99V1RCm9axvt616ZzeXGyA==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-ENLmSQCWqSA/+YN45V2FqTIemg7QspaiTjlm327eUAMeOLdqmSOVVyrQexJGNTQ5M8sDYCgVAig2Kk01Ggmqaw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2798,9 +2798,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-n/SLa4h342oyeGykZdch7Y3GNCNliRPL4k5wkeZ/5eQZs+c6/ZG1SHCJQoy7bZcmxiMyaXs9HoFmv1PEKrZgWg==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-klahlb2EIFltSUubn/VLjuc3qxp1E7th8ukayPfdkcKvvYcQ5rJztgx8JsJSuAKVzKtNTqUGOhy4On71BuyV8g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2815,9 +2815,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-4PSd46sFzqpLHSGdaSViAb1mk55sCUMpJg+X8ittXaVocQsV3QLG/uydSH8RyL0ngHX5fy3D70LcCzlB15AgHw==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-UuA+JqQIgqtkgGN2c/AQ5wi8M6mJHrahz/wciENPTeI6zEIbbLGoth5XN+sQe2pJDejEVofN9aOAp0kaazwnVg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2832,9 +2832,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-BmWoeJJyeZXmZBcfoxG6J9+rl2G7eO47qdTkAzEegj4n3aC6CBIHOuDcbE8BvhZaEjQR0nh0nJrtEDlt65Q7Sw==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-1BNQW8u4ro8bsN1+tgKENJiqmvc+WfuaUhXzMImOVSMw28pkBKdfZtX2qJPADV3terx+vNJtlsgSGeb3+W6Jiw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2849,9 +2849,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-2Ft32F7uiDTrGZUKws6CLNTlvTWHC33l4vpXrzUucf9rYtUThAdPCOt89Pmn13tNX6AulxjGEP2R0nZjTSW3eQ==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-K/p7clhCqJOQpXGykrFaBX2Dp9AUVIDHGc+PtFGBwg7V+mvBTv/tsm3LC3aUmH02H2y3gz4y+nUTQ0MLpofEEg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -2859,16 +2859,16 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.0.6"
|
||||
"@napi-rs/wasm-runtime": "^1.0.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-hC1kShXW/z221eG+WzQMN06KepvPbMBknF0iGR3VMYJLOe9gwnSTfGxFT5hf8XrPv7CEZqTWRd0GQpkSHRbGsw==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-a4EkXBtnYYsKipjS7QOhEBM4bU5IlR9N1hU+JcVEVeuTiaslIyhWVKsvf7K2YkQHyVAJ+7/A9BtrGqORFcTgng==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2883,9 +2883,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-ia32-msvc": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-AICBYromawouGjj+GS33369E8Vwhy6UwhQEhQ5evfS8jPCsyVvoICJatbDGDGH01dwtVGLD5eDFzPicUOVpe4g==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-5ZXcYyd4GxPA6QfbGrNcQjmjbuLGvfz6728pZMsQvGHI+06LT06M6TPtXvFvLgXtexc+OqvFe1yAIXJU1gob/w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2900,9 +2900,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-XpZ0M+tjoEiSc9c+uZR7FCnOI0uxDRNs1elGOMjeB0pUP1QmvVbZGYNsyLbLoP4u7e3VQN8rie1OQ8/mB6rcJg==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-tzpnRQXJrSzb8Z9sm97UD3cY0toKOImx+xRKsDLX4zHaAlRXWh7jbaKBePJXEN7gNw7Nm03PBNwphdtA8KSUYQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2917,9 +2917,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-N7pQzk9CyE7q0bBN/q0J8s6Db279r5kUZc6d7/wWRe9/zXqC52HQovVyu6iXPIDY4BEzzgbVLhVFXrOuGJ22ZQ==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-/L0htLJZbaZFL1g9OHOblTxbCYIGefErJjtYOwgl9ZqNx27P3L0SDfjhhHIss32gu5NWgnxuT2a2Hnnv6QGHKA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4437,16 +4437,6 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansis": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
|
||||
"integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
@@ -12260,15 +12250,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-beta.42",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.42.tgz",
|
||||
"integrity": "sha512-xaPcckj+BbJhYLsv8gOqezc8EdMcKKe/gk8v47B0KPvgABDrQ0qmNPAiT/gh9n9Foe0bUkEv2qzj42uU5q1WRg==",
|
||||
"version": "1.0.0-beta.52",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.52.tgz",
|
||||
"integrity": "sha512-Hbnpljue+JhMJrlOjQ1ixp9me7sUec7OjFvS+A1Qm8k8Xyxmw3ZhxFu7LlSXW1s9AX3POE9W9o2oqCEeR5uDmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.94.0",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.42",
|
||||
"ansis": "=4.2.0"
|
||||
"@oxc-project/types": "=0.99.0",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.52"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
@@ -12277,20 +12266,20 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-beta.42",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-beta.42",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-beta.42",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-beta.42",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.42",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.42",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-beta.42",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-beta.42",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-beta.42",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-beta.42",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-beta.42",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.42",
|
||||
"@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.42",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-beta.42"
|
||||
"@rolldown/binding-android-arm64": "1.0.0-beta.52",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-beta.52",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-beta.52",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-beta.52",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.52",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.52",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-beta.52",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-beta.52",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-beta.52",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-beta.52",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-beta.52",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.52",
|
||||
"@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.52",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-beta.52"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
@@ -14130,18 +14119,18 @@
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"name": "rolldown-vite",
|
||||
"version": "7.1.16",
|
||||
"resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.16.tgz",
|
||||
"integrity": "sha512-cK6tCmZyEC0KRAcXTjQ+ara+wkqmaE7WUoI0ZfZzDuvaRaZ3mtvbhTJc4cH+PjKRok++++Z1bZZaNlf3+SnnGA==",
|
||||
"version": "7.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.8.tgz",
|
||||
"integrity": "sha512-8wKihlF6EDF8grimwd7GPOhLkQkSIgj6Hlcp0CXhtO3HAXeUUqhgZmJmn07OF8e4PbTusMX6Yxmy1BptVRZsdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/runtime": "0.92.0",
|
||||
"@oxc-project/runtime": "0.99.0",
|
||||
"fdir": "^6.5.0",
|
||||
"lightningcss": "^1.30.2",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rolldown": "1.0.0-beta.42",
|
||||
"rolldown": "1.0.0-beta.52",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.583.1",
|
||||
"version": "1.585.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -65,7 +65,7 @@
|
||||
"tar": "^7.4.3",
|
||||
"tslib": "^2.6.1",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite": "npm:rolldown-vite@7.2.8",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-mkcert": "^1.17.5",
|
||||
"yootils": "^0.3.1"
|
||||
|
||||
@@ -1049,6 +1049,8 @@
|
||||
{#if oneOf && oneOf.length >= 2}
|
||||
<ToggleButtonGroup
|
||||
selected={oneOfSelected}
|
||||
wrap
|
||||
class="mb-4"
|
||||
on:selected={({ detail }) => {
|
||||
oneOfSelected = detail
|
||||
const selectedObjProperties =
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
{:else if stepDetail.value.type == 'forloopflow'}
|
||||
For loop {#if stepDetail.value.parallel}(parallel){/if}
|
||||
{#if stepDetail.value.skip_failures}(skip failures){/if}
|
||||
{#if stepDetail.value.squash}(squash){/if}
|
||||
{:else if stepDetail.value.type == 'branchall'}
|
||||
Run all branches {#if stepDetail.value.parallel}(parallel){/if}
|
||||
{:else if stepDetail.value.type == 'branchone'}
|
||||
@@ -128,7 +129,8 @@
|
||||
{:else if stepDetail.value.type == 'flow'}
|
||||
Inner flow
|
||||
{:else if stepDetail.value.type == 'whileloopflow'}
|
||||
While loop
|
||||
While loop {#if stepDetail.value.skip_failures}(skip failures){/if}
|
||||
{#if stepDetail.value.squash}(squash){/if}
|
||||
{:else if stepDetail.id === 'failure'}
|
||||
Error handler
|
||||
{:else if stepDetail.id === 'preprocessor'}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Map, View, Feature } from 'ol'
|
||||
import { Fill, Stroke, Style, Text } from 'ol/style.js'
|
||||
import { useGeographic } from 'ol/proj'
|
||||
import { OSM, Vector as VectorSource } from 'ol/source'
|
||||
import { Vector as VectorLayer, Tile as TileLayer } from 'ol/layer'
|
||||
import { Point } from 'ol/geom'
|
||||
import { defaults as defaultControls } from 'ol/control'
|
||||
import CircleStyle from 'ol/style/Circle'
|
||||
import { useGeographic } from 'ol/proj.js'
|
||||
import { OSM, Vector as VectorSource } from 'ol/source.js'
|
||||
import { Vector as VectorLayer, Tile as TileLayer } from 'ol/layer.js'
|
||||
import { Point } from 'ol/geom.js'
|
||||
import { defaults as defaultControls } from 'ol/control.js'
|
||||
import CircleStyle from 'ol/style/Circle.js'
|
||||
|
||||
interface Marker {
|
||||
lon: number
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
}
|
||||
|
||||
export async function reset() {
|
||||
if (path == '' || path == 'u//') {
|
||||
if (path == '' || path == 'u//' || path?.startsWith('tmp/')) {
|
||||
if ($lastMetaUsed == undefined || $lastMetaUsed.owner != $userStore?.username) {
|
||||
meta = {
|
||||
ownerKind: hideUser ? 'folder' : 'user',
|
||||
@@ -323,12 +323,12 @@
|
||||
|
||||
async function initPath() {
|
||||
await tick()
|
||||
if (path != undefined && path != '') {
|
||||
if (path != undefined && path != '' && !path?.startsWith('tmp/')) {
|
||||
meta = pathToMeta(path, hideUser)
|
||||
onMetaChange()
|
||||
return
|
||||
}
|
||||
if (initialPath == undefined || initialPath == '') {
|
||||
if (initialPath == undefined || initialPath == '' || initialPath?.startsWith('tmp/')) {
|
||||
reset()
|
||||
} else {
|
||||
meta = pathToMeta(initialPath, hideUser)
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</script>
|
||||
|
||||
{#if queueState}
|
||||
<div class="text-small ml-4">
|
||||
<div class="text-xs ml-4">
|
||||
<span class="text-orange-600">Queue position: <b>{queueState.position}</b></span>
|
||||
{#if !minimal}
|
||||
<span class="ml-2 text-primary">(Waiting for an available worker)</span>
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
if (templateScript) {
|
||||
script.content += '\r\n' + templateScript
|
||||
}
|
||||
scriptEditor?.inferSchema(script.content, language, true)
|
||||
scriptEditor?.inferSchema(script.content, { nlang: language, resetArgs: true })
|
||||
if (script.content != editor?.getCode()) {
|
||||
setCode(script.content)
|
||||
}
|
||||
@@ -567,7 +567,7 @@
|
||||
if (!disableHistoryChange) {
|
||||
history.replaceState(history.state, '', `/scripts/edit/${script.path}`)
|
||||
}
|
||||
if (stay || script.kind !== 'script' || script.no_main_func) {
|
||||
if (stay || (script.no_main_func && script.kind !== 'preprocessor')) {
|
||||
script.parent_hash = newHash
|
||||
sendUserToast('Deployed')
|
||||
} else {
|
||||
@@ -849,7 +849,7 @@
|
||||
captureOn.set(true)
|
||||
}
|
||||
|
||||
function addPreprocessor() {
|
||||
function addPreprocessor(e?: { detail?: { args: Record<string, any> } }) {
|
||||
const code = editor?.getCode()
|
||||
if (code) {
|
||||
const preprocessorCode = getPreprocessorFullCode(script.language, false)
|
||||
@@ -865,6 +865,11 @@
|
||||
}
|
||||
}
|
||||
selectedInputTab = 'preprocessor'
|
||||
|
||||
// Apply provided args to the preprocessor
|
||||
if (e?.detail?.args && Object.keys(e.detail.args).length > 0) {
|
||||
args = { ...args, ...e.detail.args }
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeployTrigger(trigger: Trigger) {
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
enablePreprocessorSnippet = false
|
||||
}: Props = $props()
|
||||
|
||||
let initialArgs = structuredClone($state.snapshot(args))
|
||||
|
||||
$effect.pre(() => {
|
||||
if (schema == undefined) {
|
||||
schema = emptySchema()
|
||||
@@ -269,7 +271,18 @@
|
||||
})
|
||||
}
|
||||
|
||||
export async function inferSchema(code: string, nlang?: SupportedLanguage, resetArgs = false) {
|
||||
export async function inferSchema(
|
||||
code: string,
|
||||
{
|
||||
nlang,
|
||||
resetArgs = false,
|
||||
applyInitialArgs = false
|
||||
}: {
|
||||
nlang?: SupportedLanguage
|
||||
resetArgs?: boolean
|
||||
applyInitialArgs?: boolean
|
||||
} = {}
|
||||
) {
|
||||
let nschema = schema ?? emptySchema()
|
||||
|
||||
try {
|
||||
@@ -297,6 +310,10 @@
|
||||
if (resetArgs) {
|
||||
args = {}
|
||||
}
|
||||
if (applyInitialArgs) {
|
||||
// we reapply initial args as the schema form might have cleared them between mount and the schema inference
|
||||
args = initialArgs
|
||||
}
|
||||
schema = nschema
|
||||
} catch (e) {
|
||||
validCode = false
|
||||
@@ -338,7 +355,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
inferSchema(code)
|
||||
inferSchema(code, { applyInitialArgs: true })
|
||||
loadPastTests()
|
||||
aiChatManager.saveAndClear()
|
||||
aiChatManager.changeMode(AIMode.SCRIPT)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<div class="flex flex-col h-screen bg-surface">
|
||||
<!-- Top bar -->
|
||||
<div class="h-12 px-4 border-b flex items-center gap-4">
|
||||
<div class="w-8 h-8 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="flex-1 h-6 max-w-md rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
</div>
|
||||
|
||||
<!-- Editor bar -->
|
||||
<div class="border-b shadow-sm px-4 py-2 flex gap-2">
|
||||
<div class="w-20 h-6 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="w-20 h-6 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="w-20 h-6 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="flex-1"></div>
|
||||
<div class="w-24 h-6 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- Left panel - Editor -->
|
||||
<div class="flex-1 min-w-0 border-r bg-surface-secondary">
|
||||
<div class="w-full h-full p-4">
|
||||
<div class="w-32 h-4 mb-3 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="w-48 h-4 mb-3 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="w-40 h-4 mb-3 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="w-56 h-4 mb-3 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel - Test/Preview with horizontal split -->
|
||||
<div class="w-2/5 min-w-0 flex flex-col bg-surface">
|
||||
<!-- Top section - test inputs -->
|
||||
<div class="flex-1 border-b flex flex-col">
|
||||
<div class="h-10 border-b px-4 flex items-center">
|
||||
<div class="w-24 h-4 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 space-y-3">
|
||||
<div class="w-full h-8 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="w-full h-8 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="w-32 h-8 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom section - results -->
|
||||
<div class="flex-1 flex flex-col">
|
||||
<div class="h-10 border-b px-4 flex items-center">
|
||||
<div class="w-24 h-4 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
</div>
|
||||
<div class="flex-1 p-4">
|
||||
<div class="w-full h-4 mb-2 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
<div class="w-3/4 h-4 rounded animate-skeleton dark:bg-frost-900/50"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,6 +29,9 @@
|
||||
{disabled}
|
||||
class={disabled ? 'border-0' : ''}
|
||||
allowClear={!disabled}
|
||||
onCreateItem={(c) => worker_tags.push(c)}
|
||||
onCreateItem={(c) => {
|
||||
worker_tags.push(c)
|
||||
dispatch('dirty')
|
||||
}}
|
||||
createText="Press Enter to use this tag"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Component-Managed Fields Registry
|
||||
*
|
||||
* This module provides a centralized registry of fields that are automatically
|
||||
* managed by components at runtime, rather than being configured by users.
|
||||
*
|
||||
* These fields should never be treated as static fields in force_viewer_static_fields,
|
||||
* as their values are determined dynamically by user interactions (pagination,
|
||||
* sorting, search, chat input, etc.)
|
||||
*/
|
||||
|
||||
import type { AppInput } from '$lib/components/apps/inputType'
|
||||
|
||||
/**
|
||||
* Registry of component types to their managed field names
|
||||
*/
|
||||
export const COMPONENT_MANAGED_FIELDS: Record<string, string[]> = {
|
||||
aggridinfinitecomponent: ['offset', 'limit', 'orderBy', 'isDesc', 'search'],
|
||||
aggridinfinitecomponentee: ['offset', 'limit', 'orderBy', 'isDesc', 'search'],
|
||||
chatcomponent: ['user_message']
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of managed fields for a given component type
|
||||
*/
|
||||
export function getManagedFields(componentType: string): string[] {
|
||||
return COMPONENT_MANAGED_FIELDS[componentType] ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field is managed by a specific component type
|
||||
*/
|
||||
export function isFieldManagedByComponent(componentType: string, fieldName: string): boolean {
|
||||
const managedFields = getManagedFields(componentType)
|
||||
return managedFields.includes(fieldName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert component-managed fields from static to evalv2 type
|
||||
* This ensures they are properly handled at runtime and not added to force_viewer_static_fields
|
||||
*/
|
||||
export function convertManagedFieldsToEvalv2(
|
||||
componentType: string,
|
||||
componentId: string,
|
||||
fields: Record<string, AppInput>
|
||||
): Record<string, AppInput> {
|
||||
const managedFieldNames = getManagedFields(componentType)
|
||||
|
||||
if (managedFieldNames.length === 0) {
|
||||
return fields
|
||||
}
|
||||
|
||||
const convertedFields = { ...fields }
|
||||
|
||||
for (const fieldName of managedFieldNames) {
|
||||
if (convertedFields[fieldName]) {
|
||||
// Determine the expression based on component type
|
||||
let expr: string
|
||||
|
||||
if (
|
||||
componentType === 'aggridinfinitecomponent' ||
|
||||
componentType === 'aggridinfinitecomponentee'
|
||||
) {
|
||||
// AgGrid components use params.{fieldName}
|
||||
expr = `${componentId}.params.${fieldName}`
|
||||
} else if (componentType === 'chatcomponent') {
|
||||
// Chat component uses userMessage output
|
||||
expr = `${componentId}.userMessage`
|
||||
} else {
|
||||
// Default pattern for future components
|
||||
expr = `${componentId}.${fieldName}`
|
||||
}
|
||||
|
||||
// Convert to evalv2 type while preserving fieldType
|
||||
convertedFields[fieldName] = {
|
||||
type: 'evalv2',
|
||||
expr,
|
||||
fieldType: convertedFields[fieldName].fieldType ?? 'string',
|
||||
connections: []
|
||||
} as AppInput
|
||||
}
|
||||
}
|
||||
|
||||
return convertedFields
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all managed field names across all component types
|
||||
* Useful for generic checks
|
||||
*/
|
||||
export function getAllManagedFieldNames(): string[] {
|
||||
const allFields = new Set<string>()
|
||||
Object.values(COMPONENT_MANAGED_FIELDS).forEach((fields) => {
|
||||
fields.forEach((field) => allFields.add(field))
|
||||
})
|
||||
return Array.from(allFields)
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import { components } from '../../editor/component'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { initCss } from '../../utils'
|
||||
import ChatMessage from '$lib/components/chat/ChatMessage.svelte'
|
||||
import ChatInput from '$lib/components/chat/ChatInput.svelte'
|
||||
import { parseStreamDeltas } from '$lib/components/chat/utils'
|
||||
import { randomUUID } from '$lib/components/flows/conversations/FlowChatManager.svelte'
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
componentInput: AppInput | undefined
|
||||
configuration: RichConfigurations
|
||||
customCss?: ComponentCustomCSS<'chatcomponent'> | undefined
|
||||
recomputeIds?: string[] | undefined
|
||||
render: boolean
|
||||
errorHandledByComponent?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
componentInput,
|
||||
configuration,
|
||||
customCss = undefined,
|
||||
recomputeIds = undefined,
|
||||
render,
|
||||
errorHandledByComponent = $bindable(false)
|
||||
}: Props = $props()
|
||||
|
||||
const { worldStore, app, componentControl } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
// Initialize outputs
|
||||
let outputs = initOutput($worldStore, id, {
|
||||
result: undefined as any,
|
||||
loading: false,
|
||||
jobId: undefined as string | undefined,
|
||||
messages: [] as Message[],
|
||||
userMessage: '' as string // Output for evalv2 field
|
||||
})
|
||||
|
||||
// Resolve configuration
|
||||
let resolvedConfig = $state(
|
||||
initConfig(components['chatcomponent'].initialData.configuration, configuration)
|
||||
)
|
||||
|
||||
// Initialize CSS
|
||||
let css = $state(initCss($app.css?.chatcomponent, customCss))
|
||||
|
||||
// State
|
||||
let runnableComponent: RunnableComponent | undefined = $state()
|
||||
let runnableWrapper: RunnableWrapper | undefined = $state()
|
||||
let loading = $state(false)
|
||||
let result: any = $state(undefined)
|
||||
let messages: Message[] = $state([])
|
||||
let inputValue = $state('')
|
||||
let messagesContainer: HTMLDivElement | undefined = $state()
|
||||
|
||||
// Streaming state management
|
||||
let currentStreamingMessageIndex: number | undefined = $state(undefined)
|
||||
let accumulatedContent = $state('')
|
||||
|
||||
// Generate stable memory_id for chat session (for agent memory persistence)
|
||||
let chatMemoryId = $state(randomUUID())
|
||||
|
||||
// Register component control for programmatic access
|
||||
$componentControl[id] = {
|
||||
sendMessage: (message: string) => {
|
||||
if (message && !loading) {
|
||||
inputValue = message
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom when messages change
|
||||
$effect(() => {
|
||||
if (messages.length > 0 && messagesContainer) {
|
||||
setTimeout(() => {
|
||||
messagesContainer?.scrollTo({
|
||||
top: messagesContainer.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}, 50)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle streaming updates
|
||||
function handleStreamUpdate(e: CustomEvent<{ id: string; result_stream: string }>) {
|
||||
const streamContent = e.detail.result_stream
|
||||
|
||||
const parsed = parseStreamDeltas(streamContent)
|
||||
if (parsed.content) {
|
||||
accumulatedContent = parsed.content
|
||||
} else {
|
||||
accumulatedContent = streamContent
|
||||
}
|
||||
|
||||
// Update or create streaming message
|
||||
if (currentStreamingMessageIndex !== undefined) {
|
||||
messages = messages.map((msg, idx) =>
|
||||
idx === currentStreamingMessageIndex ? { ...msg, content: accumulatedContent } : msg
|
||||
)
|
||||
} else {
|
||||
const assistantMessage: Message = {
|
||||
id: randomUUID(),
|
||||
role: 'assistant',
|
||||
content: accumulatedContent
|
||||
}
|
||||
messages = [...messages, assistantMessage]
|
||||
currentStreamingMessageIndex = messages.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Handle job completion
|
||||
function handleJobComplete(e: CustomEvent<{ id: string; result: any }>) {
|
||||
const finalResult = e.detail.result
|
||||
|
||||
// Extract final content
|
||||
let finalContent = ''
|
||||
if (typeof finalResult === 'string') {
|
||||
finalContent = finalResult
|
||||
} else if (finalResult && typeof finalResult === 'object' && 'output' in finalResult) {
|
||||
finalContent =
|
||||
typeof finalResult.output === 'string'
|
||||
? finalResult.output
|
||||
: JSON.stringify(finalResult.output, null, 2)
|
||||
} else {
|
||||
finalContent = JSON.stringify(finalResult, null, 2)
|
||||
}
|
||||
|
||||
// If we were streaming, update the message with final result to ensure completeness
|
||||
if (currentStreamingMessageIndex !== undefined && finalContent) {
|
||||
messages = messages.map((msg, idx) =>
|
||||
idx === currentStreamingMessageIndex ? { ...msg, content: finalContent } : msg
|
||||
)
|
||||
}
|
||||
// If not streaming, create new message
|
||||
else if (finalContent.length > 0) {
|
||||
messages = [
|
||||
...messages,
|
||||
{
|
||||
id: randomUUID(),
|
||||
role: 'assistant',
|
||||
content: finalContent
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Finalize streaming
|
||||
currentStreamingMessageIndex = undefined
|
||||
accumulatedContent = ''
|
||||
}
|
||||
|
||||
// Handle job error
|
||||
function handleJobError(e: CustomEvent<{ id: string; error: any }>) {
|
||||
const error = e.detail.error
|
||||
|
||||
// Add error message
|
||||
messages = [
|
||||
...messages,
|
||||
{
|
||||
id: randomUUID(),
|
||||
role: 'assistant',
|
||||
content: `Error: ${error.message || JSON.stringify(error)}`
|
||||
}
|
||||
]
|
||||
|
||||
// Reset streaming state
|
||||
currentStreamingMessageIndex = undefined
|
||||
accumulatedContent = ''
|
||||
}
|
||||
|
||||
// Handle send message
|
||||
async function handleSend() {
|
||||
if (!inputValue.trim() || loading) return
|
||||
|
||||
const userMessage = inputValue.trim()
|
||||
inputValue = ''
|
||||
|
||||
// Add user message to chat
|
||||
const newUserMessage: Message = {
|
||||
id: randomUUID(),
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
}
|
||||
messages = [...messages, newUserMessage]
|
||||
|
||||
// Reset streaming state for new message
|
||||
currentStreamingMessageIndex = undefined
|
||||
accumulatedContent = ''
|
||||
|
||||
// Update output so evalv2 field can reference it
|
||||
outputs.userMessage.set(userMessage)
|
||||
|
||||
// Trigger the runnable
|
||||
if (!runnableComponent) {
|
||||
runnableWrapper?.handleSideEffect(true)
|
||||
} else {
|
||||
await runnableComponent?.runComponent()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle enter key
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
errorHandledByComponent = resolvedConfig?.onError?.selected !== 'errorOverlay'
|
||||
})
|
||||
</script>
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
{#each Object.keys(components['chatcomponent'].initialData.configuration) as key (key)}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
{key}
|
||||
bind:resolvedConfig={resolvedConfig[key]}
|
||||
configuration={configuration[key]}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#each Object.keys(css ?? {}) as key (key)}
|
||||
<ResolveStyle
|
||||
{id}
|
||||
{customCss}
|
||||
{key}
|
||||
bind:css={css[key]}
|
||||
componentStyle={$app.css?.chatcomponent}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<RunnableWrapper
|
||||
bind:this={runnableWrapper}
|
||||
bind:runnableComponent
|
||||
bind:loading
|
||||
bind:result
|
||||
{componentInput}
|
||||
{id}
|
||||
{recomputeIds}
|
||||
{outputs}
|
||||
doOnSuccess={resolvedConfig.onSuccess}
|
||||
doOnError={resolvedConfig.onError}
|
||||
on:streamupdate={handleStreamUpdate}
|
||||
on:done={handleJobComplete}
|
||||
on:doneError={handleJobError}
|
||||
{errorHandledByComponent}
|
||||
autoRefresh={false}
|
||||
{render}
|
||||
extraQueryParams={{ memory_id: chatMemoryId }}
|
||||
>
|
||||
{#if render}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex flex-col h-full border rounded-lg bg-surface',
|
||||
css?.container?.class,
|
||||
'wm-chat-container'
|
||||
)}
|
||||
style={css?.container?.style}
|
||||
>
|
||||
<!-- Messages Container -->
|
||||
<div
|
||||
bind:this={messagesContainer}
|
||||
class={twMerge(
|
||||
'flex-1 overflow-y-auto p-4 bg-background',
|
||||
css?.messagesContainer?.class,
|
||||
'wm-chat-messages'
|
||||
)}
|
||||
style={css?.messagesContainer?.style}
|
||||
>
|
||||
{#if messages.length === 0}
|
||||
<div class="flex items-center justify-center h-full text-tertiary text-sm">
|
||||
No messages yet. Start a conversation!
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full space-y-4 xl:max-w-7xl mx-auto">
|
||||
{#each messages as message (message.id)}
|
||||
<ChatMessage
|
||||
role={message.role}
|
||||
content={message.content}
|
||||
enableMarkdown={true}
|
||||
enableS3Display={true}
|
||||
customCss={{
|
||||
userMessage: css?.userMessage,
|
||||
assistantMessage: css?.assistantMessage
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-2 text-tertiary">
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
<span class="text-sm">Processing...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Input Container -->
|
||||
<div
|
||||
class={twMerge('border-t p-3', css?.inputContainer?.class, 'wm-chat-input')}
|
||||
style={css?.inputContainer?.style}
|
||||
>
|
||||
<ChatInput
|
||||
bind:value={inputValue}
|
||||
placeholder={resolvedConfig.placeholder}
|
||||
disabled={loading}
|
||||
onSend={handleSend}
|
||||
onKeydown={handleKeydown}
|
||||
customCss={{
|
||||
input: css?.input,
|
||||
button: css?.button
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</RunnableWrapper>
|
||||
@@ -6,17 +6,17 @@
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Map, View, Feature } from 'ol'
|
||||
import { useGeographic } from 'ol/proj'
|
||||
import { OSM, Vector as VectorSource } from 'ol/source'
|
||||
import { Vector as VectorLayer, Tile as TileLayer } from 'ol/layer'
|
||||
import { Point } from 'ol/geom'
|
||||
import { defaults as defaultControls } from 'ol/control'
|
||||
import { useGeographic } from 'ol/proj.js'
|
||||
import { OSM, Vector as VectorSource } from 'ol/source.js'
|
||||
import { Vector as VectorLayer, Tile as TileLayer } from 'ol/layer.js'
|
||||
import { Point } from 'ol/geom.js'
|
||||
import { defaults as defaultControls } from 'ol/control.js'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import { components } from '../../editor/component'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import { Style, Circle, Fill, Stroke, Text } from 'ol/style'
|
||||
import { Style, Circle, Fill, Stroke, Text } from 'ol/style.js'
|
||||
import { findGridItem } from '../../editor/appUtilsCore'
|
||||
interface Marker {
|
||||
lon: number
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
result_stream?: string
|
||||
}) {
|
||||
setResult(nresult_stream, id, false)
|
||||
dispatch('streamupdate', { id, result_stream: nresult_stream })
|
||||
},
|
||||
cancel({ id }: { id: string }) {
|
||||
onCancel?.()
|
||||
@@ -442,7 +443,8 @@
|
||||
$appPath,
|
||||
id,
|
||||
await buildRequestBody(dynamicArgsOverride),
|
||||
inlineScriptOverride
|
||||
inlineScriptOverride,
|
||||
extraQueryParams
|
||||
)
|
||||
if (isEditor) {
|
||||
addJob(uuid)
|
||||
@@ -473,35 +475,36 @@
|
||||
}
|
||||
|
||||
export async function buildRequestBody(dynamicArgsOverride: Record<string, any> | undefined) {
|
||||
const nonStaticRunnableInputs = dynamicArgsOverride ?? {}
|
||||
const staticRunnableInputs = {}
|
||||
const nonStaticRunnableInputs: Record<string, any> = dynamicArgsOverride ?? {}
|
||||
const staticRunnableInputs: Record<string, any> = {}
|
||||
const allowUserResources: string[] = []
|
||||
|
||||
for (const k of Object.keys(fields ?? {})) {
|
||||
let field = fields[k]
|
||||
if (field?.type == 'static' && fields[k]) {
|
||||
const field = fields[k]
|
||||
|
||||
if (
|
||||
isEditor &&
|
||||
['user', 'evalv2', 'connected'].includes(field.type) &&
|
||||
'allowUserResources' in field &&
|
||||
field.allowUserResources
|
||||
) {
|
||||
allowUserResources.push(k)
|
||||
}
|
||||
|
||||
if (field?.type == 'static') {
|
||||
if (isEditor) {
|
||||
staticRunnableInputs[k] = field.value
|
||||
}
|
||||
} else if (field?.type == 'user') {
|
||||
nonStaticRunnableInputs[k] = args?.[k]
|
||||
if (isEditor && field.allowUserResources) {
|
||||
allowUserResources.push(k)
|
||||
}
|
||||
} else if (field?.type == 'eval' || (field?.type == 'evalv2' && inputValues[k])) {
|
||||
const ctxMatch = field?.expr?.match(ctxRegex)
|
||||
if (ctxMatch) {
|
||||
nonStaticRunnableInputs[k] = '$ctx:' + ctxMatch[1]
|
||||
} else {
|
||||
// console.log('k', k)
|
||||
nonStaticRunnableInputs[k] = await inputValues[k]?.computeExpr()
|
||||
}
|
||||
if (isEditor && field?.type == 'evalv2' && field.allowUserResources) {
|
||||
allowUserResources.push(k)
|
||||
}
|
||||
} else {
|
||||
if (isEditor && field?.type == 'connected' && field.allowUserResources) {
|
||||
allowUserResources.push(k)
|
||||
}
|
||||
nonStaticRunnableInputs[k] = runnableInputValues[k]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,7 @@
|
||||
on:cancel
|
||||
on:recompute
|
||||
on:argsChanged
|
||||
on:streamupdate
|
||||
on:resultSet={(e) => {
|
||||
const res = e.detail
|
||||
if ($initialized?.runnableInitialized?.[fullId] === undefined) {
|
||||
|
||||
@@ -31,7 +31,7 @@ function create_context_function_template(
|
||||
) {
|
||||
let hasReturnAsLastLine = noReturn || eval_string.split('\n').some((x) => x.startsWith('return '))
|
||||
return `
|
||||
return async function (context, state, createProxy, goto, setTab, recompute, globalRecompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast, waitJob, askNewResource, downloadFile) {
|
||||
return async function (context, state, createProxy, goto, setTab, recompute, globalRecompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, sendMessage, showToast, waitJob, askNewResource, downloadFile) {
|
||||
"use strict";
|
||||
${
|
||||
contextKeys && contextKeys.length > 0
|
||||
@@ -68,6 +68,7 @@ type WmFunctor = (
|
||||
invalidate,
|
||||
validateAll,
|
||||
clearFiles,
|
||||
sendMessage,
|
||||
showToast,
|
||||
waitJob,
|
||||
askNewResource,
|
||||
@@ -123,6 +124,7 @@ export async function eval_like(
|
||||
waitJob?: (jobId: string) => void
|
||||
askNewResource?: () => void
|
||||
setGroupValue?: (key: string, value: any) => void
|
||||
sendMessage?: (message: string) => void
|
||||
}
|
||||
>,
|
||||
worldStore: World | undefined,
|
||||
@@ -246,6 +248,9 @@ export async function eval_like(
|
||||
(id) => {
|
||||
controlComponents[id]?.clearFiles?.()
|
||||
},
|
||||
(id, message) => {
|
||||
controlComponents[id]?.sendMessage?.(message)
|
||||
},
|
||||
(message, error) => {
|
||||
sendUserToast(message, error)
|
||||
},
|
||||
|
||||
@@ -11,7 +11,8 @@ export async function executeRunnable(
|
||||
path: string,
|
||||
id: string,
|
||||
requestBody: ExecuteComponentData['requestBody'],
|
||||
inlineScriptOverride?: InlineScript
|
||||
inlineScriptOverride?: InlineScript,
|
||||
queryParams?: Record<string, any>
|
||||
) {
|
||||
let appPath = defaultIfEmptyString(path, `u/${username ?? 'unknown'}/newapp`)
|
||||
if (runnable?.type === 'runnableByName') {
|
||||
@@ -40,6 +41,10 @@ export async function executeRunnable(
|
||||
requestBody['version'] = version
|
||||
}
|
||||
|
||||
if (queryParams && Object.keys(queryParams).length > 0) {
|
||||
requestBody['run_query_params'] = queryParams
|
||||
}
|
||||
|
||||
const uuid = await AppService.executeComponent({
|
||||
workspace,
|
||||
path: appPath,
|
||||
|
||||
@@ -676,6 +676,7 @@
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
$darkMode = newDarkMode ?? globalDarkMode
|
||||
$app.darkMode = newDarkMode
|
||||
}
|
||||
|
||||
let priorDarkMode = document.documentElement.classList.contains('dark')
|
||||
|
||||
@@ -159,71 +159,6 @@
|
||||
sendUserToast('App reporting disabled')
|
||||
}
|
||||
|
||||
const appPreviewScript = `import puppeteer from 'puppeteer-core';
|
||||
import dayjs from 'dayjs';
|
||||
export async function main(
|
||||
app_path: string,
|
||||
startup_duration = 5,
|
||||
kind: 'pdf' | 'png' = 'pdf',
|
||||
customWidth: null | number,
|
||||
customHeight: null | number,
|
||||
) {
|
||||
let browser = null
|
||||
try {
|
||||
browser = await puppeteer.launch({ headless: true, executablePath: '/usr/bin/chromium', args: ['--no-sandbox',
|
||||
'--no-zygote',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setCookie({
|
||||
"name": "token",
|
||||
"value": Bun.env["WM_TOKEN"],
|
||||
"domain": Bun.env["BASE_URL"]?.replace(/https?:\\/\\//, '')
|
||||
})
|
||||
page
|
||||
.on('console', msg =>
|
||||
console.log(dayjs().format("HH:mm:ss") + " " + msg.type().substr(0, 3).toUpperCase() + " " + msg.text()))
|
||||
.on('pageerror', ({ msg }) => console.log(dayjs().format("HH:mm:ss") + " " + msg));
|
||||
await page.setViewport({ width: 1200, height: 2000 });
|
||||
await page.goto(Bun.env["BASE_URL"] + '/apps/get/' + app_path + '?workspace=' + Bun.env["WM_WORKSPACE"] + "&hideRefreshBar=true&hideEditBtn=true");
|
||||
await page.waitForSelector("#app-content", { timeout: 20000 })
|
||||
|
||||
const elem = await page.$('#app-content');
|
||||
let width: null | number = customWidth || 1200
|
||||
let height: null | number = customHeight || (await elem.boundingBox()).height
|
||||
await page.setViewport({ width, height });
|
||||
await new Promise((resolve, _) => {
|
||||
setTimeout(resolve, startup_duration * 1000)
|
||||
})
|
||||
try {
|
||||
await page.$eval("#sidebar", el => el.remove())
|
||||
} catch {}
|
||||
await page.$eval("#content", el => el.classList.remove("md:pl-12"))
|
||||
await page.$$eval(".app-component-refresh-btn", els => els.forEach(el => el.remove()))
|
||||
await page.$$eval(".app-table-footer-btn", els => els.forEach(el => el.remove()))
|
||||
|
||||
await new Promise((resolve, _) => setTimeout(resolve, 500))
|
||||
|
||||
const screenshot = kind === "pdf" ? await page.pdf({
|
||||
printBackground: true,
|
||||
width,
|
||||
height
|
||||
}) : await page.screenshot({
|
||||
fullPage: true,
|
||||
type: "png",
|
||||
captureBeyondViewport: false
|
||||
});
|
||||
await browser.close();
|
||||
return Buffer.from(screenshot).toString('base64');
|
||||
} catch (err) {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}`
|
||||
|
||||
const notificationScripts = {
|
||||
discord: {
|
||||
path: hubPaths.discordReport,
|
||||
@@ -347,10 +282,9 @@ export async function main(
|
||||
{
|
||||
id: 'a',
|
||||
value: {
|
||||
type: 'rawscript' as const,
|
||||
tag: 'chromium',
|
||||
content: appPreviewScript,
|
||||
language: 'bun' as const,
|
||||
type: 'script' as const,
|
||||
tag_override: 'chromium',
|
||||
path: hubPaths.appReport,
|
||||
input_transforms: {
|
||||
app_path: {
|
||||
expr: 'flow_input.app_path',
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
import AppDateInput from '../../components/inputs/AppDateInput.svelte'
|
||||
import AppSelect from '../../components/inputs/AppSelect.svelte'
|
||||
import AppBarChart from '../../components/display/AppBarChart.svelte'
|
||||
import AppChat from '../../components/display/AppChat.svelte'
|
||||
import AppDivider from '../../components/layout/AppDivider.svelte'
|
||||
import AppRangeInput from '../../components/inputs/AppRangeInput.svelte'
|
||||
import AppTabs from '../../components/layout/AppTabs.svelte'
|
||||
@@ -110,6 +111,7 @@
|
||||
'carousellistcomponent',
|
||||
'chartjscomponent',
|
||||
'chartjscomponentv2',
|
||||
'chatcomponent',
|
||||
'checkboxcomponent',
|
||||
'codeinputcomponent',
|
||||
'conditionalwrapper',
|
||||
@@ -340,6 +342,16 @@
|
||||
xData={component.xData}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'chatcomponent'}
|
||||
<AppChat
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'checkboxcomponent'}
|
||||
<AppCheckbox
|
||||
id={component.id}
|
||||
|
||||
@@ -54,7 +54,8 @@ import {
|
||||
RefreshCw,
|
||||
ListCollapse,
|
||||
GalleryThumbnails,
|
||||
Code
|
||||
Code,
|
||||
MessageSquare
|
||||
} from 'lucide-svelte'
|
||||
import type {
|
||||
Aligned,
|
||||
@@ -208,6 +209,7 @@ export type AggridInfiniteComponentEe = BaseComponent<'aggridinfinitecomponentee
|
||||
}
|
||||
|
||||
export type DisplayComponent = BaseComponent<'displaycomponent'>
|
||||
export type ChatComponent = BaseComponent<'chatcomponent'> & RecomputeOthersSource
|
||||
export type JobIdDisplayComponent = BaseComponent<'jobiddisplaycomponent'>
|
||||
export type LogComponent = BaseComponent<'logcomponent'>
|
||||
export type JobIdLogComponent = BaseComponent<'jobidlogcomponent'>
|
||||
@@ -338,6 +340,7 @@ export type RecomputeAllComponent = BaseComponent<'recomputeallcomponent'>
|
||||
export type TypedComponent =
|
||||
| DBExplorerComponent
|
||||
| DisplayComponent
|
||||
| ChatComponent
|
||||
| LogComponent
|
||||
| JobIdLogComponent
|
||||
| FlowStatusComponent
|
||||
@@ -461,6 +464,23 @@ export type AppComponentConfig<T extends TypedComponent['type']> = {
|
||||
*/
|
||||
initialData: InitialAppComponent
|
||||
customCss: ComponentCustomCSS<T>
|
||||
/**
|
||||
* Optional configuration for runnable inputs validation
|
||||
*/
|
||||
runnableInputsInfo?: {
|
||||
/**
|
||||
* Function to validate runnable inputs and return a warning if needed
|
||||
* @param fields - The fields object from componentInput.fields
|
||||
* @returns Warning object with type, title, and message, or undefined if valid
|
||||
*/
|
||||
validate?: (fields: Record<string, any>) =>
|
||||
| {
|
||||
type: 'warning' | 'error' | 'info'
|
||||
title: string
|
||||
message: string
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
}
|
||||
|
||||
export type PresetComponentConfig = {
|
||||
@@ -1128,6 +1148,56 @@ export const components = {
|
||||
}
|
||||
}
|
||||
},
|
||||
chatcomponent: {
|
||||
name: 'Chat',
|
||||
icon: MessageSquare,
|
||||
documentationLink: `${documentationBaseUrl}/chat`,
|
||||
dims: '3:8-6:12' as AppComponentDimensions,
|
||||
customCss: {
|
||||
container: { class: '', style: '' },
|
||||
messagesContainer: { class: '', style: '' },
|
||||
inputContainer: { class: '', style: '' },
|
||||
userMessage: { class: '', style: '' },
|
||||
assistantMessage: { class: '', style: '' },
|
||||
input: { class: '', style: '' },
|
||||
button: { class: '', style: '' }
|
||||
},
|
||||
runnableInputsInfo: {
|
||||
validate: (fields) => {
|
||||
const fieldNames = Object.keys(fields)
|
||||
const hasUserMessage = fieldNames.includes('user_message')
|
||||
|
||||
if (!hasUserMessage) {
|
||||
return {
|
||||
type: 'warning' as const,
|
||||
title: 'Chat input configuration',
|
||||
message:
|
||||
'The chat component requires a <code>user_message</code> parameter to work. Please add it to your event handler.'
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
componentInput: {
|
||||
type: 'runnable',
|
||||
fieldType: 'any',
|
||||
fields: {},
|
||||
runnable: undefined
|
||||
},
|
||||
recomputeIds: true,
|
||||
configuration: {
|
||||
placeholder: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
value: 'Type a message...'
|
||||
},
|
||||
onSuccess: onSuccessClick,
|
||||
onError: onErrorClick
|
||||
}
|
||||
}
|
||||
},
|
||||
jobidlogcomponent: {
|
||||
name: 'Log by Job Id',
|
||||
icon: Monitor,
|
||||
@@ -2343,12 +2413,7 @@ This is a paragraph.
|
||||
documentationLink: `${documentationBaseUrl}/select`,
|
||||
dims: '2:1-3:1' as AppComponentDimensions,
|
||||
customCss: {
|
||||
input: {
|
||||
style: '',
|
||||
tooltip:
|
||||
'https://github.com/rob-balfre/svelte-select/blob/master/docs/theming_variables.md',
|
||||
class: ''
|
||||
}
|
||||
input: { style: '', class: '' }
|
||||
},
|
||||
initialData: {
|
||||
recomputeIds: true,
|
||||
|
||||
@@ -765,5 +765,18 @@ return {
|
||||
}`,
|
||||
python3: `def main():
|
||||
return [{"foo": 1}, {"foo": 2}, {"foo": 3}]`
|
||||
},
|
||||
chatcomponent: {
|
||||
deno: `export async function main(user_message: string) {
|
||||
// Process the user message here
|
||||
// You can call APIs, query databases, use AI models, etc.
|
||||
|
||||
return "Hi, how are you?";
|
||||
}`,
|
||||
python3: `def main(user_message: str):
|
||||
# Process the user message here
|
||||
# You can call APIs, query databases, use AI models, etc.
|
||||
|
||||
return "Hi, how are you?"`
|
||||
}
|
||||
} as const
|
||||
|
||||
@@ -73,6 +73,7 @@ const display: ComponentSet = {
|
||||
'htmlcomponent',
|
||||
'mardowncomponent',
|
||||
'pdfcomponent',
|
||||
'chatcomponent',
|
||||
'displaycomponent',
|
||||
'jobidlogcomponent',
|
||||
'jobidflowstatuscomponent',
|
||||
|
||||
@@ -83,6 +83,12 @@ const validateAll = {
|
||||
example: 'validateAll(id: string, key: string)'
|
||||
}
|
||||
|
||||
const sendMessage = {
|
||||
title: 'sendMessage',
|
||||
description: 'Programmatically send a message to the chat component',
|
||||
example: 'sendMessage(id: string, message: string)'
|
||||
}
|
||||
|
||||
export function getComponentControl(type: keyof typeof components): Array<ComponentFunction> {
|
||||
switch (type) {
|
||||
case 'tabscomponent':
|
||||
@@ -135,6 +141,8 @@ export function getComponentControl(type: keyof typeof components): Array<Compon
|
||||
return [setSelectedIndex]
|
||||
case 'dbexplorercomponent':
|
||||
return [recompute]
|
||||
case 'chatcomponent':
|
||||
return [sendMessage]
|
||||
default:
|
||||
if (components[type].initialData['componentInput']) {
|
||||
return [recompute]
|
||||
|
||||
@@ -76,7 +76,11 @@ export const authorizedClassnames = [
|
||||
'wm-conditional-tabs',
|
||||
|
||||
'wm-rich-result-header',
|
||||
'wm-rich-result-container'
|
||||
'wm-rich-result-container',
|
||||
|
||||
'wm-chat-container',
|
||||
'wm-chat-messages',
|
||||
'wm-chat-input'
|
||||
// TODO: Select and mutltiselect
|
||||
]
|
||||
|
||||
@@ -619,6 +623,27 @@ export const customisationByComponent: Customisation[] = [
|
||||
],
|
||||
variables: []
|
||||
},
|
||||
{
|
||||
components: ['chatcomponent'],
|
||||
selectors: [
|
||||
{
|
||||
selector: '.wm-chat-container',
|
||||
comment: 'Main chat container',
|
||||
customCssKey: 'container'
|
||||
},
|
||||
{
|
||||
selector: '.wm-chat-messages',
|
||||
comment: 'Messages container',
|
||||
customCssKey: 'messagesContainer'
|
||||
},
|
||||
{
|
||||
selector: '.wm-chat-input',
|
||||
comment: 'Input container',
|
||||
customCssKey: 'inputContainer'
|
||||
}
|
||||
],
|
||||
variables: []
|
||||
},
|
||||
{
|
||||
components: ['mardowncomponent'],
|
||||
selectors: [
|
||||
|
||||
@@ -649,6 +649,14 @@ export const quickStyleProperties: Record<
|
||||
header: [...containerDefaultProps, typographyGrouping],
|
||||
container: containerDefaultProps
|
||||
},
|
||||
chatcomponent: {
|
||||
container: containerDefaultProps,
|
||||
messagesContainer: containerDefaultProps,
|
||||
userMessage: containerDefaultProps,
|
||||
assistantMessage: containerDefaultProps,
|
||||
input: containerDefaultProps,
|
||||
button: buttonDefaultProps
|
||||
},
|
||||
logcomponent: {
|
||||
header: [...containerDefaultProps, typographyGrouping],
|
||||
container: containerDefaultProps
|
||||
|
||||
+9
-45
@@ -25,6 +25,7 @@
|
||||
import EditorSettings from '$lib/components/EditorSettings.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import { convertManagedFieldsToEvalv2 } from '$lib/components/apps/components/componentManagedFields'
|
||||
|
||||
const {
|
||||
runnableComponents,
|
||||
@@ -127,55 +128,18 @@
|
||||
fieldType: 'number'
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
componentType === 'aggridinfinitecomponent' ||
|
||||
componentType === 'aggridinfinitecomponentee'
|
||||
) {
|
||||
newFields['offset'] = {
|
||||
type: 'evalv2',
|
||||
expr: `${id}.params.offset`,
|
||||
fieldType: 'number'
|
||||
}
|
||||
newFields['limit'] = {
|
||||
type: 'evalv2',
|
||||
expr: `${id}.params.limit`,
|
||||
fieldType: 'number'
|
||||
}
|
||||
newFields['orderBy'] = {
|
||||
type: 'evalv2',
|
||||
expr: `${id}.params.orderBy`,
|
||||
fieldType: 'string'
|
||||
}
|
||||
newFields['isDesc'] = {
|
||||
type: 'evalv2',
|
||||
expr: `${id}.params.isDesc`,
|
||||
fieldType: 'boolean'
|
||||
}
|
||||
newFields['search'] = {
|
||||
type: 'evalv2',
|
||||
expr: `${id}.params.search`,
|
||||
fieldType: 'string'
|
||||
}
|
||||
} else {
|
||||
// Convert component-managed fields to evalv2 type using centralized utility
|
||||
const convertedFields = convertManagedFieldsToEvalv2(componentType, id, newFields)
|
||||
Object.assign(newFields, convertedFields)
|
||||
}
|
||||
}
|
||||
|
||||
function assertConnections(newFields) {
|
||||
if (
|
||||
componentType === 'aggridinfinitecomponent' ||
|
||||
componentType === 'aggridinfinitecomponentee'
|
||||
) {
|
||||
const fields = ['offset', 'limit', 'orderBy', 'isDesc', 'search']
|
||||
|
||||
fields.forEach((field) => {
|
||||
if (newFields[field]?.type !== 'evalv2') {
|
||||
newFields[field] = {
|
||||
type: 'evalv2',
|
||||
expr: `${id}.params.${field}`,
|
||||
fieldType: newFields[field]?.fieldType ?? 'string'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// Convert component-managed fields to evalv2 type using centralized utility
|
||||
// This ensures that even if fields were somehow changed, they remain as evalv2
|
||||
const convertedFields = convertManagedFieldsToEvalv2(componentType ?? '', id, newFields)
|
||||
Object.assign(newFields, convertedFields)
|
||||
}
|
||||
|
||||
async function loadSchemaAndInputsByName() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext, GridItem, RichConfiguration } from '../../types'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
@@ -37,6 +38,7 @@
|
||||
import ComponentPanelDataSource from './ComponentPanelDataSource.svelte'
|
||||
import MenuItems from './MenuItems.svelte'
|
||||
import DecisionTreeGraphEditor from './DecisionTreeGraphEditor.svelte'
|
||||
import { getManagedFields } from '$lib/components/apps/components/componentManagedFields'
|
||||
import GridAgChartsLicenseKe from './GridAgChartsLicenseKe.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import ContextVariables from './ContextVariables.svelte'
|
||||
@@ -147,6 +149,10 @@
|
||||
? ccomponents[item?.data?.type]?.initialData?.componentInput
|
||||
: undefined
|
||||
|
||||
const runnableInputsInfo = item?.data?.type
|
||||
? ccomponents[item.data.type]?.runnableInputsInfo
|
||||
: undefined
|
||||
|
||||
const hasInteraction = item.data.type ? isTriggerable(item.data.type) : false
|
||||
|
||||
let evalV2editor: EvalV2InputEditor | undefined = $state(undefined)
|
||||
@@ -355,6 +361,19 @@
|
||||
parameters this component is attached to.
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<!-- Generic runnable inputs validation -->
|
||||
{#if runnableInputsInfo?.validate}
|
||||
{@const validation = runnableInputsInfo?.validate(
|
||||
item.data.componentInput.fields ?? {}
|
||||
)}
|
||||
{#if validation}
|
||||
<Alert type={validation.type} title={validation.title} size="xs" class="my-2">
|
||||
{@html validation.message}
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<InputsSpecsEditor
|
||||
id={component.id}
|
||||
shouldCapitalize={false}
|
||||
@@ -367,10 +386,7 @@
|
||||
acceptSelf={component.type === 'aggridinfinitecomponent' ||
|
||||
component.type === 'aggridinfinitecomponentee' ||
|
||||
component.type === 'steppercomponent'}
|
||||
overridenByComponent={component.type === 'aggridinfinitecomponent' ||
|
||||
component.type === 'aggridinfinitecomponentee'
|
||||
? ['offset', 'limit', 'orderBy', 'isDesc', 'search']
|
||||
: []}
|
||||
overridenByComponent={getManagedFields(component.type)}
|
||||
securedContext
|
||||
/>
|
||||
</div>
|
||||
|
||||
+9
-1
@@ -6,6 +6,7 @@
|
||||
import RunnableSelector from '../mainInput/RunnableSelector.svelte'
|
||||
import SelectedRunnable from '../SelectedRunnable.svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '$lib/components/apps/types'
|
||||
import { convertManagedFieldsToEvalv2 } from '$lib/components/apps/components/componentManagedFields'
|
||||
|
||||
interface Props {
|
||||
appInput: ResultAppInput
|
||||
@@ -26,7 +27,14 @@
|
||||
fields: Record<string, StaticAppInput>
|
||||
}) {
|
||||
if (appInput.type === 'runnable') {
|
||||
appInput = { ...appInput, runnable, fields }
|
||||
// Convert component-managed fields from static to evalv2 type
|
||||
// This ensures they are properly handled at runtime and not added to force_viewer_static_fields
|
||||
const convertedFields = convertManagedFieldsToEvalv2(
|
||||
appComponent.type,
|
||||
appComponent.id,
|
||||
fields
|
||||
) as Record<string, StaticAppInput>
|
||||
appInput = { ...appInput, runnable, fields: convertedFields }
|
||||
$selectedComponentInEditor = appComponent.id
|
||||
} else {
|
||||
console.warn('Cannot pick runnable for non-runnable input')
|
||||
|
||||
@@ -33,7 +33,7 @@ export function isFrontend(runnable: Runnable): boolean {
|
||||
}
|
||||
|
||||
export function isTriggerable(componentType: string): boolean {
|
||||
return ['buttoncomponent', 'formbuttoncomponent', 'formcomponent', 'steppercomponent'].includes(
|
||||
return ['buttoncomponent', 'formbuttoncomponent', 'formcomponent', 'steppercomponent', 'chatcomponent'].includes(
|
||||
componentType
|
||||
)
|
||||
}
|
||||
@@ -55,7 +55,12 @@ export function getAllTriggerEvents(
|
||||
const isTriggerableComponent = isTriggerable(appComponent.type)
|
||||
|
||||
if (isTriggerableComponent) {
|
||||
events.push('click')
|
||||
// Use 'send' for chat component, 'click' for others
|
||||
if (appComponent.type === 'chatcomponent') {
|
||||
events.push('send')
|
||||
} else {
|
||||
events.push('click')
|
||||
}
|
||||
|
||||
if (triggerOnAppLoad) {
|
||||
events.push('start')
|
||||
|
||||
@@ -295,6 +295,7 @@ export type AppViewerContext = {
|
||||
invalidate?: (key: string, error: string) => void
|
||||
validateAll?: () => void
|
||||
clearFiles?: () => void
|
||||
sendMessage?: (message: string) => void
|
||||
showToast?: (message: string, error?: boolean) => void
|
||||
recompute?: () => void
|
||||
askNewResource?: () => void
|
||||
|
||||
@@ -345,6 +345,12 @@ declare function validateAll(id: string): void;
|
||||
*/
|
||||
declare function clearFiles(id: string): void;
|
||||
|
||||
/** Send a message to a chat component
|
||||
* @param id component's id
|
||||
* @param message message to send
|
||||
*/
|
||||
declare function sendMessage(id: string, message: string): void;
|
||||
|
||||
/** Display a toast message
|
||||
* @param message message to display
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { ArrowUp, Square } from 'lucide-svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
import { createBubbler, stopPropagation } from 'svelte/legacy'
|
||||
|
||||
const bubble = createBubbler()
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
onSend: () => void
|
||||
onKeydown?: (e: KeyboardEvent) => void
|
||||
customCss?: {
|
||||
container?: { class?: string; style?: string }
|
||||
input?: { class?: string; style?: string }
|
||||
button?: { class?: string; style?: string }
|
||||
}
|
||||
bindTextarea?: HTMLTextAreaElement
|
||||
showCancelButton?: boolean
|
||||
onCancel?: () => void
|
||||
cancelTitle?: string
|
||||
sendTitle?: string
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(),
|
||||
placeholder = 'Type your message here...',
|
||||
disabled = false,
|
||||
onSend,
|
||||
onKeydown = undefined,
|
||||
customCss = undefined,
|
||||
bindTextarea = $bindable(undefined),
|
||||
showCancelButton = false,
|
||||
onCancel = undefined,
|
||||
cancelTitle = 'Cancel execution',
|
||||
sendTitle = 'Send message'
|
||||
}: Props = $props()
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
onKeydown?.(e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-lg border border-gray-200 dark:border-gray-600 bg-surface-input px-3 py-2 {customCss
|
||||
?.container?.class ?? ''}"
|
||||
style={customCss?.container?.style}
|
||||
>
|
||||
<textarea
|
||||
bind:this={bindTextarea}
|
||||
bind:value
|
||||
use:autosize
|
||||
onkeydown={handleKeydown}
|
||||
onpointerdown={stopPropagation(bubble('pointerdown'))}
|
||||
{placeholder}
|
||||
class="flex-1 min-h-[24px] max-h-32 resize-none !border-0 text-sm placeholder-gray-400 !outline-none !ring-0 p-0 !shadow-none focus:!border-0 focus:!outline-none focus:!ring-0 focus:!shadow-none {customCss
|
||||
?.input?.class ?? ''}"
|
||||
style={customCss?.input?.style}
|
||||
rows={3}
|
||||
></textarea>
|
||||
{#if showCancelButton && onCancel}
|
||||
<Button
|
||||
color="red"
|
||||
size="xs2"
|
||||
btnClasses="!rounded-full !p-1.5 {customCss?.button?.class ?? ''}"
|
||||
style={customCss?.button?.style}
|
||||
startIcon={{ icon: Square }}
|
||||
on:click={onCancel}
|
||||
iconOnly
|
||||
title={cancelTitle}
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
color="blue"
|
||||
size="xs2"
|
||||
btnClasses="!rounded-full !p-1.5 {customCss?.button?.class ?? ''}"
|
||||
style={customCss?.button?.style}
|
||||
startIcon={{ icon: ArrowUp }}
|
||||
disabled={!value.trim() || disabled}
|
||||
on:click={onSend}
|
||||
iconOnly
|
||||
title={sendTitle}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { Markdown } from 'svelte-exmarkdown'
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
import { Loader2, CheckCircle2, AlertTriangle } from 'lucide-svelte'
|
||||
import CodeDisplay from '$lib/components/copilot/chat/script/CodeDisplay.svelte'
|
||||
import LinkRenderer from '$lib/components/copilot/chat/LinkRenderer.svelte'
|
||||
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
interface Props {
|
||||
role: 'user' | 'assistant' | 'tool' | 'system'
|
||||
content: string
|
||||
loading?: boolean
|
||||
success?: boolean
|
||||
stepName?: string
|
||||
enableMarkdown?: boolean
|
||||
enableS3Display?: boolean
|
||||
customCss?: {
|
||||
userMessage?: { class?: string; style?: string }
|
||||
assistantMessage?: { class?: string; style?: string }
|
||||
}
|
||||
}
|
||||
|
||||
let {
|
||||
role,
|
||||
content,
|
||||
loading = false,
|
||||
success = undefined,
|
||||
stepName = undefined,
|
||||
enableMarkdown = true,
|
||||
enableS3Display = true,
|
||||
customCss = undefined
|
||||
}: Props = $props()
|
||||
|
||||
// Parse S3 objects if enabled
|
||||
const s3Object = $derived.by(() => {
|
||||
if (!enableS3Display || role !== 'assistant' || !content) return undefined
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
if (parsed?.type === 'windmill_s3_object' && parsed?.s3) {
|
||||
return parsed
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const messageClass = $derived.by(() => {
|
||||
const base = 'max-w-[90%] min-w-0 rounded-lg w-fit break-words'
|
||||
if (role === 'user') {
|
||||
const userClass = `${base} ml-auto bg-surface-secondary p-3`
|
||||
return customCss?.userMessage?.class
|
||||
? `${userClass} ${customCss.userMessage.class}`
|
||||
: userClass
|
||||
}
|
||||
// assistant, tool, and system messages use the same styling
|
||||
const assistantClass = `${base} mr-auto bg-surface border ${success !== false ? 'border-gray-200 dark:border-gray-600' : '!border-red-500'}`
|
||||
return customCss?.assistantMessage?.class
|
||||
? `${assistantClass} ${customCss.assistantMessage.class}`
|
||||
: assistantClass
|
||||
})
|
||||
|
||||
const messageStyle = $derived.by(() => {
|
||||
if (role === 'user') {
|
||||
return customCss?.userMessage?.style
|
||||
}
|
||||
return customCss?.assistantMessage?.style
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class={messageClass} style={messageStyle}>
|
||||
{#if stepName}
|
||||
<div
|
||||
class="bg-surface-secondary text-2xs text-tertiary mb-2 font-medium py-1 px-2 rounded-t-lg"
|
||||
>
|
||||
{stepName}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if role === 'user'}
|
||||
<p class="whitespace-pre-wrap text-sm text-right">{content}</p>
|
||||
{:else if loading}
|
||||
<div class="flex items-center gap-2 text-tertiary px-3 py-3">
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
<span>Processing...</span>
|
||||
</div>
|
||||
{:else if content}
|
||||
{#if s3Object}
|
||||
<div class="px-3 pb-3 {!stepName ? 'pt-3' : ''}">
|
||||
<DisplayResult result={s3Object} workspaceId={$workspaceStore} noControls={true} />
|
||||
</div>
|
||||
{:else if enableMarkdown}
|
||||
<div
|
||||
class="flex flex-row items-center gap-2 px-3 pb-3 text-sm {!stepName
|
||||
? 'pt-3'
|
||||
: ''} overflow-x-auto"
|
||||
>
|
||||
{#if role === 'tool'}
|
||||
{#if success !== false}
|
||||
<CheckCircle2 class="w-3.5 h-3.5 text-green-500" />
|
||||
{:else}
|
||||
<AlertTriangle class="w-3.5 h-3.5 text-red-500" />
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="prose prose-sm dark:prose-invert break-words prose-headings:!text-base">
|
||||
<Markdown
|
||||
md={content}
|
||||
plugins={[
|
||||
gfmPlugin(),
|
||||
{
|
||||
renderer: {
|
||||
pre: CodeDisplay,
|
||||
a: LinkRenderer
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="whitespace-pre-wrap text-sm px-3 pb-3 {!stepName ? 'pt-3' : ''}">{content}</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-tertiary text-sm px-3 py-3">No result</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
export function parseStreamDeltas(streamData: string): {
|
||||
content: string
|
||||
type?: string
|
||||
success?: boolean
|
||||
} {
|
||||
const lines = streamData.trim().split('\n')
|
||||
let content = ''
|
||||
let type = 'message'
|
||||
let success = true
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue
|
||||
try {
|
||||
const parsed = JSON.parse(line)
|
||||
if (parsed.type === 'tool_result') {
|
||||
type = 'tool_result'
|
||||
success = parsed.success
|
||||
const toolName = parsed.function_name
|
||||
content = success ? `Used ${toolName} tool` : `Failed to use ${toolName} tool`
|
||||
}
|
||||
if (parsed.type === 'token_delta' && parsed.content) {
|
||||
content += parsed.content
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse stream line:', line, e)
|
||||
}
|
||||
}
|
||||
|
||||
return { content, type, success }
|
||||
}
|
||||
+5
-14
@@ -10,6 +10,7 @@
|
||||
import ToggleButtonGroup from '../toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../toggleButton-v2/ToggleButton.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
import Badge from '../badge/Badge.svelte'
|
||||
|
||||
interface Props {
|
||||
open?: boolean
|
||||
@@ -132,32 +133,22 @@
|
||||
value={'discard'}
|
||||
{item}
|
||||
small
|
||||
class="data-[state=on]:text-white data-[state=on]:bg-red-400 w-[54px] justify-center"
|
||||
class="data-[state=on]:text-white data-[state=on]:bg-red-400 justify-center"
|
||||
/>
|
||||
<ToggleButton
|
||||
label={!trigger.isDraft && trigger.draftConfig ? 'Update' : 'Deploy'}
|
||||
value={'deploy'}
|
||||
{item}
|
||||
small
|
||||
class="data-[state=on]:bg-marine-400 data-[state=on]:text-white data-[state=on]:dark:bg-marine-50 data-[state=on]:dark:text-primary-inverse w-[54px] justify-center"
|
||||
class="data-[state=on]:bg-surface-accent-primary data-[state=on]:text-white justify-center"
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{:else if permission === 'admin-only'}
|
||||
<div
|
||||
class="text-xs font-semibold px-1.5 py-1.5 bg-red-400 text-white rounded whitespace-nowrap w-[114px] text-center"
|
||||
title="Only admins can deploy http triggers"
|
||||
>
|
||||
Admin only
|
||||
</div>
|
||||
<Badge color="red">Admin only</Badge>
|
||||
{:else if permission === 'invalid-config'}
|
||||
<div
|
||||
class="text-xs font-semibold px-1.5 py-1.5 bg-red-400 text-white rounded whitespace-nowrap w-[114px] text-center"
|
||||
title="Invalid config"
|
||||
>
|
||||
Invalid config
|
||||
</div>
|
||||
<Badge color="red">Invalid config</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<Row
|
||||
aiId={`script-run-button-${script.path}`}
|
||||
aiDescription={`Button to access the form to run the script ${script.summary ?? script.path}`}
|
||||
href={script.draft_only || script.kind !== 'script' || script.no_main_func
|
||||
href={script.draft_only || (script.no_main_func && script.kind !== 'preprocessor')
|
||||
? `${base}/scripts/edit/${script.path}`
|
||||
: `${base}/scripts/get/${script.hash}?workspace=${$workspaceStore}`}
|
||||
kind="script"
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="grow px-2 inline-flex items-center gap-4 min-w-0">
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-lg min-w-24 font-bold truncate',
|
||||
'text-lg min-w-24 font-semibold text-emphasis truncate',
|
||||
$userStore?.operator ? 'pl-10' : ''
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
initialPathStore,
|
||||
fakeInitialPath,
|
||||
previewArgs,
|
||||
flowInputEditorState
|
||||
flowInputEditorState,
|
||||
stepsInputArgs
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const selectedId = $derived(selectionManager.getSelectedId())
|
||||
@@ -115,10 +116,11 @@
|
||||
{:else if selectedId === 'Trigger'}
|
||||
<TriggersEditor
|
||||
on:applyArgs
|
||||
on:addPreprocessor={async () => {
|
||||
on:addPreprocessor={async (e) => {
|
||||
await insertNewPreprocessorModule(flowStore, flowStateStore, {
|
||||
language: 'bun'
|
||||
})
|
||||
stepsInputArgs.setStepArgs('preprocessor', e.detail.args ?? {})
|
||||
selectionManager.selectId('preprocessor')
|
||||
}}
|
||||
on:updateSchema={(e) => {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import { slide } from 'svelte/transition'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
|
||||
const { previewArgs, flowStateStore, flowStore, currentEditor } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
@@ -186,6 +187,33 @@
|
||||
class="whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="mb-2 text-sm font-bold"
|
||||
>Squash
|
||||
|
||||
<Badge
|
||||
>Beta <Tooltip documentationLink="https://www.windmill.dev/docs/flows/flow_loops">
|
||||
<span class="font-semibold"
|
||||
>This can result in unexpected behavior, use at your own risk for now.</span
|
||||
><br />
|
||||
Squashing a for loop runs all iterations on the same worker, using a single runner
|
||||
per step for the entire loop. This eliminates cold starts between iterations for supported
|
||||
languages (Bun, Deno, and Python).
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</div>
|
||||
<Toggle
|
||||
bind:checked={mod.value.squash}
|
||||
on:change={({ detail }) => {
|
||||
;(mod.value as ForloopFlow).squash = detail
|
||||
}}
|
||||
options={{
|
||||
right: 'Squash'
|
||||
}}
|
||||
class="whitespace-nowrap"
|
||||
disabled={mod.value.parallel}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="mb-2 text-sm font-bold">Run in parallel</div>
|
||||
<Toggle
|
||||
@@ -199,6 +227,7 @@
|
||||
right: 'All iterations run in parallel'
|
||||
}}
|
||||
class="whitespace-nowrap"
|
||||
disabled={mod.value.squash}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
import FlowModuleSleep from './FlowModuleSleep.svelte'
|
||||
import FlowModuleMock from './FlowModuleMock.svelte'
|
||||
import { Play } from 'lucide-svelte'
|
||||
import type { FlowModule, Job } from '$lib/gen'
|
||||
import type { FlowModule, Job, WhileloopFlow } from '$lib/gen'
|
||||
import FlowLoopIterationPreview from '$lib/components/FlowLoopIterationPreview.svelte'
|
||||
import FlowModuleDeleteAfterUse from './FlowModuleDeleteAfterUse.svelte'
|
||||
import FlowModuleSkip from './FlowModuleSkip.svelte'
|
||||
import TabsV2 from '$lib/components/common/tabs/TabsV2.svelte'
|
||||
import { useUiIntent } from '$lib/components/copilot/chat/flow/useUiIntent'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
|
||||
const { flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
@@ -86,7 +87,7 @@
|
||||
|
||||
{#if mod.value.type === 'whileloopflow'}
|
||||
<div class="flex flex-row gap-8 mt-2 mb-6">
|
||||
<div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="mb-2 text-sm font-bold"
|
||||
>Skip failures <Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/flows/while_loops"
|
||||
@@ -102,6 +103,32 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="mb-2 text-sm font-bold"
|
||||
>Squash
|
||||
|
||||
<Badge
|
||||
>Beta <Tooltip documentationLink="https://www.windmill.dev/docs/flows/while_loops">
|
||||
<span class="font-semibold"
|
||||
>This can result in unexpected behavior, use at your own risk for now.</span
|
||||
><br />
|
||||
Squashing a for loop runs all iterations on the same worker, using a single runner
|
||||
per step for the entire loop. This eliminates cold starts between iterations for supported
|
||||
languages (Bun, Deno, and Python).
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</div>
|
||||
<Toggle
|
||||
bind:checked={mod.value.squash}
|
||||
on:change={({ detail }) => {
|
||||
;(mod.value as WhileloopFlow).squash = detail
|
||||
}}
|
||||
options={{
|
||||
right: 'Squash'
|
||||
}}
|
||||
class="whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-2 flex flex-row gap-2 items-center">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Button, Alert } from '$lib/components/common'
|
||||
import { MessageCircle, Loader2, ArrowUp, Square } from 'lucide-svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
import FlowChatMessage from './FlowChatMessage.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { MessageCircle, Loader2 } from 'lucide-svelte'
|
||||
import ChatMessage from '$lib/components/chat/ChatMessage.svelte'
|
||||
import ChatInput from '$lib/components/chat/ChatInput.svelte'
|
||||
import { FlowChatManager } from './FlowChatManager.svelte'
|
||||
|
||||
interface Props {
|
||||
@@ -36,7 +36,13 @@
|
||||
{:else}
|
||||
<div class="w-full space-y-4 xl:max-w-7xl mx-auto">
|
||||
{#each manager.messages as message (message.id)}
|
||||
<FlowChatMessage {message} />
|
||||
<ChatMessage
|
||||
role={message.message_type}
|
||||
content={message.content}
|
||||
loading={message.loading}
|
||||
success={message.success}
|
||||
stepName={message.step_name}
|
||||
/>
|
||||
{/each}
|
||||
{#if manager.isWaitingForResponse}
|
||||
<div class="flex items-center gap-2 text-tertiary">
|
||||
@@ -50,43 +56,17 @@
|
||||
|
||||
<!-- Chat Input -->
|
||||
<div class="flex flex-row justify-center py-2 xl:max-w-7xl mx-auto w-full">
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-lg border border-gray-200 dark:border-gray-600 bg-surface-input w-full"
|
||||
class:opacity-50={deploymentInProgress}
|
||||
>
|
||||
<textarea
|
||||
bind:this={manager.inputElement}
|
||||
<div class="w-full" class:opacity-50={deploymentInProgress}>
|
||||
<ChatInput
|
||||
bind:value={manager.inputMessage}
|
||||
use:autosize
|
||||
onkeydown={manager.handleKeyDown}
|
||||
placeholder="Type your message here..."
|
||||
class="flex-1 min-h-[24px] max-h-32 resize-none !border-0 text-sm placeholder-gray-400 !outline-none !ring-0 p-0 !shadow-none focus:!border-0 focus:!outline-none focus:!ring-0 focus:!shadow-none"
|
||||
rows={3}
|
||||
></textarea>
|
||||
<div class="flex-shrink-0 pr-2 bg-surface-input">
|
||||
{#if manager.isWaitingForResponse || manager.isLoading}
|
||||
<Button
|
||||
color="red"
|
||||
size="xs2"
|
||||
btnClasses="!rounded-full !p-1.5"
|
||||
startIcon={{ icon: Square }}
|
||||
on:click={() => manager.cancelCurrentJob()}
|
||||
iconOnly
|
||||
title="Cancel execution"
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
color="blue"
|
||||
size="xs2"
|
||||
btnClasses="!rounded-full !p-1.5"
|
||||
startIcon={{ icon: ArrowUp }}
|
||||
disabled={!manager.inputMessage?.trim() || manager.isLoading || deploymentInProgress}
|
||||
on:click={() => manager.sendMessage()}
|
||||
iconOnly
|
||||
title={deploymentInProgress ? 'Deployment in progress' : 'Send message (Enter)'}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
bind:bindTextarea={manager.inputElement}
|
||||
disabled={manager.isLoading || deploymentInProgress}
|
||||
onSend={() => manager.sendMessage()}
|
||||
onKeydown={manager.handleKeyDown}
|
||||
showCancelButton={manager.isWaitingForResponse || manager.isLoading}
|
||||
onCancel={() => manager.cancelCurrentJob()}
|
||||
sendTitle={deploymentInProgress ? 'Deployment in progress' : 'Send message (Enter)'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { tick } from 'svelte'
|
||||
import InfiniteList from '$lib/components/InfiniteList.svelte'
|
||||
import { workspaceStore, userStore } from '$lib/stores'
|
||||
import { get } from 'svelte/store'
|
||||
import { parseStreamDeltas } from '$lib/components/chat/utils'
|
||||
|
||||
export interface ChatMessage extends FlowConversationMessage {
|
||||
loading?: boolean
|
||||
@@ -87,6 +88,7 @@ export class FlowChatManager {
|
||||
}
|
||||
|
||||
focusInput() {
|
||||
console.log('focusInput', this.inputElement)
|
||||
this.inputElement?.focus()
|
||||
}
|
||||
|
||||
@@ -320,36 +322,6 @@ export class FlowChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
private parseStreamDeltas(streamData: string): {
|
||||
type: string
|
||||
content: string
|
||||
success: boolean
|
||||
} {
|
||||
let type = 'message'
|
||||
const lines = streamData.trim().split('\n')
|
||||
let content = ''
|
||||
let success = true
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue
|
||||
try {
|
||||
const parsed = JSON.parse(line)
|
||||
if (parsed.type === 'tool_result') {
|
||||
type = 'tool_result'
|
||||
const toolName = parsed.function_name
|
||||
success = parsed.success
|
||||
content = success ? `Used ${toolName} tool` : `Failed to use ${toolName} tool`
|
||||
}
|
||||
if (parsed.type === 'token_delta' && parsed.content) {
|
||||
type = 'message'
|
||||
content += parsed.content
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse stream line:', line, e)
|
||||
}
|
||||
}
|
||||
return { type, content, success }
|
||||
}
|
||||
|
||||
private async pollConversationMessages(conversationId: string, isNewConversation?: boolean) {
|
||||
if (!get(workspaceStore)) return
|
||||
|
||||
@@ -513,7 +485,7 @@ export class FlowChatManager {
|
||||
type,
|
||||
content: newContent,
|
||||
success
|
||||
} = this.parseStreamDeltas(data.new_result_stream)
|
||||
} = parseStreamDeltas(data.new_result_stream)
|
||||
accumulatedContent += newContent
|
||||
|
||||
// Create tool message if type is tool_result
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Markdown } from 'svelte-exmarkdown'
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
import { Loader2, CheckCircle2, AlertTriangle } from 'lucide-svelte'
|
||||
import CodeDisplay from '$lib/components/copilot/chat/script/CodeDisplay.svelte'
|
||||
import LinkRenderer from '$lib/components/copilot/chat/LinkRenderer.svelte'
|
||||
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
||||
import { type ChatMessage } from './FlowChatManager.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
interface Props {
|
||||
message: ChatMessage
|
||||
}
|
||||
|
||||
let { message }: Props = $props()
|
||||
|
||||
// Parse content to detect S3 objects
|
||||
const s3Object: any | undefined = $derived.by(() => {
|
||||
if (message.message_type === 'assistant' && message.content) {
|
||||
try {
|
||||
const parsed = JSON.parse(message.content)
|
||||
// Check if it's a Windmill S3 object with type discriminator
|
||||
if (parsed?.type === 'windmill_s3_object' && parsed?.s3 && typeof parsed.s3 === 'string') {
|
||||
return parsed
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, treat as regular text
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const messageClass = $derived.by(() => {
|
||||
const base = 'max-w-[90%] min-w-0 rounded-lg w-fit'
|
||||
if (message.message_type === 'user') {
|
||||
return `${base} ml-auto bg-surface-secondary p-3`
|
||||
}
|
||||
return `${base} mr-auto bg-surface border ${message.success !== false ? 'border-gray-200 dark:border-gray-600' : '!border-red-500'}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class={messageClass} data-message-id={message.id}>
|
||||
{#if message.step_name}
|
||||
<div class="bg-surface-secondary text-2xs text-tertiary mb-2 font-medium py-1 px-2 rounded-t-lg"
|
||||
>{message.step_name}</div
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if message.message_type === 'user'}
|
||||
<p class="whitespace-pre-wrap text-sm break-words text-right">{message.content}</p>
|
||||
{:else if message.loading}
|
||||
<div class="flex items-center gap-2 text-tertiary">
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
<span>Processing...</span>
|
||||
</div>
|
||||
{:else if message.content}
|
||||
{#if s3Object}
|
||||
<div class="px-3 pb-3 {!message.step_name ? 'pt-3' : ''}">
|
||||
<DisplayResult result={s3Object} workspaceId={$workspaceStore} noControls={true} />
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-row items-center gap-2 px-3 pb-3 text-sm {!message.step_name
|
||||
? 'pt-3'
|
||||
: ''} overflow-x-auto"
|
||||
>
|
||||
{#if message.message_type === 'tool'}
|
||||
{#if message.success !== false}
|
||||
<CheckCircle2 class="w-3.5 h-3.5 text-green-500" />
|
||||
{:else}
|
||||
<AlertTriangle class="w-3.5 h-3.5 text-red-500" />
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="prose prose-sm dark:prose-invert break-words prose-headings:!text-base">
|
||||
<Markdown
|
||||
md={message.content}
|
||||
plugins={[
|
||||
gfmPlugin(),
|
||||
{
|
||||
renderer: {
|
||||
pre: CodeDisplay,
|
||||
a: LinkRenderer
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-tertiary text-sm">No result</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -11,7 +11,7 @@
|
||||
let { Icon = Plus, title, id }: Props = $props()
|
||||
</script>
|
||||
|
||||
<button class="center-center p-2 group">
|
||||
<button class="center-center p-2 group" onclick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
{title}
|
||||
{id}
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
mod.summary || (mod.value.type == 'forloopflow' ? 'For loop' : 'While loop')
|
||||
} ${mod.value.parallel ? '(parallel)' : ''} ${
|
||||
mod.value.skip_failures ? '(skip failures)' : ''
|
||||
}`}
|
||||
} ${mod.value.squash ? '(squash)' : ''}`}
|
||||
id={mod.id}
|
||||
on:changeId
|
||||
on:move
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
const gitSyncContext = getGitSyncContext()
|
||||
const repo = $derived(gitSyncContext.getRepository(idx))
|
||||
let targetBranch = $state('main')
|
||||
let targetBranch = $state<string | undefined>(undefined)
|
||||
|
||||
// Update target branch when repository changes
|
||||
$effect(() => {
|
||||
|
||||
@@ -664,12 +664,13 @@ export function createGitSyncContext(workspace: string) {
|
||||
}
|
||||
|
||||
// Helper to get target branch from git resource
|
||||
async function getTargetBranch(repo: GitSyncRepository): Promise<string> {
|
||||
async function getTargetBranch(repo: GitSyncRepository): Promise<string | undefined> {
|
||||
if (!repo.git_repo_resource_path) {
|
||||
return 'main'
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (repo._targetBranch) {
|
||||
if (repo._targetBranch === '') return undefined
|
||||
return repo._targetBranch
|
||||
}
|
||||
|
||||
@@ -681,14 +682,15 @@ export function createGitSyncContext(workspace: string) {
|
||||
|
||||
// Extract branch from git resource value
|
||||
const resourceValue = resource.value as any
|
||||
const targetBranch = resourceValue?.branch || 'main'
|
||||
const targetBranch = resourceValue?.branch
|
||||
|
||||
// Cache the result
|
||||
repo._targetBranch = targetBranch
|
||||
if (targetBranch === '') return undefined
|
||||
return targetBranch
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch git resource for branch info:', error)
|
||||
return 'main'
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
let { mode, targetBranch, repository } = $props<{
|
||||
mode?: 'sync' | 'promotion' | null
|
||||
targetBranch: string
|
||||
targetBranch: string | undefined
|
||||
repository?: GitSyncRepository | null
|
||||
}>()
|
||||
</script>
|
||||
@@ -11,12 +11,15 @@
|
||||
<div class="text-base">
|
||||
{#if mode === 'promotion'}
|
||||
<div
|
||||
><span class="font-bold">Promotion:</span> Creating branches whose promotion target is {targetBranch}</div
|
||||
><span class="font-bold">Promotion:</span> Creating branches whose promotion target is {targetBranch? `'${targetBranch}'` :
|
||||
"the repo's default branch"}</div
|
||||
>
|
||||
{#if repository?.group_by_folder}
|
||||
<div class="text-sm text-primary mt-1">Grouped by folder</div>
|
||||
{/if}
|
||||
{:else if targetBranch}
|
||||
<div><span class="font-bold">Sync:</span> Syncing back to branch '{targetBranch}'</div>
|
||||
{:else}
|
||||
<div><span class="font-bold">Sync:</span> Syncing back to branch {targetBranch}</div>
|
||||
<div><span class="font-bold">Sync:</span> Syncing back to the repo's default branch</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
const validation = $derived(idx !== null ? gitSyncContext.getValidation(idx) : null)
|
||||
const gitSyncTestJob = $derived(idx !== null ? gitSyncContext.gitSyncTestJobs?.[idx] : null)
|
||||
let confirmingDelete = $state(false)
|
||||
let targetBranch = $state('main') // Default to main, will be updated when resource is available
|
||||
let targetBranch = $state<string | undefined>(undefined) // Default to main, will be updated when resource is available
|
||||
|
||||
// Update target branch when repository changes
|
||||
$effect(() => {
|
||||
@@ -108,12 +108,13 @@
|
||||
)
|
||||
|
||||
// Determine display description based on variant and mode
|
||||
const targetOrDefaultBranch = $derived(targetBranch ? `'${targetBranch}'` : 'repo\'s default' )
|
||||
const displayDescription = $derived(
|
||||
variant === 'primary-sync' || variant === 'primary-promotion'
|
||||
? mode === 'sync'
|
||||
? `Changes will be committed directly to the ${targetBranch} branch`
|
||||
? `Changes will be committed directly to the ${targetOrDefaultBranch} branch`
|
||||
: mode === 'promotion'
|
||||
? `Changes will be made to new branches whose promotion target is ${targetBranch}`
|
||||
? `Changes will be made to new branches whose promotion target is the ${targetOrDefaultBranch} branch`
|
||||
: null
|
||||
: null
|
||||
)
|
||||
|
||||
@@ -397,7 +397,7 @@
|
||||
},
|
||||
select: (modId) => {
|
||||
// AI tools are not selectable by the flow. Selection has to be refactored to be simplier.
|
||||
if (nodes.find((n) => n.data?.moduleId === modId)?.type === 'aiTool') {
|
||||
if (nodes.find((n) => n.data?.moduleId === modId)?.type === 'aiTool' || modId === 'Trigger') {
|
||||
selectionManager.selectId(modId)
|
||||
}
|
||||
if (!notSelectable) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { MessageSquare, DiffIcon } from 'lucide-svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
import FunnelCog from '$lib/components/icons/FunnelCog.svelte'
|
||||
|
||||
interface Props {
|
||||
data: InputN['data']
|
||||
@@ -71,7 +72,11 @@
|
||||
}}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<InsertModuleButton title={`Add preprocessor step`} id={`flow-editor-add-step-0`} />
|
||||
<InsertModuleButton
|
||||
title={`Add preprocessor step`}
|
||||
id={`flow-editor-add-step-0`}
|
||||
Icon={FunnelCog}
|
||||
/>
|
||||
{/snippet}
|
||||
</InsertModulePopover>
|
||||
</div>
|
||||
|
||||
@@ -246,10 +246,10 @@
|
||||
aiId={`trigger-button-${type}`}
|
||||
aiDescription={`Trigger button for ${type}`}
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover rounded-md shadow-sm text-xs relative center-center cursor-pointer bg-slate-100 dark:bg-slate-700',
|
||||
'dark:outline dark:outline-1 outline-tertiary/20 group',
|
||||
isSelected ? 'outline-tertiary outline' : '',
|
||||
small ? 'w-[23px] h-[23px] outline-[1.5px]' : 'p-2 outline-[2px]'
|
||||
'hover:bg-surface-hover rounded-md text-xs relative center-center cursor-pointer border border-border-light',
|
||||
'group',
|
||||
isSelected ? 'bg-surface-accent-selected text-accent border-border-selected' : '',
|
||||
small ? 'w-[23px] h-[23px]' : 'p-2'
|
||||
)}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
strokeWidth?: number | string
|
||||
absoluteStrokeWidth?: boolean
|
||||
class?: string
|
||||
style?: string
|
||||
}
|
||||
|
||||
let {
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
strokeWidth = 2,
|
||||
absoluteStrokeWidth = false,
|
||||
class: clazz = '',
|
||||
style = ''
|
||||
}: Props = $props()
|
||||
|
||||
const computedStrokeWidth = $derived(
|
||||
absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth
|
||||
)
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
stroke-width={computedStrokeWidth}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class={clazz}
|
||||
{style}
|
||||
>
|
||||
<path
|
||||
d="M14.517 12.659A2 2 0 0 0 14 14v7a1 1 0 0 1-1.447.895l-2-1A1 1 0 0 1 10 20v-6a2 2 0 0 0-.517-1.341L2.258 4.67A1 1 0 0 1 3 3h8"
|
||||
/>
|
||||
<path d="m15.523 4.547-.923-.383" />
|
||||
<path d="m15.523 6.843-.923.383" />
|
||||
<path d="m16.765 2 .382.923" />
|
||||
<path d="m17.147 8.467-.383.924" />
|
||||
<path d="M19.443 2.923 19.826 2" />
|
||||
<path d="m19.825 9.391-.382-.924" />
|
||||
<path d="m21.067 4.547.924-.383" />
|
||||
<path d="m21.067 6.843.924.383" />
|
||||
<circle cx="18.295" cy="5.695" r="3" />
|
||||
</svg>
|
||||
@@ -23,7 +23,7 @@
|
||||
setDropdownWidthToButtonWidth = false,
|
||||
children,
|
||||
class: className,
|
||||
placement = 'bottom',
|
||||
placement = 'bottom-start',
|
||||
isEditor = false,
|
||||
onAddDraftTrigger,
|
||||
onAddScheduledPoll,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import AnimatedButton from '../common/button/AnimatedButton.svelte'
|
||||
import PulseButton from '../common/button/PulseButton.svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { CircleStop, History, Play, Loader2 } from 'lucide-svelte'
|
||||
import { CircleStop, History, Play, Loader2, ExternalLink } from 'lucide-svelte'
|
||||
import ConnectionIndicator, {
|
||||
type ConnectionInfo
|
||||
} from '../common/alert/ConnectionIndicator.svelte'
|
||||
@@ -67,7 +67,7 @@
|
||||
const dispatch = createEventDispatcher<{
|
||||
captureToggle: { disableOnly?: boolean }
|
||||
updateSchema: { payloadData: Record<string, any>; redirect: boolean; args?: boolean }
|
||||
addPreprocessor: null
|
||||
addPreprocessor: { args: Record<string, any> } | null
|
||||
testWithArgs: Record<string, any>
|
||||
applyArgs: { kind: 'main' | 'preprocessor'; args: Record<string, any> }
|
||||
}>()
|
||||
@@ -121,6 +121,7 @@
|
||||
let lastCaptureId: number | undefined = undefined
|
||||
let displayResult: DisplayResult | undefined = $state(undefined)
|
||||
let toolbarLocation: 'internal' | 'external' | undefined = $state(undefined)
|
||||
let showRawPayload = $state(false)
|
||||
|
||||
function selectCapture(capture: Capture) {
|
||||
selectedCapture = capture
|
||||
@@ -225,13 +226,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getCapturePayload(capture: Capture) {
|
||||
function getCapturePayload(capture: Capture | undefined, showRawPayload: boolean) {
|
||||
if (!capture) return {}
|
||||
let payloadData: any = {}
|
||||
const preprocessor_args = isObject(capture.preprocessor_args) ? capture.preprocessor_args : {}
|
||||
if ('wm_trigger' in preprocessor_args) {
|
||||
// v1
|
||||
payloadData =
|
||||
testKind === 'preprocessor'
|
||||
testKind === 'preprocessor' || showRawPayload
|
||||
? {
|
||||
...(typeof capture.main_args === 'object' ? capture.main_args : {}),
|
||||
...preprocessor_args
|
||||
@@ -239,11 +241,20 @@
|
||||
: capture.main_args
|
||||
} else {
|
||||
// v2
|
||||
payloadData = testKind === 'preprocessor' ? capture.preprocessor_args : capture.main_args
|
||||
payloadData =
|
||||
testKind === 'preprocessor' || showRawPayload
|
||||
? capture.preprocessor_args
|
||||
: capture.main_args
|
||||
}
|
||||
return payloadData
|
||||
}
|
||||
|
||||
function toggleRawPayload() {
|
||||
showRawPayload = !showRawPayload
|
||||
}
|
||||
|
||||
const payloadData = $derived(getCapturePayload(selectedCapture, showRawPayload))
|
||||
|
||||
// Start or stop capture listening based on active state
|
||||
$effect(() => {
|
||||
if (captureInfo.active) {
|
||||
@@ -380,16 +391,16 @@
|
||||
{#if selectedCapture}
|
||||
{@const SvelteComponent = triggerIconMap[captureType]}
|
||||
<div
|
||||
class={'min-w-16 text-secondary flex flex-row w-fit items-center gap-2 rounded-md bg-surface-secondary p-1 px-2 h-[27px]'}
|
||||
class="min-w-16 text-primary flex flex-row w-fit items-center gap-2 rounded-md bg-surface-secondary p-1 px-2 h-[27px]"
|
||||
>
|
||||
<SvelteComponent size={12} />
|
||||
<span class="text-xs text-secondary truncate">
|
||||
<span class="text-xs truncate">
|
||||
Capture {formatDateShort(selectedCapture?.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedCapture}
|
||||
{#if selectedCapture && !showRawPayload}
|
||||
{@const label = isFlow && testKind === 'main' ? 'Test flow with args' : 'Apply args'}
|
||||
{@const title =
|
||||
isFlow && testKind === 'main'
|
||||
@@ -455,26 +466,73 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grow min-h-0 rounded-md w-full pl-2 py-1 pb-2 overflow-auto">
|
||||
{#if isLoadingBigPayload}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else if selectedCapture?.main_args}
|
||||
<div class="bg-surface rounded-md text-sm" class:animate-highlight={newCaptureReceived}>
|
||||
<DisplayResult
|
||||
bind:this={displayResult}
|
||||
workspaceId={undefined}
|
||||
jobId={undefined}
|
||||
result={getCapturePayload(selectedCapture)}
|
||||
externalToolbarAvailable
|
||||
on:toolbar-location-changed={({ detail }) => {
|
||||
toolbarLocation = detail
|
||||
}}
|
||||
/>
|
||||
<div class="grow min-h-0 w-full pl-2 py-1 pb-2 flex flex-col">
|
||||
<div class="flex-1 min-h-0 overflow-auto">
|
||||
{#if isLoadingBigPayload}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else if selectedCapture?.main_args}
|
||||
<div class="bg-surface rounded-md text-sm" class:animate-highlight={newCaptureReceived}>
|
||||
{#if showRawPayload}
|
||||
<Alert type="info" title="Raw payload displayed" size="xs" class="mt-2 mb-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
Add a preprocessor to use the raw payload data in the flow.
|
||||
<a
|
||||
href="https://www.windmill.dev/docs/core_concepts/preprocessors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline"
|
||||
>
|
||||
Learn more about preprocessors
|
||||
<ExternalLink size={12} class="inline-block" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="accent"
|
||||
wrapperClasses="w-fit"
|
||||
onclick={() => {
|
||||
if (selectedCapture) {
|
||||
dispatch('addPreprocessor', {
|
||||
args: $state.snapshot(payloadData)
|
||||
})
|
||||
} else {
|
||||
dispatch('addPreprocessor')
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add preprocessor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
<DisplayResult
|
||||
bind:this={displayResult}
|
||||
workspaceId={undefined}
|
||||
jobId={undefined}
|
||||
result={payloadData}
|
||||
externalToolbarAvailable
|
||||
on:toolbar-location-changed={({ detail }) => {
|
||||
toolbarLocation = detail
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-primary p-4 bg-surface rounded-md"
|
||||
>No captures to show yet.</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !hasPreprocessor && !isLoadingBigPayload && selectedCapture?.main_args}
|
||||
<div class="mt-2 text-2xs text-secondary flex-shrink-0 text-right px-2">
|
||||
{showRawPayload ? '' : 'Need more data about the trigger event?'}
|
||||
<button onclick={toggleRawPayload} class="text-accent cursor-pointer">
|
||||
{showRawPayload ? 'Show processed payload' : 'Show raw payload'}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-primary p-4 bg-surface rounded-md"
|
||||
>No captures to show yet.</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
kind: 'main' | 'preprocessor'
|
||||
args: Record<string, any> | undefined
|
||||
}
|
||||
addPreprocessor: null
|
||||
addPreprocessor: { args: Record<string, any> } | undefined
|
||||
updateSchema: {
|
||||
payloadData: any
|
||||
redirect: boolean
|
||||
|
||||
@@ -39,12 +39,12 @@
|
||||
</ConfirmationModal>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
unifiedSize={small ? 'xs' : 'sm'}
|
||||
variant="subtle"
|
||||
destructive
|
||||
startIcon={{ icon: Trash }}
|
||||
iconOnly
|
||||
color={'light'}
|
||||
on:click={() => {
|
||||
confirmationModalOpen = true
|
||||
}}
|
||||
btnClasses={twMerge(small ? 'px-1 py-1' : '', 'bg-transparent hover:bg-red-500 hover:text-white')}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user