mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-16 08:02:28 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
065b0efa85 | ||
|
|
83e5bfbc17 |
@@ -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
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
Generated
+151
-164
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -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 @@
|
||||
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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -941,7 +941,6 @@ async fn create_script_internal<'c>(
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
permissioned_as,
|
||||
authed.token_prefix.as_deref(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -11,11 +11,11 @@ use std::collections::HashMap;
|
||||
use windmill_common::error::Error;
|
||||
use windmill_common::variables::get_secret_value_as_admin;
|
||||
|
||||
use crate::{approvals::{
|
||||
use crate::approvals::{
|
||||
extract_w_id_from_resume_url, handle_resume_action, ApprovalFormDetails, FieldType,
|
||||
MessageFormat, QueryDefaultArgsJson, QueryDynamicEnumJson, QueryFlowStepId, QueryMessage,
|
||||
ResumeFormField, ResumeSchema,
|
||||
}, auth::OptTokened};
|
||||
};
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
use crate::jobs::{QueryApprover, ResumeUrls};
|
||||
|
||||
@@ -116,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?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
@@ -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
@@ -63,7 +63,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.502.2";
|
||||
export const VERSION = "1.501.3";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,6 @@ node_modules
|
||||
/.svelte-kit
|
||||
/package
|
||||
/src/lib/gen
|
||||
.vite.temp.*.js
|
||||
CaddyfileRemoteRuben
|
||||
tests-out/
|
||||
storageState.json
|
||||
|
||||
Generated
+12
-23
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+5
-5
@@ -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() {
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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 <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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user