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" 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 8a6d959146..4bce2756da 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, @@ -4215,6 +4216,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 6d973d68dc..e0ff66357a 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -14402,6 +14402,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 fee9ef8546..be3c079544 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 b775a634ed..aade49bf64 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -958,6 +958,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/Cargo.toml b/backend/windmill-common/Cargo.toml index 0e4cc83591..980ec87d64 100644 --- a/backend/windmill-common/Cargo.toml +++ b/backend/windmill-common/Cargo.toml @@ -84,7 +84,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 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 a4c0363442..8b46a167a4 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -611,6 +611,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/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/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(() => { 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 28cf048daa..01a349e994 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,33 +97,12 @@ } from './triggers/utils' import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte' import { Triggers } from './triggers/triggers.svelte' - import type { AssetWithAccessType } from './assets/lib' - - interface Props { - script: NewScript & { - draft_triggers?: Trigger[] - fallback_access_types?: AssetWithAccessType[] - } - 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'), @@ -139,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 { @@ -157,7 +143,7 @@ 'u/' + ($userStore?.username?.includes('@') ? $userStore?.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '') - : $userStore?.username!) + + : $userStore?.username) + '/' + generateRandomString(12) @@ -214,8 +200,6 @@ loadTriggers() } - const dispatch = createEventDispatcher() - onMount(() => { if (functionExports) { console.log('functionExports set') @@ -578,10 +562,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 @@ -634,7 +618,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({ @@ -654,7 +638,7 @@ runnableKind: 'script' }) } - await ScriptService.createScript({ + newHash = await ScriptService.createScript({ workspace: $workspaceStore!, requestBody: { path: script.path, @@ -714,9 +698,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) { @@ -724,7 +708,7 @@ `Error while saving the script as a draft: ${error.body || error.message}`, true ) - dispatch('saveDraftError', error) + onSaveDraftError?.({ path: script.path, error }) } loadingDraft = false } @@ -732,7 +716,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 @@ -782,7 +766,7 @@ { label: 'Exit & See details', onClick: () => { - dispatch('seeDetails', initialPath) + onSeeDetails?.({ path: initialPath }) } } ] @@ -950,14 +934,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()} @@ -1177,6 +1168,14 @@
{/if} + {#if customUi?.settingsPanel?.disableRuntime} +
+ +
+ {/if}
@@ -1244,7 +1243,10 @@ group tag (queue). For instance, you could setup an "highmem", or "gpu" tag. {/snippet} - +
{#snippet header()} @@ -1698,6 +1700,18 @@ {/if}
+ {#if $workerTags} + {#if $workerTags?.length ?? 0 > 0} +
+ +
+ {/if} + {/if} {#if customUi?.topBar?.settings != false}
- - -
- { - 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/lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte b/frontend/src/lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte index d8e5f74bd1..ac4f9e7877 100644 --- a/frontend/src/lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte +++ b/frontend/src/lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte @@ -1,15 +1,26 @@ - import { twMerge } from 'tailwind-merge' - import AssistantMessage from './AssistantMessage.svelte' + import AIChatMessage from './AIChatMessage.svelte' import { type Snippet } from 'svelte' - import { - CheckIcon, - HistoryIcon, - Loader2, - Plus, - RefreshCwIcon, - StopCircleIcon, - Undo2Icon, - X, - XIcon - } from 'lucide-svelte' - import autosize from '$lib/autosize' + import { CheckIcon, HistoryIcon, Loader2, Plus, StopCircleIcon, X, XIcon } from 'lucide-svelte' import Button from '$lib/components/common/button/Button.svelte' import Popover from '$lib/components/meltComponents/Popover.svelte' import { type DisplayMessage } from './shared' import type { ContextElement } from './context' - import ContextElementBadge from './ContextElementBadge.svelte' - import ContextTextarea from './ContextTextarea.svelte' - import AvailableContextList from './AvailableContextList.svelte' import ChatQuickActions from './ChatQuickActions.svelte' import ProviderModelSelector from './ProviderModelSelector.svelte' import ChatMode from './ChatMode.svelte' import Markdown from 'svelte-exmarkdown' import { aiChatManager, AIMode } from './AIChatManager.svelte' + import AIChatInput from './AIChatInput.svelte' let { messages, @@ -63,16 +49,8 @@ suggestions?: string[] } = $props() - let contextTextareaComponent: ContextTextarea | undefined = $state() - let instructionsTextarea: HTMLTextAreaElement | undefined = $state() - - export function focusInput() { - if (aiChatManager.mode === 'script') { - contextTextareaComponent?.focus() - } else { - instructionsTextarea?.focus() - } - } + let aiChatInput: AIChatInput | undefined = $state() + let editingMessageIndex = $state(null) let scrollEl: HTMLDivElement | undefined = $state() async function scrollDown() { @@ -87,38 +65,12 @@ aiChatManager.automaticScroll && height && scrollDown() }) - 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 submitSuggestion(suggestion: string) { - aiChatManager.instructions = suggestion - aiChatManager.sendRequest() + aiChatManager.sendRequest({ instructions: suggestion }) } - function isLastUserMessage(messageIndex: number): boolean { - // Find the last user message index - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].role === 'user') { - return i === messageIndex - } - } - return false - } - - function restartGeneration(messageIndex: number) { - aiChatManager.restartLastGeneration(messageIndex) + export function focusInput() { + aiChatInput?.focusInput() } @@ -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/AiChatLayout.svelte b/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte new file mode 100644 index 0000000000..f407481e30 --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte @@ -0,0 +1,98 @@ + + +{#if !disableAi} + + +
+
+
+
+ +
+
+ {@render children?.()} +
+
+
+
+
+ + + +
+{:else} + {@render children?.()} +{/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/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) { diff --git a/frontend/src/lib/components/copilot/chat/navigator/core.ts b/frontend/src/lib/components/copilot/chat/navigator/core.ts index 7e605dd8e2..aecf60bc30 100644 --- a/frontend/src/lib/components/copilot/chat/navigator/core.ts +++ b/frontend/src/lib/components/copilot/chat/navigator/core.ts @@ -1,4 +1,3 @@ -import { page } from '$app/state' import type { ChatCompletionSystemMessageParam, ChatCompletionTool, @@ -189,7 +188,7 @@ function getTriggerableComponents(): string { // Function to get the current page name function getCurrentPageName(): string { try { - const currentPage = page.url.pathname + const currentPage = window.location.pathname switch (currentPage) { case '/': return 'Home Page' 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, diff --git a/frontend/src/lib/components/custom_ui.ts b/frontend/src/lib/components/custom_ui.ts index 0c736d0b94..e03bed0d2e 100644 --- a/frontend/src/lib/components/custom_ui.ts +++ b/frontend/src/lib/components/custom_ui.ts @@ -35,6 +35,8 @@ export type FlowBuilderWhitelabelCustomUi = { tagEdit?: boolean editorBar?: EditorBarUi downloadLogs?: boolean + tagSelectPlaceholder?: string + tagSelectNoLabel?: boolean } export type DisplayResultUi = { @@ -79,6 +81,7 @@ export type SettingsPanelMetadataUi = { disableScriptKind?: boolean editableSchemaForm?: EditableSchemaFormUi disableMute?: boolean + disableAiFilling?: boolean } export type SettingsPanelUi = { @@ -106,7 +109,7 @@ export type ScriptBuilderWhitelabelCustomUi = { } settingsPanel?: SettingsPanelUi disableTooltips?: boolean - editorBar?: EditorBarUi previewPanel?: PreviewPanelUi + tagSelectPlaceholder?: string } diff --git a/frontend/src/lib/components/diff_drawer.ts b/frontend/src/lib/components/diff_drawer.ts new file mode 100644 index 0000000000..108eddf8fc --- /dev/null +++ b/frontend/src/lib/components/diff_drawer.ts @@ -0,0 +1,24 @@ +import type { Value } from "$lib/utils" + +export type DiffDrawerDiff = +| { + mode: 'normal' + deployed: Value + draft: Value | undefined + current: Value + defaultDiffType?: 'deployed' | 'draft' + button?: { text: string; onClick: () => void } +} +| { + mode: 'simple' + original: Value + current: Value + title: string + button?: { text: string; onClick: () => void } +} + +export interface DiffDrawerI { + openDrawer: () => void + closeDrawer: () => void + setDiff: (diff: DiffDrawerDiff) => void +} \ No newline at end of file diff --git a/frontend/src/lib/components/flow_builder.ts b/frontend/src/lib/components/flow_builder.ts new file mode 100644 index 0000000000..c133ef18b4 --- /dev/null +++ b/frontend/src/lib/components/flow_builder.ts @@ -0,0 +1,52 @@ +import type { OpenFlow } from '$lib/gen' +import type { StateStore } from '$lib/utils' +import type { Writable } from 'svelte/store' +import type { FlowState } from './flows/flowState' +import type { FlowWithDraftAndDraftTriggers, Trigger } from './triggers/utils' +import type { DiffDrawerI } from './diff_drawer' +import type { FlowBuilderWhitelabelCustomUi } from './custom_ui' +import type { ScheduleTrigger } from './triggers' +import type { stepState } from './stepHistoryLoader.svelte' + +export type FlowBuilderProps = { + initialPath?: string + pathStoreInit?: string | undefined + newFlow: boolean + selectedId: string | undefined + initialArgs?: Record + loading?: boolean + flowStore: StateStore + flowStateStore: Writable + savedFlow?: FlowWithDraftAndDraftTriggers | undefined + diffDrawer?: DiffDrawerI | undefined + customUi?: FlowBuilderWhitelabelCustomUi + disableAi?: boolean + disabledFlowInputs?: boolean + savedPrimarySchedule?: ScheduleTrigger | undefined // used to set the primary schedule in the legacy primaryScheduleStore + version?: number | undefined + setSavedraftCb?: ((cb: () => void) => void) | undefined + draftTriggersFromUrl?: Trigger[] | undefined + selectedTriggerIndexFromUrl?: number | undefined + children?: import('svelte').Snippet + loadedFromHistoryFromUrl?: { + flowJobInitial: boolean | undefined + stepsState: Record + } + noInitial?: boolean + onSaveInitial?: ({ path, id }: { path: string; id: string }) => void + onSaveDraft?: ({ + path, + savedAtNewPath, + newFlow + }: { + path: string + savedAtNewPath: boolean + newFlow: boolean + }) => void + onSaveDraftError?: ({ error }: { error: any }) => void + onSaveDraftOnlyAtNewPath?: ({ path, selectedId }: { path: string; selectedId: string }) => void + onDeploy?: ({ path }: { path: string }) => void + onDeployError?: ({ error }: { error: any }) => void + onDetails?: ({ path }: { path: string }) => void + onHistoryRestore?: () => void +} diff --git a/frontend/src/lib/components/flows/CreateActionsFlow.svelte b/frontend/src/lib/components/flows/CreateActionsFlow.svelte index 1671a84e5b..1a424641ad 100644 --- a/frontend/src/lib/components/flows/CreateActionsFlow.svelte +++ b/frontend/src/lib/components/flows/CreateActionsFlow.svelte @@ -6,7 +6,7 @@ import { Button } from '$lib/components/common' import Drawer from '$lib/components/common/drawer/Drawer.svelte' import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte' - import { importFlowStore } from '$lib/components/flows/flowStore.svelte' + import { importFlowStore } from '$lib/components/flows/flowStore' import { Loader2, Plus } from 'lucide-svelte' import YAML from 'yaml' diff --git a/frontend/src/lib/components/flows/FlowHistory.svelte b/frontend/src/lib/components/flows/FlowHistory.svelte index 5349de4333..32b1a2af08 100644 --- a/frontend/src/lib/components/flows/FlowHistory.svelte +++ b/frontend/src/lib/components/flows/FlowHistory.svelte @@ -1,18 +1,20 @@ @@ -25,9 +27,9 @@ > { - drawer.closeDrawer() - dispatch('historyRestore') + onHistoryRestore={() => { + drawer?.closeDrawer() + onHistoryRestore?.() }} {path} /> diff --git a/frontend/src/lib/components/flows/FlowHistoryInner.svelte b/frontend/src/lib/components/flows/FlowHistoryInner.svelte index 296971c0f1..9e116e5244 100644 --- a/frontend/src/lib/components/flows/FlowHistoryInner.svelte +++ b/frontend/src/lib/components/flows/FlowHistoryInner.svelte @@ -1,4 +1,7 @@ @@ -85,7 +92,7 @@ {#if versions.length > 0}
{#each versions ?? [] as version} - +
{ + onclick={() => { selectedVersion = version }} > @@ -124,10 +131,11 @@ type="text" bind:value={deploymentMsgUpdate} class="!w-auto grow" - on:click|stopPropagation={() => {}} - on:keydown|stopPropagation - on:keypress|stopPropagation={({ key }) => { - if (key === 'Enter') updateDeploymentMsg(selectedVersion?.id) + onclick={stopPropagation(() => {})} + onkeydown={stopPropagation(bubble('keydown'))} + onkeypress={(e) => { + e.stopPropagation() + if (e.key === 'Enter') updateDeploymentMsg(selectedVersion?.id) }} />
diff --git a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte index 566b2cddbf..463933e618 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte @@ -73,7 +73,7 @@ {#if $selectedId?.startsWith('settings')} - + {:else if $selectedId === 'Input'} { + $effect.pre(() => { $selectedId && untrack(() => onSelectedIdChange()) }) $effect(() => { @@ -311,6 +312,10 @@ ;[flowModule.value.content, flowModule.value.language] untrack(() => assets.refresh()) }) + + let rawScriptLang = $derived( + flowModule.value.type == 'rawscript' ? flowModule.value.language : undefined + ) @@ -333,7 +338,7 @@ > {#snippet header()} { console.log('tagChange', e.detail) @@ -491,7 +496,8 @@
{#key forceReload} dispatch('tagChange', e.detail)} @@ -192,6 +194,9 @@
{#if module.value.type === 'rawscript'} dispatch('tagChange', e.detail)} /> diff --git a/frontend/src/lib/components/flows/content/FlowModuleScript.svelte b/frontend/src/lib/components/flows/content/FlowModuleScript.svelte index fcfdd4d72b..31d2e536e9 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleScript.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleScript.svelte @@ -1,25 +1,92 @@ + +
@@ -72,7 +135,7 @@
script not found at {path} in workspace {$workspaceStore}
{:else if showAllCode} {#if showDiff} - {#key previousCode + code} + {#key (previousCode ?? '') + (code ?? '')} {#await import('$lib/components/DiffEditor.svelte')} {:then Module} @@ -94,7 +157,7 @@
- + {/if} {#if lock} diff --git a/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte b/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte index 489837a2cb..21b689206c 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleWorkerTagSelect.svelte @@ -5,9 +5,16 @@ import { WorkerService } from '$lib/gen' import WorkerTagSelect from '$lib/components/WorkerTagSelect.svelte' - let { tag = $bindable(), nullTag = $bindable() }: { + let { + tag = $bindable(), + nullTag, + placeholder, + noLabel + }: { tag: string | undefined nullTag?: string | undefined + placeholder?: string + noLabel?: boolean } = $props() const { flowStore, selectedId } = getContext('FlowEditorContext') @@ -26,7 +33,13 @@ {#if $workerTags?.length > 0}
{#if flowStore.val.tag == undefined} - dispatch('change', e.detail)} /> + dispatch('change', e.detail)} + /> {:else}
{: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} diff --git a/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte index 38f233a0f8..028309588f 100644 --- a/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte @@ -6,7 +6,7 @@ import FlowBuilder from '$lib/components/FlowBuilder.svelte' import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte' import type { FlowState } from '$lib/components/flows/flowState' - import { importFlowStore, initFlow } from '$lib/components/flows/flowStore.svelte' + import { importFlowStore, initFlow } from '$lib/components/flows/flowStore' import { FlowService, type Flow } from '$lib/gen' import { initialArgsStore, userStore, workspaceStore } from '$lib/stores' import { sendUserToast } from '$lib/toast' @@ -163,14 +163,14 @@ { - goto(`/flows/edit/${e.detail}?selected=${flowBuilder?.getSelectedId?.()}`) + onSaveInitial={(e) => { + goto(`/flows/edit/${e.path}?selected=${e.id}`) }} - on:deploy={(e) => { - goto(`/flows/get/${e.detail}?workspace=${$workspaceStore}`) + onDeploy={(e) => { + goto(`/flows/get/${e.path}?workspace=${$workspaceStore}`) }} - on:details={(e) => { - goto(`/flows/get/${e.detail}?workspace=${$workspaceStore}`) + onDetails={(e) => { + goto(`/flows/get/${e.path}?workspace=${$workspaceStore}`) }} {initialPath} {pathStoreInit} diff --git a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte index 65e707d4f6..80cb8f3002 100644 --- a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte @@ -11,7 +11,7 @@ orderedJsonStringify, type StateStore } from '$lib/utils' - import { initFlow } from '$lib/components/flows/flowStore.svelte' + import { initFlow } from '$lib/components/flows/flowStore' import { goto } from '$lib/navigation' import { afterNavigate, replaceState } from '$app/navigation' import { writable } from 'svelte/store' @@ -79,7 +79,7 @@ | undefined = $state(undefined) let flowBuilder: FlowBuilder | undefined = $state(undefined) - + let notFound = $state(false) async function loadFlow(): Promise { console.log('loadFlow') loading = true @@ -93,7 +93,13 @@ workspace: $workspaceStore!, path: statePath }) - ).id + )?.id + + if (version == undefined) { + notFound = true + sendUserToast(`Flow not found at path ${statePath}`, true) + return + } savedFlow = await FlowService.getFlowByPathWithDraft({ workspace: $workspaceStore!, @@ -262,38 +268,44 @@ - { - goto(`/flows/get/${e.detail}?workspace=${$workspaceStore}`) - }} - on:details={(e) => { - goto(`/flows/get/${e.detail}?workspace=${$workspaceStore}`) - }} - on:saveDraftOnlyAtNewPath={(e) => { - const { path, selectedId } = e.detail - goto(`/flows/edit/${path}?selected=${selectedId}`) - }} - on:historyRestore={() => { - loadFlow() - }} - {flowStore} - {flowStateStore} - initialPath={$page.params.path} - newFlow={false} - {selectedId} - {initialArgs} - {loading} - bind:this={flowBuilder} - bind:savedFlow - {diffDrawer} - {savedPrimarySchedule} - {draftTriggersFromUrl} - {selectedTriggerIndexFromUrl} - {version} - {loadedFromHistoryFromUrl} -> - +

Flow not found at path {$page.params.path}

+

The flow you are looking for does not exist.

+
+{:else} + { + goto(`/flows/get/${e.path}?workspace=${$workspaceStore}`) + }} + onDetails={(e) => { + goto(`/flows/get/${e.path}?workspace=${$workspaceStore}`) + }} + onSaveDraftOnlyAtNewPath={(e) => { + goto(`/flows/edit/${e.path}?selected=${e.selectedId}`) + }} + onHistoryRestore={() => { + loadFlow() + }} + {flowStore} + {flowStateStore} + initialPath={$page.params.path} + newFlow={false} + {selectedId} + {initialArgs} + {loading} + bind:this={flowBuilder} + bind:savedFlow {diffDrawer} - getInitialAndModifiedValues={flowBuilder?.getInitialAndModifiedValues} - /> - + {savedPrimarySchedule} + {draftTriggersFromUrl} + {selectedTriggerIndexFromUrl} + {version} + {loadedFromHistoryFromUrl} + > + + +{/if} diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index d055902626..df33996942 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -417,7 +417,7 @@ }} /> {#if flow} - + {/if} {#if flow} diff --git a/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte index efe9a8d159..f23b2edfa4 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte @@ -104,13 +104,11 @@ {initialArgs} bind:this={scriptBuilder} lockedLanguage={templatePath != null || hubPath != null} - on:deploy={(e) => { - let newHash = e.detail - goto(`/scripts/get/${newHash}?workspace=${$workspaceStore}`) + onDeploy={(e) => { + goto(`/scripts/get/${e.hash}?workspace=${$workspaceStore}`) }} - on:saveInitial={(e) => { - let path = e.detail - goto(`/scripts/edit/${path}`) + onSaveInitial={(e) => { + goto(`/scripts/edit/${e.path}`) }} searchParams={$page.url.searchParams} {script} diff --git a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte index 3ee4fb7472..f5ad694ece 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte @@ -213,17 +213,14 @@ {diffDrawer} {savedPrimarySchedule} searchParams={$page.url.searchParams} - on:deploy={(e) => { - let newHash = e.detail - goto(`/scripts/get/${newHash}?workspace=${$workspaceStore}`) + onDeploy={(e) => { + goto(`/scripts/get/${e.hash}?workspace=${$workspaceStore}`) }} - on:saveInitial={(e) => { - let path = e.detail - goto(`/scripts/edit/${path}`) + onSaveInitial={(e) => { + goto(`/scripts/edit/${e.path}`) }} - on:seeDetails={(e) => { - let path = e.detail - goto(`/scripts/get/${path}?workspace=${$workspaceStore}`) + onSeeDetails={(e) => { + goto(`/scripts/get/${e.path}?workspace=${$workspaceStore}`) }} replaceStateFn={(path) => { replaceState(path, $page.state) diff --git a/frontend/src/routes/test_dev_page_2/+page.svelte b/frontend/src/routes/test_dev/public_app_imports/+page.svelte similarity index 100% rename from frontend/src/routes/test_dev_page_2/+page.svelte rename to frontend/src/routes/test_dev/public_app_imports/+page.svelte diff --git a/frontend/src/routes/test_dev/sdk_flow/+page.svelte b/frontend/src/routes/test_dev/sdk_flow/+page.svelte new file mode 100644 index 0000000000..f1ff418cc9 --- /dev/null +++ b/frontend/src/routes/test_dev/sdk_flow/+page.svelte @@ -0,0 +1,51 @@ + + + + + diff --git a/frontend/src/routes/test_dev/sdk_script/+page.svelte b/frontend/src/routes/test_dev/sdk_script/+page.svelte new file mode 100644 index 0000000000..0d82c59bcf --- /dev/null +++ b/frontend/src/routes/test_dev/sdk_script/+page.svelte @@ -0,0 +1,41 @@ + + + diff --git a/frontend/src/routes/test_dev_page/+page.svelte b/frontend/src/routes/test_dev_page/+page.svelte deleted file mode 100644 index 1c9b602ae9..0000000000 --- a/frontend/src/routes/test_dev_page/+page.svelte +++ /dev/null @@ -1,105 +0,0 @@ - - - - -