mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 08:00:59 +00:00
Merge branch 'main' into di/assets
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -1 +1 @@
|
||||
b7c6fc065a3da98933d50fae697784da61c3fe2d
|
||||
21aabec96e91c8075dd637d1e32af90e495082fc
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Remove email and span columns from audit table
|
||||
ALTER TABLE audit DROP COLUMN email;
|
||||
ALTER TABLE audit DROP COLUMN span;
|
||||
@@ -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);
|
||||
@@ -1926,6 +1926,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
&job.email,
|
||||
&job.id,
|
||||
None,
|
||||
Some(format!("handle_zombie_jobs")),
|
||||
)
|
||||
.await
|
||||
.expect("could not create job token");
|
||||
|
||||
@@ -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<Postgres>) {
|
||||
"",
|
||||
&Uuid::nil(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -14402,6 +14402,8 @@ components:
|
||||
type: string
|
||||
parameters:
|
||||
type: object
|
||||
span:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- timestamp
|
||||
|
||||
@@ -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<ApiAuthed>,
|
||||
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(),
|
||||
|
||||
@@ -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<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -836,6 +836,7 @@ pub struct ApiAuthed {
|
||||
pub folders: Vec<(String, bool, bool)>,
|
||||
pub scopes: Option<Vec<String>>,
|
||||
pub username_override: Option<String>,
|
||||
pub token_prefix: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ApiAuthed> for Authed {
|
||||
@@ -848,6 +849,7 @@ impl From<ApiAuthed> 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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<DB>,
|
||||
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
|
||||
Query(JsonPath { json_path, .. }): Query<JsonPath>,
|
||||
@@ -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<DB>) -> windmill_common::error::J
|
||||
|
||||
async fn cancel_job_api(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
opt_tokened: OptTokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
Json(CancelJob { reason }): Json<CancelJob>,
|
||||
@@ -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<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
Json(CancelJob { reason }): Json<CancelJob>,
|
||||
@@ -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<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::Result<Response> {
|
||||
@@ -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<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
Query(GetJobQuery { no_logs }): Query<GetJobQuery>,
|
||||
@@ -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<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::Result<Response> {
|
||||
@@ -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<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> JsonResult<Box<RawValue>> {
|
||||
@@ -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<ApiAuthed>,
|
||||
opt_tokened: OptTokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
|
||||
Query(approver): Query<QueryApprover>,
|
||||
QueryOrBody(value): QueryOrBody<serde_json::Value>,
|
||||
) -> error::Result<StatusCode> {
|
||||
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<ApiAuthed>,
|
||||
opt_tokened: OptTokened,
|
||||
approved: bool,
|
||||
) -> Result<StatusCode, Error> {
|
||||
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<ApiAuthed>,
|
||||
opt_tokened: OptTokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
|
||||
Query(approver): Query<QueryApprover>,
|
||||
QueryOrBody(value): QueryOrBody<serde_json::Value>,
|
||||
) -> error::Result<StatusCode> {
|
||||
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<ApiAuthed>,
|
||||
opt_tokened: OptTokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
|
||||
Query(approver): Query<QueryApprover>,
|
||||
@@ -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<DB>,
|
||||
Path((w_id, job_id)): Path<(String, Uuid)>,
|
||||
Query(JobUpdateQuery { log_offset, get_progress, running }): Query<JobUpdateQuery>,
|
||||
@@ -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<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::Result<Response> {
|
||||
@@ -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<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
Query(JsonPath { json_path, suspended_job, approver, resume_id, secret }): Query<JsonPath>,
|
||||
@@ -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<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
Query(GetCompletedJobQuery { get_started }): Query<GetCompletedJobQuery>,
|
||||
@@ -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<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -958,6 +958,7 @@ async fn create_script_internal<'c>(
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
permissioned_as,
|
||||
authed.token_prefix.as_deref(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -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<ApiAuthed>,
|
||||
opt_tokened: OptTokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Form(form_data): Form<SlackFormData>,
|
||||
) -> Result<StatusCode, Error> {
|
||||
@@ -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<ApiAuthed>,
|
||||
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?;
|
||||
|
||||
@@ -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<DB>, authed: ApiAuthed) -> Result<String> {
|
||||
async fn leave_instance(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
) -> Result<String> {
|
||||
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<String> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -20,14 +20,6 @@ use {
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub struct AuditAuthor {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub username_override: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub token_prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct AuditLog {
|
||||
pub action_kind: ActionKind,
|
||||
pub resource: Option<String>,
|
||||
pub parameters: Option<serde_json::Value>,
|
||||
pub span: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,8 @@ pub struct IdToken {
|
||||
expiration: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub const TOKEN_PREFIX_LEN: usize = 10;
|
||||
|
||||
pub fn has_expired(expiration_time: DateTime<Utc>, take: Option<Duration>) -> bool {
|
||||
let now = Utc::now();
|
||||
|
||||
@@ -66,6 +68,7 @@ pub struct JWTAuthClaims {
|
||||
pub exp: usize,
|
||||
pub job_id: Option<String>,
|
||||
pub scopes: Option<Vec<String>>,
|
||||
pub audit_span: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -92,6 +95,7 @@ impl From<JobPerms> 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<JobPerms>,
|
||||
audit_span: Option<String>,
|
||||
) -> crate::error::Result<String> {
|
||||
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)
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct Authed {
|
||||
// (folder name, can write, is owner)
|
||||
pub folders: Vec<(String, bool, bool)>,
|
||||
pub scopes: Option<Vec<String>>,
|
||||
pub token_prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -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<T: Serialize + Send + Sync + ValidableJson>(
|
||||
&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<JobPerms>)
|
||||
&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<chrono::DateTime<chrono::Utc>>,
|
||||
schedule_path: Option<String>,
|
||||
parent_job: Option<Uuid>,
|
||||
@@ -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
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+3
-3
@@ -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": {
|
||||
|
||||
+42
-27
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import AppEditor from './apps/editor/AppEditor.svelte'
|
||||
import type { AppEditorProps } from './apps/types'
|
||||
|
||||
let { app: oldApp, ...props }: AppEditorProps = $props()
|
||||
|
||||
let app = $state(oldApp)
|
||||
</script>
|
||||
|
||||
<AppEditor {app} {...props} />
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import ClipboardPanel from './details/ClipboardPanel.svelte'
|
||||
|
||||
$: url = `${$page.url.protocol}//${$page.url.hostname}/`
|
||||
let url = $derived(`${window.location.protocol}//${window.location.hostname}/`)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, getContext, untrack } from 'svelte'
|
||||
import { getFirstStepSchema } from '$lib/components/flows/flowStore.svelte'
|
||||
import { getFirstStepSchema } from '$lib/components/flows/flowStore'
|
||||
import type { FlowEditorContext } from '$lib/components/flows/types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Alert } from '$lib/components/common'
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import {
|
||||
FlowService,
|
||||
type Flow,
|
||||
@@ -26,7 +24,6 @@
|
||||
orderedJsonStringify,
|
||||
readFieldsRecursively,
|
||||
replaceFalseWithUndefined,
|
||||
type StateStore,
|
||||
type Value
|
||||
} from '$lib/utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -35,12 +32,11 @@
|
||||
import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte'
|
||||
|
||||
import { onMount, setContext, untrack, type ComponentType } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import { Badge, Button, UndoRedo } from './common'
|
||||
import FlowEditor from './flows/FlowEditor.svelte'
|
||||
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
|
||||
import type { FlowState } from './flows/flowState'
|
||||
import { dfs as dfsApply } from './flows/dfs'
|
||||
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
@@ -56,7 +52,6 @@
|
||||
type Icon,
|
||||
Settings
|
||||
} from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Awareness from './Awareness.svelte'
|
||||
import { getAllModules } from './flows/flowExplorer'
|
||||
import { type FlowCopilotContext } from './copilot/flow'
|
||||
@@ -65,7 +60,6 @@
|
||||
import Dropdown from '$lib/components/DropdownV2.svelte'
|
||||
import FlowTutorials from './FlowTutorials.svelte'
|
||||
import { ignoredTutorials } from './tutorials/ignoredTutorials'
|
||||
import type DiffDrawer from './DiffDrawer.svelte'
|
||||
import FlowHistory from './flows/FlowHistory.svelte'
|
||||
import Summary from './Summary.svelte'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
@@ -88,43 +82,7 @@
|
||||
StepHistoryLoader,
|
||||
type stepState
|
||||
} from './stepHistoryLoader.svelte'
|
||||
|
||||
interface Props {
|
||||
initialPath?: string
|
||||
pathStoreInit?: string | undefined
|
||||
newFlow: boolean
|
||||
selectedId: string | undefined
|
||||
initialArgs?: Record<string, any>
|
||||
loading?: boolean
|
||||
flowStore: StateStore<OpenFlow>
|
||||
flowStateStore: Writable<FlowState>
|
||||
savedFlow?: FlowWithDraftAndDraftTriggers | undefined
|
||||
diffDrawer?: DiffDrawer | 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<string, stepState>
|
||||
}
|
||||
noInitial?: boolean
|
||||
onSaveInitial?: ({ path, id }: { path: string; id: string }) => void
|
||||
onSaveDraft?: ({
|
||||
path,
|
||||
savedAtNewPath,
|
||||
newFlow
|
||||
}: {
|
||||
path: string
|
||||
savedAtNewPath: boolean
|
||||
newFlow: boolean
|
||||
}) => void
|
||||
}
|
||||
import type { FlowBuilderProps } from './flow_builder'
|
||||
|
||||
let {
|
||||
initialPath = $bindable(''),
|
||||
@@ -147,8 +105,16 @@
|
||||
selectedTriggerIndexFromUrl = undefined,
|
||||
children,
|
||||
loadedFromHistoryFromUrl,
|
||||
noInitial = false
|
||||
}: Props = $props()
|
||||
noInitial = false,
|
||||
onSaveInitial,
|
||||
onSaveDraft,
|
||||
onDeploy,
|
||||
onDeployError,
|
||||
onDetails,
|
||||
onSaveDraftError,
|
||||
onSaveDraftOnlyAtNewPath,
|
||||
onHistoryRestore
|
||||
}: FlowBuilderProps = $props()
|
||||
|
||||
let initialPathStore = writable(initialPath)
|
||||
|
||||
@@ -157,7 +123,7 @@
|
||||
'u/' +
|
||||
($userStore?.username?.includes('@')
|
||||
? $userStore!.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '')
|
||||
: $userStore!.username!) +
|
||||
: $userStore?.username) +
|
||||
'/' +
|
||||
generateRandomString(12)
|
||||
|
||||
@@ -229,8 +195,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule) // kept for legacy reasons
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
const simplifiedPoll = writable(false)
|
||||
@@ -361,18 +325,18 @@
|
||||
|
||||
let savedAtNewPath = false
|
||||
if (newFlow) {
|
||||
dispatch('saveInitial', $pathStore)
|
||||
onSaveInitial?.({ path: $pathStore, id: getSelectedId() })
|
||||
} else if (savedFlow?.draft_only && $pathStore !== initialPath) {
|
||||
savedAtNewPath = true
|
||||
initialPath = $pathStore
|
||||
onSaveDraftOnlyAtNewPath?.({ path: $pathStore, selectedId: getSelectedId() })
|
||||
// this is so we can use the flow builder outside of sveltekit
|
||||
dispatch('saveDraftOnlyAtNewPath', { path: $pathStore, selectedId: getSelectedId() })
|
||||
}
|
||||
dispatch('saveDraft', { path: $pathStore, savedAtNewPath, newFlow })
|
||||
onSaveDraft?.({ path: $pathStore, savedAtNewPath, newFlow })
|
||||
sendUserToast('Saved as draft')
|
||||
} catch (error) {
|
||||
sendUserToast(`Error while saving the flow as a draft: ${error.body || error.message}`, true)
|
||||
dispatch('saveDraftError', error)
|
||||
onSaveDraftError?.({ error })
|
||||
}
|
||||
loadingDraft = false
|
||||
}
|
||||
@@ -542,9 +506,10 @@
|
||||
} as Flow
|
||||
setDraftTriggers([])
|
||||
loadingSave = false
|
||||
dispatch('deploy', $pathStore)
|
||||
onDeploy?.({ path: $pathStore })
|
||||
} catch (err) {
|
||||
dispatch('deployError', err)
|
||||
onDeployError?.({ error: err })
|
||||
// this is so we can use the flow builder outside of sveltekit
|
||||
sendUserToast(`The flow could not be saved: ${err.body ?? err}`, true)
|
||||
loadingSave = false
|
||||
}
|
||||
@@ -738,7 +703,7 @@
|
||||
if (savedFlow?.draft_only === false || savedFlow?.draft_only === undefined) {
|
||||
dropdownItems.push({
|
||||
label: 'Exit & see details',
|
||||
onClick: () => dispatch('details', $pathStore)
|
||||
onClick: () => onDetails?.({ path: $pathStore })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -853,28 +818,28 @@
|
||||
let forceTestTab: Record<string, boolean> = $state({})
|
||||
let highlightArg: Record<string, string | undefined> = $state({})
|
||||
|
||||
run(() => {
|
||||
$effect.pre(() => {
|
||||
initialPathStore.set(initialPath)
|
||||
})
|
||||
run(() => {
|
||||
$effect.pre(() => {
|
||||
setContext('customUi', customUi)
|
||||
})
|
||||
run(() => {
|
||||
$effect.pre(() => {
|
||||
if (flowStore.val || $selectedIdStore) {
|
||||
readFieldsRecursively(flowStore.val)
|
||||
untrack(() => saveSessionDraft())
|
||||
}
|
||||
})
|
||||
run(() => {
|
||||
$effect.pre(() => {
|
||||
initialPath && ($pathStore = initialPath)
|
||||
})
|
||||
run(() => {
|
||||
$effect.pre(() => {
|
||||
selectedId && untrack(() => select(selectedId))
|
||||
})
|
||||
run(() => {
|
||||
$effect.pre(() => {
|
||||
initialPath && initialPath != '' && $workspaceStore && untrack(() => loadTriggers())
|
||||
})
|
||||
run(() => {
|
||||
$effect.pre(() => {
|
||||
const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasDiff() ?? false
|
||||
customUi && untrack(() => onCustomUiChange(customUi, hasAiDiff))
|
||||
})
|
||||
@@ -919,8 +884,8 @@
|
||||
{@render children?.()}
|
||||
|
||||
<DeployOverrideConfirmationModal
|
||||
bind:deployedBy
|
||||
bind:confirmCallback
|
||||
{deployedBy}
|
||||
{confirmCallback}
|
||||
bind:open
|
||||
{diffDrawer}
|
||||
bind:deployedValue
|
||||
@@ -942,7 +907,7 @@
|
||||
{#key renderCount}
|
||||
{#if !$userStore?.operator}
|
||||
{#if $pathStore}
|
||||
<FlowHistory bind:this={flowHistory} path={$pathStore} on:historyRestore />
|
||||
<FlowHistory bind:this={flowHistory} path={$pathStore} {onHistoryRestore} />
|
||||
{/if}
|
||||
<FlowYamlEditor bind:drawer={yamlEditorDrawer} />
|
||||
<FlowImportExportMenu bind:drawer={jsonViewerDrawer} />
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import AiChatLayout from './copilot/chat/AiChatLayout.svelte'
|
||||
import type { FlowBuilderProps } from './flow_builder'
|
||||
import FlowBuilder from './FlowBuilder.svelte'
|
||||
|
||||
let { flowStore: oldFlowStore, disableAi, ...props }: FlowBuilderProps = $props()
|
||||
|
||||
let flowStore = $state(oldFlowStore)
|
||||
</script>
|
||||
|
||||
<AiChatLayout noPadding={true} {disableAi}>
|
||||
<FlowBuilder {flowStore} {disableAi} {...props} />
|
||||
</AiChatLayout>
|
||||
@@ -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<string, any>; description: string }) => void
|
||||
defaultValues?: Record<string, any> | 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())
|
||||
|
||||
@@ -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<string, any>
|
||||
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 })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
{@render children?.()}
|
||||
|
||||
<DeployOverrideConfirmationModal
|
||||
bind:deployedBy
|
||||
bind:confirmCallback
|
||||
{deployedBy}
|
||||
{confirmCallback}
|
||||
bind:open
|
||||
{diffDrawer}
|
||||
bind:deployedValue
|
||||
@@ -1086,7 +1077,7 @@
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
{#if script.schema}
|
||||
{#if script.schema && !disableAi && !customUi?.settingsPanel?.metadata?.disableAiFilling}
|
||||
<div class="mt-3">
|
||||
<AIFormSettings
|
||||
bind:prompt={script.schema.prompt_for_ai as string | undefined}
|
||||
@@ -1096,52 +1087,52 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section label="Language">
|
||||
{#snippet action()}
|
||||
<DefaultScripts />
|
||||
{/snippet}
|
||||
{#if lockedLanguage}
|
||||
<div class="text-sm text-tertiary italic mb-2">
|
||||
As a forked script, the language '{script.language}' cannot be modified.
|
||||
</div>
|
||||
{/if}
|
||||
<div class=" grid grid-cols-3 gap-2">
|
||||
{#each langs as [label, lang] (lang)}
|
||||
{@const isPicked =
|
||||
(lang == script.language && template == 'script') ||
|
||||
(template == 'bunnative' && lang == 'bunnative') ||
|
||||
(template == 'docker' && lang == 'docker')}
|
||||
<Popover
|
||||
disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}
|
||||
>
|
||||
<Button
|
||||
aiId={`create-script-language-button-${lang}`}
|
||||
aiDescription={`Choose ${lang} as the language of the script`}
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'light'}
|
||||
btnClasses={isPicked
|
||||
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
|
||||
: 'm-[1px]'}
|
||||
on:click={() => onScriptLanguageTrigger(lang)}
|
||||
disabled={lockedLanguage ||
|
||||
(enterpriseLangs.includes(lang) && !$enterpriseLicense)}
|
||||
{#if !customUi?.settingsPanel?.metadata?.languages || customUi?.settingsPanel?.metadata?.languages?.length > 1}
|
||||
<Section label="Language">
|
||||
{#snippet action()}
|
||||
<DefaultScripts />
|
||||
{/snippet}
|
||||
{#if lockedLanguage}
|
||||
<div class="text-sm text-tertiary italic mb-2">
|
||||
As a forked script, the language '{script.language}' cannot be modified.
|
||||
</div>
|
||||
{/if}
|
||||
<div class=" grid grid-cols-3 gap-2">
|
||||
{#each langs as [label, lang] (lang)}
|
||||
{@const isPicked =
|
||||
(lang == script.language && template == 'script') ||
|
||||
(template == 'bunnative' && lang == 'bunnative') ||
|
||||
(template == 'docker' && lang == 'docker')}
|
||||
<Popover
|
||||
disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}
|
||||
>
|
||||
<LanguageIcon {lang} />
|
||||
<span class="ml-2 py-2 truncate">{label}</span>
|
||||
{#if lang === 'nu'}
|
||||
<span class="text-tertiary !text-xs"> BETA </span>
|
||||
{/if}
|
||||
</Button>
|
||||
{#snippet text()}
|
||||
{label} is only available with an enterprise license
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/each}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Button
|
||||
aiId={`create-script-language-button-${lang}`}
|
||||
aiDescription={`Choose ${lang} as the language of the script`}
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'light'}
|
||||
btnClasses={isPicked
|
||||
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
|
||||
: 'm-[1px]'}
|
||||
on:click={() => onScriptLanguageTrigger(lang)}
|
||||
disabled={lockedLanguage ||
|
||||
(enterpriseLangs.includes(lang) && !$enterpriseLicense)}
|
||||
>
|
||||
<LanguageIcon {lang} />
|
||||
<span class="ml-2 py-2 truncate">{label}</span>
|
||||
{#if lang === 'nu'}
|
||||
<span class="text-tertiary !text-xs"> BETA </span>
|
||||
{/if}
|
||||
</Button>
|
||||
{#snippet text()}
|
||||
{label} is only available with an enterprise license
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/each}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
{#if customUi?.settingsPanel?.metadata?.disableScriptKind !== true}
|
||||
<Section label="Script kind">
|
||||
{#snippet header()}
|
||||
@@ -1177,6 +1168,14 @@
|
||||
</ToggleButtonGroup>
|
||||
</Section>
|
||||
{/if}
|
||||
{#if customUi?.settingsPanel?.disableRuntime}
|
||||
<Section label="Worker group tag (queue)">
|
||||
<WorkerTagPicker
|
||||
bind:tag={script.tag}
|
||||
placeholder={customUi?.tagSelectPlaceholder}
|
||||
/>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="runtime">
|
||||
@@ -1244,7 +1243,10 @@
|
||||
group tag (queue). For instance, you could setup an "highmem", or "gpu" tag.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<WorkerTagPicker bind:tag={script.tag} />
|
||||
<WorkerTagPicker
|
||||
bind:tag={script.tag}
|
||||
placeholder={customUi?.tagSelectPlaceholder}
|
||||
/>
|
||||
</Section>
|
||||
<Section label="Cache">
|
||||
{#snippet header()}
|
||||
@@ -1698,6 +1700,18 @@
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-x-1 lg:gap-x-2">
|
||||
{#if $workerTags}
|
||||
{#if $workerTags?.length ?? 0 > 0}
|
||||
<div class="max-w-[200px] pr-8">
|
||||
<WorkerTagSelect
|
||||
inputClass="text-sm text-secondary !placeholder-secondary"
|
||||
nullTag={script.language}
|
||||
placeholder={customUi?.tagSelectPlaceholder}
|
||||
bind:tag={script.tag}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if customUi?.topBar?.settings != false}
|
||||
<Button
|
||||
aiId="script-builder-settings"
|
||||
@@ -1738,6 +1752,7 @@
|
||||
</div>
|
||||
|
||||
<ScriptEditor
|
||||
{disableAi}
|
||||
bind:selectedTab={selectedInputTab}
|
||||
{customUi}
|
||||
collabMode
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
stablePathForCaptures?: string
|
||||
lastSavedCode?: string | undefined
|
||||
lastDeployedCode?: string | undefined
|
||||
disableAi?: boolean
|
||||
editor_bar_right?: import('svelte').Snippet
|
||||
fallbackAccessTypes?: AssetWithAccessType[]
|
||||
}
|
||||
@@ -115,6 +116,7 @@
|
||||
stablePathForCaptures = '',
|
||||
lastSavedCode = undefined,
|
||||
lastDeployedCode = undefined,
|
||||
disableAi = false,
|
||||
editor_bar_right,
|
||||
fallbackAccessTypes = $bindable()
|
||||
}: Props = $props()
|
||||
@@ -559,7 +561,7 @@
|
||||
color="marine"
|
||||
/>
|
||||
{/if}
|
||||
{#if !aiChatManager.open}
|
||||
{#if !aiChatManager.open && !disableAi}
|
||||
{#if customUi?.editorBar?.aiGen != false && SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '')}
|
||||
<HideButton
|
||||
hidden={true}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
|
||||
import AiChatLayout from './copilot/chat/AiChatLayout.svelte'
|
||||
import type { ScriptBuilderProps } from './script_builder'
|
||||
|
||||
let { script: oldScript, disableAi, ...props }: ScriptBuilderProps = $props()
|
||||
|
||||
let script = $state(oldScript)
|
||||
</script>
|
||||
|
||||
<AiChatLayout noPadding {disableAi}>
|
||||
<ScriptBuilder {script} {disableAi} {...props} />
|
||||
</AiChatLayout>
|
||||
@@ -6,12 +6,22 @@
|
||||
import { WorkerService } from '$lib/gen'
|
||||
import WorkerTagSelect from './WorkerTagSelect.svelte'
|
||||
|
||||
export let tag: string | undefined
|
||||
export let popupPlacement: 'bottom-end' | 'top-end' = 'bottom-end'
|
||||
export let disabled = false
|
||||
interface Props {
|
||||
tag: string | undefined
|
||||
popupPlacement?: 'bottom-end' | 'top-end'
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
loadWorkerGroups()
|
||||
async function loadWorkerGroups() {
|
||||
let {
|
||||
tag = $bindable(),
|
||||
popupPlacement = 'bottom-end',
|
||||
disabled = false,
|
||||
placeholder
|
||||
}: Props = $props()
|
||||
|
||||
loadWorkerTags()
|
||||
async function loadWorkerTags() {
|
||||
if (!$workerTags) {
|
||||
$workerTags = await WorkerService.getCustomTags({ workspace: $workspaceStore })
|
||||
}
|
||||
@@ -22,7 +32,7 @@
|
||||
<div class="max-w-sm grow">
|
||||
{#if $workerTags}
|
||||
{#if $workerTags?.length ?? 0 > 0}
|
||||
<WorkerTagSelect noLabel bind:tag {disabled} />
|
||||
<WorkerTagSelect {placeholder} noLabel bind:tag {disabled} />
|
||||
{:else}
|
||||
<div class="text-sm text-secondary flex flex-row gap-2">
|
||||
No custom worker group tag defined on this instance in "Workers {'->'} Custom tags"
|
||||
@@ -48,7 +58,7 @@
|
||||
color="light"
|
||||
on:click={() => {
|
||||
$workerTags = undefined
|
||||
loadWorkerGroups()
|
||||
loadWorkerTags()
|
||||
}}
|
||||
startIcon={{ icon: RotateCw }}
|
||||
{disabled}
|
||||
|
||||
@@ -10,12 +10,18 @@
|
||||
tag = $bindable(),
|
||||
noLabel = false,
|
||||
nullTag = undefined,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
placeholder,
|
||||
inputClass
|
||||
}: {
|
||||
tag: string | undefined
|
||||
noLabel?: boolean
|
||||
nullTag?: string | undefined
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
language?: string
|
||||
class?: string
|
||||
inputClass?: string
|
||||
} = $props()
|
||||
|
||||
loadWorkerGroups()
|
||||
@@ -35,13 +41,14 @@
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
{#if !noLabel}
|
||||
<div class="text-tertiary text-2xs">tag</div>
|
||||
<div class="text-tertiary text-2xs">{placeholder ?? 'tag'}</div>
|
||||
{/if}
|
||||
<Select
|
||||
clearable
|
||||
class="w-full"
|
||||
{inputClass}
|
||||
{disabled}
|
||||
placeholder={nullTag ? `default: ${nullTag}` : 'lang default'}
|
||||
placeholder={nullTag ? nullTag : (placeholder ?? 'lang default')}
|
||||
items={safeSelectItems(items)}
|
||||
bind:value={() => tag, (value) => ((tag = value), dispatch('change', value))}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import type {
|
||||
App,
|
||||
AppEditorContext,
|
||||
AppEditorProps,
|
||||
AppViewerContext,
|
||||
ConnectingInput,
|
||||
ContextPanelContext,
|
||||
@@ -33,7 +34,7 @@
|
||||
|
||||
import ItemPicker from '$lib/components/ItemPicker.svelte'
|
||||
import VariableEditor from '$lib/components/VariableEditor.svelte'
|
||||
import { VariableService, type Policy } from '$lib/gen'
|
||||
import { VariableService } from '$lib/gen'
|
||||
import { initHistory } from '$lib/history'
|
||||
import { Component, Minus, Paintbrush, Plus, Smartphone, Scan, Hand, Grab } from 'lucide-svelte'
|
||||
import { animateTo, findGridItem, findGridItemParentGrid } from './appUtils'
|
||||
@@ -51,37 +52,10 @@
|
||||
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
|
||||
import { getTheme } from './componentsPanel/themeUtils'
|
||||
import StylePanel from './settingsPanel/StylePanel.svelte'
|
||||
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
||||
import HideButton from './settingsPanel/HideButton.svelte'
|
||||
import AppEditorBottomPanel from './AppEditorBottomPanel.svelte'
|
||||
import panzoom from 'panzoom'
|
||||
|
||||
interface Props {
|
||||
app: App
|
||||
path: string
|
||||
policy: Policy
|
||||
summary: string
|
||||
fromHub?: boolean
|
||||
diffDrawer?: DiffDrawer | undefined
|
||||
savedApp?:
|
||||
| {
|
||||
value: App
|
||||
draft?: any
|
||||
path: string
|
||||
summary: string
|
||||
policy: any
|
||||
draft_only?: boolean
|
||||
custom_path?: string
|
||||
}
|
||||
| undefined
|
||||
version?: number | undefined
|
||||
newApp?: boolean
|
||||
newPath?: string | undefined
|
||||
replaceStateFn?: (path: string) => void
|
||||
gotoFn?: (path: string, opt?: Record<string, any> | undefined) => void
|
||||
unsavedConfirmationModal?: import('svelte').Snippet<[any]>
|
||||
}
|
||||
|
||||
let {
|
||||
app,
|
||||
path,
|
||||
@@ -95,8 +69,9 @@
|
||||
newPath = undefined,
|
||||
replaceStateFn = (path: string) => window.history.replaceState(null, '', path),
|
||||
gotoFn = (path: string, opt?: Record<string, any>) => window.history.pushState(null, '', path),
|
||||
unsavedConfirmationModal
|
||||
}: Props = $props()
|
||||
unsavedConfirmationModal,
|
||||
onSavedNewAppPath
|
||||
}: AppEditorProps = $props()
|
||||
|
||||
migrateApp(app)
|
||||
|
||||
@@ -872,13 +847,13 @@
|
||||
leftPanelHidden={leftPanelSize === 0}
|
||||
rightPanelHidden={rightPanelSize === 0}
|
||||
bottomPanelHidden={runnablePanelSize === 0}
|
||||
on:savedNewAppPath
|
||||
on:showLeftPanel={() => showLeftPanel()}
|
||||
on:showRightPanel={() => showRightPanel()}
|
||||
on:hideLeftPanel={() => hideLeftPanel()}
|
||||
on:hideRightPanel={() => hideRightPanel()}
|
||||
on:hideBottomPanel={() => hideBottomPanel()}
|
||||
on:showBottomPanel={() => showBottomPanel()}
|
||||
{onSavedNewAppPath}
|
||||
onShowLeftPanel={() => showLeftPanel()}
|
||||
onShowRightPanel={() => showRightPanel()}
|
||||
onShowBottomPanel={() => showBottomPanel()}
|
||||
onHideLeftPanel={() => hideLeftPanel()}
|
||||
onHideRightPanel={() => hideRightPanel()}
|
||||
onHideBottomPanel={() => hideBottomPanel()}
|
||||
>
|
||||
{#snippet unsavedConfirmationModal({
|
||||
diffDrawer,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
Globe,
|
||||
AlertTriangle
|
||||
} from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import {
|
||||
cleanValueProperties,
|
||||
orderedJsonStringify,
|
||||
@@ -57,7 +57,6 @@
|
||||
import { secondaryMenuLeftStore, secondaryMenuRightStore } from './settingsPanel/secondaryMenu'
|
||||
import Dropdown from '$lib/components/DropdownV2.svelte'
|
||||
import AppEditorTutorial from './AppEditorTutorial.svelte'
|
||||
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
||||
import AppReportsDrawer from './AppReportsDrawer.svelte'
|
||||
import { type ColumnDef, getPrimaryKeys } from '../components/display/dbtable/utils'
|
||||
import DebugPanel from './contextPanel/DebugPanel.svelte'
|
||||
@@ -82,6 +81,7 @@
|
||||
import { collectStaticFields, type TriggerableV2 } from './commonAppUtils'
|
||||
import LazyModePanel from './contextPanel/LazyModePanel.svelte'
|
||||
import { Sha256 } from '@aws-crypto/sha256-js'
|
||||
import type { DiffDrawerI } from '$lib/components/diff_drawer'
|
||||
|
||||
async function hash(message) {
|
||||
try {
|
||||
@@ -103,7 +103,7 @@
|
||||
interface Props {
|
||||
policy: Policy
|
||||
fromHub?: boolean
|
||||
diffDrawer?: DiffDrawer | undefined
|
||||
diffDrawer?: DiffDrawerI | undefined
|
||||
savedApp?:
|
||||
| {
|
||||
value: App
|
||||
@@ -122,6 +122,13 @@
|
||||
newApp: boolean
|
||||
newPath?: string
|
||||
unsavedConfirmationModal?: import('svelte').Snippet<[any]>
|
||||
onSavedNewAppPath?: (path: string) => void
|
||||
onShowRightPanel?: () => void
|
||||
onShowLeftPanel?: () => void
|
||||
onShowBottomPanel?: () => void
|
||||
onHideRightPanel?: () => void
|
||||
onHideLeftPanel?: () => void
|
||||
onHideBottomPanel?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -135,7 +142,14 @@
|
||||
bottomPanelHidden = false,
|
||||
newApp,
|
||||
newPath = '',
|
||||
unsavedConfirmationModal
|
||||
unsavedConfirmationModal,
|
||||
onSavedNewAppPath,
|
||||
onShowLeftPanel,
|
||||
onShowRightPanel,
|
||||
onShowBottomPanel,
|
||||
onHideLeftPanel,
|
||||
onHideRightPanel,
|
||||
onHideBottomPanel
|
||||
}: Props = $props()
|
||||
|
||||
let newEditedPath = $state('')
|
||||
@@ -406,7 +420,7 @@
|
||||
} catch (e) {
|
||||
console.error('error interacting with local storage', e)
|
||||
}
|
||||
dispatch('savedNewAppPath', path)
|
||||
onSavedNewAppPath?.(path)
|
||||
} catch (e) {
|
||||
sendUserToast('Error creating app', e)
|
||||
}
|
||||
@@ -509,7 +523,7 @@
|
||||
} catch (e) {
|
||||
console.error('error interacting with local storage', e)
|
||||
}
|
||||
dispatch('savedNewAppPath', npath)
|
||||
onSavedNewAppPath?.(npath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +603,7 @@
|
||||
}
|
||||
|
||||
draftDrawerOpen = false
|
||||
dispatch('savedNewAppPath', newEditedPath)
|
||||
onSavedNewAppPath?.(newEditedPath)
|
||||
} catch (e) {
|
||||
sendUserToast('Error saving initial draft', e)
|
||||
}
|
||||
@@ -686,7 +700,7 @@
|
||||
}
|
||||
loading.saveDraft = false
|
||||
if (newApp || savedApp.draft_only) {
|
||||
dispatch('savedNewAppPath', newEditedPath || path)
|
||||
onSavedNewAppPath?.(newEditedPath || path)
|
||||
}
|
||||
} catch (e) {
|
||||
loading.saveDraft = false
|
||||
@@ -891,8 +905,6 @@
|
||||
debugAppDrawerOpen = true
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function setTheme(newDarkMode: boolean | undefined) {
|
||||
let globalDarkMode = window.localStorage.getItem('dark-mode')
|
||||
? window.localStorage.getItem('dark-mode') === 'dark'
|
||||
@@ -981,8 +993,8 @@
|
||||
})}
|
||||
{/if}
|
||||
<DeployOverrideConfirmationModal
|
||||
bind:deployedBy
|
||||
bind:confirmCallback
|
||||
{deployedBy}
|
||||
{confirmCallback}
|
||||
bind:open
|
||||
{diffDrawer}
|
||||
bind:deployedValue
|
||||
@@ -1442,9 +1454,9 @@
|
||||
hidden={leftPanelHidden}
|
||||
on:click={() => {
|
||||
if (leftPanelHidden) {
|
||||
dispatch('showLeftPanel')
|
||||
onShowLeftPanel?.()
|
||||
} else {
|
||||
dispatch('hideLeftPanel')
|
||||
onHideLeftPanel?.()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -1453,9 +1465,9 @@
|
||||
direction="bottom"
|
||||
on:click={() => {
|
||||
if (bottomPanelHidden) {
|
||||
dispatch('showBottomPanel')
|
||||
onShowBottomPanel?.()
|
||||
} else {
|
||||
dispatch('hideBottomPanel')
|
||||
onHideBottomPanel?.()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -1464,9 +1476,9 @@
|
||||
direction="right"
|
||||
on:click={() => {
|
||||
if (rightPanelHidden) {
|
||||
dispatch('showRightPanel')
|
||||
onShowRightPanel?.()
|
||||
} else {
|
||||
dispatch('hideRightPanel')
|
||||
onHideRightPanel?.()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -141,13 +141,42 @@ export type HiddenRunnable = {
|
||||
|
||||
export type AppTheme =
|
||||
| {
|
||||
type: 'path'
|
||||
path: string
|
||||
}
|
||||
type: 'path'
|
||||
path: string
|
||||
}
|
||||
| {
|
||||
type: 'inlined'
|
||||
css: string
|
||||
}
|
||||
type: 'inlined'
|
||||
css: string
|
||||
}
|
||||
|
||||
import type { DiffDrawerI } from "$lib/components/diff_drawer"
|
||||
|
||||
export interface AppEditorProps {
|
||||
app: App
|
||||
path: string
|
||||
policy: Policy
|
||||
summary: string
|
||||
fromHub?: boolean
|
||||
diffDrawer?: DiffDrawerI | undefined
|
||||
savedApp?:
|
||||
| {
|
||||
value: App
|
||||
draft?: any
|
||||
path: string
|
||||
summary: string
|
||||
policy: any
|
||||
draft_only?: boolean
|
||||
custom_path?: string
|
||||
}
|
||||
| undefined
|
||||
version?: number | undefined
|
||||
newApp?: boolean
|
||||
newPath?: string | undefined
|
||||
replaceStateFn?: (path: string) => void
|
||||
gotoFn?: (path: string, opt?: Record<string, any> | undefined) => void
|
||||
unsavedConfirmationModal?: import('svelte').Snippet<[any]>
|
||||
onSavedNewAppPath?: (path: string) => void
|
||||
}
|
||||
|
||||
export type App = {
|
||||
grid: GridItem[]
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
actionKind: ActionKind | undefined | 'all',
|
||||
scope: undefined | 'all_workspaces' | 'instance'
|
||||
): Promise<void> {
|
||||
console.log("loading logs")
|
||||
loading = true
|
||||
|
||||
if (username == 'all') {
|
||||
@@ -130,6 +131,7 @@
|
||||
hasMore = logs.length > 0 && logs.length === perPage
|
||||
|
||||
loading = false
|
||||
console.log("loadede logs")
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
@@ -353,24 +355,32 @@
|
||||
<span class="text-xs absolute -top-4">After</span>
|
||||
<input type="text" value={after ?? 'After'} disabled />
|
||||
<CalendarPicker
|
||||
clearable
|
||||
date={after}
|
||||
placement="bottom-end"
|
||||
label="After"
|
||||
on:change={async ({ detail }) => {
|
||||
on:change={({ detail }) => {
|
||||
after = new Date(detail).toISOString()
|
||||
}}
|
||||
on:clear={() => {
|
||||
after = undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<span class="text-xs absolute -top-4">Before</span>
|
||||
<input type="text" value={before ?? 'Before'} disabled />
|
||||
<CalendarPicker
|
||||
clearable
|
||||
bind:date={before}
|
||||
label="Before"
|
||||
placement="bottom-end"
|
||||
on:change={async ({ detail }) => {
|
||||
on:change={({ detail }) => {
|
||||
before = new Date(detail).toISOString()
|
||||
}}
|
||||
on:clear={() => {
|
||||
before = undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import Cell from '$lib/components/table/Cell.svelte'
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
import Head from '$lib/components/table/Head.svelte'
|
||||
import Row from '$lib/components/table/Row.svelte'
|
||||
import type { AuditLog } from '$lib/gen'
|
||||
import { displayDate } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { onMount, tick } from 'svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { ListFilter } from 'lucide-svelte'
|
||||
import { ListFilter, ChevronLeft, ChevronRight } from 'lucide-svelte'
|
||||
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let logs: AuditLog[] = []
|
||||
export let pageIndex: number | undefined = 1
|
||||
export let perPage: number | undefined = 100
|
||||
export let hasMore: boolean = true
|
||||
export let actionKind: string | undefined = undefined
|
||||
export let operation: string | undefined = undefined
|
||||
export let selectedId: number | undefined = undefined
|
||||
export let usernameFilter: string | undefined = undefined
|
||||
export let resourceFilter: string | undefined = undefined
|
||||
interface Props {
|
||||
logs?: AuditLog[]
|
||||
pageIndex?: number | undefined
|
||||
perPage?: number | undefined
|
||||
hasMore?: boolean
|
||||
actionKind?: string | undefined
|
||||
operation?: string | undefined
|
||||
selectedId?: number | undefined
|
||||
usernameFilter?: string | undefined
|
||||
resourceFilter?: string | undefined
|
||||
onselect?: (id: number) => void
|
||||
}
|
||||
|
||||
let {
|
||||
logs = [],
|
||||
pageIndex = $bindable(1),
|
||||
perPage = $bindable(100),
|
||||
hasMore = $bindable(true),
|
||||
actionKind = $bindable(),
|
||||
operation = $bindable(),
|
||||
selectedId = undefined,
|
||||
usernameFilter = $bindable(),
|
||||
resourceFilter = $bindable(),
|
||||
onselect
|
||||
}: Props = $props()
|
||||
|
||||
function groupLogsByDay(logs: AuditLog[]): Record<string, AuditLog[]> {
|
||||
const groupedLogs = {}
|
||||
@@ -42,8 +56,55 @@
|
||||
return groupedLogs
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: groupedLogs = groupLogsByDay(logs)
|
||||
type FlatLogs =
|
||||
| {
|
||||
type: 'date'
|
||||
date: string
|
||||
}
|
||||
| {
|
||||
type: 'log'
|
||||
log: AuditLog
|
||||
}
|
||||
|
||||
function flattenLogs(groupedLogs: Record<string, AuditLog[]>): Array<FlatLogs> {
|
||||
const flatLogs: Array<FlatLogs> = []
|
||||
|
||||
for (const [date, logsByDay] of Object.entries(groupedLogs)) {
|
||||
flatLogs.push({ type: 'date', date })
|
||||
for (const log of logsByDay) {
|
||||
flatLogs.push({ type: 'log', log })
|
||||
}
|
||||
}
|
||||
|
||||
return flatLogs
|
||||
}
|
||||
|
||||
let tableHeight: number = $state(0)
|
||||
let headerHeight: number = $state(0)
|
||||
let footerHeight: number = $state(48)
|
||||
|
||||
function computeHeight() {
|
||||
tableHeight =
|
||||
document.querySelector('#audit-logs-table-wrapper')!.parentElement?.clientHeight ?? 0
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
tick().then(computeHeight)
|
||||
})
|
||||
|
||||
let groupedLogs = $derived(groupLogsByDay(logs))
|
||||
let flatLogs = $derived(groupedLogs ? flattenLogs(groupedLogs) : undefined)
|
||||
let stickyIndices = $derived.by(() => {
|
||||
const nstickyIndices: number[] = []
|
||||
let index = 0
|
||||
for (const entry of flatLogs ?? []) {
|
||||
if (entry.type === 'date') {
|
||||
nstickyIndices.push(index)
|
||||
}
|
||||
index++
|
||||
}
|
||||
return nstickyIndices
|
||||
})
|
||||
|
||||
function kindToBadgeColor(kind: string) {
|
||||
if (kind == 'Execute') {
|
||||
@@ -59,118 +120,178 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<DataTable
|
||||
on:next={() => {
|
||||
pageIndex = (pageIndex ?? 1) + 1
|
||||
}}
|
||||
on:previous={() => {
|
||||
pageIndex = (pageIndex ?? 1) - 1
|
||||
}}
|
||||
currentPage={pageIndex}
|
||||
paginated
|
||||
rounded={false}
|
||||
size="sm"
|
||||
{hasMore}
|
||||
bind:perPage
|
||||
<svelte:window onresize={() => computeHeight()} />
|
||||
|
||||
<div
|
||||
class="divide-y min-w-[640px] h-full"
|
||||
id="audit-logs-table-wrapper"
|
||||
>
|
||||
<Head>
|
||||
<Cell first head>ID</Cell>
|
||||
<Cell head>Timestamp</Cell>
|
||||
<Cell head>Username</Cell>
|
||||
<Cell head>Operation</Cell>
|
||||
<Cell head last>Resource</Cell>
|
||||
</Head>
|
||||
{#if logs?.length > 0}
|
||||
<tbody class="divide-y">
|
||||
{#each Object.entries(groupedLogs) as [date, logsByDay]}
|
||||
<tr class="border-t">
|
||||
<Cell
|
||||
first
|
||||
colspan="6"
|
||||
scope="colgroup"
|
||||
class="bg-surface-secondary/30 py-2 border-b font-semibold"
|
||||
>
|
||||
{date}
|
||||
</Cell>
|
||||
</tr>
|
||||
{#each logsByDay as { id, timestamp, username, operation: op, action_kind, resource, parameters }}
|
||||
<Row
|
||||
hoverable
|
||||
selected={id === selectedId}
|
||||
on:click={() => {
|
||||
dispatch('select', id)
|
||||
}}
|
||||
>
|
||||
<Cell first>
|
||||
{id}
|
||||
</Cell>
|
||||
<Cell>
|
||||
{displayDate(timestamp)}
|
||||
</Cell>
|
||||
<Cell>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<div class="whitespace-nowrap overflow-x-auto no-scrollbar max-w-52">
|
||||
{username}
|
||||
{#if parameters && 'end_user' in parameters}
|
||||
<span> ({parameters.end_user})</span>
|
||||
{/if}
|
||||
</div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
iconOnly
|
||||
startIcon={{ icon: ListFilter }}
|
||||
on:click={() => {
|
||||
usernameFilter = username
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<div class="flex flex-row gap-1">
|
||||
<Badge
|
||||
on:click={() => {
|
||||
actionKind = action_kind.toLocaleLowerCase()
|
||||
}}
|
||||
color={kindToBadgeColor(action_kind)}>{action_kind}</Badge
|
||||
>
|
||||
<Badge
|
||||
on:click={() => {
|
||||
operation = op
|
||||
}}
|
||||
>
|
||||
{op}
|
||||
</Badge>
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell last>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<div class="whitespace-nowrap overflow-x-auto no-scrollbar w-48">
|
||||
{resource}
|
||||
</div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
iconOnly
|
||||
startIcon={{ icon: ListFilter }}
|
||||
on:click={() => {
|
||||
resourceFilter = resource
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Cell>
|
||||
</Row>
|
||||
{/each}
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
<div bind:clientHeight={headerHeight}>
|
||||
<div
|
||||
class="flex flex-row bg-surface-secondary sticky top-0 w-full p-2 pr-4 text-xs font-semibold"
|
||||
>
|
||||
<div class="w-1/12">ID</div>
|
||||
<div class="w-3/12">Timestamp</div>
|
||||
<div class="w-3/12">Username</div>
|
||||
<div class="w-3/12">Operation</div>
|
||||
<div class="w-2/12">Resource</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if logs?.length == 0}
|
||||
<div class="text-xs text-secondary p-8"> No logs found for the selected filters. </div>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-8">
|
||||
<div class="text-xs text-secondary"> No logs found for the selected filters. </div>
|
||||
</td>
|
||||
</tr>
|
||||
<VirtualList
|
||||
width="100%"
|
||||
height={tableHeight - headerHeight - footerHeight}
|
||||
itemCount={flatLogs?.length ?? 0}
|
||||
itemSize={42}
|
||||
overscanCount={20}
|
||||
{stickyIndices}
|
||||
scrollToAlignment="center"
|
||||
>
|
||||
{#snippet header()}{/snippet}
|
||||
{#snippet children({ index, style })}
|
||||
<div {style} class="w-full">
|
||||
{#if flatLogs}
|
||||
{@const logOrDate = flatLogs[index]}
|
||||
|
||||
{#if logOrDate}
|
||||
{#if logOrDate?.type === 'date'}
|
||||
<div class="bg-surface-secondary py-2 border-b font-semibold text-xs pl-5">
|
||||
{logOrDate.date}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex flex-row items-center h-full w-full px-2 py-1 hover:bg-surface-hover cursor-pointer',
|
||||
logOrDate.log.id === selectedId ? 'bg-blue-50 dark:bg-blue-900/50' : ''
|
||||
)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => {
|
||||
onselect?.(logOrDate.log.id)
|
||||
}}
|
||||
>
|
||||
<div class="w-1/12 text-xs truncate">
|
||||
{logOrDate.log.id}
|
||||
</div>
|
||||
<div class="w-3/12 text-xs">
|
||||
{displayDate(logOrDate.log.timestamp)}
|
||||
</div>
|
||||
<div class="w-3/12 text-xs">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<div class="whitespace-nowrap overflow-x-auto no-scrollbar max-w-60">
|
||||
{logOrDate.log.username}
|
||||
{#if logOrDate.log.parameters && 'end_user' in logOrDate.log.parameters}
|
||||
<span> ({logOrDate.log.parameters.end_user})</span>
|
||||
{/if}
|
||||
</div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
iconOnly
|
||||
startIcon={{ icon: ListFilter }}
|
||||
on:click={() => {
|
||||
usernameFilter = logOrDate.log.username
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-3/12 text-xs">
|
||||
<div class="flex flex-row gap-1">
|
||||
<Badge
|
||||
on:click={() => {
|
||||
actionKind = logOrDate.log.action_kind.toLocaleLowerCase()
|
||||
}}
|
||||
color={kindToBadgeColor(logOrDate.log.action_kind)}
|
||||
>
|
||||
{logOrDate.log.action_kind}
|
||||
</Badge>
|
||||
<Badge
|
||||
on:click={() => {
|
||||
operation = logOrDate.log.operation
|
||||
}}
|
||||
>
|
||||
{logOrDate.log.operation}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-2/12 text-xs">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<div class="whitespace-nowrap overflow-x-auto no-scrollbar max-w-60">
|
||||
{logOrDate.log.resource}
|
||||
</div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
iconOnly
|
||||
startIcon={{ icon: ListFilter }}
|
||||
on:click={() => {
|
||||
resourceFilter = logOrDate.log.resource
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-row items-center h-full w-full px-2">
|
||||
<div class="text-xs text-secondary">Loading...</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-row items-center h-full w-full px-2">
|
||||
<div class="text-xs text-secondary">Loading...</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet footer()}{/snippet}
|
||||
</VirtualList>
|
||||
{/if}
|
||||
</DataTable>
|
||||
<!-- Pagination footer - always visible -->
|
||||
<div class="flex flex-row justify-between items-center p-2 bg-surface-primary border-t">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
startIcon={{ icon: ChevronLeft }}
|
||||
on:click={() => {
|
||||
pageIndex = (pageIndex ?? 1) - 1
|
||||
}}
|
||||
disabled={pageIndex <= 1}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<span class="text-xs text-secondary px-2">Page {pageIndex}</span>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
endIcon={{ icon: ChevronRight }}
|
||||
on:click={() => {
|
||||
pageIndex = (pageIndex ?? 1) + 1
|
||||
}}
|
||||
disabled={!hasMore}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<span class="text-xs text-secondary">Per page:</span>
|
||||
<select
|
||||
bind:value={perPage}
|
||||
class="text-xs bg-transparent border border-gray-300 dark:border-gray-600 rounded px-2 py-1"
|
||||
>
|
||||
<option value={25}>25</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={1000}>1000</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
@@ -183,4 +304,10 @@
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* VirtualList scrollbar styling */
|
||||
:global(.virtual-list-wrapper:hover::-webkit-scrollbar) {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,564 @@
|
||||
<script lang="ts">
|
||||
import 'chartjs-adapter-date-fns'
|
||||
import zoomPlugin from 'chartjs-plugin-zoom'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
TimeScale,
|
||||
type ChartData,
|
||||
type ChartOptions
|
||||
} from 'chart.js'
|
||||
import type { AuditLog } from '$lib/gen'
|
||||
import { Scatter } from '../chartjs-wrappers/chartJs'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
|
||||
interface Props {
|
||||
logs: AuditLog[]
|
||||
minTimeSet: string | undefined
|
||||
maxTimeSet: string | undefined
|
||||
onMissingJobSpan?: (jobId: string, jobLogs: AuditLog[]) => Promise<AuditLog[]>
|
||||
onZoom?: (range: { min: Date; max: Date }) => void
|
||||
onLogSelected?: (log: any) => void
|
||||
}
|
||||
|
||||
let {
|
||||
logs = [],
|
||||
minTimeSet,
|
||||
maxTimeSet,
|
||||
onMissingJobSpan,
|
||||
onZoom,
|
||||
onLogSelected
|
||||
}: Props = $props()
|
||||
|
||||
// Register ChartJS components
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
zoomPlugin,
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
TimeScale
|
||||
)
|
||||
|
||||
function addSeconds(date: Date, seconds: number): Date {
|
||||
date.setTime(date.getTime() + seconds * 1000)
|
||||
return date
|
||||
}
|
||||
|
||||
const zoomOptions = {
|
||||
pan: {
|
||||
mode: 'x' as 'x',
|
||||
enabled: true,
|
||||
modifierKey: 'ctrl' as 'ctrl',
|
||||
onPanComplete: ({ chart }) => {
|
||||
chartInstance = chart
|
||||
onZoom?.({
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
drag: {
|
||||
enabled: true
|
||||
},
|
||||
mode: 'x' as 'x',
|
||||
scaleMode: 'y' as 'y',
|
||||
onZoom: ({ chart }) => {
|
||||
chartInstance = chart
|
||||
onZoom?.({
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Color mapping for different action kinds
|
||||
const actionColors = {
|
||||
Execute: '#3b82f6', // blue
|
||||
Delete: '#ef4444', // red
|
||||
Update: '#eab308', // yellow
|
||||
Create: '#22c55e', // green
|
||||
default: '#6b7280' // gray
|
||||
}
|
||||
|
||||
function getActionColor(actionKind: string): string {
|
||||
return actionColors[actionKind as keyof typeof actionColors] || actionColors.default
|
||||
}
|
||||
|
||||
async function groupLogsBySpan(
|
||||
logs: AuditLog[],
|
||||
onMissingJobSpan?: (jobId: string, jobLogs: AuditLog[]) => Promise<AuditLog[]>
|
||||
): Promise<{
|
||||
grouped: Record<string, AuditLog[]>
|
||||
jobGrouped: Map<string, AuditLog[]>
|
||||
}> {
|
||||
const grouped: Record<string, AuditLog[]> = {}
|
||||
|
||||
const jobGrouped: Map<string, AuditLog[]> = new Map()
|
||||
for (const log of logs) {
|
||||
const spanId = log.span || 'untraced'
|
||||
if (spanId.startsWith('job-span-')) {
|
||||
const jobid = spanId.slice('job-span-'.length)
|
||||
|
||||
if (!jobGrouped.has(jobid)) {
|
||||
jobGrouped.set(jobid, [])
|
||||
}
|
||||
jobGrouped.get(jobid)?.push(log)
|
||||
continue
|
||||
}
|
||||
if (!grouped[spanId]) {
|
||||
grouped[spanId] = []
|
||||
}
|
||||
grouped[spanId].push(log)
|
||||
}
|
||||
|
||||
for (const jobid of jobGrouped.keys()) {
|
||||
const j = Object.values(grouped)
|
||||
.flat()
|
||||
.find((log) => log.parameters?.uuid === jobid)
|
||||
if (j?.span != undefined) {
|
||||
grouped[j.span].push(...jobGrouped.get(jobid)!)
|
||||
jobGrouped.get(jobid)?.push(j)
|
||||
} else {
|
||||
// Try to fetch missing job execution audit log
|
||||
if (onMissingJobSpan) {
|
||||
try {
|
||||
const jobLogs = jobGrouped.get(jobid)!
|
||||
const additionalLogs = await onMissingJobSpan(jobid, jobLogs)
|
||||
|
||||
// Look for the job execution audit log in the new results
|
||||
const jobExecutionLog = additionalLogs.find((log) => log.parameters?.uuid === jobid)
|
||||
if (jobExecutionLog?.span) {
|
||||
if (!grouped[jobExecutionLog.span]) {
|
||||
grouped[jobExecutionLog.span] = []
|
||||
}
|
||||
grouped[jobExecutionLog.span].push(jobExecutionLog, ...jobLogs)
|
||||
jobGrouped.get(jobid)?.push(jobExecutionLog)
|
||||
continue
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch missing job audit span for job ${jobid}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!grouped[jobid]) {
|
||||
grouped[jobid] = []
|
||||
}
|
||||
grouped[jobid].push(...jobGrouped.get(jobid)!)
|
||||
}
|
||||
}
|
||||
// Sort logs within each span by timestamp
|
||||
Object.values(grouped).forEach((spanLogs) => {
|
||||
spanLogs.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
||||
})
|
||||
|
||||
return { grouped, jobGrouped }
|
||||
}
|
||||
|
||||
function isDark(): boolean {
|
||||
return document.documentElement.classList.contains('dark')
|
||||
}
|
||||
|
||||
// Function to apply zoom-aware jittering to overlapping points
|
||||
function applyJittering(dataPoints: any[], baseY: number, chartInstance?: any): any[] {
|
||||
if (dataPoints.length <= 1) return dataPoints
|
||||
|
||||
// Sort by timestamp
|
||||
const sorted = [...dataPoints].sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime())
|
||||
|
||||
// Calculate visual overlap based on chart scale
|
||||
const pointRadius = 0.8 // Current point radius
|
||||
const overlapThreshold = pointRadius * 2 // Points overlap if closer than this in pixels
|
||||
|
||||
// Group points that visually overlap
|
||||
const groups: any[][] = []
|
||||
let currentGroup: any[] = [sorted[0]]
|
||||
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
const prevTime = new Date(sorted[i - 1].x).getTime()
|
||||
const currTime = new Date(sorted[i].x).getTime()
|
||||
|
||||
// Calculate pixel distance between points
|
||||
let pixelDistance = overlapThreshold + 1 // Default to no overlap
|
||||
|
||||
if (chartInstance && chartInstance.scales && chartInstance.scales.x) {
|
||||
const prevPixel = chartInstance.scales.x.getPixelForValue(prevTime)
|
||||
const currPixel = chartInstance.scales.x.getPixelForValue(currTime)
|
||||
pixelDistance = Math.abs(currPixel - prevPixel)
|
||||
} else {
|
||||
pixelDistance = 20000
|
||||
}
|
||||
|
||||
if (pixelDistance < overlapThreshold) {
|
||||
currentGroup.push(sorted[i])
|
||||
} else {
|
||||
groups.push(currentGroup)
|
||||
currentGroup = [sorted[i]]
|
||||
}
|
||||
}
|
||||
groups.push(currentGroup)
|
||||
|
||||
const jitteredPoints: any[] = []
|
||||
groups.forEach((group) => {
|
||||
if (group.length === 1) {
|
||||
jitteredPoints.push({
|
||||
...group[0],
|
||||
y: baseY,
|
||||
isCluster: false,
|
||||
clusterSize: 1
|
||||
})
|
||||
} else {
|
||||
const jitterRange = 0.4
|
||||
|
||||
group.forEach((point, index) => {
|
||||
let jitterOffset =
|
||||
(1 - Math.exp(-group.length / 50)) * jitterRange * (Math.random() - 0.5)
|
||||
|
||||
jitteredPoints.push({
|
||||
...point,
|
||||
y: baseY + jitterOffset,
|
||||
originalY: baseY,
|
||||
isCluster: false,
|
||||
clusterSize: group.length,
|
||||
clusterIndex: index
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return jitteredPoints
|
||||
}
|
||||
|
||||
// Set chart defaults based on theme
|
||||
ChartJS.defaults.color = isDark() ? '#ccc' : '#666'
|
||||
ChartJS.defaults.borderColor = isDark() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
|
||||
|
||||
async function getGroupedData(): Promise<{
|
||||
grouped: Record<string, AuditLog[]>
|
||||
jobGrouped: Map<string, AuditLog[]>
|
||||
}> {
|
||||
if (logs.length === 0) {
|
||||
await sleep(1)
|
||||
return { grouped: {}, jobGrouped: new Map() }
|
||||
}
|
||||
|
||||
try {
|
||||
return await groupLogsBySpan(logs, onMissingJobSpan)
|
||||
} catch (error) {
|
||||
console.error('Error grouping logs:', error)
|
||||
return await groupLogsBySpan(logs)
|
||||
}
|
||||
}
|
||||
|
||||
let groupedData = usePromise(getGroupedData)
|
||||
// let isGrouping = $state(false)
|
||||
let chartInstance: any = $state(null)
|
||||
|
||||
let { minTime, maxTime } = $derived(computeMinMaxTime(logs, minTimeSet, maxTimeSet))
|
||||
|
||||
function computeMinMaxTime(
|
||||
logs: AuditLog[] | undefined,
|
||||
minTimeSet: string | undefined,
|
||||
maxTimeSet: string | undefined
|
||||
) {
|
||||
let minTime = addSeconds(new Date(), -300)
|
||||
let maxTime = new Date()
|
||||
|
||||
let minTimeSetDate = minTimeSet ? new Date(minTimeSet) : undefined
|
||||
let maxTimeSetDate = maxTimeSet ? new Date(maxTimeSet) : undefined
|
||||
if (minTimeSetDate && maxTimeSetDate) {
|
||||
minTime = minTimeSetDate
|
||||
maxTime = maxTimeSetDate
|
||||
return { minTime, maxTime }
|
||||
}
|
||||
|
||||
if (logs == undefined || logs?.length == 0) {
|
||||
minTime = minTimeSetDate ?? addSeconds(new Date(), -300)
|
||||
maxTime = maxTimeSetDate ?? new Date()
|
||||
return { minTime, maxTime }
|
||||
}
|
||||
|
||||
const maxLogsTime = new Date(
|
||||
logs.reduce((max, current) =>
|
||||
new Date(current.timestamp) > new Date(max.timestamp) ? current : max
|
||||
).timestamp
|
||||
)
|
||||
const maxJob = maxTimeSetDate === undefined ? new Date() : maxLogsTime
|
||||
|
||||
const minJob = new Date(
|
||||
logs.reduce((max, current) =>
|
||||
new Date(current.timestamp) < new Date(max.timestamp) ? current : max
|
||||
).timestamp
|
||||
)
|
||||
|
||||
const diff = (maxJob.getTime() - minJob.getTime()) / 20000
|
||||
|
||||
minTime = minTimeSetDate ?? addSeconds(minJob, -diff)
|
||||
if (maxTimeSetDate) {
|
||||
maxTime = maxTimeSetDate ?? maxJob
|
||||
} else {
|
||||
maxTime = maxTimeSetDate ?? addSeconds(maxJob, diff)
|
||||
}
|
||||
return { minTime, maxTime }
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
logs && untrack(() => groupedData.refresh())
|
||||
})
|
||||
|
||||
const groupedLogs = $derived(groupedData.value?.grouped ?? {})
|
||||
const jobGrouped = $derived(groupedData.value?.jobGrouped ?? new Map())
|
||||
const spanIds = $derived(Object.keys(groupedLogs).sort())
|
||||
const spanAuthors = $derived(
|
||||
spanIds.map((span) => {
|
||||
if (span == 'untraced') {
|
||||
return 'untraced'
|
||||
}
|
||||
const endUser = groupedLogs[span][0]?.parameters?.end_user
|
||||
const endUserText = endUser ? ` (${endUser})` : ''
|
||||
return groupedLogs[span]?.length > 0 ? `${groupedLogs[span][0].username}${endUserText}` : ''
|
||||
})
|
||||
)
|
||||
|
||||
// Transform data for ChartJS scatter plot
|
||||
const chartData = $derived((): ChartData<'scatter'> => {
|
||||
if (untrack(() => logs.length) === 0) {
|
||||
return { datasets: [] }
|
||||
}
|
||||
|
||||
const datasets: any[] = []
|
||||
|
||||
// Create datasets for regular span groups (points only)
|
||||
spanIds.forEach((spanId, index) => {
|
||||
const spanLogs = groupedLogs[spanId]
|
||||
|
||||
// Create initial data points
|
||||
const dataPoints = spanLogs.map((log) => ({
|
||||
x: log.timestamp as any,
|
||||
y: index, // Each span gets its own y-axis position
|
||||
log: log // Store full log data for tooltips
|
||||
}))
|
||||
|
||||
// Apply zoom-aware jittering to spread out overlapping points
|
||||
const jitteredPoints = applyJittering(dataPoints, index, chartInstance)
|
||||
// const jitteredPoints = dataPoints
|
||||
|
||||
datasets.push({
|
||||
label: spanId === 'untraced' ? 'Untraced' : spanId,
|
||||
data: jitteredPoints,
|
||||
backgroundColor: jitteredPoints.map((point) => {
|
||||
const baseColor = getActionColor(point.log.action_kind)
|
||||
// Make clustered points slightly more opaque
|
||||
return point.isCluster ? baseColor + 'E0' : baseColor
|
||||
}),
|
||||
borderColor: jitteredPoints.map((point) => {
|
||||
const baseColor = getActionColor(point.log.action_kind)
|
||||
// Add white border to clustered points for better visibility
|
||||
return point.isCluster ? '#ffffff' : baseColor
|
||||
}),
|
||||
borderWidth: jitteredPoints.map((point) => (point.isCluster ? 1 : 1)),
|
||||
pointRadius: jitteredPoints.map((point) => (point.isCluster ? 3 : 3)),
|
||||
pointHoverRadius: jitteredPoints.map((point) => (point.isCluster ? 5 : 5)),
|
||||
showLine: false
|
||||
})
|
||||
})
|
||||
|
||||
// Create datasets for job-connected lines
|
||||
jobGrouped.forEach((jobLogs: AuditLog[], jobId: string) => {
|
||||
if (jobLogs.length > 1) {
|
||||
// Only create lines if there are multiple points
|
||||
// Sort job logs by timestamp to ensure proper line connection
|
||||
const sortedJobLogs = [...jobLogs].sort(
|
||||
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
)
|
||||
|
||||
// Find the y-position for each log based on its span and jittered position
|
||||
const lineData = sortedJobLogs.map((log) => {
|
||||
const spanId = log.span || 'untraced'
|
||||
let baseYPosition = spanIds.indexOf(spanId)
|
||||
if (baseYPosition === -1) {
|
||||
// Handle job-span logs that might not be in regular spans
|
||||
const jobSpanId = spanId.startsWith('job-span-')
|
||||
? spanId.slice('job-span-'.length)
|
||||
: spanId
|
||||
baseYPosition = spanIds.findIndex((id) => id === jobSpanId)
|
||||
if (baseYPosition === -1) {
|
||||
// If still not found, assign to the span where this job's audit logs are grouped
|
||||
const auditSpan = Object.entries(groupedLogs).find(([, spanLogs]) =>
|
||||
spanLogs.some((l) => l.parameters?.uuid === jobId)
|
||||
)?.[0]
|
||||
baseYPosition = auditSpan ? spanIds.indexOf(auditSpan) : 0
|
||||
}
|
||||
}
|
||||
|
||||
// Find the jittered position for this specific log
|
||||
let jitteredY = baseYPosition
|
||||
if (baseYPosition >= 0 && baseYPosition < datasets.length) {
|
||||
const spanDataset = datasets[baseYPosition]
|
||||
if (spanDataset && spanDataset.data) {
|
||||
const matchingPoint = spanDataset.data.find(
|
||||
(point: any) => point.log && point.log.id === log.id
|
||||
)
|
||||
if (matchingPoint) {
|
||||
jitteredY = matchingPoint.y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: log.timestamp as any,
|
||||
y: jitteredY,
|
||||
log: log
|
||||
}
|
||||
})
|
||||
|
||||
datasets.push({
|
||||
label: `Job ${jobId} Connection`,
|
||||
data: lineData,
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#8b5cf6', // Purple color for job connections
|
||||
borderWidth: 2,
|
||||
pointRadius: 0, // Hide points for connection lines
|
||||
pointHoverRadius: 0,
|
||||
showLine: true,
|
||||
tension: 0, // Straight lines
|
||||
fill: false
|
||||
})
|
||||
}
|
||||
})
|
||||
return { datasets }
|
||||
})
|
||||
|
||||
const chartOptions = $derived(
|
||||
(): ChartOptions<'scatter'> => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
zoom: zoomOptions,
|
||||
legend: {
|
||||
display: false // We'll create our own legend
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: function (context: any) {
|
||||
const log = context[0].raw.log
|
||||
let title = `${log.operation} - ${log.action_kind}`
|
||||
return title
|
||||
},
|
||||
label: function (context: any) {
|
||||
const log = context.raw.log
|
||||
const labels = [
|
||||
`User: ${log.username}`,
|
||||
`Resource: ${log.resource}`,
|
||||
`Time: ${new Date(log.timestamp).toLocaleString()}`
|
||||
]
|
||||
return labels
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
displayFormats: {
|
||||
millisecond: 'HH:mm:ss.SSS',
|
||||
second: 'HH:mm:ss',
|
||||
minute: 'HH:mm',
|
||||
hour: 'MMM dd HH:mm',
|
||||
day: 'MMM dd',
|
||||
week: 'MMM dd',
|
||||
month: 'MMM yyyy',
|
||||
quarter: 'MMM yyyy',
|
||||
year: 'yyyy'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time'
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 15
|
||||
},
|
||||
min: minTime.getTime(),
|
||||
max: maxTime.getTime()
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
min: -1,
|
||||
max: spanIds.length,
|
||||
grid: {
|
||||
display: true,
|
||||
color: isDark() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxTicksLimit: 22,
|
||||
stepSize: 1,
|
||||
callback: function (value: any) {
|
||||
if (spanIds.length > 20) {
|
||||
return ''
|
||||
}
|
||||
const index = Math.round(value)
|
||||
if (index >= 0 && index < spanIds.length) {
|
||||
// const spanId = `${spanAuthors[index]} - ${index}`
|
||||
const spanId = spanAuthors[index]
|
||||
return spanId === 'untraced' ? 'Untraced' : spanId.slice(0, 30)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick: (event: any, elements: any, chart: any) => {
|
||||
// Capture chart instance for jittering calculations
|
||||
if (!chartInstance) {
|
||||
chartInstance = chart
|
||||
}
|
||||
if (elements.length > 0) {
|
||||
const element = elements[0]
|
||||
const log = (chartData().datasets[element.datasetIndex].data[element.index] as any).log
|
||||
onLogSelected?.(log)
|
||||
}
|
||||
},
|
||||
onHover: (event: any, elements: any, chart: any) => {
|
||||
// Capture chart instance for jittering calculations
|
||||
if (!chartInstance) {
|
||||
chartInstance = chart
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 300
|
||||
}
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="p-4 bg-surface mb-4 h-full">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-center py-8 text-secondary"> No audit logs to display </div>
|
||||
{:else if !groupedData || groupedData.status === 'loading'}
|
||||
<div class="text-center py-8 text-secondary">
|
||||
<Loader2 size={24} class="animate-spin mx-auto mb-2" />
|
||||
Processing audit logs...
|
||||
</div>
|
||||
{:else}
|
||||
<Scatter data={chartData()} options={chartOptions()} />
|
||||
{/if}
|
||||
</div>
|
||||
+18
-7
@@ -1,15 +1,26 @@
|
||||
<script lang="ts">
|
||||
import ConfirmationModal from './ConfirmationModal.svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
||||
import { type Value } from '$lib/utils'
|
||||
import type { DiffDrawerI } from '$lib/components/diff_drawer'
|
||||
|
||||
export let deployedValue: Value | undefined = undefined
|
||||
export let currentValue: Value | undefined = undefined
|
||||
export let diffDrawer: DiffDrawer | undefined = undefined
|
||||
export let confirmCallback: () => void
|
||||
export let deployedBy : string | undefined = undefined
|
||||
export let open = false
|
||||
interface Props {
|
||||
deployedValue?: Value | undefined;
|
||||
currentValue?: Value | undefined;
|
||||
diffDrawer?: DiffDrawerI | undefined;
|
||||
confirmCallback: () => void;
|
||||
deployedBy?: string | undefined;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
deployedValue = $bindable(),
|
||||
currentValue = undefined,
|
||||
diffDrawer = undefined,
|
||||
confirmCallback,
|
||||
deployedBy = undefined,
|
||||
open = $bindable(false)
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
<script lang="ts">
|
||||
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<number | null>(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()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -208,68 +160,13 @@
|
||||
>
|
||||
<div class="flex flex-col" bind:clientHeight={height}>
|
||||
{#each messages as message, messageIndex}
|
||||
<div class={twMerge(message.role === 'user' && messageIndex > 0 && 'mt-6', 'mb-2')}>
|
||||
{#if message.role === 'user' && message.contextElements}
|
||||
<div class="flex flex-row gap-1 mb-1 overflow-scroll no-scrollbar px-2">
|
||||
{#each message.contextElements as element}
|
||||
<ContextElementBadge contextElement={element} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-sm py-1 mx-2',
|
||||
message.role === 'user' &&
|
||||
'px-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 rounded-lg relative group',
|
||||
(message.role === 'assistant' || message.role === 'tool') && 'px-[1px]',
|
||||
message.role === 'tool' && 'text-tertiary'
|
||||
)}
|
||||
>
|
||||
{#if message.role === 'assistant'}
|
||||
<AssistantMessage {message} />
|
||||
{:else}
|
||||
{message.content}
|
||||
{/if}
|
||||
|
||||
{#if message.role === 'user' && isLastUserMessage(messageIndex) && !aiChatManager.loading}
|
||||
<div
|
||||
class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
color="light"
|
||||
iconOnly
|
||||
title="Restart generation"
|
||||
startIcon={{ icon: RefreshCwIcon }}
|
||||
btnClasses="!p-1 !h-6 !w-6"
|
||||
on:click={() => restartGeneration(messageIndex)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if message.role === 'user' && message.snapshot}
|
||||
<div
|
||||
class="mx-2 text-sm text-tertiary flex flex-row items-center justify-between gap-2 mt-2"
|
||||
>
|
||||
Saved a flow snapshot
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
if (message.snapshot) {
|
||||
aiChatManager.flowAiChatHelpers?.revertToSnapshot(message.snapshot)
|
||||
}
|
||||
}}
|
||||
title="Revert to snapshot"
|
||||
startIcon={{ icon: Undo2Icon }}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<AIChatMessage
|
||||
{message}
|
||||
{messageIndex}
|
||||
{availableContext}
|
||||
bind:selectedContext
|
||||
bind:editingMessageIndex
|
||||
/>
|
||||
{/each}
|
||||
{#if aiChatManager.loading && !aiChatManager.currentReply}
|
||||
<div class="mb-6 py-1 px-2">
|
||||
@@ -323,71 +220,14 @@
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if aiChatManager.mode === 'script'}
|
||||
<div class="flex flex-row gap-1 mb-1 overflow-scroll pt-2 px-2 no-scrollbar">
|
||||
<Popover>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class="border rounded-md px-1 py-0.5 font-normal text-tertiary text-xs hover:bg-surface-hover"
|
||||
>@</div
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<AvailableContextList
|
||||
{availableContext}
|
||||
{selectedContext}
|
||||
onSelect={(element) => {
|
||||
addContextToSelection(element)
|
||||
close()
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{#each selectedContext as element}
|
||||
<ContextElementBadge
|
||||
contextElement={element}
|
||||
deletable
|
||||
on:delete={() => {
|
||||
selectedContext = selectedContext?.filter(
|
||||
(c) => c.type !== element.type || c.title !== element.title
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<ContextTextarea
|
||||
bind:this={contextTextareaComponent}
|
||||
{availableContext}
|
||||
{selectedContext}
|
||||
isFirstMessage={messages.length === 0}
|
||||
onAddContext={(contextElement) => addContextToSelection(contextElement)}
|
||||
onSendRequest={() => {
|
||||
if (!aiChatManager.loading) {
|
||||
aiChatManager.sendRequest()
|
||||
}
|
||||
}}
|
||||
onUpdateInstructions={(value) => (aiChatManager.instructions = value)}
|
||||
{disabled}
|
||||
/>
|
||||
{:else}
|
||||
<div class="relative w-full px-2 scroll-pb-2 pt-2">
|
||||
<textarea
|
||||
bind:this={instructionsTextarea}
|
||||
bind:value={aiChatManager.instructions}
|
||||
use:autosize
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !aiChatManager.loading) {
|
||||
e.preventDefault()
|
||||
aiChatManager.sendRequest()
|
||||
}
|
||||
}}
|
||||
rows={3}
|
||||
placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
|
||||
class="resize-none"
|
||||
{disabled}
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
<AIChatInput
|
||||
bind:this={aiChatInput}
|
||||
bind:selectedContext
|
||||
{availableContext}
|
||||
{disabled}
|
||||
isFirstMessage={messages.length === 0}
|
||||
placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
|
||||
/>
|
||||
<div
|
||||
class={`flex flex-row ${
|
||||
aiChatManager.mode === 'script' && hasDiff ? 'justify-between' : 'justify-end'
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<script lang="ts">
|
||||
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()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div use:clickOutside>
|
||||
{#if aiChatManager.mode === 'script'}
|
||||
<div class="flex flex-row gap-1 mb-1 overflow-scroll pt-2 px-2 no-scrollbar">
|
||||
<Popover>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class="border rounded-md px-1 py-0.5 font-normal text-tertiary text-xs hover:bg-surface-hover"
|
||||
>@</div
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<AvailableContextList
|
||||
{availableContext}
|
||||
{selectedContext}
|
||||
onSelect={(element) => {
|
||||
addContextToSelection(element)
|
||||
close()
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{#each selectedContext as element}
|
||||
<ContextElementBadge
|
||||
contextElement={element}
|
||||
deletable
|
||||
on:delete={() => {
|
||||
selectedContext = selectedContext?.filter(
|
||||
(c) => c.type !== element.type || c.title !== element.title
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<ContextTextarea
|
||||
bind:this={contextTextareaComponent}
|
||||
bind:value={instructions}
|
||||
{availableContext}
|
||||
{selectedContext}
|
||||
{isFirstMessage}
|
||||
{placeholder}
|
||||
onAddContext={(contextElement) => addContextToSelection(contextElement)}
|
||||
onSendRequest={() => {
|
||||
sendRequest()
|
||||
}}
|
||||
{disabled}
|
||||
onEscape={onEditEnd}
|
||||
/>
|
||||
{:else}
|
||||
<div class="relative w-full px-2 scroll-pb-2 pt-2">
|
||||
<textarea
|
||||
bind:this={instructionsTextareaComponent}
|
||||
bind:value={instructions}
|
||||
use:autosize
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
sendRequest()
|
||||
} else if (e.key === 'Escape') {
|
||||
onEditEnd()
|
||||
}
|
||||
}}
|
||||
rows={3}
|
||||
{placeholder}
|
||||
class="resize-none"
|
||||
{disabled}
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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 ?? {},
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { DisplayMessage } from './shared'
|
||||
import ContextElementBadge from './ContextElementBadge.svelte'
|
||||
import AssistantMessage from './AssistantMessage.svelte'
|
||||
import { aiChatManager } from './AIChatManager.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { RefreshCwIcon, Undo2Icon } from 'lucide-svelte'
|
||||
import AIChatInput from './AIChatInput.svelte'
|
||||
import type { ContextElement } from './context'
|
||||
|
||||
interface Props {
|
||||
availableContext: ContextElement[]
|
||||
selectedContext: ContextElement[]
|
||||
message: DisplayMessage
|
||||
messageIndex: number
|
||||
editingMessageIndex: number | null
|
||||
}
|
||||
|
||||
let {
|
||||
message,
|
||||
messageIndex,
|
||||
availableContext,
|
||||
selectedContext = $bindable(),
|
||||
editingMessageIndex = $bindable(null)
|
||||
}: Props = $props()
|
||||
|
||||
function editMessage() {
|
||||
if (message.role !== 'user' || editingMessageIndex !== null || aiChatManager.loading) {
|
||||
return
|
||||
}
|
||||
editingMessageIndex = messageIndex
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={twMerge(
|
||||
message.role === 'user' && messageIndex > 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}
|
||||
<div class="flex flex-row gap-1 mb-1 overflow-scroll no-scrollbar px-2">
|
||||
{#each message.contextElements as element}
|
||||
<ContextElementBadge contextElement={element} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if message.role === 'user' && editingMessageIndex === messageIndex}
|
||||
<AIChatInput
|
||||
{availableContext}
|
||||
bind:selectedContext
|
||||
initialInstructions={message.content}
|
||||
{editingMessageIndex}
|
||||
onEditEnd={() => (editingMessageIndex = null)}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-sm py-1 mx-2',
|
||||
message.role === 'user' &&
|
||||
'px-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 rounded-lg relative group',
|
||||
(message.role === 'assistant' || message.role === 'tool') && 'px-[1px]',
|
||||
message.role === 'tool' && 'text-tertiary'
|
||||
)}
|
||||
>
|
||||
{#if message.role === 'assistant'}
|
||||
<AssistantMessage {message} />
|
||||
{:else}
|
||||
{message.content}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if message.role === 'user' && message.snapshot}
|
||||
<div class="mx-2 text-sm text-tertiary flex flex-row items-center justify-between gap-2 mt-2">
|
||||
Saved a flow snapshot
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
if (message.snapshot) {
|
||||
aiChatManager.flowAiChatHelpers?.revertToSnapshot(message.snapshot)
|
||||
}
|
||||
}}
|
||||
title="Revert to snapshot"
|
||||
startIcon={{ icon: Undo2Icon }}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if message.role === 'user' && message.error}
|
||||
<div class="flex justify-end px-2 -mt-1">
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
title="Retry generation"
|
||||
color="light"
|
||||
startIcon={{ icon: RefreshCwIcon }}
|
||||
onclick={() => aiChatManager.retryRequest(messageIndex)}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import AiChat from './AIChat.svelte'
|
||||
import { zIndexes } from '$lib/zIndexes'
|
||||
import { loadCopilot, userStore, workspaceStore } from '$lib/stores'
|
||||
import { chatState } from './sharedChatState.svelte'
|
||||
|
||||
interface Props {
|
||||
noPadding?: boolean
|
||||
isCollapsed?: boolean
|
||||
children: any
|
||||
onMenuOpen?: () => void
|
||||
disableAi?: boolean
|
||||
}
|
||||
let {
|
||||
noPadding: noBorder = false,
|
||||
isCollapsed = false,
|
||||
children,
|
||||
onMenuOpen,
|
||||
disableAi
|
||||
}: Props = $props()
|
||||
|
||||
$effect(() => {
|
||||
if (disableAi) {
|
||||
chatState.size = 0
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if ($workspaceStore && !disableAi) {
|
||||
loadCopilot($workspaceStore)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if !disableAi}
|
||||
<Splitpanes horizontal={false} class="flex-1 min-h-0">
|
||||
<Pane size={99.8 - chatState.size} minSize={50} class="flex flex-col min-h-0">
|
||||
<div
|
||||
id="content"
|
||||
class={classNames(
|
||||
'w-full flex-1 flex flex-col overflow-y-auto',
|
||||
noBorder || $userStore?.operator ? '!pl-0' : isCollapsed ? 'md:pl-12' : 'md:pl-40',
|
||||
'transition-all ease-in-out duration-200'
|
||||
)}
|
||||
>
|
||||
<main class="flex-1 flex flex-col">
|
||||
<div class="relative w-full flex-1 flex flex-col">
|
||||
<div
|
||||
class={classNames(
|
||||
'pt-2 px-4 sm:px-4 flex flex-row justify-between items-center shadow-sm max-w-7xl md:hidden',
|
||||
noBorder || $userStore?.operator ? 'hidden' : ''
|
||||
)}
|
||||
>
|
||||
<button
|
||||
aria-label="Menu"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
onMenuOpen?.()
|
||||
}}
|
||||
class="h-8 w-8 inline-flex items-center justify-center rounded-md text-tertiary hover:text-primary focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane
|
||||
bind:size={chatState.size}
|
||||
minSize={15}
|
||||
class={`flex flex-col min-h-0 z-[${zIndexes.aiChat}]`}
|
||||
>
|
||||
<AiChat />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{:else}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
@@ -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 @@
|
||||
<div class="relative w-full px-2 scroll-pb-2">
|
||||
<div class="textarea-input absolute top-0 left-0 pointer-events-none">
|
||||
<span class="break-words">
|
||||
{@html getHighlightedText(aiChatManager.instructions)}
|
||||
{@html getHighlightedText(value)}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
bind:this={textarea}
|
||||
onkeypress={handleKeyPress}
|
||||
onkeydown={handleKeyDown}
|
||||
bind:value={aiChatManager.instructions}
|
||||
bind:value
|
||||
use:autosize
|
||||
rows={3}
|
||||
oninput={handleInput}
|
||||
@@ -353,11 +355,9 @@
|
||||
showContextTooltip = false
|
||||
}, 200)
|
||||
}}
|
||||
placeholder={isFirstMessage ? 'Ask anything' : 'Ask followup'}
|
||||
{placeholder}
|
||||
class="textarea-input resize-none bg-transparent caret-black dark:caret-white"
|
||||
style={aiChatManager.instructions.length > 0
|
||||
? 'color: transparent; -webkit-text-fill-color: transparent;'
|
||||
: ''}
|
||||
style={value.length > 0 ? 'color: transparent; -webkit-text-fill-color: transparent;' : ''}
|
||||
{disabled}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<T>({
|
||||
tools,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<string, any>
|
||||
loading?: boolean
|
||||
flowStore: StateStore<OpenFlow>
|
||||
flowStateStore: Writable<FlowState>
|
||||
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<string, stepState>
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Drawer from '../common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '../common/drawer/DrawerContent.svelte'
|
||||
|
||||
import FlowHistoryInner from './FlowHistoryInner.svelte'
|
||||
|
||||
export let path: string
|
||||
let drawer: Drawer
|
||||
|
||||
export function open() {
|
||||
drawer.openDrawer()
|
||||
interface Props {
|
||||
path: string
|
||||
onHistoryRestore?: () => void
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let { path, onHistoryRestore }: Props = $props()
|
||||
let drawer: Drawer | undefined = $state()
|
||||
|
||||
export function open() {
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size="1200px">
|
||||
@@ -25,9 +27,9 @@
|
||||
>
|
||||
<FlowHistoryInner
|
||||
allowFork
|
||||
on:historyRestore={() => {
|
||||
drawer.closeDrawer()
|
||||
dispatch('historyRestore')
|
||||
onHistoryRestore={() => {
|
||||
drawer?.closeDrawer()
|
||||
onHistoryRestore?.()
|
||||
}}
|
||||
{path}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { run, stopPropagation, createBubbler } from 'svelte/legacy'
|
||||
|
||||
const bubble = createBubbler()
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { classNames, displayDate, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { type Flow, FlowService, type FlowVersion } from '$lib/gen'
|
||||
@@ -6,18 +9,22 @@
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { ArrowRight, Loader2, Pencil, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let path: string
|
||||
export let allowFork: boolean = false
|
||||
let loading: boolean = false
|
||||
interface Props {
|
||||
path: string
|
||||
allowFork?: boolean
|
||||
onHistoryRestore?: () => void
|
||||
}
|
||||
|
||||
let versions: FlowVersion[] = []
|
||||
let { path, allowFork = false, onHistoryRestore }: Props = $props()
|
||||
let loading: boolean = $state(false)
|
||||
|
||||
let selectedVersion: FlowVersion | undefined = undefined
|
||||
let selected: Flow | undefined = undefined
|
||||
let deploymentMsgUpdateMode = false
|
||||
let deploymentMsgUpdate: string | undefined = undefined
|
||||
let versions: FlowVersion[] = $state([])
|
||||
|
||||
let selectedVersion: FlowVersion | undefined = $state(undefined)
|
||||
let selected: Flow | undefined = $state(undefined)
|
||||
let deploymentMsgUpdateMode = $state(false)
|
||||
let deploymentMsgUpdate: string | undefined = $state(undefined)
|
||||
|
||||
async function loadFlow(version: number) {
|
||||
selected = await FlowService.getFlowVersion({
|
||||
@@ -57,8 +64,6 @@
|
||||
loadVersions()
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function restoreVersion(flow: Flow | undefined) {
|
||||
if (!flow) return
|
||||
await FlowService.updateFlow({
|
||||
@@ -69,13 +74,15 @@
|
||||
},
|
||||
path
|
||||
})
|
||||
dispatch('historyRestore')
|
||||
onHistoryRestore?.()
|
||||
sendUserToast('Flow restored from previous deployment')
|
||||
}
|
||||
|
||||
loadVersions()
|
||||
|
||||
$: selectedVersion !== undefined && loadFlow(selectedVersion.id)
|
||||
run(() => {
|
||||
selectedVersion !== undefined && loadFlow(selectedVersion.id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Splitpanes class="!overflow-visible">
|
||||
@@ -85,7 +92,7 @@
|
||||
{#if versions.length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each versions ?? [] as version}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class={classNames(
|
||||
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-surface-hover hover:text-primary',
|
||||
@@ -93,7 +100,7 @@
|
||||
)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={() => {
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -163,7 +171,7 @@
|
||||
Deployed {displayDate(selected.edited_at)} by {selected.edited_by}
|
||||
{/if}
|
||||
<button
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
deploymentMsgUpdate = selectedVersion?.deployment_msg
|
||||
deploymentMsgUpdateMode = true
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script lang="ts" module>
|
||||
let cachedValues: Record<
|
||||
string,
|
||||
{
|
||||
latestHash: string | undefined
|
||||
}
|
||||
> = {}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
@@ -7,28 +16,55 @@
|
||||
import { ScriptService, type FlowModuleValue, type PathScript } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Lock, RefreshCw, Unlock } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
|
||||
export let flowModuleValue: FlowModuleValue | undefined = undefined
|
||||
export let title: string | undefined = undefined
|
||||
export let summary: string | undefined = undefined
|
||||
interface Props {
|
||||
flowModuleValue?: FlowModuleValue | undefined
|
||||
title?: string | undefined
|
||||
summary?: string | undefined
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
flowModuleValue = undefined,
|
||||
title = undefined,
|
||||
summary = $bindable(undefined),
|
||||
children
|
||||
}: Props = $props()
|
||||
|
||||
let latestHash: string | undefined = $state(undefined)
|
||||
function getCachedKey(path: string) {
|
||||
return `${$workspaceStore}-${path}`
|
||||
}
|
||||
function getCachedValues(path: string) {
|
||||
const key = getCachedKey(path)
|
||||
latestHash = cachedValues[key]?.latestHash
|
||||
}
|
||||
if (flowModuleValue?.type === 'script' && flowModuleValue.path) {
|
||||
getCachedValues(flowModuleValue.path)
|
||||
}
|
||||
|
||||
let latestHash: string | undefined = undefined
|
||||
async function loadLatestHash(value: PathScript) {
|
||||
let script = await ScriptService.getScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: value.path
|
||||
})
|
||||
const key = getCachedKey(value.path)
|
||||
cachedValues[key] = {
|
||||
latestHash: script.hash
|
||||
}
|
||||
latestHash = script.hash
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: $workspaceStore &&
|
||||
flowModuleValue?.type === 'script' &&
|
||||
flowModuleValue.path &&
|
||||
!flowModuleValue.path.startsWith('hub/') &&
|
||||
loadLatestHash(flowModuleValue)
|
||||
$effect.pre(() => {
|
||||
$workspaceStore &&
|
||||
flowModuleValue?.type === 'script' &&
|
||||
flowModuleValue.path &&
|
||||
!flowModuleValue.path.startsWith('hub/') &&
|
||||
untrack(() => loadLatestHash(flowModuleValue))
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -114,5 +150,5 @@
|
||||
{#if title}
|
||||
<div class="text-sm font-bold text-primary pr-2">{title}</div>
|
||||
{/if}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</script>
|
||||
|
||||
{#if $selectedId?.startsWith('settings')}
|
||||
<FlowSettings {noEditor} />
|
||||
<FlowSettings {enableAi} {noEditor} />
|
||||
{:else if $selectedId === 'Input'}
|
||||
<FlowInput
|
||||
{noEditor}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { createScriptFromInlineScript, fork } from '$lib/components/flows/flowStateUtils.svelte'
|
||||
|
||||
import type { FlowModule, RawScript } from '$lib/gen'
|
||||
import type { FlowModule, RawScript, ScriptLang } from '$lib/gen'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import FlowModuleHeader from './FlowModuleHeader.svelte'
|
||||
import { getLatestHashForScript, scriptLangToEditorLang } from '$lib/scripts'
|
||||
@@ -99,7 +99,8 @@
|
||||
highlightArg = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let tag: string | undefined = $state(undefined)
|
||||
let workspaceScriptTag: string | undefined = $state(undefined)
|
||||
let workspaceScriptLang: ScriptLang | undefined = $state(undefined)
|
||||
let diffMode = $state(false)
|
||||
|
||||
let editor: Editor | undefined = $state()
|
||||
@@ -247,7 +248,7 @@
|
||||
)
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
$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
|
||||
)
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
@@ -333,7 +338,7 @@
|
||||
>
|
||||
{#snippet header()}
|
||||
<FlowModuleHeader
|
||||
{tag}
|
||||
tag={workspaceScriptTag ?? rawScriptLang ?? workspaceScriptLang}
|
||||
module={flowModule}
|
||||
on:tagChange={(e) => {
|
||||
console.log('tagChange', e.detail)
|
||||
@@ -491,7 +496,8 @@
|
||||
<div class="border-t">
|
||||
{#key forceReload}
|
||||
<FlowModuleScript
|
||||
bind:tag
|
||||
bind:tag={workspaceScriptTag}
|
||||
bind:language={workspaceScriptLang}
|
||||
showAllCode={false}
|
||||
path={flowModule.value.path}
|
||||
hash={flowModule.value.hash}
|
||||
|
||||
@@ -147,6 +147,8 @@
|
||||
{/if}
|
||||
{#if customUi?.tagEdit != false}
|
||||
<FlowModuleWorkerTagSelect
|
||||
placeholder={customUi?.tagSelectPlaceholder}
|
||||
noLabel={customUi?.tagSelectNoLabel}
|
||||
nullTag={tag}
|
||||
tag={module.value.tag_override}
|
||||
on:change={(e) => dispatch('tagChange', e.detail)}
|
||||
@@ -192,6 +194,9 @@
|
||||
<div class="px-0.5"></div>
|
||||
{#if module.value.type === 'rawscript'}
|
||||
<FlowModuleWorkerTagSelect
|
||||
placeholder={customUi?.tagSelectPlaceholder}
|
||||
noLabel={customUi?.tagSelectNoLabel}
|
||||
nullTag={tag}
|
||||
tag={module.value.tag}
|
||||
on:change={(e) => dispatch('tagChange', e.detail)}
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,92 @@
|
||||
<script lang="ts" module>
|
||||
let cachedValues: Record<
|
||||
string,
|
||||
{
|
||||
code: string | undefined
|
||||
previousCode: string | undefined
|
||||
language: ScriptLang | undefined
|
||||
lock: string | undefined
|
||||
date: string | undefined
|
||||
notFound: boolean
|
||||
tag: string | undefined
|
||||
}
|
||||
> = {}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import TimeAgo from '$lib/components/TimeAgo.svelte'
|
||||
import { ScriptService } from '$lib/gen'
|
||||
import { ScriptService, type ScriptLang } from '$lib/gen'
|
||||
import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { untrack } from 'svelte'
|
||||
|
||||
export let path: string
|
||||
export let hash: string | undefined = undefined
|
||||
export let previousHash: string | undefined = undefined
|
||||
export let showDate = false
|
||||
export let showAllCode: boolean = true
|
||||
export let tag: string | undefined = undefined
|
||||
interface Props {
|
||||
path: string
|
||||
hash?: string | undefined
|
||||
previousHash?: string | undefined
|
||||
showDate?: boolean
|
||||
showAllCode?: boolean
|
||||
tag?: string | undefined
|
||||
language?: ScriptLang | undefined
|
||||
showDiff?: boolean
|
||||
}
|
||||
|
||||
let code: string
|
||||
let previousCode: string
|
||||
let language: SupportedLanguage
|
||||
let lock: string | undefined = undefined
|
||||
let date: string | undefined = undefined
|
||||
let notFound = false
|
||||
let {
|
||||
path,
|
||||
hash = undefined,
|
||||
previousHash = undefined,
|
||||
showDate = false,
|
||||
showAllCode = $bindable(true),
|
||||
tag = $bindable(undefined),
|
||||
showDiff = false,
|
||||
language = $bindable(undefined)
|
||||
}: Props = $props()
|
||||
|
||||
let code: string | undefined = $state()
|
||||
let previousCode: string | undefined = $state()
|
||||
let lock: string | undefined = $state(undefined)
|
||||
let date: string | undefined = $state(undefined)
|
||||
let notFound = $state(false)
|
||||
|
||||
function getCachedKey(path: string, hash: string | undefined) {
|
||||
return `${$workspaceStore}-${path}-${hash ?? ''}`
|
||||
}
|
||||
function getCachedValues(path: string, hash: string | undefined) {
|
||||
const key = getCachedKey(path, hash)
|
||||
code = cachedValues[key]?.code
|
||||
language = cachedValues[key]?.language
|
||||
lock = cachedValues[key]?.lock
|
||||
date = cachedValues[key]?.date
|
||||
previousCode = cachedValues[key]?.previousCode
|
||||
tag = cachedValues[key]?.tag
|
||||
notFound = cachedValues[key]?.notFound ?? false
|
||||
console.log('cachedValues', cachedValues, code)
|
||||
}
|
||||
|
||||
getCachedValues(path, hash)
|
||||
|
||||
async function loadPreviousCode(previousHash: string) {
|
||||
try {
|
||||
const previousScript = await ScriptService.getScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
hash: previousHash
|
||||
})
|
||||
previousCode = previousScript.content
|
||||
const key = getCachedKey(path, previousHash)
|
||||
cachedValues[key] = {
|
||||
...(cachedValues[key] ?? {}),
|
||||
previousCode: previousScript.content
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShowAll() {
|
||||
showAllCode = !showAllCode
|
||||
}
|
||||
|
||||
async function loadCode(path: string, hash: string | undefined) {
|
||||
try {
|
||||
@@ -33,32 +100,28 @@
|
||||
lock = script.lock
|
||||
date = script.created_at
|
||||
tag = script.tag
|
||||
const key = getCachedKey(path, hash)
|
||||
cachedValues[key] = {
|
||||
...(cachedValues[key] ?? {}),
|
||||
code: script.content,
|
||||
language: script.language,
|
||||
lock: script.lock,
|
||||
date: script.created_at,
|
||||
tag: script.tag
|
||||
}
|
||||
} catch (e) {
|
||||
notFound = true
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreviousCode(previousHash: string) {
|
||||
try {
|
||||
const previousScript = await ScriptService.getScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
hash: previousHash
|
||||
})
|
||||
previousCode = previousScript.content
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
$: path && loadCode(path, hash)
|
||||
$: path && previousHash && loadPreviousCode(previousHash)
|
||||
|
||||
function toggleShowAll() {
|
||||
showAllCode = !showAllCode
|
||||
}
|
||||
|
||||
export let showDiff: boolean = false
|
||||
$effect.pre(() => {
|
||||
hash
|
||||
path && untrack(() => loadCode(path, hash))
|
||||
})
|
||||
$effect.pre(() => {
|
||||
path && previousHash && untrack(() => loadPreviousCode(previousHash))
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col flex-1 h-full overflow-auto p-2">
|
||||
@@ -72,7 +135,7 @@
|
||||
<div class="text-red-400">script not found at {path} in workspace {$workspaceStore}</div>
|
||||
{:else if showAllCode}
|
||||
{#if showDiff}
|
||||
{#key previousCode + code}
|
||||
{#key (previousCode ?? '') + (code ?? '')}
|
||||
{#await import('$lib/components/DiffEditor.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
@@ -94,7 +157,7 @@
|
||||
<div class="code-container h-full">
|
||||
<HighlightCode {language} code={code?.split('\n').slice(0, 10).join('\n')} />
|
||||
</div>
|
||||
<button on:click={toggleShowAll}>Show all</button>
|
||||
<button onclick={toggleShowAll}>Show all</button>
|
||||
{/if}
|
||||
|
||||
{#if lock}
|
||||
|
||||
@@ -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>('FlowEditorContext')
|
||||
@@ -26,7 +33,13 @@
|
||||
{#if $workerTags?.length > 0}
|
||||
<div class="w-40">
|
||||
{#if flowStore.val.tag == undefined}
|
||||
<WorkerTagSelect {nullTag} bind:tag on:change={(e) => dispatch('change', e.detail)} />
|
||||
<WorkerTagSelect
|
||||
{noLabel}
|
||||
{placeholder}
|
||||
{nullTag}
|
||||
bind:tag
|
||||
on:change={(e) => dispatch('change', e.detail)}
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
title="Worker Group is defined at the flow level"
|
||||
|
||||
@@ -25,9 +25,10 @@
|
||||
|
||||
interface Props {
|
||||
noEditor: boolean
|
||||
enableAi?: boolean
|
||||
}
|
||||
|
||||
let { noEditor }: Props = $props()
|
||||
let { noEditor, enableAi }: Props = $props()
|
||||
|
||||
const { flowStore, initialPathStore, previewArgs, pathStore, customUi } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
@@ -125,7 +126,7 @@
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{#if flowStore.val.schema}
|
||||
{#if flowStore.val.schema && enableAi}
|
||||
<AIFormSettings
|
||||
bind:prompt={flowStore.val.schema.prompt_for_ai as string | undefined}
|
||||
type="flow"
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import type { Flow, OpenFlow } from '$lib/gen'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { initFlowState, type FlowState } from './flowState'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
|
||||
export type FlowMode = 'push' | 'pull'
|
||||
|
||||
export const importFlowStore = writable<Flow | undefined>(undefined)
|
||||
|
||||
export async function initFlow(
|
||||
flow: Flow,
|
||||
flowStore: StateStore<Flow>,
|
||||
flowStateStore: Writable<FlowState>
|
||||
) {
|
||||
await initFlowState(flow, flowStateStore)
|
||||
flowStore.val = flow
|
||||
}
|
||||
|
||||
export async function copyFirstStepSchema(
|
||||
flowState: FlowState,
|
||||
flowStore: StateStore<OpenFlow>
|
||||
): Promise<void> {
|
||||
const firstModuleId = flowStore.val.value.modules[0]?.id
|
||||
|
||||
if (flowState[firstModuleId] && firstModuleId) {
|
||||
flowStore.val.schema = structuredClone($state.snapshot(flowState[firstModuleId].schema))
|
||||
const v = flowStore.val.value.modules[0].value
|
||||
if (v.type == 'rawscript' || v.type == 'script') {
|
||||
Object.keys(v.input_transforms ?? {}).forEach((key) => {
|
||||
v.input_transforms[key] = {
|
||||
type: 'javascript',
|
||||
expr: `flow_input.${key}`
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
return sendUserToast('Only scripts can be used as a input schema', true)
|
||||
}
|
||||
return sendUserToast('No first step found', true)
|
||||
}
|
||||
|
||||
export async function getFirstStepSchema(flowState: FlowState, flow: OpenFlow) {
|
||||
const firstModuleId = flow.value.modules[0]?.id
|
||||
|
||||
if (!firstModuleId || !flowState[firstModuleId]) {
|
||||
throw new Error('no first step found')
|
||||
}
|
||||
|
||||
const schema = structuredClone(flowState[firstModuleId].schema)
|
||||
const v = flow.value.modules[0].value
|
||||
|
||||
if (v.type !== 'rawscript' && v.type !== 'script') {
|
||||
throw new Error('only scripts can be used as a input schema')
|
||||
}
|
||||
|
||||
const simplifiedModule = {
|
||||
id: flow.value.modules[0].id,
|
||||
summary: flow.value.modules[0].summary,
|
||||
value: {
|
||||
type: flow.value.modules[0].value.type,
|
||||
...('path' in flow.value.modules[0].value ? { path: flow.value.modules[0].value.path } : {}),
|
||||
...('language' in flow.value.modules[0].value
|
||||
? { language: flow.value.modules[0].value.language }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
schema,
|
||||
mod: simplifiedModule,
|
||||
connectFirstNode: () => {
|
||||
Object.keys(v.input_transforms ?? {}).forEach((key) => {
|
||||
v.input_transforms[key] = {
|
||||
type: 'javascript',
|
||||
expr: `flow_input.${key}`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceId(expr: string, id: string, newId: string): string {
|
||||
return expr
|
||||
.replaceAll(`results.${id}`, `results.${newId}`)
|
||||
.replaceAll(`results?.${id}`, `results?.${newId}`)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { writable, type Writable } from 'svelte/store'
|
||||
import { initFlowState, type FlowState } from './flowState'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
|
||||
|
||||
export type FlowMode = 'push' | 'pull'
|
||||
|
||||
@@ -18,39 +17,37 @@ export async function initFlow(
|
||||
flowStore.val = flow
|
||||
}
|
||||
|
||||
export async function copyFirstStepSchema(flowState: FlowState, flowStore: Writable<OpenFlow>) {
|
||||
flowStore.update((flow) => {
|
||||
const firstModuleId = flow.value.modules[0]?.id
|
||||
export async function copyFirstStepSchema(
|
||||
flowState: FlowState,
|
||||
flowStore: StateStore<OpenFlow>
|
||||
): Promise<void> {
|
||||
const firstModuleId = flowStore.val.value.modules[0]?.id
|
||||
|
||||
if (flowState[firstModuleId] && firstModuleId) {
|
||||
flow.schema = structuredClone(stateSnapshot(flowState[firstModuleId].schema))
|
||||
const v = flow.value.modules[0].value
|
||||
if (v.type == 'rawscript' || v.type == 'script') {
|
||||
Object.keys(v.input_transforms ?? {}).forEach((key) => {
|
||||
v.input_transforms[key] = {
|
||||
type: 'javascript',
|
||||
expr: `flow_input.${key}`
|
||||
}
|
||||
})
|
||||
return flow
|
||||
}
|
||||
sendUserToast('Only scripts can be used as a input schema', true)
|
||||
return flow
|
||||
if (flowState[firstModuleId] && firstModuleId) {
|
||||
flowStore.val.schema = structuredClone($state.snapshot(flowState[firstModuleId].schema))
|
||||
const v = flowStore.val.value.modules[0].value
|
||||
if (v.type == 'rawscript' || v.type == 'script') {
|
||||
Object.keys(v.input_transforms ?? {}).forEach((key) => {
|
||||
v.input_transforms[key] = {
|
||||
type: 'javascript',
|
||||
expr: `flow_input.${key}`
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
sendUserToast('No first step found', true)
|
||||
return flow
|
||||
})
|
||||
return sendUserToast('Only scripts can be used as a input schema', true)
|
||||
}
|
||||
return sendUserToast('No first step found', true)
|
||||
}
|
||||
|
||||
export async function getFirstStepSchema(flowState: FlowState, flowStore: StateStore<OpenFlow>) {
|
||||
const flow = flowStore.val
|
||||
export async function getFirstStepSchema(flowState: FlowState, flow: OpenFlow) {
|
||||
const firstModuleId = flow.value.modules[0]?.id
|
||||
|
||||
if (!firstModuleId || !flowState[firstModuleId]) {
|
||||
throw new Error('no first step found')
|
||||
}
|
||||
|
||||
const schema = structuredClone(stateSnapshot(flowState[firstModuleId].schema))
|
||||
const schema = structuredClone(flowState[firstModuleId].schema)
|
||||
const v = flow.value.modules[0].value
|
||||
|
||||
if (v.type !== 'rawscript' && v.type !== 'script') {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
import { Drawer } from '$lib/components/common'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import { getDependeeAndDependentComponents } from '../flowExplorer'
|
||||
import { replaceId } from '../flowStore.svelte'
|
||||
import { replaceId } from '../flowStore'
|
||||
import FlowModuleSchemaItemViewer from './FlowModuleSchemaItemViewer.svelte'
|
||||
import type { PropPickerContext } from '$lib/components/prop_picker'
|
||||
import OutputPicker from '$lib/components/flows/propPicker/OutputPicker.svelte'
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
import { ignoredTutorials } from '$lib/components/tutorials/ignoredTutorials'
|
||||
import { tutorialInProgress } from '$lib/tutorialUtils'
|
||||
import FlowGraphV2 from '$lib/components/graph/FlowGraphV2.svelte'
|
||||
import { replaceId } from '../flowStore.svelte'
|
||||
import { replaceId } from '../flowStore'
|
||||
import { setScheduledPollSchedule, type TriggerContext } from '$lib/components/triggers'
|
||||
import type { PropPickerContext } from '$lib/components/prop_picker'
|
||||
import { JobService } from '$lib/gen'
|
||||
|
||||
@@ -388,8 +388,7 @@
|
||||
let moduleCounter = $state(0)
|
||||
function onModulesChange2(modules) {
|
||||
if (!deepEqual(modules, lastModules)) {
|
||||
console.log('modules changed', modules)
|
||||
lastModules = structuredClone($state.snapshot(modules))
|
||||
lastModules = $state.snapshot(modules)
|
||||
moduleCounter++
|
||||
}
|
||||
}
|
||||
@@ -416,6 +415,7 @@
|
||||
return
|
||||
}
|
||||
let newGraph = graph
|
||||
newGraph.nodes.sort((a, b) => b.id.localeCompare(a.id))
|
||||
;[nodes, edges] = computeAssetNodes(layoutNodes(newGraph.nodes), newGraph.edges, assetsMap, {
|
||||
moving,
|
||||
eventHandlers: eventHandler,
|
||||
|
||||
@@ -659,8 +659,8 @@
|
||||
<UnsavedConfirmationModal {diffDrawer} {getInitialAndModifiedValues} />
|
||||
|
||||
<DeployOverrideConfirmationModal
|
||||
bind:deployedBy
|
||||
bind:confirmCallback
|
||||
{deployedBy}
|
||||
{confirmCallback}
|
||||
bind:open
|
||||
{diffDrawer}
|
||||
bind:deployedValue
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { EditableSchemaWrapperProps } from './editable_schema_wrapper'
|
||||
import EditableSchemaWrapper from './EditableSchemaWrapper.svelte'
|
||||
|
||||
let {
|
||||
schema: oldSchema,
|
||||
...props
|
||||
}: EditableSchemaWrapperProps = $props()
|
||||
|
||||
let schema = $state(oldSchema)
|
||||
</script>
|
||||
|
||||
<EditableSchemaWrapper {schema} {...props} />
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import EditableSchemaForm from '../EditableSchemaForm.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import { emptySchema, validateFileExtension } from '$lib/utils'
|
||||
import { Alert } from '../common'
|
||||
@@ -10,29 +8,21 @@
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import Select from '../select/Select.svelte'
|
||||
import { safeSelectItems } from '../select/utils.svelte'
|
||||
|
||||
interface Props {
|
||||
schema: Schema | undefined | any
|
||||
uiOnly?: boolean
|
||||
noPreview?: boolean
|
||||
fullHeight?: boolean
|
||||
formatExtension?: string | undefined
|
||||
}
|
||||
import type { EditableSchemaWrapperProps } from './editable_schema_wrapper'
|
||||
|
||||
let {
|
||||
schema = $bindable(),
|
||||
uiOnly = false,
|
||||
noPreview = false,
|
||||
fullHeight = true,
|
||||
formatExtension = $bindable(undefined)
|
||||
}: Props = $props()
|
||||
formatExtension = $bindable(undefined),
|
||||
onSchemaChange
|
||||
}: EditableSchemaWrapperProps = $props()
|
||||
|
||||
let resourceIsTextFile: boolean = $state(false)
|
||||
let addPropertyComponent: AddPropertyV2 | undefined = $state(undefined)
|
||||
let editableSchemaForm: EditableSchemaForm | undefined = $state(undefined)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$effect(() => {
|
||||
if (!resourceIsTextFile && formatExtension !== undefined) {
|
||||
formatExtension = undefined
|
||||
@@ -60,7 +50,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch('change', schema)
|
||||
onSchemaChange?.({ schema: $state.snapshot(schema) })
|
||||
}
|
||||
|
||||
let suggestedFileExtensions = $state([
|
||||
@@ -89,7 +79,7 @@
|
||||
<AddPropertyV2
|
||||
bind:schema
|
||||
bind:this={addPropertyComponent}
|
||||
on:change={() => dispatch('change', schema)}
|
||||
on:change={() => onSchemaChange?.({ schema: $state.snapshot(schema) })}
|
||||
on:addNew={(e) => {
|
||||
editableSchemaForm?.openField(e.detail)
|
||||
}}
|
||||
@@ -107,7 +97,7 @@
|
||||
onlyMaskPassword
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema
|
||||
on:change={() => dispatch('change', schema)}
|
||||
on:change={() => onSchemaChange?.({ schema: $state.snapshot(schema) })}
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addPropertyComponent?.openDrawer(e.detail)
|
||||
@@ -124,7 +114,7 @@
|
||||
<AddPropertyV2
|
||||
bind:schema
|
||||
bind:this={addPropertyComponent}
|
||||
on:change={() => dispatch('change', schema)}
|
||||
on:change={() => onSchemaChange?.({ schema })}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
export type EditableSchemaWrapperProps = {
|
||||
schema: Schema | undefined | any
|
||||
uiOnly?: boolean
|
||||
noPreview?: boolean
|
||||
fullHeight?: boolean
|
||||
formatExtension?: string | undefined
|
||||
onSchemaChange?: ({ schema }: { schema: Schema }) => void
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { NewScript } from '$lib/gen'
|
||||
import type { AssetWithAccessType } from './assets/lib'
|
||||
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import type { DiffDrawerI } from './diff_drawer'
|
||||
import type { ScriptBuilderFunctionExports } from './scriptBuilder'
|
||||
import type { ScheduleTrigger } from './triggers'
|
||||
import type { NewScriptWithDraftAndDraftTriggers, Trigger } from './triggers/utils'
|
||||
|
||||
export interface ScriptBuilderProps {
|
||||
script: NewScript & {
|
||||
draft_triggers?: Trigger[]
|
||||
fallback_access_types?: AssetWithAccessType[]
|
||||
}
|
||||
disableAi?: boolean
|
||||
fullyLoaded?: boolean
|
||||
initialPath?: string
|
||||
template?: 'docker' | 'bunnative' | 'script'
|
||||
initialArgs?: Record<string, any>
|
||||
lockedLanguage?: boolean
|
||||
showMeta?: boolean
|
||||
neverShowMeta?: boolean
|
||||
diffDrawer?: DiffDrawerI | 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
|
||||
onDeploy?: (e: { path: string; hash: string }) => void
|
||||
onDeployError?: (e: { path: string; error: any }) => void
|
||||
onSaveInitial?: (e: { path: string; hash: string }) => void
|
||||
onHistoryRestore?: () => void
|
||||
onSaveDraftOnlyAtNewPath?: (e: { path: string }) => void
|
||||
onSaveDraft?: (e: { path: string; savedAtNewPath: boolean; script: NewScript }) => void
|
||||
onSeeDetails?: (e: { path: string }) => void
|
||||
onSaveDraftError?: (e: { path: string; error: any }) => void
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
Route,
|
||||
Search,
|
||||
SearchCode,
|
||||
Unplug
|
||||
Unplug,
|
||||
WandSparkles
|
||||
} from 'lucide-svelte'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
|
||||
@@ -237,7 +238,7 @@
|
||||
let opts: uFuzzy.Options = {}
|
||||
|
||||
let uf = new uFuzzy(opts)
|
||||
let defaultMenuItemLabels = defaultMenuItems.map((item) => item.label)
|
||||
// let defaultMenuItemLabels = defaultMenuItems.map((item) => item.label)
|
||||
let defaultMenuItemAndHiddenLabels = defaultMenuItemsWithHidden.map((item) => item.label)
|
||||
let switchModeItemLabels = switchModeItems.map((item) => item.label)
|
||||
let askAiButton: AskAiButton | undefined = $state()
|
||||
@@ -293,8 +294,7 @@
|
||||
}
|
||||
|
||||
if (tab === 'default') {
|
||||
if (searchTerm === '')
|
||||
itemMap['default'] = fuzzyFilter(searchTerm, defaultMenuItems, defaultMenuItemLabels)
|
||||
if (searchTerm === '') itemMap['default'] = defaultMenuItems
|
||||
else
|
||||
itemMap['default'] = fuzzyFilter(
|
||||
searchTerm,
|
||||
@@ -372,9 +372,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((itemMap[tab] ?? []).length === 0 && searchTerm.length > 0 && event.key === 'Enter') {
|
||||
askAiButton?.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,10 +650,14 @@
|
||||
<div class="overflow-y-auto relative {maxModalHeight(tab)}">
|
||||
{#if tab === 'default' || tab === 'switch-mode'}
|
||||
{@const items = (itemMap[tab] ?? []).filter((e) =>
|
||||
defaultMenuItemsWithHidden.includes(e)
|
||||
defaultMenuItemsWithHidden.some((x) => e.search_id === x.search_id)
|
||||
)}
|
||||
{#if items.length > 0}
|
||||
<div class={tab === 'switch-mode' ? 'p-2' : 'p-2 border-b'}>
|
||||
<div
|
||||
class={tab === 'switch-mode' || itemMap[tab].length === items.length
|
||||
? 'p-2'
|
||||
: 'p-2 border-b'}
|
||||
>
|
||||
{#each items as el}
|
||||
<QuickMenuItem
|
||||
onselect={(shift) => el?.action(shift)}
|
||||
@@ -674,8 +675,8 @@
|
||||
{/if}
|
||||
|
||||
{#if tab === 'default'}
|
||||
<div class="p-2">
|
||||
{#if (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)).length > 0}
|
||||
{#if (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)).length > 0}
|
||||
<div class="p-2">
|
||||
<div class="py-2 px-1 text-xs font-semibold text-tertiary">
|
||||
Flows/Scripts/Apps
|
||||
</div>
|
||||
@@ -694,19 +695,28 @@
|
||||
bind:mouseMoved
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if (itemMap[tab] ?? []).length === 0}
|
||||
{#if (itemMap[tab] ?? []).length === 0}
|
||||
<div class="p-2">
|
||||
<QuickMenuItem
|
||||
onselect={() => {
|
||||
askAiButton?.onClick()
|
||||
}}
|
||||
id={'ai:no-results-ask-ai'}
|
||||
hovered={true}
|
||||
label={`Try asking \`${searchTerm}\` to AI`}
|
||||
icon={WandSparkles}
|
||||
bind:mouseMoved
|
||||
/>
|
||||
<div class="flex w-full justify-center items-center">
|
||||
<div class="text-tertiary text-center">
|
||||
<div class="text-2xl font-bold"
|
||||
>Nothing found, ask the AI to find what you need!</div
|
||||
>
|
||||
<div class="text-sm">Tip: press `esc` to quickly clear the search bar</div>
|
||||
<div class="pt-1 text-sm">Tip: press `esc` to quickly clear the search bar</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if tab === 'content'}
|
||||
<ContentSearchInner
|
||||
search={removePrefix(searchTerm, '#')}
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
</div>
|
||||
{:else if clearable && !disabled && value}
|
||||
<div class="absolute z-10 right-2 h-full flex items-center">
|
||||
<CloseButton noBg small on:close={clearValue} />
|
||||
<CloseButton class="text-secondary" noBg small on:close={clearValue} />
|
||||
</div>
|
||||
{:else if RightIcon}
|
||||
<div class="absolute z-10 right-2 h-full flex items-center">
|
||||
|
||||
@@ -16,7 +16,7 @@ export function processItems<Item extends { label?: string; value: any }>({
|
||||
let items2 =
|
||||
items?.map((item) => ({
|
||||
...item,
|
||||
label: getLabel(item)
|
||||
label: getLabel(item) ?? ''
|
||||
})) ?? []
|
||||
if (groupBy) {
|
||||
items2 =
|
||||
@@ -46,12 +46,12 @@ export type ProcessedItem<T> = {
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
export function getLabel<T>(item: { label?: string; value: T } | undefined): string {
|
||||
export function getLabel<T>(item: { label?: string; value: T } | undefined): string | undefined {
|
||||
if (!item) return ''
|
||||
if (item.label) return item.label
|
||||
if (typeof item.value === 'string') return item.value
|
||||
if (typeof item.value == 'number' || typeof item.value == 'boolean') return item.value.toString()
|
||||
|
||||
if (item.value == null) { return undefined }
|
||||
return JSON.stringify(item.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import GcpTriggerEditorConfigSection from './GcpTriggerEditorConfigSection.svelte'
|
||||
import { base } from '$app/paths'
|
||||
import { untrack, type Snippet } from 'svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import { saveGcpTriggerFromCfg } from './utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
let drawer: Drawer | undefined = $state(undefined)
|
||||
let is_flow: boolean = $state(false)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { JobService, ScheduleService } from "$lib/gen"
|
||||
import { goto } from "$lib/navigation"
|
||||
import { sendUserToast } from "$lib/utils"
|
||||
|
||||
export async function runScheduleNow(
|
||||
@@ -25,7 +24,7 @@ export async function runScheduleNow(
|
||||
sendUserToast(`Schedule ${path} will run now`, false, [
|
||||
{
|
||||
label: 'Go to the run page',
|
||||
callback: () => goto('/run/' + run + '?workspace=' + workspace_id)
|
||||
callback: () => window.open('/run/' + run + '?workspace=' + workspace_id, '_blank')
|
||||
}
|
||||
])
|
||||
} catch (err) {
|
||||
|
||||
@@ -29,5 +29,5 @@ export async function clearUser() {
|
||||
try {
|
||||
clearStores()
|
||||
await UserService.logout()
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
WorkspaceService
|
||||
} from './gen'
|
||||
import { getLocalSetting } from './utils'
|
||||
import { workspaceAIClients } from './components/copilot/lib'
|
||||
|
||||
export interface UserExt {
|
||||
email: string
|
||||
@@ -106,6 +107,17 @@ export const copilotInfo = writable<{
|
||||
aiModels: []
|
||||
})
|
||||
|
||||
export async function loadCopilot(workspace: string) {
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
setCopilotInfo(info)
|
||||
} catch (err) {
|
||||
setCopilotInfo({})
|
||||
console.error('Could not get copilot info', err)
|
||||
}
|
||||
}
|
||||
|
||||
export function setCopilotInfo(aiConfig: AIConfig) {
|
||||
if (Object.keys(aiConfig.providers ?? {}).length > 0) {
|
||||
const aiModels = Object.entries(aiConfig.providers ?? {}).flatMap(
|
||||
@@ -162,9 +174,9 @@ const sessionProvider = getLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME)
|
||||
export const copilotSessionModel = writable<AIProviderModel | undefined>(
|
||||
sessionModel && sessionProvider
|
||||
? {
|
||||
model: sessionModel,
|
||||
provider: sessionProvider as AIProvider
|
||||
}
|
||||
model: sessionModel,
|
||||
provider: sessionProvider as AIProvider
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
export const usedTriggerKinds = writable<string[]>([])
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
// import { goto } from '$lib/navigation'
|
||||
// import { AppService, type Flow, FlowService, Script, ScriptService, type User } from '$lib/gen'
|
||||
// import { toast } from '@zerodevx/svelte-toast'
|
||||
// import type { Schema, SupportedLanguage } from './common'
|
||||
@@ -166,9 +165,9 @@ export function displayDate(
|
||||
}
|
||||
const dateChoices: Intl.DateTimeFormatOptions = displayDate
|
||||
? {
|
||||
day: 'numeric',
|
||||
month: 'numeric'
|
||||
}
|
||||
day: 'numeric',
|
||||
month: 'numeric'
|
||||
}
|
||||
: {}
|
||||
return date.toLocaleString(undefined, {
|
||||
...timeChoices,
|
||||
@@ -974,7 +973,7 @@ export async function tryEvery({
|
||||
try {
|
||||
await tryCode()
|
||||
break
|
||||
} catch (err) {}
|
||||
} catch (err) { }
|
||||
i++
|
||||
}
|
||||
if (i >= times) {
|
||||
@@ -1241,7 +1240,7 @@ export function conditionalMelt(node: HTMLElement, meltItem: AnyMeltElement | un
|
||||
if (meltItem) {
|
||||
return meltItem(node)
|
||||
}
|
||||
return { destroy: () => {} }
|
||||
return { destroy: () => { } }
|
||||
}
|
||||
|
||||
export type Item = {
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
hubBaseUrlStore,
|
||||
usedTriggerKinds,
|
||||
devopsRole,
|
||||
setCopilotInfo,
|
||||
whitelabelNameStore
|
||||
} from '$lib/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
@@ -45,7 +44,6 @@
|
||||
import { syncTutorialsTodos } from '$lib/tutorialUtils'
|
||||
import { ArrowLeft, Search, WandSparkles } from 'lucide-svelte'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { workspaceAIClients } from '$lib/components/copilot/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import OperatorMenu from '$lib/components/sidebar/OperatorMenu.svelte'
|
||||
import GlobalSearchModal from '$lib/components/search/GlobalSearchModal.svelte'
|
||||
@@ -54,10 +52,7 @@
|
||||
import { base } from '$app/paths'
|
||||
import { Menubar } from '$lib/components/meltComponents'
|
||||
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import AiChat from '$lib/components/copilot/chat/AIChat.svelte'
|
||||
import { zIndexes } from '$lib/zIndexes'
|
||||
import { chatState } from '$lib/components/copilot/chat/sharedChatState.svelte'
|
||||
import AiChatLayout from '$lib/components/copilot/chat/AiChatLayout.svelte'
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
@@ -259,24 +254,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let devOnly = $page.url.pathname.startsWith(base + '/scripts/dev')
|
||||
|
||||
async function loadCopilot(workspace: string) {
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
setCopilotInfo(info)
|
||||
} catch (err) {
|
||||
setCopilotInfo({})
|
||||
console.error('Could not get copilot info', err)
|
||||
}
|
||||
}
|
||||
|
||||
workspaceStore.subscribe(async (workspace) => {
|
||||
if (workspace) {
|
||||
loadCopilot(workspace)
|
||||
}
|
||||
})
|
||||
let devOnly = $derived($page.url.pathname.startsWith(base + '/scripts/dev'))
|
||||
|
||||
async function loadDefaultScripts(workspace: string, user: UserExt | undefined) {
|
||||
if (!user?.operator) {
|
||||
@@ -691,64 +669,14 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Splitpanes horizontal={false} class="flex-1 min-h-0">
|
||||
<Pane size={99.8 - chatState.size} minSize={50} class="flex flex-col min-h-0">
|
||||
<div
|
||||
id="content"
|
||||
class={classNames(
|
||||
'w-full flex-1 flex flex-col overflow-y-auto',
|
||||
devOnly || $userStore?.operator ? '!pl-0' : isCollapsed ? 'md:pl-12' : 'md:pl-40',
|
||||
'transition-all ease-in-out duration-200'
|
||||
)}
|
||||
>
|
||||
<main class="flex-1 flex flex-col">
|
||||
<div class="relative w-full flex-1 flex flex-col">
|
||||
<div
|
||||
class={classNames(
|
||||
'pt-2 px-4 sm:px-4 flex flex-row justify-between items-center shadow-sm max-w-7xl md:hidden',
|
||||
devOnly || $userStore?.operator ? 'hidden' : ''
|
||||
)}
|
||||
>
|
||||
<button
|
||||
aria-label="Menu"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
menuOpen = true
|
||||
}}
|
||||
class="h-8 w-8 inline-flex items-center justify-center rounded-md text-tertiary hover:text-primary focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane
|
||||
bind:size={chatState.size}
|
||||
minSize={15}
|
||||
class={`flex flex-col min-h-0 z-[${zIndexes.aiChat}]`}
|
||||
>
|
||||
<AiChat />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<AiChatLayout
|
||||
{children}
|
||||
noPadding={devOnly}
|
||||
{isCollapsed}
|
||||
onMenuOpen={() => {
|
||||
menuOpen = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<CenteredModal title="Loading user...">
|
||||
|
||||
@@ -1,40 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { page } from '$app/state'
|
||||
import type { ActionKind } from '$lib/common'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import AuditLogDetails from '$lib/components/auditLogs/AuditLogDetails.svelte'
|
||||
import AuditLogsFilters from '$lib/components/auditLogs/AuditLogsFilters.svelte'
|
||||
import AuditLogsTable from '$lib/components/auditLogs/AuditLogsTable.svelte'
|
||||
import AuditLogMobileFilters from '$lib/components/auditLogs/AuditLogMobileFilters.svelte'
|
||||
import { Alert, DrawerContent } from '$lib/components/common'
|
||||
import { Alert, DrawerContent, Skeleton } from '$lib/components/common'
|
||||
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
|
||||
import type { AuditLog } from '$lib/gen'
|
||||
import { AuditService } from '$lib/gen'
|
||||
import { enterpriseLicense, userStore, workspaceStore, userWorkspaces } from '$lib/stores'
|
||||
import { Splitpanes, Pane } from 'svelte-splitpanes'
|
||||
import AuditLogsTimeline from '$lib/components/auditLogs/AuditLogsTimeline.svelte'
|
||||
|
||||
let username: string = $state($page.url.searchParams.get('username') ?? 'all')
|
||||
let pageIndex: number | undefined = $state(Number($page.url.searchParams.get('page')) || 0)
|
||||
let before: string | undefined = $state($page.url.searchParams.get('before') ?? undefined)
|
||||
let username: string = $state(page.url.searchParams.get('username') ?? 'all')
|
||||
let pageIndex: number | undefined = $state(Number(page.url.searchParams.get('page')) || 0)
|
||||
let before: string | undefined = $state(page.url.searchParams.get('before') ?? undefined)
|
||||
let hasMore: boolean = $state(false)
|
||||
let after: string | undefined = $state($page.url.searchParams.get('after') ?? undefined)
|
||||
let perPage: number | undefined = $state(Number($page.url.searchParams.get('perPage')) || 100)
|
||||
let operation: string = $state($page.url.searchParams.get('operation') ?? 'all')
|
||||
let resource: string | undefined = $state($page.url.searchParams.get('resource') ?? undefined)
|
||||
let after: string | undefined = $state(page.url.searchParams.get('after') ?? undefined)
|
||||
let perPage: number | undefined = $state(Number(page.url.searchParams.get('perPage')) || 100)
|
||||
let operation: string = $state(page.url.searchParams.get('operation') ?? 'all')
|
||||
let resource: string | undefined = $state(page.url.searchParams.get('resource') ?? undefined)
|
||||
let scope: undefined | 'all_workspaces' | 'instance' = $state(
|
||||
($page.url.searchParams.get('scope') ?? undefined) as undefined | 'all_workspaces' | 'instance'
|
||||
(page.url.searchParams.get('scope') ?? undefined) as undefined | 'all_workspaces' | 'instance'
|
||||
)
|
||||
|
||||
let actionKind: ActionKind | 'all' = $state(
|
||||
($page.url.searchParams.get('actionKind') as ActionKind) ?? 'all'
|
||||
(page.url.searchParams.get('actionKind') as ActionKind) ?? 'all'
|
||||
)
|
||||
|
||||
let logs: AuditLog[] | undefined = $state()
|
||||
|
||||
let selectedId: number | undefined = $state(undefined)
|
||||
let auditLogDrawer: Drawer | undefined = $state()
|
||||
|
||||
// Function to fetch missing job execution audit logs
|
||||
async function fetchMissingJobSpan(jobId: string, jobLogs: AuditLog[]): Promise<AuditLog[]> {
|
||||
if (jobLogs.length === 0) return []
|
||||
|
||||
const firstJobLog = jobLogs[0]
|
||||
const timeBuffer = 10000 // 10 seconds buffer for safety
|
||||
|
||||
// Create time range around the job execution
|
||||
const jobTime = new Date(firstJobLog.timestamp).getTime()
|
||||
const beforeTime = new Date(jobTime + timeBuffer).toISOString()
|
||||
const afterTime = new Date(jobTime - timeBuffer).toISOString()
|
||||
|
||||
try {
|
||||
// Try multiple operation patterns to find the job execution
|
||||
const operationPatterns = ['jobs.run', 'jobs.run.script', 'jobs.run.flow', 'jobs.run.preview']
|
||||
|
||||
for (const operation of operationPatterns) {
|
||||
const additionalLogs = await AuditService.listAuditLogs({
|
||||
workspace: scope === 'instance' ? 'global' : $workspaceStore!,
|
||||
username: firstJobLog.username,
|
||||
operation: operation,
|
||||
before: beforeTime,
|
||||
after: afterTime,
|
||||
perPage: 100,
|
||||
allWorkspaces: scope === 'all_workspaces'
|
||||
})
|
||||
|
||||
// Check if we found the job execution log
|
||||
const jobExecutionLog = additionalLogs.find((log) => log.parameters?.uuid === jobId)
|
||||
if (jobExecutionLog) {
|
||||
return additionalLogs
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.audit_logs}
|
||||
@@ -43,15 +85,15 @@
|
||||
<p>Page not available for operators</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full h-screen">
|
||||
<div class="px-2">
|
||||
<div class="flex items-center space-x-2 flex-row justify-between">
|
||||
<div class="flex flex-row flex-wrap justify-between py-2 my-4 px-4 gap-1 items-center">
|
||||
<h1 class="!text-2xl font-semibold leading-6 tracking-tight">Audit logs</h1>
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/audit_logs">
|
||||
You can only see your own audit logs unless you are an admin.
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex flex-col w-full h-screen">
|
||||
<div class="flex items-center space-x-2 flex-row justify-between">
|
||||
<div class="flex flex-row flex-wrap justify-between py-2 my-4 px-4 gap-1 items-center">
|
||||
<h1 class="!text-2xl font-semibold leading-6 tracking-tight">Audit logs</h1>
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/audit_logs">
|
||||
You can only see your own audit logs unless you are an admin.
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap justify-between py-2 my-2 px-4 gap-1 items-center">
|
||||
<div class="hidden 2xl:block">
|
||||
<AuditLogsFilters
|
||||
bind:logs
|
||||
@@ -85,56 +127,85 @@
|
||||
</AuditLogMobileFilters>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !$enterpriseLicense || $enterpriseLicense.endsWith('_pro')}
|
||||
<Alert title="Redacted audit logs" type="warning">
|
||||
You need an enterprise license to see unredacted audit logs.
|
||||
</Alert>
|
||||
<div class="py-2"></div>
|
||||
</div>
|
||||
<div class="h-2/6">
|
||||
{#if logs}
|
||||
<AuditLogsTimeline
|
||||
{logs}
|
||||
minTimeSet={after}
|
||||
maxTimeSet={before}
|
||||
onZoom={({ min, max }) => {
|
||||
before = max.toISOString()
|
||||
after = min.toISOString()
|
||||
console.log('zoom!')
|
||||
}}
|
||||
onMissingJobSpan={fetchMissingJobSpan}
|
||||
onLogSelected={(log) => {
|
||||
console.log('selected log ')
|
||||
selectedId = log.id
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<SplitPanesWrapper class="hidden md:block">
|
||||
<Splitpanes>
|
||||
<Pane size={70} minSize={50}>
|
||||
<AuditLogsTable
|
||||
{logs}
|
||||
{selectedId}
|
||||
bind:pageIndex
|
||||
bind:perPage
|
||||
bind:actionKind
|
||||
bind:operation
|
||||
bind:usernameFilter={username}
|
||||
bind:resourceFilter={resource}
|
||||
bind:hasMore
|
||||
on:select={(e) => {
|
||||
selectedId = e.detail
|
||||
}}
|
||||
/>
|
||||
</Pane>
|
||||
<Pane size={30} minSize={15}>
|
||||
{#if logs}
|
||||
<AuditLogDetails {logs} {selectedId} />
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
<div class="flex-grow w-full">
|
||||
<div class="px-2">
|
||||
{#if !$enterpriseLicense || $enterpriseLicense.endsWith('_pro')}
|
||||
<Alert title="Redacted audit logs" type="warning">
|
||||
You need an enterprise license to see unredacted audit logs.
|
||||
</Alert>
|
||||
<div class="py-2"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes>
|
||||
<Pane size={70} minSize={50}>
|
||||
{#if logs}
|
||||
<AuditLogsTable
|
||||
{logs}
|
||||
{selectedId}
|
||||
bind:pageIndex
|
||||
bind:perPage
|
||||
bind:actionKind
|
||||
bind:operation
|
||||
bind:usernameFilter={username}
|
||||
bind:resourceFilter={resource}
|
||||
bind:hasMore
|
||||
onselect={(id) => {
|
||||
selectedId = id
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="gap-1 flex flex-col">
|
||||
{#each new Array(8) as _}
|
||||
<Skeleton layout={[[3]]} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane size={30} minSize={15}>
|
||||
{#if logs}
|
||||
<AuditLogDetails {logs} {selectedId} />
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
|
||||
<div class="md:hidden">
|
||||
<AuditLogsTable
|
||||
{logs}
|
||||
bind:hasMore
|
||||
bind:pageIndex
|
||||
bind:perPage
|
||||
bind:actionKind
|
||||
bind:operation
|
||||
bind:usernameFilter={username}
|
||||
bind:resourceFilter={resource}
|
||||
on:select={(e) => {
|
||||
selectedId = e.detail
|
||||
|
||||
auditLogDrawer?.openDrawer()
|
||||
}}
|
||||
/>
|
||||
<div class="md:hidden">
|
||||
<AuditLogsTable
|
||||
{logs}
|
||||
bind:hasMore
|
||||
bind:pageIndex
|
||||
bind:perPage
|
||||
bind:actionKind
|
||||
bind:operation
|
||||
bind:usernameFilter={username}
|
||||
bind:resourceFilter={resource}
|
||||
onselect={(id) => {
|
||||
selectedId = id
|
||||
auditLogDrawer?.openDrawer()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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 @@
|
||||
<!-- <div id="monaco-widgets-root" class="monaco-editor" style="z-index: 1200;" /> -->
|
||||
|
||||
<FlowBuilder
|
||||
on:saveInitial={(e) => {
|
||||
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}
|
||||
|
||||
@@ -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<void> {
|
||||
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 @@
|
||||
<!-- <div id="monaco-widgets-root" class="monaco-editor" style="z-index: 1200;" /> -->
|
||||
|
||||
<DiffDrawer bind:this={diffDrawer} {restoreDeployed} {restoreDraft} />
|
||||
<FlowBuilder
|
||||
on:deploy={(e) => {
|
||||
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}
|
||||
>
|
||||
<UnsavedConfirmationModal
|
||||
{#if notFound}
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<h1 class="text-2xl font-bold">Flow not found at path {$page.params.path}</h1>
|
||||
<p class="text-gray-500">The flow you are looking for does not exist.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<FlowBuilder
|
||||
onDeploy={(e) => {
|
||||
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}
|
||||
/>
|
||||
</FlowBuilder>
|
||||
{savedPrimarySchedule}
|
||||
{draftTriggersFromUrl}
|
||||
{selectedTriggerIndexFromUrl}
|
||||
{version}
|
||||
{loadedFromHistoryFromUrl}
|
||||
>
|
||||
<UnsavedConfirmationModal
|
||||
{diffDrawer}
|
||||
getInitialAndModifiedValues={flowBuilder?.getInitialAndModifiedValues}
|
||||
/>
|
||||
</FlowBuilder>
|
||||
{/if}
|
||||
|
||||
@@ -417,7 +417,7 @@
|
||||
}}
|
||||
/>
|
||||
{#if flow}
|
||||
<FlowHistory bind:this={flowHistory} path={flow.path} on:historyRestore={loadFlow} />
|
||||
<FlowHistory bind:this={flowHistory} path={flow.path} onHistoryRestore={loadFlow} />
|
||||
{/if}
|
||||
|
||||
{#if flow}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
||||
import FlowWrapper from '$lib/components/FlowWrapper.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
loadUser()
|
||||
|
||||
async function loadUser() {
|
||||
if ($workspaceStore) {
|
||||
$userStore = await getUserExt($workspaceStore)
|
||||
}
|
||||
}
|
||||
|
||||
let flowStore = $state({
|
||||
val: {
|
||||
summary: '',
|
||||
value: { modules: [] },
|
||||
path: 'u/admin/foo',
|
||||
edited_at: '',
|
||||
edited_by: '',
|
||||
archived: false,
|
||||
extra_perms: {},
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {},
|
||||
required: [],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let flowStateStore = writable({})
|
||||
|
||||
let customUi: FlowBuilderWhitelabelCustomUi = {
|
||||
// disableAi: true
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <ScriptWrapper {script} neverShowMeta={true} {customUi} /> -->
|
||||
|
||||
<FlowWrapper
|
||||
disableAi
|
||||
pathStoreInit="u/foo/bar"
|
||||
{customUi}
|
||||
selectedId={undefined}
|
||||
newFlow
|
||||
{flowStore}
|
||||
{flowStateStore}
|
||||
></FlowWrapper>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user