mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-18 16:02:29 +00:00
Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06898b9837 | ||
|
|
8306e6e5d2 | ||
|
|
6c1e7eed51 | ||
|
|
f24894b1a5 | ||
|
|
50d78a979b | ||
|
|
c75fd751b0 | ||
|
|
f0f720f490 | ||
|
|
855571f359 | ||
|
|
41219e1f84 | ||
|
|
499fe7e7fe | ||
|
|
72613f3580 | ||
|
|
7d4ee825fa | ||
|
|
1cd253405d | ||
|
|
4dab80e24d | ||
|
|
38bb5e25cb | ||
|
|
3678bf976e | ||
|
|
6623972ee9 | ||
|
|
6a4391f616 | ||
|
|
f7a83c03c1 | ||
|
|
82f541565f | ||
|
|
44e1ed6c9c | ||
|
|
58ca3a6866 | ||
|
|
0b3a084c81 | ||
|
|
17c8c8a561 | ||
|
|
1a6283b42a | ||
|
|
126b610561 | ||
|
|
0b89260540 | ||
|
|
6c4818259c | ||
|
|
283167264e | ||
|
|
b3e7a9cb79 | ||
|
|
c3a54ddd7c | ||
|
|
ae684d8608 | ||
|
|
f077849b8f | ||
|
|
cf2d09e7a8 | ||
|
|
bb354cfeb8 | ||
|
|
bebfa76fc2 | ||
|
|
b0ac12052a | ||
|
|
f412ede6ed | ||
|
|
f94651115a | ||
|
|
6f907c79b4 | ||
|
|
6e7ea4111c | ||
|
|
ff9acb4717 | ||
|
|
a393c93085 | ||
|
|
23920aee84 | ||
|
|
d47c078bb5 | ||
|
|
b4a6a7e724 | ||
|
|
332f66e348 | ||
|
|
d2dfd27b88 | ||
|
|
b56a5c3e7a |
@@ -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")
|
||||
|
||||
@@ -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 }}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
+22
@@ -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"
|
||||
}
|
||||
+38
@@ -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"
|
||||
}
|
||||
-38
@@ -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"
|
||||
}
|
||||
Vendored
+3
-1
@@ -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 }
|
||||
}
|
||||
|
||||
Generated
+25
-5
@@ -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 @@
|
||||
ab4f856708a056973faea19f10eda5efff546176
|
||||
2c3e21f4573486628e0b8969ff478c237bd0283f
|
||||
|
||||
+7
-2
@@ -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");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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 & {
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+26
-27
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 <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">
|
||||
|
||||
@@ -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, ''')
|
||||
}
|
||||
|
||||
$: 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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user