Compare commits

...
Author SHA1 Message Date
Ruben Fiszel 06898b9837 all 2025-06-16 15:16:15 +02:00
Ruben Fiszel 8306e6e5d2 all 2025-06-16 15:13:06 +02:00
Ruben Fiszel 6c1e7eed51 nit focus modal 2025-06-16 14:42:47 +02:00
Ruben Fiszel f24894b1a5 fix tab selection on app builder 2025-06-16 14:36:26 +02:00
Diego Imbert 50d78a979b Fix select not scrolling correctly (#5946) 2025-06-16 12:08:59 +02:00
Ruben Fiszel c75fd751b0 nits app builder 2025-06-15 23:57:55 +02:00
Ruben Fiszel f0f720f490 partial app svelte 5 migration (#5945) 2025-06-15 23:25:34 +02:00
Ruben Fiszel 855571f359 fix history 2025-06-15 04:22:47 +02:00
Ruben Fiszel 41219e1f84 fix app editor reactivity 2025-06-15 03:59:37 +02:00
Ruben Fiszel 499fe7e7fe field.expr error message 2025-06-15 03:16:04 +02:00
JonasGruenwald 72613f3580 Fix wording in DeployOverrideConfirmationModal.svelte (#5944) 2025-06-14 17:07:19 +02:00
Ruben Fiszel 7d4ee825fa nits 2025-06-14 13:21:45 +02:00
Ruben Fiszel 1cd253405d nits 2025-06-14 12:33:37 +02:00
Ruben Fiszel 4dab80e24d fix schemaform default handling 2025-06-14 12:31:46 +02:00
Ruben Fiszel 38bb5e25cb nit 2025-06-14 12:02:48 +02:00
Ruben Fiszel 3678bf976e improve redirect behavior 2025-06-14 10:46:39 +02:00
pyranota 6623972ee9 build: correctly build WASM parsers with nix shell (#5943) 2025-06-14 09:03:45 +02:00
Diego Imbert 6a4391f616 Fix all structuredClones missing $state.snapshot (#5941) 2025-06-13 18:46:04 +02:00
centdix f7a83c03c1 feat: add api tools to ai chat (#5921)
* add api tools to ai chat

* cleaning

* cleaning

* refine prompts

* tweaks

* add missing trigger

* cleaning

* cleaning

* fix
2025-06-13 16:29:18 +00:00
Ruben Fiszel 82f541565f fix reactivity in apps 2025-06-13 18:04:54 +02:00
Ruben Fiszel 44e1ed6c9c fix search by args 2025-06-13 17:42:06 +02:00
Ruben Fiszel 58ca3a6866 fix reactivity on app form 2025-06-13 17:37:39 +02:00
Ruben Fiszel 0b3a084c81 fix handle default values 2025-06-13 14:10:55 +02:00
HugoCasa 17c8c8a561 fix(frontend): use correct kind for flow insert module btn (#5938) 2025-06-13 11:43:09 +02:00
centdix 1a6283b42a fix: fix input with wrong height on first render (#5935)
* fix input with wrong height on first render

* better fix
2025-06-13 09:29:57 +00:00
Ruben Fiszel 126b610561 introduce QUIET_LOGS 2025-06-13 11:25:27 +02:00
pyranotaandRuben Fiszel 0b89260540 feat(go): local go.mod (#5929)
* feat(go): local go.mod

* readability refactor

* remove dbg!

* ignore module

* remove space

Signed-off-by: pyranota <pyra@duck.com>

* Update backend/windmill-worker/src/go_executor.rs

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-06-13 10:17:31 +02:00
centdix 6c4818259c use new token for docs (#5937) 2025-06-13 10:08:26 +02:00
Ruben Fiszel 283167264e more svelte 5 migrations 2025-06-13 08:47:27 +02:00
Ruben Fiszel b3e7a9cb79 more svelte 5 migration (#5936)
* all

* icons

* svelte2
2025-06-13 08:23:23 +02:00
Ruben Fiszel c3a54ddd7c fix path bind 2025-06-13 06:47:05 +02:00
Alexander Petric ae684d8608 improve teams startup time (#5931) 2025-06-12 21:40:18 +02:00
HugoCasa f077849b8f fix: flow step missing input warnings (#5916)
* fix: flow step missing input warnings

* nit
2025-06-12 21:38:26 +02:00
HugoCasa cf2d09e7a8 fix: audit logs for token refresh + consider refresh for active users (#5930)
* fix: audit logs for token refresh + consider refresh for active users

* tmp repo ref
2025-06-12 21:37:50 +02:00
Diego Imbert bb354cfeb8 standardize select auto-complete inputs
* Replaced some Autocompletes

* more AutoComplete replace

* usePromise + replace resources with Select

* Select Chevron + changed operation to new Select

* simplified EditableSchemaWrapper

* replace autocompletes

* Replace last autocomplete

* Remove 'simple-svelte-autocomplete' dependency

* re-added some createText
2025-06-12 21:36:55 +02:00
Diego Imbert bebfa76fc2 remove the 55 (#5933) 2025-06-12 21:30:56 +02:00
centdix b0ac12052a chore(frontend): convert ai context components to svelte5 (#5928)
* convert contexttextarea to svelte5

* convert availablecontextlist

* force one line text and add max width to context badge

* cleaning
2025-06-12 16:01:03 +00:00
HugoCasa f412ede6ed feat: multiple azure models support (#5920)
* feat: multiple azure models support

* all
2025-06-12 17:34:33 +02:00
Diego ImbertandRuben Fiszel f94651115a worker page ui fixes (#5927)
* worker page ui fixes

* Update +page.svelte

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-06-12 17:33:47 +02:00
centdix 6f907c79b4 fix: ai chat tooltip + user settings autocomplete issue (#5917)
* use form to avoid issues with chrome autocomplete

* fix tooltip overflowing to the right

* fix missclick on tooltip

* better fix

* dont show expiration on mcp token creation

* add zindexes to safelist

* add autocomplete
2025-06-12 17:00:52 +02:00
centdix 6e7ea4111c fix-app-drawer-offset (#5922) 2025-06-12 14:32:35 +00:00
Diego Imbert ff9acb4717 remove unnecessary dispatchIfMounted (#5926) 2025-06-12 16:21:58 +02:00
centdix a393c93085 nit: show disabled message when lang not supported (#5925)
* show disabled message when lang not supported

* remove open effect
2025-06-12 16:20:52 +02:00
Ruben Fiszel 23920aee84 feat: better graph layout algorithm + migrate to svelte 5 almost everywhere + xyflow 1.0 2025-06-12 15:44:35 +02:00
centdix d47c078bb5 feat(backend): use streamable http in favor of sse for MCP (#5910)
* draft for http streamable usage

* good stuff

* add workspace_id to extensions

* fix shutdown

* cleaning

* fix

* adapt frontend

* Revert "adapt frontend"

This reverts commit 331dffaf98.

* dont use new path

* cleaning

* cleaner way of closing sessions
2025-06-12 08:49:22 +00:00
b4a6a7e724 feat: fill runnable inputs with AI chat (#5887)
* feat: integrate TriggerableByAI with SchemaForm components

- Add currentValue and schema props to TriggerableByAI component
- Wrap all ArgInput fields with TriggerableByAI for AI chat integration
- Each input field now registers with AI chat manager including:
  - Current field value
  - Schema details (type, description, format, etc.)
  - Proper triggering mechanism for AI-driven value updates

This enables AI chat to interact with any input type generated by SchemaForm,
allowing intelligent form field modifications based on context and user intent.

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>

* add schema to description

* draft

* draft

* just use json inputs component

* good starting point

* add triggerable to scriptrow + shortcut

* fixes

* save prompt for ai in schema

* fix

* change visibility

* simplify

* cleaning

* fixes

* add tool to fetch resources

* fixes

* add try catch

* fix prompt

* cleaning

* use ask ai button

* fix

* no animation on form + fix empty summary

* add inputselectedbadge

* better action description + fix rows border

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: centdix <farhadg110@gmail.com>
2025-06-10 20:12:41 +00:00
pyranota 332f66e348 feat(rust): add rust sdk (#5909)
* upload client

* add gh workflow

* refactor build script

* remove dbg!

* fix async

* remove unused

* update dev.nu

* update CI

* fix ci

* fixin tests

* fixes + tests
2025-06-10 18:38:18 +02:00
Ruben Fiszel d2dfd27b88 nit error message 2025-06-10 15:33:48 +02:00
Diego Imbert b56a5c3e7a never exclude _ee.rs files in vscode (#5907) 2025-06-10 11:49:47 +02:00
562 changed files with 24070 additions and 14760 deletions
+2 -2
View File
@@ -12,12 +12,12 @@ jobs:
- name: Check organization membership
id: check-membership
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
COMMENTER: ${{ github.event.comment.user.login }}
run: |
ORG="windmill-labs"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $GH_TOKEN" \
-H "Authorization: token $ORG_ACCESS_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/orgs/$ORG/members/$COMMENTER")
+19
View File
@@ -0,0 +1,19 @@
name: Publish rust-client to crates.io on release
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
build_rust_and_publish_to_crates_io:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v20
with:
extra_nix_config: |
experimental-features = nix-command flakes
- run: cd rust-client && nix develop ../ --command ./dev.nu --check --publish
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -67,5 +67,5 @@
true
]
},
"hash": "3895cee539a24b4c6ea89fa7a835fc62bc93b0530efba09fc3c32a8f93eaabb1"
"hash": "5430f7728c1e9b539cc8aad29ca9e6733943278998d3df62a9486607827e59ec"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT distinct(path) FROM script WHERE workspace_id = $1 AND archived = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "8373b2649ab46310860adbdd7b717261771ac61d46d82d42d085ffebeb18be06"
}
@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users as (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n active_authors as (SELECT distinct email FROM usr WHERE usr.operator IS false AND email IN (SELECT email FROM active_users)),\n active_authors_agg as (SELECT array_agg(email) as authors FROM active_authors),\n active_ops_agg as (SELECT array_agg(email) as operators from active_users WHERE email NOT IN (SELECT email FROM active_authors))\n SELECT active_authors_agg.authors, active_ops_agg.operators, array_length(active_authors_agg.authors, 1) as author_count, array_length(active_ops_agg.operators, 1) as operator_count FROM active_authors_agg, active_ops_agg",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "authors",
"type_info": "VarcharArray"
},
{
"ordinal": 1,
"name": "operators",
"type_info": "VarcharArray"
},
{
"ordinal": 2,
"name": "author_count",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "operator_count",
"type_info": "Int4"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "cb3862634f18160207ee2621ddfca43f00456a27fda32583846497116f92f96c"
}
@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users as (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n active_authors as (SELECT distinct email FROM usr WHERE usr.operator IS false AND email IN (SELECT email FROM active_users)),\n active_authors_agg as (SELECT array_agg(email) as authors FROM active_authors),\n active_ops_agg as (SELECT array_agg(email) as operators from active_users WHERE email NOT IN (SELECT email FROM active_authors))\n SELECT active_authors_agg.authors, active_ops_agg.operators, array_length(active_authors_agg.authors, 1) as author_count, array_length(active_ops_agg.operators, 1) as operator_count FROM active_authors_agg, active_ops_agg",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "authors",
"type_info": "VarcharArray"
},
{
"ordinal": 1,
"name": "operators",
"type_info": "VarcharArray"
},
{
"ordinal": 2,
"name": "author_count",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "operator_count",
"type_info": "Int4"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "cce991f582bc9d2ba28a5b2b41c679366bb07bc6a100727721a787160ac6910c"
}
+3 -1
View File
@@ -11,5 +11,7 @@
"remote.autoForwardPorts": true,
"conventionalCommits.scopes": [
"restructring triggers, decoding trigger message on work"
]
],
"files.exclude": { "**/*ee.rs": false },
"search.exclude": { "**/*ee.rs": false }
}
+25 -5
View File
@@ -10402,13 +10402,15 @@ dependencies = [
[[package]]
name = "rmcp"
version = "0.1.5"
source = "git+https://github.com/windmill-labs/rust-sdk#9142b40202e49ca0b6530fa49a3abc8bd1b2fcf0"
source = "git+https://github.com/modelcontextprotocol/rust-sdk#db03f63e76b5b32f65d34a1bd08ae56dab595f60"
dependencies = [
"async-stream",
"axum",
"base64 0.21.7",
"base64 0.22.1",
"bytes",
"chrono",
"futures",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"paste",
"pin-project-lite",
"rand 0.9.0",
@@ -10416,20 +10418,24 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"sse-stream",
"thiserror 2.0.12",
"tokio",
"tokio-stream",
"tokio-util",
"tower-service",
"tracing",
"uuid",
]
[[package]]
name = "rmcp-macros"
version = "0.1.5"
source = "git+https://github.com/windmill-labs/rust-sdk#9142b40202e49ca0b6530fa49a3abc8bd1b2fcf0"
source = "git+https://github.com/modelcontextprotocol/rust-sdk#db03f63e76b5b32f65d34a1bd08ae56dab595f60"
dependencies = [
"proc-macro2",
"quote",
"serde_json",
"syn 2.0.101",
]
@@ -10967,6 +10973,7 @@ version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
dependencies = [
"chrono",
"dyn-clone",
"schemars_derive",
"serde",
@@ -11880,6 +11887,19 @@ dependencies = [
"uuid",
]
[[package]]
name = "sse-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f649a9f9e91db2ed32f3724516eac2bc09fab77fc33be8f670f5619b9dc6c3f"
dependencies = [
"bytes",
"futures-util",
"http-body 1.0.1",
"http-body-util",
"pin-project-lite",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
+1 -1
View File
@@ -1 +1 @@
ab4f856708a056973faea19f10eda5efff546176
2c3e21f4573486628e0b8969ff478c237bd0283f
+7 -2
View File
@@ -159,6 +159,7 @@ lazy_static::lazy_static! {
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(3600 * 12);
}
pub fn main() -> anyhow::Result<()> {
@@ -1065,7 +1066,9 @@ Windmill Community Edition {GIT_VERSION}
}
if server_mode {
tracing::info!("monitor task started");
if !*windmill_common::QUIET_LOGS {
tracing::info!("monitor task started");
}
}
monitor_db(
&conn,
@@ -1077,7 +1080,9 @@ Windmill Community Edition {GIT_VERSION}
)
.await;
if server_mode {
tracing::info!("monitor task finished");
if !*windmill_common::QUIET_LOGS {
tracing::info!("monitor task finished");
}
}
},
}
-2
View File
@@ -1324,7 +1324,6 @@ pub async fn monitor_db(
initial_load: bool,
_killpill_tx: KillpillSender,
) {
tracing::info!("Starting periodic monitor task");
let zombie_jobs_f = async {
if server_mode && !initial_load && !*DISABLE_ZOMBIE_JOBS_MONITORING {
if let Some(db) = conn.as_sql() {
@@ -1422,7 +1421,6 @@ pub async fn monitor_db(
apply_autoscaling_f,
update_min_worker_version_f,
);
tracing::info!("Periodic monitor task completed");
}
pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
+1 -1
View File
@@ -40,7 +40,7 @@ mcp = ["dep:rmcp"]
python = []
[dependencies]
rmcp = { git = "https://github.com/windmill-labs/rust-sdk", features = ["transport-sse-server"], optional = true }
rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true }
windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
+32 -4
View File
@@ -24,7 +24,7 @@ lazy_static::lazy_static! {
pub static ref AI_REQUEST_CACHE: Cache<(String, AIProvider), ExpiringAIRequestConfig> = Cache::new(500);
}
const AZURE_API_VERSION: &str = "2024-10-21";
const AZURE_API_VERSION: &str = "2025-04-01-preview";
const OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
#[derive(Deserialize, Debug)]
@@ -143,17 +143,33 @@ impl AIRequestConfig {
path: &str,
body: Bytes,
) -> Result<RequestBuilder> {
let url = format!("{}/{}", self.base_url, path);
let body = if let Some(user) = self.user {
Self::add_user_to_body(body, user)?
} else {
body
};
let is_azure = matches!(provider, AIProvider::OpenAI) && self.base_url != OPENAI_BASE_URL
let base_url = self.base_url.trim_end_matches('/');
let is_azure = matches!(provider, AIProvider::OpenAI) && base_url != OPENAI_BASE_URL
|| matches!(provider, AIProvider::AzureOpenAI);
let url = if is_azure {
if base_url.ends_with("/deployments") {
let model = Self::get_azure_model(&body)?;
format!("{}/{}/{}", base_url, model, path)
} else if base_url.ends_with("/openai") {
let model = Self::get_azure_model(&body)?;
format!("{}/deployments/{}/{}", base_url, model, path)
} else {
format!("{}/{}", base_url, path)
}
} else {
format!("{}/{}", base_url, path)
};
tracing::debug!("AI request URL: {}", url);
let mut request = HTTP_CLIENT
.post(url)
.header("content-type", "application/json")
@@ -199,6 +215,18 @@ impl AIRequestConfig {
.map_err(|e| Error::internal_err(format!("Failed to reserialize request body: {}", e)))?
.into())
}
fn get_azure_model(body: &Bytes) -> Result<String> {
#[derive(Deserialize, Debug)]
struct AzureModel {
model: String,
}
let azure_model: AzureModel = serde_json::from_slice(body)
.map_err(|e| Error::internal_err(format!("Failed to parse request body: {}", e)))?;
Ok(azure_model.model)
}
}
#[derive(Clone, Debug)]
+14 -22
View File
@@ -19,14 +19,16 @@ use crate::oauth2_oss::SlackVerifier;
use crate::smtp_server_oss::SmtpServer;
#[cfg(feature = "mcp")]
use crate::mcp::{setup_mcp_server, Runner as McpRunner};
use crate::mcp::{extract_and_store_workspace_id, setup_mcp_server, shutdown_mcp_server};
#[cfg(feature = "mcp")]
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use crate::tracing_init::MyOnFailure;
use crate::{
tracing_init::{MyMakeSpan, MyOnResponse},
users::OptAuthed,
webhook_util::WebhookShared,
};
#[cfg(feature = "agent_worker_server")]
use agent_workers_oss::AgentCache;
@@ -520,21 +522,18 @@ pub async fn run_server(
// Setup MCP server
#[allow(unused_variables)]
let (mcp_router, mcp_main_ct, mcp_service_ct) = {
let (mcp_router, mcp_session_manager) = {
#[cfg(feature = "mcp")]
if server_mode || mcp_mode {
let (mcp_sse_server, mcp_router) = setup_mcp_server(addr, "/api/mcp/w/:workspace_id")?;
#[cfg(feature = "mcp")]
let mcp_main_ct = mcp_sse_server.config.ct.clone(); // Token to signal shutdown *to* MCP
#[cfg(feature = "mcp")]
let mcp_service_ct = mcp_sse_server.with_service(McpRunner::new); // Token to wait for MCP *service* shutdown
(mcp_router, Some(mcp_main_ct), Some(mcp_service_ct))
let (mcp_router, mcp_session_manager) = setup_mcp_server().await?;
let mcp_middleware = axum::middleware::from_fn(extract_and_store_workspace_id);
(mcp_router.layer(mcp_middleware), Some(mcp_session_manager))
} else {
(Router::new(), None, None)
(Router::new(), Option::<Arc<LocalSessionManager>>::None)
}
#[cfg(not(feature = "mcp"))]
(Router::new(), None::<()>, None::<()>)
(Router::new(), Option::<()>::None)
};
#[cfg(feature = "agent_worker_server")]
@@ -660,7 +659,7 @@ pub async fn run_server(
.layer(from_extractor::<OptAuthed>())
.layer(cors.clone()),
)
.nest("/mcp/w/:workspace_id", mcp_router)
.nest("/mcp/w/:workspace_id/sse", mcp_router)
.layer(from_extractor::<OptAuthed>())
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]
@@ -819,16 +818,9 @@ pub async fn run_server(
tracing::info!("Graceful shutdown of server");
#[cfg(feature = "mcp")]
{
if let Some(mcp_main_ct) = mcp_main_ct {
tracing::info!("Received shutdown signal, cancelling MCP server...");
mcp_main_ct.cancel();
}
if let Some(mcp_service_ct) = mcp_service_ct {
tracing::info!("Waiting for MCP service cancellation...");
mcp_service_ct.cancelled().await;
tracing::info!("MCP service cancelled.");
}
if let Some(mcp_session_manager) = mcp_session_manager {
shutdown_mcp_server(mcp_session_manager).await;
tracing::info!("MCP server shutdown");
}
});
+112 -43
View File
@@ -1,11 +1,10 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use axum::body::to_bytes;
use axum::Router;
use rmcp::transport::sse_server::{SseServer, SseServerConfig};
use axum::{extract::Path, http::Request, middleware::Next, response::Response};
use rmcp::{
handler::server::ServerHandler,
model::*,
@@ -17,7 +16,6 @@ use serde_json::Value;
use sql_builder::prelude::*;
use sqlx::FromRow;
use tokio::try_join;
use tokio_util::sync::CancellationToken;
use windmill_common::db::UserDB;
use windmill_common::worker::to_raw_value;
use windmill_common::{DB, HUB_BASE_URL};
@@ -29,6 +27,9 @@ use crate::jobs::{
run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery,
};
use crate::HTTP_CLIENT;
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager, SessionManager, StreamableHttpService,
};
use windmill_common::utils::{query_elems_from_hub, StripPath};
/// Transforms the path for workspace scripts/flows.
@@ -856,28 +857,44 @@ impl ServerHandler for Runner {
})
};
let authed = context
.req_extensions
.get::<ApiAuthed>()
.ok_or_else(|| Error::internal_error("ApiAuthed not found", None))?;
let db = context
.req_extensions
.get::<DB>()
.ok_or_else(|| Error::internal_error("DB not found", None))?;
let user_db = context
.req_extensions
.get::<UserDB>()
.ok_or_else(|| Error::internal_error("UserDB not found", None))?;
let http_parts = context
.extensions
.get::<axum::http::request::Parts>()
.ok_or_else(|| {
tracing::error!("http::request::Parts not found");
Error::internal_error("http::request::Parts not found", None)
})?;
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
Error::internal_error("DB Axum extension not found", None)
})?;
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
tracing::error!("UserDB Axum extension not found");
Error::internal_error("UserDB Axum extension not found", None)
})?;
let args = parse_args(request.arguments)?;
let workspace_id = http_parts
.extensions
.get::<WorkspaceId>()
.ok_or_else(|| {
tracing::error!("WorkspaceId not found");
Error::internal_error("WorkspaceId not found", None)
})
.map(|w_id| w_id.0.clone())?;
let (tool_type, path, is_hub) =
Runner::reverse_transform(&request.name).unwrap_or_default();
let item_schema = if is_hub {
Runner::get_hub_script_schema(&format!("hub/{}", path), db).await?
} else {
Runner::get_item_schema(&path, user_db, authed, &context.workspace_id, &tool_type)
.await?
Runner::get_item_schema(&path, user_db, authed, &workspace_id, &tool_type).await?
};
let schema_obj = if let Some(ref s) = item_schema {
@@ -906,8 +923,6 @@ impl ServerHandler for Runner {
} else {
windmill_queue::PushArgsOwned::default()
};
let w_id = context.workspace_id.clone();
let script_or_flow_path = if is_hub {
StripPath(format!("hub/{}", path))
} else {
@@ -922,7 +937,7 @@ impl ServerHandler for Runner {
script_or_flow_path,
authed.clone(),
user_db.clone(),
w_id.clone(),
workspace_id.clone(),
push_args,
)
.await
@@ -934,7 +949,7 @@ impl ServerHandler for Runner {
authed.clone(),
user_db.clone(),
push_args,
w_id.clone(),
workspace_id.clone(),
)
.await
};
@@ -978,19 +993,38 @@ impl ServerHandler for Runner {
_request: Option<PaginatedRequestParam>,
mut _context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, Error> {
let workspace_id = _context.workspace_id.clone();
let db = _context
.req_extensions
.get::<DB>()
.ok_or_else(|| Error::internal_error("DB not found", None))?;
let user_db = _context
.req_extensions
.get::<UserDB>()
.ok_or_else(|| Error::internal_error("UserDB not found", None))?;
let authed = _context
.req_extensions
.get::<ApiAuthed>()
.ok_or_else(|| Error::internal_error("ApiAuthed not found", None))?;
let http_parts = _context
.extensions
.get::<axum::http::request::Parts>()
.ok_or_else(|| {
tracing::error!("http::request::Parts not found");
Error::internal_error("http::request::Parts not found", None)
})?;
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
Error::internal_error("DB Axum extension not found", None)
})?;
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
tracing::error!("UserDB Axum extension not found");
Error::internal_error("UserDB Axum extension not found", None)
})?;
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
let workspace_id = http_parts
.extensions
.get::<WorkspaceId>()
.ok_or_else(|| {
tracing::error!("WorkspaceId not found");
Error::internal_error("WorkspaceId not found", None)
})
.map(|w_id| w_id.0.clone())?;
let owned_scope = authed.scopes.as_ref().and_then(|scopes| {
scopes
.iter()
@@ -1127,15 +1161,50 @@ impl ServerHandler for Runner {
}
}
pub fn setup_mcp_server(addr: SocketAddr, path: &str) -> anyhow::Result<(SseServer, Router)> {
let config = SseServerConfig {
bind: addr,
sse_path: "/sse".to_string(),
post_path: "/message".to_string(),
full_message_path: path.to_string(),
ct: CancellationToken::new(),
sse_keep_alive: None,
#[derive(Clone, Debug)]
pub struct WorkspaceId(pub String);
pub async fn extract_and_store_workspace_id(
Path(params): Path<String>,
mut request: Request<axum::body::Body>,
next: Next,
) -> Response {
let workspace_id = params;
request.extensions_mut().insert(WorkspaceId(workspace_id));
next.run(request).await
}
pub async fn setup_mcp_server() -> anyhow::Result<(Router, Arc<LocalSessionManager>)> {
let session_manager = Arc::new(LocalSessionManager::default());
let service_config = Default::default();
let service = StreamableHttpService::new(Runner::new, session_manager.clone(), service_config);
let router = axum::Router::new().nest_service("/", service);
Ok((router, session_manager))
}
pub async fn shutdown_mcp_server(session_manager: Arc<LocalSessionManager>) {
let session_ids_to_close = {
let sessions_map = session_manager.sessions.read().await;
sessions_map.keys().cloned().collect::<Vec<_>>()
};
Ok(SseServer::new(config))
if !session_ids_to_close.is_empty() {
tracing::info!(
"Closing {} active MCP session(s)...",
session_ids_to_close.len()
);
let close_futures = session_ids_to_close
.iter()
.map(|session_id| {
let manager_clone = session_manager.clone();
async move {
if let Err(_) = manager_clone.close_session(session_id).await {
tracing::warn!("Error closing MCP session");
}
}
})
.collect::<Vec<_>>();
futures::future::join_all(close_futures).await;
}
}
+10 -3
View File
@@ -1354,9 +1354,16 @@ async fn raw_script_by_path_internal(
)
.fetch_all(&db)
.await?;
tracing::info!(
"Script {path} does not exist in workspace {w_id} but these paths do: {:?}",
other_script_o.join(", ")
let other_script_archived = sqlx::query_scalar!(
"SELECT distinct(path) FROM script WHERE workspace_id = $1 AND archived = true",
w_id
)
.fetch_all(&db)
.await?;
tracing::warn!(
"Script {path} does not exist in workspace {w_id} but these paths do, non-archived: {:?} | archived: {:?}",
other_script_o.join(", "),
other_script_archived.join(", ")
)
}
}
+17 -2
View File
@@ -542,7 +542,7 @@ async fn list_users_as_super_admin(
let rows = if active_only.is_some_and(|x| x) {
sqlx::query_as!(
GlobalUserInfo,
"WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),
"WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),
authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username
FROM password
@@ -1750,7 +1750,22 @@ async fn refresh_token(
.await?
.unwrap_or(false);
let _ = create_session_token(&authed.email, super_admin, &mut tx, cookies).await?;
let new_token = create_session_token(&authed.email, super_admin, &mut tx, cookies).await?;
audit_log(
&mut *tx,
&AuditAuthor {
email: authed.email.to_string(),
username: authed.email.to_string(),
username_override: None,
},
"users.token.refresh",
ActionKind::Create,
&"global",
Some(&truncate_token(&new_token)),
None,
)
.await?;
tx.commit().await?;
Ok("token refreshed".to_string())
+2
View File
@@ -149,6 +149,8 @@ lazy_static::lazy_static! {
pub static ref DEPLOYED_SCRIPT_INFO_CACHE: Cache<(String, i64), ScriptHashInfo> = Cache::new(1000);
pub static ref FLOW_INFO_CACHE: Cache<(String, i64), FlowVersionInfo> = Cache::new(1000);
pub static ref QUIET_LOGS: bool = std::env::var("QUIET_LOGS").map(|s| s.parse::<bool>().unwrap_or(false)).unwrap_or(false);
}
const LATEST_VERSION_ID_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(60);
+1 -1
View File
@@ -81,7 +81,7 @@ lazy_static::lazy_static! {
}
Mode::Worker
} else if &x == "agent" {
println!("Binary is in 'agent' mode");
println!("Binary is in 'agent' mode with BASE_INTERNAL_URL={}", std::env::var("BASE_INTERNAL_URL").unwrap_or_default());
if std::env::var("BASE_INTERNAL_URL").is_err() {
panic!("BASE_INTERNAL_URL is required in agent mode")
}
+2 -2
View File
@@ -2716,7 +2716,7 @@ pub async fn get_result_by_id(
.await
{
Ok(res) => Ok(res),
Err(_) => {
Err(e) => {
let root = sqlx::query!(
"SELECT
id As \"id!\",
@@ -2733,7 +2733,7 @@ pub async fn get_result_by_id(
let restarted_from_id = not_found_if_none(
root.restarted_from,
"Id not found in the result's mapping of the root job and root job had no restarted from information",
format!("parent: {}, root: {}, id: {}", flow_id, root.id, node_id),
format!("parent: {}, root: {}, id: {}, error: {e:#}", flow_id, root.id, node_id),
)?;
get_result_by_id_from_original_flow(
+29 -4
View File
@@ -3,7 +3,11 @@ use std::{collections::HashMap, fs::DirBuilder, process::Stdio};
use itertools::Itertools;
use serde_json::value::RawValue;
use tokio::{fs::File, io::AsyncReadExt, process::Command};
use tokio::{
fs::{self, File},
io::AsyncReadExt,
process::Command,
};
use uuid::Uuid;
use windmill_common::{
error::{self, Error},
@@ -91,6 +95,7 @@ pub async fn handle_go_job(
true,
skip_go_mod,
skip_tidy,
false,
worker_name,
&job.workspace_id,
occupation_metrics,
@@ -356,11 +361,21 @@ pub async fn install_go_dependencies(
non_dep_job: bool,
skip_go_mod: bool,
has_sum: bool,
raw_deps: bool,
worker_name: &str,
w_id: &str,
occupation_metrics: &mut OccupancyMetrics,
) -> error::Result<String> {
if !skip_go_mod {
if raw_deps {
let go_mod =
if let Some(module) = code.lines().find(|l| l.trim_start().starts_with("module ")) {
code.replace(module, "module mymod")
} else {
format!("module mymod\n{code}")
};
fs::write(format!("{job_dir}/go.mod"), go_mod).await?;
}
if !raw_deps && !skip_go_mod {
gen_go_mymod(code, job_dir).await?;
let mut child_cmd = Command::new(GO_PATH.as_str());
child_cmd
@@ -400,7 +415,9 @@ pub async fn install_go_dependencies(
let mut new_lockfile = false;
let hash = if !has_sum {
let hash = if raw_deps {
calculate_hash(code)
} else if !has_sum {
calculate_hash(parse_go_imports(&code)?.iter().join("\n").as_str())
} else {
"".to_string()
@@ -429,7 +446,15 @@ pub async fn install_go_dependencies(
}
}
let mod_command = if skip_tidy { "download" } else { "tidy" };
let mod_command = if skip_tidy ||
// If there is go.mod provided we want to use `download` only.
// Unlike `tidy` it does not modify local go.mod
raw_deps
{
"download"
} else {
"tidy"
};
let mut child_cmd = Command::new(GO_PATH.as_str());
child_cmd
.current_dir(job_dir)
@@ -2193,11 +2193,6 @@ async fn capture_dependency_job(
}
}
ScriptLang::Go => {
if raw_deps {
return Err(Error::ExecutionErr(
"Raw dependencies not supported for go".to_string(),
));
}
install_go_dependencies(
job_id,
job_raw_code,
@@ -2208,6 +2203,7 @@ async fn capture_dependency_job(
false,
false,
false,
raw_deps,
worker_name,
w_id,
occupancy_metrics,
+12 -3
View File
@@ -40,7 +40,7 @@ export class LockfileGenerationError extends Error {
export async function generateAllMetadata() {}
function findClosestRawReqs(
lang: "bun" | "python3" | "php" | undefined,
lang: "bun" | "python3" | "php" | "go" | undefined,
remotePath: string,
globalDeps: GlobalDeps
): string | undefined {
@@ -72,6 +72,15 @@ function findClosestRawReqs(
bestCandidate = { k, v };
}
});
} else if (lang == "go") {
Object.entries(globalDeps.goMods).forEach(([k, v]) => {
if (
remotePath.startsWith(k) &&
k.length >= (bestCandidate?.k ?? "").length
) {
bestCandidate = { k, v };
}
});
}
// @ts-ignore
return bestCandidate?.v;
@@ -193,14 +202,14 @@ export async function generateScriptMetadataInternal(
const language = inferContentTypeFromFilePath(scriptPath, opts.defaultTs);
const rawReqs = findClosestRawReqs(
language as "bun" | "python3" | "php" | undefined,
language as "bun" | "python3" | "php" | "go" | undefined,
scriptPath,
globalDeps
);
if (rawReqs) {
log.info(
(await blueColor())(
`Found raw requirements (package.json/requirements.txt/composer.json) for ${scriptPath}, using it`
`Found raw requirements (package.json/requirements.txt/composer.json/go.mod) for ${scriptPath}, using it`
)
);
}
+10 -5
View File
@@ -869,11 +869,13 @@ export type GlobalDeps = {
pkgs: Record<string, string>;
reqs: Record<string, string>;
composers: Record<string, string>;
goMods: Record<string, string>;
};
export async function findGlobalDeps(): Promise<GlobalDeps> {
const pkgs: { [key: string]: string } = {};
const reqs: { [key: string]: string } = {};
const composers: { [key: string]: string } = {};
const goMods: { [key: string]: string } = {};
const els = await FSFSElement(Deno.cwd(), [], false);
for await (const entry of readDirRecursiveWithIgnore((p, isDir) => {
p = SEP + p;
@@ -882,21 +884,24 @@ export async function findGlobalDeps(): Promise<GlobalDeps> {
!(
p.endsWith(SEP + "package.json") ||
p.endsWith(SEP + "requirements.txt") ||
p.endsWith(SEP + "composer.json")
p.endsWith(SEP + "composer.json") ||
p.endsWith(SEP + "go.mod")
)
);
}, els)) {
if (entry.isDirectory || entry.ignored) continue;
const content = await entry.getContentText();
if (entry.path.endsWith("package.json")) {
pkgs[entry.path.substring(0, entry.path.length - 12)] = content;
pkgs[entry.path.substring(0, entry.path.length - "package.json".length)] = content;
} else if (entry.path.endsWith("requirements.txt")) {
reqs[entry.path.substring(0, entry.path.length - 16)] = content;
reqs[entry.path.substring(0, entry.path.length - "requirements.txt".length)] = content;
} else if (entry.path.endsWith("composer.json")) {
composers[entry.path.substring(0, entry.path.length - 13)] = content;
composers[entry.path.substring(0, entry.path.length - "composer.json".length)] = content;
} else if (entry.path.endsWith("go.mod")) {
goMods[entry.path.substring(0, entry.path.length - "go.mod".length)] = content;
}
}
return { pkgs, reqs, composers };
return { pkgs, reqs, composers, goMods };
}
async function generateMetadata(
opts: GlobalOptions & {
+20 -2
View File
@@ -22,6 +22,15 @@
"rustfmt"
];
};
patchedClang = pkgs.llvmPackages_18.clang.overrideAttrs (oldAttrs: {
postFixup = ''
# Copy the original postFixup logic but skip add-hardening.sh
${oldAttrs.postFixup or ""}
# Remove the line that substitutes add-hardening.sh
sed -i 's/.*source.*add-hardening\.sh.*//' $out/bin/clang
'';
});
buildInputs = with pkgs; [
openssl
openssl.dev
@@ -64,16 +73,24 @@
in {
# Enter by `nix develop .#wasm`
devShells."wasm" = pkgs.mkShell {
# Explicitly set paths for headers and linker
shellHook = ''
export CC=${patchedClang}/bin/clang
'';
buildInputs = buildInputs ++ (with pkgs; [
(rust-bin.nightly.latest.default.override {
extensions = [
"rust-src" # for rust-analyzer
"rust-analyzer"
];
targets = [ "wasm32-unknown-unknown" ];
targets =
[ "wasm32-unknown-unknown" "wasm32-unknown-emscripten" ];
})
wasm-pack
deno
emscripten
# Needed for extra dependencies
glibc_multi
]);
};
@@ -81,6 +98,7 @@
buildInputs = buildInputs ++ (with pkgs; [
# Essentials
rust
cargo-watch
cargo-sweep
git
xcaddy
@@ -197,7 +215,7 @@
ORACLE_LIB_DIR = "${pkgs.oracle-instantclient.lib}/lib";
ANSIBLE_PLAYBOOK_PATH = "${pkgs.ansible}/bin/ansible-playbook";
ANSIBLE_GALAXY_PATH = "${pkgs.ansible}/bin/ansible-galaxy";
RUST_LOG = "debug";
# RUST_LOG = "debug";
SQLX_OFFLINE = "true";
# See this issue: https://github.com/NixOS/nixpkgs/issues/370494
+26 -27
View File
@@ -24,9 +24,10 @@
"@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": "^0.1.15",
"@xyflow/svelte": "^1.0.0",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.3.4",
@@ -124,7 +125,6 @@
"postcss-load-config": "^4.0.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.3.3",
"simple-svelte-autocomplete": "^2.5.1",
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^13.0.0",
"svelte": "^5.0.0",
@@ -3076,11 +3076,11 @@
"integrity": "sha512-VqAAkydywPpkw63WQhPVKCD3SdwXuihCUVZbbiY3SfSTGQyHmwRoq27y4dmJdZuJwd5JIlQoMPyGvMbUPY0RKQ=="
},
"node_modules/@svelte-put/shortcut": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-3.1.1.tgz",
"integrity": "sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-4.1.0.tgz",
"integrity": "sha512-wImNEIkbxAIWFqlfuhcbC+jRPDeRa/uJGIXHMEVVD+jqL9xCwWNnkGQJ6Qb2XVszuRLHlb8SGZDL3Io/h3vs8w==",
"peerDependencies": {
"svelte": "^3.55.0 || ^4.0.0 || ^5.0.0"
"svelte": "^5.1.0"
}
},
"node_modules/@sveltejs/acorn-typescript": {
@@ -3274,6 +3274,17 @@
"node": ">=10.13.0"
}
},
"node_modules/@tutorlatin/svelte-tiny-virtual-list": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.2.tgz",
"integrity": "sha512-Zts/GUrU6aPgmCrL6sGiD1EoiQxUU+r7Q2g9pckEUIDybNkluV2pEYWUsQLQ+28IJlSSHTopv0fhKs3qMUxqEw==",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -3871,22 +3882,21 @@
"peer": true
},
"node_modules/@xyflow/svelte": {
"version": "0.1.39",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.39.tgz",
"integrity": "sha512-QZ5mzNysvJeJW7DxmqI4Urhhef9tclqtPr7WAS5zQF5Gk6k9INwzey4CYNtEZo8XMj9H8lzgoJRmgMPnJEc1kw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.0.2.tgz",
"integrity": "sha512-biNnzWRmKF/m7t7iY9wt3/KhoTyA8XGbS0Vix3HCL9vrULDF14R1VsMIOxKZib1GJfG+f1/cD2dQ9kplfPLs/Q==",
"dependencies": {
"@svelte-put/shortcut": "3.1.1",
"@xyflow/system": "0.0.59",
"classcat": "^5.0.4"
"@svelte-put/shortcut": "^4.1.0",
"@xyflow/system": "0.0.61"
},
"peerDependencies": {
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
"svelte": "^5.25.0"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.59",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.59.tgz",
"integrity": "sha512-+xgqYhoBv5F10TQx0SiKZR/DcWtuxFYR+e/LluHb7DMtX4SsMDutZWEJ4da4fDco25jZxw5G9fOlmk7MWvYd5Q==",
"version": "0.0.61",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.61.tgz",
"integrity": "sha512-TsZG/Ez8dzxX6/Ol44LvFqVZsYvyz6dpDlAQZZk6hTL7JLGO5vN3dboRJqMwU8/Qtr5IEv5YBzojjAwIqW1HCA==",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-selection": "^3.0.10",
@@ -4710,11 +4720,6 @@
"consola": "^3.2.3"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -11145,12 +11150,6 @@
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-svelte-autocomplete": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/simple-svelte-autocomplete/-/simple-svelte-autocomplete-2.5.2.tgz",
"integrity": "sha512-6+kZP3XQGb7zs0djTWoFv8tQorBZdv64YfCKrlsYIYBCFsEeikSiNsBSSYw1Om+LYmFeMEOAYSSKoEw+qpnBXQ==",
"dev": true
},
"node_modules/sirv": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
+2 -2
View File
@@ -50,7 +50,6 @@
"postcss-load-config": "^4.0.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.3.3",
"simple-svelte-autocomplete": "^2.5.1",
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^13.0.0",
"svelte": "^5.0.0",
@@ -94,8 +93,8 @@
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@xyflow/svelte": "^1.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^0.1.15",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.3.4",
@@ -135,6 +134,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",
+69 -64
View File
@@ -1,85 +1,90 @@
export const action = (node) => {
/* Constants */
const update = new Event('update')
import { tick } from 'svelte'
/* Functions */
const init = () => {
addStyles()
observeElement()
addEventListeners()
setInitialHeight()
}
type TextArea = HTMLTextAreaElement
const dispatchUpdateEvent = () => {
node.dispatchEvent(update)
}
export const autosize = (node: TextArea) => {
/* ------------------------------------------------------------------
* Constants
* ---------------------------------------------------------------- */
const UPDATE_EVENT = new Event('update')
const MIN_HEIGHT = 30 // px
const EXTRA = 2 // px added to scrollHeight
const setInitialHeight = () => {
let height = 0
if (node.value) {
height = Math.max(node.scrollHeight ?? 0, 30) + 2
} else {
if (node.placeholder) {
node.value = node.placeholder
height = Math.max(node.scrollHeight ?? 0, 30) + 2
node.value = ''
} else {
node.value = '|'
node.style.height = '0px'
height = Math.max(node.scrollHeight ?? 0, 30) + 2
node.value = ''
}
}
let width = 0
node.style.height = height + 'px'
}
const setHeight = () => {
/* ------------------------------------------------------------------
* Core resize routine
* ---------------------------------------------------------------- */
const resize = () => {
node.style.height = 'auto'
node.style.height = Math.max(node.scrollHeight ?? 0, 30) + 2 + 'px'
node.style.height = `${Math.max(node.scrollHeight, MIN_HEIGHT) + EXTRA}px`
}
const addStyles = () => {
node.style.boxSizing = 'border-box'
}
/* ------------------------------------------------------------------
* Patch `value` so programmatic changes trigger resize
* ---------------------------------------------------------------- */
const proto = Object.getPrototypeOf(node)
const desc = Object.getOwnPropertyDescriptor(proto, 'value')
const observeElement = () => {
let elementPrototype = Object.getPrototypeOf(node)
let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, 'value')
if (desc) {
Object.defineProperty(node, 'value', {
get: function () {
return descriptor?.get?.apply(this, arguments as any)
get() {
return desc.get?.call(this)
},
set: function () {
descriptor?.set?.apply(this, arguments as any)
dispatchUpdateEvent()
set(v: unknown) {
desc.set?.call(this, v)
node.dispatchEvent(UPDATE_EVENT)
}
})
}
const addEventListeners = () => {
node.addEventListener('input', (e) => {
dispatchUpdateEvent()
})
node.addEventListener('update', setHeight)
}
/* ------------------------------------------------------------------
* Event listeners
* ---------------------------------------------------------------- */
const onInput = () => node.dispatchEvent(UPDATE_EVENT)
const removeEventListeners = () => {
node.removeEventListener('input', dispatchUpdateEvent)
node.removeEventListener('update', setHeight)
}
node.addEventListener('input', onInput)
node.addEventListener('update', resize)
if (node.tagName.toLowerCase() !== 'textarea') {
throw new Error('svelte-textarea-auto-height can only be used on textarea elements.')
} else {
init()
/* ------------------------------------------------------------------
* Inline styling
* ---------------------------------------------------------------- */
node.style.boxSizing = 'border-box'
return {
destroy() {
removeEventListeners()
}
/* ------------------------------------------------------------------
* Wait for DOM mount, then do an initial measure.
* If the <textarea> is already visible (offsetWidth > 0) this covers it;
* otherwise the first ResizeObserver callback will.
* ---------------------------------------------------------------- */
;(async () => {
await tick()
resize()
})()
/* ------------------------------------------------------------------
* ResizeObserver handles:
* first time the element gets a real width
* container/window resizes afterwards
* ---------------------------------------------------------------- */
const ro = new ResizeObserver(([entry]) => {
const newWidth = entry.contentRect.width
if (newWidth !== width) {
width = newWidth
resize()
}
})
ro.observe(node)
/* ------------------------------------------------------------------
* Action lifecycle
* ---------------------------------------------------------------- */
return {
destroy() {
ro.disconnect()
node.removeEventListener('input', onInput)
node.removeEventListener('update', resize)
}
}
}
export default action
export default autosize
+35 -33
View File
@@ -13,8 +13,8 @@
const dispatch = createEventDispatcher()
let email: string
let username: string
let email: string | undefined = $state()
let username: string | undefined = $state()
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
@@ -24,7 +24,7 @@
}
}
let automateUsernameCreation = false
let automateUsernameCreation = $state(false)
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? false
@@ -35,17 +35,17 @@
await WorkspaceService.addUser({
workspace: $workspaceStore!,
requestBody: {
email,
email: email!,
username: automateUsernameCreation ? undefined : username,
is_admin: selected == 'admin',
operator: selected == 'operator'
}
})
sendUserToast(`Added ${email}`)
if (!(await UserService.existsEmail({ email }))) {
if (!(await UserService.existsEmail({ email: email! }))) {
let isSuperadmin = $superadmin
if (!isCloudHosted()) {
const emailCopy = email
const emailCopy = email!
sendUserToast(
`User ${email} is not registered yet on the instance. ${
!isSuperadmin
@@ -62,7 +62,7 @@
goto('#superadmin-settings')
}
}
]
]
: []
)
}
@@ -70,47 +70,49 @@
dispatch('new')
}
let selected: 'operator' | 'developer' | 'admin' = 'developer'
let selected: 'operator' | 'developer' | 'admin' = $state('developer')
</script>
<Popover placement="bottom-end">
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: UserPlus }}>
Add new user
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<div class="flex flex-col w-72 p-4">
<span class="text-sm mb-2 leading-6 font-semibold">Add a new user</span>
<span class="text-xs mb-1 leading-6">Email</span>
<input type="email mb-1" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
<input type="email mb-1" onkeyup={handleKeyUp} placeholder="email" bind:value={email} />
{#if !automateUsernameCreation}
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
<input type="text" on:keyup={handleKeyUp} placeholder="username" bind:value={username} />
<input type="text" onkeyup={handleKeyUp} placeholder="username" bind:value={username} />
{/if}
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
<ToggleButtonGroup bind:selected class="mb-4" let:item>
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
<ToggleButtonGroup bind:selected class="mb-4">
{#snippet children({ item })}
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
{/snippet}
</ToggleButtonGroup>
<Button
variant="contained"
@@ -129,5 +131,5 @@
Add
</Button>
</div>
</svelte:fragment>
{/snippet}
</Popover>
@@ -6,7 +6,11 @@
import type { DurationStatus } from './graph'
import type { Writable } from 'svelte/store'
export let states: Writable<Record<string, DurationStatus>> | undefined
interface Props {
states: Writable<Record<string, DurationStatus>> | undefined
}
let { states }: Props = $props()
</script>
<div class="flex flex-col">
@@ -23,31 +23,43 @@
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 { onDestroy, untrack } from 'svelte'
export let resourceType: string
export let resourceTypeInfo: ResourceType | undefined
export let args: Record<string, any> | any = {}
export let linkedSecret: string | undefined = undefined
export let isValid = true
export let linkedSecretCandidates: string[] | undefined = undefined
export let description: string | undefined = undefined
interface Props {
resourceType: string
resourceTypeInfo: ResourceType | undefined
args?: Record<string, any> | any
linkedSecret?: string | undefined
isValid?: boolean
linkedSecretCandidates?: string[] | undefined
description?: string | undefined
}
let schema = emptySchema()
let notFound = false
let {
resourceType,
resourceTypeInfo,
args = $bindable({}),
linkedSecret = $bindable(undefined),
isValid = $bindable(true),
linkedSecretCandidates = undefined,
description = $bindable(undefined)
}: Props = $props()
let supabaseWizard = false
let schema = $state(emptySchema())
let notFound = $state(false)
let loadingGithubInstallations = false
let githubInstallations: GetGlobalConnectedRepositoriesResponse = []
let workspaceGithubInstallations: GetGlobalConnectedRepositoriesResponse = []
let selectedGHAppAccountId: string | undefined = undefined
let selectedGHAppRepository: string | undefined = undefined
let githubInstallationUrl: string | undefined = undefined
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 = false
let importJwt = ''
let githubAppPopover: { open: () => void; close: () => void } | null = null
let isCheckingInstallation = $state(false)
let importJwt = $state('')
let githubAppPopover: { open: () => void; close: () => void } | null = $state(null)
async function loadGithubInstallations() {
if (!$enterpriseLicense) return
@@ -159,9 +171,6 @@
notFound = true
}
}
$: $workspaceStore && loadSchema()
$: notFound && rawCode && parseJson()
function parseJson() {
try {
@@ -173,13 +182,9 @@
error = e.message
}
}
let error = ''
let rawCode = ''
let viewJsonSchema = false
$: rawCode && parseJson()
$: textFileContent && parseTextFileContent()
let error = $state('')
let rawCode = $state('')
let viewJsonSchema = $state(false)
function switchTab(asJson: boolean) {
viewJsonSchema = asJson
@@ -193,11 +198,8 @@
}
}
$: resourceType == 'postgresql' && isSupabaseAvailable()
$: resourceType == 'git_repository' && $userStore?.is_admin && loadGithubInstallations()
let connectionString = ''
let validConnectionString = true
let connectionString = $state('')
let validConnectionString = $state(true)
function parseConnectionString(close: (_: any) => void) {
const regex =
/postgres(?:ql)?:\/\/(?<user>[^:@]+)(?::(?<password>[^@]+))?@(?<host>[^:\/?]+)(?::(?<port>\d+))?\/(?<dbname>[^\?]+)?(?:\?.*sslmode=(?<sslmode>[^&]+))?/
@@ -241,8 +243,8 @@
close(null)
}
let rawCodeEditor: SimpleEditor | undefined = undefined
let textFileContent: string
let rawCodeEditor: SimpleEditor | undefined = $state(undefined)
let textFileContent: string | undefined = $state(undefined)
function parseTextFileContent() {
args = {
@@ -250,13 +252,6 @@
}
}
$: githubInstallationsNotInWorkspace = githubInstallations.filter((installation) => {
return !workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
})
async function deleteInstallation(installation_id: number) {
if (!$workspaceStore) {
sendUserToast('Failed to delete installation', true)
@@ -329,6 +324,34 @@
}
}
}
$effect(() => {
$workspaceStore && untrack(() => loadSchema())
})
$effect(() => {
notFound && rawCode && untrack(() => parseJson())
})
$effect(() => {
rawCode && untrack(() => parseJson())
})
$effect(() => {
textFileContent && untrack(() => parseTextFileContent())
})
$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}
@@ -346,7 +369,7 @@
placement: 'bottom'
}}
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button
spacingSize="sm"
size="xs"
@@ -357,8 +380,8 @@
>
From connection string
</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<div class="block text-primary p-4">
<div class="w-[550px] flex flex-col items-start gap-1">
<div class="flex flex-row gap-1 w-full">
@@ -384,7 +407,7 @@
{/if}
</div>
</div>
</svelte:fragment>
{/snippet}
</Popover>
{/if}
{#if resourceType == 'postgresql' && supabaseWizard}
@@ -419,7 +442,7 @@
}}
disabled={!$enterpriseLicense || loadingGithubInstallations}
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button
color="none"
variant="border"
@@ -433,8 +456,8 @@
>
{$enterpriseLicense ? 'GitHub App' : 'GitHub App (ee only)'}
</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<div class="block text-primary p-4">
<div class="flex flex-col gap-4 w-[600px]">
{#if workspaceGithubInstallations.length > 0}
@@ -654,7 +677,7 @@
</div>
</div>
</div>
</svelte:fragment>
{/snippet}
</Popover>
{:else}
<Button
@@ -61,29 +61,31 @@
on:refresh
express={expressOAuthSetup}
/>
<div slot="actions" class="flex gap-1">
{#if step > 1}
<Button variant="border" on:click={appConnectInner?.back}>Back</Button>
{/if}
{#if isGoogleSignin}
<button {disabled} on:click={appConnectInner?.next}>
<img
class="h-10 w-auto object-contain"
src={darkMode ? '/google_signin_dark.png' : '/google_signin_light.png'}
alt="Google sign-in"
/>
</button>
{:else}
<Button {disabled} on:click={appConnectInner?.next}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
Next
{:else}
Save
{/if}
</Button>
{/if}
</div>
{#snippet actions()}
<div class="flex gap-1">
{#if step > 1}
<Button variant="border" on:click={appConnectInner?.back ?? (() => {})}>Back</Button>
{/if}
{#if isGoogleSignin}
<button {disabled} on:click={appConnectInner?.next}>
<img
class="h-10 w-auto object-contain"
src={darkMode ? '/google_signin_dark.png' : '/google_signin_light.png'}
alt="Google sign-in"
/>
</button>
{:else}
<Button {disabled} on:click={appConnectInner?.next ?? (() => {})}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
Next
{:else}
Save
{/if}
</Button>
{/if}
</div>
{/snippet}
</DrawerContent>
</Drawer>
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { userStore, workspaceStore } from '$lib/stores'
import IconedResourceType from './IconedResourceType.svelte'
import {
@@ -27,14 +29,25 @@
import { apiTokenApps, forceSecretValue, linkedSecretValue } from './app_connect'
import type { SchemaProperty } from '$lib/common'
export let step = 1
export let resourceType = ''
export let isGoogleSignin = false
export let disabled = false
export let manual = true
export let express = false
interface Props {
step?: number
resourceType?: string
isGoogleSignin?: boolean
disabled?: boolean
manual?: boolean
express?: boolean
}
let isValid = true
let {
step = $bindable(1),
resourceType = $bindable(''),
isGoogleSignin = $bindable(false),
disabled = $bindable(false),
manual = $bindable(true),
express = false
}: Props = $props()
let isValid = $state(true)
const nativeLanguagesCategory = [
'postgresql',
@@ -46,15 +59,15 @@
'oracledb'
]
let filter = ''
let value: string = ''
let filter = $state('')
let value: string = $state('')
let valueToken: TokenResponse | undefined = undefined
let connects: string[] | undefined = undefined
let connects: string[] | undefined = $state(undefined)
let connectsManual:
| [string, { img?: string; instructions: string[]; key?: string }][]
| undefined = undefined
let args: any = {}
let renderDescription = true
| undefined = $state(undefined)
let args: any = $state({})
let renderDescription = $state(true)
function computeCandidates(resourceType: string, argsKeys: string[], passwords: string[]) {
return apiTokenApps[resourceType]?.linkedSecret
@@ -66,8 +79,8 @@
)
}
let linkedSecret: string | undefined = undefined
let linkedSecretCandidates: string[] | undefined = undefined
let linkedSecret: string | undefined = $state(undefined)
let linkedSecretCandidates: string[] | undefined = $state(undefined)
function computeLinkedSecret(resourceType: string, argsKeys: string[], passwords: string[]) {
linkedSecretCandidates = computeCandidates(resourceType, argsKeys, passwords)
return (
@@ -76,14 +89,14 @@
)
}
let scopes: string[] = []
let scopes: string[] = $state([])
let extra_params: [string, string][] = []
let path: string
let description = ''
let path: string = $state('')
let description = $state('')
let resourceTypeInfo: ResourceType | undefined = undefined
let resourceTypeInfo: ResourceType | undefined = $state(undefined)
let pathError = ''
let pathError = $state('')
export async function open(rt?: string) {
if (!rt) {
@@ -112,7 +125,9 @@
async function loadConnects() {
if (!connects) {
try {
connects = (await OauthService.listOauthConnects()).filter((x) => x != 'supabase_wizard')
connects = (await OauthService.listOauthConnects())
.filter((x) => x != 'supabase_wizard')
.sort((a, b) => a.localeCompare(b))
} catch (e) {
connects = []
console.error('Error loading OAuth connects', e)
@@ -122,27 +137,31 @@
const connectAndManual = ['gitlab']
$: isGoogleSignin =
step == 1 &&
(resourceType == 'google' ||
resourceType == 'gmail' ||
resourceType == 'gcal' ||
resourceType == 'gdrive' ||
resourceType == 'gsheets')
run(() => {
isGoogleSignin =
step == 1 &&
(resourceType == 'google' ||
resourceType == 'gmail' ||
resourceType == 'gcal' ||
resourceType == 'gdrive' ||
resourceType == 'gsheets')
})
$: disabled =
(step == 1 && resourceType == '') ||
(step == 2 &&
value == '' &&
args &&
args['token'] == '' &&
args['password'] == '' &&
args['api_key'] == '' &&
args['key'] == '' &&
linkedSecret != undefined) ||
step == 3 ||
(step == 4 && pathError != '') ||
!isValid
run(() => {
disabled =
(step == 1 && resourceType == '') ||
(step == 2 &&
value == '' &&
args &&
args['token'] == '' &&
args['password'] == '' &&
args['api_key'] == '' &&
args['key'] == '' &&
linkedSecret != undefined) ||
step == 3 ||
(step == 4 && pathError != '') ||
!isValid
})
export async function loadResourceTypes() {
if (connectsManual) {
@@ -154,14 +173,18 @@
connectsManual = availableRts
.filter((x) => connectAndManual.includes(x) || !Object.keys(connects ?? {}).includes(x))
.map((x) => [
x,
apiTokenApps[x] ?? {
instructions: '',
img: undefined,
linkedSecret: undefined
}
])
.map(
(x) =>
[
x,
apiTokenApps[x] ?? {
instructions: '',
img: undefined,
linkedSecret: undefined
}
] as [string, { img?: string; instructions: string[]; key?: string }]
)
.sort((a, b) => a[0].localeCompare(b[0]))
const filteredNativeLanguages = filteredConnectsManual?.filter(
(o) => nativeLanguagesCategory?.includes(o[0]) ?? false
)
@@ -177,7 +200,7 @@
}
function popupListener(event) {
console.log('Received oauth popup message')
console.log('Received oauth popup message', event)
let data = event.data
if (event.origin == null || event.origin !== window.location.origin) {
console.log(
@@ -280,6 +303,9 @@
// dispatch('close')
// }
} else {
if (!path) {
throw Error('Path is not set')
}
let exists = await VariableService.existsVariable({
workspace: $workspaceStore!,
path
@@ -370,31 +396,29 @@
}
}
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{ error: string; refresh: string; close: void }>()
let filteredConnects: { key: string }[] = []
let filteredConnects: { key: string }[] = $state([])
let filteredConnectsManual: [string, { img?: string; instructions: string[]; key?: string }][] =
[]
$state([])
let editScopes = false
let editScopes = $state(false)
</script>
{#if !express}
<SearchItems
{filter}
items={connects
? connects
.sort((a, b) => a.localeCompare(b))
.map((key) => ({
key
}))
? connects.map((key) => ({
key
}))
: undefined}
bind:filteredItems={filteredConnects}
f={(x) => x.key}
/>
<SearchItems
{filter}
items={connectsManual?.sort((a, b) => a[0].localeCompare(b[0]))}
items={connectsManual}
bind:filteredItems={filteredConnectsManual}
f={(x) => x[0]}
/>
@@ -590,7 +614,7 @@
</div>
<h3 class="mb-4 flex gap-4"
>Scopes <button
on:click={() => {
onclick={() => {
editScopes = !editScopes
}}><Pen size={14} /></button
></h3
@@ -5,17 +5,21 @@
import { workspaceStore } from '$lib/stores'
import { onMount } from 'svelte'
export let resourceType: string | undefined = undefined
export let workspace: string
export let express = false
interface Props {
resourceType?: string | undefined
workspace: string
express?: boolean
}
let step = 1
let disabled = false
let manual = true
let { resourceType = $bindable(undefined), workspace, express = false }: Props = $props()
let appConnect: AppConnectInner | undefined = undefined
let step = $state(1)
let disabled = $state(false)
let manual = $state(true)
let darkMode: boolean = false
let appConnect: AppConnectInner | undefined = $state(undefined)
let darkMode: boolean = $state(false)
if (workspace) {
$workspaceStore = workspace
+50 -43
View File
@@ -1,60 +1,67 @@
<script lang="ts">
import type { EnumType } from '$lib/common'
import AutoComplete from 'simple-svelte-autocomplete'
import { createEventDispatcher } from 'svelte'
import { twMerge } from 'tailwind-merge'
import Select from './Select.svelte'
export let disabled: boolean
export let value: any
export let enum_: EnumType
export let autofocus: boolean | null
export let defaultValue: string | undefined
export let valid: boolean
export let create: boolean
export let required: boolean
export let enumLabels: Record<string, string> | undefined = undefined
interface Props {
disabled: boolean
value: string | undefined
enum_: EnumType
autofocus: boolean | null
defaultValue: string | undefined
valid: boolean
create: boolean
enumLabels?: Record<string, string> | undefined
}
let {
disabled,
value = $bindable(),
enum_,
autofocus,
defaultValue,
valid,
create,
enumLabels = undefined
}: Props = $props()
const dispatch = createEventDispatcher()
let customItems: string[] = []
let customItems: string[] = $state([])
function onCreate(newItem: string) {
console.log(newItem)
customItems = [...customItems, newItem]
let items = $derived.by(() => {
const l = [...(enum_ ? enum_ : []), ...customItems].map((item) => ({
value: item,
label: enumLabels?.[item] ?? item
}))
if (create && filterText && l.every((i) => i.value !== filterText)) {
l.push({ value: filterText, label: `Add new: ${filterText}` })
}
return l
})
return newItem
}
let filterText = $state('')
</script>
<div class="w-full flex-col">
<div class="w-full">
<AutoComplete
items={[...(required ? [] : ['']), ...(enum_ ? enum_ : []), ...customItems]}
labelFunction={(val) => (enumLabels ? enumLabels[val] ?? val : val)}
bind:selectedItem={value}
inputClassName={twMerge(
'bg-surface-secondary flex',
valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'
)}
value={value ?? defaultValue}
hideArrow={true}
dropdownClassName="!text-sm !py-2 !rounded-sm !border-gray-200 !border !shadow-md !bg-surface-primary"
className="w-full"
noInputStyles
onFocus={() => {
dispatch('focus')
}}
onBlur={() => {
dispatch('blur')
}}
{create}
{required}
{onCreate}
<Select
inputClass={valid ? '' : '!border-red-500/60'}
clearable
{disabled}
{autofocus}
createText="Press enter to use this non-predefined value"
autofocus={autofocus ?? undefined}
bind:filterText
{items}
bind:value={
() => value ?? defaultValue,
(newValue) => {
if (newValue && items.findIndex((i) => i.value === newValue) === -1)
customItems.push(newValue)
value = newValue
}
}
onFocus={() => dispatch('focus')}
onBlur={() => dispatch('blur')}
/>
</div>
</div>
+17 -13
View File
@@ -8,9 +8,13 @@
import Tooltip from './Tooltip.svelte'
import { Button, DrawerContent } from './common'
export let value: any
let jsonViewer: Drawer
let jsonViewerContent: any | undefined
interface Props {
value: any
}
let { value }: Props = $props()
let jsonViewer: Drawer | undefined = $state()
let jsonViewerContent: any | undefined = $state()
function isString(value: any) {
return typeof value === 'string' || value instanceof String
@@ -33,7 +37,7 @@
<Drawer bind:this={jsonViewer} size="800px">
<DrawerContent title="Argument Details" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
on:click={() => copyToClipboard(JSON.stringify(jsonViewerContent, null, 4))}
color="light"
@@ -42,7 +46,7 @@
>
Copy
</Button>
</svelte:fragment>
{/snippet}
{#if isString(jsonViewerContent)}
<pre>{jsonViewerContent}</pre>
{:else}
@@ -61,17 +65,17 @@
{:else if isString(value) && value.startsWith('$res:')}
<button
class="text-xs text-blue-500"
on:click={async () => {
onclick={async () => {
await getResource(value.substring('$res:'.length))
jsonViewer.toggleDrawer()
jsonViewer?.toggleDrawer()
}}>{value}</button
>
{:else if isString(value) && value.startsWith('$var:')}
<button
class="text-xs text-blue-500"
on:click={async () => {
onclick={async () => {
await getVariable(value.substring('$res:'.length))
jsonViewer.toggleDrawer()
jsonViewer?.toggleDrawer()
}}>{value}</button
>
{:else if typeof value !== 'object'}
@@ -80,9 +84,9 @@
{#if JSON.stringify(value).length > 80}
<button
class="text-xs text-blue-500"
on:click={() => {
onclick={() => {
jsonViewerContent = value
jsonViewer.toggleDrawer()
jsonViewer?.toggleDrawer()
}}>See expanded</button
>
{/if}
@@ -92,9 +96,9 @@
{#if JSON.stringify(value).length > 120}
<button
class="text-xs absolute top-0 right-4 text-tertiary"
on:click={() => {
onclick={() => {
jsonViewerContent = value
jsonViewer.toggleDrawer()
jsonViewer?.toggleDrawer()
}}><Expand size={18} /></button
>
{/if}
+299 -194
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { preventDefault, stopPropagation, createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import type { EnumType, SchemaProperty } from '$lib/common'
import {
setInputCat as computeInputCat,
@@ -7,7 +10,7 @@
getSchemaFromProperties
} from '$lib/utils'
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
import { createEventDispatcher, onMount, tick } from 'svelte'
import { createEventDispatcher, onMount, tick, untrack } from 'svelte'
import Multiselect from 'svelte-multiselect'
import { fade } from 'svelte/transition'
import { Button, SecondsInput } from './common'
@@ -36,99 +39,166 @@
import { deepEqual } from 'fast-equals'
import DynSelect from './DynSelect.svelte'
import type { Script } from '$lib/gen'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { ComponentCustomCSS } from './apps/types'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let label: string = ''
export let value: any
export let defaultValue: any = undefined
interface Props {
label?: string
value: any
defaultValue?: any
description?: string
format?: string
contentEncoding?: 'base64' | 'binary' | undefined
type?: string | undefined
oneOf?: SchemaProperty[] | undefined
required?: boolean
pattern?: undefined | string
valid?: boolean
enum_?: EnumType
disabled?: boolean
itemsType?:
| {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
multiselect?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
}
| undefined
displayHeader?: boolean
properties?: { [name: string]: SchemaProperty } | undefined
nestedRequired?: string[] | undefined
autofocus?: boolean | null
compact?: boolean
password?: boolean
pickForField?: string | undefined
variableEditor?: VariableEditor | undefined
itemPicker?: ItemPicker | undefined
noMargin?: boolean
extra?: Record<string, any>
minW?: boolean
prettifyHeader?: boolean
resourceTypes: string[] | undefined
disablePortal?: boolean
showSchemaExplorer?: boolean
simpleTooltip?: string | undefined
customErrorMessage?: string | undefined
onlyMaskPassword?: boolean
nullable?: boolean
title?: string | undefined
placeholder?: string | undefined
order?: string[] | undefined
editor?: SimpleEditor | undefined
orderEditable?: boolean
shouldDispatchChanges?: boolean
noDefaultOnSelectFirst?: boolean
helperScript?:
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
| { type: 'hash'; hash: string }
| undefined
otherArgs?: Record<string, any>
lightHeader?: boolean
diffStatus?: SchemaDiff | undefined
hideNested?: boolean
nestedParent?: { label: string; nestedParent: any | undefined } | undefined
nestedClasses?: string
displayType?: boolean
css?: ComponentCustomCSS<'schemaformcomponent'> | undefined
appPath?: string | undefined
computeS3ForceViewerPolicies?:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined
workspace?: string | undefined
actions?: import('svelte').Snippet
}
export let description: string = ''
export let format: string = ''
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
export let type: string | undefined = undefined
export let oneOf: SchemaProperty[] | undefined = undefined
export let required = false
export let pattern: undefined | string = undefined
export let valid = true
export let enum_: EnumType = undefined
export let disabled = false
export let itemsType:
| {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
multiselect?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
}
| undefined = undefined
let {
label = '',
value = $bindable(),
defaultValue = $bindable(undefined),
description = $bindable(undefined),
format = $bindable(undefined),
contentEncoding = undefined,
type = undefined,
oneOf = $bindable(undefined),
required = false,
pattern = $bindable(undefined),
valid = $bindable(undefined),
enum_ = $bindable(undefined),
disabled = false,
itemsType = $bindable(undefined),
displayHeader = true,
properties = $bindable(undefined),
nestedRequired = undefined,
autofocus = null,
compact = false,
password = false,
pickForField = $bindable(undefined),
variableEditor = undefined,
itemPicker = undefined,
noMargin = false,
extra = {},
minW = true,
prettifyHeader = false,
resourceTypes,
disablePortal = false,
showSchemaExplorer = false,
simpleTooltip = undefined,
customErrorMessage = undefined,
onlyMaskPassword = false,
nullable = false,
title = $bindable(undefined),
placeholder = $bindable(undefined),
order = $bindable(undefined),
editor = $bindable(undefined),
orderEditable = false,
shouldDispatchChanges = false,
noDefaultOnSelectFirst = false,
helperScript = undefined,
otherArgs = {},
lightHeader = false,
diffStatus = undefined,
hideNested = false,
nestedParent = undefined,
nestedClasses = '',
displayType = true,
css = undefined,
appPath = undefined,
computeS3ForceViewerPolicies = undefined,
workspace = undefined,
actions
}: Props = $props()
export let displayHeader = true
export let properties: { [name: string]: SchemaProperty } | undefined = undefined
export let nestedRequired: string[] | undefined = undefined
export let autofocus: boolean | null = null
export let compact = false
export let password = false
export let pickForField: string | undefined = undefined
export let variableEditor: VariableEditor | undefined = undefined
export let itemPicker: ItemPicker | undefined = undefined
export let noMargin = false
export let extra: Record<string, any> = {}
export let minW = true
export let prettifyHeader = false
export let resourceTypes: string[] | undefined
export let disablePortal = false
export let showSchemaExplorer = false
export let simpleTooltip: string | undefined = undefined
export let customErrorMessage: string | undefined = undefined
export let onlyMaskPassword = false
export let nullable: boolean = false
export let title: string | undefined = undefined
export let placeholder: string | undefined = undefined
export let order: string[] | undefined = undefined
export let editor: SimpleEditor | undefined = undefined
export let orderEditable = false
export let shouldDispatchChanges: boolean = false
export let noDefaultOnSelectFirst: boolean = false
export let helperScript:
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
| { type: 'hash'; hash: string }
| undefined = undefined
export let otherArgs: Record<string, any> = {}
export let lightHeader = false
export let diffStatus: SchemaDiff | undefined = undefined
export let hideNested = false
export let nestedParent: { label: string; nestedParent: any | undefined } | undefined = undefined
export let nestedClasses = ''
export let displayType = true
export let css: ComponentCustomCSS<'schemaformcomponent'> | undefined = undefined
export let appPath: string | undefined = undefined
export let computeS3ForceViewerPolicies:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined = undefined
export let workspace: string | undefined = undefined
$effect(() => {
if (description == undefined) {
description = ''
}
if (format == undefined) {
format = ''
}
if (valid == undefined) {
valid = true
}
})
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
let oneOfSelected: string | undefined = undefined
let oneOfSelected: string | undefined = $state(undefined)
let tagKey = $derived(
oneOf?.find((o) => Object.keys(o.properties ?? {})?.includes('kind')) ? 'kind' : 'label'
)
async function updateOneOfSelected(oneOf: SchemaProperty[] | undefined) {
if (
oneOf &&
oneOf.length >= 2 &&
(!oneOfSelected || !oneOf.some((o) => o.title === oneOfSelected) || !value)
) {
const tagKey = oneOf.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
? 'kind'
: 'label'
if (value && value[tagKey] && oneOf.some((o) => o.title === value[tagKey])) {
const existingValue = JSON.parse(JSON.stringify(value))
oneOfSelected = value[tagKey]
@@ -142,10 +212,6 @@
}
}
$: updateOneOfSelected(oneOf)
$: oneOf && value && onOneOfChange()
function onOneOfChange() {
const label = value?.['label']
const kind = value?.['kind']
@@ -157,54 +223,50 @@
}
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
let ignoreValueUndefined = false
let error: string = ''
let s3FilePicker: S3FilePicker
let s3FileUploadRawMode: false
let isListJson = false
let hasIsListJsonChanged = false
let ignoreValueUndefined = $state(false)
let error: string = $state('')
let s3FilePicker: S3FilePicker | undefined = $state()
let s3FileUploadRawMode: boolean = $state(false)
let isListJson = $state(false)
let hasIsListJsonChanged = $state(false)
let el: HTMLTextAreaElement | undefined = undefined
let el: HTMLTextAreaElement | undefined = $state(undefined)
let rawValue: string | undefined = undefined
let rawValue: string | undefined = $state(undefined)
function computeDefaultValue(
nvalue?: any,
inputCat?: string,
defaultValue?: any,
nnullable?: boolean
) {
let lastValue: any = null
function computeDefaultValue(inputCat?: string, defaultValue?: any, nnullable?: boolean) {
let nvalue: any = null
if (label == 'toString' && typeof value == 'function') {
value = undefined
nvalue = undefined
}
if ((value == undefined || value == null) && !ignoreValueUndefined) {
value = structuredClone(defaultValue)
nvalue = structuredClone($state.snapshot(defaultValue))
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
value = nullable ? null : ''
nvalue = nullable ? null : ''
} else if (inputCat == 'enum' && required) {
value = enum_?.[0]
nvalue = enum_?.[0]
} else if (inputCat == 'boolean') {
value = false
nvalue = false
} else if (inputCat == 'list') {
value = []
nvalue = []
}
} else if (inputCat === 'object') {
evalValueToRaw()
}
}
if (nnullable && type === 'string' && value === '') {
if (nnullable && type === 'string' && value === '' && nvalue == null) {
value = null
} else if (nvalue != null && !deepEqual(nvalue, value)) {
value = nvalue
lastValue = value
}
}
$: computeDefaultValue(value, inputCat, defaultValue, nullable)
let lastValue: any = undefined
// By setting isListJson to true, we can render inputs even if the value is not an array of the correct type
// This avoids the issue of the input being rendered as a string with value: [object Object], or as a number with value: NaN
function checkArrayValueType() {
@@ -233,16 +295,7 @@
lastValue = value
}
$: !isListJson &&
inputCat === 'list' &&
value != lastValue &&
itemsType?.type &&
!hasIsListJsonChanged &&
checkArrayValueType()
$: defaultValue != undefined && handleDefaultValueChange()
let oldDefaultValue = structuredClone(defaultValue)
let oldDefaultValue = structuredClone($state.snapshot(defaultValue))
function handleDefaultValueChange() {
if (
deepEqual(value, oldDefaultValue) &&
@@ -251,7 +304,7 @@
) {
value = defaultValue
}
oldDefaultValue = structuredClone(defaultValue)
oldDefaultValue = structuredClone($state.snapshot(defaultValue))
}
function isObjectCat(inputCat?: string) {
@@ -286,11 +339,6 @@
}
let setCodeDisabled = false
$: (inputCat &&
(isObjectCat(inputCat) || isRawStringEditor(inputCat)) &&
!oneOf &&
evalValueToRaw()) ||
value
let timeout: NodeJS.Timeout | undefined = undefined
function setNewValueFromCode(nvalue: any) {
@@ -305,7 +353,6 @@
}
onMount(() => {
computeDefaultValue()
evalValueToRaw()
})
@@ -389,22 +436,62 @@
e.stopPropagation()
}
let redraw = 0
let itemsLimit = 50
$: validateInput(pattern, value, required)
let redraw = $state(0)
let itemsLimit = $state(50)
let oldValue = value
function compareValues(value) {
if (!deepEqual(oldValue, value)) {
oldValue = value
dispatchIfMounted('change')
dispatch('change')
}
}
let debounced = debounce(() => compareValues(value), 50)
$: shouldDispatchChanges && debounced(value)
let inputCat = $derived(computeInputCat(type, format, itemsType?.type, enum_, contentEncoding))
$effect(() => {
oneOf && untrack(() => updateOneOfSelected(oneOf))
})
$effect(() => {
oneOf && value && untrack(() => onOneOfChange())
})
$effect.pre(() => {
value
let args = [inputCat, defaultValue, nullable]
untrack(() => {
if (deepEqual(value, lastValue)) return
computeDefaultValue(...args)
})
})
$effect.pre(() => {
!isListJson &&
inputCat === 'list' &&
value != lastValue &&
itemsType?.type &&
!hasIsListJsonChanged &&
untrack(() => checkArrayValueType())
})
$effect.pre(() => {
defaultValue != undefined && untrack(() => handleDefaultValueChange())
})
$effect.pre(() => {
;(inputCat &&
(isObjectCat(inputCat) || isRawStringEditor(inputCat)) &&
!oneOf &&
untrack(() => evalValueToRaw())) ||
value
})
$effect(() => {
let args = [pattern, value, required] as const
untrack(() => validateInput(...args))
})
$effect(() => {
shouldDispatchChanges && debounced(value)
})
</script>
<S3FilePicker
@@ -417,7 +504,7 @@
readOnlyMode={false}
/>
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<div
class={twMerge(
'flex flex-col w-full rounded-md relative',
@@ -441,17 +528,21 @@
>
<button
class="p-1 bg-green-500 text-white hover:bg-green-600"
on:click|preventDefault|stopPropagation={() => {
dispatch('acceptChange', { label, nestedParent })
}}
onclick={stopPropagation(
preventDefault(() => {
dispatch('acceptChange', { label, nestedParent })
})
)}
>
<Check size={14} />
</button>
<button
class="p-1 hover:bg-red-500 hover:text-white"
on:click|preventDefault|stopPropagation={() => {
dispatch('rejectChange', { label, nestedParent })
}}
onclick={stopPropagation(
preventDefault(() => {
dispatch('rejectChange', { label, nestedParent })
})
)}
>
<X size={14} />
</button>
@@ -502,11 +593,11 @@
<div class="relative w-full">
<input
{autofocus}
on:focus
on:blur
onfocus={bubble('focus')}
onblur={bubble('blur')}
{disabled}
type="number"
on:keydown={() => {
onkeydown={() => {
ignoreValueUndefined = true
}}
class={valid
@@ -545,6 +636,9 @@
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
{disabled}
bind:selected={value}
onremove={(e) => {
if (Array.isArray(value)) value = value.filter((v) => v !== e.option)
}}
options={itemsType?.multiselect ?? []}
selectedOptionsDraggable={true}
onopen={() => {
@@ -552,14 +646,17 @@
}}
/>
</div>
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && Array.isArray(value)}
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && (Array.isArray(value) || value == undefined)}
<div class="items-start">
<Multiselect
ulOptionsClass={'p-2 !bg-surface-secondary'}
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
{disabled}
onremove={(e) => {
if (Array.isArray(value)) value = value.filter((v) => v !== e.option)
}}
bind:selected={
() => [...value],
() => [...(value ?? [])],
(v) => {
if (!deepEqual(v, value)) {
value = v
@@ -610,7 +707,7 @@
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value[i] = val))}
onchange={(x) => fileChanged(x, (val) => (value[i] = val))}
multiple={false}
/>
{:else if itemsType?.type == 'object' && itemsType?.resourceType === undefined && itemsType?.properties === undefined}
@@ -624,7 +721,6 @@
{/await}
{:else if Array.isArray(itemsType?.enum)}
<ArgEnum
required
create={extra['disableCreate'] != true}
on:focus={() => {
dispatch('focus')
@@ -633,7 +729,7 @@
dispatch('blur')
}}
{defaultValue}
{valid}
valid={valid ?? true}
{disabled}
{autofocus}
bind:value={value[i]}
@@ -680,7 +776,7 @@
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover ml-2"
aria-label="Clear"
on:click={() => {
onclick={() => {
value = value.filter((_, index) => index !== i)
redraw += 1
}}
@@ -691,7 +787,7 @@
{/if}
{/each}
{#if value.length > itemsLimit}
<button on:click={() => (itemsLimit += 50)} class="text-xs py-2 text-blue-600"
<button onclick={() => (itemsLimit += 50)} class="text-xs py-2 text-blue-600"
>{itemsLimit}/{value.length}: Load 50 more...</button
>
{/if}
@@ -754,16 +850,16 @@
args={otherArgs}
{helperScript}
bind:value
entrypoint={format.substring('dynselect_'.length)}
entrypoint={format?.substring('dynselect_'.length) ?? ''}
/>
{:else if inputCat == 'resource-object' && resourceTypes == undefined}
<span class="text-2xs text-tertiary">Loading resource types...</span>
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format.split('-').length > 1 && resourceTypes.includes(format.substring('resource-'.length))))}
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))}
<ObjectResourceInput
{defaultValue}
selectFirst={!noDefaultOnSelectFirst}
{disablePortal}
{format}
format={format ?? ''}
bind:value
bind:editor
on:clear={() => {
@@ -771,7 +867,7 @@
}}
{showSchemaExplorer}
/>
{:else if inputCat == 'resource-object' && format.split('-').length > 1 && format
{:else if inputCat == 'resource-object' && format && format.split('-').length > 1 && format
.replace('resource-', '')
.replace('_', '')
.toLowerCase() == 's3object'}
@@ -855,11 +951,12 @@
: 'label'
value = { ...toKeep, [tagKey]: detail }
}}
let:item
>
{#each oneOf as obj}
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
{/each}
{#snippet children({ item })}
{#each oneOf as obj}
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
{/each}
{/snippet}
</ToggleButtonGroup>
{#if oneOfSelected}
{@const objIdx = oneOf.findIndex((o) => o.title === oneOfSelected)}
@@ -873,13 +970,18 @@
{onlyMaskPassword}
{disablePortal}
{disabled}
schema={{
properties: obj.properties,
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}}
bind:schema={
() => ({
properties: obj.properties ?? {},
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}),
() => {
dispatch('nestedChange')
}
}
bind:args={value}
dndType={`nested-${title}`}
hiddenArgs={['label', 'kind']}
@@ -889,9 +991,6 @@
oneOf[objIdx].order = keys
}
}}
on:change={() => {
dispatch('nestedChange')
}}
on:nestedChange
{shouldDispatchChanges}
/>
@@ -909,7 +1008,12 @@
required: obj.required ?? [],
type: 'object'
}}
bind:args={value}
bind:args={
() => value,
(v) => {
value = { ...v, [tagKey]: oneOfSelected }
}
}
{shouldDispatchChanges}
on:change={() => {
dispatch('nestedChange')
@@ -971,22 +1075,24 @@
{onlyMaskPassword}
{disablePortal}
{disabled}
schema={{
properties,
$schema: '',
required: nestedRequired ?? [],
type: 'object',
order
}}
bind:schema={
() => ({
properties,
$schema: '',
required: nestedRequired ?? [],
type: 'object',
order
}),
(newSchema) => {
dispatch('nestedChange')
}
}
bind:args={value}
dndType={`nested-${title}`}
on:reorder={(e) => {
const keys = e.detail
order = keys
}}
on:change={() => {
dispatch('nestedChange')
}}
diff={diffStatus && typeof diffStatus.diff === 'object' ? diffStatus.diff : {}}
on:acceptChange={(e) => {
dispatch('acceptChange', e.detail)
@@ -1065,10 +1171,9 @@
{:else if inputCat == 'enum'}
<div class="flex flex-row w-full gap-1">
<ArgEnum
{required}
create={extra['disableCreate'] != true}
{defaultValue}
{valid}
valid={valid ?? true}
{disabled}
bind:value
{enum_}
@@ -1119,7 +1224,7 @@
<input
{autofocus}
type="file"
on:change={(x) => fileChanged(x, (val) => (value = val))}
onchange={(x) => fileChanged(x, (val) => (value = val))}
multiple={false}
/>
{#if value?.length}
@@ -1144,8 +1249,8 @@
{:else if inputCat == 'email'}
<input
{autofocus}
on:focus
on:blur
onfocus={bubble('focus')}
onblur={bubble('blur')}
{disabled}
type="email"
class={valid
@@ -1177,14 +1282,14 @@
{autofocus}
rows={extra?.['minRows'] ? extra['minRows']?.toString() : '1'}
bind:this={el}
on:focus={(e) => {
onfocus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
onblur={(e) => {
dispatch('blur')
}}
use:autosize
on:keydown={onKeyDown}
onkeydown={onKeyDown}
{disabled}
class={twMerge(
'w-full',
@@ -1197,10 +1302,10 @@
></textarea>
{/key}
{#if !disabled && itemPicker && extra?.['disableVariablePicker'] != true}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<button
class="absolute right-1 top-1 py-1 min-w-min !px-2 items-center text-gray-800 bg-surface-secondary border rounded center-center hover:bg-gray-300 transition-all cursor-pointer"
on:click={() => {
onclick={() => {
pickForField = label
itemPicker?.openDrawer?.()
}}
@@ -1216,7 +1321,7 @@
{#if value && typeof value == 'string' && value?.startsWith('$var:')}
Linked to variable <button
class="text-blue-500 underline"
on:click={() => variableEditor?.editVariable?.(value.slice(5))}
onclick={() => variableEditor?.editVariable?.(value.slice(5))}
>{value.slice(5)}</button
>
{/if}
@@ -1224,7 +1329,7 @@
{/if}
</div>
{/if}
<slot name="actions" />
{@render actions?.()}
</div>
{#if !compact || (error && error != '')}
@@ -9,17 +9,25 @@
import EditableSchemaDrawer from './schema/EditableSchemaDrawer.svelte'
import type { SchemaProperty } from '$lib/common'
export let canEditResourceType: boolean = false
export let originalType: string | undefined = undefined
export let itemsType:
| {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
}
| undefined
interface Props {
canEditResourceType?: boolean
originalType?: string | undefined
itemsType:
| {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
}
| undefined
}
let {
canEditResourceType = false,
originalType = undefined,
itemsType = $bindable()
}: Props = $props()
let selected:
| 'string'
@@ -29,7 +37,7 @@
| 'enum'
| 'resource'
| 's3object'
| undefined =
| undefined = $state(
itemsType?.type != 'string'
? itemsType?.type == 'object' && itemsType?.resourceType == 's3object'
? 's3object'
@@ -39,12 +47,13 @@
: itemsType?.contentEncoding == 'base64'
? 'bytes'
: 'string'
)
let schema = {
let schema = $state({
properties: itemsType?.properties || {},
order: Object.keys(itemsType?.properties || {}),
required: Object.values(itemsType?.properties || {}).map((p) => p.required)
}
})
function updateItemsType() {
itemsType = {
@@ -59,7 +68,7 @@
<Label label="Items type">
<select
bind:value={selected}
on:change={() => {
onchange={() => {
if (selected == 'enum') {
itemsType = { type: 'string', enum: [] }
} else if (selected == 'string') {
@@ -127,7 +136,7 @@
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover ml-2"
on:click={() => {
onclick={() => {
if (itemsType && itemsType.enum) {
const enumValue = itemsType.enum[index]
itemsType.enum = itemsType.enum.filter((el) => el !== enumValue)
@@ -6,10 +6,19 @@
import Tooltip from './Tooltip.svelte'
import AssignableTagsInner from './AssignableTagsInner.svelte'
export let placement: 'bottom-end' | 'top-end' = 'bottom-end'
export let color: 'nord' | 'dark' = 'dark'
export let disabled = false
export let showWorkspaceRestriction = false
interface Props {
placement?: 'bottom-end' | 'top-end'
color?: 'nord' | 'dark'
disabled?: boolean
showWorkspaceRestriction?: boolean
}
let {
placement = 'bottom-end',
color = 'dark',
disabled = false,
showWorkspaceRestriction = false
}: Props = $props()
</script>
<Popover
@@ -18,7 +27,7 @@
closeButton
usePointerDownOutside
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button {color} size="xs" nonCaptureEvent={true} {disabled}>
<div class="flex flex-row gap-1 items-center"
><Pen size={14} /> Custom tags&nbsp;<Tooltip light
@@ -29,8 +38,8 @@
></div
>
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<AssignableTagsInner {showWorkspaceRestriction} on:refresh />
</svelte:fragment>
{/snippet}
</Popover>
@@ -1,4 +1,6 @@
<script lang="ts">
import { preventDefault, stopPropagation } from 'svelte/legacy'
import { Button } from './common'
import { ExternalLink, Loader2, X } from 'lucide-svelte'
import { SettingService, WorkerService } from '$lib/gen'
@@ -9,9 +11,13 @@
import { base } from '$lib/base'
import { createEventDispatcher } from 'svelte'
export let showWorkspaceRestriction = false
let newTag: string = ''
let customTags: string[] | undefined = undefined
interface Props {
showWorkspaceRestriction?: boolean
}
let { showWorkspaceRestriction = false }: Props = $props()
let newTag: string = $state('')
let customTags: string[] | undefined = $state(undefined)
async function loadCustomTags() {
try {
@@ -40,15 +46,17 @@
<button
class="z-10 rounded-full p-1 duration-200 hover:bg-gray-200"
aria-label="Remove item"
on:click|preventDefault|stopPropagation={async () => {
await SettingService.setGlobal({
key: CUSTOM_TAGS_SETTING,
requestBody: { value: customTags?.filter((x) => x != customTag) }
onclick={stopPropagation(
preventDefault(async () => {
await SettingService.setGlobal({
key: CUSTOM_TAGS_SETTING,
requestBody: { value: customTags?.filter((x) => x != customTag) }
})
dispatch('refresh')
loadCustomTags()
sendUserToast('Tag removed')
})
dispatch('refresh')
loadCustomTags()
sendUserToast('Tag removed')
}}
)}
>
<X size={12} />
</button><NoWorkerWithTagWarning tag={customTag} />
@@ -74,7 +82,9 @@
}}
disabled={newTag.trim() == '' || !$superadmin}
>
Add {#if !$superadmin} <span class="text-2xs text-tertiary">superadmin only</span> {/if}
Add {#if !$superadmin}
<span class="text-2xs text-tertiary">superadmin only</span>
{/if}
</Button>
<span class="text-sm text-primary"
>Configure <a href="{base}/workers" target="_blank" class="inline-flex gap-1 items-baseline"
@@ -6,11 +6,11 @@
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
export let value: any
interface Props {
value: any
}
$: enabled = value != undefined
$: changeDomain(value?.['domain'], value?.['custom'])
let { value = $bindable() }: Props = $props()
function changeDomain(domain, custom) {
if (value) {
@@ -31,10 +31,14 @@
}
}
}
let enabled = $derived(value != undefined)
$effect(() => {
changeDomain(value?.['domain'], value?.['custom'])
})
</script>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-sm font-medium text-primary flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name="auth0" after={true} /></div><Toggle
checked={enabled}
@@ -57,10 +61,11 @@
on:selected={({ detail }) => {
value['custom'] = detail === 'custom'
}}
let:item
>
<ToggleButton value="org" label={'Org'} {item} />
<ToggleButton value="custom" label="Custom" {item} />
{#snippet children({ item })}
<ToggleButton value="org" label={'Org'} {item} />
<ToggleButton value="custom" label="Custom" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
<div class="grow">
+214 -178
View File
@@ -18,9 +18,31 @@
import { ExternalLink, Plus } from 'lucide-svelte'
import AzureOauthSettings from './AzureOauthSettings.svelte'
export let snowflakeAccountIdentifier = ''
export let oauths: Record<string, any> = {}
export let requirePreexistingUserForOauth: boolean = false
interface Props {
snowflakeAccountIdentifier?: string
oauths?: Record<string, any>
requirePreexistingUserForOauth?: boolean
scim?: import('svelte').Snippet
}
let {
snowflakeAccountIdentifier = $bindable(),
oauths = $bindable(),
requirePreexistingUserForOauth = $bindable(),
scim
}: Props = $props()
$effect(() => {
if (snowflakeAccountIdentifier == undefined) {
snowflakeAccountIdentifier = ''
}
if (oauths == undefined) {
oauths = {}
}
if (requirePreexistingUserForOauth == undefined) {
requirePreexistingUserForOauth = false
}
})
const windmillBuiltins = [
'azure_oauth',
@@ -45,12 +67,12 @@
'xero'
]
let oauth_name = undefined
let oauth_name = $state(undefined)
let clientName = ''
let resourceName = ''
let clientName = $state('')
let resourceName = $state('')
let tab: 'sso' | 'oauth' | 'scim' = 'sso'
let tab: 'sso' | 'oauth' | 'scim' = $state('sso')
</script>
<div>
@@ -62,60 +84,147 @@
</div>
<div class="mb-6">
{#if tab === 'sso'}
{#if !$enterpriseLicense || $enterpriseLicense.endsWith('_pro')}
<Alert type="warning" title="Limited to 10 SSO users">
Without EE, the number of SSO users is limited to 10. SCIM/SAML is available on EE
</Alert>
{/if}
{#if oauths}
{#if tab === 'sso'}
{#if !$enterpriseLicense || $enterpriseLicense.endsWith('_pro')}
<Alert type="warning" title="Limited to 10 SSO users">
Without EE, the number of SSO users is limited to 10. SCIM/SAML is available on EE
</Alert>
{/if}
<div class="py-1"></div>
<div class="mb-2">
<span class="text-primary text-sm"
>When at least one of the below options is set, users will be able to login to Windmill
via their third-party account.
<br /> To test SSO, the recommended workflow is to to save the settings and try to login
in an incognito window.
<a target="_blank" href="https://www.windmill.dev/docs/misc/setup_oauth#sso">Learn more</a
></span
>
</div>
<div class="flex flex-col gap-3 py-4">
<OAuthSetting name="google" bind:value={oauths['google']} />
<OAuthSetting name="microsoft" bind:value={oauths['microsoft']} />
<OktaSetting bind:value={oauths['okta']} />
<Auth0Setting bind:value={oauths['auth0']} />
<OAuthSetting name="github" bind:value={oauths['github']} />
<OAuthSetting name="gitlab" bind:value={oauths['gitlab']} />
<OAuthSetting name="jumpcloud" bind:value={oauths['jumpcloud']} />
<KeycloakSetting bind:value={oauths['keycloak']} />
<AuthentikSetting bind:value={oauths['authentik']} />
<AutheliaSetting bind:value={oauths['authelia']} />
<KanidmSetting bind:value={oauths['kanidm']} />
<ZitadelSetting bind:value={oauths['zitadel']} />
{#each Object.keys(oauths) as k}
{#if !['authelia', 'authentik', 'google', 'microsoft', 'github', 'gitlab', 'jumpcloud', 'okta', 'auth0', 'keycloak', 'slack', 'kanidm', 'zitadel'].includes(k) && 'login_config' in oauths[k]}
{#if oauths[k]}
<div class="flex flex-col gap-2 pb-4">
<div class="flex flex-row items-center gap-2">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-md font-medium text-primary">{k}</label>
<CloseButton
on:close={() => {
if (oauths) {
delete oauths[k]
oauths = { ...oauths }
}
}}
/>
</div>
<div class="p-2 border rounded">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input
type="text"
placeholder="Custom Name"
bind:value={oauths[k]['display_name']}
/>
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={oauths[k]['id']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Secret</span>
<input
type="text"
placeholder="Client Secret"
bind:value={oauths[k]['secret']}
/>
</label>
{#if !windmillBuiltins.includes(k) && k != 'slack'}
<CustomSso bind:login_config={oauths[k]['login_config']} />
{/if}
</div>
</div>
{/if}
{/if}
{/each}
</div>
<div class="flex gap-2 py-4">
<input type="text" placeholder="client_id" bind:value={clientName} />
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: Plus }}
disabled={clientName == ''}
on:click={() => {
if (oauths) {
oauths[clientName] = { id: '', secret: '', login_config: {} }
}
clientName = ''
}}
>
Add custom SSO client {!$enterpriseLicense ? '(requires ee)' : ''}
</Button>
</div>
<div class="flex gap-2 py-4">
<Toggle
options={{
right: 'Require users to have been added manually to Windmill to sign in through OAuth'
}}
bind:checked={requirePreexistingUserForOauth}
/>
</div>
{:else if tab === 'oauth'}
<div class="mb-2">
<span class="text-primary text-sm"
>When one of the below options is set, you will be able to create a specific resource
containing a token automatically generated by the third-party provider.
<br />
To test it after setting an oauth client, go to the Resources menu and create a new one of
the type of your oauth client (i.e. a 'github' resource if you set Github OAuth).
<br /><a target="_blank" href="https://www.windmill.dev/docs/misc/setup_oauth#oauth"
>Learn more</a
></span
>
</div>
<div class="py-1"></div>
<OAuthSetting login={false} name="slack" bind:value={oauths['slack']} />
<div class="py-1"></div>
<OAuthSetting login={false} name="teams" eeOnly={true} bind:value={oauths['teams']} />
<div class="py-1"></div>
<div class="py-1"></div>
<div class="mb-2">
<span class="text-primary text-sm"
>When at least one of the below options is set, users will be able to login to Windmill via
their third-party account.
<br /> To test SSO, the recommended workflow is to to save the settings and try to login in
an incognito window.
<a target="_blank" href="https://www.windmill.dev/docs/misc/setup_oauth#sso">Learn more</a
></span
>
</div>
<div class="flex flex-col gap-3 py-4">
<OAuthSetting name="google" bind:value={oauths['google']} />
<OAuthSetting name="microsoft" bind:value={oauths['microsoft']} />
<OktaSetting bind:value={oauths['okta']} />
<Auth0Setting bind:value={oauths['auth0']} />
<OAuthSetting name="github" bind:value={oauths['github']} />
<OAuthSetting name="gitlab" bind:value={oauths['gitlab']} />
<OAuthSetting name="jumpcloud" bind:value={oauths['jumpcloud']} />
<KeycloakSetting bind:value={oauths['keycloak']} />
<AuthentikSetting bind:value={oauths['authentik']} />
<AutheliaSetting bind:value={oauths['authelia']} />
<KanidmSetting bind:value={oauths['kanidm']} />
<ZitadelSetting bind:value={oauths['zitadel']} />
{#each Object.keys(oauths) as k}
{#if !['authelia', 'authentik', 'google', 'microsoft', 'github', 'gitlab', 'jumpcloud', 'okta', 'auth0', 'keycloak', 'slack', 'kanidm', 'zitadel'].includes(k) && 'login_config' in oauths[k]}
{#if oauths[k]}
{#if oauths[k] && !('login_config' in oauths[k])}
{#if !['slack', 'teams'].includes(k) && oauths[k]}
<div class="flex flex-col gap-2 pb-4">
<div class="flex flex-row items-center gap-2">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-md font-medium text-primary">{k}</label>
<CloseButton
on:close={() => {
delete oauths[k]
oauths = { ...oauths }
if (oauths) {
delete oauths[k]
oauths = { ...oauths }
}
}}
/>
</div>
<div class="p-2 border rounded">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input
type="text"
placeholder="Custom Name"
bind:value={oauths[k]['display_name']}
/>
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={oauths[k]['id']} />
@@ -124,141 +233,68 @@
<span class="text-primary font-semibold text-sm">Client Secret</span>
<input type="text" placeholder="Client Secret" bind:value={oauths[k]['secret']} />
</label>
{#if !windmillBuiltins.includes(k) && k != 'slack'}
<CustomSso bind:login_config={oauths[k]['login_config']} />
{#if k === 'azure_oauth'}
<AzureOauthSettings bind:connect_config={oauths[k]['connect_config']} />
{:else if !windmillBuiltins.includes(k) && k != 'slack'}
<CustomOauth bind:connect_config={oauths[k]['connect_config']} />
{/if}
{#if k == 'snowflake_oauth'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm flex gap-2 items-center"
><a
href="https://docs.snowflake.com/en/user-guide/admin-account-identifier#using-an-account-name-as-an-identifier"
target="_blank">Snowflake Account Identifier</a
><ExternalLink size={12} /></span
>
<input
type="text"
placeholder="<orgname>-<account_name>"
required={true}
bind:value={snowflakeAccountIdentifier}
/>
</label>
{/if}
</div>
</div>
{/if}
{/if}
{/each}
</div>
<div class="flex gap-2 py-4">
<input type="text" placeholder="client_id" bind:value={clientName} />
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: Plus }}
disabled={clientName == ''}
on:click={() => {
oauths[clientName] = { id: '', secret: '', login_config: {} }
clientName = ''
}}
>
Add custom SSO client {!$enterpriseLicense ? '(requires ee)' : ''}
</Button>
</div>
<div class="flex gap-2 py-4">
<Toggle
options={{
right: 'Require users to have been added manually to Windmill to sign in through OAuth'
}}
bind:checked={requirePreexistingUserForOauth}
/>
</div>
{:else if tab === 'oauth'}
<div class="mb-2">
<span class="text-primary text-sm"
>When one of the below options is set, you will be able to create a specific resource
containing a token automatically generated by the third-party provider.
<br />
To test it after setting an oauth client, go to the Resources menu and create a new one of the
type of your oauth client (i.e. a 'github' resource if you set Github OAuth).
<br /><a target="_blank" href="https://www.windmill.dev/docs/misc/setup_oauth#oauth"
>Learn more</a
></span
>
</div>
<div class="py-1"></div>
<OAuthSetting login={false} name="slack" bind:value={oauths['slack']} />
<div class="py-1"></div>
<OAuthSetting login={false} name="teams" eeOnly={true} bind:value={oauths['teams']} />
<div class="py-1"></div>
{#each Object.keys(oauths) as k}
{#if oauths[k] && !('login_config' in oauths[k])}
{#if !['slack', 'teams'].includes(k) && oauths[k]}
<div class="flex flex-col gap-2 pb-4">
<div class="flex flex-row items-center gap-2">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="text-md font-medium text-primary">{k}</label>
<CloseButton
on:close={() => {
delete oauths[k]
oauths = { ...oauths }
}}
/>
</div>
<div class="p-2 border rounded">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={oauths[k]['id']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Secret</span>
<input type="text" placeholder="Client Secret" bind:value={oauths[k]['secret']} />
</label>
{#if k === 'azure_oauth'}
<AzureOauthSettings bind:connect_config={oauths[k]['connect_config']} />
{:else if !windmillBuiltins.includes(k) && k != 'slack'}
<CustomOauth bind:connect_config={oauths[k]['connect_config']} />
{/if}
{#if k == 'snowflake_oauth'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm flex gap-2 items-center"
><a
href="https://docs.snowflake.com/en/user-guide/admin-account-identifier#using-an-account-name-as-an-identifier"
target="_blank">Snowflake Account Identifier</a
><ExternalLink size={12} /></span
>
<input
type="text"
placeholder="<orgname>-<account_name>"
required={true}
bind:value={snowflakeAccountIdentifier}
/>
</label>
{/if}
</div>
</div>
<div class="flex gap-2">
<select name="oauth_name" id="oauth_name" bind:value={oauth_name}>
<option value={undefined}>Select an OAuth client</option>
<option value="custom">Fully Custom (requires ee)</option>
{#each windmillBuiltins as name}
<option value={name}>{capitalize(name)}</option>
{/each}
</select>
{#if oauth_name == 'custom'}
<input type="text" placeholder="client_id" bind:value={resourceName} />
{:else}
<input type="text" value={oauth_name ?? ''} disabled />
{/if}
{/if}
{/each}
<div class="flex gap-2">
<select name="oauth_name" id="oauth_name" bind:value={oauth_name}>
<option value={undefined}>Select an OAuth client</option>
<option value="custom">Fully Custom (requires ee)</option>
{#each windmillBuiltins as name}
<option value={name}>{capitalize(name)}</option>
{/each}
</select>
{#if oauth_name == 'custom'}
<input type="text" placeholder="client_id" bind:value={resourceName} />
{:else}
<input type="text" value={oauth_name ?? ''} disabled />
{/if}
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: Plus }}
disabled={!oauth_name ||
(oauth_name == 'custom' && resourceName == '') ||
(oauth_name == 'custom' && !$enterpriseLicense)}
on:click={() => {
let name = oauth_name == 'custom' ? resourceName : oauth_name
oauths[name ?? ''] = { id: '', secret: '' }
resourceName = ''
}}
>
Add OAuth client {oauth_name == 'custom' && !$enterpriseLicense ? '(requires ee)' : ''}
</Button>
</div>
{:else if tab == 'scim'}
<slot name="scim" />
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: Plus }}
disabled={!oauth_name ||
(oauth_name == 'custom' && resourceName == '') ||
(oauth_name == 'custom' && !$enterpriseLicense)}
on:click={() => {
if (oauths) {
let name = oauth_name == 'custom' ? resourceName : oauth_name
oauths[name ?? ''] = { id: '', secret: '' }
}
resourceName = ''
}}
>
Add OAuth client {oauth_name == 'custom' && !$enterpriseLicense ? '(requires ee)' : ''}
</Button>
</div>
{:else if tab == 'scim'}
{@render scim?.()}
{/if}
{/if}
</div>
@@ -11,11 +11,15 @@
import Label from './Label.svelte'
import MultiSelect from 'svelte-multiselect'
export let config: AutoscalingConfig | undefined
export let worker_tags: string[] | undefined
interface Props {
config: AutoscalingConfig | undefined
worker_tags: string[] | undefined
}
let { config = $bindable(), worker_tags }: Props = $props()
const dispatch = createEventDispatcher()
let test_input: number = 3
let test_input: number = $state(3)
</script>
<div class="flex flex-row gap-16 pt-2">
@@ -49,11 +53,11 @@
}
}}
/>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Min # of Workers
{#if config !== undefined}
<input on:input={() => dispatch('dirty')} type="number" bind:value={config.min_workers} />
<input oninput={() => dispatch('dirty')} type="number" bind:value={config.min_workers} />
{#if config.min_workers !== undefined && config.min_workers != undefined && config.min_workers > config.max_workers}
<div class="text-red-600 text-xs whitespace-nowrap"
>Minimum cannot be {'>'} to Maximum</div
@@ -63,11 +67,11 @@
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Max # of Workers
{#if config !== undefined}
<input on:input={() => dispatch('dirty')} type="number" bind:value={config.max_workers} />
<input oninput={() => dispatch('dirty')} type="number" bind:value={config.max_workers} />
{:else}
<input type="number" disabled />
{/if}
@@ -75,12 +79,12 @@
<div class="p-2">
<Section label="Advanced" small collapsable={true}>
<div class="flex flex-col gap-2 text-2xs">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Cooldown seconds after an incremental scale-in/out
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="30"
@@ -91,12 +95,12 @@
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Cooldown seconds after a full scale out
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="30"
@@ -107,12 +111,12 @@
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num jobs waiting to trigger an incremental scale-out
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
bind:value={config.inc_scale_num_jobs_waiting}
placeholder="1"
@@ -122,14 +126,14 @@
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num jobs waiting to trigger a full scale out <Tooltip
>Default: max_workers, full scale out = scale out to max workers</Tooltip
>
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
placeholder="max workers"
bind:value={config.full_scale_jobs_waiting}
@@ -138,14 +142,14 @@
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Occupancy rate % threshold to go below to trigger a scale-in (decrease) <Tooltip
>Default: 25%, need to go below average of all of 15s, 5m and 30m occupancy rates</Tooltip
>
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="0"
@@ -157,14 +161,14 @@
<input type="number" step="0.01" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Occupancy rate threshold to exceed to trigger an incremental scale-out (increase) <Tooltip
>Default: 75%, need to exceed average of all of 15s, 5m and 30m occupancy rates</Tooltip
>
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="0"
@@ -176,14 +180,14 @@
<input type="number" step="0.01" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num workers to scale-in/out by when incremental <Tooltip
>Default: (max_workers - min_workers) / 5</Tooltip
>
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="1"
@@ -196,12 +200,12 @@
</label>
<Label label="Custom tags to autoscale on">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip>
By default, autoscaling will apply to the tags the worker group is assigned to but
you can override this here.
</Tooltip>
</svelte:fragment>
{/snippet}
{#if config}
{#if config.custom_tags}
<MultiSelect
@@ -259,30 +263,31 @@
on:selected={(e) => dispatch('dirty')}
bind:selected={config.integration.type}
class="mb-4 mt-2"
let:item
>
<ToggleButton
value="dryrun"
label="Dry run"
tooltip="See autoscaling events but not actual scaling actions will be performed"
{item}
/>
<ToggleButton
value="script"
label="Custom script"
tooltip="Run a custom script to scale your worker group"
{item}
/>
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
<ToggleButton disabled value="kubernetes" label="Kubernetes (soon)" {item} />
{#snippet children({ item })}
<ToggleButton
value="dryrun"
label="Dry run"
tooltip="See autoscaling events but not actual scaling actions will be performed"
{item}
/>
<ToggleButton
value="script"
label="Custom script"
tooltip="Run a custom script to scale your worker group"
{item}
/>
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
<ToggleButton disabled value="kubernetes" label="Kubernetes (soon)" {item} />
{/snippet}
</ToggleButtonGroup>
{#if config.integration.type === 'script'}
<label>
Script path on the 'admins' workspace
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="text"
bind:value={config.integration.path}
/>
@@ -292,7 +297,7 @@
Custom tag for executing script (optional)
{#if config.integration.tag}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="text"
bind:value={config.integration.tag}
/>
@@ -337,12 +342,14 @@
</div>
{/if}
{:else}
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2" let:item>
<ToggleButton value="dryrun" label="Dry run" {item} />
<ToggleButton value="script" label="Custom script" {item} />
<ToggleButton value="ecs" label="ECS (soon)" {item} />
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
<ToggleButton value="kubernetes" label="Kubernetes (soon)" {item} />
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2">
{#snippet children({ item })}
<ToggleButton value="dryrun" label="Dry run" {item} />
<ToggleButton value="script" label="Custom script" {item} />
<ToggleButton value="ecs" label="ECS (soon)" {item} />
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
<ToggleButton value="kubernetes" label="Kubernetes (soon)" {item} />
{/snippet}
</ToggleButtonGroup>
<label>
@@ -18,10 +18,14 @@
import YAML from 'yaml'
import { twMerge } from 'tailwind-merge'
import ContentSearchInnerItem from './ContentSearchInnerItem.svelte'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
const dispatch = createEventDispatcher()
export let search: string = ''
interface Props {
search?: string
}
let { search = $bindable('') }: Props = $props()
export async function open(nsearch?: string) {
await Promise.all([loadScripts(), loadResources(), loadApps(), loadFlows()])
@@ -46,19 +50,19 @@
flows = await FlowService.listSearchFlow({ workspace: $workspaceStore ?? '' })
}
let searchKind: 'all' | 'scripts' | 'flows' | 'apps' | 'resources' = 'all'
let searchKind: 'all' | 'scripts' | 'flows' | 'apps' | 'resources' = $state('all')
let scripts: undefined | { path: string; content: string }[] = undefined
let filteredScriptItems: { path: string; content: string; marked: any }[] = []
let scripts: undefined | { path: string; content: string }[] = $state(undefined)
let filteredScriptItems: { path: string; content: string; marked: any }[] = $state([])
let resources: undefined | { path: string; value: any }[] = undefined
let filteredResourceItems: { path: string; value: any; marked: any }[] = []
let resources: undefined | { path: string; value: any }[] = $state(undefined)
let filteredResourceItems: { path: string; value: any; marked: any }[] = $state([])
let flows: undefined | { path: string; value: any }[] = undefined
let filteredFlowItems: { path: string; value: any; marked: any }[] = []
let flows: undefined | { path: string; value: any }[] = $state(undefined)
let filteredFlowItems: { path: string; value: any; marked: any }[] = $state([])
let apps: undefined | { path: string; value: any }[] = undefined
let filteredAppItems: { path: string; value: any; marked: any }[] = []
let apps: undefined | { path: string; value: any }[] = $state(undefined)
let filteredAppItems: { path: string; value: any; marked: any }[] = $state([])
function getCounts(n: number) {
return ` (${n})`
@@ -73,23 +77,34 @@
.replace(/'/g, '&#39;')
}
$: counts =
let showNbScripts = $state(10)
let showNbApps = $state(10)
let showNbResources = $state(10)
let showNbFlows = $state(10)
function resetShows() {
showNbScripts = 10
showNbApps = 10
showNbResources = 10
showNbFlows = 10
}
let counts = $derived(
search == '' ||
!scripts ||
!resources ||
!flows ||
!apps ||
!filteredAppItems ||
!filteredFlowItems ||
!filteredResourceItems ||
!filteredScriptItems
!scripts ||
!resources ||
!flows ||
!apps ||
!filteredAppItems ||
!filteredFlowItems ||
!filteredResourceItems ||
!filteredScriptItems
? {
all: '',
apps: '',
flows: '',
resources: '',
scripts: ''
}
}
: {
all: getCounts(
filteredAppItems.length +
@@ -101,21 +116,11 @@
resources: getCounts(filteredResourceItems.length),
flows: getCounts(filteredFlowItems.length),
scripts: getCounts(filteredScriptItems.length)
}
let showNbScripts = 10
let showNbApps = 10
let showNbResources = 10
let showNbFlows = 10
$: search && resetShows()
function resetShows() {
showNbScripts = 10
showNbApps = 10
showNbResources = 10
showNbFlows = 10
}
}
)
$effect(() => {
search && untrack(() => resetShows())
})
</script>
<SearchItems
@@ -157,42 +162,44 @@
<div class="flex flex-col gap-2">
<div class="flex gap-2 flex-wrap sticky top-0 left-0 right-0 bg-surface">
<div class="p-2">
<ToggleButtonGroup bind:selected={searchKind} class="h-10 " let:item>
<ToggleButton small light value="all" label={'All' + counts.all} {item} />
<ToggleButton
small
light
value="scripts"
icon={Code2}
label={'Scripts' + counts.scripts}
{item}
/>
<ToggleButton
small
light
value="resources"
icon={Boxes}
label={'Resources' + counts.resources}
{item}
/>
<ToggleButton
small
light
value="flows"
label={'Flows' + counts.flows}
icon={FlowIcon}
selectedColor="#14b8a6"
{item}
/>
<ToggleButton
small
light
value="apps"
label={'Apps' + counts.apps}
icon={LayoutDashboard}
selectedColor="#fb923c"
{item}
/>
<ToggleButtonGroup bind:selected={searchKind} class="h-10 ">
{#snippet children({ item })}
<ToggleButton small light value="all" label={'All' + counts.all} {item} />
<ToggleButton
small
light
value="scripts"
icon={Code2}
label={'Scripts' + counts.scripts}
{item}
/>
<ToggleButton
small
light
value="resources"
icon={Boxes}
label={'Resources' + counts.resources}
{item}
/>
<ToggleButton
small
light
value="flows"
label={'Flows' + counts.flows}
icon={FlowIcon}
selectedColor="#14b8a6"
{item}
/>
<ToggleButton
small
light
value="apps"
label={'Apps' + counts.apps}
icon={LayoutDashboard}
selectedColor="#fb923c"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
</div>
@@ -245,7 +252,7 @@
href={`/scripts/get/${item.path}`}
on:close
>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
href={`/scripts/get/${item.path}`}
color="light"
@@ -267,7 +274,7 @@
>
Edit
</Button>
</svelte:fragment>
{/snippet}
<pre class="text-xs border rounded-md p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code>
</pre>
@@ -295,7 +302,7 @@
href={`/resources#${item.path}`}
on:close
>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
href={`/resources#${item.path}`}
color="light"
@@ -305,7 +312,7 @@
>
Edit
</Button>
</svelte:fragment>
{/snippet}
<pre class="text-xs border rounded-md p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
>
@@ -333,7 +340,7 @@
href={`/flows/get/${item.path}`}
on:close
>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
href={`/flows/get/${item.path}`}
color="light"
@@ -354,7 +361,7 @@
>
Edit
</Button>
</svelte:fragment>
{/snippet}
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
@@ -383,7 +390,7 @@
href={`/apps/get/${item.path}`}
on:close
>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
href={`/apps/get/${item.path}`}
color="light"
@@ -404,7 +411,7 @@
>
Edit
</Button>
</svelte:fragment>
{/snippet}
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
@@ -1,8 +1,14 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let title: string
export let href: string
interface Props {
title: string
href: string
actions?: import('svelte').Snippet
children?: import('svelte').Snippet
}
let { title, href, actions, children }: Props = $props()
const dispatch = createEventDispatcher()
</script>
@@ -12,7 +18,7 @@
<a
class="text-sm text-blue-500 truncate hover:underline"
{href}
on:click={() => {
onclick={() => {
dispatch('close')
}}
>
@@ -20,8 +26,8 @@
</a>
<div class="flex flex-row gap-2 items-center">
<slot name="actions" />
{@render actions?.()}
</div>
</div>
<slot />
{@render children?.()}
</div>
@@ -69,7 +69,7 @@
<textarea rows="4" use:autosize bind:value placeholder="Variable value"></textarea>
</Section>
</div>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
on:click={() => updateVariable()}
disabled={value === '' || name === ''}
@@ -79,6 +79,6 @@
>
{edit ? 'Update' : 'Save'}
</Button>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
@@ -249,7 +249,7 @@
</Pane>
</Splitpanes>
{/if}
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
loading={refreshing}
on:click={() => refresh()}
@@ -266,7 +266,7 @@
size="xs"
color="light"
/>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
{/if}
@@ -24,10 +24,11 @@
class="mb-4"
selected={dbSchema.publicOnly ? 'dbo' : 'all'}
on:selected={handleSelected}
let:item
>
<ToggleButton value="dbo" label={dbSchema.schema.dbo ? 'Dbo' : 'Public'} {item} />
<ToggleButton value="all" label="All" {item} />
{#snippet children({ item })}
<ToggleButton value="dbo" label={dbSchema.schema.dbo ? 'Dbo' : 'Public'} {item} />
<ToggleButton value="all" label="All" {item} />
{/snippet}
</ToggleButtonGroup>
{/if}
{#if dbSchema.lang === 'graphql'}
+3 -2
View File
@@ -13,6 +13,7 @@
import { deepEqual } from 'fast-equals'
import 'ag-grid-community/styles/ag-grid.css'
import '$lib/components/apps/components/display/table/theme/windmill-theme.css'
import { untrack } from 'svelte'
type Props = {
dbTableOps: IDbTableOps
@@ -45,7 +46,7 @@
let refreshCount = $state(0)
const refresh = () => (refreshCount += 1)
$effect(() => eGui && mountGrid())
$effect(() => eGui && untrack(() => mountGrid()))
function mountGrid() {
if (eGui && !api) {
createGrid(eGui, {
@@ -95,7 +96,7 @@
const key = { quicksearch, colDefs: dbTableOps.colDefs, refreshCount }
if (deepEqual(key, prevUpdateKey)) return
prevUpdateKey = key
updateGrid()
untrack(() => updateGrid())
})
function updateGrid() {
dbTableOps.getCount({ quicksearch }).then((result) => (rowCount = result))
@@ -262,7 +262,6 @@
? o.split('.')[1]
: o
}))}
clearable={false}
/>
</Cell>
<Cell>
@@ -1,24 +1,39 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import { createEventDispatcher, untrack } from 'svelte'
import { Button } from './common'
import { Clock, X } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
// import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
// import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
export let value: string | undefined = undefined
interface Props {
// import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
value?: string | undefined
clearable?: boolean
autofocus?: boolean | null
useDropdown?: boolean
minDate?: string | undefined
maxDate?: string | undefined
disabled?: boolean | undefined
inputClass?: string | undefined
}
export let clearable: boolean = false
export let autofocus: boolean | null = false
export let useDropdown: boolean = false
export let minDate: string | undefined = undefined
export let maxDate: string | undefined = undefined
export let disabled: boolean | undefined = undefined
export let inputClass: string | undefined = undefined
let {
value = $bindable(undefined),
clearable = false,
autofocus = false,
useDropdown = false,
minDate = undefined,
maxDate = undefined,
disabled = undefined,
inputClass = undefined
}: Props = $props()
let date: string | undefined = undefined
let time: string | undefined = undefined
let date: string | undefined = $state(undefined)
let time: string | undefined = $state(undefined)
// let format: 'local' | 'utc' = 'local'
@@ -38,10 +53,15 @@
: '12:00'
}
$: parseValue(value)
$effect(() => {
value
untrack(() => {
parseValue(value)
})
})
let initialDate = date
let initialTime = time
let initialDate = untrack(() => date)
let initialTime = untrack(() => time)
function parseDateAndTime(date: string | undefined, time: string | undefined) {
if (date && time && (initialDate != date || initialTime != time)) {
@@ -54,7 +74,12 @@
}
}
$: parseDateAndTime(date, time)
$effect(() => {
;[date, time]
untrack(() => {
parseDateAndTime(date, time)
})
})
function isValidDate(d: Date | undefined): boolean {
return d instanceof Date && !isNaN(d as any)
@@ -73,8 +98,13 @@
let randomId = 'datetarget-' + Math.random().toString(36).substring(7)
</script>
<div class="flex flex-row gap-1 items-center w-full" id={randomId} on:pointerdown on:focus>
<!-- svelte-ignore a11y-autofocus -->
<div
class="flex flex-row gap-1 items-center w-full"
id={randomId}
onpointerdown={bubble('pointerdown')}
onfocus={bubble('focus')}
>
<!-- svelte-ignore a11y_autofocus -->
<input
type="date"
bind:value={date}
@@ -36,11 +36,11 @@
/>
{/if}
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
disabled={workspaceToDeployTo == undefined}
on:click={() => deployWorkspace?.deployAll()}>Deploy All</Button
>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+74 -65
View File
@@ -11,7 +11,6 @@
OpenAPI,
type Preview,
type OpenFlow,
type FlowModule,
WorkspaceService,
type InputTransform,
type TriggersCount
@@ -20,7 +19,7 @@
import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores'
import { emptySchema, sendUserToast } from '$lib/utils'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { onDestroy, onMount, setContext } from 'svelte'
import { onDestroy, onMount, setContext, untrack } from 'svelte'
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
import { page } from '$app/stores'
import { getUserExt } from '$lib/user'
@@ -49,16 +48,6 @@
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
import type { PickableProperties } from './flows/previousResults'
import { Triggers } from './triggers/triggers.svelte'
$: token = $page.url.searchParams.get('wm_token') ?? undefined
$: workspace = $page.url.searchParams.get('workspace') ?? undefined
$: themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
$: themeDark = themeDarkRaw == '2' || themeDarkRaw == '4'
$: if (token) {
OpenAPI.WITH_CREDENTIALS = true
OpenAPI.TOKEN = token
loadUser()
}
let flowCopilotContext: FlowCopilotContext = {
shouldUpdatePropertyType: writable<{
@@ -87,14 +76,6 @@
}
}
}
$: if (workspace) {
$workspaceStore = workspace
setupCopilotInfo()
}
$: if (workspace && token) {
loadUser()
}
async function loadUser() {
try {
@@ -105,31 +86,26 @@
}
}
let darkModeToggle: DarkModeToggle
let darkMode: boolean | undefined = undefined
let modeInitialized = false
let darkModeToggle: DarkModeToggle | undefined = $state()
let darkMode: boolean | undefined = $state(undefined)
let modeInitialized = $state(false)
function initializeMode() {
modeInitialized = true
darkModeToggle.toggle()
darkModeToggle?.toggle()
}
$: darkModeToggle &&
themeDark != darkMode &&
darkMode != undefined &&
!modeInitialized &&
initializeMode()
let testJobLoader: TestJobLoader
let testJobLoader: TestJobLoader | undefined = $state()
let socket: WebSocket | undefined = undefined
// Test args input
let args: Record<string, any> = {}
let isValid: boolean = true
let args: Record<string, any> = $state({})
let isValid: boolean = $state(true)
// Test
let testIsLoading = false
let testJob: Job | undefined
let pastPreviews: CompletedJob[] = []
let validCode = true
let testIsLoading = $state(false)
let testJob: Job | undefined = $state()
let pastPreviews: CompletedJob[] = $state([])
let validCode = $state(true)
type LastEditScript = {
content: string
@@ -140,9 +116,9 @@
tag?: string
}
let currentScript: LastEditScript | undefined = undefined
let currentScript: LastEditScript | undefined = $state(undefined)
let schema = emptySchema()
let schema = $state(emptySchema())
const href = window.location.href
const indexQ = href.indexOf('?')
const searchParams = indexQ > -1 ? new URLSearchParams(href.substring(indexQ)) : undefined
@@ -151,12 +127,12 @@
connectWs()
}
let useLock = false
let useLock = $state(false)
let lockChanges = false
let timeout: NodeJS.Timeout | undefined = undefined
let loadingCodebaseButton = false
let loadingCodebaseButton = $state(false)
let lastCommandId = ''
const el = (event) => {
@@ -174,7 +150,7 @@
}
} else if (event.data.type == 'testPreviewBundle') {
if (event.data.id == lastCommandId && currentScript) {
testJobLoader.runPreview(
testJobLoader?.runPreview(
currentScript.path,
event.data.file,
currentScript.language,
@@ -333,7 +309,7 @@
}
}
let typescriptBundlePreviewMode = false
let typescriptBundlePreviewMode = $state(false)
function runTest() {
if (mode == 'script') {
if (!currentScript) {
@@ -391,7 +367,7 @@
}
}
let relativePaths: any[] = []
let relativePaths: any[] = $state([])
let lastPath: string | undefined = undefined
async function replaceScript(lastEdit: LastEditScript) {
mode = 'script'
@@ -416,14 +392,16 @@
}
}
let mode: 'script' | 'flow' = 'script'
let mode: 'script' | 'flow' = $state('script')
const flowStore = writable({
summary: '',
value: { modules: [] },
extra_perms: {},
schema: emptySchema()
} as OpenFlow)
const flowStore = $state({
val: {
summary: '',
value: { modules: [] },
extra_perms: {},
schema: emptySchema()
} as OpenFlow
})
type LastEditFlow = {
flow: OpenFlow
@@ -436,14 +414,14 @@
// sendUserToast(JSON.stringify(lastEdit.flow), true)
// return
try {
if (!deepEqual(lastEdit.flow, $flowStore)) {
if (!deepEqual(lastEdit.flow, flowStore)) {
if (!lastEdit.flow.summary) {
lastEdit.flow.summary = 'New flow'
}
if (!lastEdit.flow.value?.modules) {
lastEdit.flow.value = { modules: [] }
}
$flowStore = lastEdit.flow
flowStore.val = lastEdit.flow
inferModuleArgs($selectedIdStore)
}
} catch (e) {
@@ -453,10 +431,10 @@
const flowStateStore = writable({} as FlowState)
const previewArgsStore = writable<Record<string, any>>({})
const previewArgsStore = $state({ val: {} })
const scriptEditorDrawer = writable(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
const history = initHistory($flowStore)
const moving = writable<{ id: string } | undefined>(undefined)
const history = initHistory(flowStore.val)
const testStepStore = writable<Record<string, any>>({})
const selectedIdStore = writable('settings-metadata')
@@ -496,7 +474,6 @@
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
pickablePropertiesFiltered: writable<PickableProperties | undefined>(undefined)
})
$: updateFlow($flowStore)
let lastSent: OpenFlow | undefined = undefined
function updateFlow(flow: OpenFlow) {
@@ -509,17 +486,15 @@
}
}
$: $selectedIdStore && inferModuleArgs($selectedIdStore)
let flowPreviewButtons: FlowPreviewButtons
let reload = 0
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
let reload = $state(0)
async function inferModuleArgs(selectedIdStore: string) {
if (selectedIdStore == '') {
return
}
//@ts-ignore
dfs($flowStore.value.modules, async (mod) => {
dfs(flowStore.value.modules, async (mod) => {
if (mod.id == selectedIdStore) {
if (
mod.value.type == 'rawscript' ||
@@ -544,9 +519,44 @@
}
})
}
let token = $derived($page.url.searchParams.get('wm_token') ?? undefined)
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(() => {
if (token) {
OpenAPI.WITH_CREDENTIALS = true
OpenAPI.TOKEN = token
untrack(() => loadUser())
}
})
$effect(() => {
if (workspace) {
$workspaceStore = workspace
untrack(() => setupCopilotInfo())
}
})
$effect(() => {
if (workspace && token) {
untrack(() => loadUser())
}
})
$effect(() => {
darkModeToggle &&
themeDark != darkMode &&
darkMode != undefined &&
!modeInitialized &&
untrack(() => initializeMode())
})
$effect(() => {
flowStore.val && untrack(() => updateFlow(flowStore.val))
})
$effect(() => {
$selectedIdStore && untrack(() => inferModuleArgs($selectedIdStore))
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<TestJobLoader
on:done={loadPastTests}
@@ -686,10 +696,9 @@
</div>
<Splitpanes horizontal class="h-full max-h-screen grow">
<Pane size={67}>
{#if $flowStore?.value?.modules}
{#if flowStore.val?.value?.modules}
<div id="flow-editor"></div>
<FlowModuleSchemaMap
bind:modules={$flowStore.value.modules}
disableAi
disableTutorials
smallErrorHandler={true}
@@ -709,7 +718,7 @@
$testStepStore['preprocessor'] = ev.detail.args ?? {}
$selectedIdStore = 'preprocessor'
} else {
$previewArgsStore = ev.detail.args ?? {}
previewArgsStore.val = ev.detail.args ?? {}
flowPreviewButtons?.openPreview()
}
}}
@@ -55,7 +55,6 @@
diffViewer.closeDrawer()
}
function prepareDiff(data: Value) {
const metadata = structuredClone(cleanValueProperties(replaceFalseWithUndefined(data)))
const content = metadata['content']
@@ -126,8 +125,8 @@
data_[dataType]?.content !== data_.current.content
? 'content'
: data_[dataType]?.metadata !== data_.current.metadata
? 'metadata'
: undefined
? 'metadata'
: undefined
}
$: updateContentType(data, diffType)
@@ -256,7 +255,7 @@
<Loader2 class="animate-spin" />
{/if}
</div>
<svelte:fragment slot="actions">
{#snippet actions()}
{#if data?.button}
<Button
color="light"
@@ -268,6 +267,6 @@
}}>{data.button.text}</Button
>
{/if}
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+103 -73
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import DisplayResult from './DisplayResult.svelte'
import { Highlight } from 'svelte-highlight'
import { json } from 'svelte-highlight/languages'
import { copyToClipboard, roughSizeOfObject } from '$lib/utils'
@@ -36,30 +38,12 @@
import type { DisplayResultUi } from './custom_ui'
import { getContext, hasContext, createEventDispatcher, onDestroy } from 'svelte'
import { toJsonStr } from '$lib/utils'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let result: any
export let requireHtmlApproval = false
export let filename: string | undefined = undefined
export let disableExpand = false
export let jobId: string | undefined = undefined
export let workspaceId: string | undefined = undefined
export let hideAsJson: boolean = false
export let noControls: boolean = false
export let drawerOpen = false
export let nodeId: string | undefined = undefined
export let language: string | undefined = undefined
export let appPath: string | undefined = undefined
export let customUi: DisplayResultUi | undefined = undefined
export let isTest: boolean = true
export let externalToolbarAvailable: boolean = false
const IMG_MAX_SIZE = 10000000
const TABLE_MAX_SIZE = 5000000
const DISPLAY_MAX_SIZE = 100000
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
let resultKind:
| 'json'
@@ -82,14 +66,53 @@
| 'map'
| 'nondisplayable'
| 'pdf'
| undefined
| undefined = $state()
let hasBigInt = false
$: resultKind = inferResultKind(result)
let hasBigInt = $state(false)
export let forceJson = false
let enableHtml = false
let s3FileDisplayRawMode = false
interface Props {
result: any
requireHtmlApproval?: boolean
filename?: string | undefined
disableExpand?: boolean
jobId?: string | undefined
workspaceId?: string | undefined
hideAsJson?: boolean
noControls?: boolean
drawerOpen?: boolean
nodeId?: string | undefined
language?: string | undefined
appPath?: string | undefined
customUi?: DisplayResultUi | undefined
isTest?: boolean
externalToolbarAvailable?: boolean
forceJson?: boolean
copilot_fix?: import('svelte').Snippet
children?: import('svelte').Snippet
}
let {
result,
requireHtmlApproval = false,
filename = undefined,
disableExpand = false,
jobId = undefined,
workspaceId = undefined,
hideAsJson = false,
noControls = false,
drawerOpen = $bindable(false),
nodeId = undefined,
language = undefined,
appPath = undefined,
customUi = undefined,
isTest = true,
externalToolbarAvailable = false,
forceJson = $bindable(false),
copilot_fix,
children
}: Props = $props()
let enableHtml = $state(false)
let s3FileDisplayRawMode = $state(false)
function isTableRow(result: any): boolean {
return Array.isArray(result) && result.every((x) => Array.isArray(x))
@@ -125,14 +148,14 @@
)
}
let largeObject: boolean | undefined = undefined
let largeObject: boolean | undefined = $state(undefined)
function checkIfS3(result: any, keys: string[]) {
return keys.includes('s3') && typeof result.s3 === 'string'
}
let is_render_all = false
let download_as_csv = false
let is_render_all = $state(false)
let download_as_csv = $state(false)
function inferResultKind(result: any) {
try {
if (result === 'WINDMILL_TOO_BIG') {
@@ -267,8 +290,8 @@
return 'json'
}
let jsonViewer: Drawer
let s3FileViewer: S3FilePicker
let jsonViewer: Drawer | undefined = $state()
let s3FileViewer: S3FilePicker | undefined = $state()
function checkIfHasBigInt(result: any) {
if (typeof result === 'number' && Number.isInteger(result) && !Number.isSafeInteger(result)) {
@@ -386,20 +409,20 @@
}
export function openDrawer() {
jsonViewer.openDrawer()
jsonViewer?.openDrawer()
}
let globalForceJson: boolean = false
let globalForceJson: boolean = $state(false)
let seeS3PreviewFileFromList = ''
let seeS3PreviewFileFromList = $state('')
const disableTooltips = hasContext('disableTooltips')
? getContext('disableTooltips') === true
: false
let resultHeaderHeight = 0
let resultHeaderHeight = $state(0)
let toolbarLocation: 'self' | 'external' | undefined = undefined
let toolbarLocation: 'self' | 'external' | undefined = $state(undefined)
function chooseToolbarLocation(shouldShowToolbar: boolean, resultHeaderHeight: number) {
if (!shouldShowToolbar) {
toolbarLocation = undefined
@@ -408,27 +431,32 @@
} else {
toolbarLocation = 'self'
}
dispatchIfMounted('toolbar-location-changed', toolbarLocation)
dispatch('toolbar-location-changed', toolbarLocation)
}
export function getToolbarLocation() {
return toolbarLocation
}
$: chooseToolbarLocation(
!is_render_all &&
resultKind != 'nondisplayable' &&
result != undefined &&
length != undefined &&
largeObject != undefined &&
!disableExpand &&
!noControls,
resultHeaderHeight
)
onDestroy(() => {
dispatch('toolbar-location-changed', undefined)
})
$effect(() => {
;[result]
resultKind = inferResultKind(result)
})
$effect(() => {
chooseToolbarLocation(
!is_render_all &&
resultKind != 'nondisplayable' &&
result != undefined &&
length != undefined &&
largeObject != undefined &&
!disableExpand &&
!noControls,
resultHeaderHeight
)
})
</script>
<HighlightTheme />
@@ -442,17 +470,18 @@
on:selected={(ev) => {
globalForceJson = ev.detail === 'json'
}}
let:item
>
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} {item} />
{#snippet children({ item })}
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} {item} />
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
{/snippet}
</ToggleButtonGroup>
</div>
{/if}
<div class="flex flex-col w-full gap-10">
{#each result['render_all'] as res}
<svelte:self
<DisplayResult
{noControls}
result={res}
{requireHtmlApproval}
@@ -484,30 +513,31 @@
{#if !hideAsJson && !['json', 's3object'].includes(resultKind ?? '') && typeof result === 'object'}<ToggleButtonGroup
class="h-6"
selected={forceJson ? 'json' : resultKind?.startsWith('table-') ? 'table' : 'pretty'}
let:item
on:selected={(ev) => {
forceJson = ev.detail === 'json'
}}
>
{#if ['table-col', 'table-row', 'table-row-object'].includes(resultKind ?? '')}
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} {item} />
{:else}
<ToggleButton
class="px-1.5"
value="pretty"
label="Pretty"
icon={Highlighter}
{item}
/>
{/if}
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
{#snippet children({ item })}
{#if ['table-col', 'table-row', 'table-row-object'].includes(resultKind ?? '')}
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} {item} />
{:else}
<ToggleButton
class="px-1.5"
value="pretty"
label="Pretty"
icon={Highlighter}
{item}
/>
{/if}
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
{/snippet}
</ToggleButtonGroup>
{/if}
</div>
<div class="text-secondary text-xs flex gap-2.5 z-10 items-center">
{#if customUi?.disableAiFix !== true}
<slot name="copilot-fix" />
{@render copilot_fix?.()}
{/if}
{#if toolbarLocation === 'self'}
<!-- TODO : When svelte 5 is released, use a snippet to pass the toolbar to a parent -->
@@ -647,7 +677,7 @@
>{JSON.stringify(result.error.extra, null, 4)}</pre
>
{/if}
<slot />
{@render children?.()}
</div>
{#if !isTest && language === 'bun'}
<div class="pt-20"></div>
@@ -723,7 +753,7 @@
/>
<button
class="text-secondary underline text-2xs whitespace-nowrap"
on:click={() => {
onclick={() => {
s3FileViewer?.open?.(result)
}}
><span class="flex items-center gap-1"
@@ -738,7 +768,7 @@
<FileDownload {workspaceId} s3object={result} {appPath} />
<button
class="text-secondary underline text-2xs whitespace-nowrap"
on:click={() => {
onclick={() => {
s3FileViewer?.open?.(result)
}}
><span class="flex items-center gap-1"
@@ -813,7 +843,7 @@
/>
<button
class="text-secondary text-2xs whitespace-nowrap"
on:click={() => {
onclick={() => {
s3FileViewer?.open?.(s3object)
}}
><span class="flex items-center gap-1"
@@ -835,7 +865,7 @@
/>{:else}
<button
class="text-secondary whitespace-nowrap flex gap-2 items-center"
on:click={() => {
onclick={() => {
seeS3PreviewFileFromList = s3object?.s3
}}
>open table preview <ArrowDownFromLine />
@@ -855,7 +885,7 @@
{:else}
<button
class="text-secondary whitespace-nowrap flex gap-2 items-center"
on:click={() => {
onclick={() => {
seeS3PreviewFileFromList = s3object?.s3
}}
>open image preview <ArrowDownFromLine />
@@ -956,7 +986,7 @@
{#if !disableExpand && !noControls}
<Drawer bind:this={jsonViewer} bind:open={drawerOpen} size="900px">
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
{#if customUi?.disableDownload !== true}
<Button
download="{filename ?? 'result'}.json"
@@ -982,8 +1012,8 @@
>
Copy to clipboard
</Button>
</svelte:fragment>
<svelte:self
{/snippet}
<DisplayResult
{noControls}
{result}
{requireHtmlApproval}
+12 -4
View File
@@ -2,19 +2,27 @@
import Popover from './Popover.svelte'
import { Badge } from './common'
export let has_draft: boolean = false
export let draft_only: boolean = false
interface Props {
has_draft?: boolean
draft_only?: boolean
}
let { has_draft = false, draft_only = false }: Props = $props()
</script>
{#if has_draft}
{#if draft_only}
<Popover notClickable>
<svelte:fragment slot="text">Never deployed and is only a draft</svelte:fragment>
{#snippet text()}
Never deployed and is only a draft
{/snippet}
<Badge small color="indigo" baseClass="border border-indigo-200">Draft only</Badge>
</Popover>
{:else}
<Popover notClickable>
<svelte:fragment slot="text">Is deployed and has a draft</svelte:fragment>
{#snippet text()}
Is deployed and has a draft
{/snippet}
<Badge small color="indigo" baseClass="border border-indigo-200">+Draft</Badge>
</Popover>
{/if}
+3 -2
View File
@@ -7,6 +7,7 @@
import ResultJobLoader from './ResultJobLoader.svelte'
import Tooltip from './Tooltip.svelte'
import { Loader2 } from 'lucide-svelte'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
export let value: any = undefined
export let helperScript:
@@ -87,7 +88,7 @@
})
}
let lastArgs = structuredClone({ ...args, [name]: undefined })
let lastArgs = structuredClone({ ...stateSnapshot(args), [name]: undefined })
$: (entrypoint || helperScript) && refreshOptions()
$: args && changeArgs()
@@ -96,7 +97,7 @@
function changeArgs() {
timeout && clearTimeout(timeout)
timeout = setTimeout(() => {
let argsWithoutSelf = { ...args, [name]: undefined }
let argsWithoutSelf = { ...stateSnapshot(args), [name]: undefined }
if (deepEqual(argsWithoutSelf, lastArgs)) {
return
}
@@ -1,4 +1,7 @@
<script lang="ts">
import { createBubbler, preventDefault, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import type { Schema } from '$lib/common'
import { VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -11,7 +14,7 @@
import FlowPropertyEditor from './schema/FlowPropertyEditor.svelte'
import PropertyEditor from './schema/PropertyEditor.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import { createEventDispatcher, tick } from 'svelte'
import { createEventDispatcher, tick, untrack } from 'svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import Label from './Label.svelte'
@@ -22,72 +25,110 @@
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
import { deepEqual } from 'fast-equals'
import { tweened } from 'svelte/motion'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let schema: Schema | any
export let hiddenArgs: string[] = []
export let args: Record<string, any> = {}
export let shouldHideNoInputs: boolean = false
export let noVariablePicker = false
export let flexWrap = false
export let uiOnly: boolean = false
export let isFlowInput: boolean = false
export let noPreview: boolean = false
export let jsonEnabled: boolean = true
export let isAppInput: boolean = false
export let displayWebhookWarning: boolean = false
export let onlyMaskPassword: boolean = false
export let dndType: string | undefined = undefined
export let editTab:
| 'inputEditor'
| 'history'
| 'savedInputs'
| 'json'
| 'captures'
| 'firstStepInputs'
| undefined
export let previewSchema: Record<string, any> | undefined = undefined
export let editPanelInitialSize: number | undefined = undefined
export let editPanelSize = 0
export let diff: Record<string, SchemaDiff> = {}
export let disableDnd: boolean = false
export let shouldDispatchChanges: boolean = false
export let isValid: boolean = true
export let customUi: EditableSchemaFormUi | undefined = undefined
// export let openEditTab: () => void = () => {}
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
let clazz: string = ''
export { clazz as class }
$: if (args == undefined || typeof args !== 'object') {
args = {}
interface Props {
schema: Schema | any
hiddenArgs?: string[]
args?: Record<string, any>
shouldHideNoInputs?: boolean
noVariablePicker?: boolean
flexWrap?: boolean
uiOnly?: boolean
isFlowInput?: boolean
noPreview?: boolean
jsonEnabled?: boolean
isAppInput?: boolean
displayWebhookWarning?: boolean
onlyMaskPassword?: boolean
dndType?: string | undefined
editTab:
| 'inputEditor'
| 'history'
| 'savedInputs'
| 'json'
| 'captures'
| 'firstStepInputs'
| undefined
previewSchema?: Record<string, any> | undefined
editPanelInitialSize?: number | undefined
editPanelSize?: number
diff?: Record<string, SchemaDiff>
disableDnd?: boolean
shouldDispatchChanges?: boolean
isValid?: boolean
customUi?: EditableSchemaFormUi | undefined
pannelExtraButtonWidth?: number
class?: string
openEditTab?: import('svelte').Snippet
addProperty?: import('svelte').Snippet
runButton?: import('svelte').Snippet
extraTab?: import('svelte').Snippet
}
let {
schema = $bindable(),
hiddenArgs = [],
args = $bindable(undefined),
shouldHideNoInputs = false,
noVariablePicker = false,
flexWrap = false,
uiOnly = false,
isFlowInput = false,
noPreview = false,
jsonEnabled = true,
isAppInput = false,
displayWebhookWarning = false,
onlyMaskPassword = false,
dndType = undefined,
editTab,
previewSchema = undefined,
editPanelInitialSize = undefined,
editPanelSize = $bindable(0),
diff = {},
disableDnd = false,
shouldDispatchChanges = false,
isValid = $bindable(true),
customUi = undefined,
pannelExtraButtonWidth = 0,
class: clazz = '',
openEditTab,
addProperty,
runButton,
extraTab
}: Props = $props()
$effect.pre(() => {
if (args == undefined) {
args = {}
}
})
export function setDefaults() {
const nargs = {}
Object.keys(schema?.properties ?? {}).forEach((key) => {
if (schema?.properties[key].default != undefined && args[key] == undefined) {
if (schema?.properties[key].default != undefined && args?.[key] == undefined) {
let value = schema?.properties[key].default
nargs[key] = value === 'object' ? structuredClone(value) : value
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
}
})
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let itemPicker: ItemPicker | undefined = $state(undefined)
let variableEditor: VariableEditor | undefined = $state(undefined)
let keys: string[] = Array.isArray(schema?.order)
? [...schema.order]
: (Object.keys(schema?.properties ?? {}) ?? Object.keys(schema?.properties ?? {}))
$: schema && onSchemaChange()
let keys: string[] = $state(
Array.isArray(schema?.order)
? [...schema.order]
: (Object.keys(schema?.properties ?? {}) ?? Object.keys(schema?.properties ?? {}))
)
function alignOrderWithProperties(schema: {
properties: Record<string, any>
@@ -134,15 +175,25 @@
}
}
let opened: string | undefined = keys[0]
let selected = ''
let opened: string | undefined = $state(untrack(() => keys[0]))
function computeSelected(property: any) {
if (!opened) return ''
return property.type !== 'object'
? property.type
: property.format === 'resource-s3_object'
? 'S3'
: property.oneOf && property.oneOf.length >= 2
? 'oneOf'
: 'object'
}
export function openField(key: string) {
opened = key
}
export function deleteField(key: string) {
delete args[key]
delete args?.[key]
delete schema.properties[key]
if (schema.required?.includes(key)) {
schema.required = schema.required?.filter((x) => x !== key)
@@ -152,20 +203,6 @@
}
}
$: opened && updateSelected(schema.properties[opened])
function updateSelected(property: any) {
if (!property) return
selected = opened
? property.type !== 'object'
? property.type
: property.format === 'resource-s3_object'
? 'S3'
: property.oneOf && property.oneOf.length >= 2
? 'oneOf'
: 'object'
: ''
}
function renameProperty(oldName: string, key: string) {
const el = document.getElementById(key) as HTMLInputElement
const newName = el.value
@@ -179,8 +216,10 @@
// clear the input
el.value = oldName
} else {
args[newName] = args[oldName]
delete args[oldName]
if (args) {
args[newName] = args[oldName]
delete args[oldName]
}
schema.properties[newName] = schema.properties[oldName]
delete schema.properties[oldName]
@@ -199,26 +238,34 @@
opened = newName
schema = schema
schema = $state.snapshot(schema)
dispatch('change', schema)
sendUserToast('Argument renamed successfully')
sendUserToast('Argument renamed')
}
}
let jsonView: boolean = customUi?.jsonOnly == true
let schemaString: string = JSON.stringify(schema, null, '\t')
let error: string | undefined = undefined
let editor: SimpleEditor | undefined = undefined
let jsonView: boolean = $state(customUi?.jsonOnly == true)
let schemaString: string = $state(JSON.stringify(schema, null, '\t'))
let error: string | undefined = $state(undefined)
let editor: SimpleEditor | undefined = $state(undefined)
export function updateJson() {
schemaString = JSON.stringify(schema, null, '\t')
editor?.setCode(schemaString)
}
const editTabDefaultSize = noPreview ? 100 : 50
editPanelSize = editTab ? (editPanelInitialSize ?? editTabDefaultSize) : 0
let inputPanelSize = 100 - editPanelSize
let inputPanelSize = $state(100 - editPanelSize)
let editPanelSizeSmooth = tweened(editPanelSize, {
duration: 150
})
let inputPanelSizeSmooth = tweened(inputPanelSize, { duration: 150 })
let inputPanelSizeSmooth = tweened(
untrack(() => inputPanelSize),
{ duration: 150 }
)
function openEditTab() {
function openEditTabFn() {
if (editPanelSize > 0) return
editPanelSizeSmooth.set(editTabDefaultSize)
inputPanelSizeSmooth.set(100 - editTabDefaultSize)
@@ -232,19 +279,24 @@
function updatePanelSizes(editSize: number, inputSize: number) {
editPanelSize = editSize
inputPanelSize = inputSize
dispatchIfMounted('editPanelSizeChanged', editSize)
dispatch('editPanelSizeChanged', editSize)
}
$: updatePanelSizes($editPanelSizeSmooth, $inputPanelSizeSmooth)
$: !!editTab ? openEditTab() : closeEditTab()
let panelButtonWidth: number = 0
export let pannelExtraButtonWidth: number = 0
export function updateJson() {
schemaString = JSON.stringify(schema, null, '\t')
editor?.setCode(schemaString)
}
let panelButtonWidth: number = $state(0)
$effect(() => {
if (args == undefined || typeof args !== 'object') {
args = {}
}
})
$effect(() => {
schema && untrack(() => onSchemaChange())
})
$effect(() => {
updatePanelSizes($editPanelSizeSmooth, $inputPanelSizeSmooth)
})
$effect(() => {
!!editTab ? openEditTabFn() : closeEditTab()
})
</script>
<div class="w-full h-full">
@@ -254,37 +306,41 @@
style="right: calc({editPanelSize}% - 1px - {pannelExtraButtonWidth}px); top: 0px;"
bind:clientWidth={panelButtonWidth}
>
<slot name="openEditTab" />
{@render openEditTab?.()}
</div>
</div>
<Splitpanes class="splitter-hidden w-full">
{#if !noPreview}
<Pane bind:size={inputPanelSize} minSize={20}>
<div
class="h-full flex flex-col gap-2 {$$slots.openEditTab && editPanelSize > 0
class="h-full flex flex-col gap-2 {openEditTab && editPanelSize > 0
? 'pr-[38px]'
: 'pr-2'}"
>
{#if $$slots.addProperty}
{#if addProperty}
<div class="w-full justify-left pr-2 grow-0">
<div
style={editPanelSize > 0
? `width: 100%;`
: `width: calc(100% - ${panelButtonWidth - pannelExtraButtonWidth}px);`}
>
<slot name="addProperty" />
{@render addProperty?.()}
</div>
</div>
{/if}
<div
class="min-h-0 overflow-y-auto grow rounded-md {$$slots.runButton
? 'flex flex-col gap-2'
: ''}"
class="min-h-0 overflow-y-auto grow rounded-md {runButton ? 'flex flex-col gap-2' : ''}"
>
<SchemaFormDnd
nestedClasses={'flex flex-col gap-1'}
schema={previewSchema ? previewSchema : schema}
bind:schema={
() => (previewSchema ? previewSchema : schema),
(newSchema) => {
schema = newSchema
tick().then(() => dispatch('change', schema))
}
}
{dndType}
{disableDnd}
{onlyMaskPassword}
@@ -293,12 +349,10 @@
opened = e.detail
}}
on:reorder={(e) => {
schema.order = e.detail
schema = schema
tick().then(() => dispatch('change', schema))
}}
on:change={() => {
schema = schema
schema = {
...schema,
order: e.detail
}
tick().then(() => dispatch('change', schema))
}}
prettifyHeader={isAppInput}
@@ -314,7 +368,7 @@
noVariablePicker={noVariablePicker || customUi?.disableVariablePicker === true}
/>
<slot name="runButton" />
{@render runButton?.()}
</div>
</div>
</Pane>
@@ -327,7 +381,7 @@
class={twMerge('border rounded-md', panelButtonWidth > 0 ? 'rounded-tl-none' : '')}
>
{#if editTab !== 'inputEditor'}
<slot name="extraTab" />
{@render extraTab?.()}
{:else}
<!-- WIP -->
{#if jsonEnabled && customUi?.jsonOnly != true}
@@ -357,14 +411,14 @@
{#if keys.length > 0}
{#each keys as argName, i (argName)}
<div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={twMerge(
'w-full flex bg-gray-50 dark:bg-gray-800 px-4 py-1 justify-between items-center hover:bg-gray-100 cursor-pointer',
opened === argName ? 'bg-gray-100 hover:bg-gray-200' : ''
)}
on:click={() => {
onclick={() => {
if (opened === argName) {
opened = undefined
} else {
@@ -375,9 +429,9 @@
<div class="flex flex-row gap-2">
{argName}
{#if !uiOnly}
<div on:click|stopPropagation|preventDefault>
<div onclick={stopPropagation(preventDefault(bubble('click')))}>
<Popover placement="bottom-end" containerClasses="p-4" closeButton>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button
color="light"
size="xs2"
@@ -385,8 +439,8 @@
startIcon={{ icon: Pen }}
iconOnly
/>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<Label label="Name" class="p-4">
<div class="flex flex-col gap-2">
<input
@@ -394,7 +448,7 @@
class="w-full !bg-surface"
value={argName}
id={argName + i}
on:keydown={(event) => {
onkeydown={(event) => {
if (event.key === 'Enter') {
renameProperty(argName, argName + i)
close()
@@ -414,7 +468,7 @@
</Button>
</div>
</Label>
</svelte:fragment>
{/snippet}
</Popover>
</div>
{/if}
@@ -429,7 +483,7 @@
class="rounded-full p-1 text-gray-500 bg-white
duration-200 hover:bg-gray-600 focus:bg-gray-600 hover:text-white dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800"
aria-label="Clear"
on:click={() => {
onclick={() => {
dispatch('delete', argName)
}}
>
@@ -461,103 +515,103 @@
{isFlowInput}
{isAppInput}
on:change={() => {
schema = schema
schema = $state.snapshot(schema)
dispatch('change', schema)
}}
>
<svelte:fragment slot="typeeditor">
{#snippet typeeditor()}
{#if isFlowInput || isAppInput}
<Label label="Type">
<ToggleButtonGroup
tabListClass="flex-wrap"
class="h-auto"
let:item
bind:selected
bind:selected={
() => computeSelected(schema.properties[opened ?? '']),
(v) => {
const isS3 = v == 'S3'
const isOneOf = v == 'oneOf'
const emptyProperty = {
contentEncoding: undefined,
enum_: undefined,
pattern: undefined,
default: undefined,
min: undefined,
max: undefined,
currency: undefined,
currencyLocale: undefined,
multiselect: undefined,
password: undefined,
dateFormat: undefined,
...(v == 'array' ? { items: { type: 'string' } } : {}),
showExpr: undefined,
nullable: undefined,
required: undefined
}
if (isS3) {
schema.properties[argName] = {
...emptyProperty,
type: 'object',
format: 'resource-s3_object'
}
} else if (isOneOf) {
schema.properties[argName] = {
...emptyProperty,
type: 'object',
oneOf: [
{
title: 'Option 1',
type: 'object',
properties: {
label: {
type: 'string',
enum: ['Option 1']
},
property_1: {
type: 'string'
}
}
},
{
title: 'Option 2',
type: 'object',
properties: {
label: {
type: 'string',
enum: ['Option 2']
},
property_2: {
type: 'string'
}
}
}
]
}
} else {
schema.properties[argName] = {
...emptyProperty,
format: undefined,
type: v
}
}
}
}
on:selected={(e) => {
const isS3 = e.detail == 'S3'
const isOneOf = e.detail == 'oneOf'
const emptyProperty = {
contentEncoding: undefined,
enum_: undefined,
pattern: undefined,
default: undefined,
min: undefined,
max: undefined,
currency: undefined,
currencyLocale: undefined,
multiselect: undefined,
password: undefined,
dateFormat: undefined,
...(e.detail == 'array'
? { items: { type: 'string' } }
: {}),
showExpr: undefined,
nullable: undefined,
required: undefined
}
if (isS3) {
schema.properties[argName] = {
...emptyProperty,
type: 'object',
format: 'resource-s3_object'
}
} else if (isOneOf) {
schema.properties[argName] = {
...emptyProperty,
type: 'object',
oneOf: [
{
title: 'Option 1',
type: 'object',
properties: {
label: {
type: 'string',
enum: ['Option 1']
},
property_1: {
type: 'string'
}
}
},
{
title: 'Option 2',
type: 'object',
properties: {
label: {
type: 'string',
enum: ['Option 2']
},
property_2: {
type: 'string'
}
}
}
]
}
} else {
schema.properties[argName] = {
...emptyProperty,
format: undefined,
type: e.detail
}
}
// No better solution than this, needs future rework
setTimeout(() => {
schema = schema
dispatch('change', schema)
}, 100)
schema = schema
dispatch('change', schema)
dispatch('schemaChange')
}}
>
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
<ToggleButton value={x[1]} label={x[0]} {item} />
{/each}
{#snippet children({ item })}
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
<ToggleButton value={x[1]} label={x[0]} {item} />
{/each}
{/snippet}
</ToggleButtonGroup>
</Label>
{/if}
</svelte:fragment>
{/snippet}
{#if isFlowInput || isAppInput}
<FlowPropertyEditor
@@ -594,7 +648,7 @@
dispatch('change', schema)
}}
on:schemaChange={(e) => {
schema = schema
schema = $state.snapshot(schema)
dispatch('change', schema)
dispatch('schemaChange')
}}
@@ -651,7 +705,9 @@
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
args[pickForField] = '$var:' + path
if (args) {
args[pickForField] = '$var:' + path
}
}
}}
itemName="Variable"
@@ -664,17 +720,19 @@
...x
}))}
>
<div slot="submission">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => variableEditor?.initNew?.()}
>
New variable
</Button>
</div>
{#snippet submission()}
<div>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => variableEditor?.initNew?.()}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<VariableEditor bind:this={variableEditor} />
+15 -10
View File
@@ -206,7 +206,7 @@
let websocketInterval: NodeJS.Timeout | undefined
let lastWsAttempt: Date = new Date()
let nbWsAttempt = 0
let disposeMethod: () => void | undefined
let disposeMethod: (() => void) | undefined
const dispatch = createEventDispatcher()
let graphqlService: MonacoGraphQLAPI | undefined = undefined
@@ -1267,15 +1267,20 @@
onFileChanges()
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
fontSize: !small ? 14 : 12,
lineNumbersMinChars,
// overflowWidgetsDomNode: widgets,
tabSize: lang == 'python' ? 4 : 2,
folding
})
try {
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
fontSize: !small ? 14 : 12,
lineNumbersMinChars,
// overflowWidgetsDomNode: widgets,
tabSize: lang == 'python' ? 4 : 2,
folding
})
} catch (e) {
console.error('Error loading monaco:', e)
return
}
keepModelAroundToAvoidDisposalOfWorkers()
+174 -129
View File
@@ -1,8 +1,10 @@
<script context="module" lang="ts">
<script module lang="ts">
export const EDITOR_BAR_WIDTH_THRESHOLD = 1044
</script>
<script lang="ts">
import { run } from 'svelte/legacy'
import { ResourceService, VariableService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -19,7 +21,7 @@
import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import { SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS } from '$lib/consts'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { sendUserToast } from '$lib/toast'
import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
import Toggle from './Toggle.svelte'
@@ -47,110 +49,145 @@
import type { EditorBarUi } from './custom_ui'
import EditorSettings from './EditorSettings.svelte'
export let lang: SupportedLanguage | 'bunnative' | undefined
export let editor: Editor | undefined
export let websocketAlive: {
pyright: boolean
ruff: boolean
deno: boolean
go: boolean
shellcheck: boolean
interface Props {
lang: SupportedLanguage | 'bunnative' | undefined
editor: Editor | undefined
websocketAlive: {
pyright: boolean
ruff: boolean
deno: boolean
go: boolean
shellcheck: boolean
}
iconOnly?: boolean
validCode?: boolean
kind?: 'script' | 'trigger' | 'approval'
template?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative'
collabMode?: boolean
collabLive?: boolean
collabUsers?: { name: string }[]
scriptPath?: string | undefined
diffEditor?: DiffEditor | undefined
args: Record<string, any>
noHistory?: boolean
saveToWorkspace?: boolean
customUi?: EditorBarUi
lastDeployedCode?: string | undefined
diffMode?: boolean
showHistoryDrawer?: boolean
right?: import('svelte').Snippet
}
export let iconOnly: boolean = false
export let validCode: boolean = true
export let kind: 'script' | 'trigger' | 'approval' = 'script'
export let template: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' =
'script'
export let collabMode = false
export let collabLive = false
export let collabUsers: { name: string }[] = []
export let scriptPath: string | undefined = undefined
export let diffEditor: DiffEditor | undefined = undefined
export let args: Record<string, any>
export let noHistory = false
export let saveToWorkspace = false
export let customUi: EditorBarUi = {}
export let lastDeployedCode: string | undefined = undefined
export let diffMode: boolean = false
export let showHistoryDrawer: boolean = false
let contextualVariablePicker: ItemPicker
let variablePicker: ItemPicker
let resourcePicker: ItemPicker
let resourceTypePicker: ItemPicker
let variableEditor: VariableEditor
let resourceEditor: ResourceEditorDrawer
let showContextVarPicker = false
let showVarPicker = false
let showResourcePicker = false
let showResourceTypePicker = false
let {
lang,
editor,
websocketAlive,
iconOnly = false,
validCode = true,
kind = 'script',
template = 'script',
collabMode = false,
collabLive = false,
collabUsers = [],
scriptPath = undefined,
diffEditor = undefined,
args,
noHistory = false,
saveToWorkspace = false,
customUi = {},
lastDeployedCode = undefined,
diffMode = false,
showHistoryDrawer = $bindable(false),
right
}: Props = $props()
$: showContextVarPicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
$: showVarPicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
$: showResourcePicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
$: showResourceTypePicker =
['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) ||
lang === 'python3' ||
lang === 'php'
let contextualVariablePicker: ItemPicker | undefined = $state()
let variablePicker: ItemPicker | undefined = $state()
let resourcePicker: ItemPicker | undefined = $state()
let resourceTypePicker: ItemPicker | undefined = $state()
let variableEditor: VariableEditor | undefined = $state()
let resourceEditor: ResourceEditorDrawer | undefined = $state()
let showContextVarPicker = $state(false)
let showVarPicker = $state(false)
let showResourcePicker = $state(false)
let showResourceTypePicker = $state(false)
let codeViewer: Drawer
let codeObj: { language: SupportedLanguage; content: string } | undefined = undefined
run(() => {
showContextVarPicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
})
run(() => {
showVarPicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
})
run(() => {
showResourcePicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
})
run(() => {
showResourceTypePicker =
['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) ||
lang === 'python3' ||
lang === 'php'
})
let codeViewer: Drawer | undefined = $state()
let codeObj: { language: SupportedLanguage; content: string } | undefined = $state(undefined)
function addEditorActions() {
editor?.addAction('insert-variable', 'Windmill: Insert variable', () => {
variablePicker.openDrawer()
variablePicker?.openDrawer()
})
editor?.addAction('insert-resource', 'Windmill: Insert resource', () => {
resourcePicker.openDrawer()
resourcePicker?.openDrawer()
})
}
$: editor && addEditorActions()
run(() => {
editor && untrack(() => addEditorActions())
})
async function loadVariables() {
return await VariableService.listVariable({ workspace: $workspaceStore ?? '' })
@@ -162,9 +199,9 @@
})
}
let scriptPicker: Drawer
let pick_existing: 'hub' | 'workspace' = 'hub'
let filter = ''
let scriptPicker: Drawer | undefined = $state()
let pick_existing: 'hub' | 'workspace' = $state('hub')
let filter = $state('')
async function onScriptPick(e: { detail: { path: string } }) {
codeObj = undefined
@@ -462,21 +499,23 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
itemName="Variable"
extraField="path"
loadItems={loadVariables}
buttons={{ 'Edit/View': (x) => variableEditor.editVariable(x) }}
buttons={{ 'Edit/View': (x) => variableEditor?.editVariable(x) }}
>
<div slot="submission" class="flex flex-row">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor.initNew()
}}
>
New variable
</Button>
</div>
{#snippet submission()}
<div class="flex flex-row">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew()
}}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<ItemPicker
@@ -548,24 +587,26 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
tooltip="Resources represent connections to third party systems. Resources are a good way to define a connection to a frequently used third party system such as a database."
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
itemName="Resource"
buttons={{ 'Edit/View': (x) => resourceEditor.initEdit(x) }}
buttons={{ 'Edit/View': (x) => resourceEditor?.initEdit(x) }}
extraField="description"
extraField2="resource_type"
loadItems={async () =>
await ResourceService.listResource({ workspace: $workspaceStore ?? 'NO_W' })}
>
<div slot="submission" class="flex flex-row gap-x-1 mr-2">
<Button
startIcon={{ icon: Plus }}
target="_blank"
variant="border"
color="blue"
size="sm"
href="{base}/resources?connect_app=undefined"
>
Add resource
</Button>
</div>
{#snippet submission()}
<div class="flex flex-row gap-x-1 mr-2">
<Button
startIcon={{ icon: Plus }}
target="_blank"
variant="border"
color="blue"
size="sm"
href="{base}/resources?connect_app=undefined"
>
Add resource
</Button>
</div>
{/snippet}
</ItemPicker>
{#if showResourceTypePicker}
@@ -650,7 +691,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
size="xs"
spacingSize="md"
color="light"
on:click={resourceTypePicker.openDrawer}
on:click={() => resourceTypePicker?.openDrawer()}
{iconOnly}
startIcon={{ icon: Package }}
>
@@ -724,7 +765,9 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
}}
/>
<Popover>
<svelte:fragment slot="text">Toggle diff mode</svelte:fragment>
{#snippet text()}
Toggle diff mode
{/snippet}
<DiffIcon class="ml-1 text-tertiary" size={14} />
</Popover>
</div>
@@ -739,14 +782,16 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
on:change={() => dispatch('toggleCollabMode')}
/>
<Popover>
<svelte:fragment slot="text">Multiplayer</svelte:fragment>
{#snippet text()}
Multiplayer
{/snippet}
<Users class="ml-1 text-tertiary" size={14} />
</Popover>
{#if collabLive}
<button
title="Show invite link"
class="p-1 rounded hover:bg-gray-400 mx-1 border"
on:click={() => dispatch('collabPopup')}><Link size={14} /></button
onclick={() => dispatch('collabPopup')}><Link size={14} /></button
>
<div class="isolate flex -space-x-2 pl-2">
{#each collabUsers as user}
@@ -773,7 +818,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
</div>
<div class="flex flex-row items-center gap-2">
<slot name="right" />
{@render right?.()}
{#if scriptPath && !noHistory}
<Button
btnClasses="!font-medium text-tertiary"
@@ -329,7 +329,7 @@
schema={customHandlerSchema}
bind:args={handlerExtraArgs}
shouldHideNoInputs
class="text-xs"
className="text-xs"
/>
{/await}
{#if customHandlerSchema && customHandlerSchema.properties && Object.keys(customHandlerSchema.properties).length === 0}
@@ -368,7 +368,7 @@
}}
bind:args={handlerExtraArgs}
shouldHideNoInputs
class="text-xs"
className="text-xs"
/>
{/await}
{:else if workspaceConnectedToSlack == undefined}
@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, getContext } from 'svelte'
import { getFirstStepSchema } from '$lib/components/flows/flowStore'
import { createEventDispatcher, onDestroy, getContext, untrack } from 'svelte'
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'
@@ -13,13 +13,13 @@
const { flowStore, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher()
let schema: Record<string, any> | undefined = undefined
let schema: Record<string, any> | undefined = $state(undefined)
let error: string | undefined = undefined
let mod: any | undefined = undefined
let error: string | undefined = $state(undefined)
let mod: any | undefined = $state(undefined)
async function loadSchema() {
try {
const res = await getFirstStepSchema($flowStateStore, flowStore)
const res = await getFirstStepSchema($flowStateStore, flowStore.val)
schema = res.schema
mod = res.mod
dispatch('connectFirstNode', { connectFirstNode: res.connectFirstNode })
@@ -27,7 +27,9 @@
error = e
}
}
$: $flowStore && $flowStateStore && loadSchema()
$effect(() => {
flowStore.val && $flowStateStore && untrack(() => loadSchema())
})
function handleClick() {
selected = !selected
@@ -39,7 +41,7 @@
dispatch('select', undefined)
})
let selected: boolean = false
let selected: boolean = $state(false)
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && selected) {
@@ -50,14 +52,16 @@
}
}
let firstUpdate = true
$: if (schema && !selected && firstUpdate) {
firstUpdate = false
setTimeout(() => {
selected = true
dispatch('select', schema)
}, 200)
}
let firstUpdate = $state(true)
$effect(() => {
if (schema && !selected && firstUpdate) {
firstUpdate = false
setTimeout(() => {
selected = true
dispatch('select', schema)
}, 200)
}
})
export function resetSelected(dispatchEvent?: boolean) {
selected = false
@@ -67,7 +71,7 @@
}
</script>
<svelte:window on:keydown={handleKeydown} />
<svelte:window onkeydown={handleKeydown} />
<div class="h-full">
{#if schema && mod}
@@ -77,10 +81,10 @@
selected ? 'bg-surface-selected' : 'hover:bg-surface-hover'
)}
disabled={!schema}
on:click={handleClick}
onclick={handleClick}
>
<FlowModuleSchemaItemViewer
on:click={handleClick}
onclick={handleClick}
deletable={false}
id={mod.id}
label={mod.summary ||
@@ -90,24 +94,26 @@
: 'To be defined')}
path={`path` in mod.value && mod.summary ? mod.value.path : ''}
>
<div slot="icon">
{#if mod.value.type === 'rawscript'}
<LanguageIcon lang={mod.value.language} width={16} height={16} />
{:else if mod.value.type === 'script'}
{#if mod.value.path.startsWith('hub/')}
<div>
<IconedResourceType
width="20px"
height="20px"
name={mod.value.path.split('/')[2]}
silent={true}
/>
</div>
{:else}
<Building size={14} />
{#snippet icon()}
<div>
{#if mod.value.type === 'rawscript'}
<LanguageIcon lang={mod.value.language} width={16} height={16} />
{:else if mod.value.type === 'script'}
{#if mod.value.path.startsWith('hub/')}
<div>
<IconedResourceType
width="20px"
height="20px"
name={mod.value.path.split('/')[2]}
silent={true}
/>
</div>
{:else}
<Building size={14} />
{/if}
{/if}
{/if}
</div>
</div>
{/snippet}
</FlowModuleSchemaItemViewer>
</button>
{:else}
+140 -95
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import {
FlowService,
type Flow,
type FlowModule,
DraftService,
type PathScript,
type OpenFlow,
@@ -23,14 +24,16 @@
encodeState,
generateRandomString,
orderedJsonStringify,
readFieldsRecursively,
replaceFalseWithUndefined,
type StateStore,
type Value
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { Drawer } from '$lib/components/common'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import { onMount, setContext, type ComponentType } from 'svelte'
import { onMount, setContext, untrack, type ComponentType } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Badge, Button, UndoRedo } from './common'
@@ -48,7 +51,7 @@
import { getAllModules } from './flows/flowExplorer'
import { type FlowCopilotContext } from './copilot/flow'
import FlowAIButton from './copilot/chat/flow/FlowAIButton.svelte'
import { loadFlowModuleState } from './flows/flowStateUtils'
import { loadFlowModuleState } from './flows/flowStateUtils.svelte'
import FlowBuilderTutorials from './FlowBuilderTutorials.svelte'
import Dropdown from '$lib/components/DropdownV2.svelte'
import FlowTutorials from './FlowTutorials.svelte'
@@ -71,27 +74,51 @@
import { Triggers } from './triggers/triggers.svelte'
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
export let initialPath: string = ''
export let pathStoreInit: string | undefined = undefined
export let newFlow: boolean
export let selectedId: string | undefined
export let initialArgs: Record<string, any> = {}
export let loading = false
export let flowStore: Writable<OpenFlow>
export let flowStateStore: Writable<FlowState>
export let savedFlow: FlowWithDraftAndDraftTriggers | undefined = undefined
export let diffDrawer: DiffDrawer | undefined = undefined
export let customUi: FlowBuilderWhitelabelCustomUi = {}
export let disableAi: boolean = false
export let disabledFlowInputs = false
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined // used to set the primary schedule in the legacy primaryScheduleStore
export let version: number | undefined = undefined
export let setSavedraftCb: ((cb: () => void) => void) | undefined = undefined
export let draftTriggersFromUrl: Trigger[] | undefined = undefined
export let selectedTriggerIndexFromUrl: number | undefined = undefined
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
}
let {
initialPath = $bindable(''),
pathStoreInit = undefined,
newFlow,
selectedId,
initialArgs = {},
loading = false,
flowStore,
flowStateStore,
savedFlow = $bindable(undefined),
diffDrawer = undefined,
customUi = {},
disableAi = false,
disabledFlowInputs = false,
savedPrimarySchedule = undefined,
version = undefined,
setSavedraftCb = undefined,
draftTriggersFromUrl = undefined,
selectedTriggerIndexFromUrl = undefined,
children
}: Props = $props()
let initialPathStore = writable(initialPath)
$: initialPathStore.set(initialPath)
// used for new flows for captures
let fakeInitialPath =
@@ -103,13 +130,13 @@
generateRandomString(12)
// Used by multiplayer deploy collision warning
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
let deployedValue: Value | undefined = $state(undefined) // Value to diff against
let deployedBy: string | undefined = $state(undefined) // Author
let confirmCallback: () => void = $state(() => {}) // What happens when user clicks `override` in warning
let open: boolean = $state(false) // Is confirmation modal open
// Draft triggers confirmation modal
let draftTriggersModalOpen = false
let draftTriggersModalOpen = $state(false)
let confirmDeploymentCallback: (triggersToDeploy: Trigger[]) => void = () => {}
async function handleDraftTriggersConfirmed(event: CustomEvent<{ selectedTriggers: Trigger[] }>) {
@@ -119,13 +146,11 @@
confirmDeploymentCallback(selectedTriggers)
}
$: setContext('customUi', customUi)
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
return {
savedValue: savedFlow,
modifiedValue: {
...$flowStore,
...flowStore.val,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
}
}
@@ -175,8 +200,8 @@
triggersState.selectedTriggerIndex = index
}
let loadingSave = false
let loadingDraft = false
let loadingSave = $state(false)
let loadingDraft = $state(false)
export async function saveDraft(forceSave = false): Promise<void> {
if (!newFlow && !savedFlow) {
@@ -185,11 +210,13 @@
if (savedFlow) {
const draftOrDeployed = cleanValueProperties(savedFlow.draft || savedFlow)
const currentDraftTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
const current = cleanValueProperties({
...$flowStore,
path: $pathStore,
draft_triggers: currentDraftTriggers
})
const current = cleanValueProperties(
$state.snapshot({
...flowStore.val,
path: $pathStore,
draft_triggers: currentDraftTriggers
})
)
if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
sendUserToast('No changes detected, ignoring', false, [
{
@@ -204,7 +231,7 @@
}
loadingDraft = true
try {
const flow = cleanInputs($flowStore)
const flow = cleanInputs(flowStore.val)
try {
localStorage.removeItem('flow')
localStorage.removeItem(`flow-${$pathStore}`)
@@ -261,13 +288,13 @@
savedFlow = {
...(newFlow || savedFlow?.draft_only
? {
...structuredClone($flowStore),
...structuredClone($state.snapshot(flowStore.val)),
path: $pathStore,
draft_only: true
}
: savedFlow),
draft: {
...structuredClone($flowStore),
...structuredClone($state.snapshot(flowStore.val)),
path: $pathStore,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
}
@@ -314,9 +341,9 @@
if (
deployedValue &&
$flowStore &&
flowStore.val &&
orderedJsonStringify(deployedValue) ===
orderedJsonStringify(replaceFalseWithUndefined({ ...$flowStore, path: $pathStore }))
orderedJsonStringify(replaceFalseWithUndefined({ ...flowStore.val, path: $pathStore }))
) {
await saveFlow(deploymentMsg)
} else {
@@ -359,7 +386,7 @@
loadingSave = true
try {
const flow = cleanInputs($flowStore)
const flow = cleanInputs(flowStore.val)
// console.log('flow', computeUnlockedSteps(flow)) // del
// loadingSave = false // del
// return
@@ -441,11 +468,11 @@
})
}
const { draft_triggers: _, ...newSavedFlow } = $flowStore as OpenFlow & {
const { draft_triggers: _, ...newSavedFlow } = flowStore.val as OpenFlow & {
draft_triggers: Trigger[]
}
savedFlow = {
...structuredClone(newSavedFlow),
...structuredClone($state.snapshot(newSavedFlow)),
path: $pathStore
} as Flow
setDraftTriggers([])
@@ -453,19 +480,13 @@
dispatch('deploy', $pathStore)
} catch (err) {
dispatch('deployError', err)
sendUserToast(`The flow could not be saved: ${err.body}`, true)
sendUserToast(`The flow could not be saved: ${err.body ?? err}`, true)
loadingSave = false
}
}
let timeout: NodeJS.Timeout | undefined = undefined
$: {
if ($flowStore || $selectedIdStore) {
saveSessionDraft()
}
}
function saveSessionDraft() {
timeout && clearTimeout(timeout)
timeout = setTimeout(() => {
@@ -473,7 +494,7 @@
localStorage.setItem(
initialPath && initialPath != '' ? `flow-${initialPath}` : 'flow',
encodeState({
flow: $flowStore,
flow: flowStore.val,
path: $pathStore,
selectedId: $selectedIdStore,
draft_triggers: triggersState.getDraftTriggersSnapshot(),
@@ -492,10 +513,10 @@
return $selectedIdStore
}
const previewArgsStore = writable<Record<string, any>>(initialArgs)
const previewArgsStore = $state({ val: initialArgs })
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
const history = initHistory($flowStore)
const moving = writable<{ id: string } | undefined>(undefined)
const history = initHistory(flowStore.val)
const pathStore = writable<string>(pathStoreInit ?? initialPath)
const captureOn = writable<boolean>(false)
const showCaptureHint = writable<boolean | undefined>(undefined)
@@ -504,7 +525,6 @@
editPanelSize: 0,
payloadData: undefined
})
$: initialPath && ($pathStore = initialPath)
const testStepStore = writable<Record<string, any>>({})
@@ -536,14 +556,16 @@
})
// Add triggers context store
const triggersState = new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'email', path: '', isDraft: false },
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
],
selectedTriggerIndexFromUrl,
saveSessionDraft
const triggersState = $state(
new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'email', path: '', isDraft: false },
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
],
selectedTriggerIndexFromUrl,
saveSessionDraft
)
)
setContext<TriggerContext>('TriggerContext', {
@@ -575,10 +597,6 @@
}
}
$: selectedId && select(selectedId)
$: initialPath && initialPath != '' && $workspaceStore && loadTriggers()
function onKeyDown(event: KeyboardEvent) {
let classes = event.target?.['className']
if (
@@ -591,13 +609,13 @@
switch (event.key) {
case 'Z':
if (event.ctrlKey || event.metaKey) {
$flowStore = redo(history)
flowStore.val = redo(history)
event.preventDefault()
}
break
case 'z':
if (event.ctrlKey || event.metaKey) {
$flowStore = undo(history, $flowStore)
flowStore.val = undo(history, flowStore.val)
$selectedIdStore = 'Input'
event.preventDefault()
}
@@ -638,7 +656,7 @@
'settings-metadata',
'constants',
'preprocessor',
...dfsApply($flowStore.value.modules, (module) => module.id)
...dfsApply(flowStore.val.value.modules, (module) => module.id)
]
}
@@ -663,7 +681,7 @@
}
}
let flowCopilotContext: FlowCopilotContext = {
let flowCopilotContext: FlowCopilotContext = $state({
shouldUpdatePropertyType: writable<{
[key: string]: 'static' | 'javascript' | undefined
}>({}),
@@ -674,16 +692,16 @@
[key: string]: string | undefined
}>({}),
stepInputsLoading: writable<boolean>(false)
}
})
setContext('FlowCopilotContext', flowCopilotContext)
let renderCount = 0
let flowTutorials: FlowTutorials | undefined = undefined
let renderCount = $state(0)
let flowTutorials: FlowTutorials | undefined = $state(undefined)
let jsonViewerDrawer: Drawer | undefined = undefined
let yamlEditorDrawer: Drawer | undefined = undefined
let flowHistory: FlowHistory | undefined = undefined
let jsonViewerDrawer: Drawer | undefined = $state(undefined)
let yamlEditorDrawer: Drawer | undefined = $state(undefined)
let flowHistory: FlowHistory | undefined = $state(undefined)
export function triggerTutorial() {
const urlParams = new URLSearchParams(window.location.search)
@@ -701,9 +719,7 @@
icon: ComponentType<Icon>
action: () => void
disabled?: boolean
}[] = []
$: onCustomUiChange(customUi)
}[] = $state([])
function onCustomUiChange(customUi: FlowBuilderWhitelabelCustomUi | undefined) {
moreItems = [
@@ -748,12 +764,37 @@
}
}
let flowPreviewButtons: FlowPreviewButtons
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
run(() => {
initialPathStore.set(initialPath)
})
run(() => {
setContext('customUi', customUi)
})
run(() => {
if (flowStore.val || $selectedIdStore) {
readFieldsRecursively(flowStore.val)
untrack(() => saveSessionDraft())
}
})
run(() => {
initialPath && ($pathStore = initialPath)
})
run(() => {
selectedId && untrack(() => select(selectedId))
})
run(() => {
initialPath && initialPath != '' && $workspaceStore && untrack(() => loadTriggers())
})
run(() => {
customUi && untrack(() => onCustomUiChange(customUi))
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<slot />
{@render children?.()}
<DeployOverrideConfirmationModal
bind:deployedBy
@@ -761,7 +802,7 @@
bind:open
{diffDrawer}
bind:deployedValue
currentValue={$flowStore}
currentValue={flowStore.val}
/>
<DraftTriggersConfirmationModal
@@ -791,18 +832,18 @@
<div class="flex w-full max-w-md gap-4 items-center">
<Summary
disabled={customUi?.topBar?.editableSummary == false}
bind:value={$flowStore.summary}
bind:value={flowStore.val.summary}
/>
<UndoRedo
undoProps={{ disabled: $history.index === 0 }}
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
on:undo={() => {
const currentModules = $flowStore?.value?.modules
const currentModules = flowStore.val?.value?.modules
$flowStore = undo(history, $flowStore)
flowStore.val = undo(history, flowStore.val)
const newModules = $flowStore?.value?.modules
const newModules = flowStore.val?.value?.modules
const restoredModules = newModules?.filter(
(node) => !currentModules?.some((currentNode) => currentNode?.id === node?.id)
)
@@ -820,7 +861,7 @@
$selectedIdStore = 'Input'
}}
on:redo={() => {
$flowStore = redo(history)
flowStore.val = redo(history)
}}
/>
</div>
@@ -853,7 +894,7 @@
<div class="flex justify-start w-full">
<div>
<button
on:click={async () => {
onclick={async () => {
select('settings-metadata')
document.getElementById('path')?.focus()
}}
@@ -871,7 +912,7 @@
readonly
value={$pathStore && $pathStore != '' ? $pathStore : 'Choose a path'}
class="font-mono !text-xs !min-w-[96px] !max-w-[300px] !w-full !h-[28px] !my-0 !py-0 !border-l-0 !rounded-l-none"
on:focus={({ currentTarget }) => {
onfocus={({ currentTarget }) => {
currentTarget.select()
}}
/>
@@ -915,7 +956,11 @@
mode: 'normal',
deployed: deployedValue ?? savedFlow,
draft: savedFlow?.draft,
current: { ...$flowStore, path: $pathStore, draft_triggers: currentDraftTriggers }
current: {
...flowStore.val,
path: $pathStore,
draft_triggers: currentDraftTriggers
}
})
}}
disabled={!savedFlow}
@@ -980,7 +1025,7 @@
}
}}
on:testWithArgs={(e) => {
$previewArgsStore = JSON.parse(JSON.stringify(e.detail))
previewArgsStore.val = JSON.parse(JSON.stringify(e.detail))
flowPreviewButtons?.openPreview(true)
}}
{savedFlow}
@@ -45,8 +45,7 @@
modules={flow?.value?.modules}
failureModule={flow?.value?.failure_module}
preprocessorModule={flow?.value?.preprocessor_module}
on:select={(e) => {
let nodeId = e?.detail
onSelect={(nodeId) => {
if (nodeId === 'triggers') {
dispatch('triggerDetail')
return
@@ -55,7 +54,7 @@
} else if (nodeId === 'preprocessor') {
stepDetail = flow?.value?.preprocessor_module
} else {
stepDetail = dfs(flow?.value?.modules ?? [], (m) => m).find((m) => m?.id === e?.detail)
stepDetail = dfs(flow?.value?.modules ?? [], (m) => m).find((m) => m?.id === nodeId)
}
stepDetail = stepDetail ?? nodeId
dispatch('select', stepDetail)
@@ -11,13 +11,23 @@
import { CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
import type { Schema } from '$lib/common'
export let open: boolean
interface Props {
open: boolean
jobId?: string | undefined
job?: Job | undefined
modules: FlowModule[]
previewArgs?: Record<string, any>
whileLoop?: boolean
}
export let jobId: string | undefined = undefined
export let job: Job | undefined = undefined
export let modules: FlowModule[]
export let previewArgs: Record<string, any> = {}
export let whileLoop = false
let {
open,
jobId = $bindable(undefined),
job = $bindable(undefined),
modules,
previewArgs = $bindable({}),
whileLoop = false
}: Props = $props()
export const forloopSchema: Schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema' as string | undefined,
@@ -54,10 +64,10 @@
type: 'object'
}
let selectedJobStep: string | undefined = undefined
let selectedJobStep: string | undefined = $state(undefined)
let isRunning: boolean = false
let jobProgressReset: () => void
let isRunning: boolean = $state(false)
let jobProgressReset: (() => void) | undefined = $state(undefined)
export function test() {
runPreview(previewArgs, undefined)
@@ -70,7 +80,7 @@
args: Record<string, any>,
restartedFrom: RestartedFrom | undefined
) {
jobProgressReset()
jobProgressReset?.()
const newFlow = { value: { modules }, summary: '' }
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
isRunning = true
@@ -89,12 +99,14 @@
}
}
$: if (job?.type === 'CompletedJob') {
isRunning = false
}
$effect(() => {
if (job?.type === 'CompletedJob') {
isRunning = false
}
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<div class="flex flex-col space-y-2 h-screen bg-surface px-6 py-2 w-full" id="flow-preview-content">
<div class="flex flex-row justify-between w-full items-center gap-x-2">
@@ -156,7 +168,7 @@
<SchemaForm
noVariablePicker
compact
class="py-4 max-w-3xl"
className="py-4 max-w-3xl"
schema={whileLoop ? whileLoopSchema : forloopSchema}
bind:args={previewArgs}
/>
@@ -13,7 +13,7 @@
import { AlertTriangle, ArrowRight, CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
import { emptyString, sendUserToast } from '$lib/utils'
import { dfs } from './flows/dfs'
import { sliceModules } from './flows/flowStateUtils'
import { sliceModules } from './flows/flowStateUtils.svelte'
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import Toggle from './Toggle.svelte'
import JsonInputs from './JsonInputs.svelte'
@@ -53,7 +53,7 @@
export function test() {
renderCount++
runPreview($previewArgs, undefined)
runPreview(previewArgs.val, undefined)
}
const {
@@ -75,9 +75,9 @@
function extractFlow(previewMode: 'upTo' | 'whole'): OpenFlow {
if (previewMode === 'whole') {
return $flowStore
return flowStore.val
} else {
const flow: Flow = JSON.parse(JSON.stringify($flowStore))
const flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
const idOrders = dfs(flow.value.modules, (x) => x.id)
let upToIndex = idOrders.indexOf($selectedId)
@@ -97,13 +97,13 @@
initial = false
}
try {
lastPreviewFlow = JSON.stringify($flowStore)
lastPreviewFlow = JSON.stringify(flowStore.val)
jobProgressReset()
const newFlow = extractFlow(previewMode)
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
isRunning = true
if (inputSelected) {
savedArgs = $previewArgs
savedArgs = previewArgs.val
inputSelected = undefined
}
} catch (e) {
@@ -120,7 +120,7 @@
case 'Enter':
if (event.ctrlKey || event.metaKey) {
event.preventDefault()
runPreview($previewArgs, undefined)
runPreview(previewArgs.val, undefined)
}
break
@@ -156,20 +156,20 @@
}
}
let savedArgs = $previewArgs
let savedArgs = previewArgs.val
let inputSelected: 'captures' | 'history' | 'saved' | undefined = undefined
async function selectInput(input, type?: 'captures' | 'history' | 'saved' | undefined) {
if (!input) {
$previewArgs = savedArgs
previewArgs.val = savedArgs
inputSelected = undefined
setTimeout(() => {
preventEscape = false
}, 100)
} else {
$previewArgs = input
previewArgs.val = input
inputSelected = type
preventEscape = true
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
}
}
@@ -185,7 +185,7 @@
async function loadIndividualStepsStates() {
// console.log('loadIndividualStepsStates')
dfs($flowStore.value.modules, async (module) => {
dfs(flowStore.val.value.modules, async (module) => {
// console.log('module', $flowStateStore[module.id], module.id)
const prev = $flowStateStore[module.id]?.previewResult
if (prev && prev != NEVER_TESTED_THIS_FAR) {
@@ -283,7 +283,7 @@
variant="border"
title={`Re-start this flow from step ${selectedJobStep} (included).`}
on:click={() => {
runPreview($previewArgs, {
runPreview(previewArgs.val, {
flow_job_id: jobId,
step_id: selectedJobStep,
branch_or_iteration_n: 0
@@ -308,7 +308,7 @@
color="blue"
startIcon={{ icon: RefreshCw }}
on:click={() => {
runPreview($previewArgs, {
runPreview(previewArgs.val, {
flow_job_id: jobId,
step_id: selectedJobStep,
branch_or_iteration_n: 0
@@ -354,7 +354,7 @@
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Restart flow"
on:click|once={() => {
runPreview($previewArgs, {
runPreview(previewArgs.val, {
flow_job_id: jobId,
step_id: selectedJobStep,
branch_or_iteration_n: branchOrIterationN
@@ -375,7 +375,7 @@
color="dark"
size="sm"
btnClasses="w-full max-w-lg"
on:click={() => runPreview($previewArgs, undefined)}
on:click={() => runPreview(previewArgs.val, undefined)}
id="flow-editor-test-flow-drawer"
shortCut={{ Icon: CornerDownLeft }}
>
@@ -385,7 +385,7 @@
{/if}
</div>
<div class="w-full flex flex-col gap-y-1">
{#if lastPreviewFlow && JSON.stringify($flowStore) != lastPreviewFlow}
{#if lastPreviewFlow && JSON.stringify(flowStore.val) != lastPreviewFlow}
<div class="pt-1">
<div
class="bg-orange-200 text-orange-600 border border-orange-600 p-2 flex items-center gap-2 rounded"
@@ -409,7 +409,7 @@
runnableId={$initialPathStore}
stablePathForCaptures={$initialPathStore || fakeInitialPath}
runnableType={'FlowPath'}
previewArgs={$previewArgs}
previewArgs={previewArgs.val}
on:openTriggers
on:select={(e) => {
selectInput(e.detail.payload, e.detail?.type)
@@ -419,7 +419,7 @@
>
<div class="w-full flex flex-row justify-between">
<InputSelectedBadge
on:click={() => schemaFormWithArgPicker?.resetSelected()}
onReject={() => schemaFormWithArgPicker?.resetSelected()}
{inputSelected}
/>
<div class="flex flex-row gap-2">
@@ -433,7 +433,7 @@
}}
lightMode
on:change={(e) => {
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
refresh()
}}
/>
@@ -445,7 +445,7 @@
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
$previewArgs = e.detail
previewArgs.val = e.detail
}
}}
updateOnBlur={false}
@@ -458,10 +458,10 @@
<SchemaForm
noVariablePicker
compact
schema={$flowStore.schema}
bind:args={$previewArgs}
schema={flowStore.val.schema}
bind:args={previewArgs.val}
on:change={() => {
savedArgs = $previewArgs
savedArgs = previewArgs.val
}}
bind:isValid
/>
@@ -29,7 +29,7 @@
import Alert from './common/alert/Alert.svelte'
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
import { buildPrefix } from './graph/graphBuilder'
import { buildPrefix } from './graph/graphBuilder.svelte'
const dispatch = createEventDispatcher()
@@ -1253,31 +1253,29 @@
success={jobId != undefined && isSuccess(job?.['success'])}
flowModuleStates={$localModuleStates}
bind:expandedSubflows
on:select={(e) => {
onSelect={(e) => {
console.log('onSelect', e)
if (rightColumnSelect != 'node_definition') {
rightColumnSelect = 'node_status'
}
if (typeof e.detail == 'string') {
if (e.detail == 'Input') {
if (typeof e == 'string') {
if (e == 'Input') {
selectedNode = 'start'
stepDetail = undefined
} else if (e.detail == 'Result') {
} else if (e == 'Result') {
selectedNode = 'end'
stepDetail = 'end'
} else {
const mod = dfs(job?.raw_flow?.modules ?? [], (m) => m).find(
(m) => m?.id === e?.detail
)
const mod = dfs(job?.raw_flow?.modules ?? [], (m) => m).find((m) => m?.id === e)
stepDetail = mod
selectedNode = e?.detail
selectedNode = e
}
} else {
stepDetail = e.detail
selectedNode = e.detail.id
stepDetail = e
selectedNode = e.id
}
}}
on:selectedIteration={async (e) => {
let detail = e.detail
onSelectedIteration={async (detail) => {
if (detail.manuallySet) {
let rootJobId = detail.id
await tick()
+24 -14
View File
@@ -8,20 +8,30 @@
import FlowViewerInner from './FlowViewerInner.svelte'
import FlowInputViewer from './FlowInputViewer.svelte'
export let flow: {
summary: string
description?: string
value: FlowValue
schema?: any
interface Props {
flow: {
summary: string
description?: string
value: FlowValue
schema?: any
}
initialOpen?: number | undefined
noSide?: boolean
noGraph?: boolean
tab?: 'ui' | 'raw' | 'schema'
noSummary?: boolean
noGraphDownload?: boolean
}
export let initialOpen: number | undefined = undefined
export let noSide = false
export let noGraph: boolean = false
export let tab: 'ui' | 'raw' | 'schema' = noGraph ? 'schema' : 'ui'
export let noSummary = false
export let noGraphDownload = false
let {
flow,
initialOpen = undefined,
noSide = false,
noGraph = false,
tab = $bindable(noGraph ? 'schema' : 'ui'),
noSummary = false,
noGraphDownload = false
}: Props = $props()
let open: { [id: number]: boolean } = {}
if (initialOpen) {
@@ -38,7 +48,7 @@
<Tab value="raw">Raw</Tab>
<Tab value="schema">Input Schema</Tab>
<svelte:fragment slot="content">
{#snippet content()}
<TabContent value="ui">
<div class="flow-root w-full pb-4">
{#if !noSummary}
@@ -65,5 +75,5 @@
<div class="my-4"></div>
<SchemaViewer schema={flow.schema} />
</TabContent>
</svelte:fragment>
{/snippet}
</Tabs>
@@ -11,16 +11,20 @@
import HighlightTheme from './HighlightTheme.svelte'
import { filteredContentForExport } from './flows/utils'
export let flow: {
summary: string
description?: string
value: FlowValue
schema?: any
interface Props {
flow: {
summary: string
description?: string
value: FlowValue
schema?: any
}
}
$: flowFiltered = filteredContentForExport(flow)
let { flow }: Props = $props()
let rawType: 'json' | 'yaml' = 'yaml'
let flowFiltered = $derived(filteredContentForExport(flow))
let rawType: 'json' | 'yaml' = $state('yaml')
function trimStringToLines(inputString: string, maxLines: number = 100): string {
const lines = inputString?.split('\n') ?? []
@@ -29,7 +33,7 @@
return linesToKeep.join('\n')
}
let code: string = ''
let code: string = $state('')
function computeCode() {
const str =
@@ -44,11 +48,13 @@
code = str
}
let shouldDisplayLoadMore = false
let shouldDisplayLoadMore = $state(false)
$: flowFiltered && rawType && computeCode()
$effect(() => {
flowFiltered && rawType && computeCode()
})
let maxLines = 100
let maxLines = $state(100)
</script>
<HighlightTheme />
@@ -62,7 +68,7 @@
>
<Tab value="yaml">YAML</Tab>
<Tab value="json">JSON</Tab>
<svelte:fragment slot="content">
{#snippet content()}
<div class="relative pt-2">
<Button
on:click={() =>
@@ -105,7 +111,7 @@
</Button>
{/if}
</div>
</svelte:fragment>
{/snippet}
</Tabs>
</div>
+31 -35
View File
@@ -7,7 +7,6 @@
GranularAclService,
GroupService
} from '$lib/gen'
import AutoComplete from 'simple-svelte-autocomplete'
import TableCustom from './TableCustom.svelte'
import { Alert, Button, Drawer, DrawerContent } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
@@ -19,6 +18,7 @@
import Label from './Label.svelte'
import { sendUserToast } from '$lib/toast'
import { createEventDispatcher } from 'svelte'
import Select from './Select.svelte'
export let name: string
let can_write = false
@@ -184,25 +184,20 @@
</Alert>
<div class="flex items-center gap-1">
<div>
<ToggleButtonGroup
bind:selected={ownerKind}
on:selected={() => (ownerItem = '')}
let:item
>
<ToggleButton value="user" label="User" {item} />
<ToggleButton value="group" label="Group" {item} />
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (ownerItem = '')}>
{#snippet children({ item })}
<ToggleButton value="user" label="User" {item} />
<ToggleButton value="group" label="Group" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
{#key ownerKind}
<AutoComplete
required
noInputStyles
items={ownerKind === 'user'
{@const items =
ownerKind === 'user'
? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/' + x))
: groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/' + x))}
bind:selectedItem={ownerItem}
/>
<Select items={items.map((x) => ({ label: x, value: x }))} bind:value={ownerItem} />
{#if ownerKind == 'group'}
<Button
title="View Group"
@@ -278,7 +273,6 @@
<ToggleButtonGroup
disabled={owner_name == 'u/' + $userStore?.username && !$userStore?.is_admin}
selected={role}
let:item
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
@@ -315,28 +309,30 @@
loadFolder()
}}
>
<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}
/>
{#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="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}
/>
<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}
+25 -24
View File
@@ -7,7 +7,6 @@
type InstanceGroup
} from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import AutoComplete from 'simple-svelte-autocomplete'
import { createEventDispatcher } from 'svelte'
import { Button } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
@@ -18,6 +17,7 @@
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import Section from './Section.svelte'
import Label from './Label.svelte'
import Select from './Select.svelte'
export let name: string
let can_write = false
@@ -129,7 +129,7 @@
<Section label={`Members (${members?.length ?? 0})`}>
{#if can_write}
<div class="flex items-start">
<AutoComplete required noInputStyles items={usernames} bind:selectedItem={username} />
<Select items={usernames?.map((u) => ({ value: u, label: u }))} bind:value={username} />
<Button variant="contained" color="blue" size="sm" btnClasses="!ml-4" on:click={addToGroup}>
Add member
</Button>
@@ -150,7 +150,6 @@
<div>
<ToggleButtonGroup
selected={role}
let:item
on:selected={async (e) => {
const role = e.detail
// const wasInGroup = (group?.members ?? []).includes(group)
@@ -211,31 +210,33 @@
loadGroup()
}}
>
<ToggleButton
value="member"
size="xs"
label="Member"
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
{item}
/>
<ToggleButton
position="right"
value="admin"
size="xs"
label="Admin"
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
{item}
/>
{#if role === 'manager'}
{#snippet children({ item })}
<ToggleButton
value="manager"
value="member"
size="xs"
label="Manager"
tooltip="A manager of a group can manage the group, adding and removing users and
change their roles. Being a manager does not make you a member"
label="Member"
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
{item}
/>
{/if}
<ToggleButton
position="right"
value="admin"
size="xs"
label="Admin"
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
{item}
/>
{#if role === 'manager'}
<ToggleButton
value="manager"
size="xs"
label="Manager"
tooltip="A manager of a group can manage the group, adding and removing users and
change their roles. Being a manager does not make you a member"
{item}
/>
{/if}
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
@@ -8,21 +8,34 @@
import HistoricList from './HistoricList.svelte'
import { Loader2 } from 'lucide-svelte'
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let loading: boolean = false
export let selected: string | undefined = undefined
export let showAuthor = false
export let placement: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end' = 'top-end'
export let limitPayloadSize = false
export let searchArgs: Record<string, any> | undefined = undefined
interface Props {
runnableId?: string | undefined
runnableType?: RunnableType | undefined
loading?: boolean
selected?: string | undefined
showAuthor?: boolean
placement?: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end'
limitPayloadSize?: boolean
searchArgs?: Record<string, any> | undefined
}
let historicList: HistoricList | undefined = undefined
let {
runnableId = undefined,
runnableType = undefined,
loading = $bindable(false),
selected = $bindable(undefined),
showAuthor = false,
placement = 'top-end',
limitPayloadSize = false,
searchArgs = undefined
}: Props = $props()
let historicList: HistoricList | undefined = $state(undefined)
const dispatch = createEventDispatcher()
let jobs: Job[] = []
let jobs: Job[] = $state([])
let hasMoreCurrentRuns = false
let page = 1
let page = $state(1)
async function handleSelected(data: any) {
if (selected === data.id) {
@@ -34,7 +47,10 @@
const fullPayload = await data.getFullPayload?.()
dispatch('select', { args: fullPayload, jobId: data.id })
} else {
dispatch('select', { args: structuredClone(data.payloadData), jobId: data.id })
dispatch('select', {
args: structuredClone($state.snapshot(data.payloadData)),
jobId: data.id
})
}
}
@@ -77,11 +93,11 @@
} else if (runnableType === 'ScriptHash') {
return 'script,preview'
}
return 'all'
return ''
}
</script>
<svelte:window on:keydown={handleKeydown} />
<svelte:window onkeydown={handleKeydown} />
{#if runnableId}
<JobLoader
+31 -17
View File
@@ -1,21 +1,33 @@
<script lang="ts">
import { InputService, type RunnableType } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onDestroy } from 'svelte'
import { onDestroy, untrack } from 'svelte'
import InfiniteList from './InfiniteList.svelte'
import JobSchemaPicker from './schema/JobSchemaPicker.svelte'
export let runnableId: string | undefined
export let runnableType: RunnableType | undefined
export let selected: string | undefined = undefined
export let showAuthor = false
export let placement: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end' = 'bottom-start'
export let limitPayloadSize = false
export let searchArgs: Record<string, any> | undefined = undefined
interface Props {
runnableId: string | undefined
runnableType: RunnableType | undefined
selected?: string | undefined
showAuthor?: boolean
placement?: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end'
limitPayloadSize?: boolean
searchArgs?: Record<string, any> | undefined
}
let infiniteList: InfiniteList | undefined = undefined
let {
runnableId,
runnableType,
selected = undefined,
showAuthor = false,
placement = 'bottom-start',
limitPayloadSize = false,
searchArgs = undefined
}: Props = $props()
let infiniteList: InfiniteList | undefined = $state(undefined)
let loadInputsPageFn: ((page: number, perPage: number) => Promise<any>) | undefined = undefined
let viewerOpen = false
let viewerOpen = $state(false)
let openStates: Record<string, boolean> = {} // Track open state for each item
export async function refresh(clearCurrentRuns: boolean = false) {
@@ -101,18 +113,20 @@
viewerOpen = Object.values(openStates).some((state) => state)
}
$: $workspaceStore && runnableId && runnableType && infiniteList && initLoadInputs()
$effect(() => {
$workspaceStore && runnableId && runnableType && infiniteList && untrack(() => initLoadInputs())
})
</script>
<InfiniteList bind:this={infiniteList} selectedItemId={selected} on:error on:select>
<svelte:fragment slot="columns">
{#snippet columns()}
<colgroup>
<col class="w-8" />
<col class="w-16" />
<col />
</colgroup>
</svelte:fragment>
<svelte:fragment let:item let:hover>
{/snippet}
{#snippet children({ item, hover })}
<JobSchemaPicker
job={item}
hovering={hover}
@@ -125,10 +139,10 @@
}}
{limitPayloadSize}
/>
</svelte:fragment>
<svelte:fragment slot="empty">
{/snippet}
{#snippet empty()}
<div class="text-center text-tertiary text-xs py-2">
{runnableId ? 'No previous inputs' : 'Save draft to see previous runs'}
</div>
</svelte:fragment>
{/snippet}
</InfiniteList>
@@ -1,18 +1,35 @@
<script>
import { FileText } from 'lucide-svelte'
import { APP_TO_ICON_COMPONENT } from './icons'
export let name
export let silent = false
export let after = false
export let height = '24px'
export let width = '24px'
export let center = false
export let isSelected = false
export let formatExtension = undefined
/**
* @typedef {Object} Props
* @property {any} name
* @property {boolean} [silent]
* @property {boolean} [after]
* @property {string} [height]
* @property {string} [width]
* @property {boolean} [center]
* @property {boolean} [isSelected]
* @property {any} [formatExtension]
*/
$: iconComponent = name === "teams"
? APP_TO_ICON_COMPONENT.ms_teams_webhook
: APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
/** @type {Props} */
let {
name,
silent = false,
after = false,
height = '24px',
width = '24px',
center = false,
isSelected = false,
formatExtension = undefined
} = $props()
let iconComponent = $derived(
name === 'teams'
? APP_TO_ICON_COMPONENT.ms_teams_webhook
: APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
)
</script>
<div class="truncate flex flex-row gap-2 {center ? 'justify-center items-center' : ''} -pl-2">
@@ -20,12 +37,13 @@
{name}
{/if}
{#if iconComponent}
{@const SvelteComponent = iconComponent}
<span class={isSelected ? 'text-secondary' : 'text-secondary grayscale'}>
<svelte:component this={iconComponent} {height} {width} />
<SvelteComponent {height} {width} />
</span>
{:else if formatExtension}
<span class={isSelected ? 'text-secondary' : 'text-secondary grayscale'}>
<svelte:component this={FileText} {height} {width} />
<FileText {height} {width} />
</span>
{:else}
<span style="width: {width}; height: {height}" class="bg-gray-100 rounded-full"></span>
@@ -34,4 +52,3 @@
{name}
{/if}
</div>
+43 -21
View File
@@ -6,26 +6,48 @@
const dispatch = createEventDispatcher()
export let loading = false
export let items: any[] | undefined = undefined
export let selectedItemId: any | undefined = undefined
export let isEmpty: boolean = true
export let length: number = 0
export let rounded: boolean = true
export let noBorder: boolean = false
export let extraRowClasses: { bgSelected: string; bgHover: string; class: string } = {
bgSelected: '',
bgHover: '',
class: ''
interface Props {
loading?: boolean
items?: any[] | undefined
selectedItemId?: any | undefined
isEmpty?: boolean
length?: number
rounded?: boolean
noBorder?: boolean
extraRowClasses?: { bgSelected: string; bgHover: string; class: string }
neverShowLoader?: boolean
columns?: import('svelte').Snippet
extra_row?: import('svelte').Snippet<[any]>
children?: import('svelte').Snippet<[any]>
empty?: import('svelte').Snippet<[any]>
}
export let neverShowLoader = false
let {
loading = $bindable(false),
items = $bindable(undefined),
selectedItemId = $bindable(undefined),
isEmpty = $bindable(true),
length = $bindable(0),
rounded = true,
noBorder = false,
extraRowClasses = {
bgSelected: '',
bgHover: '',
class: ''
},
neverShowLoader = false,
columns,
extra_row,
children,
empty
}: Props = $props()
const perPage = 20
let hasMore = false
let hasMore = $state(false)
let page = 1
let hasAlreadyFailed = false
let hovered: any | undefined = undefined
let hovered: any | undefined = $state(undefined)
let initLoad = false
let loadInputs: ((page: number, perPage: number) => Promise<any[]>) | undefined = undefined
let deleteItemFn: ((id: any) => Promise<any>) | undefined = undefined
@@ -39,7 +61,7 @@
page = 1
}
let loadingMore = false
let loadingMore = $state(false)
export async function loadData(loadOption: 'refresh' | 'forceRefresh' | 'loadMore' = 'loadMore') {
// console.log('loadData', loadOption, length, items?.length)
@@ -146,7 +168,7 @@
{noBorder}
{neverShowLoader}
>
<slot name="columns" />
{@render columns?.()}
<tbody class="h-full w-full">
<Row
@@ -158,7 +180,7 @@
)}
on:hover={(e) => (hovered = e.detail ? 'extraRow' : undefined)}
>
<slot name="extra-row" hover={hovered === 'extraRow'} />
{@render extra_row?.({ hover: hovered === 'extraRow' })}
</Row>
{#each items ?? [] as item, index}
{@const hover = item.id === hovered}
@@ -171,16 +193,16 @@
)}
on:hover={(e) => (hovered = e.detail ? item.id : undefined)}
>
<slot {item} {hover} />
{@render children?.({ item, hover })}
</Row>
{/each}
</tbody>
<svelte:fragment slot="emptyMessage">
{#snippet emptyMessage()}
{#if (!items || items?.length === 0) && (!loading || neverShowLoader)}
<slot name="empty" {items} />
{@render empty?.({ items })}
{/if}
</svelte:fragment>
{/snippet}
</DataTable>
<style>
@@ -1,4 +1,4 @@
<script context="module">
<script module>
const dynamicTemplateRegexPairs = buildPrefixRegex([
'flow_input',
'results',
@@ -10,7 +10,7 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import type { InputCat } from '$lib/utils'
import { createEventDispatcher, getContext } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import ArgInput from './ArgInput.svelte'
import FieldHeader from './FieldHeader.svelte'
@@ -37,40 +37,61 @@
import { twMerge } from 'tailwind-merge'
import FlowPlugConnect from './FlowPlugConnect.svelte'
import { deepEqual } from 'fast-equals'
export let schema: Schema | { properties?: Record<string, any>; required?: string[] }
export let arg: InputTransform | any
export let argName: string
export let headerTooltip: string | undefined = undefined
export let headerTooltipIconClass = ''
export let HeaderTooltipIcon = InfoIcon
export let extraLib: string = 'missing extraLib'
export let inputCheck: boolean = true
export let previousModuleId: string | undefined
export let pickForField: string | undefined = undefined
export let variableEditor: VariableEditor | undefined = undefined
export let itemPicker: ItemPicker | undefined = undefined
export let noDynamicToggle = false
export let argExtra: Record<string, any> = {}
export let pickableProperties: PickableProperties | undefined = undefined
export let enableAi = false
export let hideHelpButton = false
let monaco: SimpleEditor | undefined = undefined
let monacoTemplate: TemplateEditor | undefined = undefined
let argInput: ArgInput | undefined = undefined
interface Props {
schema: Schema | { properties?: Record<string, any>; required?: string[] }
arg: InputTransform | any
argName: string
headerTooltip?: string | undefined
headerTooltipIconClass?: string
HeaderTooltipIcon?: any
extraLib?: string
inputCheck?: boolean
previousModuleId: string | undefined
pickForField?: string | undefined
variableEditor?: VariableEditor | undefined
itemPicker?: ItemPicker | undefined
noDynamicToggle?: boolean
argExtra?: Record<string, any>
pickableProperties?: PickableProperties | undefined
enableAi?: boolean
hideHelpButton?: boolean
class?: string
editor?: SimpleEditor | undefined
}
let {
schema = $bindable(),
arg = $bindable(),
argName = $bindable(),
headerTooltip = undefined,
headerTooltipIconClass = '',
HeaderTooltipIcon = InfoIcon,
extraLib = $bindable('missing extraLib'),
inputCheck = $bindable(true),
previousModuleId,
pickForField = $bindable(undefined),
variableEditor = undefined,
itemPicker = undefined,
noDynamicToggle = false,
argExtra = {},
pickableProperties = undefined,
enableAi = false,
hideHelpButton = false,
class: className = '',
editor = $bindable(undefined)
}: Props = $props()
let monaco: SimpleEditor | undefined = $state(undefined)
let monacoTemplate: TemplateEditor | undefined = $state(undefined)
let argInput: ArgInput | undefined = $state(undefined)
let focusedPrev = false
const dispatch = createEventDispatcher()
$: inputCat = computeInputCat(
schema?.properties?.[argName]?.type,
schema?.properties?.[argName]?.format,
schema?.properties?.[argName]?.items?.type,
schema?.properties?.[argName]?.enum,
schema?.properties?.[argName]?.contentEncoding
)
let propertyType = getPropertyType(arg)
$effect(() => {
editor = monaco
})
const { shouldUpdatePropertyType, exprsToSet } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
@@ -79,6 +100,18 @@
getContext<PropPickerWrapperContext>('PropPickerWrapper')
const { inputMatches, focusProp, propPickerConfig, clearFocus } = propPickerWrapperContext ?? {}
let inputCat = $derived(
computeInputCat(
schema?.properties?.[argName]?.type,
schema?.properties?.[argName]?.format,
schema?.properties?.[argName]?.items?.type,
schema?.properties?.[argName]?.enum,
schema?.properties?.[argName]?.contentEncoding
)
)
let propertyType = $state(getPropertyType(arg))
function setExpr() {
const newArg = $exprsToSet?.[argName]
if (newArg) {
@@ -98,8 +131,6 @@
})
}
$: $exprsToSet?.[argName] && setExpr()
function updatePropertyType() {
propertyType = $shouldUpdatePropertyType?.[argName] || 'static'
shouldUpdatePropertyType?.set({
@@ -108,10 +139,6 @@
})
}
$: $shouldUpdatePropertyType?.[argName] &&
arg?.type === $shouldUpdatePropertyType?.[argName] &&
updatePropertyType()
function getPropertyType(arg: InputTransform | any): 'static' | 'javascript' {
let type: 'static' | 'javascript' = arg?.type ?? 'static'
@@ -153,7 +180,7 @@
}
}
let codeInjectionDetected = false
let codeInjectionDetected = $state(false)
function checkCodeInjection(rawValue: string) {
if (!arg || !rawValue || rawValue.length < 3 || !dynamicTemplateRegexPairs) {
@@ -247,11 +274,10 @@
}
let prevArg: any = undefined
$: inputCat && propertyType && arg && onArgChange()
function onArgChange() {
const newArg = { arg, propertyType, inputCat }
if (!deepEqual(newArg, prevArg)) {
prevArg = structuredClone(newArg)
prevArg = structuredClone($state.snapshot(newArg))
updateStaticInput(inputCat, propertyType, arg)
}
}
@@ -285,23 +311,39 @@
}
focusedPrev = focused
}
$: updateFocused(focused)
$: schema?.properties?.[argName]?.default && setDefaultCode()
let resourceTypes: string[] | undefined = undefined
let resourceTypes: string[] | undefined = $state(undefined)
async function loadResourceTypes() {
resourceTypes = await getResourceTypes()
}
let focused = false
let stepInputGen: StepInputGen | undefined = undefined
let focused = $state(false)
let stepInputGen: StepInputGen | undefined = $state(undefined)
loadResourceTypes()
$: connecting =
$effect(() => {
$exprsToSet?.[argName] && untrack(() => setExpr())
})
$effect(() => {
$shouldUpdatePropertyType?.[argName] &&
arg?.type === $shouldUpdatePropertyType?.[argName] &&
untrack(() => updatePropertyType())
})
$effect(() => {
inputCat && propertyType && arg && untrack(() => onArgChange())
})
$effect(() => {
;[focused]
untrack(() => updateFocused(focused))
})
$effect(() => {
schema?.properties?.[argName]?.default && untrack(() => setDefaultCode())
})
let connecting = $derived(
$propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'
)
</script>
{#if arg != undefined}
@@ -311,7 +353,7 @@
$propPickerConfig?.propName == argName
? 'bg-surface border-l-4 border-blue-500 shadow-md rounded-l-md z-50 '
: 'hover:rounded-md',
$$props.class
className
)}
>
<div class="flex flex-row justify-between gap-1 pb-1 px-2">
@@ -375,7 +417,6 @@
<div>
<ToggleButtonGroup
selected={propertyType}
let:item
on:selected={(e) => {
if (e.detail == propertyType) return
const staticTemplate = isStaticTemplate(inputCat)
@@ -428,41 +469,43 @@
}
}}
>
{#if isStaticTemplate(inputCat)}
<ToggleButton
tooltip={`Write text or surround javascript with \`\$\{\` and \`\}\`. Use \`results\` to connect to another node\'s output.`}
light
value="static"
size="xs2"
label={'${}'}
{item}
/>
{:else}
<ToggleButton small label="Static" value="static" {item} />
{/if}
{#snippet children({ item })}
{#if isStaticTemplate(inputCat)}
<ToggleButton
tooltip={`Write text or surround javascript with \`\$\{\` and \`\}\`. Use \`results\` to connect to another node\'s output.`}
light
value="static"
size="xs2"
label={'${}'}
{item}
/>
{:else}
<ToggleButton small label="Static" value="static" {item} />
{/if}
{#if codeInjectionDetected && propertyType == 'static'}
<Button
size="xs2"
color="light"
btnClasses="font-normal text-xs w-fit bg-green-100 text-green-800 hover:bg-green-100 dark:text-green-300 dark:bg-green-700 dark:hover:bg-green-600"
on:click={() => setJavaScriptExpr(arg.value)}
>
<span class="font-normal whitespace-nowrap flex gap-2 items-center"
><FunctionSquare size={14} /> detected -
<span class="font-bold">TAB</span>
</span>
</Button>
{:else}
<ToggleButton
small
light
tooltip="JavaScript expression ('flow_input' or 'results')."
value="javascript"
icon={FunctionSquare}
{item}
/>
{/if}
{#if codeInjectionDetected && propertyType == 'static'}
<Button
size="xs2"
color="light"
btnClasses="font-normal text-xs w-fit bg-green-100 text-green-800 hover:bg-green-100 dark:text-green-300 dark:bg-green-700 dark:hover:bg-green-600"
on:click={() => setJavaScriptExpr(arg.value)}
>
<span class="font-normal whitespace-nowrap flex gap-2 items-center"
><FunctionSquare size={14} /> detected -
<span class="font-bold">TAB</span>
</span>
</Button>
{:else}
<ToggleButton
small
light
tooltip="JavaScript expression ('flow_input' or 'results')."
value="javascript"
icon={FunctionSquare}
{item}
/>
{/if}
{/snippet}
</ToggleButtonGroup>
</div>
@@ -491,8 +534,8 @@
</div>
<div class="max-w-xs"></div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="relative" on:keyup={handleKeyUp}>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="relative" onkeyup={handleKeyUp}>
<!-- {#if $propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'}
<span
class={'text-white z-50 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute top-0 right-0 bg-blue-500'}
@@ -3,7 +3,7 @@
import { VariableService, type InputTransform } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { untrack } from 'svelte'
import { Button } from './common'
import StepInputsGen from './copilot/StepInputsGen.svelte'
import type { PickableProperties } from './flows/previousResults'
@@ -12,31 +12,44 @@
import VariableEditor from './VariableEditor.svelte'
import { Plus } from 'lucide-svelte'
export let schema: Schema | { properties?: Record<string, any> }
export let args: Record<string, InputTransform | any> = {}
export let isValid: boolean = true
export let extraLib: string = 'missing extraLib'
export let previousModuleId: string | undefined = undefined
export let filter: string[] | undefined = undefined
export let noDynamicToggle = false
export let pickableProperties: PickableProperties | undefined = undefined
export let enableAi = false
let clazz: string = ''
export { clazz as class }
let inputCheck: { [id: string]: boolean } = {}
const dispatch = createEventDispatcher()
$: isValid = allTrue(inputCheck) ?? false
$: if (args == undefined || typeof args !== 'object') {
args = {}
interface Props {
schema: Schema | { properties?: Record<string, any> }
args?: Record<string, InputTransform | any>
isValid?: boolean
extraLib?: string
previousModuleId?: string | undefined
filter?: string[] | undefined
noDynamicToggle?: boolean
pickableProperties?: PickableProperties | undefined
enableAi?: boolean
class?: string
}
let {
schema = $bindable(),
args = $bindable({}),
isValid = $bindable(true),
extraLib = $bindable('missing extraLib'),
previousModuleId = undefined,
filter = undefined,
noDynamicToggle = false,
pickableProperties = undefined,
enableAi = false,
class: clazz = ''
}: Props = $props()
let inputCheck: { [id: string]: boolean } = $state({})
$effect(() => {
isValid = allTrue(inputCheck) ?? false
})
$effect(() => {
if (args == undefined || typeof args !== 'object') {
args = {}
}
})
export function setArgs(nargs: Record<string, InputTransform | any>) {
args = nargs
}
@@ -51,18 +64,18 @@
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let pickForField: string | undefined = $state()
let itemPicker: ItemPicker | undefined = $state(undefined)
let variableEditor: VariableEditor | undefined = $state(undefined)
let keys: string[] = []
$: {
let keys: string[] = $state([])
$effect(() => {
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
removeExtraKey()
untrack(() => removeExtraKey())
}
}
})
</script>
<div class="w-full {clazz}">
@@ -84,15 +97,17 @@
</div>
{/if}
{#if keys.length > 0}
{#each keys as argName (argName)}
{#each keys as argName, index (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
<div class="pt-2 relative">
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
bind:schema
bind:argName
bind:inputCheck={inputCheck[argName]}
bind:argName={keys[index]}
bind:inputCheck={
() => inputCheck[argName] ?? false, (value) => (inputCheck[argName] = value)
}
bind:extraLib
{variableEditor}
{itemPicker}
@@ -100,10 +115,6 @@
{noDynamicToggle}
{pickableProperties}
{enableAi}
on:change={(e) => {
const { argName } = e.detail
dispatch('changeArg', { argName })
}}
/>
</div>
{/if}
@@ -128,22 +139,21 @@
...x
}))}
>
<div
slot="submission"
class="flex flex-row-reverse w-full border-t border-gray-200 rounded-bl-lg rounded-br-lg"
>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew?.()
}}
>
New variable
</Button>
</div>
{#snippet submission()}
<div class="flex flex-row-reverse w-full border-t border-gray-200 rounded-bl-lg rounded-br-lg">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew?.()
}}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<VariableEditor bind:this={variableEditor} />
@@ -41,10 +41,14 @@
import TeamSelector from './TeamSelector.svelte'
import ChannelSelector from './ChannelSelector.svelte'
export let setting: Setting
export let version: string
export let values: Writable<Record<string, any>>
export let loading = true
interface Props {
setting: Setting
version: string
values: Writable<Record<string, any>>
loading?: boolean
}
let { setting, version, values, loading = true }: Props = $props()
const dispatch = createEventDispatcher()
if (
@@ -57,9 +61,9 @@
let latestKeyRenewalAttempt: {
result: string
attempted_at: string
} | null
} | null = $state(null)
let isFetching = false
let isFetching = $state(false)
function showSetting(setting: string, values: Record<string, any>) {
if (setting == 'dev_instance') {
@@ -70,11 +74,11 @@
return true
}
let licenseKeyChanged = false
let renewing = false
let opening = false
let licenseKeyChanged = $state(false)
let renewing = $state(false)
let opening = $state(false)
let to: string = ''
let to: string = $state('')
async function reloadKeyrenewalAttemptInfo() {
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
@@ -135,9 +139,9 @@
}
}
let pythonAvailableVersions: ListAvailablePythonVersionsResponse = []
let pythonAvailableVersions: ListAvailablePythonVersionsResponse = $state([])
let isPyFetching = false
let isPyFetching = $state(false)
async function fetch_available_python_versions() {
if (isPyFetching) return
isPyFetching = true
@@ -200,7 +204,7 @@
</script>
<!-- {JSON.stringify($values, null, 2)} -->
{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null)}
{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null) && !(setting.hiddenIfEmpty && !$values[setting.key])}
{#if setting.ee_only != undefined && !$enterpriseLicense}
<div class="flex text-xs items-center gap-1 text-yellow-500 whitespace-nowrap">
<AlertTriangle size={16} />
@@ -209,7 +213,7 @@
{/if}
{#if setting.fieldType == 'select'}
<div>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">{setting.label}</span>
{#if setting.description}
@@ -218,20 +222,22 @@
</span>
{/if}
</label>
<ToggleButtonGroup bind:selected={$values[setting.key]} let:item={toggleButton}>
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButton}
/>
{/each}
<ToggleButtonGroup bind:selected={$values[setting.key]}>
{#snippet children({ item: toggleButton })}
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButton}
/>
{/each}
{/snippet}
</ToggleButtonGroup>
</div>
{:else if setting.fieldType == 'select_python'}
<div>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">{setting.label}</span>
{#if setting.description}
@@ -241,56 +247,59 @@
{/if}
</label>
<ToggleButtonGroup bind:selected={$values[setting.key]} let:item={toggleButtonn}>
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButtonn}
/>
{/each}
<PopoverMelt closeButton={!isPyFetching}>
<svelte:fragment slot="trigger">
{#if setting.select_items?.some((e) => e.label == $values[setting.key] || e.value == $values[setting.key])}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs bg-surface-secondary border-0"
nonCaptureEvent={true}>Select Custom</Button
>
{:else}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs border-0 shadow-md"
nonCaptureEvent={true}>Custom | {$values[setting.key]}</Button
>
{/if}
</svelte:fragment>
<svelte:fragment slot="content">
{#if isPyFetching}
<div class="p-4">
<LoadingIcon />
</div>
{:else}
<ToggleButtonGroup
bind:selected={$values[setting.key]}
let:item={toggleButtonn}
class="mr-10 h-full"
tabListClass="flex-wrap p-2"
>
{#each pythonAvailableVersions as item}
<ToggleButton value={item} label={item} tooltip={item} item={toggleButtonn} />
{/each}
</ToggleButtonGroup>
{/if}
</svelte:fragment>
</PopoverMelt>
<ToggleButtonGroup bind:selected={$values[setting.key]}>
{#snippet children({ item: toggleButtonn })}
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButtonn}
/>
{/each}
<PopoverMelt closeButton={!isPyFetching}>
{#snippet trigger()}
{#if setting.select_items?.some((e) => e.label == $values[setting.key] || e.value == $values[setting.key])}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs bg-surface-secondary border-0"
nonCaptureEvent={true}>Select Custom</Button
>
{:else}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs border-0 shadow-md"
nonCaptureEvent={true}>Custom | {$values[setting.key]}</Button
>
{/if}
{/snippet}
{#snippet content()}
{#if isPyFetching}
<div class="p-4">
<LoadingIcon />
</div>
{:else}
<ToggleButtonGroup
bind:selected={$values[setting.key]}
class="mr-10 h-full"
tabListClass="flex-wrap p-2"
>
{#snippet children({ item: toggleButtonn })}
{#each pythonAvailableVersions as item}
<ToggleButton value={item} label={item} tooltip={item} item={toggleButtonn} />
{/each}
{/snippet}
</ToggleButtonGroup>
{/if}
{/snippet}
</PopoverMelt>
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">{setting.label}</span>
{#if setting.description}
@@ -439,27 +448,29 @@
on {attemptedAt}
</span>
</div>
<div slot="text">
{#if latestKeyRenewalAttempt.result === 'success'}
<span class="text-green-300">
Latest key renewal succeeded on {attemptedAt}
</span>
{:else if isTrial}
<span class="text-yellow-300">
License key cannot be renewed during trial ({attemptedAt})
</span>
{:else}
<span class="text-red-300">
Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt.result.replace(
'error: ',
''
)}
</span>
{/if}
<br />
As long as invoices are paid and usage corresponds to the subscription, the key is
renewed daily with a validity of 35 days (grace period).
</div>
{#snippet text()}
<div>
{#if latestKeyRenewalAttempt?.result === 'success'}
<span class="text-green-300">
Latest key renewal succeeded on {attemptedAt}
</span>
{:else if isTrial}
<span class="text-yellow-300">
License key cannot be renewed during trial ({attemptedAt})
</span>
{:else}
<span class="text-red-300">
Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt?.result.replace(
'error: ',
''
)}
</span>
{/if}
<br />
As long as invoices are paid and usage corresponds to the subscription, the key
is renewed daily with a validity of 35 days (grace period).
</div>
{/snippet}
</Popover>
</div>
{/if}
@@ -515,7 +526,7 @@
<div class="flex w-full max-w-lg mt-1 gap-2 items-center">
<select
class="max-w-24"
on:change={(e) => {
onchange={(e) => {
if (e.target?.['value']) {
$values[setting.key][i] = {
[e.target['value']]: ''
@@ -540,7 +551,7 @@
<input
type="text"
placeholder="Slack channel"
on:input={(e) => {
oninput={(e) => {
if (e.target?.['value']) {
$values[setting.key][i] = {
slack_channel: e.target['value']
@@ -598,7 +609,7 @@
/>
{/if}
<div>
<button on:click={fetchTeams} class="flex items-center gap-1 mt-2">
<button onclick={fetchTeams} class="flex items-center gap-1 mt-2">
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
</button>
</div>
@@ -607,7 +618,7 @@
<input
type="email"
placeholder="Email address"
on:input={(e) => {
oninput={(e) => {
if (e.target?.['value']) {
$values[setting.key][i] = {
email: e.target['value']
@@ -621,7 +632,7 @@
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
aria-label="Clear"
on:click={() => {
onclick={() => {
$values[setting.key] = $values[setting.key].filter(
(_, index) => index !== i
)
@@ -718,7 +729,7 @@
type="number"
id="writer_memory_budget"
placeholder="300"
on:input={(e) => {
oninput={(e) => {
if (e.target instanceof HTMLInputElement) {
if (e.target.valueAsNumber) {
$values[setting.key].writer_memory_budget =
@@ -776,7 +787,7 @@
type="number"
id="max_indexed_job_log_size"
placeholder="1024"
on:input={(e) => {
oninput={(e) => {
if (e.target instanceof HTMLInputElement) {
if (e.target.valueAsNumber) {
$values[setting.key].max_indexed_job_log_size =
@@ -16,20 +16,29 @@
import InstanceSetting from './InstanceSetting.svelte'
import { writable, type Writable } from 'svelte/store'
export let tab: string = 'Core'
export let hideTabs: boolean = false
export let hideSave: boolean = false
export let closeDrawer: (() => void) | undefined = () => {}
interface Props {
tab?: string
hideTabs?: boolean
hideSave?: boolean
closeDrawer?: (() => void) | undefined
}
let {
tab = $bindable('Core'),
hideTabs = false,
hideSave = false,
closeDrawer = () => {}
}: Props = $props()
let values: Writable<Record<string, any>> = writable({})
let initialOauths: Record<string, any> = {}
let initialRequirePreexistingUserForOauth: boolean = false
let requirePreexistingUserForOauth: boolean = false
let requirePreexistingUserForOauth: boolean = $state(false)
let initialValues: Record<string, any> = {}
let snowflakeAccountIdentifier = ''
let version: string = ''
let loading = true
let snowflakeAccountIdentifier = $state('')
let version: string = $state('')
let loading = $state(true)
loadSettings()
loadVersion()
@@ -39,7 +48,7 @@
async function loadVersion() {
version = await SettingsService.backendVersion()
}
let oauths: Record<string, any> = {}
let oauths: Record<string, any> = $state({})
async function loadSettings() {
loading = true
@@ -240,13 +249,13 @@
</script>
<div class="pb-8">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<Tabs {hideTabs} bind:selected={tab}>
{#each settingsKeys as category}
<Tab value={category}>{category}</Tab>
{/each}
<svelte:fragment slot="content">
{#snippet content()}
{#each Object.keys(settings) as category}
<TabContent value={category}>
{#if category == 'SMTP'}
@@ -332,7 +341,7 @@
bind:snowflakeAccountIdentifier
bind:requirePreexistingUserForOauth
>
<svelte:fragment slot="scim">
{#snippet scim()}
<div class="flex-col flex gap-2 pb-4">
{#each scimSamlSetting as setting}
<InstanceSetting
@@ -344,7 +353,7 @@
/>
{/each}
</div>
</svelte:fragment>
{/snippet}
</AuthSettings>
{/if}
<div>
@@ -362,7 +371,7 @@
</div>
</TabContent>
{/each}
</svelte:fragment>
{/snippet}
</Tabs>
</div>
+29 -27
View File
@@ -13,7 +13,7 @@
const dispatch = createEventDispatcher()
let email: string
let email: string = $state('')
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
@@ -52,7 +52,7 @@
goto('#superadmin-settings')
}
}
]
]
: []
)
}
@@ -62,45 +62,47 @@
email = ''
}
let selected: 'operator' | 'developer' | 'admin' = 'developer'
let selected: 'operator' | 'developer' | 'admin' = $state('developer')
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: MailPlus }}>
Invite
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<div class="flex flex-col gap-2 p-4">
<input
type="email"
on:keyup={handleKeyUp}
onkeyup={handleKeyUp}
placeholder="email"
bind:value={email}
class="mr-4"
/>
<ToggleButtonGroup bind:selected let:item>
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButtonGroup bind:selected>
{#snippet children({ item })}
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
{/snippet}
</ToggleButtonGroup>
<Button
variant="contained"
@@ -112,5 +114,5 @@
Invite
</Button>
</div>
</svelte:fragment>
{/snippet}
</Popover>
+43 -25
View File
@@ -7,23 +7,41 @@
import SearchItems from './SearchItems.svelte'
type Item = Record<string, any>
export let pickCallback: (path: string, f: string) => void
export let loadItems: () => Promise<Item[] | undefined>
export let extraField: string = 'path'
export let extraField2: string | undefined = undefined
export let itemName: string
export let closeOnClick = true
/** Displayed if the load function returns no items. */
export let noItemMessage = 'There are no items in the list'
/** Displayed if the search returns no items. */
export let buttons: Record<string, (x: string) => void> = {}
export let tooltip: string = ''
export let documentationLink: string | undefined = undefined
let loading = false
let items: Item[] | undefined = []
let filteredItems: Item[] | undefined = []
let filter = ''
interface Props {
pickCallback: (path: string, f: string) => void
loadItems: () => Promise<Item[] | undefined>
extraField?: string
extraField2?: string | undefined
itemName: string
closeOnClick?: boolean
/** Displayed if the load function returns no items. */
noItemMessage?: string
/** Displayed if the search returns no items. */
buttons?: Record<string, (x: string) => void>
tooltip?: string
documentationLink?: string | undefined
submission?: import('svelte').Snippet
}
let {
pickCallback,
loadItems,
extraField = 'path',
extraField2 = undefined,
itemName,
closeOnClick = true,
noItemMessage = 'There are no items in the list',
buttons = {},
tooltip = '',
documentationLink = undefined,
submission
}: Props = $props()
let loading = $state(false)
let items: Item[] | undefined = $state([])
let filteredItems: Item[] | undefined = $state([])
let filter = $state('')
export function openDrawer() {
loading = true
@@ -34,12 +52,12 @@
.finally(() => {
loading = false
})
drawer.openDrawer?.()
drawer?.openDrawer?.()
}
let drawer: Drawer
let drawer: Drawer | undefined = $state()
let refreshing = false
let refreshing = $state(false)
</script>
<SearchItems
@@ -65,7 +83,7 @@
>
<div class="w-full h-full flex flex-col">
<div class="flex flex-row gap-2 pb-4">
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
placeholder="Search {itemName}s"
@@ -110,9 +128,9 @@
<button
class="py-2 px-1 gap-1 flex grow border-gray-300 border-opacity-0
text-primary"
on:click={() => {
onclick={() => {
if (closeOnClick) {
drawer.closeDrawer()
drawer?.closeDrawer()
}
pickCallback(obj['path'], obj[extraField])
}}
@@ -174,8 +192,8 @@
<NoItemFound />
{/if}
</div>
<svelte:fragment slot="actions">
<slot name="submission" />
</svelte:fragment>
{#snippet actions()}
{@render submission?.()}
{/snippet}
</DrawerContent>
</Drawer>
+2 -2
View File
@@ -109,7 +109,7 @@ ${Object.entries(args)
<Drawer bind:this={jsonViewer} size="900px">
<DrawerContent title="Expanded Args" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
download="windmill-args.json"
href={id && workspace
@@ -137,7 +137,7 @@ ${Object.entries(args)
>
Copy to clipboard
</Button>
</svelte:fragment>
{/snippet}
{#if jsonStr.length > 100000 || (id && workspace && args && isWindmillTooBigObject(args))}
<div class="text-sm mb-2 text-tertiary">
<a
@@ -4,6 +4,7 @@
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { createEventDispatcher } from 'svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let code: string | undefined
export let value: any = undefined
export let error = ''
+2 -2
View File
@@ -149,7 +149,7 @@
<Drawer bind:this={logViewer} bind:open={drawerOpen} size="900px">
<DrawerContent title="Expanded Logs" on:close={logViewer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
{#if jobId && download}
<Button
href="{base}/api/w/{$workspaceStore}/jobs_u/get_logs/{jobId}"
@@ -174,7 +174,7 @@
>
Copy to clipboard
</Button>
</svelte:fragment>
{/snippet}
<div>
<pre
class="bg-surface-secondary text-secondary text-xs w-full p-2 whitespace-pre-wrap border rounded-md"
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import type { Schema } from '$lib/common'
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -16,31 +18,47 @@
import { evalValue } from './flows/utils'
import type { PickableProperties } from './flows/previousResults'
export let mod: FlowModule
export let schema: Schema | { properties?: Record<string, any> }
export let pickableProperties: PickableProperties | undefined
export let testJob: Job | undefined = undefined
export let testIsLoading = false
export let noEditor = false
export let scriptProgress = undefined
interface Props {
mod: FlowModule
schema: Schema | { properties?: Record<string, any> }
pickableProperties: PickableProperties | undefined
testJob?: Job | undefined
testIsLoading?: boolean
noEditor?: boolean
scriptProgress?: any
}
let {
mod,
schema,
pickableProperties,
testJob = $bindable(undefined),
testIsLoading = $bindable(false),
noEditor = false,
scriptProgress = $bindable(undefined)
}: Props = $props()
const { flowStore, flowStateStore, testStepStore, pathStore } =
getContext<FlowEditorContext>('FlowEditorContext')
// Test
let testJobLoader: TestJobLoader
let testJobLoader: TestJobLoader | undefined = $state()
let jobProgressReset: () => void = () => {}
let stepArgs: Record<string, any> | undefined = Object.fromEntries(
Object.keys(schema.properties ?? {}).map((k) => [
k,
evalValue(k, mod, $testStepStore, pickableProperties, false)
])
let stepArgs: Record<string, any> | undefined = $state(
Object.fromEntries(
Object.keys(schema.properties ?? {}).map((k) => [
k,
evalValue(k, mod, $testStepStore, pickableProperties, false)
])
)
)
$: $testStepStore[mod.id] = stepArgs
run(() => {
$testStepStore[mod.id] = stepArgs
})
export function runTestWithStepArgs() {
runTest(stepArgs)
@@ -58,7 +76,7 @@
val.content,
val.language,
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
$flowStore?.tag ?? val.tag
flowStore.val?.tag ?? val.tag
)
} else if (val.type == 'script') {
const script = val.hash
@@ -69,7 +87,7 @@
script.content,
script.language,
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
$flowStore?.tag ?? (val.tag_override ? val.tag_override : script.tag),
flowStore.val?.tag ?? (val.tag_override ? val.tag_override : script.tag),
script.lock,
val.hash ?? script.hash
)
@@ -104,7 +122,7 @@
/>
<div class="p-4">
{#if $flowStore.value.same_worker}
{#if flowStore.val.value.same_worker}
<div class="mb-1 bg-yellow-100 text-yellow-700 p-1 text-xs"
>The `./shared` folder is not passed across individual "Test this step"</div
>
@@ -5,7 +5,7 @@
import { RefreshCw } from 'lucide-svelte'
import ArgInput from './ArgInput.svelte'
import { Button } from './common'
import { getContext } from 'svelte'
import { getContext, untrack } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import { evalValue } from './flows/utils'
import type { FlowModule } from '$lib/gen'
@@ -13,22 +13,36 @@
import type SimpleEditor from './SimpleEditor.svelte'
import { getResourceTypes } from './resourceTypesStore'
export let schema: Schema | { properties?: Record<string, any>; required?: string[] }
export let args: Record<string, any> = {}
export let mod: FlowModule
export let pickableProperties: PickableProperties | undefined
interface Props {
schema: Schema | { properties?: Record<string, any>; required?: string[] }
args?: Record<string, any>
mod: FlowModule
pickableProperties: PickableProperties | undefined
isValid?: boolean
autofocus?: boolean
}
export let isValid: boolean = true
export let autofocus = false
let {
schema,
args = $bindable({}),
mod,
pickableProperties,
isValid = $bindable(true),
autofocus = false
}: Props = $props()
const { testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
let inputCheck: { [id: string]: boolean } = {}
$: isValid = allTrue(inputCheck) ?? false
let inputCheck: { [id: string]: boolean } = $state({})
$effect(() => {
isValid = allTrue(inputCheck) ?? false
})
$: if (args == undefined || typeof args !== 'object') {
args = {}
}
$effect(() => {
if (args == undefined || typeof args !== 'object') {
args = {}
}
})
function removeExtraKey() {
const nargs = {}
@@ -40,18 +54,18 @@
args = nargs
}
let keys: string[] = []
$: {
let keys: string[] = $state([])
$effect(() => {
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
removeExtraKey()
untrack(() => removeExtraKey())
}
}
})
function plugIt(argName: string) {
args[argName] = structuredClone(
evalValue(argName, mod, testStepStore, pickableProperties, true)
$state.snapshot(evalValue(argName, mod, testStepStore, pickableProperties, true))
)
try {
editor?.[argName]?.setCode(JSON.stringify(args[argName], null, 4))
@@ -60,9 +74,9 @@
}
}
let editor: Record<string, SimpleEditor | undefined> = {}
let editor: Record<string, SimpleEditor | undefined> = $state({})
let resourceTypes: string[] | undefined = undefined
let resourceTypes: string[] | undefined = $state(undefined)
async function loadResourceTypes() {
resourceTypes = await getResourceTypes()
@@ -11,32 +11,54 @@
import { getContext } from 'svelte'
import { getStringError } from './copilot/chat/utils'
export let lang: Script['language']
export let editor: Editor | undefined
export let diffEditor: DiffEditor | undefined
export let loopStatus:
| { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' }
| undefined = undefined
export let lastJob: Job | undefined = undefined
export let scriptProgress: number | undefined = undefined
export let testJob: Job | undefined = undefined
export let mod: FlowModule
export let testIsLoading: boolean = false
export let disableMock: boolean = false
export let disableHistory: boolean = false
interface Props {
lang: Script['language']
editor: Editor | undefined
diffEditor: DiffEditor | undefined
loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined
lastJob?: Job | undefined
scriptProgress?: number | undefined
testJob?: Job | undefined
mod: FlowModule
testIsLoading?: boolean
disableMock?: boolean
disableHistory?: boolean
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
}
const { flowStore, testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
let {
lang,
editor,
diffEditor,
loopStatus = undefined,
lastJob = undefined,
scriptProgress = $bindable(undefined),
testJob = undefined,
mod,
testIsLoading = false,
disableMock = false,
disableHistory = false,
onUpdateMock
}: Props = $props()
let selectedJob: Job | undefined = undefined
const { testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
let selectedJob: Job | undefined = $state(undefined)
let fetchingLastJob = false
let preview: 'mock' | 'job' | undefined = undefined
let outputPicker: OutputPickerInner | undefined = undefined
let jobProgressReset: () => void = () => {}
let preview: 'mock' | 'job' | undefined = $state(undefined)
let jobProgressReset: () => void = $state(() => {})
$: lastJob && outputPicker?.setLastJob(lastJob, false)
$: testJob && outputPicker?.setLastJob(testJob, true)
let nlastJob = $derived.by(() => {
if (testJob) {
return { ...testJob, preview: true }
}
if (lastJob) {
return { ...lastJob, preview: false }
}
return undefined
})
let forceJson = false
let forceJson = $state(false)
</script>
<Splitpanes horizontal>
@@ -51,15 +73,12 @@
{/if}
<OutputPickerInner
bind:this={outputPicker}
lastJob={nlastJob}
fullResult
moduleId={mod.id}
closeOnOutsideClick={true}
getLogs
on:updateMock={({ detail }) => {
mod.mock = detail
$flowStore = $flowStore
}}
{onUpdateMock}
mock={mod.mock}
bind:forceJson
bind:selectedJob
@@ -70,11 +89,11 @@
{disableMock}
{disableHistory}
>
<svelte:fragment slot="copilot-fix">
{#snippet copilot_fix()}
{#if lang && editor && diffEditor && $testStepStore[mod.id] && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)}
<ScriptFix {lang} />
{/if}
</svelte:fragment>
{/snippet}
</OutputPickerInner>
</Pane>
<Pane size={50} minSize={10}>
@@ -117,8 +117,8 @@
<div class="flex flex-col mb-2 gap-6">
<Path disabled={!own} {kind} {initialPath} bind:path />
</div>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button disabled={!own} on:click={updatePath}>Move/Rename</Button>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+15 -8
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import CollapseLink from './CollapseLink.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
@@ -6,11 +8,11 @@
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
export let value: any
interface Props {
value: any
}
$: enabled = value != undefined
$: changeDomain(value?.['domain'], value?.['custom'])
let { value = $bindable() }: Props = $props()
function changeDomain(domain, custom) {
if (value) {
@@ -31,10 +33,14 @@
}
}
}
let enabled = $derived(value != undefined)
run(() => {
changeDomain(value?.['domain'], value?.['custom'])
})
</script>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-sm font-medium text-primary flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name="okta" after={true} /></div><Toggle
checked={enabled}
@@ -57,10 +63,11 @@
on:selected={({ detail }) => {
value['custom'] = detail === 'custom'
}}
let:item
>
<ToggleButton value="org" label={'Org'} {item} />
<ToggleButton value="custom" label="Custom" {item} />
{#snippet children({ item })}
<ToggleButton value="org" label={'Org'} {item} />
<ToggleButton value="custom" label="Custom" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
<div class="grow">
+115 -83
View File
@@ -1,4 +1,4 @@
<script lang="ts" context="module">
<script lang="ts" module>
const lastMetaUsed = writable<Meta | undefined>(undefined)
</script>
@@ -24,7 +24,7 @@
GcpTriggerService
} from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher, getContext } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import { writable } from 'svelte/store'
import { Alert, Button } from './common'
import Badge from './common/badge/Badge.svelte'
@@ -52,27 +52,51 @@
| 'mqtt_trigger'
| 'sqs_trigger'
| 'gcp_trigger'
let meta: Meta | undefined = undefined
export let fullNamePlaceholder: string | undefined = undefined
export let namePlaceholder = ''
export let initialPath: string
export let path = ''
export let error = ''
export let disabled = false
export let checkInitialPathExistence = false
export let autofocus = true
export let dirty = false
export let kind: PathKind
export let hideUser: boolean = false
export let disableEditing = false
let meta: Meta | undefined = $state(undefined)
interface Props {
fullNamePlaceholder?: string | undefined
namePlaceholder?: string
initialPath: string
path?: string
error?: string
disabled?: boolean
checkInitialPathExistence?: boolean
autofocus?: boolean
dirty?: boolean
kind: PathKind
hideUser?: boolean
disableEditing?: boolean
}
let inputP: HTMLInputElement | undefined = undefined
let {
fullNamePlaceholder = undefined,
namePlaceholder = '',
initialPath,
path = $bindable(undefined),
error = $bindable(undefined),
disabled = $bindable(false),
checkInitialPathExistence = false,
autofocus = true,
dirty = $bindable(false),
kind,
hideUser = false,
disableEditing = false
}: Props = $props()
$effect.pre(() => {
if (path == undefined) {
path = ''
}
if (error == undefined) {
error = ''
}
})
let inputP: HTMLInputElement | undefined = $state(undefined)
const dispatch = createEventDispatcher()
let folders: { name: string; write: boolean }[] = []
$: meta && onMetaChange()
let folders: { name: string; write: boolean }[] = $state([])
function onMetaChange() {
if (meta) {
@@ -150,7 +174,7 @@
meta = newMeta
path = metaToPath(meta)
} else {
meta = pathToMeta(path, hideUser)
meta = pathToMeta(path ?? '', hideUser)
}
}
@@ -204,6 +228,7 @@
}
async function pathExists(path: string, kind: PathKind): Promise<boolean> {
if (!path.length) return false
if (kind == 'flow') {
return await FlowService.existsFlowByPath({ workspace: $workspaceStore!, path: path })
} else if (kind == 'script') {
@@ -288,13 +313,6 @@
}
}
$: {
if ($workspaceStore && $userStore) {
loadFolders()
initPath()
}
}
async function initPath() {
await tick()
if (path != undefined && path != '') {
@@ -319,45 +337,58 @@
'openSearchWithPrefilledText'
)
$: displayPathChangedWarning =
$effect(() => {
;[meta?.name, meta?.owner, meta?.ownerKind]
meta && untrack(() => onMetaChange())
})
$effect(() => {
if ($workspaceStore && $userStore) {
untrack(() => {
loadFolders()
initPath()
})
}
})
let displayPathChangedWarning = $derived(
(['flow', 'script', 'resource', 'variable'] as PathKind[]).includes(kind) &&
initialPath &&
initialPath !== path
$: pathUsageInFlowsPromise =
initialPath &&
initialPath !== path
)
let pathUsageInFlowsPromise = $derived(
(kind == 'script' || kind == 'flow') &&
$workspaceStore &&
initialPath &&
FlowService.listFlowPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath,
runnableKind: kind
})
$: pathUsageInAppsPromise =
$workspaceStore &&
initialPath &&
FlowService.listFlowPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath,
runnableKind: kind
})
)
let pathUsageInAppsPromise = $derived(
(kind == 'script' || kind == 'flow') &&
$workspaceStore &&
initialPath &&
AppService.listAppPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath,
runnableKind: kind
})
$: pathUsageInScriptsPromise =
$workspaceStore &&
initialPath &&
AppService.listAppPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath,
runnableKind: kind
})
)
let pathUsageInScriptsPromise = $derived(
kind == 'script' &&
$workspaceStore &&
initialPath &&
ScriptService.listScriptPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath
})
$workspaceStore &&
initialPath &&
ScriptService.listScriptPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath
})
)
</script>
<div>
<div class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 pb-0 mb-1">
{#if meta != undefined}
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
{#if !hideUser}
<div class="block">
<ToggleButtonGroup
@@ -377,29 +408,30 @@
}
}}
disabled={disabled || disableEditing}
let:item
>
<ToggleButton
icon={User}
disabled={disabled || disableEditing}
light
size="xs"
value="user"
position="left"
label="User"
{item}
/>
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
<ToggleButton
icon={Folder}
disabled={disabled || disableEditing}
light
size="xs"
value="folder"
position="right"
label="Folder"
{item}
/>
{#snippet children({ item })}
<ToggleButton
icon={User}
disabled={disabled || disableEditing}
light
size="xs"
value="user"
position="left"
label="User"
{item}
/>
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
<ToggleButton
icon={Folder}
disabled={disabled || disableEditing}
light
size="xs"
value="folder"
position="right"
label="Folder"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
{/if}
@@ -417,7 +449,7 @@
disabled={disabled ||
!($superadmin || ($userStore?.is_admin ?? false)) ||
disableEditing}
on:keydown={setDirty}
onkeydown={setDirty}
/>
</label>
{:else if meta.ownerKind === 'folder'}
@@ -428,7 +460,7 @@
</div>
<span class="text-xl">/</span>
<label class="block grow w-full max-w-md">
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input
disabled={disabled || disableEditing}
type="text"
@@ -436,7 +468,7 @@
{autofocus}
bind:this={inputP}
autocomplete="off"
on:keyup={handleKeyUp}
onkeyup={handleKeyUp}
bind:value={meta.name}
placeholder={namePlaceholder}
class={error === ''
@@ -461,7 +493,7 @@
value={path}
size={path?.length || 50}
class="font-mono !text-xs max-w-[calc(100%-70px)] !w-auto !h-[24px] !py-0 !border-l-0 !rounded-l-none"
on:focus={({ currentTarget }) => {
onfocus={({ currentTarget }) => {
currentTarget.select()
}}
/>
@@ -59,9 +59,12 @@
queuedJobs = loadingQueuedJobs
const endStart = new Date().getTime()
// toggle queuedJobsLoading to false in 1 secs to let some time for the animation to play
setTimeout(() => {
queuedJobsLoading = false
}, 3000 - (endStart - timeStart))
setTimeout(
() => {
queuedJobsLoading = false
},
3000 - (endStart - timeStart)
)
}
async function scaleToZero() {
@@ -167,18 +170,20 @@
</tbody>
</TableCustom>
<div slot="actions" class="flex gap-1">
<Button
color="red"
disabled={cancellingInProgress === true || queuedJobs.length === 0}
on:click={scaleToZero}
>
{#if cancellingInProgress}
<Loader2 class="animate-spin" /> Stopping jobs
{:else}
Scale down to 0
{/if}
</Button>
</div>
{#snippet actions()}
<div class="flex gap-1">
<Button
color="red"
disabled={cancellingInProgress === true || queuedJobs.length === 0}
on:click={scaleToZero}
>
{#if cancellingInProgress}
<Loader2 class="animate-spin" /> Stopping jobs
{:else}
Scale down to 0
{/if}
</Button>
</div>
{/snippet}
</DrawerContent>
</Drawer>
+1 -1
View File
@@ -57,6 +57,6 @@
export let name = undefined
</script>
<div use:portal={{ target, name }} hidden>
<div use:portal={{ target, name }} hidden class={$$props.class}>
<slot />
</div>
@@ -1,8 +1,10 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import type { Schema } from '$lib/common'
import { ResourceService, type Resource, type ResourceType } from '$lib/gen'
import { canWrite, emptyString, isOwner, urlize } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { Alert, Skeleton } from './common'
import Path from './Path.svelte'
import Required from './Required.svelte'
@@ -20,44 +22,48 @@
import TestTriggerConnection from './triggers/TestTriggerConnection.svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let canSave = true
export let resource_type: string | undefined = undefined
export let path: string = ''
export let newResource: boolean = false
export let hidePath: boolean = false
export let watchChanges: boolean = false
export let defaultValues: Record<string, any> | undefined = undefined
$: if (defaultValues && Object.keys(defaultValues).length > 0) {
args = defaultValues
interface Props {
canSave?: boolean
resource_type?: string | undefined
path?: string
newResource?: boolean
hidePath?: boolean
watchChanges?: boolean
defaultValues?: Record<string, any> | undefined
}
let isValid = true
let jsonError = ''
let can_write = true
let {
canSave = $bindable(true),
resource_type = $bindable(undefined),
path = $bindable(''),
newResource = false,
hidePath = false,
watchChanges = false,
defaultValues = undefined
}: Props = $props()
$: canSave = can_write && isValid && jsonError == ''
let isValid = $state(true)
let jsonError = $state('')
let can_write = $state(true)
let initialPath = path
let resourceToEdit: Resource | undefined = undefined
let resourceToEdit: Resource | undefined = $state(undefined)
let description: string = ''
let description: string = $state('')
let DESCRIPTION_PLACEHOLDER = `Describe what this resource is for`
let resourceSchema: Schema | undefined = undefined
let args: Record<string, any> = {}
let loadingSchema = true
let linkedVars: string[] = []
let resourceTypeInfo: ResourceType | undefined = undefined
let editDescription = false
let viewJsonSchema = false
let resourceSchema: Schema | undefined = $state(undefined)
let args: Record<string, any> = $state({})
let loadingSchema = $state(true)
let linkedVars: string[] = $state([])
let resourceTypeInfo: ResourceType | undefined = $state(undefined)
let editDescription = $state(false)
let viewJsonSchema = $state(false)
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
$: watchChanges && dispatchIfMounted('change', { path, args, description })
let rawCode: string | undefined = undefined
let rawCode: string | undefined = $state(undefined)
async function initEdit() {
resourceToEdit = await ResourceService.getResource({ workspace: $workspaceStore!, path })
@@ -81,9 +87,6 @@
sendUserToast('Resource type cannot be undefined for new resource creation', true)
}
$: rawCode && parseJson()
$: linkedVars.length > 0 && path && updateArgsFromLinkedVars()
export async function editResource(): Promise<void> {
if (resourceToEdit) {
await ResourceService.updateResource({
@@ -150,8 +153,7 @@
})
}
let textFileContent: string = ''
$: textFileContent && parseTextFileContent()
let textFileContent: string = $state('')
function switchTab(asJson: boolean) {
viewJsonSchema = asJson
@@ -170,6 +172,26 @@
content: textFileContent
}
}
run(() => {
if (defaultValues && Object.keys(defaultValues).length > 0) {
args = defaultValues
}
})
run(() => {
canSave = can_write && isValid && jsonError == ''
})
run(() => {
watchChanges && dispatchIfMounted('change', { path, args, description })
})
run(() => {
rawCode && untrack(() => parseJson())
})
run(() => {
linkedVars.length > 0 && path && untrack(() => updateArgsFromLinkedVars())
})
run(() => {
textFileContent && untrack(() => parseTextFileContent())
})
</script>
<div>
@@ -57,7 +57,7 @@
bind:canSave
/>
{/await}
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
startIcon={{ icon: Save }}
on:click={() => {
@@ -72,6 +72,6 @@
>
Save
</Button>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+133 -52
View File
@@ -13,52 +13,20 @@
import TimeAgo from './TimeAgo.svelte'
import Popover from './meltComponents/Popover.svelte'
import { Calendar, CornerDownLeft } from 'lucide-svelte'
import { Calendar, Check, CornerDownLeft } from 'lucide-svelte'
import RunFormAdvancedPopup from './RunFormAdvancedPopup.svelte'
import { page } from '$app/stores'
import { replaceState } from '$app/navigation'
import JsonInputs from '$lib/components/JsonInputs.svelte'
import TriggerableByAI from './TriggerableByAI.svelte'
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import { untrack } from 'svelte'
export let runnable:
| {
summary?: string
schema?: Schema | any
description?: string
path?: string
is_template?: boolean
hash?: string
kind?: string
can_write?: boolean
created_at?: string
created_by?: string
extra_perms?: Record<string, boolean>
}
| undefined
export let runAction: (
scheduledForStr: string | undefined,
args: Record<string, any>,
invisible_to_owner: boolean | undefined,
overrideTag: string | undefined
) => void
export let buttonText = 'Run'
export let schedulable = true
export let detailed = true
export let autofocus = false
export let topButton = false
export let loading = false
export let noVariablePicker = false
export let viewKeybinding = false
export let scheduledForStr: string | undefined
export let invisible_to_owner: boolean | undefined
export let overrideTag: string | undefined
export let args: Record<string, any> = {}
export let jsonView = false
let reloadArgs = 0
let jsonEditor: JsonInputs | undefined = undefined
let schemaHeight = 0
let reloadArgs = $state(0)
let jsonEditor: JsonInputs | undefined = $state(undefined)
let schemaHeight = $state(0)
let showInputSelectedBadge = $state(false)
let savedPreviousArgs: Record<string, any> | undefined = $state(undefined)
export async function setArgs(nargs: Record<string, any>) {
args = nargs
@@ -66,12 +34,72 @@
}
export function run() {
runAction(scheduledForStr, args, invisible_to_owner, overrideTag)
runAction(scheduledForStr, args ?? {}, invisible_to_owner, overrideTag)
}
export let isValid = true
interface Props {
runnable:
| {
summary?: string
schema?: Schema | any
description?: string
path?: string
is_template?: boolean
hash?: string
kind?: string
can_write?: boolean
created_at?: string
created_by?: string
extra_perms?: Record<string, boolean>
}
| undefined
runAction: (
scheduledForStr: string | undefined,
args: Record<string, any>,
invisible_to_owner: boolean | undefined,
overrideTag: string | undefined
) => void
buttonText?: string
schedulable?: boolean
detailed?: boolean
autofocus?: boolean
topButton?: boolean
loading?: boolean
noVariablePicker?: boolean
viewKeybinding?: boolean
scheduledForStr: string | undefined
invisible_to_owner: boolean | undefined
overrideTag: string | undefined
args?: Record<string, any>
jsonView?: boolean
isValid?: boolean
}
let {
runnable = $bindable(),
runAction,
buttonText = 'Run',
schedulable = true,
detailed = true,
autofocus = false,
topButton = false,
loading = false,
noVariablePicker = false,
viewKeybinding = false,
scheduledForStr = $bindable(),
invisible_to_owner = $bindable(),
overrideTag = $bindable(),
args = $bindable(),
jsonView = false,
isValid = $bindable(true)
}: Props = $props()
$effect.pre(() => {
if (args == undefined) {
args = {}
}
})
$: onArgsChange(args)
let debounced: NodeJS.Timeout | undefined = undefined
function onArgsChange(args: any) {
@@ -95,8 +123,57 @@
export function setCode(code: string) {
jsonEditor?.setCode(code)
}
$effect(() => {
Object.keys(args ?? {}).forEach((key) => {
args?.[key]
})
untrack(() => onArgsChange(args))
})
</script>
<TriggerableByAI
id={`run-form-${runnable?.path ?? ''}`}
description={`Form to fill the inputs to run ${runnable?.summary && runnable?.summary.length > 0 ? runnable?.summary : runnable?.path}.
## Script description: ${runnable?.description ?? ''}.
## Schema used: ${JSON.stringify(runnable?.schema)}.
## Current args: ${JSON.stringify(args)}}`}
onTrigger={(value) => {
savedPreviousArgs = args
setArgs(JSON.parse(value ?? '{}'))
showInputSelectedBadge = true
}}
showAnimation={false}
/>
{#snippet acceptButton()}
<Button
startIcon={{
icon: Check
}}
size="xs2"
btnClasses="border border-gray-200 dark:border-gray-600 !bg-surface text-primary"
on:click={() => {
showInputSelectedBadge = false
savedPreviousArgs = undefined
}}
>
Accept
</Button>
{/snippet}
{#if showInputSelectedBadge}
<InputSelectedBadge
inputSelected="ai"
labelColor="text-violet-800 dark:text-primary"
className="dark:!bg-violet-800 !bg-violet-200 !border-violet-200 dark:!border-violet-800"
{acceptButton}
onReject={() => {
setArgs(savedPreviousArgs ?? {})
savedPreviousArgs = undefined
showInputSelectedBadge = false
}}
/>
{/if}
<div class="max-w-3xl">
{#if detailed}
{#if runnable}
@@ -137,6 +214,10 @@
</div>
</div>
{:else}
<TriggerableByAI
id="run-form-loading"
description="Run form is loading, should scan the page until this is gone"
/>
<h1>Loading...</h1>
{/if}
{/if}
@@ -144,7 +225,7 @@
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid || jsonView}
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
on:click={() => runAction(undefined, args ?? {}, invisible_to_owner, overrideTag)}
>
{buttonText}
</Button>
@@ -174,7 +255,7 @@
? {
type: 'hash',
hash: runnable.hash
}
}
: undefined}
prettifyHeader
{noVariablePicker}
@@ -198,26 +279,26 @@
color="dark"
btnClasses="!px-6 !py-1 !h-8 inline-flex gap-2"
disabled={!isValid || jsonView}
on:click={() => runAction(scheduledForStr, args, invisible_to_owner, overrideTag)}
on:click={() => runAction(scheduledForStr, args ?? {}, invisible_to_owner, overrideTag)}
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
>
{scheduledForStr ? 'Schedule to run later' : buttonText}
</Button>
<div>
<Popover placement="bottom" closeButton usePointerDownOutside>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button nonCaptureEvent startIcon={{ icon: Calendar }} size="xs" color="light">
Advanced
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<RunFormAdvancedPopup
bind:scheduledForStr
bind:invisible_to_owner
bind:overrideTag
bind:runnable
/>
</svelte:fragment>
{/snippet}
</Popover>
</div>
</div>
@@ -236,7 +317,7 @@
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid || jsonView}
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
on:click={() => runAction(undefined, args ?? {}, invisible_to_owner, overrideTag)}
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
>
{buttonText}
+164 -140
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { run, createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import {
File as FileIcon,
FolderClosed,
@@ -17,47 +20,57 @@
import { Alert, Button, Drawer } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import Section from './Section.svelte'
import { createEventDispatcher } from 'svelte'
import VirtualList from 'svelte-tiny-virtual-list'
import { createEventDispatcher, untrack } from 'svelte'
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
import TableSimple from './TableSimple.svelte'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import FileUploadModal from './common/fileUpload/FileUploadModal.svelte'
import PdfViewer from './display/PdfViewer.svelte'
import { twMerge } from 'tailwind-merge'
let deletionModalOpen = false
let fileDeletionInProgress = false
let deletionModalOpen = $state(false)
let fileDeletionInProgress = $state(false)
let fileListUnavailable: boolean | undefined = undefined
let fileListUnavailable: boolean | undefined = $state(undefined)
let moveModalOpen = false
let moveDestKey: string | undefined = undefined
let fileMoveInProgress = false
let moveModalOpen = $state(false)
let moveDestKey: string | undefined = $state(undefined)
let fileMoveInProgress = $state(false)
let uploadModalOpen = false
let uploadModalOpen = $state(false)
let workspaceSettingsInitialized = true
let workspaceSettingsInitialized = $state(true)
export let fromWorkspaceSettings: boolean = false
export let readOnlyMode: boolean
export let initialFileKey: { s3: string; storage?: string } | undefined = undefined
let initialFileKeyInternalCopy: { s3: string; storage?: string }
export let selectedFileKey: { s3: string; storage?: string } | undefined = undefined
export let folderOnly = false
export let regexFilter: RegExp | undefined = undefined
interface Props {
fromWorkspaceSettings?: boolean
readOnlyMode: boolean
initialFileKey?: { s3: string; storage?: string } | undefined
selectedFileKey?: { s3: string; storage?: string } | undefined
folderOnly?: boolean
regexFilter?: RegExp | undefined
}
let csvSeparatorChar: string = ','
let csvHasHeader: boolean = true
let {
fromWorkspaceSettings = false,
readOnlyMode,
initialFileKey = $bindable(undefined),
selectedFileKey = $bindable(undefined),
folderOnly = false,
regexFilter = undefined
}: Props = $props()
let csvSeparatorChar: string = $state(',')
let csvHasHeader: boolean = $state(true)
let dispatch = createEventDispatcher<{
close: { s3: string; storage: string | undefined } | undefined
}>()
let drawer: Drawer
let drawer: Drawer | undefined = $state()
let fileInfoLoading: boolean = true
let fileListLoading: boolean = true
let fileInfoLoading: boolean = $state(true)
let fileListLoading: boolean = $state(true)
let allFilesByKey: Record<
string,
{
@@ -69,10 +82,10 @@
nestingLevel: number
count: number
}
> = {}
let displayedFileKeys: string[] = []
> = $state({})
let displayedFileKeys: string[] = $state([])
let listDivHeight: number = 0
let listDivHeight: number = $state(0)
let fileMetadata:
| {
@@ -82,29 +95,28 @@
sizeStr: string | undefined
lastModified: string | undefined
}
| undefined = undefined
let filePreviewLoading: boolean = false
| undefined = $state(undefined)
let filePreviewLoading: boolean = $state(false)
let filePreview:
| {
fileKey: string
contentPreview: string | undefined
contentType: string | undefined
}
| undefined = undefined
| undefined = $state(undefined)
let listMarkers: string[]
let page = 0
let page = $state(0)
const maxKeys = 1000
let count = 0
let displayedCount = 0
let count = $state(0)
let displayedCount = $state(0)
let filter = ''
let filter = $state('')
let timeout: NodeJS.Timeout | undefined = undefined
let firstLoad = true
$: filter != undefined && onFilterChange()
function onFilterChange() {
if (!firstLoad) {
@@ -117,7 +129,7 @@
}
}
let lastKeyFolders: string[] = []
let lastKeyFolders: string[] = $state([])
async function loadFiles() {
fileListLoading = true
let availableFiles = await HelpersService.listStoredFiles({
@@ -364,7 +376,7 @@
await loadFileMetadataPlusPreviewAsync(selectedFileKey.s3)
}
let storage: string | undefined = undefined
let storage: string | undefined = $state(undefined)
export async function open(
preSelectedFileKey: { s3: string; storage?: string } | undefined = undefined
) {
@@ -374,7 +386,7 @@
selectedFileKey = { ...preSelectedFileKey }
}
reloadContent()
drawer.openDrawer?.()
drawer?.openDrawer?.()
}
async function reloadContent() {
@@ -405,14 +417,14 @@
}
async function selectAndClose() {
drawer.closeDrawer?.()
drawer?.closeDrawer?.()
}
async function exit() {
if (initialFileKeyInternalCopy !== undefined) {
selectedFileKey = { ...initialFileKeyInternalCopy }
}
drawer.closeDrawer?.()
drawer?.closeDrawer?.()
}
function selectItem(index: number, toggleCollapsed: boolean = true) {
@@ -463,10 +475,13 @@
loadFileMetadataPlusPreviewAsync(selectedFileKey.s3)
}
}
run(() => {
filter != undefined && untrack(() => onFilterChange())
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<Drawer
bind:this={drawer}
on:close={() => {
@@ -551,51 +566,55 @@
itemCount={displayedFileKeys.length}
itemSize={42}
>
<div
slot="item"
let:index
let:style
{style}
class={twMerge(
'hover:bg-surface-hover border-b',
index === displayedFileKeys.length - 1 && 'border-b-0'
)}
>
{#snippet header()}{/snippet}
{#snippet footer()}{/snippet}
{#snippet children({ index, style })}
{@const file_info = allFilesByKey[displayedFileKeys[index]]}
{#if file_info}
<div
on:click={() => selectItem(index)}
class={twMerge(
'flex flex-row h-full font-semibold text-xs items-center justify-start',
selectedFileKey !== undefined && selectedFileKey.s3 === file_info.full_key
? 'bg-surface-hover'
: ''
)}
>
<div
{style}
class={twMerge(
'hover:bg-surface-hover border-b',
index === displayedFileKeys.length - 1 && 'border-b-0'
)}
>
{#if file_info}
<div
class={`flex flex-row w-full gap-2 h-full items-center`}
style={`margin-left: ${(2 + file_info.nestingLevel) * 0.25}rem;`}
onclick={() => selectItem(index)}
class={twMerge(
'flex flex-row h-full font-semibold text-xs items-center justify-start',
selectedFileKey !== undefined &&
selectedFileKey.s3 === file_info.full_key
? 'bg-surface-hover'
: ''
)}
>
{#if file_info.type === 'folder'}
{#if file_info.collapsed}<FolderClosed size={16} />{:else}<FolderOpen
size={16}
/>{/if}
<div class="truncate text-ellipsis w-56">
{file_info.display_name} ({file_info.count}{count % 1000 === 0 &&
lastKeyFolders[file_info.nestingLevel / 2] === file_info.display_name
? '+'
: ''} item{file_info.count === 1 ? '' : 's'})
</div>
{:else}
<FileIcon size={16} />
<div class="truncate text-ellipsis w-56">
{file_info.display_name}
</div>
{/if}
<div
class={`flex flex-row w-full gap-2 h-full items-center`}
style={`margin-left: ${(2 + file_info.nestingLevel) * 0.25}rem;`}
>
{#if file_info.type === 'folder'}
{#if file_info.collapsed}<FolderClosed size={16} />{:else}<FolderOpen
size={16}
/>{/if}
<div class="truncate text-ellipsis w-56">
{file_info.display_name} ({file_info.count}{count % 1000 === 0 &&
lastKeyFolders[file_info.nestingLevel / 2] ===
file_info.display_name
? '+'
: ''} item{file_info.count === 1 ? '' : 's'})
</div>
{:else}
<FileIcon size={16} />
<div class="truncate text-ellipsis w-56">
{file_info.display_name}
</div>
{/if}
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
{/snippet}
</VirtualList>
</div>
<div
@@ -643,42 +662,44 @@
{:else}
<div class="p-4 gap-2">
<Section label={fileMetadata.fileKey} breakAll>
<div slot="action" class="flex gap-2">
{#if filePreview !== undefined}
<Button
title="Download file from S3"
variant="border"
color="light"
href={`${base}/api/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(
fileMetadata?.fileKey ?? ''
)}${storage ? `&storage=${storage}` : ''}`}
download={fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'}
startIcon={{ icon: Download }}
iconOnly={true}
/>
<Button
title="Move file"
variant="border"
color="light"
on:click={() => {
moveDestKey = fileMetadata?.fileKey ?? ''
moveModalOpen = true
}}
startIcon={{ icon: MoveRight }}
iconOnly={true}
/>
<Button
title="Delete file from S3"
variant="border"
color="red"
on:click={() => {
deletionModalOpen = true
}}
startIcon={{ icon: Trash }}
iconOnly={true}
/>
{/if}
</div>
{#snippet action()}
<div class="flex gap-2">
{#if filePreview !== undefined}
<Button
title="Download file from S3"
variant="border"
color="light"
href={`${base}/api/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(
fileMetadata?.fileKey ?? ''
)}${storage ? `&storage=${storage}` : ''}`}
download={fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'}
startIcon={{ icon: Download }}
iconOnly={true}
/>
<Button
title="Move file"
variant="border"
color="light"
on:click={() => {
moveDestKey = fileMetadata?.fileKey ?? ''
moveModalOpen = true
}}
startIcon={{ icon: MoveRight }}
iconOnly={true}
/>
<Button
title="Delete file from S3"
variant="border"
color="red"
on:click={() => {
deletionModalOpen = true
}}
startIcon={{ icon: Trash }}
iconOnly={true}
/>
{/if}
</div>
{/snippet}
</Section>
<TableSimple
headers={['Last modified', 'Size', 'Type']}
@@ -721,7 +742,7 @@
<select
class="h-8"
bind:value={csvSeparatorChar}
on:change={(e) =>
onchange={(e) =>
loadFilePreview(
fileMetadata?.fileKey ?? '',
fileMetadata?.size,
@@ -737,19 +758,20 @@
Header row:
<div class="inline-flex item-center w-4 ml-2 mr-2">
<input
on:focus
on:click
onfocus={bubble('focus')}
onclick={bubble('click')}
disabled={false}
type="checkbox"
id="csv-header"
class="h-5"
bind:checked={csvHasHeader}
on:change|stopPropagation={(e) =>
onchange={stopPropagation((e) =>
loadFilePreview(
fileMetadata?.fileKey ?? '',
fileMetadata?.size,
fileMetadata?.mimeType
)}
)
)}
/>
</div>
{:else}
@@ -766,27 +788,29 @@
</div>
{/if}
<div slot="actions" class="flex gap-1">
{#if !readOnlyMode}
<Button
variant="border"
color="light"
disabled={workspaceSettingsInitialized === false}
startIcon={{ icon: FileUp }}
on:click={() => {
uploadModalOpen = true
}}>Upload File</Button
>
{#if !fromWorkspaceSettings}
{#snippet actions()}
<div class="flex gap-1">
{#if !readOnlyMode}
<Button
disabled={selectedFileKey === undefined ||
emptyString(selectedFileKey.s3) ||
(folderOnly && allFilesByKey[selectedFileKey.s3]?.type !== 'folder')}
on:click={selectAndClose}>Select</Button
variant="border"
color="light"
disabled={workspaceSettingsInitialized === false}
startIcon={{ icon: FileUp }}
on:click={() => {
uploadModalOpen = true
}}>Upload File</Button
>
{#if !fromWorkspaceSettings}
<Button
disabled={selectedFileKey === undefined ||
emptyString(selectedFileKey.s3) ||
(folderOnly && allFilesByKey[selectedFileKey.s3]?.type !== 'folder')}
on:click={selectAndClose}>Select</Button
>
{/if}
{/if}
{/if}
</div>
</div>
{/snippet}
</DrawerContent>
</Drawer>
@@ -3,7 +3,7 @@
import { InputService, type Input, type RunnableType } from '$lib/gen/index.js'
import { userStore, workspaceStore } from '$lib/stores.js'
import { sendUserToast } from '$lib/utils.js'
import { createEventDispatcher, onDestroy } from 'svelte'
import { createEventDispatcher, onDestroy, untrack } from 'svelte'
import { Edit, Trash2, Save } from 'lucide-svelte'
import Toggle from './Toggle.svelte'
import { Cell } from './table/index'
@@ -13,13 +13,25 @@
import { twMerge } from 'tailwind-merge'
import SavedInputsPickerViewer from './SavedInputsPickerViewer.svelte'
export let previewArgs: any = undefined
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let isValid: boolean = false
export let noButton: boolean = false
export let jsonView: boolean = false
export let limitPayloadSize: boolean = false
interface Props {
previewArgs?: any
runnableId?: string | undefined
runnableType?: RunnableType | undefined
isValid?: boolean
noButton?: boolean
jsonView?: boolean
limitPayloadSize?: boolean
}
let {
previewArgs = undefined,
runnableId = undefined,
runnableType = undefined,
isValid = false,
noButton = false,
jsonView = false,
limitPayloadSize = false
}: Props = $props()
interface EditableInput extends Input {
isEditing?: boolean
@@ -30,13 +42,13 @@
getFullPayload?: () => Promise<any>
}
let infiniteList: InfiniteList | null = null
let draft = true
let selectedInput: string | null = null
let isEditing: EditableInput | null = null
let viewerOpen = false
let openStates: Record<string, boolean> = {}
let clientWidth: number = 0
let infiniteList: InfiniteList | null = $state(null)
let draft = $state(true)
let selectedInput: string | null = $state(null)
let isEditing: EditableInput | null = $state(null)
let viewerOpen = $state(false)
let openStates: Record<string, boolean> = $state({})
let clientWidth: number = $state(0)
const dispatch = createEventDispatcher()
@@ -125,7 +137,7 @@
const fullPayload = await input.getFullPayload?.()
dispatch('select', fullPayload)
} else {
selectedArgs = structuredClone(input.payloadData ?? {})
selectedArgs = structuredClone($state.snapshot(input.payloadData) ?? {})
dispatch('select', selectedArgs)
}
}
@@ -185,25 +197,27 @@
viewerOpen = Object.values(openStates).some((state) => state)
}
$: $workspaceStore &&
runnableId &&
runnableType &&
(infiniteList && initLoadInputs(), (draft = false))
$effect(() => {
$workspaceStore &&
runnableId &&
runnableType &&
(infiniteList && untrack(() => initLoadInputs()), (draft = false))
})
</script>
<svelte:window on:keydown={handleKeydown} />
<svelte:window onkeydown={handleKeydown} />
<div class="w-full flex flex-col gap-1 h-full overflow-y-auto">
{#if !noButton}
<div>
<Popover class="w-full" placement="bottom" disablePopup={runnableId && previewArgs}>
<svelte:fragment slot="text">
{#snippet text()}
{#if !runnableId}
Save draft first before you can save inputs
{:else if !previewArgs}
Add inputs before saving
{/if}
</svelte:fragment>
{/snippet}
<SaveInputsButton
{runnableId}
{runnableType}
@@ -225,13 +239,13 @@
on:error={(e) => handleError(e.detail)}
on:select={(e) => handleSelect(e.detail)}
>
<svelte:fragment slot="columns">
{#snippet columns()}
<colgroup>
<col class="w-8" />
<col />
</colgroup>
</svelte:fragment>
<svelte:fragment let:item let:hover>
{/snippet}
{#snippet children({ item, hover })}
{@const editOptions =
item.created_by == $userStore?.username ||
$userStore?.is_admin ||
@@ -249,7 +263,7 @@
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
{#if isEditing && isEditing.id === item.id}
<form
on:submit={() => {
onsubmit={() => {
updateInput(isEditing)
setEditing(null)
}}
@@ -336,10 +350,10 @@
</div>
</div>
</Cell>
</svelte:fragment>
<svelte:fragment slot="empty">
{/snippet}
{#snippet empty()}
<div class="text-center text-xs text-tertiary">No saved Inputs</div>
</svelte:fragment>
{/snippet}
</InfiniteList>
{/if}
</div>
@@ -13,15 +13,28 @@
import MultiSelect from './multiselect/MultiSelectWrapper.svelte'
const dispatch = createEventDispatcher()
export let scriptHash: string | null = null
export let scriptPath: string | null = null
export let flowPath: string | null = null
export let inputSelected: 'saved' | 'history' | undefined = undefined
export let jsonView: boolean = false
export let schema: any
// Are the current Inputs valid and able to be saved?
export let isValid: boolean
export let args: object
interface Props {
scriptHash?: string | null
scriptPath?: string | null
flowPath?: string | null
inputSelected?: 'saved' | 'history' | undefined
jsonView?: boolean
schema: any
// Are the current Inputs valid and able to be saved?
isValid: boolean
args: object
}
let {
scriptHash = null,
scriptPath = null,
flowPath = null,
inputSelected = $bindable(undefined),
jsonView = false,
schema,
isValid,
args
}: Props = $props()
export function resetSelected() {
historicInputs?.resetSelected(true)
@@ -29,18 +42,14 @@
}
let savedArgs: any = undefined
let runnableType: RunnableType | undefined = undefined
let savedInputsPicker: SavedInputsPicker | undefined = undefined
let historicInputs: HistoricInputs | undefined = undefined
let savedInputsPicker: SavedInputsPicker | undefined = $state(undefined)
let historicInputs: HistoricInputs | undefined = $state(undefined)
$: runnableId = scriptHash || scriptPath || flowPath || undefined
$: runnableType = scriptHash
? 'ScriptHash'
: scriptPath
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
let runnableId = $derived(scriptHash || scriptPath || flowPath || undefined)
let runnableType: RunnableType | undefined = $derived(
scriptHash ? 'ScriptHash' : scriptPath ? 'ScriptPath' : flowPath ? 'FlowPath' : undefined
)
function selectArgs(selected_args: any, type: 'saved' | 'history' | undefined) {
if (selected_args) {
@@ -55,18 +64,18 @@
}
}
let searchArgs = {}
let appliedSearchArgs = {}
let searchArgs = $state({})
let appliedSearchArgs = $state({})
let searchArgsFields = [] as string[]
let searchArgsFields = $state([] as string[])
$: emptySearchArgs = Object.keys(appliedSearchArgs).length === 0
let emptySearchArgs = $derived(Object.keys(appliedSearchArgs).length === 0)
$: filteredSchema = {
let filteredSchema = $derived({
properties: Object.fromEntries(
Object.entries(schema?.properties ?? {}).filter(([key]) => searchArgsFields.includes(key))
)
}
})
</script>
<div class="min-w-[300px] h-full flex flex-col">
@@ -78,7 +87,7 @@
wrapperClass="h-full"
small={true}
>
<svelte:fragment slot="action">
{#snippet action()}
<SaveInputsButton
{args}
disabled={!isValid || jsonView}
@@ -88,7 +97,7 @@
savedInputsPicker?.refresh()
}}
/>
</svelte:fragment>
{/snippet}
<SavedInputsPicker
bind:this={savedInputsPicker}
@@ -107,7 +116,7 @@
<Pane class="px-4 py-4 h-full">
<Section label="History" wrapperClass="h-full" small={true}>
<svelte:fragment slot="action">
{#snippet action()}
<div class="flex space-x-2">
<Popover
class="w-fit"
@@ -118,7 +127,7 @@
}}
floatingConfig={{ placement: 'top-end' }}
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button
variant="contained"
size="xs2"
@@ -130,9 +139,9 @@
>
{emptySearchArgs ? '' : 'Active filters'}
</Button>
</svelte:fragment>
{/snippet}
<svelte:fragment slot="content">
{#snippet content()}
<div id="multi-select-search"></div>
<div class="p-2 overflow-auto max-h-[400px] min-h-[300px] w-[400px]">
<div class="flex items-center flex-wrap gap-x-2 justify-between">
@@ -153,7 +162,7 @@
{/if}
<Button
on:click={async () => {
appliedSearchArgs = structuredClone(searchArgs)
appliedSearchArgs = structuredClone($state.snapshot(searchArgs))
await tick()
historicInputs?.refresh(true)
}}
@@ -180,7 +189,7 @@
<div class="text-secondary text-sm my-8 mx-auto">No filters</div>
{/if}
</div>
</svelte:fragment>
{/snippet}
</Popover>
<Button
size="xs2"
@@ -194,7 +203,7 @@
All runs
</Button>
</div>
</svelte:fragment>
{/snippet}
<HistoricInputs
bind:this={historicInputs}
{runnableId}
+193 -123
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import type { Schema } from '$lib/common'
import { VariableService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -11,103 +14,139 @@
import { getResourceTypes } from './resourceTypesStore'
import { Plus } from 'lucide-svelte'
import ArgInput from './ArgInput.svelte'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { deepEqual } from 'fast-equals'
import { dragHandleZone, type Options as DndOptions } from '@windmill-labs/svelte-dnd-action'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { ComponentCustomCSS } from './apps/types'
export let schema: Schema | any
export let hiddenArgs: string[] = []
export let schemaFieldTooltip: Record<string, string> = {}
export let args: Record<string, any> = {}
export let disabledArgs: string[] = []
export let disabled = false
interface Props {
schema: Schema | any
hiddenArgs?: string[]
schemaFieldTooltip?: Record<string, string>
args?: Record<string, any>
disabledArgs?: string[]
disabled?: boolean
isValid?: boolean
autofocus?: boolean
defaultValues?: Record<string, any>
shouldHideNoInputs?: boolean
compact?: boolean
linkedSecret?: string | undefined
linkedSecretCandidates?: string[] | undefined
noVariablePicker?: boolean
flexWrap?: boolean
noDelete?: boolean
prettifyHeader?: boolean
disablePortal?: boolean
showSchemaExplorer?: boolean
showReset?: boolean
onlyMaskPassword?: boolean
dndConfig?: DndOptions | undefined
items?: { id: string; value: string }[] | undefined
helperScript?:
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
| { type: 'hash'; hash: string }
| undefined
lightHeader?: boolean
diff?: Record<string, SchemaDiff>
nestedParent?: { label: string; nestedParent: any | undefined } | undefined
shouldDispatchChanges?: boolean
nestedClasses?: string
dynamicEnums?: Record<string, any>
largeGap?: boolean
css?: ComponentCustomCSS<'schemaformcomponent'> | undefined
displayType?: boolean
appPath?: string | undefined
className?: string
computeS3ForceViewerPolicies?:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined
workspace?: string | undefined
actions?: import('svelte').Snippet
}
export let isValid: boolean = true
export let autofocus = false
export let defaultValues: Record<string, any> = {}
export let shouldHideNoInputs: boolean = false
export let compact = false
export let linkedSecret: string | undefined = undefined
export let linkedSecretCandidates: string[] | undefined = undefined
export let noVariablePicker = false
export let flexWrap = false
export let noDelete = false
export let prettifyHeader = false
export let disablePortal = false
export let showSchemaExplorer = false
export let showReset = false
export let onlyMaskPassword = false
export let dndConfig: DndOptions | undefined = undefined
export let items: { id: string; value: string }[] | undefined = undefined
export let helperScript:
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
| { type: 'hash'; hash: string }
| undefined = undefined
export let lightHeader = false
export let diff: Record<string, SchemaDiff> = {}
export let nestedParent: { label: string; nestedParent: any | undefined } | undefined = undefined
export let shouldDispatchChanges = false
export let nestedClasses = ''
export let dynamicEnums: Record<string, any> = {}
export let largeGap: boolean = false
export let css: ComponentCustomCSS<'schemaformcomponent'> | undefined = undefined
export let displayType: boolean = true
export let appPath: string | undefined = undefined
export let computeS3ForceViewerPolicies:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined = undefined
export let workspace: string | undefined = undefined
let {
schema = $bindable(),
hiddenArgs = [],
schemaFieldTooltip = {},
args = $bindable(undefined),
disabledArgs = [],
disabled = false,
isValid = $bindable(true),
autofocus = false,
defaultValues = {},
shouldHideNoInputs = false,
compact = false,
linkedSecret = $bindable(undefined),
linkedSecretCandidates = undefined,
noVariablePicker = false,
flexWrap = false,
noDelete = false,
prettifyHeader = false,
disablePortal = false,
showSchemaExplorer = false,
showReset = false,
onlyMaskPassword = false,
dndConfig = undefined,
items = undefined,
helperScript = undefined,
lightHeader = false,
diff = {},
nestedParent = undefined,
shouldDispatchChanges = false,
nestedClasses = '',
dynamicEnums = {},
largeGap = false,
css = undefined,
displayType = true,
appPath = undefined,
className = '',
computeS3ForceViewerPolicies = undefined,
workspace = undefined,
actions
}: Props = $props()
const dispatch = createEventDispatcher()
let inputCheck: { [id: string]: boolean } = {}
$: if (args == undefined || typeof args !== 'object') {
args = {}
}
let inputCheck: { [id: string]: boolean } = $state({})
export function setDefaults() {
const nargs = structuredClone(defaultValues)
const nargs = structuredClone($state.snapshot(defaultValues))
Object.keys(schema?.properties ?? {}).forEach((key) => {
if (schema?.properties[key].default != undefined && args[key] == undefined) {
if (schema?.properties[key].default != undefined && args && args[key] == undefined) {
let value = schema?.properties[key].default
nargs[key] = value === 'object' ? structuredClone(value) : value
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
}
})
args = nargs
}
let keys: string[]
$: keys = Array.isArray(schema?.order) ? schema?.order : Object.keys(schema?.properties ?? {})
let keys: string[] = $state([])
function removeExtraKey() {
const nargs = {}
Object.keys(args ?? {}).forEach((key) => {
if (keys.includes(key)) {
if (keys.includes(key) && args) {
nargs[key] = args[key]
}
})
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let pickForField: string | undefined = $state()
let itemPicker: ItemPicker | undefined = $state(undefined)
let variableEditor: VariableEditor | undefined = $state(undefined)
let resourceTypes: string[] | undefined = undefined
let resourceTypes: string[] | undefined = $state(undefined)
async function loadResourceTypes() {
resourceTypes = await getResourceTypes()
@@ -115,8 +154,6 @@
loadResourceTypes()
$: schema && (reorder(), (hidden = {}))
function hasExtraKeys() {
return Object.keys(args ?? {}).some((x) => !keys.includes(x))
}
@@ -147,20 +184,26 @@
dispatch('change')
}
}
// let missingKeys = keys.filter((x) => args && !(x in args))
// console.log('missingKeys', missingKeys)
// missingKeys.forEach((x) => {
// if (args) {
// args[x] = undefined
// }
// })
if (!noDelete && hasExtraKeys()) {
removeExtraKey()
}
}
$: fields = items ?? keys.map((x) => ({ id: x, value: x }))
let hidden: Record<string, boolean> = {}
let hidden: Record<string, boolean> = $state({})
let fields = $derived(items ?? keys.map((x) => ({ id: x, value: x })))
function handleHiddenFields(schema: Schema | any, args: Record<string, any>) {
for (const x of fields) {
if (schema?.properties[x.value]?.showExpr) {
if (computeShow(x.value, schema.properties[x.value].showExpr, args)) {
if (schema?.properties?.[x.value]?.showExpr) {
if (computeShow(x.value, schema.properties?.[x.value]?.showExpr, args)) {
hidden[x.value] = false
} else if (!hidden[x.value]) {
hidden[x.value] = true
@@ -173,12 +216,37 @@
}
}
$: handleHiddenFields(schema, args)
$effect.pre(() => {
if (args == undefined || typeof args !== 'object') {
args = {}
}
})
$effect.pre(() => {
const newKeys = [
...new Set(
(Array.isArray(schema?.order)
? schema?.order
: Object.keys(schema?.properties ?? {})) as string[]
)
]
$: isValid = allTrue(inputCheck ?? {})
if (!deepEqual(keys, newKeys)) {
keys = newKeys
}
})
$effect.pre(() => {
schema && (untrack(() => reorder()), (hidden = {}))
})
$effect.pre(() => {
;[schema, args]
untrack(() => handleHiddenFields(schema, args ?? {}))
})
$effect.pre(() => {
isValid = allTrue(inputCheck ?? {})
})
const actions_render = $derived(actions)
</script>
<!-- {JSON.stringify(args)} -->
{#if showReset}
<div class="flex flex-row-reverse w-full">
<Button size="xs" color="light" on:click={() => setDefaults()}>
@@ -188,14 +256,12 @@
{/if}
<div
class="w-full {$$props.class} {flexWrap
? 'flex flex-row flex-wrap gap-x-6 '
: ''} {nestedClasses}"
class="w-full {className} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''} {nestedClasses}"
use:dragHandleZone={dndConfig ?? { items: [], dragDisabled: true }}
on:finalize
on:consider
onfinalize={bubble('finalize')}
onconsider={bubble('consider')}
>
{#if keys.length > 0}
{#if keys.length > 0 && args}
{#each fields as item, i (item.id)}
{@const argName = item.value}
<div
@@ -203,7 +269,7 @@
? 'bg-red-300 dark:bg-red-800 rounded-md'
: ''}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
{#if !hiddenArgs.includes(argName) && keys.includes(argName)}
{#if typeof diff[argName] === 'object' && diff[argName].oldSchema}
{@const formerProperty = diff[argName].oldSchema}
@@ -221,7 +287,8 @@
required={formerProperty?.required}
pattern={formerProperty?.pattern}
valid={inputCheck[argName]}
defaultValue={defaultValues?.[argName] ?? structuredClone(formerProperty?.default)}
defaultValue={defaultValues?.[argName] ??
structuredClone($state.snapshot(formerProperty?.default))}
enum_={dynamicEnums?.[argName] ?? formerProperty?.enum}
format={formerProperty?.format}
contentEncoding={formerProperty?.contentEncoding}
@@ -257,10 +324,10 @@
/>
</div>
{/if}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex flex-row items-center {largeGap ? 'pb-4' : ''} "
on:click={() => {
onclick={() => {
dispatch('click', argName)
}}
>
@@ -292,7 +359,7 @@
pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={defaultValues?.[argName] ??
structuredClone(schema.properties[argName].default)}
structuredClone($state.snapshot(schema.properties[argName].default))}
enum_={dynamicEnums?.[argName] ?? schema.properties[argName].enum}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
@@ -330,8 +397,8 @@
{css}
{displayType}
>
<svelte:fragment slot="actions">
<slot name="actions" />
{#snippet actions()}
{@render actions_render?.()}
{#if linkedSecretCandidates?.includes(argName)}
<div>
<ToggleButtonGroup
@@ -343,30 +410,31 @@
linkedSecret = undefined
}
}}
let:item
>
<ToggleButton
value="inlined"
size="sm"
label="Inlined"
tooltip="The value is inlined in the resource and thus has no special treatment."
{item}
/>
<ToggleButton
position="right"
value="secret"
size="sm"
label="Secret"
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
{item}
/>
{#snippet children({ item })}
<ToggleButton
value="inlined"
size="sm"
label="Inlined"
tooltip="The value is inlined in the resource and thus has no special treatment."
{item}
/>
<ToggleButton
position="right"
value="secret"
size="sm"
label="Secret"
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>{/if}</svelte:fragment
>
</div>{/if}
{/snippet}
</ArgInput>
{/if}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
{/if}
</div>
{/if}
@@ -380,7 +448,7 @@
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
if (pickForField && args) {
args[pickForField] = '$var:' + path
}
}}
@@ -394,17 +462,19 @@
...x
}))}
>
<div slot="submission">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => variableEditor?.initNew?.()}
>
New variable
</Button>
</div>
{#snippet submission()}
<div>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => variableEditor?.initNew?.()}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<VariableEditor bind:this={variableEditor} />
@@ -94,7 +94,7 @@
<div class="h-fit">
<div class="relative z-[100000] w-full">
<div class="absolute" style="right: calc({100 - inputPanelSize}%); top: -1px;">
<SideBarTab expandRight={rightPanelOpen} {dropdownItems} fullMenu={true} noTrigger={true} />
<SideBarTab {dropdownItems} fullMenu={true} noTrigger={true} />
</div>
</div>
<Splitpanes class={!rightPanelOpen ? 'splitter-hidden' : ''}>

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