From f35dae4ad9d9b747ba3dc54b9fb4dfc1f14a912f Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:48:56 +0200 Subject: [PATCH 1/9] Fix unstable flow layout (#6126) --- frontend/src/lib/components/graph/FlowGraphV2.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index c11864610f..e7dc28d78d 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -343,6 +343,7 @@ return } let newGraph = graph + newGraph.nodes.sort((a, b) => b.id.localeCompare(a.id)) nodes = layoutNodes(newGraph.nodes) edges = newGraph.edges From 13716c7df343f240ade395496dc9edfcd410bd21 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:08:25 +0000 Subject: [PATCH 2/9] chore: update croner dependency to version 2.2.0 (#6119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update croner dependency from 2.0.6 to 2.2.0 as requested in #6118. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel Co-authored-by: Ruben Fiszel --- backend/windmill-common/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/windmill-common/Cargo.toml b/backend/windmill-common/Cargo.toml index 250f48e336..d98897d8cb 100644 --- a/backend/windmill-common/Cargo.toml +++ b/backend/windmill-common/Cargo.toml @@ -83,7 +83,7 @@ strum_macros.workspace = true url.workspace = true semver.workspace = true -croner = "2.0.6" +croner = "2.2.0" quick_cache.workspace = true pin-project-lite.workspace = true futures.workspace = true From 835645643e5b3b1312af5b349547b6fbd06bdaae Mon Sep 17 00:00:00 2001 From: wendrul <53628737+wendrul@users.noreply.github.com> Date: Fri, 4 Jul 2025 06:02:45 +0900 Subject: [PATCH 3/9] feat: Better tracing for audit logs, including a graph to visualize them (#6078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate audit log page to svelte 5 * Add email and span cols to audit table * Add token_prefixs to audit logs (into AuditAuthorable trait) * Add audit logs graph (wip) * Add audit span on push and jwt * Unify same job audit into the same audit span * Improve the graph visually * Fix typo * functioning graph with svelte issue * Fix leak * feat: migrate AuditLogsTable from DataTable to VirtualList for performance - Replace DataTable component with VirtualList for handling thousands of rows - Migrate to Svelte 5 runes ($props, $bindable, $derived, $state) - Implement flattenLogs() for virtual scrolling with grouped date headers - Add sticky indices and dynamic height calculation - Update parent component to use callback prop pattern instead of events - Preserve all existing functionality: filtering, selection, pagination - Follows RunsTable.svelte implementation pattern Resolves performance issues when displaying large audit log datasets. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Ruben Fiszel * Fix remaining virtual list issues * WIP graph * Fix chart styling * Fix npm check * Fix missing audit_span arguments * Update sqlx * use varchar 255 for email as in other tables * Remove syntax inconsistency * Match struct with ee crate * Update ee-repo-ref.txt * Update worker_flow.rs * Remove redefinition of trait to prevent shadowing * Re add trait on oss but only when no `private` flag --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel Co-authored-by: GitHub Action Co-authored-by: Ruben Fiszel --- backend/ee-repo-ref.txt | 2 +- ...50135_add_email_and_span_to_audit.down.sql | 3 + ...5150135_add_email_and_span_to_audit.up.sql | 3 + backend/src/monitor.rs | 1 + backend/tests/worker.rs | 6 +- backend/windmill-api/openapi.yaml | 2 + backend/windmill-api/src/approvals.rs | 4 + backend/windmill-api/src/apps.rs | 9 + backend/windmill-api/src/auth.rs | 10 +- backend/windmill-api/src/db.rs | 6 + backend/windmill-api/src/flows.rs | 2 + backend/windmill-api/src/jobs.rs | 83 ++- backend/windmill-api/src/resources.rs | 1 + backend/windmill-api/src/schedule.rs | 11 +- backend/windmill-api/src/scripts.rs | 1 + backend/windmill-api/src/slack_approvals.rs | 12 +- backend/windmill-api/src/users.rs | 34 +- backend/windmill-api/src/variables.rs | 9 +- backend/windmill-audit/src/audit_oss.rs | 24 +- backend/windmill-audit/src/lib.rs | 1 + backend/windmill-common/src/auth.rs | 15 +- backend/windmill-common/src/db.rs | 1 + backend/windmill-queue/src/jobs.rs | 11 +- backend/windmill-queue/src/schedule.rs | 1 + .../windmill-worker/src/result_processor.rs | 7 +- backend/windmill-worker/src/worker_flow.rs | 3 + .../windmill-worker/src/worker_lockfiles.rs | 1 + .../auditLogs/AuditLogsFilters.svelte | 14 +- .../auditLogs/AuditLogsTable.svelte | 379 ++++++++---- .../auditLogs/AuditLogsTimeline.svelte | 564 ++++++++++++++++++ .../(root)/(logged)/audit_logs/+page.svelte | 205 ++++--- 31 files changed, 1165 insertions(+), 260 deletions(-) create mode 100644 backend/migrations/20250605150135_add_email_and_span_to_audit.down.sql create mode 100644 backend/migrations/20250605150135_add_email_and_span_to_audit.up.sql create mode 100644 frontend/src/lib/components/auditLogs/AuditLogsTimeline.svelte diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 188513dd18..4645cca3de 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -b7c6fc065a3da98933d50fae697784da61c3fe2d +21aabec96e91c8075dd637d1e32af90e495082fc diff --git a/backend/migrations/20250605150135_add_email_and_span_to_audit.down.sql b/backend/migrations/20250605150135_add_email_and_span_to_audit.down.sql new file mode 100644 index 0000000000..637b360023 --- /dev/null +++ b/backend/migrations/20250605150135_add_email_and_span_to_audit.down.sql @@ -0,0 +1,3 @@ +-- Remove email and span columns from audit table +ALTER TABLE audit DROP COLUMN email; +ALTER TABLE audit DROP COLUMN span; diff --git a/backend/migrations/20250605150135_add_email_and_span_to_audit.up.sql b/backend/migrations/20250605150135_add_email_and_span_to_audit.up.sql new file mode 100644 index 0000000000..b75b49af9f --- /dev/null +++ b/backend/migrations/20250605150135_add_email_and_span_to_audit.up.sql @@ -0,0 +1,3 @@ +-- Add email and span columns to audit table +ALTER TABLE audit ADD COLUMN email VARCHAR(255); +ALTER TABLE audit ADD COLUMN span VARCHAR(255); diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 4b4946e3e4..ff07a31bd0 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -1926,6 +1926,7 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, worker &job.email, &job.id, None, + Some(format!("handle_zombie_jobs")), ) .await .expect("could not create job token"); diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 184bb9daa6..9403df9f08 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -322,7 +322,7 @@ mod suspend_resume { let second = completed.next().await.unwrap(); // print_job(second, &db).await; - let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap(); + let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None, None).await.unwrap(); let secret = reqwest::get(format!( "http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben" )) @@ -427,7 +427,7 @@ mod suspend_resume { /* ... and send a request resume it. */ let second = completed.next().await.unwrap(); - let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap(); + let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None, None).await.unwrap(); let secret = reqwest::get(format!( "http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}" )) @@ -935,6 +935,7 @@ impl RunJob { /* user */ "test-user", /* email */ "test@windmill.dev", /* permissioned_as */ "u/test-user".to_string(), + /* token_prefix */ None, /* scheduled_for_o */ None, /* schedule_path */ None, /* parent_job */ None, @@ -4209,6 +4210,7 @@ async fn test_result_format(db: Pool) { "", &Uuid::nil(), None, + None, ) .await .unwrap(); diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0e3aa9ab3f..7633f7b3e7 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -14304,6 +14304,8 @@ components: type: string parameters: type: object + span: + type: string required: - id - timestamp diff --git a/backend/windmill-api/src/approvals.rs b/backend/windmill-api/src/approvals.rs index f4ee9dd843..f16ed00abe 100644 --- a/backend/windmill-api/src/approvals.rs +++ b/backend/windmill-api/src/approvals.rs @@ -4,6 +4,7 @@ use uuid::Uuid; use std::str::FromStr; use regex::Regex; use serde_json::Value; +use crate::auth::OptTokened; use crate::db::{ApiAuthed, DB}; use crate::jobs::{cancel_suspended_job, resume_suspended_job, QueryApprover, QueryOrBody, ResumeUrls, get_resume_urls_internal}; use axum::{extract::{Path, Query}, Extension}; @@ -101,6 +102,7 @@ pub fn extract_w_id_from_resume_url(resume_url: &str) -> Result<&str, Error> { pub async fn handle_resume_action( authed: Option, + opt_tokened: OptTokened, db: DB, resume_url: &str, form_data: Value, @@ -135,6 +137,7 @@ pub async fn handle_resume_action( let res = if action == "resume" { resume_suspended_job( authed, + opt_tokened, Extension(db.clone()), Path(( w_id.to_string(), @@ -149,6 +152,7 @@ pub async fn handle_resume_action( } else { cancel_suspended_job( authed, + opt_tokened, Extension(db.clone()), Path(( w_id.to_string(), diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 85d1745caa..ad4020a671 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -9,6 +9,7 @@ use std::{collections::HashMap, sync::Arc}; */ use crate::{ + auth::OptTokened, db::{ApiAuthed, DB}, resources::get_resource_value_interpolated_internal, users::{require_owner_of_path, OptAuthed}, @@ -52,6 +53,7 @@ use windmill_audit::audit_oss::audit_log; use windmill_audit::ActionKind; use windmill_common::{ apps::{AppScriptId, ListAppQuery}, + auth::TOKEN_PREFIX_LEN, cache::{self, future::FutureCachedExt}, db::UserDB, error::{to_anyhow, Error, JsonResult, Result}, @@ -1040,6 +1042,7 @@ async fn create_app_internal<'a>( &authed.username, &authed.email, windmill_common::users::username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -1411,6 +1414,7 @@ async fn update_app_internal<'a>( &authed.username, &authed.email, windmill_common::users::username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -1517,6 +1521,7 @@ fn empty_triggerables(mut policy: Policy) -> Policy { async fn execute_component( OptAuthed(opt_authed): OptAuthed, + tokened: OptTokened, Extension(db): Extension, Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, @@ -1719,6 +1724,10 @@ async fn execute_component( &username, email, permissioned_as, + opt_authed + .and_then(|a| a.token_prefix) + .or_else(|| tokened.token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string())) + .as_deref(), None, None, None, diff --git a/backend/windmill-api/src/auth.rs b/backend/windmill-api/src/auth.rs index 954fcad43c..a61e2ee72b 100644 --- a/backend/windmill-api/src/auth.rs +++ b/backend/windmill-api/src/auth.rs @@ -21,7 +21,7 @@ use std::sync::{ use tokio::sync::RwLock; use windmill_common::{ - auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims}, + auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, TOKEN_PREFIX_LEN}, jwt, users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL}, }; @@ -134,6 +134,7 @@ impl AuthCache { folders: claims.folders, scopes: None, username_override, + token_prefix: claims.audit_span, }; AUTH_CACHE.insert( @@ -217,6 +218,7 @@ impl AuthCache { folders, scopes: None, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } else { let groups = vec![name.to_string()]; @@ -238,6 +240,7 @@ impl AuthCache { folders, scopes: None, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } } else { @@ -252,6 +255,7 @@ impl AuthCache { folders, scopes: None, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } } @@ -299,6 +303,7 @@ impl AuthCache { folders, scopes, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } None if super_admin => Some(ApiAuthed { @@ -310,6 +315,7 @@ impl AuthCache { folders: vec![], scopes, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }), None => None, } @@ -323,6 +329,7 @@ impl AuthCache { folders: Vec::new(), scopes, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } } @@ -355,6 +362,7 @@ impl AuthCache { folders: Vec::new(), scopes: None, username_override: None, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } else { None diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index 6e3343494d..fc21edc5de 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -836,6 +836,7 @@ pub struct ApiAuthed { pub folders: Vec<(String, bool, bool)>, pub scopes: Option>, pub username_override: Option, + pub token_prefix: Option, } impl From for Authed { @@ -848,6 +849,7 @@ impl From for Authed { groups: value.groups, folders: value.folders, scopes: value.scopes, + token_prefix: value.token_prefix, } } } @@ -858,6 +860,7 @@ impl From<&ApiAuthed> for AuditAuthor { email: value.email.clone(), username: value.username.clone(), username_override: value.username_override.clone(), + token_prefix: value.token_prefix.clone(), } } } @@ -878,6 +881,9 @@ impl AuditAuthorable for ApiAuthed { fn username_override(&self) -> Option<&str> { self.username_override.as_deref() } + fn token_prefix(&self) -> Option<&str> { + self.token_prefix.as_deref() + } } impl Authable for ApiAuthed { diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index af0c8cef7a..40f87a1f94 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -494,6 +494,7 @@ async fn create_flow( &authed.username, &authed.email, windmill_common::users::username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -935,6 +936,7 @@ async fn update_flow( &authed.username, &authed.email, windmill_common::users::username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 862c472933..f0421bc031 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -22,7 +22,7 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use tokio::io::AsyncReadExt; use tower::ServiceBuilder; -use windmill_common::auth::is_super_admin_email; +use windmill_common::auth::{is_super_admin_email, TOKEN_PREFIX_LEN}; use windmill_common::error::JsonResult; use windmill_common::flow_status::{JobResult, RestartedFrom}; use windmill_common::jobs::{format_completed_job_result, format_result, ENTRYPOINT_OVERRIDE}; @@ -33,6 +33,7 @@ use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH; use windmill_common::variables::get_workspace_key; use crate::add_webhook_allowed_origin; +use crate::auth::{OptTokened, Tokened}; use crate::concurrency_groups::join_concurrency_key; use crate::db::ApiAuthed; @@ -293,6 +294,7 @@ struct JsonPath { } async fn get_result_by_id( authed: ApiAuthed, + tokened: Tokened, Extension(db): Extension, Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>, Query(JsonPath { json_path, .. }): Query, @@ -301,7 +303,7 @@ async fn get_result_by_id( windmill_queue::get_result_by_id(db.clone(), w_id.clone(), flow_id, node_id, json_path) .await?; - log_job_view(&db, Some(&authed), &w_id, &flow_id).await?; + log_job_view(&db, Some(&authed), Some(&tokened.token), &w_id, &flow_id).await?; Ok(Json(res)) } @@ -337,6 +339,7 @@ async fn get_db_clock(Extension(db): Extension) -> windmill_common::error::J async fn cancel_job_api( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Json(CancelJob { reason }): Json, @@ -349,6 +352,7 @@ async fn cancel_job_api( username: "anonymous".to_string(), username_override: None, email: "anonymous".to_string(), + token_prefix: opt_tokened.token.map(|s| s[0..TOKEN_PREFIX_LEN].to_string()), }, }; let (mut tx, job_option) = tokio::time::timeout( @@ -447,6 +451,7 @@ async fn cancel_persistent_script_api( async fn force_cancel( OptAuthed(opt_authed): OptAuthed, + tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Json(CancelJob { reason }): Json, @@ -459,6 +464,7 @@ async fn force_cancel( username: "anonymous".to_string(), username_override: None, email: "anonymous".to_string(), + token_prefix: tokened.token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string()), }, }; @@ -510,6 +516,7 @@ async fn force_cancel( async fn get_flow_job_debug_info( OptAuthed(opt_authed): OptAuthed, + tokened_o: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, ) -> error::Result { @@ -561,7 +568,7 @@ async fn get_flow_job_debug_info( } } - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), tokened_o.token.as_deref(), &w_id, &id).await?; Ok(Json(jobs).into_response()) } else { @@ -621,6 +628,7 @@ struct GetJobQuery { async fn get_job( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Query(GetJobQuery { no_logs }): Query, @@ -640,7 +648,7 @@ async fn get_job( let mut job = get.fetch(&db, id, &w_id).await?; job.fetch_outstanding_wait_time(&db).await?; - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(job).into_response()) } @@ -1088,6 +1096,7 @@ async fn get_logs_from_disk( async fn get_job_logs( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, ) -> error::Result { @@ -1125,7 +1134,7 @@ async fn get_job_logs( } let logs = record.logs.unwrap_or_default(); - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; #[cfg(all(feature = "enterprise", feature = "parquet"))] if let Some(r) = get_logs_from_store(record.log_offset, &logs, &record.log_file_index).await @@ -1162,7 +1171,7 @@ async fn get_job_logs( } let logs = text.logs.unwrap_or_default(); - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; #[cfg(all(feature = "enterprise", feature = "parquet"))] if let Some(r) = @@ -1186,6 +1195,7 @@ async fn get_job_logs( async fn get_args( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, ) -> JsonResult> { @@ -1211,7 +1221,7 @@ async fn get_args( )); } - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(record.args.map(|x| x.0).unwrap_or_default())) } else { @@ -1232,7 +1242,7 @@ async fn get_args( )); } - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(record.args.map(|x| x.0).unwrap_or_default())) } @@ -1993,13 +2003,23 @@ pub async fn resume_suspended_flow_as_owner( pub async fn resume_suspended_job( authed: Option, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>, Query(approver): Query, QueryOrBody(value): QueryOrBody, ) -> error::Result { resume_suspended_job_internal( - value, db, w_id, job_id, resume_id, approver, secret, authed, true, + value, + db, + w_id, + job_id, + resume_id, + approver, + secret, + authed, + opt_tokened, + true, ) .await } @@ -2013,6 +2033,7 @@ async fn resume_suspended_job_internal( approver: QueryApprover, secret: String, authed: Option, + opt_tokened: OptTokened, approved: bool, ) -> Result { let value = value.unwrap_or(serde_json::Value::Null); @@ -2091,6 +2112,7 @@ async fn resume_suspended_job_internal( email: approver.clone(), username: approver.clone(), username_override: None, + token_prefix: opt_tokened.token.map(|s| s[0..TOKEN_PREFIX_LEN].to_string()), }, }; audit_log( @@ -2260,13 +2282,14 @@ async fn get_suspended_flow_info<'c>( pub async fn cancel_suspended_job( authed: Option, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>, Query(approver): Query, QueryOrBody(value): QueryOrBody, ) -> error::Result { resume_suspended_job_internal( - value, db, w_id, job_id, resume_id, approver, secret, authed, false, + value, db, w_id, job_id, resume_id, approver, secret, authed, opt_tokened, false, ) .await } @@ -2284,6 +2307,7 @@ pub struct QueryApprover { pub async fn get_suspended_job_flow( authed: Option, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>, Query(approver): Query, @@ -2350,7 +2374,7 @@ pub async fn get_suspended_job_flow( approvers_from_status }; - log_job_view(&db, authed.as_ref(), &w_id, &job).await?; + log_job_view(&db, authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job).await?; Ok(Json(SuspendedJobFlow { job: flow, approvers }).into_response()) } @@ -3507,6 +3531,7 @@ pub async fn run_flow_by_path_inner( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -3600,6 +3625,7 @@ pub async fn restart_flow( &authed.username, &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -3692,6 +3718,7 @@ pub async fn run_script_by_path_inner( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -3837,6 +3864,7 @@ pub async fn run_workflow_as_code( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, Some(job_id), @@ -4252,6 +4280,7 @@ impl JobViewCache { async fn log_job_view( db: &DB, opt_authed: Option<&ApiAuthed>, + opt_token: Option<&str>, w_id: &str, job_id: &Uuid, ) -> error::Result<()> { @@ -4262,6 +4291,7 @@ async fn log_job_view( username: "anonymous".to_string(), username_override: None, email: "anonymous".to_string(), + token_prefix: opt_token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string()) }, }; if JOB_VIEW_CACHE @@ -4360,6 +4390,7 @@ pub async fn run_wait_result_job_by_path_get( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), None, None, run_query.parent_job, @@ -4500,6 +4531,7 @@ pub async fn run_wait_result_script_by_path_internal( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), None, None, run_query.parent_job, @@ -4613,6 +4645,7 @@ pub async fn run_wait_result_script_by_hash( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), None, None, run_query.parent_job, @@ -4727,6 +4760,7 @@ pub async fn run_wait_result_flow_by_path_internal( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -4797,6 +4831,7 @@ async fn run_preview_script( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), scheduled_for, None, None, @@ -4885,6 +4920,7 @@ async fn run_bundle_preview_script( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), scheduled_for, None, None, @@ -5052,6 +5088,7 @@ async fn run_dependencies_job( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -5109,6 +5146,7 @@ async fn run_flow_dependencies_job( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -5449,6 +5487,7 @@ async fn run_preview_flow_job( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), scheduled_for, None, None, @@ -5568,6 +5607,7 @@ pub async fn run_job_by_hash_inner( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -5660,6 +5700,7 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R async fn get_job_update( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, job_id)): Path<(String, Uuid)>, Query(JobUpdateQuery { log_offset, get_progress, running }): Query, @@ -5716,7 +5757,7 @@ async fn get_job_update( "As a non logged in user, you can only see jobs ran by anonymous users".to_string(), )); } - log_job_view(&db, opt_authed.as_ref(), &w_id, &job_id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job_id).await?; Ok(Json(JobUpdate { running: record.running, completed: record.completed, @@ -6004,6 +6045,7 @@ async fn list_completed_jobs( async fn get_completed_job<'a>( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, ) -> error::Result { @@ -6029,7 +6071,7 @@ async fn get_completed_job<'a>( // .fetch_optional(db) // .await.ok().flatten().flatten(); - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(response) } @@ -6043,6 +6085,7 @@ pub struct RawResult { async fn get_completed_job_result( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Query(JsonPath { json_path, suspended_job, approver, resume_id, secret }): Query, @@ -6131,7 +6174,7 @@ async fn get_completed_job_result( raw_result.result.as_mut(), ); - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(raw_result.result).into_response()) } @@ -6189,6 +6232,7 @@ struct GetCompletedJobQuery { async fn get_completed_job_result_maybe( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Query(GetCompletedJobQuery { get_started }): Query, @@ -6221,7 +6265,7 @@ async fn get_completed_job_result_maybe( )); } - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(CompletedJobResult { started: Some(true), @@ -6259,6 +6303,7 @@ async fn get_completed_job_result_maybe( async fn delete_completed_job<'a>( authed: ApiAuthed, + Tokened { token }: Tokened, Extension(user_db): Extension, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, @@ -6308,5 +6353,11 @@ async fn delete_completed_job<'a>( .await?; tx.commit().await?; - return get_completed_job(OptAuthed(Some(authed)), Extension(db), Path((w_id, id))).await; + return get_completed_job( + OptAuthed(Some(authed)), + OptTokened { token: Some(token) }, + Extension(db), + Path((w_id, id)), + ) + .await; } diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 7999356344..fd988a271c 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -508,6 +508,7 @@ pub async fn transform_json_value<'c>( email: "backend".to_string(), username: "backend".to_string(), username_override: None, + token_prefix: None, }), ) .await?; diff --git a/backend/windmill-api/src/schedule.rs b/backend/windmill-api/src/schedule.rs index 3cb13798d9..f5a1aa0fcd 100644 --- a/backend/windmill-api/src/schedule.rs +++ b/backend/windmill-api/src/schedule.rs @@ -7,10 +7,7 @@ */ use crate::{ - db::{ApiAuthed, DB}, - settings::{delete_global_setting, set_global_setting_internal}, - users::maybe_refresh_folders, - utils::require_super_admin, + db::{ApiAuthed, DB}, settings::{delete_global_setting, set_global_setting_internal}, users::maybe_refresh_folders, utils::require_super_admin }; use axum::{ extract::{Extension, Path, Query}, @@ -25,11 +22,7 @@ use std::str::FromStr; use windmill_audit::audit_oss::audit_log; use windmill_audit::ActionKind; use windmill_common::{ - db::UserDB, - error::{Error, JsonResult, Result}, - schedule::Schedule, - utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath}, - worker::to_raw_value, + db::UserDB, error::{Error, JsonResult, Result}, schedule::Schedule, utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath}, worker::to_raw_value }; use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; use windmill_queue::schedule::push_scheduled_job; diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 6b74c6381b..83773d4087 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -941,6 +941,7 @@ async fn create_script_internal<'c>( &authed.username, &authed.email, permissioned_as, + authed.token_prefix.as_deref(), None, None, None, diff --git a/backend/windmill-api/src/slack_approvals.rs b/backend/windmill-api/src/slack_approvals.rs index 019f83ecb2..82c1b68d86 100644 --- a/backend/windmill-api/src/slack_approvals.rs +++ b/backend/windmill-api/src/slack_approvals.rs @@ -11,11 +11,11 @@ use std::collections::HashMap; use windmill_common::error::Error; use windmill_common::variables::get_secret_value_as_admin; -use crate::approvals::{ +use crate::{approvals::{ extract_w_id_from_resume_url, handle_resume_action, ApprovalFormDetails, FieldType, MessageFormat, QueryDefaultArgsJson, QueryDynamicEnumJson, QueryFlowStepId, QueryMessage, ResumeFormField, ResumeSchema, -}; +}, auth::OptTokened}; use crate::db::{ApiAuthed, DB}; use crate::jobs::{QueryApprover, ResumeUrls}; @@ -116,6 +116,7 @@ struct PrivateMetadata { pub async fn slack_app_callback_handler( authed: Option, + opt_tokened: OptTokened, Extension(db): Extension, Form(form_data): Form, ) -> Result { @@ -124,8 +125,8 @@ pub async fn slack_app_callback_handler( tracing::debug!("Payload: {:#?}", payload); match payload.r#type { - PayloadType::ViewSubmission => handle_submission(authed, db, &payload, "resume").await?, - PayloadType::ViewClosed => handle_submission(authed, db, &payload, "cancel").await?, + PayloadType::ViewSubmission => handle_submission(authed, opt_tokened, db, &payload, "resume").await?, + PayloadType::ViewClosed => handle_submission(authed, opt_tokened, db, &payload, "cancel").await?, _ => { if let Some(actions) = &payload.actions { if let Some(action) = actions.first() { @@ -256,6 +257,7 @@ pub async fn request_slack_approval( async fn handle_submission( authed: Option, + opt_tokened: OptTokened, db: DB, payload: &Payload, action: &str, @@ -294,7 +296,7 @@ async fn handle_submission( } // Use the common handler to process the resume/cancel action - handle_resume_action(authed, db.clone(), &resume_url, state_json, action).await?; + handle_resume_action(authed, opt_tokened, db.clone(), &resume_url, state_json, action).await?; let w_id = extract_w_id_from_resume_url(&resume_url)?; let slack_token = get_slack_token(&db, &resource_path, w_id).await?; diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index 3a4c2be568..99296c6689 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -44,7 +44,7 @@ use tower_cookies::{Cookie, Cookies}; use tracing::Instrument; use windmill_audit::audit_oss::{audit_log, AuditAuthor}; use windmill_audit::ActionKind; -use windmill_common::auth::fetch_authed_from_permissioned_as; +use windmill_common::auth::{fetch_authed_from_permissioned_as, TOKEN_PREFIX_LEN}; use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING; use windmill_common::oauth2::InstanceEvent; use windmill_common::users::COOKIE_NAME; @@ -243,13 +243,14 @@ pub async fn fetch_api_authed_from_permissioned_as( let api_authed = ApiAuthed { username: authed.username, - email: email, + email, is_admin: authed.is_admin, is_operator: authed.is_operator, groups: authed.groups, folders: authed.folders, scopes: authed.scopes, username_override: None, + token_prefix: authed.token_prefix, }; API_AUTHED_CACHE.insert( @@ -690,7 +691,12 @@ async fn logout( }; audit_log( &mut *tx, - &AuditAuthor { email: email.clone(), username: email, username_override: None }, + &AuditAuthor { + email: email.clone(), + username: email, + username_override: None, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), + }, audit_message, ActionKind::Delete, "global", @@ -1274,7 +1280,10 @@ async fn join_workspace<'c>( Ok((tx, username)) } -async fn leave_instance(Extension(db): Extension, authed: ApiAuthed) -> Result { +async fn leave_instance( + Extension(db): Extension, + authed: ApiAuthed, +) -> Result { let mut tx = db.begin().await?; sqlx::query!("DELETE FROM password WHERE email = $1", &authed.email) .execute(&mut *tx) @@ -1639,8 +1648,12 @@ async fn login( ) -> Result { let mut tx = db.begin().await?; let email = email.to_lowercase(); - let audit_author = - AuditAuthor { email: email.clone(), username: email.clone(), username_override: None }; + let audit_author = AuditAuthor { + email: email.clone(), + username: email.clone(), + username_override: None, + token_prefix: None, + }; let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as( "SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \ 'password'", @@ -1689,6 +1702,13 @@ async fn login( let token = create_session_token(&email, super_admin, &mut tx, cookies).await?; + let audit_author = AuditAuthor { + email: email.clone(), + username: email.clone(), + username_override: None, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), + }; + audit_log( &mut *tx, &audit_author, @@ -1758,6 +1778,7 @@ async fn refresh_token( email: authed.email.to_string(), username: authed.email.to_string(), username_override: None, + token_prefix: authed.token_prefix, }, "users.token.refresh", ActionKind::Create, @@ -1798,6 +1819,7 @@ pub async fn create_session_token<'c>( email: email.to_string(), username: email.to_string(), username_override: None, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }, "users.token.invalidate_old_sessions", ActionKind::Delete, diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 4a6a970db5..73aba91229 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -7,9 +7,7 @@ */ use crate::{ - db::{ApiAuthed, DB}, - users::{maybe_refresh_folders, require_owner_of_path}, - webhook_util::{WebhookMessage, WebhookShared}, + db::{ApiAuthed, DB}, users::{maybe_refresh_folders, require_owner_of_path}, webhook_util::{WebhookMessage, WebhookShared} }; use axum::{ @@ -23,10 +21,7 @@ use serde_json::Value; use windmill_audit::audit_oss::{audit_log, AuditAuthorable}; use windmill_audit::ActionKind; use windmill_common::{ - db::UserDB, - error::{Error, JsonResult, Result}, - utils::{not_found_if_none, paginate, Pagination, StripPath}, - variables::{ + db::UserDB, error::{Error, JsonResult, Result}, utils::{not_found_if_none, paginate, Pagination, StripPath}, variables::{ build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable, }, worker::CLOUD_HOSTED, diff --git a/backend/windmill-audit/src/audit_oss.rs b/backend/windmill-audit/src/audit_oss.rs index d29daaa7ae..f5979f45d7 100644 --- a/backend/windmill-audit/src/audit_oss.rs +++ b/backend/windmill-audit/src/audit_oss.rs @@ -20,14 +20,6 @@ use { }, }; -#[derive(Clone)] -#[cfg(not(feature = "private"))] -pub struct AuditAuthor { - pub username: String, - pub email: String, - pub username_override: Option, -} - #[cfg(not(feature = "private"))] impl AuditAuthorable for AuditAuthor { fn email(&self) -> &str { @@ -41,6 +33,10 @@ impl AuditAuthorable for AuditAuthor { fn username_override(&self) -> Option<&str> { self.username_override.as_deref() } + + fn token_prefix(&self) -> Option<&str> { + self.token_prefix.as_deref() + } } #[cfg(not(feature = "private"))] @@ -48,6 +44,18 @@ pub trait AuditAuthorable { fn username(&self) -> &str; fn email(&self) -> &str; fn username_override(&self) -> Option<&str>; + fn token_prefix(&self) -> Option<&str> { + None + } +} + +#[derive(Clone)] +#[cfg(not(feature = "private"))] +pub struct AuditAuthor { + pub username: String, + pub email: String, + pub username_override: Option, + pub token_prefix: Option, } #[cfg(not(feature = "private"))] diff --git a/backend/windmill-audit/src/lib.rs b/backend/windmill-audit/src/lib.rs index 15bda522c1..1c19890c37 100644 --- a/backend/windmill-audit/src/lib.rs +++ b/backend/windmill-audit/src/lib.rs @@ -24,6 +24,7 @@ pub struct AuditLog { pub action_kind: ActionKind, pub resource: Option, pub parameters: Option, + pub span: Option, } #[derive(Deserialize)] diff --git a/backend/windmill-common/src/auth.rs b/backend/windmill-common/src/auth.rs index 5823fccf7c..f4940793e4 100644 --- a/backend/windmill-common/src/auth.rs +++ b/backend/windmill-common/src/auth.rs @@ -17,6 +17,8 @@ pub struct IdToken { expiration: DateTime, } +pub const TOKEN_PREFIX_LEN: usize = 10; + pub fn has_expired(expiration_time: DateTime, take: Option) -> bool { let now = Utc::now(); @@ -66,6 +68,7 @@ pub struct JWTAuthClaims { pub exp: usize, pub job_id: Option, pub scopes: Option>, + pub audit_span: Option, } #[derive(Deserialize, Debug)] @@ -92,6 +95,7 @@ impl From for Authed { .filter_map(|x| serde_json::from_value::<(String, bool, bool)>(x).ok()) .collect(), scopes: None, + token_prefix: None, } } } @@ -171,38 +175,41 @@ pub async fn fetch_authed_from_permissioned_as( let folders = get_folders_for_user(w_id, &name, &groups, db).await?; Ok(Authed { - email: email, + email, username: name.to_string(), is_admin, is_operator, groups, folders, scopes: None, + token_prefix: None, }) } else { let groups = vec![name.to_string()]; let folders = get_folders_for_user(&w_id, "", &groups, db).await?; Ok(Authed { - email: email, + email, username: format!("group-{name}"), is_admin: false, groups, is_operator: false, folders, scopes: None, + token_prefix: None, }) } } else { let groups = vec![]; let folders = vec![]; Ok(Authed { - email: email, + email, username: permissioned_as, is_admin: super_admin, is_operator: true, groups, folders, scopes: None, + token_prefix: None, }) } } @@ -262,6 +269,7 @@ pub async fn create_token_for_owner( email: &str, job_id: &Uuid, perms: Option, + audit_span: Option, ) -> crate::error::Result { let job_perms = if perms.is_some() { Ok(perms) @@ -302,6 +310,7 @@ pub async fn create_token_for_owner( as usize, job_id: Some(job_id.to_string()), scopes: None, + audit_span, }; let token = jwt::encode_with_internal_secret(&payload) diff --git a/backend/windmill-common/src/db.rs b/backend/windmill-common/src/db.rs index 580e14f9d9..fe0d215c69 100644 --- a/backend/windmill-common/src/db.rs +++ b/backend/windmill-common/src/db.rs @@ -12,6 +12,7 @@ pub struct Authed { // (folder name, can write, is owner) pub folders: Vec<(String, bool, bool)>, pub scopes: Option>, + pub token_prefix: Option, } #[derive(Clone)] diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index fe2e08d0a1..26bc231e5b 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -428,6 +428,7 @@ pub async fn push_init_job<'c>( worker_name, "worker@windmill.dev", SUPERADMIN_SECRET_EMAIL.to_string(), + Some("worker_init_job"), None, None, None, @@ -1212,6 +1213,7 @@ pub async fn add_completed_job( &queued_job.created_by, &queued_job.permissioned_as_email, queued_job.permissioned_as.clone(), + Some(&format!("add.completed.job{}", queued_job.id)), scheduled_for, queued_job.schedule_path(), None, @@ -1735,6 +1737,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>( }, email, permissioned_as, + Some(&format!("error.handler.{job_id}")), None, None, Some(job_id), @@ -1842,6 +1845,7 @@ async fn handle_recovered_schedule<'a, 'c, T: Serialize + Send + Sync>( SCHEDULE_RECOVERY_HANDLER_USERNAME, email, permissioned_as, + Some(&format!("recovered.schedule.{job_id}")), None, None, Some(job_id), @@ -1930,6 +1934,7 @@ async fn handle_successful_schedule<'a, 'c, T: Serialize + Send + Sync>( SCHEDULE_RECOVERY_HANDLER_USERNAME, email, permissioned_as, + Some(&format!("successful.schedule.recovery{job_id}")), None, None, Some(job_id), @@ -2220,6 +2225,7 @@ pub async fn create_token(db: &DB, job: &MiniPulledJob, perms: Option) &job.permissioned_as_email, &job.id, perms, + Some(format!("job-span-{}", job.flow_innermost_root_job.unwrap_or(job.id))), ) .warn_after_seconds(5) .await @@ -3257,6 +3263,7 @@ pub async fn push<'c, 'd>( user: &str, mut email: &str, mut permissioned_as: String, + token_prefix: Option<&str>, scheduled_for_o: Option>, schedule_path: Option, parent_job: Option, @@ -4372,12 +4379,14 @@ pub async fn push<'c, 'd>( email: email.to_string(), username: permissioned_as.trim_start_matches("u/").to_string(), username_override: Some(user.to_string()), + token_prefix: token_prefix.map(|s| s.to_string()), } } else { AuditAuthor { email: email.to_string(), username: user.to_string(), username_override: None, + token_prefix: token_prefix.map(|s| s.to_string()), } }; @@ -4700,4 +4709,4 @@ pub async fn get_same_worker_job( same_worker_job.job_id, e )) }) -} \ No newline at end of file +} diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index 294ece7509..f539ac7818 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -271,6 +271,7 @@ pub async fn push_scheduled_job<'c>( &schedule_to_user(&schedule.path), email, permissioned_as, + Some(&schedule.path), Some(next), Some(schedule.path.clone()), None, diff --git a/backend/windmill-worker/src/result_processor.rs b/backend/windmill-worker/src/result_processor.rs index 5056851389..ce314a00e6 100644 --- a/backend/windmill-worker/src/result_processor.rs +++ b/backend/windmill-worker/src/result_processor.rs @@ -17,12 +17,7 @@ use windmill_common::otel_oss::FutureExt; use uuid::Uuid; use windmill_common::{ - add_time, - error::{self, Error}, - jobs::JobKind, - utils::WarnAfterExt, - worker::{to_raw_value, Connection, WORKER_GROUP}, - KillpillSender, DB, + add_time, error::{self, Error}, jobs::JobKind, utils::WarnAfterExt, worker::{to_raw_value, Connection, WORKER_GROUP}, KillpillSender, DB }; #[cfg(feature = "benchmark")] diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 95ffc28062..98c292b97a 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -17,6 +17,7 @@ use crate::worker_utils::get_tag_and_concurrency; use crate::{ JobCompletedSender, PreviousResult, SameWorkerSender, SendResult, UpdateFlow, KEEP_JOB_DIR, }; + use anyhow::Context; use futures::TryFutureExt; use mappable_rc::Marc; @@ -2030,6 +2031,7 @@ async fn push_next_flow_job( .to_string(), email: flow_job.permissioned_as_email.clone(), username_override: None, + token_prefix: Some(format!("psh.nxt.flowjob-{}", client.token.to_string())), }; if can_be_resumed || disapproved_or_timeout_but_continue { @@ -2772,6 +2774,7 @@ async fn push_next_flow_job( &flow_job.created_by, email, permissioned_as, + Some(&format!("job-span-{}", flow_job.flow_innermost_root_job.unwrap_or(flow_job.id))), scheduled_for_o, flow_job.schedule_path(), Some(flow_job.id), diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 7fee5932e4..e24ca1972a 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -608,6 +608,7 @@ async fn trigger_dependents_to_recompute_dependencies( &created_by, email, permissioned_as.to_string(), + Some("trigger.dependents.to.recompute.dependencies"), None, None, None, diff --git a/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte b/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte index ff0198ccf7..c0582ffc44 100644 --- a/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte +++ b/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte @@ -97,6 +97,7 @@ actionKind: ActionKind | undefined | 'all', scope: undefined | 'all_workspaces' | 'instance' ): Promise { + console.log("loading logs") loading = true if (username == 'all') { @@ -130,6 +131,7 @@ hasMore = logs.length > 0 && logs.length === perPage loading = false + console.log("loadede logs") } async function loadUsers() { @@ -353,24 +355,32 @@ After { + on:change={({ detail }) => { after = new Date(detail).toISOString() }} + on:clear={() => { + after = undefined + }} />
Before { + on:change={({ detail }) => { before = new Date(detail).toISOString() }} + on:clear={() => { + before = undefined + }} />
diff --git a/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte b/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte index 601fb9c567..ba98b6259a 100644 --- a/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte +++ b/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte @@ -1,24 +1,38 @@ - { - pageIndex = (pageIndex ?? 1) + 1 - }} - on:previous={() => { - pageIndex = (pageIndex ?? 1) - 1 - }} - currentPage={pageIndex} - paginated - rounded={false} - size="sm" - {hasMore} - bind:perPage + computeHeight()} /> + +
- - ID - Timestamp - Username - Operation - Resource - - {#if logs?.length > 0} - - {#each Object.entries(groupedLogs) as [date, logsByDay]} - - - {date} - - - {#each logsByDay as { id, timestamp, username, operation: op, action_kind, resource, parameters }} - { - dispatch('select', id) - }} - > - - {id} - - - {displayDate(timestamp)} - - -
-
- {username} - {#if parameters && 'end_user' in parameters} - ({parameters.end_user}) - {/if} -
-
-
- -
- { - actionKind = action_kind.toLocaleLowerCase() - }} - color={kindToBadgeColor(action_kind)}>{action_kind} - { - operation = op - }} - > - {op} - -
-
- -
-
- {resource} -
-
-
-
- {/each} - {/each} - + +
+
+
ID
+
Timestamp
+
Username
+
Operation
+
Resource
+
+
+ {#if logs?.length == 0} +
No logs found for the selected filters.
{:else} - - -
No logs found for the selected filters.
- - + + {#snippet header()}{/snippet} + {#snippet children({ index, style })} +
+ {#if flatLogs} + {@const logOrDate = flatLogs[index]} + + {#if logOrDate} + {#if logOrDate?.type === 'date'} +
+ {logOrDate.date} +
+ {:else} + + +
{ + onselect?.(logOrDate.log.id) + }} + > +
+ {logOrDate.log.id} +
+
+ {displayDate(logOrDate.log.timestamp)} +
+
+
+
+ {logOrDate.log.username} + {#if logOrDate.log.parameters && 'end_user' in logOrDate.log.parameters} + ({logOrDate.log.parameters.end_user}) + {/if} +
+
+
+
+
+ { + actionKind = logOrDate.log.action_kind.toLocaleLowerCase() + }} + color={kindToBadgeColor(logOrDate.log.action_kind)} + > + {logOrDate.log.action_kind} + + { + operation = logOrDate.log.operation + }} + > + {logOrDate.log.operation} + +
+
+
+
+
+ {logOrDate.log.resource} +
+
+
+
+ {/if} + {:else} +
+
Loading...
+
+ {/if} + {:else} +
+
Loading...
+
+ {/if} +
+ {/snippet} + {#snippet footer()}{/snippet} +
{/if} - + +
+
+ + Page {pageIndex} + +
+
+ Per page: + +
+
+
diff --git a/frontend/src/lib/components/auditLogs/AuditLogsTimeline.svelte b/frontend/src/lib/components/auditLogs/AuditLogsTimeline.svelte new file mode 100644 index 0000000000..e2f7fc2365 --- /dev/null +++ b/frontend/src/lib/components/auditLogs/AuditLogsTimeline.svelte @@ -0,0 +1,564 @@ + + +
+ {#if logs.length === 0} +
No audit logs to display
+ {:else if !groupedData || groupedData.status === 'loading'} +
+ + Processing audit logs... +
+ {:else} + + {/if} +
diff --git a/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte b/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte index e28cbbb20c..30fd36ace4 100644 --- a/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte @@ -1,40 +1,82 @@ {#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.audit_logs} @@ -43,15 +85,15 @@

Page not available for operators

{:else} -
-
-
-
-

Audit logs

- - You can only see your own audit logs unless you are an admin. - -
+
+
+
+

Audit logs

+ + You can only see your own audit logs unless you are an admin. + +
+
- - {#if !$enterpriseLicense || $enterpriseLicense.endsWith('_pro')} - - You need an enterprise license to see unredacted audit logs. - -
+
+
+ {#if logs} + { + before = max.toISOString() + after = min.toISOString() + console.log('zoom!') + }} + onMissingJobSpan={fetchMissingJobSpan} + onLogSelected={(log) => { + console.log('selected log ') + selectedId = log.id + }} + /> {/if}
- +
+
+ {#if !$enterpriseLicense || $enterpriseLicense.endsWith('_pro')} + + You need an enterprise license to see unredacted audit logs. + +
+ {/if} +
+ + + + {#if logs} + { + selectedId = id + }} + /> + {:else} +
+ {#each new Array(8) as _} + + {/each} +
+ {/if} +
+ + {#if logs} + + {/if} + +
+
-
- { - selectedId = e.detail - - auditLogDrawer?.openDrawer() - }} - /> +
+ { + selectedId = id + auditLogDrawer?.openDrawer() + }} + /> +
{/if} From c498c48ced893181034a2fdf936a1f4fbd41b21c Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:11:29 +0200 Subject: [PATCH 4/9] feat: allow editing messages in AI chat (#6117) * feat: add message editing functionality to AI chat - Add editMessage() method to AIChatManager for editing user messages - Add edit button UI that appears on hover for all user messages - Transform message display into textarea when editing - Remove subsequent messages and resend on Enter - Add keyboard shortcuts: Enter to save, Escape to cancel - Maintain existing restart generation functionality Co-authored-by: Ruben Fiszel * fix * big cleaning * use aichatinput when editing message * cleaning * fix * remove logs * better retry button * fix * Update frontend/src/lib/components/copilot/chat/ContextTextarea.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * cleaning * fix retry logic --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel Co-authored-by: centdix Co-authored-by: centdix <40307056+centdix@users.noreply.github.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .../copilot/chat/AIChatDisplay.svelte | 206 ++---------------- .../copilot/chat/AIChatInput.svelte | 162 ++++++++++++++ .../copilot/chat/AIChatManager.svelte.ts | 69 ++++-- .../copilot/chat/AIChatMessage.svelte | 112 ++++++++++ .../copilot/chat/ContextTextarea.svelte | 42 ++-- .../src/lib/components/copilot/chat/shared.ts | 35 ++- 6 files changed, 386 insertions(+), 240 deletions(-) create mode 100644 frontend/src/lib/components/copilot/chat/AIChatInput.svelte create mode 100644 frontend/src/lib/components/copilot/chat/AIChatMessage.svelte diff --git a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte index aea971c0f7..11f42445de 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte @@ -1,31 +1,17 @@ @@ -208,68 +160,13 @@ >
{#each messages as message, messageIndex} -
0 && 'mt-6', 'mb-2')}> - {#if message.role === 'user' && message.contextElements} -
- {#each message.contextElements as element} - - {/each} -
- {/if} -
- {#if message.role === 'assistant'} - - {:else} - {message.content} - {/if} - - {#if message.role === 'user' && isLastUserMessage(messageIndex) && !aiChatManager.loading} -
-
- {/if} -
- {#if message.role === 'user' && message.snapshot} -
- Saved a flow snapshot - -
- {/if} -
+ {/each} {#if aiChatManager.loading && !aiChatManager.currentReply}
@@ -323,71 +220,14 @@
{/if} - {#if aiChatManager.mode === 'script'} -
- - -
@
-
- - { - addContextToSelection(element) - close() - }} - /> - -
- {#each selectedContext as element} - { - selectedContext = selectedContext?.filter( - (c) => c.type !== element.type || c.title !== element.title - ) - }} - /> - {/each} -
- addContextToSelection(contextElement)} - onSendRequest={() => { - if (!aiChatManager.loading) { - aiChatManager.sendRequest() - } - }} - onUpdateInstructions={(value) => (aiChatManager.instructions = value)} - {disabled} - /> - {:else} -
- -
- {/if} +
+ import Popover from '$lib/components/meltComponents/Popover.svelte' + import AvailableContextList from './AvailableContextList.svelte' + import ContextElementBadge from './ContextElementBadge.svelte' + import ContextTextarea from './ContextTextarea.svelte' + import autosize from '$lib/autosize' + import type { ContextElement } from './context' + import { aiChatManager } from './AIChatManager.svelte' + + interface Props { + availableContext: ContextElement[] + selectedContext: ContextElement[] + isFirstMessage?: boolean + disabled?: boolean + placeholder?: string + initialInstructions?: string + editingMessageIndex?: number | null + onEditEnd?: () => void + } + + let { + availableContext, + selectedContext = $bindable([]), + disabled = false, + isFirstMessage = false, + placeholder = 'Ask anything', + initialInstructions = '', + editingMessageIndex = null, + onEditEnd = () => {} + }: Props = $props() + + let contextTextareaComponent: ContextTextarea | undefined = $state() + let instructionsTextareaComponent: HTMLTextAreaElement | undefined = $state() + let instructions = $state(initialInstructions) + + export function focusInput() { + if (aiChatManager.mode === 'script') { + contextTextareaComponent?.focus() + } else { + instructionsTextareaComponent?.focus() + } + } + + function clickOutside(node: HTMLElement) { + function handleClick(event: MouseEvent) { + if (node && !node.contains(event.target as Node) && editingMessageIndex !== null) { + onEditEnd() + } + } + + document.addEventListener('click', handleClick, true) + return { + destroy() { + document.removeEventListener('click', handleClick, true) + } + } + } + + function addContextToSelection(contextElement: ContextElement) { + if ( + selectedContext && + availableContext && + !selectedContext.find( + (c) => c.type === contextElement.type && c.title === contextElement.title + ) && + availableContext.find( + (c) => c.type === contextElement.type && c.title === contextElement.title + ) + ) { + selectedContext = [...selectedContext, contextElement] + } + } + + function sendRequest() { + if (aiChatManager.loading) { + return + } + if (editingMessageIndex !== null) { + aiChatManager.restartGeneration(editingMessageIndex, instructions) + onEditEnd() + } else { + aiChatManager.sendRequest({ instructions }) + instructions = '' + } + } + + $effect(() => { + if (editingMessageIndex !== null) { + focusInput() + } + }) + + +
+ {#if aiChatManager.mode === 'script'} +
+ + +
@
+
+ + { + addContextToSelection(element) + close() + }} + /> + +
+ {#each selectedContext as element} + { + selectedContext = selectedContext?.filter( + (c) => c.type !== element.type || c.title !== element.title + ) + }} + /> + {/each} +
+ addContextToSelection(contextElement)} + onSendRequest={() => { + sendRequest() + }} + {disabled} + onEscape={onEditEnd} + /> + {:else} +
+ +
+ {/if} +
diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 2480304522..ebff218afe 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -32,7 +32,6 @@ import type { DBSchemas } from '$lib/stores' import { askTools, prepareAskSystemMessage } from './ask/core' import { chatState, DEFAULT_SIZE, triggerablesByAi } from './sharedChatState.svelte' - export enum AIMode { SCRIPT = 'script', FLOW = 'flow', @@ -217,6 +216,32 @@ class AIChatManager { } } + retryRequest = (messageIndex: number) => { + const message = this.displayMessages[messageIndex] + if (message && message.role === 'user') { + this.restartGeneration(messageIndex) + message.error = false + } else { + throw new Error('No user message found at the specified index') + } + } + + private getLastUserMessage = () => { + for (let i = this.displayMessages.length - 1; i >= 0; i--) { + const message = this.displayMessages[i] + if (message.role === 'user') { + return message + } + } + } + + private flagLastMessageAsError = () => { + const lastUserMessage = this.getLastUserMessage() + if (lastUserMessage) { + lastUserMessage.error = true + } + } + private chatRequest = async ({ messages, abortController, @@ -403,7 +428,8 @@ class AIChatManager { role: 'user', content: this.instructions, contextElements: this.mode === AIMode.SCRIPT ? oldSelectedContext : undefined, - snapshot + snapshot, + index: this.messages.length // matching with actual messages index. not -1 because it's not yet added to the messages array } ] const oldInstructions = this.instructions @@ -423,8 +449,8 @@ class AIChatManager { : this.mode === AIMode.NAVIGATOR ? prepareNavigatorUserMessage(oldInstructions) : await prepareScriptUserMessage(oldInstructions, lang, oldSelectedContext, { - isPreprocessor - }) + isPreprocessor + }) this.messages.push(userMessage) await this.historyManager.saveChat(this.displayMessages, this.messages) @@ -478,6 +504,7 @@ class AIChatManager { await this.historyManager.saveChat(this.displayMessages, this.messages) } catch (err) { console.error(err) + this.flagLastMessageAsError() if (err instanceof Error) { sendUserToast('Failed to send request: ' + err.message, true) } else { @@ -492,7 +519,7 @@ class AIChatManager { this.abortController?.abort() } - restartLastGeneration = (displayMessageIndex: number) => { + restartGeneration = (displayMessageIndex: number, newContent?: string) => { const userMessage = this.displayMessages[displayMessageIndex] if (!userMessage || userMessage.role !== 'user') { @@ -502,23 +529,17 @@ class AIChatManager { // Remove all messages including and after the specified user message this.displayMessages = this.displayMessages.slice(0, displayMessageIndex) - // Find the last user message in actual messages and remove it and everything after it - let lastActualUserMessageIndex = -1 - for (let i = this.messages.length - 1; i >= 0; i--) { - if (this.messages[i].role === 'user') { - lastActualUserMessageIndex = i - break - } - } + // Find corresponding message in actual messages and remove it and everything after it + let actualMessageIndex = this.messages.findIndex((_, i) => i === userMessage.index) - if (lastActualUserMessageIndex === -1) { + if (actualMessageIndex === -1) { throw new Error('No actual user message found to restart from') } - this.messages = this.messages.slice(0, lastActualUserMessageIndex) + this.messages = this.messages.slice(0, actualMessageIndex) // Resend the request with the same instructions - this.instructions = userMessage.content + this.instructions = newContent ?? userMessage.content this.sendRequest() } @@ -648,15 +669,15 @@ class AIChatManager { const editorRelated = currentEditor && currentEditor.type === 'script' && currentEditor.stepId === module.id ? { - diffMode: currentEditor.diffMode, - lastDeployedCode: currentEditor.lastDeployedCode, - lastSavedCode: undefined - } + diffMode: currentEditor.diffMode, + lastDeployedCode: currentEditor.lastDeployedCode, + lastSavedCode: undefined + } : { - diffMode: false, - lastDeployedCode: undefined, - lastSavedCode: undefined - } + diffMode: false, + lastDeployedCode: undefined, + lastSavedCode: undefined + } return { args: moduleState?.previewArgs ?? {}, diff --git a/frontend/src/lib/components/copilot/chat/AIChatMessage.svelte b/frontend/src/lib/components/copilot/chat/AIChatMessage.svelte new file mode 100644 index 0000000000..66f6585a40 --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/AIChatMessage.svelte @@ -0,0 +1,112 @@ + + +
0 && 'mt-6', + 'mb-2', + message.role !== 'user' ? 'cursor-default' : 'cursor-pointer' + )} + role="button" + tabindex="0" + onclick={() => editMessage()} + onkeydown={() => {}} +> + {#if message.role === 'user' && message.contextElements && editingMessageIndex !== messageIndex} +
+ {#each message.contextElements as element} + + {/each} +
+ {/if} + {#if message.role === 'user' && editingMessageIndex === messageIndex} + (editingMessageIndex = null)} + /> + {:else} +
+ {#if message.role === 'assistant'} + + {:else} + {message.content} + {/if} +
+ {/if} + {#if message.role === 'user' && message.snapshot} +
+ Saved a flow snapshot + +
+ {/if} +
+{#if message.role === 'user' && message.error} +
+ +
+{/if} diff --git a/frontend/src/lib/components/copilot/chat/ContextTextarea.svelte b/frontend/src/lib/components/copilot/chat/ContextTextarea.svelte index cfec386ba3..b348862048 100644 --- a/frontend/src/lib/components/copilot/chat/ContextTextarea.svelte +++ b/frontend/src/lib/components/copilot/chat/ContextTextarea.svelte @@ -3,28 +3,31 @@ import { tick } from 'svelte' import type { ContextElement } from './context' import AvailableContextList from './AvailableContextList.svelte' - import { aiChatManager } from './AIChatManager.svelte' import Portal from '$lib/components/Portal.svelte' import { zIndexes } from '$lib/zIndexes' interface Props { + value: string availableContext: ContextElement[] selectedContext: ContextElement[] isFirstMessage: boolean + placeholder: string disabled: boolean - onUpdateInstructions: (value: string) => void onSendRequest: () => void onAddContext: (contextElement: ContextElement) => void + onEscape: () => void } - const { + let { + value = $bindable(''), availableContext, selectedContext, isFirstMessage, + placeholder, disabled, - onUpdateInstructions, onSendRequest, - onAddContext + onAddContext, + onEscape }: Props = $props() let showContextTooltip = $state(false) @@ -163,11 +166,10 @@ } function updateInstructionsWithContext(contextElement: ContextElement) { - const index = aiChatManager.instructions.lastIndexOf('@') + const index = value.lastIndexOf('@') if (index !== -1) { - const newInstructions = - aiChatManager.instructions.substring(0, index) + `@${contextElement.title}` - onUpdateInstructions(newInstructions) + const newInstructions = value.substring(0, index) + `@${contextElement.title}` + value = newInstructions } } @@ -252,7 +254,7 @@ function handleInput(e: Event) { textarea = e.target as HTMLTextAreaElement - const words = aiChatManager.instructions.split(/\s+/) + const words = value.split(/\s+/) const lastWord = words[words.length - 1] if ( @@ -267,7 +269,6 @@ contextTooltipWord = '' selectedSuggestionIndex = 0 } - onUpdateInstructions(aiChatManager.instructions) } function handleKeyPress(e: KeyboardEvent) { @@ -283,10 +284,7 @@ (c) => c.title === contextElement.title && c.type === contextElement.type ) // If the context element is already in the selected context and the last word in the instructions is the same as the context element title, send request - if ( - isInSelectedContext && - aiChatManager.instructions.split(' ').pop() === '@' + contextElement.title - ) { + if (isInSelectedContext && value.split(' ').pop() === '@' + contextElement.title) { onSendRequest() return } @@ -301,6 +299,10 @@ } function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + onEscape() + } + if (!showContextTooltip) return const filteredContext = availableContext.filter( @@ -337,14 +339,14 @@
- {@html getHighlightedText(aiChatManager.instructions)} + {@html getHighlightedText(value)}
diff --git a/frontend/src/lib/components/copilot/chat/shared.ts b/frontend/src/lib/components/copilot/chat/shared.ts index f1fa174f38..e0ef74bb40 100644 --- a/frontend/src/lib/components/copilot/chat/shared.ts +++ b/frontend/src/lib/components/copilot/chat/shared.ts @@ -8,18 +8,29 @@ import type { ContextElement } from './context' import { workspaceStore } from '$lib/stores' import type { ExtendedOpenFlow } from '$lib/components/flows/types' -export type DisplayMessage = - | { - role: 'user' | 'assistant' - content: string - contextElements?: ContextElement[] - snapshot?: ExtendedOpenFlow - } - | { - role: 'tool' - tool_call_id: string - content: string - } +type BaseDisplayMessage = { + content: string + contextElements?: ContextElement[] + snapshot?: ExtendedOpenFlow +} + +export type UserDisplayMessage = BaseDisplayMessage & { + role: 'user' + index: number // Used to match index with actual chat messages + error?: boolean +} + +export type ToolDisplayMessage = { + role: 'tool' + tool_call_id: string + content: string +} + +export type AssistantDisplayMessage = BaseDisplayMessage & { + role: 'assistant' +} + +export type DisplayMessage = UserDisplayMessage | ToolDisplayMessage | AssistantDisplayMessage async function callTool({ tools, From e3aee0c587371b57d89c99603ade0fc0a4a1a939 Mon Sep 17 00:00:00 2001 From: wendrul <53628737+wendrul@users.noreply.github.com> Date: Fri, 4 Jul 2025 20:13:58 +0900 Subject: [PATCH 5/9] fix: ctrl k not showing navigation items + improve ai button by making it an item in the menu (#6132) * Fix missing navigation after migration to svelte 5 of ctrl k search .includes doesn't work anymore because the items inside the array are treated as state, and thus svelte wraps them with a Proxy thing * Change ask ai to be a menu item * Remove comment --- .../search/GlobalSearchModal.svelte | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/frontend/src/lib/components/search/GlobalSearchModal.svelte b/frontend/src/lib/components/search/GlobalSearchModal.svelte index 422d3ba7e1..2a36576dfd 100644 --- a/frontend/src/lib/components/search/GlobalSearchModal.svelte +++ b/frontend/src/lib/components/search/GlobalSearchModal.svelte @@ -25,7 +25,8 @@ Route, Search, SearchCode, - Unplug + Unplug, + WandSparkles } from 'lucide-svelte' import Portal from '$lib/components/Portal.svelte' @@ -293,8 +294,7 @@ } if (tab === 'default') { - if (searchTerm === '') - itemMap['default'] = fuzzyFilter(searchTerm, defaultMenuItems, defaultMenuItemLabels) + if (searchTerm === '') itemMap['default'] = defaultMenuItems else itemMap['default'] = fuzzyFilter( searchTerm, @@ -372,9 +372,6 @@ } } } - if ((itemMap[tab] ?? []).length === 0 && searchTerm.length > 0 && event.key === 'Enter') { - askAiButton?.onClick() - } } } @@ -653,10 +650,14 @@
{#if tab === 'default' || tab === 'switch-mode'} {@const items = (itemMap[tab] ?? []).filter((e) => - defaultMenuItemsWithHidden.includes(e) + defaultMenuItemsWithHidden.some((x) => e.search_id === x.search_id) )} {#if items.length > 0} -
+
{#each items as el} el?.action(shift)} @@ -674,8 +675,8 @@ {/if} {#if tab === 'default'} -
- {#if (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)).length > 0} + {#if (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)).length > 0} +
Flows/Scripts/Apps
@@ -694,19 +695,28 @@ bind:mouseMoved /> {/each} - {/if} +
+ {/if} - {#if (itemMap[tab] ?? []).length === 0} + {#if (itemMap[tab] ?? []).length === 0} +
+ { + askAiButton?.onClick() + }} + id={'ai:no-results-ask-ai'} + hovered={true} + label={`Try asking \`${searchTerm}\` to AI`} + icon={WandSparkles} + bind:mouseMoved + />
-
Nothing found, ask the AI to find what you need!
-
Tip: press `esc` to quickly clear the search bar
+
Tip: press `esc` to quickly clear the search bar
- {/if} -
+
+ {/if} {:else if tab === 'content'} Date: Fri, 4 Jul 2025 13:18:57 +0200 Subject: [PATCH 6/9] internal: better claude review (#6130) * better claude review * Update pr-ready-review.yml --------- Co-authored-by: Ruben Fiszel --- .github/workflows/pr-ready-review.yml | 49 +++++++++++++++++++-------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr-ready-review.yml b/.github/workflows/pr-ready-review.yml index bddb0788ae..8140b8bb55 100644 --- a/.github/workflows/pr-ready-review.yml +++ b/.github/workflows/pr-ready-review.yml @@ -1,22 +1,41 @@ -name: Auto Comment on PR Ready for Review +name: Claude Auto Review on: pull_request: - types: [opened, ready_for_review] + types: [ready_for_review, opened] + +concurrency: + group: claude-review-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: - add-review-comment: - if: github.event.pull_request.draft == false - runs-on: ubicloud-standard-2 + auto-review: + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false || github.event.pull_request.ready_for_review == true + permissions: + contents: read + pull-requests: read + id-token: write steps: - - name: Add review comment - uses: actions/github-script@v7 + - name: Checkout repository + uses: actions/checkout@v4 with: - github-token: ${{ secrets.PUBLIC_REPO_TOKEN }} - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body: '/ai review this PR' - }); + fetch-depth: 1 + + - name: Automatic PR Review + uses: anthropics/claude-code-action@beta + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + timeout_minutes: "60" + direct_prompt: | + Please review this pull request and provide comprehensive feedback. + + Focus on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security implications + + Provide constructive feedback with specific suggestions for improvement. + Use inline comments to highlight specific areas of concern. + allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff" From be62977047cf2cac60b6754d0e8af3c8cca0cbd0 Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Fri, 4 Jul 2025 13:19:21 +0200 Subject: [PATCH 7/9] fix(frontend): make sure to set workspaceStore and token before mount in extension (#6129) * fix(frontend): make sure to set workspaceStore and token before mount in extension * nit --- frontend/src/lib/components/Dev.svelte | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index 0ee118010f..9908ed8af6 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -524,22 +524,21 @@ let workspace = $derived($page.url.searchParams.get('workspace') ?? undefined) let themeDarkRaw = $derived($page.url.searchParams.get('activeColorTheme')) let themeDark = $derived(themeDarkRaw == '2' || themeDarkRaw == '4') - $effect(() => { + $effect.pre(() => { if (token) { OpenAPI.WITH_CREDENTIALS = true OpenAPI.TOKEN = token - untrack(() => loadUser()) } }) - $effect(() => { + $effect.pre(() => { if (workspace) { $workspaceStore = workspace - untrack(() => setupCopilotInfo()) } }) - $effect(() => { + $effect.pre(() => { if (workspace && token) { untrack(() => loadUser()) + untrack(() => setupCopilotInfo()) } }) $effect(() => { From d69d277ff9409b624df0537b507c76ec13909814 Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Fri, 4 Jul 2025 13:19:42 +0200 Subject: [PATCH 8/9] fix: correctly set selected step editor code when reverting to snapshot (#6131) --- .../components/copilot/chat/flow/FlowAIChat.svelte | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte index 0dd3bfa20b..5c54e31dcb 100644 --- a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte +++ b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte @@ -115,6 +115,19 @@ if (snapshot) { flowStore.val = snapshot refreshStateStore(flowStore) + + if ($currentEditor) { + const module = getModule($currentEditor.stepId, snapshot) + if (module) { + if ($currentEditor.type === 'script' && module.value.type === 'rawscript') { + $currentEditor.editor.setCode(module.value.content) + } else if ($currentEditor.type === 'iterator' && module.value.type === 'forloopflow') { + $currentEditor.editor.setCode( + module.value.iterator.type === 'javascript' ? module.value.iterator.expr : '' + ) + } + } + } } }, showModuleDiff(id: string) { From 3fbd3ec4f914349767a20111157c96b62c9f43ce Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 7 Jul 2025 10:27:09 +0000 Subject: [PATCH 9/9] fix: tag select in script builder top bar (#6136) * all * update * react 18 * update * update * update --- frontend/package-lock.json | 6 +- frontend/package.json | 69 +++--- frontend/scripts/untar_ui_builder.js | 27 ++- frontend/src/lib/components/AppWrapper.svelte | 10 + frontend/src/lib/components/CliHelpBox.svelte | 3 +- .../src/lib/components/FirstStepInputs.svelte | 2 +- .../src/lib/components/FlowBuilder.svelte | 97 +++------ .../src/lib/components/FlowWrapper.svelte | 13 ++ .../src/lib/components/ResourceEditor.svelte | 10 +- .../src/lib/components/ScriptBuilder.svelte | 197 ++++++++++-------- .../src/lib/components/ScriptEditor.svelte | 4 +- .../src/lib/components/ScriptWrapper.svelte | 13 ++ .../src/lib/components/WorkerTagPicker.svelte | 24 ++- .../src/lib/components/WorkerTagSelect.svelte | 13 +- .../components/apps/editor/AppEditor.svelte | 49 ++--- .../apps/editor/AppEditorHeader.svelte | 48 +++-- frontend/src/lib/components/apps/types.ts | 41 +++- .../DeployOverrideConfirmationModal.svelte | 25 ++- .../copilot/chat/AiChatLayout.svelte | 98 +++++++++ .../components/copilot/chat/navigator/core.ts | 3 +- frontend/src/lib/components/custom_ui.ts | 5 +- frontend/src/lib/components/diff_drawer.ts | 24 +++ frontend/src/lib/components/flow_builder.ts | 52 +++++ .../components/flows/CreateActionsFlow.svelte | 2 +- .../lib/components/flows/FlowHistory.svelte | 22 +- .../components/flows/FlowHistoryInner.svelte | 48 +++-- .../flows/common/FlowCardHeader.svelte | 58 +++++- .../flows/content/FlowEditorPanel.svelte | 2 +- .../flows/content/FlowModuleComponent.svelte | 16 +- .../flows/content/FlowModuleHeader.svelte | 5 + .../flows/content/FlowModuleScript.svelte | 135 ++++++++---- .../content/FlowModuleWorkerTagSelect.svelte | 17 +- .../flows/content/FlowSettings.svelte | 5 +- .../lib/components/flows/flowStore.svelte.ts | 87 -------- .../src/lib/components/flows/flowStore.ts | 45 ++-- .../flows/map/FlowModuleSchemaItem.svelte | 2 +- .../flows/map/FlowModuleSchemaMap.svelte | 2 +- .../lib/components/graph/FlowGraphV2.svelte | 3 +- .../raw_apps/RawAppEditorHeader.svelte | 4 +- .../schema/EditableSchemaSdkWrapper.svelte | 13 ++ .../schema/EditableSchemaWrapper.svelte | 26 +-- .../schema/editable_schema_wrapper.ts | 10 + frontend/src/lib/components/script_builder.ts | 35 ++++ .../search/GlobalSearchModal.svelte | 2 +- .../src/lib/components/select/Select.svelte | 2 +- .../src/lib/components/select/utils.svelte.ts | 6 +- .../triggers/gcp/GcpTriggerEditorInner.svelte | 2 +- .../components/triggers/scheduled/utils.ts | 3 +- frontend/src/lib/logout.ts | 2 +- frontend/src/lib/stores.ts | 18 +- frontend/src/lib/utils.ts | 11 +- .../src/routes/(root)/(logged)/+layout.svelte | 92 +------- .../(root)/(logged)/flows/add/+page.svelte | 14 +- .../flows/edit/[...path]/+page.svelte | 86 ++++---- .../(logged)/flows/get/[...path]/+page.svelte | 2 +- .../(root)/(logged)/scripts/add/+page.svelte | 10 +- .../scripts/edit/[...path]/+page.svelte | 15 +- .../public_app_imports}/+page.svelte | 0 .../src/routes/test_dev/sdk_flow/+page.svelte | 51 +++++ .../routes/test_dev/sdk_script/+page.svelte | 41 ++++ .../src/routes/test_dev_page/+page.svelte | 105 ---------- 61 files changed, 1058 insertions(+), 774 deletions(-) create mode 100644 frontend/src/lib/components/AppWrapper.svelte create mode 100644 frontend/src/lib/components/FlowWrapper.svelte create mode 100644 frontend/src/lib/components/ScriptWrapper.svelte create mode 100644 frontend/src/lib/components/copilot/chat/AiChatLayout.svelte create mode 100644 frontend/src/lib/components/diff_drawer.ts create mode 100644 frontend/src/lib/components/flow_builder.ts delete mode 100644 frontend/src/lib/components/flows/flowStore.svelte.ts create mode 100644 frontend/src/lib/components/schema/EditableSchemaSdkWrapper.svelte create mode 100644 frontend/src/lib/components/schema/editable_schema_wrapper.ts create mode 100644 frontend/src/lib/components/script_builder.ts rename frontend/src/routes/{test_dev_page_2 => test_dev/public_app_imports}/+page.svelte (100%) create mode 100644 frontend/src/routes/test_dev/sdk_flow/+page.svelte create mode 100644 frontend/src/routes/test_dev/sdk_script/+page.svelte delete mode 100644 frontend/src/routes/test_dev_page/+page.svelte diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e33a53c091..efe3d64cbd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.502.2", + "version": "1.502.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.502.2", + "version": "1.502.5", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -151,7 +151,7 @@ "fsevents": "^2.3.3" }, "peerDependencies": { - "svelte": "^4.0.0" + "svelte": "^5.0.0" } }, "node_modules/@alloc/quick-lru": { diff --git a/frontend/package.json b/frontend/package.json index 12778fc663..cdd5241f4b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,7 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "postinstall": "node scripts/untar_ui_builder.js && node scripts/patch_files.js", + "postinstall": "if [ -f ./scripts/untar_ui_builder.js ]; then node ./scripts/untar_ui_builder.js && node ./scripts/patch_files.js; fi", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --threshold warning", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", @@ -161,7 +161,7 @@ "zod-to-json-schema": "^3.24.5" }, "peerDependencies": { - "svelte": "^4.0.0" + "svelte": "^5.0.0" }, "exports": { "./package.json": "./package.json", @@ -169,7 +169,16 @@ "types": "./dist/index.d.ts", "svelte": "./dist/index.js" }, + "./scripts/untar_ui_builder.js": "./scripts/untar_ui_builder.js", "./assets/app.css": "./package/assets/app.css", + "./components/custom_ui": { + "types": "./package/components/custom_ui.d.ts", + "default": "./package/components/custom_ui.js" + }, + "./components/scriptBuilder": { + "types": "./package/components/scriptBuilder.d.ts", + "default": "./package/components/scriptBuilder.js" + }, "./components/TestJobLoader.svelte": { "types": "./package/components/TestJobLoader.svelte.d.ts", "svelte": "./package/components/TestJobLoader.svelte", @@ -255,20 +264,20 @@ "svelte": "./package/components/FlowStatusViewer.svelte", "default": "./package/components/FlowStatusViewer.svelte" }, - "./components/FlowBuilder.svelte": { - "types": "./package/components/FlowBuilder.svelte.d.ts", - "svelte": "./package/components/FlowBuilder.svelte", - "default": "./package/components/FlowBuilder.svelte" + "./components/FlowWrapper.svelte": { + "types": "./package/components/FlowWrapper.svelte.d.ts", + "svelte": "./package/components/FlowWrapper.svelte", + "default": "./package/components/FlowWrapper.svelte" }, - "./components/AppEditor.svelte": { - "types": "./package/components/apps/editor/AppEditor.svelte.d.ts", - "svelte": "./package/components/apps/editor/AppEditor.svelte", - "default": "./package/components/apps/editor/AppEditor.svelte" + "./components/AppWrapper.svelte": { + "types": "./package/components/AppWrapper.svelte.d.ts", + "svelte": "./package/components/AppWrapper.svelte", + "default": "./package/components/AppWrapper.svelte" }, - "./components/ScriptBuilder.svelte": { - "types": "./package/components/ScriptBuilder.svelte.d.ts", - "svelte": "./package/components/ScriptBuilder.svelte", - "default": "./package/components/ScriptBuilder.svelte" + "./components/ScriptWrapper.svelte": { + "types": "./package/components/ScriptWrapper.svelte.d.ts", + "svelte": "./package/components/ScriptWrapper.svelte", + "default": "./package/components/ScriptWrapper.svelte" }, "./components/FlowEditor.svelte": { "types": "./package/components/flows/FlowEditor.svelte.d.ts", @@ -285,10 +294,10 @@ "svelte": "./package/components/SchemaForm.svelte", "default": "./package/components/SchemaForm.svelte" }, - "./components/EditableSchemaWrapper.svelte": { - "types": "./package/components/schema/EditableSchemaWrapper.svelte.d.ts", - "svelte": "./package/components/schema/EditableSchemaWrapper.svelte", - "default": "./package/components/schema/EditableSchemaWrapper.svelte" + "./components/EditableSchemaSdkWrapper.svelte": { + "types": "./package/components/schema/EditableSchemaSdkWrapper.svelte.d.ts", + "svelte": "./package/components/schema/EditableSchemaSdkWrapper.svelte", + "default": "./package/components/schema/EditableSchemaSdkWrapper.svelte" }, "./components/ResourceEditor.svelte": { "types": "./package/components/ResourceEditor.svelte.d.ts", @@ -451,14 +460,14 @@ "components/FlowBuilder.svelte": [ "./package/components/FlowBuilder.svelte.d.ts" ], - "components/AppEditor.svelte": [ - "./package/components/apps/editor/AppEditor.svelte.d.ts" + "components/AppWrapper.svelte": [ + "./package/components/AppWrapper.svelte.d.ts" ], - "components/ScriptBuilder.svelte": [ - "./package/components/ScriptBuilder.svelte.d.ts" + "components/ScriptWrapper.svelte": [ + "./package/components/ScriptWrapper.svelte.d.ts" ], - "components/FlowEditor.svelte": [ - "./package/components/flows/FlowEditor.svelte.d.ts" + "components/FlowWrapper.svelte": [ + "./package/components/FlowWrapper.svelte.d.ts" ], "components/SchemaViewer.svelte": [ "./package/components/SchemaViewer.svelte.d.ts" @@ -466,8 +475,8 @@ "components/SchemaEditor.svelte": [ "./package/components/SchemaEditor.svelte.d.ts" ], - "components/EditableSchemaWrapper.svelte": [ - "./package/components/schema/EditableSchemaWrapper.svelte.d.ts" + "components/EditableSchemaSdkWrapper.svelte": [ + "./package/components/schema/EditableSchemaSdkWrapper.svelte.d.ts" ], "components/flows/FlowHistoryInner.svelte": [ "./package/components/flows/FlowHistoryInner.svelte.d.ts" @@ -519,6 +528,12 @@ ], "tailwindUtils": [ "./package/components/apps/editor/componentsPanel/tailwindUtils.d.ts" + ], + "components/scriptBuilder": [ + "./package/components/scriptBuilder.d.ts" + ], + "components/custom_ui": [ + "./package/components/custom_ui.d.ts" ] } }, @@ -526,4 +541,4 @@ "@rollup/rollup-linux-x64-gnu": "^4.35.0", "fsevents": "^2.3.3" } -} +} \ No newline at end of file diff --git a/frontend/scripts/untar_ui_builder.js b/frontend/scripts/untar_ui_builder.js index c64b1b1484..f0ebbd8c87 100644 --- a/frontend/scripts/untar_ui_builder.js +++ b/frontend/scripts/untar_ui_builder.js @@ -1,6 +1,23 @@ import path from 'path' import fs from 'fs' + +// Check if we're in node_modules (installed as dependency) +if (process.cwd().includes('node_modules')) { + console.log('Skipping postinstall - running as dependency'); + process.exit(0); +} + +// Check if we're in the root project +if (process.env.INIT_CWD && process.env.INIT_CWD !== process.cwd()) { + console.log('Skipping postinstall - not root project'); + process.exit(0); +} + +// Your actual postinstall logic here +console.log('Running postinstall for root project'); + + import { x } from 'tar' const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-d44b577.tar.gz' @@ -18,16 +35,6 @@ const response = await fetch(tarUrl) const buffer = await response.arrayBuffer() await fs.promises.writeFile(outputTarPath, Buffer.from(buffer)) -// Check if this script is being run from the package root -const isRootInstall = process.cwd() + '/scripts' === __dirname - -if (isRootInstall) { - console.log('Running postinstall: direct install') - // Your postinstall logic here -} else { - console.log('Skipping postinstall: installed as dependency') - process.exit(0) -} // Create extract directory if it doesn't exist try { diff --git a/frontend/src/lib/components/AppWrapper.svelte b/frontend/src/lib/components/AppWrapper.svelte new file mode 100644 index 0000000000..37e5bdfc2e --- /dev/null +++ b/frontend/src/lib/components/AppWrapper.svelte @@ -0,0 +1,10 @@ + + + diff --git a/frontend/src/lib/components/CliHelpBox.svelte b/frontend/src/lib/components/CliHelpBox.svelte index e05ba0f763..7c992d9154 100644 --- a/frontend/src/lib/components/CliHelpBox.svelte +++ b/frontend/src/lib/components/CliHelpBox.svelte @@ -1,9 +1,8 @@
diff --git a/frontend/src/lib/components/FirstStepInputs.svelte b/frontend/src/lib/components/FirstStepInputs.svelte index ef7856828b..bd71e6a4b3 100644 --- a/frontend/src/lib/components/FirstStepInputs.svelte +++ b/frontend/src/lib/components/FirstStepInputs.svelte @@ -1,6 +1,6 @@ + + + + diff --git a/frontend/src/lib/components/ResourceEditor.svelte b/frontend/src/lib/components/ResourceEditor.svelte index 7b71759eb8..9030c0ea2e 100644 --- a/frontend/src/lib/components/ResourceEditor.svelte +++ b/frontend/src/lib/components/ResourceEditor.svelte @@ -20,7 +20,6 @@ import autosize from '$lib/autosize' import GfmMarkdown from './GfmMarkdown.svelte' import TestTriggerConnection from './triggers/TestTriggerConnection.svelte' - import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' import GitHubAppIntegration from './GitHubAppIntegration.svelte' interface Props { @@ -29,7 +28,7 @@ path?: string newResource?: boolean hidePath?: boolean - watchChanges?: boolean + onChange?: (args: { path: string; args: Record; description: string }) => void defaultValues?: Record | undefined } @@ -39,7 +38,7 @@ path = $bindable(''), newResource = false, hidePath = false, - watchChanges = false, + onChange, defaultValues = undefined }: Props = $props() @@ -62,7 +61,6 @@ let viewJsonSchema = $state(false) const dispatch = createEventDispatcher() - const dispatchIfMounted = createDispatcherIfMounted(dispatch) let rawCode: string | undefined = $state(undefined) @@ -181,8 +179,8 @@ run(() => { canSave = can_write && isValid && jsonError == '' }) - run(() => { - watchChanges && dispatchIfMounted('change', { path, args, description }) + $effect(() => { + onChange && onChange({ path, args, description }) }) run(() => { rawCode && untrack(() => parseJson()) diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 480cb03e18..31ece0aab1 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -11,7 +11,8 @@ type TriggersCount, PostgresTriggerService, CaptureService, - type ScriptLang + type ScriptLang, + WorkerService } from '$lib/gen' import { inferArgs } from '$lib/infer' import { initialCode } from '$lib/script_helpers' @@ -21,6 +22,7 @@ enterpriseLicense, usedTriggerKinds, userStore, + workerTags, workspaceStore } from '$lib/stores' import { @@ -65,16 +67,15 @@ import ScriptSchema from './ScriptSchema.svelte' import Section from './Section.svelte' import Label from './Label.svelte' - import type DiffDrawer from './DiffDrawer.svelte' import type Editor from './Editor.svelte' import WorkerTagPicker from './WorkerTagPicker.svelte' import MetadataGen from './copilot/MetadataGen.svelte' import { writable } from 'svelte/store' import { defaultScriptLanguages, processLangs } from '$lib/scripts' import DefaultScripts from './DefaultScripts.svelte' - import { createEventDispatcher, onMount, setContext, untrack } from 'svelte' + import { onMount, setContext, untrack } from 'svelte' import Summary from './Summary.svelte' - import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui' + import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte' import TriggersEditor from './triggers/TriggersEditor.svelte' import type { ScheduleTrigger, TriggerContext } from './triggers' @@ -86,7 +87,6 @@ } from '$lib/script_helpers' import CaptureTable from './triggers/CaptureTable.svelte' import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes' - import type { ScriptBuilderFunctionExports } from './scriptBuilder' import DeployButton from './DeployButton.svelte' import { type NewScriptWithDraftAndDraftTriggers, @@ -97,29 +97,12 @@ } from './triggers/utils' import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte' import { Triggers } from './triggers/triggers.svelte' - - interface Props { - script: NewScript & { draft_triggers?: Trigger[] } - fullyLoaded?: boolean - initialPath?: string - template?: 'docker' | 'bunnative' | 'script' - initialArgs?: Record - lockedLanguage?: boolean - showMeta?: boolean - neverShowMeta?: boolean - diffDrawer?: DiffDrawer | undefined - savedScript?: NewScriptWithDraftAndDraftTriggers | undefined - searchParams?: URLSearchParams - disableHistoryChange?: boolean - replaceStateFn?: (url: string) => void - customUi?: ScriptBuilderWhitelabelCustomUi - savedPrimarySchedule?: ScheduleTrigger | undefined - functionExports?: ((exports: ScriptBuilderFunctionExports) => void) | undefined - children?: import('svelte').Snippet - } + import type { ScriptBuilderProps } from './script_builder' + import type { DiffDrawerI } from './diff_drawer' + import WorkerTagSelect from './WorkerTagSelect.svelte' let { - script = $bindable(), + script, fullyLoaded = true, initialPath = $bindable(''), template = $bindable('script'), @@ -135,8 +118,15 @@ customUi = {}, savedPrimarySchedule = undefined, functionExports = undefined, - children - }: Props = $props() + children, + onDeploy, + onDeployError, + onSaveInitial, + onSeeDetails, + onSaveDraftError, + onSaveDraft, + disableAi + }: ScriptBuilderProps = $props() export function getInitialAndModifiedValues(): SavedAndModifiedValue { return { @@ -153,7 +143,7 @@ 'u/' + ($userStore?.username?.includes('@') ? $userStore?.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '') - : $userStore?.username!) + + : $userStore?.username) + '/' + generateRandomString(12) @@ -210,8 +200,6 @@ loadTriggers() } - const dispatch = createEventDispatcher() - onMount(() => { if (functionExports) { console.log('functionExports set') @@ -573,10 +561,10 @@ script.parent_hash = newHash sendUserToast('Deployed') } else { - dispatch('deploy', newHash) + onDeploy?.({ path: script.path, hash: newHash }) } } catch (error) { - dispatch('deployError', error) + onDeployError?.({ path: script.path, error }) sendUserToast(`Error while saving the script: ${error.body || error.message}`, true) } loadingSave = false @@ -629,7 +617,7 @@ } catch (error) { sendUserToast(`Could not parse code, are you sure it is valid?`, true) } - + let newHash = '' if (initialPath == '' || savedScript?.draft_only) { if (savedScript?.draft_only) { await ScriptService.deleteScriptByPath({ @@ -649,7 +637,7 @@ runnableKind: 'script' }) } - await ScriptService.createScript({ + newHash = await ScriptService.createScript({ workspace: $workspaceStore!, requestBody: { path: script.path, @@ -709,9 +697,9 @@ if (initialPath == '' || (savedScript?.draft_only && script.path !== initialPath)) { savedAtNewPath = true initialPath = script.path - dispatch('saveInitial', script.path) + onSaveInitial?.({ path: script.path, hash: newHash }) } - dispatch('saveDraft', { path: script.path, savedAtNewPath, script }) + onSaveDraft?.({ path: script.path, savedAtNewPath, script }) sendUserToast('Saved as draft') } catch (error) { @@ -719,7 +707,7 @@ `Error while saving the script as a draft: ${error.body || error.message}`, true ) - dispatch('saveDraftError', error) + onSaveDraftError?.({ path: script.path, error }) } loadingDraft = false } @@ -727,7 +715,7 @@ function computeDropdownItems( initialPath: string, savedScript: NewScriptWithDraftAndDraftTriggers | undefined, - diffDrawer: DiffDrawer | undefined + diffDrawer: DiffDrawerI | undefined ) { let dropdownItems: { label: string; onClick: () => void }[] = initialPath != '' && customUi?.topBar?.extraDeployOptions != false @@ -777,7 +765,7 @@ { label: 'Exit & See details', onClick: () => { - dispatch('seeDetails', initialPath) + onSeeDetails?.({ path: initialPath }) } } ] @@ -945,14 +933,21 @@ $effect(() => { !disableHistoryChange && untrack(() => encodeScriptState(script)) }) + + loadWorkerTags() + async function loadWorkerTags() { + if (!$workerTags) { + $workerTags = await WorkerService.getCustomTags({ workspace: $workspaceStore }) + } + } {@render children?.()} - {#if script.schema} + {#if script.schema && !disableAi && !customUi?.settingsPanel?.metadata?.disableAiFilling}
- -
- {#snippet action()} - - {/snippet} - {#if lockedLanguage} -
- As a forked script, the language '{script.language}' cannot be modified. -
- {/if} -
- {#each langs as [label, lang] (lang)} - {@const isPicked = - (lang == script.language && template == 'script') || - (template == 'bunnative' && lang == 'bunnative') || - (template == 'docker' && lang == 'docker')} - - - {#snippet text()} - {label} is only available with an enterprise license - {/snippet} - - {/each} -
-
- + + {#snippet text()} + {label} is only available with an enterprise license + {/snippet} + + {/each} +
+ + {/if} {#if customUi?.settingsPanel?.metadata?.disableScriptKind !== true}
{#snippet header()} @@ -1172,6 +1167,14 @@
{/if} + {#if customUi?.settingsPanel?.disableRuntime} +
+ +
+ {/if}
@@ -1239,7 +1242,10 @@ group tag (queue). For instance, you could setup an "highmem", or "gpu" tag. {/snippet} - +
{#snippet header()} @@ -1693,6 +1699,18 @@ {/if}
+ {#if $workerTags} + {#if $workerTags?.length ?? 0 > 0} +
+ +
+ {/if} + {/if} {#if customUi?.topBar?.settings != false}