Compare commits

..
Author SHA1 Message Date
claude[bot]andDiego Imbert 065b0efa85 Replace all structuredClone patterns with clone() utility
- Replace structuredClone($state.snapshot(x)) with clone(x)
- Replace structuredClone(stateSnapshot(x)) with clone(x)
- Replace structuredClone(x) with clone(x)
- Add clone import to 40+ files
- Remove unused stateSnapshot imports
- Fix syntax errors and import conflicts

Total: 101 structuredClone patterns replaced across codebase

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
2025-06-26 13:52:00 +00:00
Diego Imbert 83e5bfbc17 clone impl 2025-06-26 11:36:36 +02:00
513 changed files with 9319 additions and 13803 deletions
+4 -4
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.88.0
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -44,7 +44,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.88.0
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -81,7 +81,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.88.0
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -121,7 +121,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.88.0
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
+15 -34
View File
@@ -1,41 +1,22 @@
name: Claude Auto Review
name: Auto Comment on PR Ready for Review
on:
pull_request:
types: [ready_for_review, opened]
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
types: [opened, ready_for_review]
jobs:
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
add-review-comment:
if: github.event.pull_request.draft == false
runs-on: ubicloud-standard-2
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Add review comment
uses: actions/github-script@v7
with:
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"
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'
});
@@ -1,4 +1,4 @@
name: Git commands
name: Update SQLx
on:
issue_comment:
@@ -103,73 +103,3 @@ jobs:
repo: context.repo.repo,
body: 'Successfully ran sqlx update'
})
update-ee-ref:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/eeref')
runs-on: ubicloud-standard-2
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Comment on PR - Starting
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Starting ee ref update...'
})
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: ${{ github.event.issue.pull_request.head.ref }}
fetch-depth: 0
- name: Checkout windmill-ee-private
uses: actions/checkout@v3
with:
repository: windmill-labs/windmill-ee-private
path: windmill-ee-private
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
- name: Get last commit hash of private-repo
id: get-commit-hash
run: |
cd windmill-ee-private
COMMIT_HASH=$(git rev-parse HEAD)
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
echo "Latest commit hash: $COMMIT_HASH"
- name: Update ee-repo-ref.txt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${{ steps.get-commit-hash.outputs.commit_hash }}" > backend/ee-repo-ref.txt
echo "Updated backend/ee-repo-ref.txt with commit hash: ${{ steps.get-commit-hash.outputs.commit_hash }}"
# commit and push the changes
PR_NUMBER=${{ github.event.issue.number }}
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add backend/ee-repo-ref.txt
git commit -m "Update ee-repo-ref.txt" || echo "No changes to commit"
git push origin $BRANCH_NAME
- name: Comment on PR - Completed
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Successfully updated ee-repo-ref.txt'
})
-44
View File
@@ -1,49 +1,5 @@
# Changelog
## [1.502.2](https://github.com/windmill-labs/windmill/compare/v1.502.1...v1.502.2) (2025-07-01)
### Bug Fixes
* bad spacing ai chat context elements ([#6111](https://github.com/windmill-labs/windmill/issues/6111)) ([2fb912b](https://github.com/windmill-labs/windmill/commit/2fb912b78c90d9e70d3db4b0c3c473831161c8b4))
* **frontend:** improve step job load ([#6109](https://github.com/windmill-labs/windmill/issues/6109)) ([0afe3f9](https://github.com/windmill-labs/windmill/commit/0afe3f9691d93f837b10b29a0cf125eaa175589d))
* **frontend:** only show test button for script modules ([#6107](https://github.com/windmill-labs/windmill/issues/6107)) ([7042a6f](https://github.com/windmill-labs/windmill/commit/7042a6f52db823d6b9b5ad14fa83af36880bd2d5))
## [1.502.1](https://github.com/windmill-labs/windmill/compare/v1.502.0...v1.502.1) (2025-07-01)
### Bug Fixes
* **frontend:** update test job logs ([#6102](https://github.com/windmill-labs/windmill/issues/6102)) ([a4c295b](https://github.com/windmill-labs/windmill/commit/a4c295b5e857314d78de6bb6ab942dc60ff99279))
## [1.502.0](https://github.com/windmill-labs/windmill/compare/v1.501.4...v1.502.0) (2025-06-30)
### Features
* kafka better retry and errors ([#6067](https://github.com/windmill-labs/windmill/issues/6067)) ([8edf4b2](https://github.com/windmill-labs/windmill/commit/8edf4b2b92fe77ad86d96d41095b05540176e783))
* use FIM for code autocomplete ([#6081](https://github.com/windmill-labs/windmill/issues/6081)) ([431437c](https://github.com/windmill-labs/windmill/commit/431437c3449ddcd8c45bacd696a4db209577773b))
### Bug Fixes
* add support for GCS object storage ([#6083](https://github.com/windmill-labs/windmill/issues/6083)) ([c51e128](https://github.com/windmill-labs/windmill/commit/c51e128920801ec7199033c59655a0bcdd5341ba))
* fix critical alerts flapping on low disk ([#6075](https://github.com/windmill-labs/windmill/issues/6075)) ([bcba462](https://github.com/windmill-labs/windmill/commit/bcba46225f094e60bb7e77ed74fc060ffaccb6c6))
* fix s3 settings reset ([8ba3959](https://github.com/windmill-labs/windmill/commit/8ba3959adac955cd4ec5cbebd357703992c68baa))
* **frontend:** improve flow editor settings bar UX ([#6049](https://github.com/windmill-labs/windmill/issues/6049)) ([ded54f2](https://github.com/windmill-labs/windmill/commit/ded54f2e68da09618c377cd699e0a2eaa53a63a8))
* optimize public apps rendering ([a7e78f0](https://github.com/windmill-labs/windmill/commit/a7e78f01f1697b8a4a3c61cd4377bc34b8077d38))
* public url in app menu ([ca368ab](https://github.com/windmill-labs/windmill/commit/ca368aba7a334efc9963f69b3a4462d9972997f4))
* test up to broken due to mutable flow ai chat preview ([#6096](https://github.com/windmill-labs/windmill/issues/6096)) ([805a8b5](https://github.com/windmill-labs/windmill/commit/805a8b574c057b911bff5d335e0c63051c6587ee))
## [1.501.4](https://github.com/windmill-labs/windmill/compare/v1.501.3...v1.501.4) (2025-06-26)
### Bug Fixes
* add windows paths to uv install to find git/ssh ([#6063](https://github.com/windmill-labs/windmill/issues/6063)) ([835f1d2](https://github.com/windmill-labs/windmill/commit/835f1d2ec945145942deaa41cb3bd176ed276279))
* optionally enable CSP headers ([#6033](https://github.com/windmill-labs/windmill/issues/6033)) ([d933648](https://github.com/windmill-labs/windmill/commit/d933648d3666b2ca9d813e04b9f19ddc3c7efda3))
* schemaform reorder ([#6069](https://github.com/windmill-labs/windmill/issues/6069)) ([1a4b096](https://github.com/windmill-labs/windmill/commit/1a4b096f3ce40e238f1724aa4fd26649d70cb62a))
## [1.501.3](https://github.com/windmill-labs/windmill/compare/v1.501.2...v1.501.3) (2025-06-25)
+1 -1
View File
@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.88-slim-bookworm
ARG RUST_IMAGE=rust:1.86-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
+151 -164
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.502.2"
version = "1.501.3"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.502.2"
version = "1.501.3"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -348,7 +348,7 @@ nkeys = "0.4.4"
nu-parser = { version = "0.101.0", default-features = false }
datafusion = "47.0.0"
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure", "gcp"] }
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure"] }
openidconnect = { version = "4.0.0-rc.1" }
aws-config = "^1"
aws-sdk-sqs = "1.57.0"
+1 -1
View File
@@ -1 +1 @@
21aabec96e91c8075dd637d1e32af90e495082fc
835a91c7c31ea749759cd8af0922ad837049ea2a
@@ -1,3 +0,0 @@
-- Remove email and span columns from audit table
ALTER TABLE audit DROP COLUMN email;
ALTER TABLE audit DROP COLUMN span;
@@ -1,3 +0,0 @@
-- Add email and span columns to audit table
ALTER TABLE audit ADD COLUMN email VARCHAR(255);
ALTER TABLE audit ADD COLUMN span VARCHAR(255);
+3 -4
View File
@@ -54,8 +54,7 @@ use windmill_common::{
stats_oss::schedule_stats,
triggers::TriggerKind,
utils::{
create_worker_suffix,
worker_name_with_suffix,
create_default_worker_suffix, create_ssh_agent_worker_suffix, worker_name_with_suffix,
Mode, GIT_VERSION, HOSTNAME, MODE_AND_ADDONS,
},
worker::{
@@ -348,7 +347,7 @@ async fn windmill_main() -> anyhow::Result<()> {
"Creating http client for cluster using base internal url {}",
std::env::var("BASE_INTERNAL_URL").unwrap_or_default()
);
let suffix = create_worker_suffix(&hostname);
let suffix = create_ssh_agent_worker_suffix(&hostname);
(
Connection::Http(build_agent_http_client(&suffix)),
Some(suffix),
@@ -688,7 +687,7 @@ Windmill Community Edition {GIT_VERSION}
let suffix = if i == 0 && first_suffix.is_some() {
first_suffix.as_ref().unwrap().clone()
} else {
create_worker_suffix(&hostname)
create_default_worker_suffix(&hostname)
};
let worker_conn = WorkerConn {
-1
View File
@@ -1926,7 +1926,6 @@ 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");
+2 -87
View File
@@ -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, None).await.unwrap();
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), 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, None).await.unwrap();
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
))
@@ -935,7 +935,6 @@ 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,
@@ -2090,89 +2089,6 @@ def main():
assert_eq!(result, serde_json::json!("hello world"));
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_global_site_packages(db: Pool<Postgres>) {
use windmill_common::{cache::concatcp, worker::ROOT_CACHE_DIR};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
// Shared for all 3.12.*
let path = concatcp!(ROOT_CACHE_DIR, "python_3_12/global-site-packages").to_owned();
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path + "/my_global_site_package_3_12_any.py", "").unwrap();
// 3.12
{
let content = r#"# py: ==3.12
#requirements:
#
import my_global_site_package_3_12_any
def main():
return "hello world"
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
lock: None,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("hello world"));
}
// 3.12.1
{
let content = r#"# py: ==3.12.1
#requirements:
#
import my_global_site_package_3_12_any
def main():
return "hello world"
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
lock: None,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("hello world"));
}
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_job_heavy_dep(db: Pool<Postgres>) {
@@ -4210,7 +4126,6 @@ async fn test_result_format(db: Pool<Postgres>) {
"",
&Uuid::nil(),
None,
None,
)
.await
.unwrap();
+3 -9
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.502.2
version: 1.501.3
title: Windmill API
contact:
@@ -14304,8 +14304,6 @@ components:
type: string
parameters:
type: object
span:
type: string
required:
- id
- timestamp
@@ -16565,13 +16563,11 @@ components:
properties:
type:
type: string
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc", "GoogleCloudStorage"]
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
s3_resource_path:
type: string
azure_blob_resource_path:
type: string
gcs_resource_path:
type: string
public_resource:
type: boolean
secondary_storage:
@@ -16582,13 +16578,11 @@ components:
type:
type: string
enum:
["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc", "GoogleCloudStorage"]
["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
s3_resource_path:
type: string
azure_blob_resource_path:
type: string
gcs_resource_path:
type: string
public_resource:
type: boolean
-4
View File
@@ -4,7 +4,6 @@ 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};
@@ -102,7 +101,6 @@ 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,
@@ -137,7 +135,6 @@ 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(),
@@ -152,7 +149,6 @@ pub async fn handle_resume_action(
} else {
cancel_suspended_job(
authed,
opt_tokened,
Extension(db.clone()),
Path((
w_id.to_string(),
-9
View File
@@ -9,7 +9,6 @@ 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},
@@ -53,7 +52,6 @@ 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},
@@ -1042,7 +1040,6 @@ 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,
@@ -1414,7 +1411,6 @@ 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,
@@ -1521,7 +1517,6 @@ 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)>,
@@ -1724,10 +1719,6 @@ 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,
+1 -9
View File
@@ -21,7 +21,7 @@ use std::sync::{
use tokio::sync::RwLock;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, TOKEN_PREFIX_LEN},
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims},
jwt,
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
};
@@ -134,7 +134,6 @@ impl AuthCache {
folders: claims.folders,
scopes: None,
username_override,
token_prefix: claims.audit_span,
};
AUTH_CACHE.insert(
@@ -218,7 +217,6 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
} else {
let groups = vec![name.to_string()];
@@ -240,7 +238,6 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
} else {
@@ -255,7 +252,6 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
}
@@ -303,7 +299,6 @@ impl AuthCache {
folders,
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
None if super_admin => Some(ApiAuthed {
@@ -315,7 +310,6 @@ impl AuthCache {
folders: vec![],
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
}),
None => None,
}
@@ -329,7 +323,6 @@ impl AuthCache {
folders: Vec::new(),
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
}
@@ -362,7 +355,6 @@ impl AuthCache {
folders: Vec::new(),
scopes: None,
username_override: None,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
} else {
None
-6
View File
@@ -836,7 +836,6 @@ 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 {
@@ -849,7 +848,6 @@ impl From<ApiAuthed> for Authed {
groups: value.groups,
folders: value.folders,
scopes: value.scopes,
token_prefix: value.token_prefix,
}
}
}
@@ -860,7 +858,6 @@ 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(),
}
}
}
@@ -881,9 +878,6 @@ 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 {
-2
View File
@@ -494,7 +494,6 @@ 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,
@@ -936,7 +935,6 @@ 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,
+76 -73
View File
@@ -20,20 +20,22 @@ use sqlx::Pool;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
#[cfg(feature = "prometheus")]
use std::sync::atomic::Ordering;
use tokio::io::AsyncReadExt;
#[cfg(feature = "prometheus")]
use tokio::time::Instant;
use tower::ServiceBuilder;
use windmill_common::auth::{is_super_admin_email, TOKEN_PREFIX_LEN};
use windmill_common::auth::is_super_admin_email;
use windmill_common::error::JsonResult;
use windmill_common::flow_status::{JobResult, RestartedFrom};
use windmill_common::jobs::{format_completed_job_result, format_result, ENTRYPOINT_OVERRIDE};
use windmill_common::utils::WarnAfterExt;
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
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;
@@ -82,6 +84,9 @@ use windmill_common::{
},
};
#[cfg(feature = "prometheus")]
use windmill_common::{METRICS_DEBUG_ENABLED, METRICS_ENABLED};
use windmill_common::{
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, BASE_URL,
@@ -91,6 +96,36 @@ use windmill_queue::{
PushArgsOwned, PushIsolationLevel,
};
#[cfg(feature = "prometheus")]
type Histo = prometheus::Histogram;
#[cfg(not(feature = "prometheus"))]
type Histo = ();
#[cfg(feature = "prometheus")]
fn setup_list_jobs_debug_metrics() -> Option<Histo> {
let api_list_jobs_query_duration = if METRICS_DEBUG_ENABLED.load(Ordering::Relaxed)
&& METRICS_ENABLED.load(Ordering::Relaxed)
{
Some(
prometheus::register_histogram!(prometheus::HistogramOpts::new(
"api_list_jobs_query_duration",
"Duration of listing jobs (query)",
))
.expect("register prometheus metric"),
)
} else {
None
};
api_list_jobs_query_duration
}
#[cfg(not(feature = "prometheus"))]
fn setup_list_jobs_debug_metrics() -> Option<Histo> {
None
}
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
@@ -101,6 +136,8 @@ pub fn workspaced_service() -> Router {
let ce_headers =
ServiceBuilder::new().layer(axum::middleware::from_fn(add_webhook_allowed_origin));
let api_list_jobs_query_duration = setup_list_jobs_debug_metrics();
Router::new()
.route(
"/run/f/*script_path",
@@ -175,7 +212,10 @@ pub fn workspaced_service() -> Router {
)
.route("/add_batch_jobs/:n", post(add_batch_jobs))
.route("/run/preview_flow", post(run_preview_flow_job))
.route("/list", get(list_jobs))
.route(
"/list",
get(list_jobs).layer(Extension(api_list_jobs_query_duration)),
)
.route(
"/list_selected_job_groups",
// We use post because sending a huge array as a query param can produce
@@ -294,7 +334,6 @@ 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>,
@@ -303,7 +342,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), Some(&tokened.token), &w_id, &flow_id).await?;
log_job_view(&db, Some(&authed), &w_id, &flow_id).await?;
Ok(Json(res))
}
@@ -339,7 +378,6 @@ 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>,
@@ -352,7 +390,6 @@ 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(
@@ -451,7 +488,6 @@ 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>,
@@ -464,7 +500,6 @@ 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()),
},
};
@@ -516,7 +551,6 @@ 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> {
@@ -568,7 +602,7 @@ async fn get_flow_job_debug_info(
}
}
log_job_view(&db, opt_authed.as_ref(), tokened_o.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(jobs).into_response())
} else {
@@ -628,7 +662,6 @@ 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>,
@@ -648,7 +681,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(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(job).into_response())
}
@@ -1096,7 +1129,6 @@ 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> {
@@ -1134,7 +1166,7 @@ async fn get_job_logs(
}
let logs = record.logs.unwrap_or_default();
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &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
@@ -1171,7 +1203,7 @@ async fn get_job_logs(
}
let logs = text.logs.unwrap_or_default();
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(r) =
@@ -1195,7 +1227,6 @@ 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>> {
@@ -1221,7 +1252,7 @@ async fn get_args(
));
}
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(record.args.map(|x| x.0).unwrap_or_default()))
} else {
@@ -1242,7 +1273,7 @@ async fn get_args(
));
}
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(record.args.map(|x| x.0).unwrap_or_default()))
}
@@ -1895,6 +1926,7 @@ async fn list_jobs(
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListCompletedQuery>,
Extension(_api_list_jobs_query_duration): Extension<Option<Histo>>,
) -> error::JsonResult<Vec<Job>> {
check_scopes(&authed, || format!("jobs:listjobs"))?;
@@ -1958,12 +1990,24 @@ async fn list_jobs(
};
let mut tx: Transaction<'_, Postgres> = user_db.begin(&authed).await?;
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql)
.fetch_all(&mut *tx)
.warn_after_seconds_with_sql(5, format!("list_jobs: {}", sql))
.await?;
#[cfg(feature = "prometheus")]
let start = Instant::now();
#[cfg(feature = "prometheus")]
if _api_list_jobs_query_duration.is_some() || true {
tracing::info!("list_jobs query: {}", sql);
}
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql).fetch_all(&mut *tx).await?;
tx.commit().await?;
#[cfg(feature = "prometheus")]
if let Some(api_list_jobs_query_duration) = _api_list_jobs_query_duration {
let duration = start.elapsed().as_secs_f64();
api_list_jobs_query_duration.observe(duration);
tracing::info!("list_jobs query took {}s: {}", duration, sql);
}
Ok(Json(jobs.into_iter().map(From::from).collect()))
}
@@ -2003,23 +2047,13 @@ 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,
opt_tokened,
true,
value, db, w_id, job_id, resume_id, approver, secret, authed, true,
)
.await
}
@@ -2033,7 +2067,6 @@ 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);
@@ -2112,7 +2145,6 @@ 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(
@@ -2282,14 +2314,13 @@ 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, opt_tokened, false,
value, db, w_id, job_id, resume_id, approver, secret, authed, false,
)
.await
}
@@ -2307,7 +2338,6 @@ 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>,
@@ -2374,7 +2404,7 @@ pub async fn get_suspended_job_flow(
approvers_from_status
};
log_job_view(&db, authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job).await?;
log_job_view(&db, authed.as_ref(), &w_id, &job).await?;
Ok(Json(SuspendedJobFlow { job: flow, approvers }).into_response())
}
@@ -3531,7 +3561,6 @@ 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,
@@ -3625,7 +3654,6 @@ 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,
@@ -3718,7 +3746,6 @@ 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,
@@ -3864,7 +3891,6 @@ pub async fn run_workflow_as_code(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
Some(job_id),
@@ -4280,7 +4306,6 @@ 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<()> {
@@ -4291,7 +4316,6 @@ 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
@@ -4390,7 +4414,6 @@ 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,
@@ -4531,7 +4554,6 @@ 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,
@@ -4645,7 +4667,6 @@ 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,
@@ -4760,7 +4781,6 @@ 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,
@@ -4831,7 +4851,6 @@ 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,
@@ -4920,7 +4939,6 @@ 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,
@@ -5088,7 +5106,6 @@ async fn run_dependencies_job(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -5146,7 +5163,6 @@ 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,
@@ -5487,7 +5503,6 @@ 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,
@@ -5607,7 +5622,6 @@ 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,
@@ -5700,7 +5714,6 @@ 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>,
@@ -5757,7 +5770,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(), opt_tokened.token.as_deref(), &w_id, &job_id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &job_id).await?;
Ok(Json(JobUpdate {
running: record.running,
completed: record.completed,
@@ -6045,7 +6058,6 @@ 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> {
@@ -6071,7 +6083,7 @@ async fn get_completed_job<'a>(
// .fetch_optional(db)
// .await.ok().flatten().flatten();
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(response)
}
@@ -6085,7 +6097,6 @@ 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>,
@@ -6174,7 +6185,7 @@ async fn get_completed_job_result(
raw_result.result.as_mut(),
);
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(raw_result.result).into_response())
}
@@ -6232,7 +6243,6 @@ 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>,
@@ -6265,7 +6275,7 @@ async fn get_completed_job_result_maybe(
));
}
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(CompletedJobResult {
started: Some(true),
@@ -6303,7 +6313,6 @@ 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)>,
@@ -6353,11 +6362,5 @@ async fn delete_completed_job<'a>(
.await?;
tx.commit().await?;
return get_completed_job(
OptAuthed(Some(authed)),
OptTokened { token: Some(token) },
Extension(db),
Path((w_id, id)),
)
.await;
return get_completed_job(OptAuthed(Some(authed)), Extension(db), Path((w_id, id))).await;
}
-1
View File
@@ -508,7 +508,6 @@ pub async fn transform_json_value<'c>(
email: "backend".to_string(),
username: "backend".to_string(),
username_override: None,
token_prefix: None,
}),
)
.await?;
+9 -2
View File
@@ -7,7 +7,10 @@
*/
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},
@@ -22,7 +25,11 @@ 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;
-1
View File
@@ -941,7 +941,6 @@ async fn create_script_internal<'c>(
&authed.username,
&authed.email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
None,
+5 -7
View File
@@ -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,7 +116,6 @@ 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> {
@@ -125,8 +124,8 @@ pub async fn slack_app_callback_handler(
tracing::debug!("Payload: {:#?}", payload);
match payload.r#type {
PayloadType::ViewSubmission => handle_submission(authed, opt_tokened, db, &payload, "resume").await?,
PayloadType::ViewClosed => handle_submission(authed, opt_tokened, db, &payload, "cancel").await?,
PayloadType::ViewSubmission => handle_submission(authed, db, &payload, "resume").await?,
PayloadType::ViewClosed => handle_submission(authed, db, &payload, "cancel").await?,
_ => {
if let Some(actions) = &payload.actions {
if let Some(action) = actions.first() {
@@ -257,7 +256,6 @@ pub async fn request_slack_approval(
async fn handle_submission(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
db: DB,
payload: &Payload,
action: &str,
@@ -296,7 +294,7 @@ async fn handle_submission(
}
// Use the common handler to process the resume/cancel action
handle_resume_action(authed, opt_tokened, db.clone(), &resume_url, state_json, action).await?;
handle_resume_action(authed, 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?;
+6 -28
View File
@@ -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, TOKEN_PREFIX_LEN};
use windmill_common::auth::fetch_authed_from_permissioned_as;
use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING;
use windmill_common::oauth2::InstanceEvent;
use windmill_common::users::COOKIE_NAME;
@@ -243,14 +243,13 @@ 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(
@@ -691,12 +690,7 @@ async fn logout(
};
audit_log(
&mut *tx,
&AuditAuthor {
email: email.clone(),
username: email,
username_override: None,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
},
&AuditAuthor { email: email.clone(), username: email, username_override: None },
audit_message,
ActionKind::Delete,
"global",
@@ -1280,10 +1274,7 @@ 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)
@@ -1648,12 +1639,8 @@ 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,
token_prefix: None,
};
let audit_author =
AuditAuthor { email: email.clone(), username: email.clone(), username_override: 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'",
@@ -1702,13 +1689,6 @@ 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,
@@ -1778,7 +1758,6 @@ 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,
@@ -1819,7 +1798,6 @@ 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,
-6
View File
@@ -415,9 +415,3 @@ pub struct ExpiringCacheEntry<T> {
pub value: T,
pub expiry: std::time::Instant,
}
#[cfg(all(feature = "kafka", feature = "enterprise", feature = "private"))]
pub async fn update_rw_lock<T>(lock: std::sync::Arc<tokio::sync::RwLock<T>>, value: T) -> () {
let mut w = lock.write().await;
*w = value;
}
+7 -2
View File
@@ -7,7 +7,9 @@
*/
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::{
@@ -21,7 +23,10 @@ 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,
+1 -2
View File
@@ -14,7 +14,6 @@ use windmill_audit::ActionKind;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
auth::is_super_admin_email,
error::{Error, Result},
utils::require_admin,
};
@@ -33,7 +32,7 @@ pub(crate) async fn change_workspace_id(
Extension(db): Extension<DB>,
Json(rw): Json<ChangeWorkspaceId>,
) -> Result<String> {
if *CLOUD_HOSTED && !is_super_admin_email(&db, &authed.email).await? {
if *CLOUD_HOSTED {
return Err(Error::BadRequest(
"This feature is not available on the cloud".to_string(),
));
+8 -16
View File
@@ -20,6 +20,14 @@ 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 {
@@ -33,10 +41,6 @@ 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"))]
@@ -44,18 +48,6 @@ 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"))]
-1
View File
@@ -24,7 +24,6 @@ pub struct AuditLog {
pub action_kind: ActionKind,
pub resource: Option<String>,
pub parameters: Option<serde_json::Value>,
pub span: Option<String>,
}
#[derive(Deserialize)]
+1 -1
View File
@@ -83,7 +83,7 @@ strum_macros.workspace = true
url.workspace = true
semver.workspace = true
croner = "2.2.0"
croner = "2.0.6"
quick_cache.workspace = true
pin-project-lite.workspace = true
futures.workspace = true
+3 -12
View File
@@ -17,8 +17,6 @@ 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();
@@ -68,7 +66,6 @@ pub struct JWTAuthClaims {
pub exp: usize,
pub job_id: Option<String>,
pub scopes: Option<Vec<String>>,
pub audit_span: Option<String>,
}
#[derive(Deserialize, Debug)]
@@ -95,7 +92,6 @@ impl From<JobPerms> for Authed {
.filter_map(|x| serde_json::from_value::<(String, bool, bool)>(x).ok())
.collect(),
scopes: None,
token_prefix: None,
}
}
}
@@ -175,41 +171,38 @@ 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,
})
}
}
@@ -269,7 +262,6 @@ 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)
@@ -310,7 +302,6 @@ 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)
-1
View File
@@ -12,7 +12,6 @@ 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)]
+1 -78
View File
@@ -10,8 +10,6 @@ use object_store::aws::AwsCredential;
#[cfg(feature = "parquet")]
use object_store::azure::MicrosoftAzureBuilder;
#[cfg(feature = "parquet")]
use object_store::gcp::GoogleCloudStorageBuilder;
#[cfg(feature = "parquet")]
use object_store::ObjectStore;
#[cfg(feature = "parquet")]
use object_store::{aws::AmazonS3Builder, ClientOptions};
@@ -223,7 +221,6 @@ pub enum LargeFileStorage {
AzureBlobStorage(AzureBlobStorage),
S3AwsOidc(S3Storage),
AzureWorkloadIdentity(AzureBlobStorage),
GoogleCloudStorage(GoogleCloudStorage),
// TODO: Add a filesystem type here in the future if needed
}
@@ -241,18 +238,10 @@ pub struct AzureBlobStorage {
pub public_resource: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GoogleCloudStorage {
pub gcs_resource_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_resource: Option<bool>,
}
#[derive(Clone, Debug)]
pub enum ObjectStoreResource {
S3(S3Resource),
Azure(AzureBlobResource),
Gcs(GcsResource),
}
impl ObjectStoreResource {
@@ -270,7 +259,6 @@ pub enum StorageResourceType {
AzureBlob,
S3AwsOidc,
AzureWorkloadIdentity,
GoogleCloudStorage,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -312,22 +300,6 @@ pub struct AzureBlobResource {
pub federated_token_file: Option<String>,
}
fn as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
serde_json::to_string(&v).map_err(serde::de::Error::custom)
}
#[derive(Debug, Deserialize, Clone)]
pub struct GcsResource {
pub bucket: String,
#[serde(rename = "serviceAccountKey")]
#[serde(deserialize_with = "as_string")]
pub service_account_key: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, Hash)]
pub struct S3AwsOidcResource {
#[serde(rename = "bucket")]
@@ -408,7 +380,6 @@ pub async fn build_object_store_client(
ObjectStoreResource::Azure(azure_blob_resource_ref) => {
build_azure_blob_client(&azure_blob_resource_ref)
}
ObjectStoreResource::Gcs(gcs_resource_ref) => build_gcs_client(&gcs_resource_ref).await,
}
}
@@ -604,59 +575,18 @@ fn build_azure_blob_client(
return Ok(Arc::new(store));
}
#[cfg(feature = "parquet")]
async fn build_gcs_client(gcs_resource_ref: &GcsResource) -> error::Result<Arc<dyn ObjectStore>> {
let gcs_resource = gcs_resource_ref.clone();
let mut store_builder = GoogleCloudStorageBuilder::new()
.with_client_options(
ClientOptions::new()
.with_timeout_disabled()
.with_default_headers(HeaderMap::from_iter(vec![(
"Accept-Encoding".parse().unwrap(),
"".parse().unwrap(),
)])),
)
.with_bucket_name(gcs_resource.bucket);
store_builder = store_builder.with_service_account_key(gcs_resource.service_account_key);
// if private key is malformed, it will panic => https://github.com/apache/arrow-rs-object-store/issues/419
let store = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| store_builder.build()))
.map_err(|panic_info| {
tracing::error!(
"Panic while building GCS object store client: {:?}",
panic_info
);
error::Error::internal_err(format!(
"Panic while building GCS object store client: {:?}",
panic_info
))
})?
.map_err(|err| {
tracing::error!("Error building GCS object store client: {:?}", err);
error::Error::internal_err(format!(
"Error building GCS object store client: {}",
err.to_string()
))
})?;
return Ok(Arc::new(store));
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "typ", content = "value")]
pub enum ObjectStoreSettings {
S3(S3Settings),
}
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(tag = "type")]
pub enum ObjectSettings {
S3(S3Settings),
Azure(AzureBlobResource),
AwsOidc(S3AwsOidcResource),
Gcs(GcsResource),
}
impl ObjectSettings {
@@ -665,7 +595,6 @@ impl ObjectSettings {
ObjectSettings::S3(s3_settings) => s3_settings.bucket.as_ref(),
ObjectSettings::Azure(azure_settings) => Some(&azure_settings.container_name),
ObjectSettings::AwsOidc(s3_aws_oidc_settings) => Some(&s3_aws_oidc_settings.bucket),
ObjectSettings::Gcs(gcs_settings) => Some(&gcs_settings.bucket),
}
}
}
@@ -699,12 +628,6 @@ pub async fn build_object_store_from_settings(
refresh: Some(ObjectStoreRefresh::new(settings.clone(), res.expiration())),
})
}
ObjectSettings::Gcs(gcs_settings) => {
let gcs_resource = gcs_settings;
build_gcs_client(&gcs_resource)
.await
.map(|x| ExpirableObjectStore::from(x))
}
}
}
+18 -28
View File
@@ -196,9 +196,22 @@ fn instance_name(hostname: &str) -> String {
}
const DEFAULT_WORKER_SUFFIX_LEN: usize = 5;
pub const SSH_AGENT_WORKER_SUFFIX: &'static str = "/ssh";
pub fn create_worker_suffix(hostname: &str) -> String {
format!("{}-{}", instance_name(hostname), rd_string(DEFAULT_WORKER_SUFFIX_LEN))
pub fn create_worker_suffix(hostname: &str, rd_string_len: usize, ssh_ag_worker: bool) -> String {
let mut wk_suffix = format!("{}-{}", instance_name(hostname), rd_string(rd_string_len));
if ssh_ag_worker {
wk_suffix.push_str(SSH_AGENT_WORKER_SUFFIX);
}
wk_suffix
}
pub fn create_ssh_agent_worker_suffix(hostname: &str) -> String {
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, true)
}
pub fn create_default_worker_suffix(hostname: &str) -> String {
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, false)
}
pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str) -> String {
@@ -739,31 +752,16 @@ pub trait WarnAfterExt: Future + Sized {
#[track_caller]
fn warn_after_seconds(self, seconds: u8) -> WarnAfterFuture<Self> {
let caller = Location::caller();
self.build_from_caller(seconds, caller, None)
}
fn build_from_caller(
self,
seconds: u8,
caller: &Location,
sql: Option<String>,
) -> WarnAfterFuture<Self> {
let location = format!("{}:{}", caller.file(), caller.line());
WarnAfterFuture {
future: self,
timeout: time::sleep(Duration::from_secs(seconds as u64)),
warned: false,
start_time: std::time::Instant::now(),
location,
location: location,
seconds,
sql,
}
}
#[track_caller]
fn warn_after_seconds_with_sql(self, seconds: u8, sql: String) -> WarnAfterFuture<Self> {
let caller = Location::caller();
self.build_from_caller(seconds, caller, Some(sql))
}
}
// Blanket implementation for all futures.
@@ -780,7 +778,6 @@ pin_project! {
location: String,
start_time: std::time::Instant,
seconds: u8,
sql: Option<String>,
}
}
@@ -790,20 +787,13 @@ impl<F: Future> Future for WarnAfterFuture<F> {
fn poll(self: Pin<&mut Self>, cx: &mut TContext<'_>) -> Poll<Self::Output> {
let this = self.project();
fn build_query_string(location: &str, sql: Option<&str>) -> String {
match sql {
Some(sql) => format!("{}: {}", location, sql),
None => location.to_string(),
}
}
// Poll the timeout future to check if it has elapsed.
if !*this.warned {
if this.timeout.poll(cx).is_ready() {
tracing::warn!(
location = this.location,
"SLOW_QUERY: query {} to db taking longer than expected (> {} seconds)",
build_query_string(&this.location, this.sql.as_deref()),
this.location,
this.seconds,
);
*this.warned = true;
@@ -818,7 +808,7 @@ impl<F: Future> Future for WarnAfterFuture<F> {
tracing::warn!(
location = this.location,
"SLOW_QUERY: completed query {} with total duration: {:.2?}",
build_query_string(&this.location, this.sql.as_deref()),
this.location,
elapsed
);
}
+1 -10
View File
@@ -428,7 +428,6 @@ pub async fn push_init_job<'c>(
worker_name,
"worker@windmill.dev",
SUPERADMIN_SECRET_EMAIL.to_string(),
Some("worker_init_job"),
None,
None,
None,
@@ -1213,7 +1212,6 @@ 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,
@@ -1737,7 +1735,6 @@ 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),
@@ -1845,7 +1842,6 @@ 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),
@@ -1934,7 +1930,6 @@ 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),
@@ -2225,7 +2220,6 @@ 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
@@ -3263,7 +3257,6 @@ 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>,
@@ -4379,14 +4372,12 @@ 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()),
}
};
@@ -4709,4 +4700,4 @@ pub async fn get_same_worker_job(
same_worker_job.job_id, e
))
})
}
}
-1
View File
@@ -271,7 +271,6 @@ 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,
-7
View File
@@ -775,13 +775,6 @@ async fn get_workspace_s3_resource_path(
resource_path.to_string(),
)
}
Some(LargeFileStorage::GoogleCloudStorage(gcs)) => {
let resource_path = gcs.gcs_resource_path.trim_start_matches("$res:");
(
StorageResourceType::GoogleCloudStorage,
resource_path.to_string(),
)
}
None => {
return Ok(None);
}
@@ -10,9 +10,9 @@ use windmill_queue::{CanceledBy, MiniPulledJob};
use serde::Deserialize;
use crate::common::build_args_map;
use crate::common::{build_http_client, resolve_job_timeout, OccupancyMetrics};
use crate::handle_child::run_future_with_polling_update_job_poller;
use crate::common::build_args_map;
use windmill_common::client::AuthedClient;
#[derive(Deserialize)]
@@ -107,20 +107,6 @@ pub async fn do_graphql(
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
// Check HTTP status before processing response
if !response.status().is_success() {
let status = response.status();
let error_body = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
return Err(Error::ExecutionErr(format!(
"GraphQL request failed with HTTP {}: {}",
status.as_u16(),
error_body
)));
}
let result_stream = response.bytes_stream();
let mut i = 0;
@@ -17,7 +17,12 @@ 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")]
+3 -5
View File
@@ -367,10 +367,6 @@ lazy_static::lazy_static! {
.and_then(|x| x.parse().ok())
.unwrap_or(false);
pub static ref OUTSTANDING_WAIT_TIME_THRESHOLD_MS: i64 = std::env::var("OUTSTANDING_WAIT_TIME_THRESHOLD_MS")
.ok()
.and_then(|x| x.parse::<i64>().ok())
.unwrap_or(1000);
}
type Envs = Vec<(String, String)>;
@@ -592,6 +588,8 @@ pub async fn drop_cache() {
}
}
const OUTSTANDING_WAIT_TIME_THRESHOLD_MS: i64 = 1000;
async fn insert_wait_time(
job_id: Uuid,
root_job_id: Option<Uuid>,
@@ -1622,7 +1620,7 @@ pub async fn run_worker(
.expect("send job completed END");
add_time!(bench, "sent job completed");
} else {
add_outstanding_wait_time(&conn, &job, *OUTSTANDING_WAIT_TIME_THRESHOLD_MS);
add_outstanding_wait_time(&conn, &job, OUTSTANDING_WAIT_TIME_THRESHOLD_MS);
#[cfg(feature = "prometheus")]
register_metric(
@@ -17,7 +17,6 @@ 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;
@@ -2031,7 +2030,6 @@ 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 {
@@ -2774,7 +2772,6 @@ 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),
@@ -608,7 +608,6 @@ async fn trigger_dependents_to_recompute_dependencies(
&created_by,
email,
permissioned_as.to_string(),
Some("trigger.dependents.to.recompute.dependencies"),
None,
None,
None,
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.502.2";
export const VERSION = "v1.501.3";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -63,7 +63,7 @@ export {
// }
// });
export const VERSION = "1.502.2";
export const VERSION = "1.501.3";
const command = new Command()
.name("wmill")
+2 -2
View File
@@ -1,8 +1,8 @@
FROM ghcr.io/windmill-labs/windmill:dev
# Rust
COPY --from=rust:1.88.0 /usr/local/cargo /usr/local/cargo
COPY --from=rust:1.88.0 /usr/local/rustup /usr/local/rustup
COPY --from=rust:1.86.0 /usr/local/cargo /usr/local/cargo
COPY --from=rust:1.86.0 /usr/local/rustup /usr/local/rustup
RUN /usr/local/cargo/bin/cargo install cargo-sweep --version ^0.7
# Ansible
+2 -2
View File
@@ -20,8 +20,8 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
FROM ghcr.io/windmill-labs/windmill-ee:dev
# Rust
COPY --from=rust:1.88.0 /usr/local/cargo /usr/local/cargo
COPY --from=rust:1.88.0 /usr/local/rustup /usr/local/rustup
COPY --from=rust:1.86.0 /usr/local/cargo /usr/local/cargo
COPY --from=rust:1.86.0 /usr/local/rustup /usr/local/rustup
RUN /usr/local/cargo/bin/cargo install cargo-sweep --version ^0.7
# Ansible
Generated
-16
View File
@@ -33,21 +33,6 @@
"type": "indirect"
}
},
"nixpkgs-claude": {
"locked": {
"lastModified": 1751271578,
"narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1736320768,
@@ -68,7 +53,6 @@
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nixpkgs-claude": "nixpkgs-claude",
"rust-overlay": "rust-overlay"
}
},
+2 -11
View File
@@ -3,11 +3,9 @@
nixpkgs.url = "nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
# Use separate channel for claude code. It always needs to be latest
nixpkgs-claude.url = "nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, nixpkgs-claude, flake-utils, rust-overlay }:
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
@@ -15,10 +13,6 @@
config.allowUnfree = true;
overlays = [ (import rust-overlay) ];
};
claude-code = (import nixpkgs-claude {
inherit system;
config.allowUnfree = true;
}).claude-code;
lib = pkgs.lib;
stdenv = pkgs.stdenv;
rust = pkgs.rust-bin.stable.latest.default.override {
@@ -101,10 +95,7 @@
};
devShells.default = pkgs.mkShell {
buildInputs = buildInputs ++ [
# To update run: `nix flake update nixpkgs-claude`
claude-code
] ++ (with pkgs; [
buildInputs = buildInputs ++ (with pkgs; [
# Essentials
rust
cargo-watch
-1
View File
@@ -4,7 +4,6 @@ node_modules
/.svelte-kit
/package
/src/lib/gen
.vite.temp.*.js
CaddyfileRemoteRuben
tests-out/
storageState.json
+12 -23
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.502.5",
"version": "1.501.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.502.5",
"version": "1.501.3",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -46,7 +46,6 @@
"hash-sum": "^2.0.0",
"highlight.js": "^11.8.0",
"idb": "^8.0.2",
"lru-cache": "^11.1.0",
"lucide-svelte": "^0.399.0",
"minimatch": "^10.0.1",
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~16.1.1",
@@ -151,7 +150,7 @@
"fsevents": "^2.3.3"
},
"peerDependencies": {
"svelte": "^5.0.0"
"svelte": "^4.0.0"
}
},
"node_modules/@alloc/quick-lru": {
@@ -7051,20 +7050,6 @@
"node": ">=10"
}
},
"node_modules/hosted-git-info/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/html-tags": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@@ -7837,12 +7822,16 @@
}
},
"node_modules/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"license": "ISC",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"peer": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": "20 || >=22"
"node": ">=10"
}
},
"node_modules/ltgt": {
+30 -46
View File
@@ -1,11 +1,11 @@
{
"name": "windmill-components",
"version": "1.502.2",
"version": "1.501.3",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"postinstall": "if [ -f ./scripts/untar_ui_builder.js ]; then node ./scripts/untar_ui_builder.js && node ./scripts/patch_files.js; fi",
"postinstall": "node scripts/untar_ui_builder.js && node scripts/patch_files.js",
"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 .",
@@ -91,10 +91,9 @@
"@redocly/json-to-json-schema": "^0.0.1",
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^1.0.0",
"@xterm/addon-fit": "^0.10.0",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.3.4",
@@ -113,7 +112,6 @@
"hash-sum": "^2.0.0",
"highlight.js": "^11.8.0",
"idb": "^8.0.2",
"lru-cache": "^11.1.0",
"lucide-svelte": "^0.399.0",
"minimatch": "^10.0.1",
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~16.1.1",
@@ -135,6 +133,7 @@
"svelte-exmarkdown": "^5.0.0",
"svelte-infinite-loading": "^1.4.0",
"svelte-tiny-virtual-list": "^2.0.5",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"tailwind-merge": "^1.13.2",
"vscode": "npm:@codingame/monaco-vscode-extension-api@~16.1.1",
"vscode-languageclient": "~9.0.1",
@@ -161,7 +160,7 @@
"zod-to-json-schema": "^3.24.5"
},
"peerDependencies": {
"svelte": "^5.0.0"
"svelte": "^4.0.0"
},
"exports": {
"./package.json": "./package.json",
@@ -169,16 +168,7 @@
"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",
@@ -264,20 +254,20 @@
"svelte": "./package/components/FlowStatusViewer.svelte",
"default": "./package/components/FlowStatusViewer.svelte"
},
"./components/FlowWrapper.svelte": {
"types": "./package/components/FlowWrapper.svelte.d.ts",
"svelte": "./package/components/FlowWrapper.svelte",
"default": "./package/components/FlowWrapper.svelte"
"./components/FlowBuilder.svelte": {
"types": "./package/components/FlowBuilder.svelte.d.ts",
"svelte": "./package/components/FlowBuilder.svelte",
"default": "./package/components/FlowBuilder.svelte"
},
"./components/AppWrapper.svelte": {
"types": "./package/components/AppWrapper.svelte.d.ts",
"svelte": "./package/components/AppWrapper.svelte",
"default": "./package/components/AppWrapper.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/ScriptWrapper.svelte": {
"types": "./package/components/ScriptWrapper.svelte.d.ts",
"svelte": "./package/components/ScriptWrapper.svelte",
"default": "./package/components/ScriptWrapper.svelte"
"./components/ScriptBuilder.svelte": {
"types": "./package/components/ScriptBuilder.svelte.d.ts",
"svelte": "./package/components/ScriptBuilder.svelte",
"default": "./package/components/ScriptBuilder.svelte"
},
"./components/FlowEditor.svelte": {
"types": "./package/components/flows/FlowEditor.svelte.d.ts",
@@ -294,10 +284,10 @@
"svelte": "./package/components/SchemaForm.svelte",
"default": "./package/components/SchemaForm.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/EditableSchemaWrapper.svelte": {
"types": "./package/components/schema/EditableSchemaWrapper.svelte.d.ts",
"svelte": "./package/components/schema/EditableSchemaWrapper.svelte",
"default": "./package/components/schema/EditableSchemaWrapper.svelte"
},
"./components/ResourceEditor.svelte": {
"types": "./package/components/ResourceEditor.svelte.d.ts",
@@ -460,14 +450,14 @@
"components/FlowBuilder.svelte": [
"./package/components/FlowBuilder.svelte.d.ts"
],
"components/AppWrapper.svelte": [
"./package/components/AppWrapper.svelte.d.ts"
"components/AppEditor.svelte": [
"./package/components/apps/editor/AppEditor.svelte.d.ts"
],
"components/ScriptWrapper.svelte": [
"./package/components/ScriptWrapper.svelte.d.ts"
"components/ScriptBuilder.svelte": [
"./package/components/ScriptBuilder.svelte.d.ts"
],
"components/FlowWrapper.svelte": [
"./package/components/FlowWrapper.svelte.d.ts"
"components/FlowEditor.svelte": [
"./package/components/flows/FlowEditor.svelte.d.ts"
],
"components/SchemaViewer.svelte": [
"./package/components/SchemaViewer.svelte.d.ts"
@@ -475,8 +465,8 @@
"components/SchemaEditor.svelte": [
"./package/components/SchemaEditor.svelte.d.ts"
],
"components/EditableSchemaSdkWrapper.svelte": [
"./package/components/schema/EditableSchemaSdkWrapper.svelte.d.ts"
"components/EditableSchemaWrapper.svelte": [
"./package/components/schema/EditableSchemaWrapper.svelte.d.ts"
],
"components/flows/FlowHistoryInner.svelte": [
"./package/components/flows/FlowHistoryInner.svelte.d.ts"
@@ -528,12 +518,6 @@
],
"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"
]
}
},
@@ -541,4 +525,4 @@
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
"fsevents": "^2.3.3"
}
}
}
+10 -17
View File
@@ -1,23 +1,6 @@
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'
@@ -35,6 +18,16 @@ 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 {
@@ -1,4 +1,4 @@
import { triggerablesByAi } from '$lib/components/copilot/chat/sharedChatState.svelte'
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
export interface TriggerableByAIOptions {
id?: string
@@ -60,15 +60,15 @@ export function triggerableByAI(element: HTMLElement, options: TriggerableByAIOp
// register the triggerable
const currentData = { description, onTrigger: handleTrigger }
triggerablesByAi[id] = currentData
aiChatManager.triggerablesByAI[id] = currentData
}
function unregister() {
if (isDisabled || !id) return
// unregister the triggerable
if (triggerablesByAi[id]) {
delete triggerablesByAi[id]
if (aiChatManager.triggerablesByAI[id]) {
delete aiChatManager.triggerablesByAI[id]
}
}
@@ -77,7 +77,7 @@ export function triggerableByAI(element: HTMLElement, options: TriggerableByAIOp
return {
update(newOptions: TriggerableByAIOptions) {
; ({ id, description, callback, showAnimation = true } = newOptions)
;({ id, description, callback, showAnimation = true } = newOptions)
register()
},
destroy() {
+503 -17
View File
@@ -1,18 +1,29 @@
<script lang="ts">
import { OauthService, type ResourceType } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import {
OauthService,
GitSyncService,
type ResourceType,
type GetGlobalConnectedRepositoriesResponse
} from '$lib/gen'
import {
workspaceStore,
enterpriseLicense,
userWorkspaces,
userStore,
workspaceColor
} from '$lib/stores'
import { base } from '$lib/base'
import { emptySchema, emptyString } from '$lib/utils'
import SchemaForm from './SchemaForm.svelte'
import type SimpleEditor from './SimpleEditor.svelte'
import Toggle from './Toggle.svelte'
import TestConnection from './TestConnection.svelte'
import SupabaseIcon from './icons/SupabaseIcon.svelte'
import { sendUserToast } from '$lib/toast'
import Popover from './meltComponents/Popover.svelte'
import Button from './common/button/Button.svelte'
import { Loader2 } from 'lucide-svelte'
import { untrack } from 'svelte'
import { base } from '$lib/base'
import GitHubAppIntegration from './GitHubAppIntegration.svelte'
import { Loader2, Github, RotateCw, Plus, Minus, Download } from 'lucide-svelte'
import { onDestroy, untrack } from 'svelte'
interface Props {
resourceType: string
@@ -39,6 +50,109 @@
let supabaseWizard = $state(false)
let loadingGithubInstallations = $state(false)
let githubInstallations: GetGlobalConnectedRepositoriesResponse = $state([])
let workspaceGithubInstallations: GetGlobalConnectedRepositoriesResponse = $state([])
let selectedGHAppAccountId: string | undefined = $state(undefined)
let selectedGHAppRepository: string | undefined = $state(undefined)
let githubInstallationUrl: string | undefined = $state(undefined)
let installationCheckInterval: number | undefined = undefined
let isCheckingInstallation = $state(false)
let importJwt = $state('')
let githubAppPopover: { open: () => void; close: () => void } | null = $state(null)
async function loadGithubInstallations() {
if (!$enterpriseLicense) return
try {
loadingGithubInstallations = true
// Reset for reactivity
githubInstallations = []
workspaceGithubInstallations = []
const installations = await GitSyncService.getGlobalConnectedRepositories()
githubInstallations = installations
workspaceGithubInstallations = githubInstallations.filter(
(_) => _.workspace_id === $workspaceStore
)
const state = encodeURIComponent(
JSON.stringify({
workspace_id: $workspaceStore,
base_url: window.location.origin + base
})
)
githubInstallationUrl = `https://github.com/apps/windmill-sync-helper/installations/new?state=${state}`
} catch (err) {
console.error(err)
sendUserToast('Failed to load GitHub installations', true)
githubInstallations = []
workspaceGithubInstallations = []
} finally {
loadingGithubInstallations = false
}
}
function startInstallationCheck() {
isCheckingInstallation = true
installationCheckInterval = window.setInterval(async () => {
const installations = await GitSyncService.getGlobalConnectedRepositories()
if (installations.length > 0) {
stopInstallationCheck()
githubInstallations = installations
workspaceGithubInstallations = githubInstallations.filter(
(_) => _.workspace_id === $workspaceStore
)
// Open the popover with a small delay as otherwise it doesn't open
setTimeout(() => {
githubAppPopover?.open()
}, 100)
}
}, 2000)
}
function stopInstallationCheck() {
if (installationCheckInterval) {
clearInterval(installationCheckInterval)
installationCheckInterval = undefined
}
isCheckingInstallation = false
}
// Clean up interval when component is destroyed
onDestroy(() => {
stopInstallationCheck()
})
function getRepositories(accountId: string) {
return githubInstallations.find((_) => _.account_id === accountId)?.repositories || []
}
async function addInstallationToWorkspace(
installation_id: number | undefined,
workspaceId: string | undefined
) {
if (!installation_id || !workspaceId || !$workspaceStore) {
sendUserToast('Installation or workspace invalid', true)
return
}
try {
await GitSyncService.installFromWorkspace({
workspace: $workspaceStore,
requestBody: {
source_workspace_id: workspaceId,
installation_id: installation_id
}
})
sendUserToast('Successfully added installation to workspace', false)
await loadGithubInstallations()
} catch (err) {
console.error(err)
sendUserToast('Failed to add installation to workspace', true)
}
}
async function isSupabaseAvailable() {
try {
supabaseWizard =
@@ -113,6 +227,22 @@
}
}
function applyRepositoryURL(close: (_: any) => void) {
if (!selectedGHAppRepository) return
rawCode = JSON.stringify(
{
...args,
url: selectedGHAppRepository,
is_github_app: true
},
null,
2
)
description = `Repository ${selectedGHAppRepository} with permissions fetched using Windmill Github App. ${description ?? ''}`
rawCodeEditor?.setCode(rawCode)
close(null)
}
let rawCodeEditor: SimpleEditor | undefined = $state(undefined)
let textFileContent: string | undefined = $state(undefined)
@@ -121,6 +251,79 @@
content: textFileContent
}
}
async function deleteInstallation(installation_id: number) {
if (!$workspaceStore) {
sendUserToast('Failed to delete installation', true)
return
}
try {
await GitSyncService.deleteFromWorkspace({
workspace: $workspaceStore,
installationId: installation_id
})
sendUserToast('Successfully deleted installation', false)
await loadGithubInstallations()
} catch (err) {
console.error(err)
sendUserToast('Failed to delete installation', true)
}
}
async function exportInstallation(installationId: number) {
if (!$workspaceStore) {
sendUserToast('Failed to export installation', true)
return
}
try {
const response = await GitSyncService.exportInstallation({
workspace: $workspaceStore,
installationId: installationId
})
if (!response.jwt_token) {
sendUserToast('Failed to export installation', true)
return
}
// Copy to clipboard
await navigator.clipboard.writeText(response.jwt_token)
sendUserToast(
'JWT token copied to clipboard. This token is sensitive and should be kept secret!',
false,
undefined,
undefined,
10000
)
} catch (error) {
console.error(error)
sendUserToast('Failed to export installation', true)
}
}
async function importInstallation(jwt: string) {
if (!$workspaceStore) {
sendUserToast('Failed to import installation', true)
return
}
try {
await GitSyncService.importInstallation({
workspace: $workspaceStore,
requestBody: { jwt_token: jwt }
})
importJwt = ''
sendUserToast('Installation imported successfully', false)
await loadGithubInstallations()
} catch (error) {
sendUserToast('Failed to import installation', true)
}
}
function handleInstallClick() {
if (githubInstallations.length === 0) {
if (!isCheckingInstallation) {
startInstallationCheck()
}
}
}
$effect(() => {
$workspaceStore && untrack(() => loadSchema())
})
@@ -136,6 +339,19 @@
$effect(() => {
resourceType == 'postgresql' && untrack(() => isSupabaseAvailable())
})
$effect(() => {
resourceType == 'git_repository' &&
$userStore?.is_admin &&
untrack(() => loadGithubInstallations())
})
let githubInstallationsNotInWorkspace = $derived(
githubInstallations.filter((installation) => {
return !workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
})
)
</script>
{#if !notFound}
@@ -204,17 +420,287 @@
<div class="text-[#11181C] dark:text-[#EDEDED] font-semibold">Connect Supabase</div>
</a>
{/if}
<GitHubAppIntegration
{resourceType}
{args}
{description}
onArgsUpdate={(newArgs) => {
args = newArgs
rawCode = JSON.stringify(args, null, 2)
rawCodeEditor?.setCode(rawCode)
}}
onDescriptionUpdate={(newDescription) => (description = newDescription)}
/>
{#if resourceType == 'git_repository' && $workspaceStore && $userStore?.is_admin}
{#if !loadingGithubInstallations}
<Button
color="light"
variant="contained"
size="xs"
on:click={loadGithubInstallations}
disabled={!$enterpriseLicense}
startIcon={{ icon: RotateCw }}
/>
{:else}
<Loader2 class="animate-spin w-10 h-4" />
{/if}
{#if githubInstallations.length > 0}
<Popover
documentationLink="https://www.windmill.dev/docs/integrations/git_repository#github-app"
bind:this={githubAppPopover}
floatingConfig={{
placement: 'bottom'
}}
disabled={!$enterpriseLicense || loadingGithubInstallations}
>
{#snippet trigger()}
<Button
color="none"
variant="border"
size="xs"
disabled={!$enterpriseLicense || loadingGithubInstallations}
startIcon={{
icon: loadingGithubInstallations ? Loader2 : Github,
classes: loadingGithubInstallations ? 'animate-spin' : ''
}}
nonCaptureEvent
>
{$enterpriseLicense ? 'GitHub App' : 'GitHub App (ee only)'}
</Button>
{/snippet}
{#snippet content({ close })}
<div class="block text-primary p-4">
<div class="flex flex-col gap-4 w-[600px]">
{#if workspaceGithubInstallations.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary">Select Repository</p>
<div class="flex flex-row gap-2 w-full">
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">Github Account ID</p>
<select bind:value={selectedGHAppAccountId}>
<option value="" disabled>Select GitHub Account ID</option>
{#each workspaceGithubInstallations as installation}
<option value={installation.account_id}
>{installation.account_id}</option
>
{/each}
</select>
</div>
{#if selectedGHAppAccountId}
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">Repository</p>
<div class="flex flex-row gap-2">
<select bind:value={selectedGHAppRepository}>
<option value="" disabled selected>Select Repository</option>
{#each getRepositories(selectedGHAppAccountId) as repository}
<option value={repository.url}>{repository.name}</option>
{/each}
</select>
</div>
</div>
{/if}
<div class="pt-[26px]">
<Button
size="xs"
color="blue"
buttonType="button"
disabled={!selectedGHAppRepository}
on:click={() => {
applyRepositoryURL(close)
}}
>
Apply
</Button>
</div>
</div>
</div>
{/if}
<div
class={`${
workspaceGithubInstallations.length > 0
? 'border-t border-gray-200 dark:border-gray-700'
: ''
} pt-4`}
>
<div class="flex flex-col gap-4">
<div class="flex">
<Button
color="none"
variant="border"
size="xs"
href={githubInstallationUrl}
startIcon={{ icon: Plus }}
target="_blank"
>
Add new installation
</Button>
</div>
{#if workspaceGithubInstallations.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary">Current installations:</p>
<div class="flex flex-col gap-1">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-tertiary">
<th class="pb-2 w-1/3">Org</th>
<th class="pb-2 w-1/6">Workspace</th>
<th class="pb-2 w-1/6">Repos</th>
<th class="pb-2 w-1/3"></th>
</tr>
</thead>
<tbody>
{#each workspaceGithubInstallations as installation}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
{#if $workspaceColor}
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs"
style="background-color: {$workspaceColor}20; color: {$workspaceColor}"
>
{installation.workspace_id}
</span>
{:else}
<span class="text-xs text-tertiary"
>{installation.workspace_id}</span
>
{/if}
</td>
<td class="py-2 text-tertiary">
{installation.repositories.length} repos
</td>
<td class="py-2 text-right">
<div class="flex justify-end gap-1">
<Button
size="xs2"
color="blue"
title="Export installation to other instance"
startIcon={{ icon: Download }}
on:click={() =>
exportInstallation(installation.installation_id)}
>
Export
</Button>
<Button
size="xs2"
color="red"
title="Remove installation from workspace"
startIcon={{ icon: Minus }}
on:click={() =>
deleteInstallation(installation.installation_id)}
>
Remove
</Button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
{#if githubInstallationsNotInWorkspace.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary"
>Installations in other workspaces:</p
>
<div class="flex flex-col gap-1">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-tertiary">
<th class="pb-2 w-1/3">Org</th>
<th class="pb-2 w-1/6">Workspace</th>
<th class="pb-2 w-1/6">Repos</th>
<th class="pb-2 w-1/3"></th>
</tr>
</thead>
<tbody>
{#each githubInstallationsNotInWorkspace as installation}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
{#if $userWorkspaces.find((w) => w.id === installation.workspace_id)?.color}
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs"
style="background-color: {$userWorkspaces.find(
(w) => w.id === installation.workspace_id
)?.color}20; color: {$userWorkspaces.find(
(w) => w.id === installation.workspace_id
)?.color}"
>
{installation.workspace_id}
</span>
{:else}
<span class="text-xs text-tertiary"
>{installation.workspace_id}</span
>
{/if}
</td>
<td class="py-2 text-tertiary">
{installation.repositories.length} repos
</td>
<td class="pl-8 py-2 text-right">
<Button
size="xs2"
color="blue"
title="Add installation to workspace"
startIcon={{ icon: Plus }}
on:click={() =>
addInstallationToWorkspace(
installation.installation_id,
installation.workspace_id
)}
>
Add to workspace
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</div>
</div>
<div class="mt-4 flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary"
>Import installation from other instance:</p
>
<div class="flex gap-2">
<input
type="text"
placeholder="Paste JWT token here"
bind:value={importJwt}
class="flex-1"
/>
<Button
color="blue"
on:click={() => importInstallation(importJwt)}
disabled={!importJwt}
>
Import
</Button>
</div>
</div>
</div>
</div>
{/snippet}
</Popover>
{:else}
<Button
color="none"
variant="border"
size="xs"
disabled={!$enterpriseLicense || loadingGithubInstallations}
startIcon={{
icon: loadingGithubInstallations || isCheckingInstallation ? Loader2 : Github,
classes: loadingGithubInstallations || isCheckingInstallation ? 'animate-spin' : ''
}}
href={githubInstallationUrl}
target="_blank"
on:click={handleInstallClick}
>
{$enterpriseLicense
? isCheckingInstallation
? 'Waiting for installation...'
: 'Install GitHub App'
: 'GitHub App (ee only)'}
</Button>
{/if}
{/if}
</div>
{:else}
<p class="italic text-tertiary text-xs mb-4"
@@ -1,10 +0,0 @@
<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} />
+6 -5
View File
@@ -7,7 +7,8 @@
setInputCat as computeInputCat,
debounce,
emptyString,
getSchemaFromProperties
getSchemaFromProperties,
clone
} from '$lib/utils'
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
@@ -244,7 +245,7 @@
nvalue = undefined
}
if ((value == undefined || value == null) && !ignoreValueUndefined) {
nvalue = structuredClone($state.snapshot(defaultValue))
nvalue = clone(defaultValue)
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
nvalue = nullable ? null : ''
@@ -296,7 +297,7 @@
lastValue = value
}
let oldDefaultValue = structuredClone($state.snapshot(defaultValue))
let oldDefaultValue = clone(defaultValue)
function handleDefaultValueChange() {
if (
deepEqual(value, oldDefaultValue) &&
@@ -305,7 +306,7 @@
) {
value = defaultValue
}
oldDefaultValue = structuredClone($state.snapshot(defaultValue))
oldDefaultValue = clone(defaultValue)
}
function isObjectCat(inputCat?: string) {
@@ -846,7 +847,7 @@
format={format ?? ''}
bind:value
bind:editor
onClear={() => {
on:clear={() => {
defaultValue = null
}}
{showSchemaExplorer}
@@ -1,8 +0,0 @@
<script lang="ts">
import ComponentInner from './apps/editor/component/ComponentInner.svelte'
// import AppPreview from './apps/editor/AppPreview.svelte'
// AppPreview
ComponentInner
</script>
@@ -4,13 +4,9 @@
import { autoPlacement } from '@floating-ui/core'
import ChangeInstanceUsernameInner from './ChangeInstanceUsernameInner.svelte'
interface Props {
email: string
username: string
isConflict?: boolean
}
let { email, username, isConflict = false }: Props = $props()
export let email: string
export let username: string
export let isConflict = false
</script>
<Popover
@@ -23,12 +19,12 @@
}}
closeButton
>
{#snippet trigger()}
<svelte:fragment slot="trigger">
<Button color={isConflict ? 'red' : 'light'} size="xs" spacingSize="xs2" nonCaptureEvent={true}
>{isConflict ? 'Fix username conflict' : 'Change username'}</Button
>
{/snippet}
{#snippet content()}
</svelte:fragment>
<svelte:fragment slot="content">
<ChangeInstanceUsernameInner
{email}
{username}
@@ -36,5 +32,5 @@
on:close={() => close()}
on:renamed
/>
{/snippet}
</svelte:fragment>
</Popover>
@@ -1,8 +1,9 @@
<script lang="ts">
import { page } from '$app/stores'
import { workspaceStore } from '$lib/stores'
import ClipboardPanel from './details/ClipboardPanel.svelte'
let url = $derived(`${window.location.protocol}//${window.location.hostname}/`)
$: url = `${$page.url.protocol}//${$page.url.hostname}/`
</script>
<div>
+5 -10
View File
@@ -3,24 +3,19 @@
import { Button } from './common'
import { Clock } from 'lucide-svelte'
import Popover from './meltComponents/Popover.svelte'
interface Props {
children?: import('svelte').Snippet<[any]>
}
let { children }: Props = $props()
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }} closeButton>
{#snippet trigger()}
<svelte:fragment slot="trigger">
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: Clock }}>
Use simplified builder
</Button>
{/snippet}
{#snippet content({ close })}
</svelte:fragment>
<svelte:fragment slot="content" let:close>
<Section label="CRON Builder" wrapperClass="p-4">
<div class="flex flex-col w-72">
{@render children?.({ close })}
<slot {close} />
</div>
</Section>
{/snippet}
</svelte:fragment>
</Popover>
+192 -208
View File
@@ -10,35 +10,23 @@
import Select from './select/Select.svelte'
import MultiSelect from './select/MultiSelect.svelte'
import { safeSelectItems } from './select/utils.svelte'
import { untrack } from 'svelte'
interface Props {
schedule: string
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
timezone: string // = Intl.DateTimeFormat().resolvedOptions().timeZone
disabled?: boolean
validCRON?: boolean
cronVersion?: string
}
export let schedule: string
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
export let timezone: string // = Intl.DateTimeFormat().resolvedOptions().timeZone
export let disabled = false
export let validCRON = true
export let cronVersion: string = 'v2'
let {
schedule = $bindable(),
timezone = $bindable(),
disabled = false,
validCRON = $bindable(true),
cronVersion = $bindable('v2')
}: Props = $props()
let preview: string[] = $state([])
let preview: string[] = []
// If the user has already entered a cron string, switching to the basic tab will override it.
let executeEvery: 'second' | 'minute' | 'hour' | 'day-month' | 'month' | 'day-week' =
$state('minute')
let executeEvery: 'second' | 'minute' | 'hour' | 'day-month' | 'month' | 'day-week' = 'minute'
let seconds = $state(30)
let minutes = $state(30)
let hours = $state(1)
let seconds = 30
let minutes = 30
let hours = 1
const daysOfMonthOptions: number[] = Array.from(Array(31).keys()).map((i) => i + 1)
let daysOfMonth: number[] = $state([])
let daysOfMonth: number[] = []
// let lastDayOfMonth = false
const monthsOfYearOptions: string[] = [
'January',
@@ -54,7 +42,7 @@
'November',
'December'
]
let monthsOfYear: string[] = $state([])
let monthsOfYear: string[] = []
const daysOfWeekOptions: string[] = [
'Sunday',
'Monday',
@@ -64,8 +52,10 @@
'Friday',
'Saturday'
]
let daysOfWeek: string[] = $state([])
let UTCTime: string = $state('')
let daysOfWeek: string[] = []
let UTCTime: string = ''
$: !emptyString(schedule) && handleScheduleInput(schedule, timezone)
async function handleScheduleInput(input: string, timezone: string): Promise<void> {
try {
@@ -83,60 +73,9 @@
}
}
let nschedule = $state('')
let nschedule = ''
function formatDate(timezone) {
try {
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
timeZoneName: 'short'
}).format
} catch (ee) {
sendUserToast(
`Invalid timezone: ${timezone}. Update your browser's timezone preference`,
true
)
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'Europe/Paris',
timeZoneName: 'short'
}).format
}
}
const items = Object.keys(timezones)
.map((key) => {
return Object.keys(timezones[key])
.map((subKey) => {
return {
value: subKey,
label: subKey,
group: timezones[key][subKey][1] as string
}
})
.flat()
})
.flat()
$effect(() => {
schedule
untrack(() => {
!emptyString(schedule) && handleScheduleInput(schedule, timezone)
})
})
$effect(() => {
$: {
// CRON string format
// sec min hour day of month month day of week year
// 0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2
@@ -194,18 +133,65 @@
} else if (executeEvery === 'day-week') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} * * ${s_daysOfWeek}`
}
})
let dateFormatter = $derived(formatDate(timezone))
}
$: dateFormatter = formatDate(timezone)
function formatDate(timezone) {
try {
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
timeZoneName: 'short'
}).format
} catch (ee) {
sendUserToast(
`Invalid timezone: ${timezone}. Update your browser's timezone preference`,
true
)
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'Europe/Paris',
timeZoneName: 'short'
}).format
}
}
const items = Object.keys(timezones)
.map((key) => {
return Object.keys(timezones[key])
.map((subKey) => {
return {
value: subKey,
label: subKey,
group: timezones[key][subKey][1] as string
}
})
.flat()
})
.flat()
</script>
<div class="w-full flex space-x-8">
<div class="w-full flex flex-col gap-4">
<Label label="Cron" class="font-semibold" primary={true}>
{#snippet error()}
<svelte:fragment slot="error">
{#if !validCRON}
<div class="text-red-600 text-xs"> Invalid cron syntax </div>
{/if}
{/snippet}
</svelte:fragment>
<div class="flex flex-row-reverse text-2xs text-tertiary -mt-1 hover:underline">
<a
class="text-tertiary"
@@ -245,145 +231,143 @@
{#if !disabled}
<div class="flex flex-row gap-2 mb-2">
<CronBuilder>
{#snippet children({ close })}
<div class="w-full flex flex-col">
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Execute schedule every</div>
<CronBuilder let:close>
<div class="w-full flex flex-col">
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Execute schedule every</div>
<div class="w-full flex gap-4">
<div class="w-full flex flex-col gap-1 mb-2">
<select
{disabled}
name="execute_every"
id="execute_every"
bind:value={executeEvery}
>
<option value="second">Second(s)</option>
<option value="minute">Minute(s)</option>
<option value="hour">Hour(s)</option>
<option value="day-month">Day of the month</option>
<option value="month">Month(s)</option>
<option value="day-week">Day of the week</option>
</select>
</div>
<div class="w-full flex gap-4">
<div class="w-full flex flex-col gap-1 mb-2">
<select
{disabled}
name="execute_every"
id="execute_every"
bind:value={executeEvery}
>
<option value="second">Second(s)</option>
<option value="minute">Minute(s)</option>
<option value="hour">Hour(s)</option>
<option value="day-month">Day of the month</option>
<option value="month">Month(s)</option>
<option value="day-week">Day of the week</option>
</select>
</div>
<div class="w-full flex flex-col gap-1 justify-center">
{#if executeEvery == 'second'}
<input {disabled} type="number" min="0" max="59" bind:value={seconds} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'minute'}
<input {disabled} type="number" min="0" max="59" bind:value={minutes} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'hour'}
<input {disabled} type="number" min="0" max="23" bind:value={hours} />
<small>Valid range 0-23</small>
{:else if executeEvery == 'day-month'}
<!-- <div class="w-full flex">
<div class="w-full flex flex-col gap-1 justify-center">
{#if executeEvery == 'second'}
<input {disabled} type="number" min="0" max="59" bind:value={seconds} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'minute'}
<input {disabled} type="number" min="0" max="59" bind:value={minutes} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'hour'}
<input {disabled} type="number" min="0" max="23" bind:value={hours} />
<small>Valid range 0-23</small>
{:else if executeEvery == 'day-month'}
<!-- <div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div> -->
{/if}
</div>
</div>
</div>
<div class="w-full flex flex-col gap-4">
{#if executeEvery == 'month'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={monthsOfYear}
items={safeSelectItems(monthsOfYearOptions)}
placeholder="Every month"
/>
</div>
{/if}
{#if executeEvery == 'day-week'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfWeek}
items={safeSelectItems(daysOfWeekOptions)}
placeholder="Every day"
/>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month'}
<div class="w-full flex flex-col gap-1">
{#if executeEvery == 'month'}
<small class="font-bold">On day of the month</small>
{/if}
<div class="w-full flex gap-4">
<div class="w-full flex">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfMonth}
items={safeSelectItems(daysOfMonthOptions)}
placeholder="Every day"
/>
</div>
<!-- {#if executeEvery == 'month'}
<div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div> -->
{/if}
</div>
{/if} -->
</div>
<small>Schedule will only execute on valid calendar days</small>
</div>
</div>
{/if}
<div class="w-full flex flex-col gap-4">
{#if executeEvery == 'month'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={monthsOfYear}
items={safeSelectItems(monthsOfYearOptions)}
placeholder="Every month"
/>
</div>
{/if}
{#if executeEvery == 'day-week'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfWeek}
items={safeSelectItems(daysOfWeekOptions)}
placeholder="Every day"
/>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month'}
<div class="w-full flex flex-col gap-1">
{#if executeEvery == 'month'}
<small class="font-bold">On day of the month</small>
{/if}
<div class="w-full flex gap-4">
<div class="w-full flex">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfMonth}
items={safeSelectItems(daysOfMonthOptions)}
placeholder="Every day"
/>
</div>
<!-- {#if executeEvery == 'month'}
<div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div>
{/if} -->
</div>
<small>Schedule will only execute on valid calendar days</small>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">At Time</small>
<input
{disabled}
type="time"
name="atUTCTime"
id="atUTCTime"
bind:value={UTCTime}
/>
</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Preview New Cron</div>
<div class="flex p-2 px-4 rounded-md bg-surface-secondary">
<span>{nschedule}</span>
{#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">At Time</small>
<input
{disabled}
type="time"
name="atUTCTime"
id="atUTCTime"
bind:value={UTCTime}
/>
</div>
</div>
{/if}
</div>
<div class="mt-4">
<Button
color="dark"
size="xs"
on:click={() => {
schedule = nschedule
close()
}}
>
Set cron schedule
</Button>
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Preview New Cron</div>
<div class="flex p-2 px-4 rounded-md bg-surface-secondary">
<span>{nschedule}</span>
</div>
</div>
{/snippet}
</div>
<div class="mt-4">
<Button
color="dark"
size="xs"
on:click={() => {
schedule = nschedule
close()
}}
>
Set cron schedule
</Button>
</div>
</CronBuilder>
<CronGen bind:schedule bind:cronVersion />
</div>
+1 -3
View File
@@ -187,9 +187,7 @@
<div class="flex gap-1 w-full justify-between items-center text-xs text-primary p-2">
<div>
<Popover>
{#snippet text()}
Download
{/snippet}
<svelte:fragment slot="text">Download</svelte:fragment>
<Button
startIcon={{ icon: Download }}
color="light"
+6 -13
View File
@@ -6,21 +6,14 @@
import DefaultTagsInner from './DefaultTagsInner.svelte'
interface Props {
defaultTagPerWorkspace?: boolean | undefined
defaultTagWorkspaces?: string[]
}
let {
defaultTagPerWorkspace = $bindable(undefined),
defaultTagWorkspaces = $bindable([])
}: Props = $props()
export let defaultTagPerWorkspace: boolean | undefined = undefined
export let defaultTagWorkspaces: string[] = []
let placement: 'bottom-end' | 'top-end' = 'bottom-end'
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: placement }} contentClasses="p-4">
{#snippet trigger()}
<svelte:fragment slot="trigger">
<Button color="dark" size="xs" nonCaptureEvent={true}>
<div class="flex flex-row gap-1 items-center"
><Pen size={14} /> Default tags&nbsp;<Tooltip light
@@ -29,8 +22,8 @@
></div
>
</Button>
{/snippet}
{#snippet content()}
</svelte:fragment>
<svelte:fragment slot="content">
<DefaultTagsInner bind:defaultTagPerWorkspace bind:defaultTagWorkspaces />
{/snippet}
</svelte:fragment>
</Popover>
@@ -51,12 +51,10 @@
}
}}
{hideDropdown}
on:dropdownOpen={({ detail }) => {
dropdownOpen = detail
}}
on:dropdownOpen={({ detail }) => (dropdownOpen = detail)}
>
Deploy
{#snippet tooltip()}
<svelte:fragment slot="tooltip">
<div class="flex flex-row gap-2 w-80 p-4 bg-surface rounded-lg shadow-lg border z-[5001]">
<input
type="text"
@@ -78,5 +76,5 @@
Deploy
</Button>
</div>
{/snippet}
</svelte:fragment>
</Button>
+5 -4
View File
@@ -524,21 +524,22 @@
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.pre(() => {
$effect(() => {
if (token) {
OpenAPI.WITH_CREDENTIALS = true
OpenAPI.TOKEN = token
untrack(() => loadUser())
}
})
$effect.pre(() => {
$effect(() => {
if (workspace) {
$workspaceStore = workspace
untrack(() => setupCopilotInfo())
}
})
$effect.pre(() => {
$effect(() => {
if (workspace && token) {
untrack(() => loadUser())
untrack(() => setupCopilotInfo())
}
})
$effect(() => {
@@ -9,7 +9,8 @@
orderedJsonStringify,
orderedYamlStringify,
replaceFalseWithUndefined,
type Value
type Value,
clone
} from '$lib/utils'
import type { Script } from '$lib/gen'
@@ -68,7 +69,7 @@
}
function prepareDiff(data: Value) {
const metadata = structuredClone(cleanValueProperties(replaceFalseWithUndefined(data)))
const metadata = clone(cleanValueProperties(replaceFalseWithUndefined(data)))
const content = metadata['content']
if (metadata['content'] !== undefined) {
metadata['content'] = 'check content diff'
@@ -13,8 +13,7 @@
Table2,
Braces,
Highlighter,
ArrowDownFromLine,
Loader2
ArrowDownFromLine
} from 'lucide-svelte'
import Portal from '$lib/components/Portal.svelte'
import DisplayResultControlBar from './DisplayResultControlBar.svelte'
@@ -35,6 +34,7 @@
import { convertJsonToCsv } from './table/tableUtils'
import Tooltip from './Tooltip.svelte'
import HighlightTheme from './HighlightTheme.svelte'
import PdfViewer from './display/PdfViewer.svelte'
import type { DisplayResultUi } from './custom_ui'
import { getContext, hasContext, createEventDispatcher, onDestroy } from 'svelte'
import { toJsonStr } from '$lib/utils'
@@ -634,14 +634,10 @@
</div>
{:else if !forceJson && resultKind === 'pdf'}
<div class="h-96 mt-2 border">
{#await import('$lib/components/display/PdfViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
allowFullscreen
source="data:application/pdf;base64,{contentOrRootString(result.pdf)}"
/>
{/await}
<PdfViewer
allowFullscreen
source="data:application/pdf;base64,{contentOrRootString(result.pdf)}"
/>
</div>
{:else if !forceJson && resultKind === 'plain'}<div class="h-full text-2xs"
><pre class="whitespace-pre-wrap"
@@ -813,22 +809,18 @@
</div>
{:else if result?.s3?.endsWith('.pdf')}
<div class="h-96 mt-2 border">
{#await import('$lib/components/display/PdfViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
allowFullscreen
source="{`/api/w/${workspaceId}/${
appPath
? 'apps_u/download_s3_file/' + appPath
: 'job_helpers/load_image_preview'
}?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(result.s3)}` +
(result.storage ? `&storage=${result.storage}` : '')}{appPath &&
result.presigned
? `&${result.presigned}`
: ''}"
/>
{/await}
<PdfViewer
allowFullscreen
source="{`/api/w/${workspaceId}/${
appPath
? 'apps_u/download_s3_file/' + appPath
: 'job_helpers/load_image_preview'
}?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(result.s3)}` +
(result.storage ? `&storage=${result.storage}` : '')}{appPath &&
result.presigned
? `&${result.presigned}`
: ''}"
/>
</div>
{/if}
{/if}
@@ -901,16 +893,12 @@
{/if}
{:else if s3object?.s3?.endsWith('.pdf')}
<div class="h-96 mt-2 border" data-interactive>
{#await import('$lib/components/display/PdfViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
allowFullscreen
source={`/api/w/${workspaceId}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
s3object.s3
)}` + (s3object.storage ? `&storage=${s3object.storage}` : '')}
/>
{/await}
<PdfViewer
allowFullscreen
source={`/api/w/${workspaceId}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
s3object.s3
)}` + (s3object.storage ? `&storage=${s3object.storage}` : '')}
/>
</div>
{/if}
{/each}
@@ -6,27 +6,14 @@
import type { DisplayResultUi } from './custom_ui'
import { createEventDispatcher } from 'svelte'
interface Props {
customUi?: DisplayResultUi | undefined
filename?: string | undefined
workspaceId?: string | undefined
jobId?: string | undefined
nodeId?: string | undefined
base: string
result: any
disableTooltips?: boolean
}
let {
customUi = undefined,
filename = undefined,
workspaceId = undefined,
jobId = undefined,
nodeId = undefined,
base,
result,
disableTooltips = false
}: Props = $props()
export let customUi: DisplayResultUi | undefined = undefined
export let filename: string | undefined = undefined
export let workspaceId: string | undefined = undefined
export let jobId: string | undefined = undefined
export let nodeId: string | undefined = undefined
export let base: string
export let result: any
export let disableTooltips: boolean = false
const dispatch = createEventDispatcher()
@@ -55,19 +42,19 @@
{/if}
{#if disableTooltips !== true}
<Popover documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering">
{#snippet text()}
<svelte:fragment slot="text">
The result renderer in Windmill supports rich display rendering, allowing you to customize
the display format of your results.
{/snippet}
</svelte:fragment>
<div>
<InfoIcon size={14} />
</div>
</Popover>
{/if}
<button onclick={() => copyToClipboard(toJsonStr(result))}>
<button on:click={() => copyToClipboard(toJsonStr(result))}>
<ClipboardCopy size={14} />
</button>
<button onclick={() => dispatch('open-drawer')}>
<button on:click={() => dispatch('open-drawer')}>
<Expand size={14} />
</button>
</div>
+28 -57
View File
@@ -1,4 +1,4 @@
<script module lang="ts">
<script context="module" lang="ts">
import { writable } from 'svelte/store'
const activeDropdown = writable<{ id: string | null; close: (() => void) | null }>({
id: null,
@@ -16,49 +16,25 @@
import ResolveOpen from '$lib/components/common/menu/ResolveOpen.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import { untrack } from 'svelte'
import { triggerableByAI } from '$lib/actions/triggerableByAI'
interface Props {
aiId?: string | undefined
aiDescription?: string | undefined
items?: Item[] | (() => Item[]) | (() => Promise<Item[]>)
disabled?: boolean
placement?: Placement
usePointerDownOutside?: boolean
closeOnOtherDropdownOpen?: boolean
fixedHeight?: boolean
hidePopup?: boolean
open?: boolean
customWidth?: number | undefined
customMenu?: boolean
class?: string | undefined
buttonReplacement?: import('svelte').Snippet
menu?: import('svelte').Snippet
}
export let aiId: string | undefined = undefined
export let aiDescription: string | undefined = undefined
export let items: Item[] | (() => Item[]) | (() => Promise<Item[]>) = []
export let disabled = false
export let placement: Placement = 'bottom-end'
export let usePointerDownOutside = false
export let closeOnOtherDropdownOpen = true
export let fixedHeight = true
export let hidePopup = false
export let open = false
export let customWidth: number | undefined = undefined
export let customMenu = false
let {
aiId = undefined,
aiDescription = undefined,
items = [],
disabled = false,
placement = 'bottom-end',
usePointerDownOutside = false,
closeOnOtherDropdownOpen = true,
fixedHeight = true,
hidePopup = false,
open = $bindable(false),
customWidth = undefined,
customMenu = false,
class: classNames = undefined,
buttonReplacement,
menu
}: Props = $props()
let buttonEl: HTMLButtonElement | undefined = $state(undefined)
let buttonEl: HTMLButtonElement | undefined = undefined
const {
elements: { menu: menuEl, item, trigger },
elements: { menu, item, trigger },
states,
ids: { menu: dropdownId }
} = createDropdownMenu({
@@ -84,12 +60,7 @@
})
const sync = createSync(states)
$effect(() => {
open
untrack(() => {
sync.open(open, (v) => (open = Boolean(v)))
})
})
$: sync.open(open, (v) => (open = Boolean(v)))
export function close() {
open = false
@@ -117,25 +88,25 @@
description: aiDescription,
callback: () => buttonEl?.click()
}}
class={twMerge('w-full flex items-center justify-end', fixedHeight && 'h-8', classNames)}
class={twMerge('w-full flex items-center justify-end', fixedHeight && 'h-8', $$props.class)}
use:melt={$trigger}
{disabled}
onclick={(e) => e.stopPropagation()}
on:click={(e) => e.stopPropagation()}
use:pointerDownOutside={{
capture: true,
stopPropagation: false,
exclude: getMenuElements,
customEventName: 'pointerdown_menu',
onClickOutside: () => {
if (usePointerDownOutside) {
close()
}
customEventName: 'pointerdown_menu'
}}
on:pointerdown_outside={() => {
if (usePointerDownOutside) {
close()
}
}}
data-menu
>
{#if buttonReplacement}
{@render buttonReplacement?.()}
{#if $$slots.buttonReplacement}
<slot name="buttonReplacement" />
{:else}
<Button
nonCaptureEvent
@@ -148,9 +119,9 @@
</button>
{#if open && !hidePopup}
<div use:melt={$menuEl} data-menu class="z-[6000] transition-all duration-100">
<div use:melt={$menu} data-menu class="z-[6000] transition-all duration-100">
{#if customMenu}
{@render menu?.()}
<slot name="menu" />
{:else}
<div
class="bg-surface border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto py-1 max-h-[50vh]"
@@ -1,5 +1,5 @@
<script lang="ts">
import MenuItem from '$lib/components/meltComponents/MenuItem.svelte'
import { MenuItem } from '$lib/components/meltComponents'
import { Loader2 } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import type { MenubarMenuElements } from '@melt-ui/svelte'
@@ -29,9 +29,8 @@
<div class="flex flex-col">
{#each computedItems ?? [] as item}
<MenuItem
onClick={(e) => item?.action?.(e)}
on:click={(e) => item?.action?.(e)}
href={item?.href}
target={item?.hrefTarget}
disabled={item?.disabled}
class={twMerge(
'px-4 py-2 text-primary font-semibold hover:bg-surface-hover cursor-pointer text-xs transition-all w-full',
+2 -2
View File
@@ -23,7 +23,7 @@
import Tooltip from './Tooltip.svelte'
import { Loader2 } from 'lucide-svelte'
import { untrack } from 'svelte'
import { readFieldsRecursively } from '$lib/utils'
import { readFieldsRecursively, clone} from '$lib/utils'
interface Props {
value?: any
@@ -37,7 +37,7 @@
let { value = $bindable(), helperScript, entrypoint, args: _args, name }: Props = $props()
let args = $state(structuredClone($state.snapshot(_args)))
let args = $state(clone(_args))
$effect(() => {
readFieldsRecursively(_args, { excludeField: [name] })
untrack(() => !deepEqual(args, _args) && (args = $state.snapshot(_args)))
@@ -20,7 +20,7 @@
import Label from './Label.svelte'
import { sendUserToast } from '$lib/toast'
import Toggle from './Toggle.svelte'
import { emptyString } from '$lib/utils'
import { emptyString, clone} from '$lib/utils'
import Popover from './meltComponents/Popover.svelte'
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
import { deepEqual } from 'fast-equals'
@@ -114,7 +114,7 @@
Object.keys(schema?.properties ?? {}).forEach((key) => {
if (schema?.properties[key].default != undefined && args?.[key] == undefined) {
let value = schema?.properties[key].default
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
nargs[key] = value === 'object' ? clone(value) : value
}
})
args = nargs
+71 -17
View File
@@ -105,10 +105,15 @@
vimMode
} from '$lib/stores'
import { editorConfig, updateOptions } from '$lib/editorUtils'
import { createHash as randomHash } from '$lib/editorLangUtils'
import {
createHash as randomHash,
editorConfig,
langToExt,
updateOptions,
extToLang
} from '$lib/editorUtils'
import { workspaceStore } from '$lib/stores'
import { type Preview, ResourceService, type ScriptLang, UserService } from '$lib/gen'
import { type Preview, ResourceService, UserService } from '$lib/gen'
import type { Text } from 'yjs'
import { initializeVscode, keepModelAroundToAvoidDisposalOfWorkers } from '$lib/components/vscode'
@@ -144,14 +149,13 @@
import * as htmllang from '$lib/svelteMonarch'
import { conf, language } from '$lib/vueMonarch'
import { Autocompletor } from './copilot/autocomplete/Autocompletor'
import { Autocompletor } from './copilot/autocomplete/monaco-adapter'
import { AIChatEditorHandler } from './copilot/chat/monaco-adapter'
import GlobalReviewButtons from './copilot/chat/GlobalReviewButtons.svelte'
import { writable } from 'svelte/store'
import { formatResourceTypes } from './copilot/chat/script/core'
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
import { editorPositionMap } from '$lib/utils'
import { extToLang, langToExt } from '$lib/editorLangUtils'
// import EditorTheme from './EditorTheme.svelte'
let divEl: HTMLDivElement | null = null
@@ -646,29 +650,79 @@
}
}
let autocompletor: Autocompletor | undefined = undefined
$: $reviewingChanges && autocompletor?.reject()
function addAutoCompletor(
editor: meditor.IStandaloneCodeEditor,
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
) {
if (autocompletor) {
autocompletor.dispose()
let completorDisposable: IDisposable | undefined = undefined
let autocompletor: Autocompletor | undefined = undefined
function addSuperCompletor(editor: meditor.IStandaloneCodeEditor) {
try {
if (completorDisposable) {
completorDisposable.dispose()
}
if (!scriptLang) {
throw new Error('No script lang')
}
autocompletor = new Autocompletor(editor, lang, scriptLang)
// last user events (currently disabled):
// let lastTs = Date.now()
// editor.onDidChangeModelContent((e) => {
// const thisTs = Date.now()
// lastTs = thisTs
// setTimeout(() => {
// if (thisTs === lastTs) {
// autocompletor?.savePatch()
// }
// }, 150)
// })
completorDisposable = editor.onDidChangeCursorPosition((e) => {
autocompletor?.reject()
if ($reviewingChanges) {
return
}
const position = editor.getPosition()
if (!position) {
return
}
const upToText = editor.getModel()?.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 0,
endLineNumber: position.lineNumber,
endColumn: position.column
})
const lastChar = upToText ? upToText[upToText.length - 1] : ''
if (lastChar && lastChar.match(/[\(\{\s:="',]/)) {
autocompletor?.predict()
}
})
editor.onKeyDown((e) => {
if (e.keyCode === KeyCode.Escape) {
autocompletor?.reject()
} else if (e.keyCode === KeyCode.Tab && autocompletor?.hasChanges()) {
e.preventDefault()
e.stopPropagation()
autocompletor?.accept()
autocompletor?.predict()
}
})
} catch (err) {
console.error('Could not add supercompletor', err)
}
autocompletor = new Autocompletor(editor, scriptLang)
}
$: $copilotInfo.enabled &&
$copilotInfo.codeCompletionModel &&
$codeCompletionSessionEnabled &&
Autocompletor.isProviderModelSupported($copilotInfo.codeCompletionModel) &&
initialized &&
editor &&
scriptLang &&
addAutoCompletor(editor, scriptLang)
addSuperCompletor(editor)
$: $copilotInfo.enabled && initialized && editor && addChatHandler(editor)
$: !$codeCompletionSessionEnabled && autocompletor?.dispose()
$: !$codeCompletionSessionEnabled && (completorDisposable?.dispose(), autocompletor?.reject())
const outputChannel = {
name: 'Language Server Client',
@@ -1471,7 +1525,7 @@
disposeMethod && disposeMethod()
websocketInterval && clearInterval(websocketInterval)
sqlSchemaCompletor && sqlSchemaCompletor.dispose()
autocompletor && autocompletor.dispose()
completorDisposable && completorDisposable.dispose()
sqlTypeCompletor && sqlTypeCompletor.dispose()
timeoutModel && clearTimeout(timeoutModel)
loadTimeout && clearTimeout(loadTimeout)
+1 -1
View File
@@ -719,7 +719,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
{/if}
{#if customUi?.assistants != false}
{#if lang == 'deno' || lang == 'python3' || lang == 'go' || lang == 'bash'}
{#if lang == 'deno' || lang == 'python3' || lang == 'go' || lang == 'bash' || lang == 'nu'}
<Button
aiId="editor-bar-reload-assistants"
aiDescription="Reload assistants"
@@ -478,7 +478,7 @@
>{teams_team_name}</Badge
>
</p>
<Tooltip>
<Tooltip text={teams_team_name}>
Each workspace can only be connected to one Microsoft Teams team. You can configure it under <a
target="_blank"
href="{base}/workspace_settings?tab=teams">workspace settings</a
@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, getContext, untrack } from 'svelte'
import { getFirstStepSchema } from '$lib/components/flows/flowStore'
import { getFirstStepSchema } from '$lib/components/flows/flowStore.svelte'
import type { FlowEditorContext } from '$lib/components/flows/types'
import { twMerge } from 'tailwind-merge'
import { Alert } from '$lib/components/common'
+72 -64
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import {
FlowService,
type Flow,
@@ -24,7 +26,9 @@
orderedJsonStringify,
readFieldsRecursively,
replaceFalseWithUndefined,
type Value
type StateStore,
type Value,
clone
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { Drawer } from '$lib/components/common'
@@ -32,34 +36,29 @@
import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte'
import { onMount, setContext, untrack, type ComponentType } from 'svelte'
import { writable } from 'svelte/store'
import { writable, type 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'
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
import { cleanInputs } from './flows/utils'
import {
Calendar,
Pen,
Save,
DiffIcon,
HistoryIcon,
FileJson,
type Icon,
Settings
} from 'lucide-svelte'
import { Calendar, Pen, Save, DiffIcon, HistoryIcon, FileJson, type Icon } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import Awareness from './Awareness.svelte'
import { getAllModules } from './flows/flowExplorer'
import { type FlowCopilotContext } from './copilot/flow'
import FlowAIButton from './copilot/chat/flow/FlowAIButton.svelte'
import { loadFlowModuleState } from './flows/flowStateUtils.svelte'
import FlowBuilderTutorials from './FlowBuilderTutorials.svelte'
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'
@@ -82,7 +81,33 @@
StepHistoryLoader,
type stepState
} from './stepHistoryLoader.svelte'
import type { FlowBuilderProps } from './flow_builder'
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
}
let {
initialPath = $bindable(''),
@@ -105,16 +130,8 @@
selectedTriggerIndexFromUrl = undefined,
children,
loadedFromHistoryFromUrl,
noInitial = false,
onSaveInitial,
onSaveDraft,
onDeploy,
onDeployError,
onDetails,
onSaveDraftError,
onSaveDraftOnlyAtNewPath,
onHistoryRestore
}: FlowBuilderProps = $props()
noInitial = false
}: Props = $props()
let initialPathStore = writable(initialPath)
@@ -123,7 +140,7 @@
'u/' +
($userStore?.username?.includes('@')
? $userStore!.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '')
: $userStore?.username) +
: $userStore!.username!) +
'/' +
generateRandomString(12)
@@ -169,7 +186,7 @@
savedValue: savedFlow,
modifiedValue: {
...flowStore.val,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
draft_triggers: clone(triggersState.getDraftTriggersSnapshot())
}
}
}
@@ -195,6 +212,8 @@
}
}
const dispatch = createEventDispatcher()
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule) // kept for legacy reasons
const triggersCount = writable<TriggersCount | undefined>(undefined)
const simplifiedPoll = writable(false)
@@ -232,7 +251,7 @@
if (savedFlow) {
const draftOrDeployed = cleanValueProperties(savedFlow.draft || savedFlow)
const currentDraftTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
const currentDraftTriggers = clone(triggersState.getDraftTriggersSnapshot())
const current = cleanValueProperties(
$state.snapshot({
...flowStore.val,
@@ -311,32 +330,32 @@
savedFlow = {
...(newFlow || savedFlow?.draft_only
? {
...structuredClone($state.snapshot(flowStore.val)),
...clone(flowStore.val),
path: $pathStore,
draft_only: true
}
: savedFlow),
draft: {
...structuredClone($state.snapshot(flowStore.val)),
...clone(flowStore.val),
path: $pathStore,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
draft_triggers: clone(triggersState.getDraftTriggersSnapshot())
}
} as FlowWithDraftAndDraftTriggers
let savedAtNewPath = false
if (newFlow) {
onSaveInitial?.({ path: $pathStore, id: getSelectedId() })
dispatch('saveInitial', $pathStore)
} 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() })
}
onSaveDraft?.({ path: $pathStore, savedAtNewPath, newFlow })
dispatch('saveDraft', { path: $pathStore, savedAtNewPath, newFlow })
sendUserToast('Saved as draft')
} catch (error) {
sendUserToast(`Error while saving the flow as a draft: ${error.body || error.message}`, true)
onSaveDraftError?.({ error })
dispatch('saveDraftError', error)
}
loadingDraft = false
}
@@ -501,15 +520,14 @@
draft_triggers: Trigger[]
}
savedFlow = {
...structuredClone($state.snapshot(newSavedFlow)),
...clone(newSavedFlow),
path: $pathStore
} as Flow
setDraftTriggers([])
loadingSave = false
onDeploy?.({ path: $pathStore })
dispatch('deploy', $pathStore)
} catch (err) {
onDeployError?.({ error: err })
// this is so we can use the flow builder outside of sveltekit
dispatch('deployError', err)
sendUserToast(`The flow could not be saved: ${err.body ?? err}`, true)
loadingSave = false
}
@@ -703,7 +721,7 @@
if (savedFlow?.draft_only === false || savedFlow?.draft_only === undefined) {
dropdownItems.push({
label: 'Exit & see details',
onClick: () => onDetails?.({ path: $pathStore })
onClick: () => dispatch('details', $pathStore)
})
}
@@ -772,7 +790,7 @@
}
]
: []),
...(customUi?.topBar?.export != false
...(customUi?.topBar?.history != false
? [
{
displayName: 'Export',
@@ -786,17 +804,6 @@
disabled: hasAiDiff
}
]
: []),
...(customUi?.topBar?.settings != false
? [
{
displayName: 'Flow settings',
icon: Settings,
action: () => {
select('settings-metadata')
}
}
]
: [])
]
}
@@ -818,28 +825,28 @@
let forceTestTab: Record<string, boolean> = $state({})
let highlightArg: Record<string, string | undefined> = $state({})
$effect.pre(() => {
run(() => {
initialPathStore.set(initialPath)
})
$effect.pre(() => {
run(() => {
setContext('customUi', customUi)
})
$effect.pre(() => {
run(() => {
if (flowStore.val || $selectedIdStore) {
readFieldsRecursively(flowStore.val)
untrack(() => saveSessionDraft())
}
})
$effect.pre(() => {
run(() => {
initialPath && ($pathStore = initialPath)
})
$effect.pre(() => {
run(() => {
selectedId && untrack(() => select(selectedId))
})
$effect.pre(() => {
run(() => {
initialPath && initialPath != '' && $workspaceStore && untrack(() => loadTriggers())
})
$effect.pre(() => {
run(() => {
const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasDiff() ?? false
customUi && untrack(() => onCustomUiChange(customUi, hasAiDiff))
})
@@ -884,8 +891,8 @@
{@render children?.()}
<DeployOverrideConfirmationModal
{deployedBy}
{confirmCallback}
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
@@ -907,7 +914,7 @@
{#key renderCount}
{#if !$userStore?.operator}
{#if $pathStore}
<FlowHistory bind:this={flowHistory} path={$pathStore} {onHistoryRestore} />
<FlowHistory bind:this={flowHistory} path={$pathStore} on:historyRestore />
{/if}
<FlowYamlEditor bind:drawer={yamlEditorDrawer} />
<FlowImportExportMenu bind:drawer={jsonViewerDrawer} />
@@ -1036,7 +1043,7 @@
await syncWithDeployed()
const currentDraftTriggers = structuredClone(
const currentDraftTriggers = clone(
triggersState.getDraftTriggersSnapshot()
)
@@ -1062,6 +1069,9 @@
</div>
</Button>
{/if}
{#if !disableAi && customUi?.topBar?.aiBuilder != false && !aiChatManager.open}
<FlowAIButton openPanel={() => aiChatManager.openChat()} />
{/if}
<FlowPreviewButtons
on:openTriggers={(e) => {
select('triggers')
@@ -1094,6 +1104,7 @@
/>
</div>
</div>
<!-- metadata -->
{#if $flowStateStore}
<FlowEditor
@@ -1133,9 +1144,6 @@
}}
{forceTestTab}
{highlightArg}
aiChatOpen={aiChatManager.open}
showFlowAiButton={!disableAi && customUi?.topBar?.aiBuilder != false}
toggleAiChat={() => aiChatManager.toggleOpen()}
onRunPreview={() => {
flowPreviewButtons?.openPreview(true)
}}
@@ -7,8 +7,8 @@
import FlowTutorials from './FlowTutorials.svelte'
import { tutorialsToDo } from '$lib/stores'
let targetTutorial: string | undefined = $state(undefined)
let flowTutorials: FlowTutorials | undefined = $state(undefined)
let targetTutorial: string | undefined = undefined
let flowTutorials: FlowTutorials | undefined = undefined
async function getTutorialItems() {
const tutorials = [
@@ -42,7 +42,7 @@
{#key $tutorialsToDo}
<Dropdown items={getTutorialItems} class="w-fit">
{#snippet buttonReplacement()}
<svelte:fragment slot="buttonReplacement">
<Button
nonCaptureEvent
size="xs"
@@ -51,7 +51,7 @@
id="tutorials-button"
startIcon={{ icon: BookOpen }}
/>
{/snippet}
</svelte:fragment>
</Dropdown>
{/key}
@@ -11,11 +11,10 @@
import { base } from '$lib/base'
import FlowJobResult from './FlowJobResult.svelte'
import FlowPreviewStatus from './preview/FlowPreviewStatus.svelte'
import DisplayResult from './DisplayResult.svelte'
import { createEventDispatcher, getContext, tick } from 'svelte'
import { onDestroy } from 'svelte'
import { Badge, Button, Skeleton, Tab } from './common'
import DisplayResult from './DisplayResult.svelte'
import Tabs from './common/tabs/Tabs.svelte'
import { type DurationStatus, type FlowStatusViewerContext, type GraphModuleState } from './graph'
import ModuleStatus from './ModuleStatus.svelte'
+21 -33
View File
@@ -1,52 +1,40 @@
<script lang="ts">
import { debounce, displayDate, msToSec } from '$lib/utils'
import { onDestroy, untrack } from 'svelte'
import { onDestroy } from 'svelte'
import { getDbClockNow } from '$lib/forLater'
import { Loader2 } from 'lucide-svelte'
import TimelineBar from './TimelineBar.svelte'
import type { Writable } from 'svelte/store'
import WaitTimeWarning from './common/waitTimeWarning/WaitTimeWarning.svelte'
interface Props {
selfWaitTime?: number | undefined
aggregateWaitTime?: number | undefined
flowModules: string[]
durationStatuses: Writable<
Record<
string,
{
byJob: Record<string, { created_at?: number; started_at?: number; duration_ms?: number }>
iteration_from?: number
iteration_total?: number
}
>
export let selfWaitTime: number | undefined = undefined
export let aggregateWaitTime: number | undefined = undefined
export let flowModules: string[]
export let durationStatuses: Writable<
Record<
string,
{
byJob: Record<string, { created_at?: number; started_at?: number; duration_ms?: number }>
iteration_from?: number
iteration_total?: number
}
>
flowDone?: boolean
}
>
export let flowDone = false
let {
selfWaitTime = undefined,
aggregateWaitTime = undefined,
flowModules,
durationStatuses,
flowDone = false
}: Props = $props()
let min: undefined | number = $state(undefined)
let max: undefined | number = $state(undefined)
let total: number | undefined = $state(undefined)
let min: undefined | number = undefined
let max: undefined | number = undefined
let total: number | undefined = undefined
let items:
| Record<
string,
Array<{ created_at?: number; started_at?: number; duration_ms?: number; id: string }>
>
| undefined = $state(undefined)
| undefined = undefined
let { debounced, clearDebounce } = debounce(() => computeItems($durationStatuses), 30)
$effect(() => {
flowDone != undefined && $durationStatuses && untrack(() => debounced())
})
$: flowDone != undefined && $durationStatuses && debounced()
export function reset() {
min = undefined
@@ -116,7 +104,7 @@
}
}
let now = $state(getDbClockNow().getTime())
let now = getDbClockNow().getTime()
let interval = setInterval((x) => {
if (!max) {
@@ -177,7 +165,7 @@
<div class="w-full flex flex-row-reverse sticky top-0">
<button
class="!text-secondary underline mr-2 text-2xs text-right whitespace-nowrap"
onclick={() => {
on:click={() => {
let r = $durationStatuses[k]
if (r.iteration_from) {
r.iteration_from -= 20
@@ -6,12 +6,11 @@
import FlowBuilderTutorialForLoop from './tutorials/FlowBuilderTutorialForLoop.svelte'
import FlowBuilderTutorialErrorHandler from './tutorials/FlowBuilderTutorialErrorHandler.svelte'
let flowBuilderTutorialSimpleFlow: FlowBuilderTutorialSimpleFlow | undefined = $state(undefined)
let flowBuilderTutorialForLoop: FlowBuilderTutorialForLoop | undefined = $state(undefined)
let flowBuilderTutorialBranchOne: FlowBuilderTutorialBranchOne | undefined = $state(undefined)
let flowBuilderTutorialBranchAll: FlowBuilderTutorialBranchAll | undefined = $state(undefined)
let flowBuilderTutorialErrorHandler: FlowBuilderTutorialErrorHandler | undefined =
$state(undefined)
let flowBuilderTutorialSimpleFlow: FlowBuilderTutorialSimpleFlow | undefined = undefined
let flowBuilderTutorialForLoop: FlowBuilderTutorialForLoop | undefined = undefined
let flowBuilderTutorialBranchOne: FlowBuilderTutorialBranchOne | undefined = undefined
let flowBuilderTutorialBranchAll: FlowBuilderTutorialBranchAll | undefined = undefined
let flowBuilderTutorialErrorHandler: FlowBuilderTutorialErrorHandler | undefined = undefined
export function runTutorialById(id: string, indexToInsertAt?: number | undefined) {
if (id === 'forloop') {
@@ -1,13 +0,0 @@
<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>
+118 -127
View File
@@ -1,6 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { userStore, workspaceStore } from '$lib/stores'
import {
type Folder,
@@ -23,22 +21,18 @@
import Select from './select/Select.svelte'
import { safeSelectItems } from './select/utils.svelte'
interface Props {
name: string
}
let { name }: Props = $props()
let can_write = $state(false)
export let name: string
let can_write = false
type Role = 'viewer' | 'writer' | 'admin'
let folder: Folder | undefined
let perms: { owner_name: string; role: Role }[] | undefined = $state(undefined)
let usernames: string[] = $state([])
let groups: string[] = $state([])
let ownerItem: string = $state('')
let perms: { owner_name: string; role: Role }[] | undefined = undefined
let usernames: string[] = []
let groups: string[] = []
let ownerItem: string = ''
let newGroup: Drawer | undefined = $state(undefined)
let viewGroup: Drawer | undefined = $state(undefined)
let newGroup: Drawer
let viewGroup: Drawer
async function loadUsernames(): Promise<void> {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
@@ -48,6 +42,12 @@
groups = await GroupService.listGroupNames({ workspace: $workspaceStore! })
}
$: {
if ($workspaceStore && $userStore) {
load()
}
}
async function load() {
loadUsernames()
loadGroups()
@@ -67,7 +67,7 @@
loadFolder()
}
let folderNotFound: boolean | undefined = $state(undefined)
let folderNotFound: boolean | undefined = undefined
async function loadFolder(): Promise<void> {
try {
@@ -111,10 +111,10 @@
}
}
let ownerKind: 'user' | 'group' = $state('user')
let groupCreated: string | undefined = $state(undefined)
let newGroupName: string = $state('')
let summary: string = $state('')
let ownerKind: 'user' | 'group' = 'user'
let groupCreated: string | undefined = undefined
let newGroupName: string = ''
let summary: string = ''
async function addGroup() {
await GroupService.createGroup({
@@ -139,18 +139,13 @@
dispatch('update')
loadFolder()
}
run(() => {
if ($workspaceStore && $userStore) {
load()
}
})
</script>
<Drawer bind:this={newGroup}>
<DrawerContent
title="New Group"
on:close={() => {
newGroup?.closeDrawer()
newGroup.closeDrawer()
groupCreated = undefined
}}
>
@@ -185,7 +180,7 @@
<Section label={`Permissions (${perms?.length ?? 0})`}>
<div class="flex flex-col gap-6">
{#if can_write}
<Alert type="info" title="New permissions may take up to 60s to apply">
<Alert role="info" title="New permissions may take up to 60s to apply">
<span class="text-xs text-tertiary">Due to permissions cache invalidation </span>
</Alert>
<div class="flex items-center gap-1">
@@ -265,120 +260,116 @@
{/if}
{#if perms}
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>user/group</th>
<th></th>
<th></th>
</tr>
{#snippet body()}
<tbody>
{#each perms ?? [] as { owner_name, role }}<tr>
<td>{owner_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
disabled={owner_name == 'u/' + $userStore?.username &&
!$userStore?.is_admin}
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
// const inAcl = (
// folder?.extra_perms ? Object.keys(folder?.extra_perms) : []
// ).includes(folder)
if (role == 'admin') {
await FolderService.addOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name
}
})
} else if (role == 'writer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: true
}
})
} else if (role == 'viewer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: false
}
})
}
loadFolder()
}}
>
{#snippet children({ item })}
<ToggleButton
value="viewer"
label="Viewer"
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="center"
value="writer"
label="Writer"
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="right"
value="admin"
label="Admin"
tooltip="An admin of a folder has read AND write access to all the elements inside the folders and can manage the permissions as well as add new admins"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
{role}
{/if}</td
>
<td>
{#if can_write && (owner_name != 'u/' + $userStore?.username || $userStore?.is_admin)}
<button
class="ml-2 text-red-500"
onclick={async () => {
await Promise.all([
FolderService.removeOwnerToFolder({
<tbody slot="body">
{#each perms as { owner_name, role }}<tr>
<td>{owner_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
disabled={owner_name == 'u/' + $userStore?.username && !$userStore?.is_admin}
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
// const inAcl = (
// folder?.extra_perms ? Object.keys(folder?.extra_perms) : []
// ).includes(folder)
if (role == 'admin') {
await FolderService.addOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: { owner: owner_name }
}),
GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name
}
})
])
} else if (role == 'writer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: true
}
})
} else if (role == 'viewer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: false
}
})
}
loadFolder()
}}>remove</button
}}
>
{:else}
<span class="text-tertiary text-xs">cannot remove yourself</span>
{/if}</td
>
</tr>{/each}
</tbody>
{/snippet}
{#snippet children({ item })}
<ToggleButton
value="viewer"
label="Viewer"
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="center"
value="writer"
label="Writer"
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="right"
value="admin"
label="Admin"
tooltip="An admin of a folder has read AND write access to all the elements inside the folders and can manage the permissions as well as add new admins"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
{role}
{/if}</td
>
<td>
{#if can_write && (owner_name != 'u/' + $userStore?.username || $userStore?.is_admin)}
<button
class="ml-2 text-red-500"
on:click={async () => {
await Promise.all([
FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: { owner: owner_name }
}),
GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name
}
})
])
loadFolder()
}}>remove</button
>
{:else}
<span class="text-tertiary text-xs">cannot remove yourself</span>
{/if}</td
>
</tr>{/each}
</tbody>
</TableCustom>
<!-- <h2 class="mt-10"
>Folders managing this folder <Tooltip
@@ -1,11 +1,7 @@
<script lang="ts">
import Popover from './Popover.svelte'
interface Props {
members: string[]
}
let { members }: Props = $props()
export let members: string[]
</script>
<Popover>
@@ -15,7 +11,5 @@
<span class="text-tertiary text-xs">{members?.join(', ')} </span>
</div>
</div>
{#snippet text()}
<span>{members?.join(', ')} </span>
{/snippet}
<span slot="text">{members?.join(', ')} </span>
</Popover>
@@ -1,440 +0,0 @@
<script lang="ts">
import { workspaceStore, enterpriseLicense, userStore } from '$lib/stores'
import Popover from './meltComponents/Popover.svelte'
import Button from './common/button/Button.svelte'
import { Loader2, Github, RotateCw, Plus, Minus, Download } from 'lucide-svelte'
import { onDestroy } from 'svelte'
import {
createGitHubAppState,
loadGithubInstallations,
startInstallationCheck,
stopInstallationCheck,
getRepositories,
addInstallationToWorkspace,
deleteInstallation,
exportInstallation,
importInstallation,
applyRepositoryURL,
handleInstallClick,
type GitHubAppState
} from '$lib/githubApp'
interface Props {
resourceType: string
args?: Record<string, any>
description?: string
onArgsUpdate?: (args: Record<string, any>) => void
onDescriptionUpdate?: (description: string) => void
}
let {
resourceType,
args = {},
description = '',
onArgsUpdate,
onDescriptionUpdate
}: Props = $props()
// GitHub App state using the service utilities
let githubState: GitHubAppState = $state(createGitHubAppState())
let githubAppPopover: { open: () => void; close: () => void } | null = $state(null)
// Filter and deduplicate installations not in current workspace
let githubInstallationsNotInWorkspace = $derived(
githubState.githubInstallations
.filter(
(installation) =>
!githubState.workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
)
.filter(
(installation, index, array) =>
array.findIndex((item) => item.installation_id === installation.installation_id) === index
)
)
let showGitHubApp = $derived(
resourceType === 'git_repository' && $workspaceStore && $userStore?.is_admin
)
// Load GitHub installations when conditions are met
$effect(() => {
if (showGitHubApp && $enterpriseLicense && $workspaceStore) {
loadGithubInstallations(githubState, $workspaceStore).catch((error) => {
console.error('Failed to load GitHub installations:', error)
})
}
})
// Clean up interval when component is destroyed
onDestroy(() => {
stopInstallationCheck(githubState)
})
// Extracted event handlers for better maintainability
function handleApplyRepositoryURL(close: (_: any) => void) {
try {
applyRepositoryURL(
githubState,
args,
description,
(newArgs) => {
if (onArgsUpdate) {
onArgsUpdate(newArgs)
}
},
(newDescription) => {
if (onDescriptionUpdate) {
onDescriptionUpdate(newDescription)
}
}
)
close(null)
} catch (error) {
console.error('Failed to apply repository URL:', error)
}
}
async function handleDeleteInstallation(installationId: number) {
if (!$workspaceStore) return
try {
await deleteInstallation($workspaceStore, installationId, () =>
loadGithubInstallations(githubState, $workspaceStore!)
)
} catch (error) {
console.error('Failed to delete installation:', error)
}
}
async function handleAddInstallation(installationId: number, workspaceId: string) {
if (!$workspaceStore) return
try {
await addInstallationToWorkspace($workspaceStore, installationId, workspaceId, () =>
loadGithubInstallations(githubState, $workspaceStore!)
)
} catch (error) {
console.error('Failed to add installation:', error)
}
}
async function handleExportInstallation(installationId: number) {
if (!$workspaceStore) return
try {
await exportInstallation($workspaceStore, installationId)
} catch (error) {
console.error('Failed to export installation:', error)
}
}
async function handleImportInstallation() {
if (!$workspaceStore) return
try {
await importInstallation($workspaceStore, githubState.importJwt, () => {
githubState.importJwt = ''
loadGithubInstallations(githubState, $workspaceStore!)
})
} catch (error) {
console.error('Failed to import installation:', error)
}
}
function handleRefreshInstallations() {
if (!$workspaceStore) return
loadGithubInstallations(githubState, $workspaceStore).catch((error) => {
console.error('Failed to refresh installations:', error)
})
}
function handleInstallClickWithPopover() {
if (!$workspaceStore) return
handleInstallClick(githubState, $workspaceStore, () => {
githubAppPopover?.open()
})
}
</script>
{#if showGitHubApp}
{#if !githubState.loadingGithubInstallations}
<Button
color="light"
variant="contained"
size="xs"
on:click={handleRefreshInstallations}
disabled={!$enterpriseLicense}
startIcon={{ icon: RotateCw }}
/>
{:else}
<Loader2 class="animate-spin w-10 h-4" />
{/if}
{#if showGitHubApp}
<Popover
documentationLink="https://www.windmill.dev/docs/integrations/git_repository#github-app"
bind:this={githubAppPopover}
floatingConfig={{
placement: 'bottom'
}}
disabled={!$enterpriseLicense || githubState.loadingGithubInstallations}
>
{#snippet trigger()}
<Button
color="none"
variant="border"
size="xs"
disabled={!$enterpriseLicense || githubState.loadingGithubInstallations}
startIcon={{
icon: githubState.loadingGithubInstallations ? Loader2 : Github,
classes: githubState.loadingGithubInstallations ? 'animate-spin' : ''
}}
nonCaptureEvent
>
{$enterpriseLicense ? 'GitHub App' : 'GitHub App (ee only)'}
</Button>
{/snippet}
{#snippet content({ close })}
<div class="block text-primary p-4">
<div class="flex flex-col gap-4 w-[600px]">
{#if githubState.workspaceGithubInstallations.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary">Select Repository</p>
<div class="flex flex-row gap-2 w-full">
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">GitHub Account ID</p>
<select bind:value={githubState.selectedGHAppAccountId}>
<option value="" disabled>Select GitHub Account ID</option>
{#each githubState.workspaceGithubInstallations as installation (`select-${installation.installation_id}-${installation.workspace_id}`)}
<option value={installation.account_id}>{installation.account_id}</option>
{/each}
</select>
</div>
{#if githubState.selectedGHAppAccountId}
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">Repository</p>
<div class="flex flex-row gap-2">
<select bind:value={githubState.selectedGHAppRepository}>
<option value="" disabled selected>Select Repository</option>
{#each getRepositories(githubState, githubState.selectedGHAppAccountId) as repository (repository.url)}
<option value={repository.url}>{repository.name}</option>
{/each}
</select>
</div>
</div>
{/if}
<div class="pt-[26px]">
<Button
size="xs"
color="blue"
buttonType="button"
disabled={!githubState.selectedGHAppRepository}
on:click={() => handleApplyRepositoryURL(close)}
>
Apply
</Button>
</div>
</div>
</div>
{/if}
<div
class={`${
githubState.workspaceGithubInstallations.length > 0
? 'border-t border-gray-200 dark:border-gray-700'
: ''
} pt-4`}
>
<div class="flex flex-col gap-4">
<div class="flex">
<Button
color="none"
variant="border"
size="xs"
href={githubState.githubInstallationUrl}
startIcon={{
icon: githubState.isCheckingInstallation ? Loader2 : Plus,
classes: githubState.isCheckingInstallation ? 'animate-spin' : ''
}}
target="_blank"
disabled={githubState.isCheckingInstallation}
on:click={() => {
if ($workspaceStore) {
startInstallationCheck(githubState, $workspaceStore, () =>
loadGithubInstallations(githubState, $workspaceStore!)
)
}
}}
>
{githubState.isCheckingInstallation
? 'Checking for new installations...'
: 'Add new installation'}
</Button>
</div>
{#if githubState.workspaceGithubInstallations.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary">Current installations:</p>
<div class="flex flex-col gap-1">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-tertiary">
<th class="pb-2 w-1/3">Org</th>
<th class="pb-2 w-1/6">Workspace</th>
<th class="pb-2 w-1/6">Repos</th>
<th class="pb-2 w-1/3"></th>
</tr>
</thead>
<tbody>
{#each githubState.workspaceGithubInstallations as installation (`current-${installation.installation_id}-${installation.workspace_id}`)}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
<span class="text-xs text-tertiary"
>{installation.workspace_id}</span
>
</td>
<td class="py-2 text-tertiary">
{installation.repositories.length} repos
</td>
<td class="py-2 text-right">
<div class="flex justify-end gap-1">
<Button
size="xs2"
color="blue"
title="Export installation to other instance"
startIcon={{ icon: Download }}
on:click={() =>
handleExportInstallation(installation.installation_id)}
>
Export
</Button>
<Button
size="xs2"
color="red"
title="Remove installation from workspace"
startIcon={{ icon: Minus }}
on:click={() =>
handleDeleteInstallation(installation.installation_id)}
>
Remove
</Button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
{#if githubInstallationsNotInWorkspace.length > 0}
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary"
>Installations in other workspaces:</p
>
<div class="flex flex-col gap-1">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-tertiary">
<th class="pb-2 w-1/3">Org</th>
<th class="pb-2 w-1/6">Workspace</th>
<th class="pb-2 w-1/6">Repos</th>
<th class="pb-2 w-1/3"></th>
</tr>
</thead>
<tbody>
{#each githubInstallationsNotInWorkspace as installation (`other-${installation.installation_id}-${installation.workspace_id}`)}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
<span class="text-xs text-tertiary"
>{installation.workspace_id}</span
>
</td>
<td class="py-2 text-tertiary">
{installation.repositories.length} repos
</td>
<td class="pl-8 py-2 text-right">
<Button
size="xs2"
color="blue"
title="Add installation to workspace"
startIcon={{ icon: Plus }}
on:click={() => {
if (installation.workspace_id) {
handleAddInstallation(
installation.installation_id,
installation.workspace_id
)
}
}}
>
Add to workspace
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</div>
</div>
<div class="mt-4 flex flex-col gap-2">
<p class="text-sm font-semibold text-secondary"
>Import installation from other instance:</p
>
<div class="flex gap-2">
<input
type="text"
placeholder="Paste JWT token here"
bind:value={githubState.importJwt}
class="flex-1"
/>
<Button
color="blue"
on:click={handleImportInstallation}
disabled={!githubState.importJwt}
>
Import
</Button>
</div>
</div>
</div>
</div>
{/snippet}
</Popover>
{:else}
<Button
color="none"
variant="border"
size="xs"
disabled={!$enterpriseLicense || githubState.loadingGithubInstallations}
startIcon={{
icon:
githubState.loadingGithubInstallations || githubState.isCheckingInstallation
? Loader2
: Github,
classes:
githubState.loadingGithubInstallations || githubState.isCheckingInstallation
? 'animate-spin'
: ''
}}
href={githubState.githubInstallationUrl}
target="_blank"
on:click={handleInstallClickWithPopover}
>
{$enterpriseLicense
? githubState.isCheckingInstallation
? 'Waiting for installation...'
: 'Install GitHub App'
: 'GitHub App (ee only)'}
</Button>
{/if}
{/if}
+4 -12
View File
@@ -2,22 +2,16 @@
import { GroupService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Popover from './Popover.svelte'
import { untrack } from 'svelte'
interface Props {
name: string
}
export let name: string
let { name }: Props = $props()
$: $workspaceStore && loadMembers()
let members: string[] | undefined = $state([])
let members: string[] | undefined = []
async function loadMembers() {
members = (await GroupService.getGroup({ workspace: $workspaceStore!, name })).members
}
$effect(() => {
$workspaceStore && untrack(() => loadMembers())
})
</script>
{#if members}
@@ -28,8 +22,6 @@
><span class="text-tertiary text-xs">{members?.join(', ')}</span></div
></div
>
{#snippet text()}
<span>{members?.join(', ')}</span>
{/snippet}</Popover
<span slot="text">{members?.join(', ')}</span></Popover
>
{/if}
@@ -1,4 +1,5 @@
<script lang="ts">
import { clone } from '$lib/utils'
import { type RunnableType, type Job } from '$lib/gen/index.js'
import { sendUserToast } from '$lib/utils.js'
import RunningJobSchemaPicker from '$lib/components/schema/RunningJobSchemaPicker.svelte'
@@ -48,7 +49,7 @@
dispatch('select', { args: fullPayload, jobId: data.id })
} else {
dispatch('select', {
args: structuredClone($state.snapshot(data.payloadData)),
args: clone(data.payloadData),
jobId: data.id
})
}
@@ -28,7 +28,7 @@
import type ItemPicker from './ItemPicker.svelte'
import type { InputTransform } from '$lib/gen'
import TemplateEditor from './TemplateEditor.svelte'
import { setInputCat as computeInputCat, isCodeInjection } from '$lib/utils'
import { setInputCat as computeInputCat, isCodeInjection, clone} from '$lib/utils'
import { FunctionSquare, InfoIcon } from 'lucide-svelte'
import { getResourceTypes } from './resourceTypesStore'
import type { FlowCopilotContext } from './copilot/flow'
@@ -280,7 +280,7 @@
function onArgChange() {
const newArg = { arg, propertyType, inputCat }
if (!deepEqual(newArg, prevArg)) {
prevArg = structuredClone($state.snapshot(newArg))
prevArg = clone(newArg)
updateStaticInput(inputCat, propertyType, arg)
}
}
@@ -649,7 +649,7 @@
<Button
variant="border"
color="light"
size="xs"
size="md"
btnClasses="mt-1"
on:click={() => {
if ($values[setting.key] == undefined || !Array.isArray($values[setting.key])) {
@@ -668,7 +668,7 @@
disabled={!$enterpriseLicense}
variant="border"
color="light"
size="xs"
size="md"
on:click={async () => {
try {
await SettingService.testCriticalChannels({
+13 -27
View File
@@ -12,30 +12,18 @@
import { classNames, emptyString, parseQueryParams } from '$lib/utils'
import { base } from '$lib/base'
import { getUserExt } from '$lib/user'
import { Button, Skeleton } from '$lib/components/common'
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
import { refreshSuperadmin } from '$lib/refreshUser'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Button from './common/button/Button.svelte'
interface Props {
rd?: string | undefined
email?: string | undefined
password?: string | undefined
error?: string | undefined
popup?: boolean
firstTime?: boolean
}
let {
rd = undefined,
email = $bindable(undefined),
password = $bindable(undefined),
error = undefined,
popup = false,
firstTime = false
}: Props = $props()
export let rd: string | undefined = undefined
export let email: string | undefined = undefined
export let password: string | undefined = undefined
export let error: string | undefined = undefined
export let popup: boolean = false
export let firstTime: boolean = false
const providers = [
{
@@ -72,9 +60,9 @@
const providersType = providers.map((p) => p.type as string)
let showPassword = $state(false)
let logins: OAuthLogin[] | undefined = $state(undefined)
let saml: string | undefined = $state(undefined)
let showPassword = false
let logins: OAuthLogin[] | undefined = undefined
let saml: string | undefined = undefined
type OAuthLogin = {
type: string
@@ -263,9 +251,7 @@
}
}
$effect(() => {
error && sendUserToast(error, true)
})
$: error && sendUserToast(error, true)
</script>
<div class="bg-surface px-4 py-8 shadow md:border sm:rounded-lg sm:px-10">
@@ -366,7 +352,7 @@
</label>
<div>
<input
onkeyup={handleKeyUp}
on:keyup={handleKeyUp}
bind:value={password}
id="password"
type="password"
@@ -378,7 +364,7 @@
<div class="pt-2">
<button
onclick={login}
on:click={login}
disabled={!email || !password}
class="flex w-full justify-center rounded-md bg-frost-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-frost-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-frost-600"
>
@@ -24,7 +24,7 @@
disableMock?: boolean
disableHistory?: boolean
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
loadingJob?: boolean
loadingHistory?: boolean
}
let {
@@ -40,18 +40,27 @@
disableMock = false,
disableHistory = false,
onUpdateMock,
loadingJob = false
loadingHistory = false
}: Props = $props()
const { testSteps } = getContext<FlowEditorContext>('FlowEditorContext')
let selectedJob: Job | undefined = $state(undefined)
let fetchingLastJob = false
let preview: 'mock' | 'job' | undefined = $state(undefined)
let jobProgressReset: () => void = $state(() => {})
let forceJson = $state(false)
let nlastJob = $derived.by(() => {
if (testJob && testJob.type === 'CompletedJob') {
return { ...testJob, preview: true }
}
if (lastJob) {
return { ...lastJob, preview: false }
}
return undefined
})
const logJob = $derived(testJob ?? selectedJob)
let forceJson = $state(false)
</script>
<Splitpanes horizontal>
@@ -66,8 +75,7 @@
{/if}
<OutputPickerInner
{lastJob}
{testJob}
lastJob={nlastJob}
fullResult
moduleId={mod.id}
closeOnOutsideClick={true}
@@ -76,7 +84,7 @@
mock={mod.mock}
bind:forceJson
bind:selectedJob
isLoading={testIsLoading || loadingJob}
isLoading={(testIsLoading && !scriptProgress) || fetchingLastJob || loadingHistory}
bind:preview
path={`path` in mod.value ? mod.value.path : ''}
{loopStatus}
@@ -102,12 +110,14 @@
{:else}
<LogViewer
small
jobId={logJob?.id}
duration={logJob?.['duration_ms']}
mem={logJob?.['mem_peak']}
content={logJob?.logs}
isLoading={(testIsLoading && logJob?.['running'] == false) || loadingJob}
tag={logJob?.tag}
jobId={selectedJob?.id}
duration={selectedJob?.['duration_ms']}
mem={selectedJob?.['mem_peak']}
content={selectedJob?.logs}
isLoading={(testIsLoading && selectedJob?.['running'] == false) ||
fetchingLastJob ||
loadingHistory}
tag={selectedJob?.tag}
/>
{/if}
</Pane>
+17 -15
View File
@@ -1,7 +1,8 @@
<script lang="ts" module>
type testModuleState = {
loading: boolean
cancel?: () => Promise<void>
instances: number
cancel?: () => void
}
let testModulesState = $state<Record<string, testModuleState>>({})
@@ -52,10 +53,7 @@
// Not defined if JobProgressBar not loaded
if (jobProgressReset) jobProgressReset()
testModulesState[mod.id].cancel = async () => {
await testJobLoader?.cancelJob()
testJob = undefined
}
testModulesState[mod.id].cancel = testJobLoader?.cancelJob
const val = mod.value
// let jobId: string | undefined = undefined
@@ -113,7 +111,14 @@
const modId = mod.id
testModulesState[modId] = {
...(testModulesState[modId] ?? { loading: false, instances: 0 }),
loading: testIsLoading
loading: testIsLoading,
instances: testModulesState[modId]!.instances + 1
}
return () => {
testModulesState[modId].instances -= 1
if (testModulesState[modId].instances < 1) {
delete testModulesState[modId]
}
}
})
</script>
@@ -125,15 +130,12 @@
bind:this={testJobLoader}
bind:isLoading={
() => testModulesState[mod.id]?.loading ?? false,
(v) => {
let newLoading = v ?? false
if (testModulesState[mod.id]?.loading !== newLoading) {
testModulesState[mod.id] = {
...(testModulesState[mod.id] ?? {}),
loading: newLoading
}
}
}
(v) =>
(testModulesState[mod.id] = {
...testModulesState[mod.id],
loading: v ?? false,
instances: testModulesState[mod.id]?.instances ?? 0
})
}
bind:job={testJob}
/>
@@ -4,8 +4,6 @@
</script>
<Popover notClickable>
{#snippet text()}
The script has no main function exported
{/snippet}
<svelte:fragment slot="text">The script has no main function exported</svelte:fragment>
<Badge small color="yellow" baseClass="border border-indigo-200">No main</Badge>
</Popover>
@@ -17,7 +17,6 @@
defaultValue: any
editor?: SimpleEditor | undefined
path?: string
onClear?: () => void
}
let {
@@ -27,8 +26,7 @@
showSchemaExplorer = false,
selectFirst = false,
defaultValue,
editor = $bindable(undefined),
onClear = undefined
editor = $bindable(undefined)
}: Props = $props()
function isResource() {
@@ -50,7 +48,7 @@
<ResourcePicker
{selectFirst}
{disablePortal}
{onClear}
on:clear
bind:value={
() => valueToPath(),
(v) => {
@@ -6,7 +6,6 @@
import { sendUserToast } from '$lib/toast'
import TestConnection from './TestConnection.svelte'
import { enterpriseLicense } from '$lib/stores'
import SimpleEditor from './SimpleEditor.svelte'
type S3Config = {
type: 'S3'
@@ -36,14 +35,7 @@
roleArn: string
}
type GcsConfig = {
type: 'Gcs'
bucket: string
serviceAccountKey: Record<string, string>
}
export let bucket_config: S3Config | AzureConfig | AwsOidcConfig | GcsConfig | undefined =
undefined
export let bucket_config: S3Config | AzureConfig | AwsOidcConfig | undefined = undefined
$: bucket_config?.type == 'S3' &&
bucket_config.allow_http == undefined &&
@@ -113,11 +105,10 @@
buttonTextOverride="Test from a worker"
/>
</div>
<Tabs
selected={bucket_config?.type ?? 'S3'}
bind:selected={bucket_config.type}
on:selected={(e) => {
if (e.detail === 'S3' && bucket_config?.type !== 'S3') {
if (e.detail === 'S3') {
bucket_config = {
type: 'S3',
bucket: '',
@@ -126,7 +117,7 @@
secret_key: '',
endpoint: ''
}
} else if (e.detail === 'Azure' && bucket_config?.type !== 'Azure') {
} else if (e.detail === 'Azure') {
bucket_config = {
type: 'Azure',
accountName: '',
@@ -136,26 +127,12 @@
clientId: '',
accessKey: ''
}
} else if (e.detail === 'Gcs' && bucket_config?.type !== 'Gcs') {
bucket_config = {
type: 'Gcs',
bucket: '',
serviceAccountKey: {}
}
} else if (e.detail === 'AwsOidc' && bucket_config?.type !== 'AwsOidc') {
bucket_config = {
type: 'AwsOidc',
bucket: '',
region: '',
roleArn: ''
}
}
}}
>
<Tab size="sm" value="S3">S3</Tab>
<Tab size="sm" value="Azure">Azure Blob</Tab>
<Tab size="sm" value="AwsOidc">AWS OIDC</Tab>
<Tab size="sm" value="Gcs">Google Cloud Storage</Tab>
</Tabs>
<div class="flex flex-col gap-2 mt-2 p-2 border rounded-md">
{#if bucket_config.type === 'S3'}
@@ -258,37 +235,6 @@
bind:value={bucket_config.roleArn}
/>
</label>
{:else if bucket_config.type === 'Gcs'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Bucket</span>
<input type="text" placeholder="bucket-name" bind:value={bucket_config.bucket} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Service Account Key</span>
<span class="text-tertiary text-2xs">JSON content of the service account key file</span>
<SimpleEditor
lang="json"
bind:code={
() => {
if (bucket_config?.type === 'Gcs') {
return JSON.stringify(bucket_config.serviceAccountKey)
} else {
return '{}'
}
},
(v) => {
if (bucket_config?.type === 'Gcs') {
try {
bucket_config.serviceAccountKey = JSON.parse(v ?? '{}')
} catch (_) {
bucket_config.serviceAccountKey = {}
}
}
}
}
class="h-80"
/>
</label>
{:else}
<div>Unknown bucket type {bucket_config['type']}</div>
{/if}
+23 -49
View File
@@ -5,39 +5,16 @@
import { twMerge } from 'tailwind-merge'
import { ExternalLink } from 'lucide-svelte'
import { untrack } from 'svelte'
interface Props {
placement?: PopoverPlacement
notClickable?: boolean
popupClass?: string
disablePopup?: boolean
disappearTimeout?: number
appearTimeout?: number
documentationLink?: string | undefined
style?: string | undefined
forceOpen?: boolean
class?: string
children?: import('svelte').Snippet
text?: import('svelte').Snippet
onClick?: () => void
}
let {
placement = 'bottom-end',
notClickable = false,
popupClass = '',
disablePopup = false,
disappearTimeout = 100,
appearTimeout = 300,
documentationLink = undefined,
style = undefined,
forceOpen = false,
class: classNames = '',
children,
text,
onClick
}: Props = $props()
export let placement: PopoverPlacement = 'bottom-end'
export let notClickable = false
export let popupClass = ''
export let disablePopup = false
export let disappearTimeout = 100
export let appearTimeout = 300
export let documentationLink: string | undefined = undefined
export let style: string | undefined = undefined
export let forceOpen = false
const [popperRef, popperContent] = createPopperActions({ placement })
@@ -55,7 +32,7 @@
]
}
let showTooltip = $state(false)
let showTooltip = false
let timeout: NodeJS.Timeout | undefined = undefined
let inTimeout: NodeJS.Timeout | undefined = undefined
@@ -73,43 +50,40 @@
timeout = setTimeout(() => (showTooltip = false), disappearTimeout)
}
$effect(() => {
;[forceOpen]
untrack(() => (forceOpen ? open() : close()))
})
$: forceOpen ? open() : close()
</script>
{#if notClickable}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span {style} use:popperRef onmouseenter={open} onmouseleave={close} class={classNames}>
{@render children?.()}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span {style} use:popperRef on:mouseenter={open} on:mouseleave={close} class={$$props.class}>
<slot />
</span>
{:else}
<button
{style}
use:popperRef
onmouseenter={open}
onmouseleave={close}
onclick={onClick}
class={classNames}
on:mouseenter={open}
on:mouseleave={close}
on:click
class={$$props.class}
>
{@render children?.()}
<slot />
</button>
{/if}
{#if showTooltip && !disablePopup}
<Portal name="popover">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
use:popperContent={popperOptions}
onmouseenter={open}
onmouseleave={close}
on:mouseenter={open}
on:mouseleave={close}
class={twMerge(
'z-[5001] py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800 whitespace-normal text-left',
popupClass
)}
>
<div class="max-w-sm break-words">
{@render text?.()}
<slot name="text" />
{#if documentationLink}
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
<div class="flex flex-row gap-2 mt-4">
@@ -20,7 +20,7 @@
import autosize from '$lib/autosize'
import GfmMarkdown from './GfmMarkdown.svelte'
import TestTriggerConnection from './triggers/TestTriggerConnection.svelte'
import GitHubAppIntegration from './GitHubAppIntegration.svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
interface Props {
canSave?: boolean
@@ -28,7 +28,7 @@
path?: string
newResource?: boolean
hidePath?: boolean
onChange?: (args: { path: string; args: Record<string, any>; description: string }) => void
watchChanges?: boolean
defaultValues?: Record<string, any> | undefined
}
@@ -38,7 +38,7 @@
path = $bindable(''),
newResource = false,
hidePath = false,
onChange,
watchChanges = false,
defaultValues = undefined
}: Props = $props()
@@ -61,6 +61,7 @@
let viewJsonSchema = $state(false)
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
let rawCode: string | undefined = $state(undefined)
@@ -179,8 +180,8 @@
run(() => {
canSave = can_write && isValid && jsonError == ''
})
$effect(() => {
onChange && onChange({ path, args, description })
run(() => {
watchChanges && dispatchIfMounted('change', { path, args, description })
})
run(() => {
rawCode && untrack(() => parseJson())
@@ -246,33 +247,19 @@
<GfmMarkdown md={description} />
{/if}
<div class="w-full flex gap-4 flex-row-reverse items-center mt-4">
<div class="flex w-full justify-between items-center mt-4">
<div></div>
{#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'}
<TestTriggerConnection kind={resourceToEdit?.resource_type} args={{ connection: args }} />
{:else}
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
{/if}
<Toggle
on:change={(e) => switchTab(e.detail)}
options={{
right: 'As JSON'
}}
/>
{#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'}
<TestTriggerConnection kind={resourceToEdit?.resource_type} args={{ connection: args }} />
{:else}
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
{/if}
{#if resource_type === 'git_repository' && $workspaceStore && $userStore?.is_admin}
<GitHubAppIntegration
resourceType={resource_type}
{args}
{description}
onArgsUpdate={(newArgs) => {
args = newArgs
// Update rawCode if in JSON view mode
if (viewJsonSchema) {
rawCode = JSON.stringify(args, null, 2)
}
}}
onDescriptionUpdate={(newDescription) => (description = newDescription)}
/>
{/if}
</div>
<div>
{#if loadingSchema}
@@ -1,7 +1,7 @@
<script lang="ts">
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onMount, untrack } from 'svelte'
import { createEventDispatcher, onMount, untrack } from 'svelte'
import AppConnect from './AppConnectDrawer.svelte'
import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
@@ -11,6 +11,10 @@
import { sendUserToast } from '$lib/toast'
import { isDbType } from './apps/components/display/dbtable/utils'
import Select from './select/Select.svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
interface Props {
initialValue?: string | undefined
@@ -24,7 +28,6 @@
expressOAuthSetup?: boolean
defaultValues?: Record<string, any> | undefined
placeholder?: string | undefined
onClear?: () => void
}
let {
@@ -38,8 +41,7 @@
selectFirst = false,
expressOAuthSetup = false,
defaultValues = undefined,
placeholder = undefined,
onClear = undefined
placeholder = undefined
}: Props = $props()
if (initialValue && value == undefined) {
@@ -138,6 +140,10 @@
)
})
$effect(() => {
dispatchIfMounted('change', value)
})
let appConnect: AppConnect | undefined = $state()
let resourceEditor: ResourceEditorDrawer | undefined = $state()
</script>
@@ -180,7 +186,7 @@
initialValue = undefined
value = undefined
valueType = undefined
onClear?.()
dispatch('clear')
}}
items={collection}
clearable

Some files were not shown because too many files have changed in this diff Show More