mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 08:16:49 +00:00
Merge remote-tracking branch 'origin/main' into di/svelte5-migration
This commit is contained in:
@@ -73,3 +73,7 @@ profiles:
|
||||
3) Upload to asciinema.org:
|
||||
XDG_DATA_HOME=/tmp/.local/share \
|
||||
asciinema upload --server-url https://asciinema.org /tmp/demo.cast
|
||||
|
||||
linkedRepos:
|
||||
- repo: windmill-labs/windmill-ee-private
|
||||
alias: ee
|
||||
+1
-1
@@ -11,7 +11,7 @@ worktree_prefix: ""
|
||||
window_prefix: "wm-"
|
||||
|
||||
auto_name:
|
||||
model: "claude-sonnet-4.6"
|
||||
model: "gemini-2.5-flash-lite"
|
||||
system_prompt: |
|
||||
Generate a concise git branch name based on the task description.
|
||||
|
||||
|
||||
+10
-2
@@ -65,7 +65,7 @@ Setting up zsh autocomplete is also recommended — see the [workmux docs](https
|
||||
Each worktree is assigned a **slot** that determines its ports:
|
||||
|
||||
| Slot | Backend | Frontend |
|
||||
|------|---------|----------|
|
||||
| ---- | ------- | -------- |
|
||||
| 0 | 8000 | 3000 |
|
||||
| 1 | 8010 | 3010 |
|
||||
| 2 | 8020 | 3020 |
|
||||
@@ -192,7 +192,6 @@ sandbox:
|
||||
|
||||
This mounts both the main EE repo (used by the main worktree) and the EE worktrees directory (used by feature worktrees) into every sandbox container.
|
||||
|
||||
|
||||
## Cursor SSH Integration (`wmc`)
|
||||
|
||||
`wm-cursor` (aliased as `wmc`) gives each worktree its own Cursor SSH remote window with an independently-focused tmux session. All windows are visible in the status bar across all Cursor terminals, but each one is focused on its own worktree.
|
||||
@@ -219,6 +218,7 @@ This:
|
||||
1. **Merges `.vscode/settings.json`** — adds the `wm-tmux` terminal profile (auto-attaches to the `main` tmux session), disables auto port forwarding, configures forwarding for ports 8000/3000/5432, and stops rust-analyzer from auto-starting. Existing settings are preserved.
|
||||
2. **Creates `.vscode/tasks.json`** — auto-starts the dev database (`start-dev-db.sh`) when the folder opens.
|
||||
3. **Adds `wmc` alias to `~/.zshrc`** — so you can use `wmc` from any tmux window.
|
||||
4. **Adds `eval "$(wmc completions)"`** to `~/.zshrc` — provides tab-completion for subcommands and worktree names (for `open`, `open-ee`, and `close`).
|
||||
|
||||
After setup, reopen Cursor's terminal to pick up the new profile.
|
||||
|
||||
@@ -240,6 +240,14 @@ This runs `workmux add`, creates a grouped tmux session, writes `.vscode/setting
|
||||
wmc open my-feature
|
||||
```
|
||||
|
||||
**Open the EE worktree in Cursor (no tmux session):**
|
||||
|
||||
```bash
|
||||
wmc open-ee my-feature
|
||||
```
|
||||
|
||||
This finds the matching `windmill-ee-private__worktrees/<name>` directory and opens it in a new Cursor window.
|
||||
|
||||
**Close a worktree's Cursor window and tmux window (keeps the worktree):**
|
||||
|
||||
```bash
|
||||
|
||||
+2
-1
@@ -20,7 +20,8 @@
|
||||
"resource",
|
||||
"variable",
|
||||
"ducklake",
|
||||
"datatable"
|
||||
"datatable",
|
||||
"volume"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -46,11 +46,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO token (token, email, label, expiration, scopes, workspace_id)\n VALUES ($1, $2, $3, now() + ($4 || ' seconds')::interval, $5, $6)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7e4aa6b19b110bca423b3a3f428826d92b9808c64ef989fef2142bc8e02d6630"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )",
|
||||
"query": "SELECT email FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%@', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,5 +19,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0ef37117c369f03236e18f9dbb1f3d52776c8cb73f2507199c6ca16d4d2405ba"
|
||||
"hash": "886a921adc115f0a9c6f3a68381bd8f5a16866135120175d9073b9b2c41bbd51"
|
||||
}
|
||||
+2
-1
@@ -16,7 +16,8 @@
|
||||
"resource",
|
||||
"variable",
|
||||
"ducklake",
|
||||
"datatable"
|
||||
"datatable",
|
||||
"volume"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING",
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%@', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -12,5 +12,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2e1d1c59bfc53d58962251822c85cf9a26e3b2888702e5e9d5fc1b082901df09"
|
||||
"hash": "c0fad64e5d707ffa29d236f558e23b608168dc3a1b3857d2ad33ec20627acbff"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes, workspace_id)\n VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"TextArray",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
a4546264d41ce7122dfd127f4b17f724eb63c40a
|
||||
2fb7884849a563bae023574baa2d55fa1fab1176
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'custom_instance_user') THEN
|
||||
ALTER ROLE custom_instance_user NOREPLICATION;
|
||||
END IF;
|
||||
EXCEPTION
|
||||
WHEN others THEN
|
||||
RAISE NOTICE 'Error revoking REPLICATION from custom_instance_user: %', SQLERRM;
|
||||
END
|
||||
$$;
|
||||
@@ -0,0 +1,10 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'custom_instance_user') THEN
|
||||
ALTER ROLE custom_instance_user REPLICATION;
|
||||
END IF;
|
||||
EXCEPTION
|
||||
WHEN others THEN
|
||||
RAISE NOTICE 'Error granting REPLICATION to custom_instance_user: %', SQLERRM;
|
||||
END
|
||||
$$;
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP INDEX IF EXISTS idx_asset_ws_path_kind_recent;
|
||||
|
||||
-- Restore the dropped indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_workspace_created_id ON asset (workspace_id, created_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_kind_path ON asset (workspace_id, kind, path);
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Covering index for the list_assets CTE: GROUP BY (path, kind) + MAX(created_at, id) + ORDER BY
|
||||
-- Includes usage_kind and usage_path to allow full index-only scan (avoiding heap lookups for filter conditions)
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_ws_path_kind_recent
|
||||
ON asset (workspace_id, path, kind, created_at DESC, id DESC)
|
||||
INCLUDE (usage_kind, usage_path);
|
||||
|
||||
-- Drop indexes now subsumed by idx_asset_ws_path_kind_recent:
|
||||
-- idx_asset_workspace_created_id (workspace_id, created_at DESC, id DESC) - only used by list_assets CTE
|
||||
-- idx_asset_kind_path (workspace_id, kind, path) - only used by list_assets CTE/outer join, covered by new index + PK
|
||||
DROP INDEX IF EXISTS idx_asset_workspace_created_id;
|
||||
DROP INDEX IF EXISTS idx_asset_kind_path;
|
||||
@@ -144,7 +144,7 @@ async fn list_assets(
|
||||
format!(
|
||||
r#"FROM asset
|
||||
LEFT JOIN v2_job job_cte ON asset.usage_kind = 'job'
|
||||
AND asset.usage_path = job_cte.id::text
|
||||
AND job_cte.id = CASE WHEN asset.usage_kind = 'job' THEN asset.usage_path::uuid END
|
||||
AND job_cte.workspace_id = $1"#
|
||||
)
|
||||
} else {
|
||||
@@ -209,7 +209,7 @@ async fn list_assets(
|
||||
) = resource.path
|
||||
AND resource.workspace_id = $1
|
||||
LEFT JOIN v2_job job ON asset.usage_kind = 'job'
|
||||
AND asset.usage_path = job.id::text
|
||||
AND job.id = CASE WHEN asset.usage_kind = 'job' THEN asset.usage_path::uuid END
|
||||
AND job.workspace_id = $1
|
||||
WHERE asset.workspace_id = $1
|
||||
AND (asset.kind <> 'resource' OR resource.path IS NOT NULL)
|
||||
|
||||
@@ -12,24 +12,24 @@ use std::{collections::HashMap, time::Duration};
|
||||
mod ee;
|
||||
pub mod ee_oss;
|
||||
|
||||
use windmill_api_auth::{require_super_admin, ApiAuthed};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_api_auth::require_devops_role;
|
||||
use windmill_api_auth::{require_super_admin, ApiAuthed};
|
||||
use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
|
||||
use windmill_common::DB;
|
||||
|
||||
use ee_oss::validate_license_key;
|
||||
use windmill_common::usernames::generate_instance_username_for_all_users;
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
routing::{get, post},
|
||||
body::Body,
|
||||
response::Response,
|
||||
Json, Router,
|
||||
};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use axum::extract::Query;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Extension, Path},
|
||||
response::Response,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -43,9 +43,8 @@ use windmill_common::{
|
||||
get_database_url,
|
||||
global_settings::{
|
||||
APP_WORKSPACED_ROUTE_SETTING, AUTOMATE_USERNAME_CREATION_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING,
|
||||
EMAIL_DOMAIN_SETTING, ENV_SETTINGS, HUB_ACCESSIBLE_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING,
|
||||
ENV_SETTINGS, HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
},
|
||||
instance_config::{self, ApplyMode, InstanceConfig},
|
||||
server::Smtp,
|
||||
@@ -171,7 +170,9 @@ pub async fn test_s3_bucket(
|
||||
.await?
|
||||
.store;
|
||||
|
||||
let mut list = client.list(Some(&windmill_object_store::object_store_reexports::Path::from("".to_string())));
|
||||
let mut list = client.list(Some(
|
||||
&windmill_object_store::object_store_reexports::Path::from("".to_string()),
|
||||
));
|
||||
let first_file = list.next().await;
|
||||
if first_file.is_some() {
|
||||
if let Err(e) = first_file.as_ref().unwrap() {
|
||||
@@ -189,7 +190,10 @@ pub async fn test_s3_bucket(
|
||||
));
|
||||
tracing::info!("Testing blob storage at path: {path}");
|
||||
client
|
||||
.put(&path, windmill_object_store::object_store_reexports::PutPayload::from_static(b"hello"))
|
||||
.put(
|
||||
&path,
|
||||
windmill_object_store::object_store_reexports::PutPayload::from_static(b"hello"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("error writing file to {path}: {e:#}"))?;
|
||||
let content = client
|
||||
@@ -290,17 +294,17 @@ pub async fn set_global_setting_internal(
|
||||
match value {
|
||||
serde_json::Value::Null => {
|
||||
if instance_config::PROTECTED_SETTINGS.contains(&key.as_str()) {
|
||||
return Err(error::Error::BadRequest(
|
||||
format!("{key} is a protected setting and cannot be deleted"),
|
||||
));
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"{key} is a protected setting and cannot be deleted"
|
||||
)));
|
||||
}
|
||||
delete_global_setting(db, &key).await?;
|
||||
}
|
||||
serde_json::Value::String(x) if x.is_empty() => {
|
||||
if instance_config::PROTECTED_SETTINGS.contains(&key.as_str()) {
|
||||
return Err(error::Error::BadRequest(
|
||||
format!("{key} is a protected setting and cannot be set to empty"),
|
||||
));
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"{key} is a protected setting and cannot be set to empty"
|
||||
)));
|
||||
}
|
||||
delete_global_setting(db, &key).await?;
|
||||
}
|
||||
@@ -437,7 +441,8 @@ async fn get_instance_config_yaml(
|
||||
let config = InstanceConfig::from_db(&db)
|
||||
.await
|
||||
.map_err(|e| error::Error::internal_err(e.to_string()))?;
|
||||
let yaml = config.to_sorted_yaml()
|
||||
let yaml = config
|
||||
.to_sorted_yaml()
|
||||
.map_err(|e| error::Error::internal_err(e))?;
|
||||
Response::builder()
|
||||
.header("content-type", "application/yaml")
|
||||
@@ -478,8 +483,7 @@ async fn set_instance_config(
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.clone(),
|
||||
serde_json::to_value(v)
|
||||
.expect("WorkerGroupConfig serialization cannot fail"),
|
||||
serde_json::to_value(v).expect("WorkerGroupConfig serialization cannot fail"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -489,8 +493,7 @@ async fn set_instance_config(
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.clone(),
|
||||
serde_json::to_value(v)
|
||||
.expect("WorkerGroupConfig serialization cannot fail"),
|
||||
serde_json::to_value(v).expect("WorkerGroupConfig serialization cannot fail"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -944,7 +947,8 @@ async fn setup_custom_instance_pg_database_inner(
|
||||
GRANT CREATE ON DATABASE \"{dbname}\" TO custom_instance_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO custom_instance_user;
|
||||
ALTER ROLE custom_instance_user CREATEROLE;"
|
||||
ALTER ROLE custom_instance_user CREATEROLE;
|
||||
ALTER ROLE custom_instance_user REPLICATION;"
|
||||
))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -1083,19 +1087,16 @@ async fn sync_cached_resource_types(
|
||||
use windmill_common::worker::HUB_RT_CACHE_DIR;
|
||||
let cache_path = format!("{}/resource_types.json", HUB_RT_CACHE_DIR);
|
||||
|
||||
let content = tokio::fs::read_to_string(&cache_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error::Error::NotFound(format!(
|
||||
"No cached resource types found at {}: {}",
|
||||
cache_path, e
|
||||
))
|
||||
})?;
|
||||
let content = tokio::fs::read_to_string(&cache_path).await.map_err(|e| {
|
||||
error::Error::NotFound(format!(
|
||||
"No cached resource types found at {}: {}",
|
||||
cache_path, e
|
||||
))
|
||||
})?;
|
||||
|
||||
let cached_types: Vec<CachedResourceType> =
|
||||
serde_json::from_str(&content).map_err(|e| {
|
||||
error::Error::InternalErr(format!("Failed to parse cached resource types: {}", e))
|
||||
})?;
|
||||
let cached_types: Vec<CachedResourceType> = serde_json::from_str(&content).map_err(|e| {
|
||||
error::Error::InternalErr(format!("Failed to parse cached resource types: {}", e))
|
||||
})?;
|
||||
|
||||
let mut synced_count = 0;
|
||||
|
||||
@@ -1176,7 +1177,10 @@ mod tests {
|
||||
deserialized.global_settings.base_url.as_deref(),
|
||||
Some("https://windmill.example.com")
|
||||
);
|
||||
assert_eq!(deserialized.global_settings.retention_period_secs, Some(86400));
|
||||
assert_eq!(
|
||||
deserialized.global_settings.retention_period_secs,
|
||||
Some(86400)
|
||||
);
|
||||
assert_eq!(deserialized.global_settings.expose_metrics, Some(true));
|
||||
let wc = &deserialized.worker_configs["default"];
|
||||
assert_eq!(
|
||||
@@ -1208,9 +1212,7 @@ mod tests {
|
||||
let retention_pos = yaml.find("retention_period_secs:").unwrap();
|
||||
|
||||
assert!(
|
||||
base_url_pos < email_pos
|
||||
&& email_pos < expose_pos
|
||||
&& expose_pos < retention_pos,
|
||||
base_url_pos < email_pos && email_pos < expose_pos && expose_pos < retention_pos,
|
||||
"global_settings keys should be alphabetically sorted, got yaml:\n{yaml}"
|
||||
);
|
||||
}
|
||||
@@ -1220,22 +1222,34 @@ mod tests {
|
||||
let config = InstanceConfig {
|
||||
global_settings: GlobalSettings::default(),
|
||||
worker_configs: BTreeMap::from([
|
||||
("gpu".to_string(), WorkerGroupConfig {
|
||||
init_bash: Some("echo gpu".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
("native".to_string(), WorkerGroupConfig {
|
||||
init_bash: Some("echo native".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
("default".to_string(), WorkerGroupConfig {
|
||||
init_bash: Some("echo default".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
("alpha".to_string(), WorkerGroupConfig {
|
||||
init_bash: Some("echo alpha".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
(
|
||||
"gpu".to_string(),
|
||||
WorkerGroupConfig {
|
||||
init_bash: Some("echo gpu".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"native".to_string(),
|
||||
WorkerGroupConfig {
|
||||
init_bash: Some("echo native".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"default".to_string(),
|
||||
WorkerGroupConfig {
|
||||
init_bash: Some("echo default".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"alpha".to_string(),
|
||||
WorkerGroupConfig {
|
||||
init_bash: Some("echo alpha".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
@@ -1264,26 +1278,40 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
worker_configs: BTreeMap::from([
|
||||
("default".to_string(), WorkerGroupConfig {
|
||||
worker_tags: Some(vec!["deno".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
("native".to_string(), WorkerGroupConfig {
|
||||
init_bash: Some("echo hi".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
(
|
||||
"default".to_string(),
|
||||
WorkerGroupConfig {
|
||||
worker_tags: Some(vec!["deno".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"native".to_string(),
|
||||
WorkerGroupConfig {
|
||||
init_bash: Some("echo hi".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
let yaml = config.to_sorted_yaml().unwrap();
|
||||
let deserialized: InstanceConfig = serde_yml::from_str(&yaml).unwrap();
|
||||
|
||||
assert_eq!(deserialized.global_settings.base_url.as_deref(), Some("https://rt.test"));
|
||||
assert_eq!(deserialized.global_settings.retention_period_secs, Some(7200));
|
||||
assert_eq!(
|
||||
deserialized.global_settings.base_url.as_deref(),
|
||||
Some("https://rt.test")
|
||||
);
|
||||
assert_eq!(
|
||||
deserialized.global_settings.retention_period_secs,
|
||||
Some(7200)
|
||||
);
|
||||
assert_eq!(deserialized.global_settings.expose_metrics, Some(false));
|
||||
assert_eq!(deserialized.worker_configs.len(), 2);
|
||||
assert_eq!(
|
||||
deserialized.worker_configs["default"].worker_tags.as_deref(),
|
||||
deserialized.worker_configs["default"]
|
||||
.worker_tags
|
||||
.as_deref(),
|
||||
Some(["deno".to_string()].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -10680,6 +10680,16 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: resume_button_text
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: cancel_button_text
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Interactive slack approval message sent successfully
|
||||
@@ -10726,6 +10736,16 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: resume_button_text
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: cancel_button_text
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Interactive slack approval message sent successfully
|
||||
|
||||
@@ -663,12 +663,30 @@ async fn global_proxy(
|
||||
|
||||
let base_url = provider.get_base_url(None, &db).await?;
|
||||
|
||||
let url = format!("{}/{}", base_url, ai_path);
|
||||
let is_anthropic = provider.is_anthropic();
|
||||
let is_anthropic_sdk = headers.get("X-Anthropic-SDK").is_some();
|
||||
|
||||
let url = if is_anthropic_sdk {
|
||||
let truncated_base_url = base_url.trim_end_matches("/v1");
|
||||
format!("{}/{}", truncated_base_url, ai_path)
|
||||
} else {
|
||||
format!("{}/{}", base_url, ai_path)
|
||||
};
|
||||
|
||||
let mut request = HTTP_CLIENT
|
||||
.request(method, url)
|
||||
.header("content-type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key));
|
||||
.header("Authorization", format!("Bearer {}", &api_key));
|
||||
|
||||
if is_anthropic {
|
||||
request = request.header("X-API-Key", &api_key);
|
||||
}
|
||||
|
||||
for (header_name, header_value) in headers.iter() {
|
||||
if header_name.to_string().starts_with("anthropic-") {
|
||||
request = request.header(header_name, header_value);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom headers from AI_HTTP_HEADERS environment variable
|
||||
for (header_name, header_value) in AI_HTTP_HEADERS.iter() {
|
||||
|
||||
@@ -83,6 +83,12 @@ pub struct QueryDynamicEnumJson {
|
||||
pub dynamic_enums_json: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct QueryButtonText {
|
||||
pub resume_button_text: Option<String>,
|
||||
pub cancel_button_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApprovalFormDetails {
|
||||
pub message_str: String,
|
||||
@@ -266,7 +272,12 @@ pub async fn get_approval_form_details(
|
||||
})
|
||||
});
|
||||
|
||||
let args_str = args.map_or("None".to_string(), |a| a.get().to_string());
|
||||
let args_str = args.map_or("None".to_string(), |a| {
|
||||
serde_json::from_str::<serde_json::Value>(a.get())
|
||||
.ok()
|
||||
.and_then(|v| serde_json::to_string_pretty(&v).ok())
|
||||
.unwrap_or_else(|| a.get().to_string())
|
||||
});
|
||||
let parent_job_id_str = parent_job_id.map_or("None".to_string(), |id| id.to_string());
|
||||
let script_path_str = script_path.as_deref().unwrap_or("None");
|
||||
|
||||
@@ -282,7 +293,7 @@ pub async fn get_approval_form_details(
|
||||
{}: {created_by}\n\n\
|
||||
{}: {created_at_formatted}\n\n\
|
||||
{}: {script_path_str}\n\n\
|
||||
{}: {args_str}\n\n\
|
||||
{}:\n```\n{args_str}\n```\n\n\
|
||||
{}: {parent_job_id_str}\n\n",
|
||||
bold_format.replace("{}", "Created by"),
|
||||
bold_format.replace("{}", "Created at"),
|
||||
|
||||
@@ -78,6 +78,9 @@ lazy_static::lazy_static! {
|
||||
(20260207000004, include_str!(
|
||||
"../../migrations/20260207000004_concurrent_indexes_other.up.sql"
|
||||
).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY").replace("DROP INDEX", "DROP INDEX CONCURRENTLY")),
|
||||
(20260225100000, include_str!(
|
||||
"../../migrations/20260225100000_asset_covering_index.up.sql"
|
||||
).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY").replace("DROP INDEX", "DROP INDEX CONCURRENTLY")),
|
||||
].into_iter().collect();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ use crate::jobs::{QueryApprover, ResumeUrls};
|
||||
use crate::{
|
||||
approvals::{
|
||||
extract_w_id_from_resume_url, handle_resume_action, ApprovalFormDetails, FieldType,
|
||||
MessageFormat, QueryDefaultArgsJson, QueryDynamicEnumJson, QueryFlowStepId, QueryMessage,
|
||||
ResumeFormField, ResumeSchema,
|
||||
MessageFormat, QueryButtonText, QueryDefaultArgsJson, QueryDynamicEnumJson,
|
||||
QueryFlowStepId, QueryMessage, ResumeFormField, ResumeSchema,
|
||||
},
|
||||
auth::OptTokened,
|
||||
};
|
||||
@@ -107,6 +107,8 @@ struct ModalActionValue {
|
||||
flow_step_id: Option<String>,
|
||||
default_args_json: Option<String>,
|
||||
dynamic_enums_json: Option<String>,
|
||||
resume_button_text: Option<String>,
|
||||
cancel_button_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -200,6 +202,8 @@ pub async fn slack_app_callback_handler(
|
||||
container,
|
||||
default_args_json.as_ref(),
|
||||
dynamic_enums_json.as_ref(),
|
||||
parsed_value.resume_button_text.as_deref(),
|
||||
parsed_value.cancel_button_text.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||
@@ -229,6 +233,7 @@ pub async fn request_slack_approval(
|
||||
Query(flow_step_id): Query<QueryFlowStepId>,
|
||||
Query(default_args_json): Query<QueryDefaultArgsJson>,
|
||||
Query(dynamic_enums_json): Query<QueryDynamicEnumJson>,
|
||||
Query(button_text): Query<QueryButtonText>,
|
||||
) -> Result<StatusCode, Error> {
|
||||
let slack_resource_path = slack_resource_path.slack_resource_path;
|
||||
let channel_id = channel_id.channel_id;
|
||||
@@ -255,6 +260,8 @@ pub async fn request_slack_approval(
|
||||
flow_step_id.as_str(),
|
||||
default_args_json.default_args_json.as_ref(),
|
||||
dynamic_enums_json.dynamic_enums_json.as_ref(),
|
||||
button_text.resume_button_text.as_deref(),
|
||||
button_text.cancel_button_text.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||
@@ -752,6 +759,8 @@ async fn send_slack_message(
|
||||
flow_step_id: &str,
|
||||
default_args_json: Option<&serde_json::Value>,
|
||||
dynamic_enums_json: Option<&serde_json::Value>,
|
||||
resume_button_text: Option<&str>,
|
||||
cancel_button_text: Option<&str>,
|
||||
) -> Result<StatusCode, Box<dyn std::error::Error>> {
|
||||
let url = "https://slack.com/api/chat.postMessage";
|
||||
|
||||
@@ -779,6 +788,14 @@ async fn send_slack_message(
|
||||
value["dynamic_enums_json"] = dynamic_enums_json.clone();
|
||||
}
|
||||
|
||||
if let Some(resume_button_text) = resume_button_text {
|
||||
value["resume_button_text"] = serde_json::json!(resume_button_text);
|
||||
}
|
||||
|
||||
if let Some(cancel_button_text) = cancel_button_text {
|
||||
value["cancel_button_text"] = serde_json::json!(cancel_button_text);
|
||||
}
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"channel": channel_id,
|
||||
"text": "A flow has been suspended. Please approve or reject the flow.",
|
||||
@@ -842,6 +859,8 @@ async fn get_modal_blocks(
|
||||
container: Container,
|
||||
default_args_json: Option<&serde_json::Value>,
|
||||
dynamic_enums_json: Option<&serde_json::Value>,
|
||||
resume_button_text: Option<&str>,
|
||||
cancel_button_text: Option<&str>,
|
||||
) -> Result<axum::Json<serde_json::Value>, Error> {
|
||||
let approval_details = crate::approvals::get_approval_form_details(
|
||||
db,
|
||||
@@ -895,6 +914,8 @@ async fn get_modal_blocks(
|
||||
&urls.resume,
|
||||
resource_path,
|
||||
container,
|
||||
resume_button_text,
|
||||
cancel_button_text,
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -905,6 +926,8 @@ fn construct_payload(
|
||||
resume_url: &str,
|
||||
resource_path: &str,
|
||||
container: Container,
|
||||
resume_button_text: Option<&str>,
|
||||
cancel_button_text: Option<&str>,
|
||||
) -> serde_json::Value {
|
||||
let mut view = serde_json::json!({
|
||||
"type": "modal",
|
||||
@@ -912,12 +935,12 @@ fn construct_payload(
|
||||
"notify_on_close": true,
|
||||
"title": {
|
||||
"type": "plain_text",
|
||||
"text": "Worfklow Suspended"
|
||||
"text": "Workflow Suspended"
|
||||
},
|
||||
"blocks": blocks,
|
||||
"submit": {
|
||||
"type": "plain_text",
|
||||
"text": "Resume Workflow"
|
||||
"text": resume_button_text.unwrap_or("Resume Workflow")
|
||||
},
|
||||
"private_metadata": serde_json::json!({ "resume_url": resume_url, "resource_path": resource_path, "container": container, "hide_cancel": hide_cancel }).to_string(),
|
||||
});
|
||||
@@ -925,7 +948,7 @@ fn construct_payload(
|
||||
if !hide_cancel {
|
||||
view["close"] = serde_json::json!({
|
||||
"type": "plain_text",
|
||||
"text": "Cancel Workflow"
|
||||
"text": cancel_button_text.unwrap_or("Cancel Workflow")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -949,6 +972,8 @@ async fn open_modal_with_blocks(
|
||||
container: Container,
|
||||
default_args_json: Option<&serde_json::Value>,
|
||||
dynamic_enums_json: Option<&serde_json::Value>,
|
||||
resume_button_text: Option<&str>,
|
||||
cancel_button_text: Option<&str>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let resume_id = rand::random::<u32>();
|
||||
let blocks_json = match get_modal_blocks(
|
||||
@@ -964,6 +989,8 @@ async fn open_modal_with_blocks(
|
||||
container,
|
||||
default_args_json,
|
||||
dynamic_enums_json,
|
||||
resume_button_text,
|
||||
cancel_button_text,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
+1257
-234
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.41"
|
||||
duckdb = { rev = "fe0702529de6ec5a568337726bba9355503157d2", git = "https://github.com/windmill-labs/duckdb-rs.git", features = ["bundled"] }
|
||||
duckdb = { version = "1.4.4", features = ["bundled"] }
|
||||
rust_decimal = "1.37.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{
|
||||
ptr::null_mut,
|
||||
};
|
||||
|
||||
use duckdb::{Row, params_from_iter, types::TimeUnit};
|
||||
use duckdb::{Row, core::LogicalTypeId, params_from_iter, types::TimeUnit};
|
||||
use rust_decimal::{Decimal, prelude::FromPrimitive};
|
||||
use serde::Deserialize;
|
||||
use serde_json::value::RawValue;
|
||||
@@ -267,7 +267,10 @@ fn do_duckdb_inner(
|
||||
(0..stmt.column_count())
|
||||
.map(|i| {
|
||||
let logical_type = stmt.column_logical_type(i);
|
||||
if logical_type.is_invalid() {
|
||||
let logical_type_id = logical_type.id();
|
||||
let invalid = logical_type_id == LogicalTypeId::Invalid
|
||||
|| logical_type_id == LogicalTypeId::Unsupported;
|
||||
if invalid {
|
||||
None
|
||||
} else {
|
||||
logical_type.get_alias()
|
||||
|
||||
@@ -274,16 +274,19 @@ pub async fn eval_timeout_quickjs(
|
||||
|
||||
let expr_clone = expr.clone();
|
||||
|
||||
// Run the QuickJS evaluation with a timeout
|
||||
// Run the QuickJS evaluation with a timeout.
|
||||
// Use the current runtime handle rather than creating an independent
|
||||
// current-thread runtime so that HTTP connections opened by the global
|
||||
// reqwest client (HTTP_CLIENT) are managed by the main runtime.
|
||||
// Creating a child runtime caused connection-pool dispatch tasks to be
|
||||
// dropped when the child runtime exited, poisoning pooled connections
|
||||
// and producing spurious "DispatchGone" / "runtime dropped the dispatch
|
||||
// task" errors on subsequent requests from the main runtime.
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_millis(EVAL_TIMEOUT_MS),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
// Create a new tokio runtime for async operations within the blocking context
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
rt.block_on(async move {
|
||||
handle.block_on(async move {
|
||||
eval_quickjs_inner(
|
||||
&expr_clone,
|
||||
filtered_context,
|
||||
@@ -300,7 +303,9 @@ pub async fn eval_timeout_quickjs(
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!("The expression evaluation `{expr}` took too long to execute (>{EVAL_TIMEOUT_MS}ms)")
|
||||
anyhow::anyhow!(
|
||||
"The expression evaluation `{expr}` took too long to execute (>{EVAL_TIMEOUT_MS}ms)"
|
||||
)
|
||||
})??
|
||||
}
|
||||
|
||||
@@ -787,13 +792,11 @@ pub async fn eval_simple_js(
|
||||
expr: String,
|
||||
globals: HashMap<String, serde_json::Value>,
|
||||
) -> anyhow::Result<Box<RawValue>> {
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_millis(EVAL_TIMEOUT_MS),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
rt.block_on(async move {
|
||||
handle.block_on(async move {
|
||||
let runtime = AsyncRuntime::new()?;
|
||||
runtime.set_memory_limit(QUICKJS_MEMORY_LIMIT).await;
|
||||
let context = AsyncContext::full(&runtime).await?;
|
||||
|
||||
@@ -361,19 +361,13 @@ mod tests {
|
||||
fn test_timestamp() {
|
||||
let result =
|
||||
Converter::try_from_str(Some(Type::TIMESTAMP), "2024-01-15 14:30:00.0").unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Value::String("2024-01-15 14:30:00".to_string())
|
||||
);
|
||||
assert_eq!(result, Value::String("2024-01-15 14:30:00".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamptz() {
|
||||
let result = Converter::try_from_str(
|
||||
Some(Type::TIMESTAMPTZ),
|
||||
"2024-01-15 14:30:00.0+00",
|
||||
)
|
||||
.unwrap();
|
||||
let result =
|
||||
Converter::try_from_str(Some(Type::TIMESTAMPTZ), "2024-01-15 14:30:00.0+00").unwrap();
|
||||
assert!(result.as_str().unwrap().contains("2024-01-15"));
|
||||
}
|
||||
|
||||
@@ -484,8 +478,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_array_with_escaped_backslash() {
|
||||
let result =
|
||||
Converter::try_from_str(Some(Type::TEXT_ARRAY), r#"{"a\\b","c"}"#).unwrap();
|
||||
let result = Converter::try_from_str(Some(Type::TEXT_ARRAY), r#"{"a\\b","c"}"#).unwrap();
|
||||
assert_eq!(result, json!(["a\\b", "c"]));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use windmill_api_auth::ApiAuthed;
|
||||
use windmill_store::resources::try_get_resource_from_db_as;
|
||||
use windmill_trigger::trigger_helpers::TriggerJobArgs;
|
||||
use chrono::Utc;
|
||||
use itertools::Itertools;
|
||||
use native_tls::{Certificate, TlsConnector};
|
||||
@@ -13,6 +10,8 @@ use rust_postgres_native_tls::MakeTlsConnector;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::FromRow;
|
||||
use windmill_api_auth::ApiAuthed;
|
||||
use windmill_common::workspaces::get_datatable_resource_from_db_unchecked;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{to_anyhow, Error, Result},
|
||||
@@ -20,6 +19,8 @@ use windmill_common::{
|
||||
utils::empty_as_none,
|
||||
DB,
|
||||
};
|
||||
use windmill_store::resources::try_get_resource_from_db_as;
|
||||
use windmill_trigger::trigger_helpers::TriggerJobArgs;
|
||||
|
||||
mod bool;
|
||||
mod converter;
|
||||
@@ -373,6 +374,26 @@ pub async fn get_raw_postgres_connection(
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn resolve_postgres_resource(
|
||||
authed: &ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
db: &DB,
|
||||
postgres_resource_path: &str,
|
||||
w_id: &str,
|
||||
) -> Result<Postgres> {
|
||||
if let Some(datatable_name) = postgres_resource_path.strip_prefix("datatable://") {
|
||||
let resource_value =
|
||||
get_datatable_resource_from_db_unchecked(db, w_id, datatable_name).await?;
|
||||
serde_json::from_value::<Postgres>(resource_value).map_err(|e| Error::SerdeJson {
|
||||
error: e,
|
||||
location: "resolve_postgres_resource".to_string(),
|
||||
})
|
||||
} else {
|
||||
try_get_resource_from_db_as::<Postgres>(authed, user_db, db, postgres_resource_path, w_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_pg_connection(
|
||||
authed: ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
@@ -382,8 +403,7 @@ pub async fn get_pg_connection(
|
||||
logical_mode: bool,
|
||||
) -> Result<Client> {
|
||||
let database =
|
||||
try_get_resource_from_db_as::<Postgres>(&authed, user_db, db, postgres_resource_path, w_id)
|
||||
.await?;
|
||||
resolve_postgres_resource(&authed, user_db, db, postgres_resource_path, w_id).await?;
|
||||
|
||||
Ok(get_raw_postgres_connection(&database, logical_mode).await?)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use windmill_common::{
|
||||
DB,
|
||||
};
|
||||
|
||||
use windmill_store::resources::try_get_resource_from_db_as;
|
||||
use windmill_trigger::{listener::ListeningTrigger, trigger_helpers::TriggerJobArgs, Listener};
|
||||
|
||||
use super::{
|
||||
@@ -26,7 +25,8 @@ use super::{
|
||||
LogicalReplicationMessage::{Begin, Commit, Delete, Insert, Relation, Type, Update},
|
||||
PrimaryKeepAliveBody, ReplicationMessage,
|
||||
},
|
||||
Postgres, PostgresConfig, PostgresTrigger, ERROR_PUBLICATION_NAME_NOT_EXISTS,
|
||||
resolve_postgres_resource, Postgres, PostgresConfig, PostgresTrigger,
|
||||
ERROR_PUBLICATION_NAME_NOT_EXISTS,
|
||||
};
|
||||
|
||||
const ERROR_REPLICATION_SLOT_NOT_EXISTS: &str = r#"The replication slot associated with this trigger no longer exists. Recreate a new replication slot or select an existing one in the advanced tab, or delete and recreate a new trigger"#;
|
||||
@@ -147,7 +147,7 @@ impl Listener for PostgresTrigger {
|
||||
.authed(db, &Self::TRIGGER_KIND.to_string())
|
||||
.await?;
|
||||
|
||||
let database = try_get_resource_from_db_as::<Postgres>(
|
||||
let database = resolve_postgres_resource(
|
||||
&authed,
|
||||
Some(UserDB::new(db.clone())),
|
||||
&db,
|
||||
|
||||
@@ -180,7 +180,10 @@ mod tests {
|
||||
fn test_postgres_to_typescript_date_types() {
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::DATE)), "string");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::TIMESTAMP)), "string");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::TIMESTAMPTZ)), "string");
|
||||
assert_eq!(
|
||||
postgres_to_typescript_type(Some(Type::TIMESTAMPTZ)),
|
||||
"string"
|
||||
);
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::UUID)), "string");
|
||||
}
|
||||
|
||||
|
||||
@@ -80,10 +80,17 @@ mod tests {
|
||||
use rust_postgres::types::Type;
|
||||
use serde_json::json;
|
||||
|
||||
use super::super::replication_message::{Column, ReplicaIdentity, RelationBody};
|
||||
use super::super::replication_message::{Column, RelationBody, ReplicaIdentity};
|
||||
|
||||
fn make_relation(o_id: Oid, columns: Vec<Column>) -> RelationBody {
|
||||
RelationBody::new(None, o_id, "public".to_string(), "test".to_string(), ReplicaIdentity::Default, columns)
|
||||
RelationBody::new(
|
||||
None,
|
||||
o_id,
|
||||
"public".to_string(),
|
||||
"test".to_string(),
|
||||
ReplicaIdentity::Default,
|
||||
columns,
|
||||
)
|
||||
}
|
||||
|
||||
fn text_col(name: &str, typ: Option<Type>) -> Column {
|
||||
@@ -93,10 +100,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_row_to_json_text_columns() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("id", Some(Type::INT4)),
|
||||
text_col("name", Some(Type::TEXT)),
|
||||
]));
|
||||
converter.add_relation(make_relation(
|
||||
1,
|
||||
vec![
|
||||
text_col("id", Some(Type::INT4)),
|
||||
text_col("name", Some(Type::TEXT)),
|
||||
],
|
||||
));
|
||||
|
||||
let tuple = vec![
|
||||
TupleData::Text(Bytes::from("42")),
|
||||
@@ -111,15 +121,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_row_to_json_with_null() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("id", Some(Type::INT4)),
|
||||
text_col("email", Some(Type::TEXT)),
|
||||
]));
|
||||
converter.add_relation(make_relation(
|
||||
1,
|
||||
vec![
|
||||
text_col("id", Some(Type::INT4)),
|
||||
text_col("email", Some(Type::TEXT)),
|
||||
],
|
||||
));
|
||||
|
||||
let tuple = vec![
|
||||
TupleData::Text(Bytes::from("1")),
|
||||
TupleData::Null,
|
||||
];
|
||||
let tuple = vec![TupleData::Text(Bytes::from("1")), TupleData::Null];
|
||||
|
||||
let result = converter.row_to_json((1, tuple)).unwrap();
|
||||
assert_eq!(result["id"], json!(1));
|
||||
@@ -129,9 +139,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_row_to_json_with_unchanged_toast() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("data", Some(Type::TEXT)),
|
||||
]));
|
||||
converter.add_relation(make_relation(1, vec![text_col("data", Some(Type::TEXT))]));
|
||||
|
||||
let tuple = vec![TupleData::UnchangedToast];
|
||||
let result = converter.row_to_json((1, tuple)).unwrap();
|
||||
@@ -141,9 +149,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_row_to_json_binary_rejected() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("data", Some(Type::BYTEA)),
|
||||
]));
|
||||
converter.add_relation(make_relation(1, vec![text_col("data", Some(Type::BYTEA))]));
|
||||
|
||||
let tuple = vec![TupleData::Binary(Bytes::from("data"))];
|
||||
assert!(matches!(
|
||||
@@ -168,8 +174,12 @@ mod tests {
|
||||
converter.add_relation(make_relation(1, vec![text_col("a", Some(Type::TEXT))]));
|
||||
converter.add_relation(make_relation(2, vec![text_col("b", Some(Type::INT4))]));
|
||||
|
||||
let result1 = converter.row_to_json((1, vec![TupleData::Text(Bytes::from("hello"))])).unwrap();
|
||||
let result2 = converter.row_to_json((2, vec![TupleData::Text(Bytes::from("42"))])).unwrap();
|
||||
let result1 = converter
|
||||
.row_to_json((1, vec![TupleData::Text(Bytes::from("hello"))]))
|
||||
.unwrap();
|
||||
let result2 = converter
|
||||
.row_to_json((2, vec![TupleData::Text(Bytes::from("42"))]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result1["a"], json!("hello"));
|
||||
assert_eq!(result2["b"], json!(42));
|
||||
@@ -178,11 +188,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_row_to_json_bool_column() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("active", Some(Type::BOOL)),
|
||||
]));
|
||||
converter.add_relation(make_relation(1, vec![text_col("active", Some(Type::BOOL))]));
|
||||
|
||||
let result = converter.row_to_json((1, vec![TupleData::Text(Bytes::from("t"))])).unwrap();
|
||||
let result = converter
|
||||
.row_to_json((1, vec![TupleData::Text(Bytes::from("t"))]))
|
||||
.unwrap();
|
||||
assert_eq!(result["active"], json!(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,10 +682,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_insert_with_null_tuple() {
|
||||
let data = Bytes::from(build_insert_message(
|
||||
10,
|
||||
&[(TUPLE_DATA_NULL_BYTE, &[])],
|
||||
));
|
||||
let data = Bytes::from(build_insert_message(10, &[(TUPLE_DATA_NULL_BYTE, &[])]));
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Insert(insert) => {
|
||||
@@ -698,10 +695,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_insert_with_toast_tuple() {
|
||||
let data = Bytes::from(build_insert_message(
|
||||
10,
|
||||
&[(TUPLE_DATA_TOAST_BYTE, &[])],
|
||||
));
|
||||
let data = Bytes::from(build_insert_message(10, &[(TUPLE_DATA_TOAST_BYTE, &[])]));
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Insert(insert) => {
|
||||
@@ -759,7 +753,7 @@ mod tests {
|
||||
buf.extend_from_slice(b"users\0"); // name
|
||||
buf.push(REPLICA_IDENTITY_DEFAULT_BYTE as u8); // replica identity
|
||||
buf.extend_from_slice(&1i16.to_be_bytes()); // num columns
|
||||
// column: flags=0, name="id", type_oid=23 (INT4), type_modifier=-1
|
||||
// column: flags=0, name="id", type_oid=23 (INT4), type_modifier=-1
|
||||
buf.push(0); // flags
|
||||
buf.extend_from_slice(b"id\0"); // name
|
||||
buf.extend_from_slice(&23u32.to_be_bytes()); // type_oid (INT4)
|
||||
|
||||
@@ -1099,21 +1099,22 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
|| (flow_jobs.is_some() && (skip_loop_failures || skip_seq_branch_failure)))
|
||||
&& !(stop_early && stop_early_err_msg.is_some() && !skip_if_stop_early)
|
||||
{
|
||||
let is_skipped = if current_module.as_ref().is_some_and(|m| m.skip_if.is_some())
|
||||
{
|
||||
sqlx::query_scalar!(
|
||||
"SELECT kind = 'identity' FROM v2_job WHERE id = $1",
|
||||
job_id_for_status
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal_err(format!("error during skip check: {e:#}"))
|
||||
})?
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
stop_early && skip_if_stop_early // Mark as skipped when stop_after_if with skip_if_stopped=true
|
||||
};
|
||||
let is_skipped = (stop_early && skip_if_stop_early)
|
||||
|| if current_module.as_ref().is_some_and(|m| m.skip_if.is_some()) {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT kind = 'identity' FROM v2_job WHERE id = $1",
|
||||
job_id_for_status
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal_err(format!("error during skip check: {e:#}"))
|
||||
})?
|
||||
.flatten()
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
success = true;
|
||||
(
|
||||
true,
|
||||
|
||||
Generated
+83
-94
@@ -102,9 +102,9 @@
|
||||
"@melt-ui/svelte": "^0.86.2",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.49.2",
|
||||
"@sveltejs/package": "^2.3.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@sveltejs/kit": "^2.53.2",
|
||||
"@sveltejs/package": "^2.5.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/d3": "^7.4.0",
|
||||
@@ -131,9 +131,9 @@
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"style-to-object": "^0.4.1",
|
||||
"stylelint-config-recommended": "^13.0.0",
|
||||
"svelte": "^5.38.0",
|
||||
"svelte": "^5.53.5",
|
||||
"svelte-awesome-color-picker": "^3.0.4",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-check": "^4.4.3",
|
||||
"svelte-fast-check": "^0.4.5",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"svelte-highlight": "^7.6.0",
|
||||
@@ -835,7 +835,6 @@
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
||||
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -847,7 +846,6 @@
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
||||
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -858,7 +856,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
|
||||
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1348,7 +1345,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz",
|
||||
"integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1503,7 +1499,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1520,7 +1515,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1537,7 +1531,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1554,7 +1547,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1571,7 +1563,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1588,7 +1579,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1605,7 +1595,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1622,7 +1611,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1639,7 +1627,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1656,7 +1643,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1673,7 +1659,6 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1690,7 +1675,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1707,7 +1691,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2102,9 +2085,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.49.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.2.tgz",
|
||||
"integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==",
|
||||
"version": "2.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.2.tgz",
|
||||
"integrity": "sha512-M+MqAvFve12T1HWws/2npP/s3hFtyjw3GB/OXW/8a1jZBk48qnvPJrtgE+VOMc3RnjUMxc4mv/vQ73nvj2uNMg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2113,13 +2096,12 @@
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.3.2",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
"mrmime": "^2.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"set-cookie-parser": "^3.0.0",
|
||||
"sirv": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -2130,24 +2112,28 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/package": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-2.5.4.tgz",
|
||||
"integrity": "sha512-8+1hccAt0M3PPkHVPKH54Wc+cc1PNxRqCrICZiv/hEEto8KwbQVRghxNgTB4htIPyle+4CIB8RayTQH5zRQh9A==",
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-2.5.7.tgz",
|
||||
"integrity": "sha512-qqD9xa9H7TDiGFrF6rz7AirOR8k15qDK/9i4MIE8te4vWsv5GEogPks61rrZcLy+yWph+aI6pIj2MdoK3YI8AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"chokidar": "^5.0.0",
|
||||
"kleur": "^4.1.5",
|
||||
"sade": "^1.8.1",
|
||||
"semver": "^7.5.4",
|
||||
@@ -2163,17 +2149,47 @@
|
||||
"svelte": "^3.44.0 || ^4.0.0 || ^5.0.0-next.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/package/node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/package/node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz",
|
||||
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
||||
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
"debug": "^4.4.1",
|
||||
"deepmerge": "^4.3.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.0",
|
||||
"vitefu": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2313,7 +2329,6 @@
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2723,8 +2738,7 @@
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
@@ -3442,9 +3456,9 @@
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4971,10 +4985,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz",
|
||||
"integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==",
|
||||
"devOptional": true,
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
@@ -5705,9 +5718,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
|
||||
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
@@ -7193,7 +7206,7 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -7692,7 +7705,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7713,7 +7725,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7734,7 +7745,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7755,7 +7765,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7776,7 +7785,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7797,7 +7805,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7818,7 +7825,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7839,7 +7845,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7860,7 +7865,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7881,7 +7885,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7902,7 +7905,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -9234,7 +9236,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -9541,7 +9543,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
@@ -11566,7 +11568,7 @@
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mri": "^1.1.0"
|
||||
@@ -11615,9 +11617,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
|
||||
"integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -12430,21 +12432,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.39.12",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.12.tgz",
|
||||
"integrity": "sha512-CEzwxFuEycokU8K8CE/OuwVbmei+ivu2HvBGYIdASfMa1hCRSNr4RRkzNSvbAvu6h+BOig2CsZTAEY+WKvwZpA==",
|
||||
"version": "5.53.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz",
|
||||
"integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"acorn": "^8.12.1",
|
||||
"aria-query": "^5.3.1",
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.1.0",
|
||||
"esrap": "^2.2.2",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
@@ -12488,9 +12492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz",
|
||||
"integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.3.tgz",
|
||||
"integrity": "sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -12529,21 +12533,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||
@@ -13274,7 +13263,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
"@melt-ui/svelte": "^0.86.2",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.49.2",
|
||||
"@sveltejs/package": "^2.3.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@sveltejs/kit": "^2.53.2",
|
||||
"@sveltejs/package": "^2.5.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/d3": "^7.4.0",
|
||||
@@ -56,9 +56,9 @@
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"style-to-object": "^0.4.1",
|
||||
"stylelint-config-recommended": "^13.0.0",
|
||||
"svelte": "^5.38.0",
|
||||
"svelte": "^5.53.5",
|
||||
"svelte-awesome-color-picker": "^3.0.4",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-check": "^4.4.3",
|
||||
"svelte-fast-check": "^0.4.5",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"svelte-highlight": "^7.6.0",
|
||||
|
||||
+88
-85
@@ -9,113 +9,116 @@ const USER_CUSTOM_PROMPTS_KEY = 'userCustomAIPrompts'
|
||||
const sessionModel = getLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME)
|
||||
const sessionProvider = getLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME)
|
||||
export const copilotSessionModel = writable<AIProviderModel | undefined>(
|
||||
sessionModel && sessionProvider
|
||||
? {
|
||||
model: sessionModel,
|
||||
provider: sessionProvider as AIProvider
|
||||
}
|
||||
: undefined
|
||||
sessionModel && sessionProvider
|
||||
? {
|
||||
model: sessionModel,
|
||||
provider: sessionProvider as AIProvider
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
|
||||
|
||||
export const copilotInfo = writable<{
|
||||
enabled: boolean
|
||||
codeCompletionModel?: AIProviderModel
|
||||
defaultModel?: AIProviderModel
|
||||
aiModels: AIProviderModel[]
|
||||
customPrompts?: Record<string, string>
|
||||
maxTokensPerModel?: Record<string, number>
|
||||
enabled: boolean
|
||||
codeCompletionModel?: AIProviderModel
|
||||
defaultModel?: AIProviderModel
|
||||
aiModels: AIProviderModel[]
|
||||
customPrompts?: Record<string, string>
|
||||
maxTokensPerModel?: Record<string, number>
|
||||
}>({
|
||||
enabled: false,
|
||||
codeCompletionModel: undefined,
|
||||
defaultModel: undefined,
|
||||
aiModels: [],
|
||||
customPrompts: {},
|
||||
maxTokensPerModel: {}
|
||||
enabled: false,
|
||||
codeCompletionModel: undefined,
|
||||
defaultModel: undefined,
|
||||
aiModels: [],
|
||||
customPrompts: {},
|
||||
maxTokensPerModel: {}
|
||||
})
|
||||
|
||||
export async function loadCopilot(workspace: string) {
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
setCopilotInfo(info)
|
||||
} catch (err) {
|
||||
setCopilotInfo({})
|
||||
console.error('Could not get copilot info', err)
|
||||
}
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
setCopilotInfo(info)
|
||||
} catch (err) {
|
||||
setCopilotInfo({})
|
||||
console.error('Could not get copilot info', err)
|
||||
}
|
||||
}
|
||||
|
||||
export function setCopilotInfo(aiConfig: AIConfig) {
|
||||
if (Object.keys(aiConfig.providers ?? {}).length > 0) {
|
||||
const aiModels = Object.entries(aiConfig.providers ?? {}).flatMap(
|
||||
([provider, providerConfig]) =>
|
||||
providerConfig.models.map((m) => ({ model: m, provider: provider as AIProvider }))
|
||||
)
|
||||
if (Object.keys(aiConfig.providers ?? {}).length > 0) {
|
||||
const aiModels = Object.entries(aiConfig.providers ?? {}).flatMap(
|
||||
([provider, providerConfig]) =>
|
||||
providerConfig.models.map((m) => ({ model: m, provider: provider as AIProvider }))
|
||||
)
|
||||
|
||||
copilotSessionModel.update((model) => {
|
||||
if (
|
||||
model &&
|
||||
!aiModels.some((m) => m.model === model.model && m.provider === model.provider)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
return model
|
||||
})
|
||||
copilotSessionModel.update((model) => {
|
||||
if (
|
||||
model &&
|
||||
!aiModels.some((m) => m.model === model.model && m.provider === model.provider)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
return model
|
||||
})
|
||||
|
||||
copilotInfo.set({
|
||||
enabled: true,
|
||||
codeCompletionModel: aiConfig.code_completion_model,
|
||||
defaultModel: aiConfig.default_model,
|
||||
aiModels: aiModels,
|
||||
customPrompts: aiConfig.custom_prompts ?? {},
|
||||
maxTokensPerModel: aiConfig.max_tokens_per_model ?? {}
|
||||
})
|
||||
} else {
|
||||
copilotSessionModel.set(undefined)
|
||||
copilotInfo.set({
|
||||
enabled: true,
|
||||
codeCompletionModel: aiConfig.code_completion_model,
|
||||
defaultModel: aiConfig.default_model,
|
||||
aiModels: aiModels,
|
||||
customPrompts: aiConfig.custom_prompts ?? {},
|
||||
maxTokensPerModel: aiConfig.max_tokens_per_model ?? {}
|
||||
})
|
||||
} else {
|
||||
copilotSessionModel.set(undefined)
|
||||
|
||||
copilotInfo.set({
|
||||
enabled: false,
|
||||
codeCompletionModel: undefined,
|
||||
defaultModel: undefined,
|
||||
aiModels: [],
|
||||
customPrompts: {},
|
||||
maxTokensPerModel: {}
|
||||
})
|
||||
}
|
||||
copilotInfo.set({
|
||||
enabled: false,
|
||||
codeCompletionModel: undefined,
|
||||
defaultModel: undefined,
|
||||
aiModels: [],
|
||||
customPrompts: {},
|
||||
maxTokensPerModel: {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentModel() {
|
||||
const model =
|
||||
get(copilotSessionModel) ?? get(copilotInfo).defaultModel ?? get(copilotInfo).aiModels[0]
|
||||
if (!model) {
|
||||
throw new Error('No model selected')
|
||||
}
|
||||
return model
|
||||
export function getCurrentModel(): AIProviderModel {
|
||||
const model =
|
||||
get(copilotSessionModel) ?? get(copilotInfo).defaultModel ?? get(copilotInfo).aiModels[0]
|
||||
if (!model) {
|
||||
throw new Error('No model selected')
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
export function tryGetCurrentModel(): AIProviderModel | undefined {
|
||||
return get(copilotSessionModel) ?? get(copilotInfo).defaultModel ?? get(copilotInfo).aiModels[0]
|
||||
}
|
||||
|
||||
export function getUserCustomPrompts(): Record<string, string> {
|
||||
const stored = getLocalSetting(USER_CUSTOM_PROMPTS_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user custom prompts', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
const stored = getLocalSetting(USER_CUSTOM_PROMPTS_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user custom prompts', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function getCombinedCustomPrompt(mode: string): string | undefined {
|
||||
const workspacePrompt = get(copilotInfo).customPrompts?.[mode]
|
||||
const userPrompts = getUserCustomPrompts()
|
||||
const userPrompt = userPrompts[mode]
|
||||
const workspacePrompt = get(copilotInfo).customPrompts?.[mode]
|
||||
const userPrompts = getUserCustomPrompts()
|
||||
const userPrompt = userPrompts[mode]
|
||||
|
||||
const prompts = [workspacePrompt, userPrompt].filter((p) => p?.trim())
|
||||
const prompts = [workspacePrompt, userPrompt].filter((p) => p?.trim())
|
||||
|
||||
if (prompts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
if (prompts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return prompts.join('\n\n')
|
||||
return prompts.join('\n\n')
|
||||
}
|
||||
|
||||
@@ -1057,6 +1057,7 @@
|
||||
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))}
|
||||
<!-- {JSON.stringify(value)} -->
|
||||
<ObjectResourceInput
|
||||
datatableAsPgResource={label === 'database'}
|
||||
{disabled}
|
||||
{defaultValue}
|
||||
selectFirst={!noDefaultOnSelectFirst && required}
|
||||
|
||||
@@ -8,21 +8,37 @@
|
||||
ArrowUpRight,
|
||||
Building,
|
||||
DiffIcon,
|
||||
GitFork
|
||||
GitFork,
|
||||
Loader2,
|
||||
Trash2,
|
||||
Upload
|
||||
} from 'lucide-svelte'
|
||||
import { Alert, Badge } from './common'
|
||||
import {
|
||||
AppService,
|
||||
EmailTriggerService,
|
||||
FlowService,
|
||||
FolderService,
|
||||
GcpTriggerService,
|
||||
HttpTriggerService,
|
||||
KafkaTriggerService,
|
||||
MqttTriggerService,
|
||||
NatsTriggerService,
|
||||
PostgresTriggerService,
|
||||
ScheduleService,
|
||||
ScriptService,
|
||||
SqsTriggerService,
|
||||
UserService,
|
||||
WebsocketTriggerService,
|
||||
WorkspaceService,
|
||||
type WorkspaceComparison,
|
||||
type WorkspaceItemDiff
|
||||
} from '$lib/gen'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
||||
import Row from './common/table/Row.svelte'
|
||||
import DiffDrawer from './DiffDrawer.svelte'
|
||||
import DeployWorkspaceDrawer from './DeployWorkspaceDrawer.svelte'
|
||||
import ParentWorkspaceProtectionAlert from './ParentWorkspaceProtectionAlert.svelte'
|
||||
import { userStore, userWorkspaces, workspaceStore } from '$lib/stores'
|
||||
|
||||
@@ -36,6 +52,10 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import WorkspaceDeployLayout from './WorkspaceDeployLayout.svelte'
|
||||
import type { TriggerKind } from './triggers'
|
||||
import { triggerDisplayNamesMap, triggerKindToTriggerType } from './triggers/utils'
|
||||
import { getEmailAddress, getEmailDomain } from './triggers/email/utils'
|
||||
import { base } from '$lib/base'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
|
||||
@@ -56,9 +76,7 @@
|
||||
let canDeployToParent = $state(true)
|
||||
let canPreserveInParent = $state(false)
|
||||
let canPreserveInCurrent = $state(false)
|
||||
let canPreserveOnBehalfOf = $derived(
|
||||
mergeIntoParent ? canPreserveInParent : canPreserveInCurrent
|
||||
)
|
||||
let canPreserveOnBehalfOf = $derived(mergeIntoParent ? canPreserveInParent : canPreserveInCurrent)
|
||||
|
||||
let selectableDiffs = $derived(
|
||||
comparison?.diffs.filter((diff) => {
|
||||
@@ -438,6 +456,235 @@
|
||||
diff
|
||||
}))
|
||||
)
|
||||
|
||||
// --- Fork Triggers ---
|
||||
|
||||
type ForkTrigger = {
|
||||
path: string
|
||||
triggerKind: TriggerKind
|
||||
scriptPath: string
|
||||
isFlow: boolean
|
||||
enabled?: boolean
|
||||
extraLabel?: string
|
||||
}
|
||||
|
||||
let forkTriggers = $state<ForkTrigger[]>([])
|
||||
let loadingTriggers = $state(true)
|
||||
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state(undefined)
|
||||
let triggerToDelete = $state<ForkTrigger | undefined>(undefined)
|
||||
|
||||
/** Deployable trigger kinds and their list+delete services */
|
||||
const triggerServices = {
|
||||
schedules: {
|
||||
list: (ws: string) => ScheduleService.listSchedules({ workspace: ws }),
|
||||
delete: (ws: string, path: string) => ScheduleService.deleteSchedule({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'schedules',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.enabled,
|
||||
extraLabel: item.schedule
|
||||
})
|
||||
},
|
||||
routes: {
|
||||
list: (ws: string) => HttpTriggerService.listHttpTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
HttpTriggerService.deleteHttpTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'routes',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled',
|
||||
extraLabel: `${(item.http_method ?? 'get').toUpperCase()} ${item.route_path ?? ''}`
|
||||
})
|
||||
},
|
||||
websockets: {
|
||||
list: (ws: string) => WebsocketTriggerService.listWebsocketTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
WebsocketTriggerService.deleteWebsocketTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'websockets',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled',
|
||||
extraLabel: item.url
|
||||
})
|
||||
},
|
||||
kafka: {
|
||||
list: (ws: string) => KafkaTriggerService.listKafkaTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
KafkaTriggerService.deleteKafkaTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'kafka',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled',
|
||||
extraLabel: item.topics?.join(', ')
|
||||
})
|
||||
},
|
||||
postgres: {
|
||||
list: (ws: string) => PostgresTriggerService.listPostgresTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
PostgresTriggerService.deletePostgresTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'postgres',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled'
|
||||
})
|
||||
},
|
||||
nats: {
|
||||
list: (ws: string) => NatsTriggerService.listNatsTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
NatsTriggerService.deleteNatsTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'nats',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled',
|
||||
extraLabel: item.subjects?.join(', ')
|
||||
})
|
||||
},
|
||||
mqtt: {
|
||||
list: (ws: string) => MqttTriggerService.listMqttTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
MqttTriggerService.deleteMqttTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'mqtt',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled'
|
||||
})
|
||||
},
|
||||
sqs: {
|
||||
list: (ws: string) => SqsTriggerService.listSqsTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
SqsTriggerService.deleteSqsTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'sqs',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled',
|
||||
extraLabel: item.queue_url
|
||||
})
|
||||
},
|
||||
gcp: {
|
||||
list: (ws: string) => GcpTriggerService.listGcpTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
GcpTriggerService.deleteGcpTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'gcp',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled',
|
||||
extraLabel: item.topic_id
|
||||
})
|
||||
},
|
||||
emails: {
|
||||
list: (ws: string) => EmailTriggerService.listEmailTriggers({ workspace: ws }),
|
||||
delete: (ws: string, path: string) =>
|
||||
EmailTriggerService.deleteEmailTrigger({ workspace: ws, path }),
|
||||
normalize: (item: any): ForkTrigger => ({
|
||||
path: item.path,
|
||||
triggerKind: 'emails',
|
||||
scriptPath: item.script_path,
|
||||
isFlow: item.is_flow,
|
||||
enabled: item.mode === 'enabled',
|
||||
extraLabel: getEmailAddress(
|
||||
item.local_part,
|
||||
item.workspaced_local_part,
|
||||
currentWorkspaceId,
|
||||
emailDomain ?? ''
|
||||
)
|
||||
})
|
||||
}
|
||||
} as const
|
||||
|
||||
let emailDomain = $state<string | undefined>(undefined)
|
||||
|
||||
async function fetchAllTriggers() {
|
||||
loadingTriggers = true
|
||||
try {
|
||||
emailDomain = await getEmailDomain()
|
||||
const entries = Object.values(triggerServices)
|
||||
const results = await Promise.allSettled(
|
||||
entries.map(async (svc) => {
|
||||
const items = await svc.list(currentWorkspaceId)
|
||||
return items.map(svc.normalize)
|
||||
})
|
||||
)
|
||||
forkTriggers = results.flatMap((r) => (r.status === 'fulfilled' ? r.value : []))
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch fork triggers:', e)
|
||||
forkTriggers = []
|
||||
} finally {
|
||||
loadingTriggers = false
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTrigger(trigger: ForkTrigger) {
|
||||
triggerToDelete = trigger
|
||||
}
|
||||
|
||||
async function confirmDeleteTrigger() {
|
||||
const trigger = triggerToDelete
|
||||
if (!trigger) return
|
||||
triggerToDelete = undefined
|
||||
const triggerType = triggerKindToTriggerType(trigger.triggerKind)
|
||||
const displayName = triggerType ? triggerDisplayNamesMap[triggerType] : trigger.triggerKind
|
||||
try {
|
||||
const svc = triggerServices[trigger.triggerKind as keyof typeof triggerServices]
|
||||
if (!svc) {
|
||||
throw new Error(`No service for trigger kind: ${trigger.triggerKind}`)
|
||||
}
|
||||
await svc.delete(currentWorkspaceId, trigger.path)
|
||||
forkTriggers = forkTriggers.filter(
|
||||
(t) => !(t.path === trigger.path && t.triggerKind === trigger.triggerKind)
|
||||
)
|
||||
sendUserToast(`Deleted ${displayName} trigger '${trigger.path}'`)
|
||||
} catch (e: any) {
|
||||
sendUserToast(`Failed to delete trigger '${trigger.path}': ${e.body || e.message}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
function getTriggerDisplayName(triggerKind: TriggerKind): string {
|
||||
const triggerType = triggerKindToTriggerType(triggerKind)
|
||||
return triggerType ? triggerDisplayNamesMap[triggerType] : triggerKind
|
||||
}
|
||||
|
||||
const triggerKindToPagePath: Record<string, string> = {
|
||||
schedules: '/schedules',
|
||||
routes: '/routes',
|
||||
websockets: '/websocket_triggers',
|
||||
kafka: '/kafka_triggers',
|
||||
postgres: '/postgres_triggers',
|
||||
nats: '/nats_triggers',
|
||||
mqtt: '/mqtt_triggers',
|
||||
sqs: '/sqs_triggers',
|
||||
gcp: '/gcp_triggers',
|
||||
emails: '/email_triggers'
|
||||
}
|
||||
|
||||
function getTriggerHref(triggerKind: TriggerKind): string | undefined {
|
||||
const pagePath = triggerKindToPagePath[triggerKind]
|
||||
return pagePath ? `${base}${pagePath}` : undefined
|
||||
}
|
||||
|
||||
// Fetch triggers when workspace is available
|
||||
$effect(() => {
|
||||
if (currentWorkspaceId) {
|
||||
fetchAllTriggers()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $workspaceStore != currentWorkspaceId}
|
||||
@@ -725,9 +972,8 @@
|
||||
<span class="text-xs text-yellow-600">
|
||||
You must set the "on behalf of" user for all items before deploying
|
||||
<Tooltip class="text-yellow-600">
|
||||
The "run on behalf of" field defines which user's permissions will be
|
||||
applied during execution. Make sure this is set to an appropriate
|
||||
user before deploying.
|
||||
The "run on behalf of" field defines which user's permissions will be applied
|
||||
during execution. Make sure this is set to an appropriate user before deploying.
|
||||
</Tooltip>
|
||||
</span>
|
||||
{/if}
|
||||
@@ -749,7 +995,99 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</WorkspaceDeployLayout>
|
||||
|
||||
<!-- Fork Triggers Section -->
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="text-sm font-semibold">Triggers created in this fork</h3>
|
||||
{#if !loadingTriggers}
|
||||
<Badge color="indigo" size="xs"
|
||||
>{forkTriggers.length} trigger{forkTriggers.length !== 1 ? 's' : ''}</Badge
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Alert title="Deploy and/or delete these triggers" type="info" class="mb-2">
|
||||
When forking a workspace, triggers are not forked to avoid unnecessary executions or
|
||||
collisions. If you created this triggers with the intention of deploying them to the parent
|
||||
workspace, you can do so here. Otherwise it is recommended to delete them or disable them.
|
||||
</Alert>
|
||||
|
||||
{#if loadingTriggers}
|
||||
<div class="flex items-center gap-2 text-secondary text-sm p-4">
|
||||
<Loader2 class="animate-spin w-4 h-4" />
|
||||
Loading triggers...
|
||||
</div>
|
||||
{:else if forkTriggers.length === 0}
|
||||
<div class="text-secondary text-sm p-4 border rounded-md bg-surface-tertiary">
|
||||
No triggers in this fork workspace.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border rounded-md bg-surface-tertiary">
|
||||
{#each forkTriggers as trigger (trigger.triggerKind + ':' + trigger.path)}
|
||||
<Row
|
||||
kind="trigger"
|
||||
triggerKind={trigger.triggerKind}
|
||||
path={trigger.path}
|
||||
href={getTriggerHref(trigger.triggerKind)}
|
||||
marked={undefined}
|
||||
isSelectable={false}
|
||||
canFavorite={false}
|
||||
workspaceId={currentWorkspaceId}
|
||||
>
|
||||
{#snippet customSummary()}
|
||||
<span>{getTriggerDisplayName(trigger.triggerKind)}</span>
|
||||
<span class="text-secondary mx-1">→</span>
|
||||
<span class="text-secondary">{trigger.scriptPath}</span>
|
||||
{#if trigger.isFlow}
|
||||
<Badge color="blue" size="xs">flow</Badge>
|
||||
{/if}
|
||||
{#if trigger.extraLabel}
|
||||
<span class="text-tertiary text-xs">({trigger.extraLabel})</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
{#if trigger.enabled != null}
|
||||
<Badge color={trigger.enabled ? 'green' : 'gray'} size="xs">
|
||||
{trigger.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
{/if}
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
onclick={() => {
|
||||
deploymentDrawer?.openDrawer(trigger.path, 'trigger', {
|
||||
triggers: { kind: trigger.triggerKind }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Upload size={12} />
|
||||
Deploy
|
||||
</Button>
|
||||
<Button size="xs" variant="subtle" color="red" onclick={() => deleteTrigger(trigger)}>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Row>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
|
||||
<DiffDrawer bind:this={diffDrawer} {isFlow} />
|
||||
<ConfirmationModal
|
||||
title="Delete trigger"
|
||||
confirmationText="Delete"
|
||||
open={!!triggerToDelete}
|
||||
onConfirmed={confirmDeleteTrigger}
|
||||
onCanceled={() => (triggerToDelete = undefined)}
|
||||
>
|
||||
{#if triggerToDelete}
|
||||
Are you sure you want to delete the {getTriggerDisplayName(triggerToDelete.triggerKind)} trigger
|
||||
'{triggerToDelete.path}'?
|
||||
{/if}
|
||||
</ConfirmationModal>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="text-gray-500">No comparison data available</div>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
editor?: SimpleEditor | undefined
|
||||
path?: string
|
||||
disabled?: boolean
|
||||
datatableAsPgResource?: boolean
|
||||
onClear?: () => void
|
||||
}
|
||||
|
||||
@@ -30,16 +31,23 @@
|
||||
defaultValue,
|
||||
editor = $bindable(undefined),
|
||||
disabled = false,
|
||||
datatableAsPgResource = false,
|
||||
onClear = undefined
|
||||
}: Props = $props()
|
||||
|
||||
function isResource() {
|
||||
return isString(value) && value.length >= '$res:'.length
|
||||
return isString(value) && value.startsWith('$res:')
|
||||
}
|
||||
|
||||
function isDatatable(val: any): boolean {
|
||||
return (isString(val) && val.startsWith('datatable://')) || val === 'datatable'
|
||||
}
|
||||
|
||||
function valueToPath() {
|
||||
if (isResource()) {
|
||||
return value.substr('$res:'.length)
|
||||
} else if (isDatatable(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -50,6 +58,7 @@
|
||||
<S3ObjectPicker bind:value />
|
||||
{:else if value == undefined || typeof value === 'string'}
|
||||
<ResourcePicker
|
||||
{datatableAsPgResource}
|
||||
{disabled}
|
||||
{selectFirst}
|
||||
{disablePortal}
|
||||
@@ -60,7 +69,7 @@
|
||||
if (v == undefined) {
|
||||
value = undefined
|
||||
} else {
|
||||
value = `$res:${v}`
|
||||
value = isDatatable(v) ? v : `$res:${v}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { ResourceService, WorkspaceService } from '$lib/gen'
|
||||
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
|
||||
import { onMount, untrack } from 'svelte'
|
||||
import AppConnect from './AppConnectDrawer.svelte'
|
||||
@@ -28,6 +28,7 @@
|
||||
class?: string
|
||||
onClear?: () => void
|
||||
excludedValues?: string[]
|
||||
datatableAsPgResource?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -45,7 +46,8 @@
|
||||
selectInputClass = '',
|
||||
class: className = '',
|
||||
onClear = undefined,
|
||||
excludedValues = undefined
|
||||
excludedValues = undefined,
|
||||
datatableAsPgResource = false
|
||||
}: Props = $props()
|
||||
|
||||
if (initialValue && value == undefined) {
|
||||
@@ -117,6 +119,23 @@
|
||||
type: x.resource_type
|
||||
}))
|
||||
|
||||
if (datatableAsPgResource && resourceType === 'postgresql') {
|
||||
try {
|
||||
const datatables = await WorkspaceService.listDataTables({
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
for (const dt of datatables) {
|
||||
nc.push({
|
||||
value: `datatable://${dt}`,
|
||||
label: `datatable://${dt}`,
|
||||
type: 'postgresql'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load data tables', e)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check if this is needed
|
||||
if (!nc.find((x) => x.value == value) && (initialValue || value)) {
|
||||
nc.push({ value: value ?? initialValue!, label: value ?? initialValue!, type: '' })
|
||||
@@ -157,6 +176,7 @@
|
||||
let resourceEditor: ResourceEditorDrawer | undefined = $state()
|
||||
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
|
||||
let hovering = $state(false)
|
||||
let isDatatableSelected = $derived(value?.startsWith('datatable://') ?? false)
|
||||
</script>
|
||||
|
||||
<AppConnect
|
||||
@@ -212,16 +232,18 @@
|
||||
id="resource-picker-select"
|
||||
>
|
||||
{#snippet endSnippet({ item, close })}
|
||||
<Button
|
||||
{disabled}
|
||||
variant="subtle"
|
||||
size="xs2"
|
||||
wrapperClasses="-mr-2 pl-1 -my-2"
|
||||
btnClasses="hover:bg-surface-tertiary"
|
||||
on:click={() => (resourceEditor?.initEdit?.(item.value ?? ''), close())}
|
||||
startIcon={{ icon: Pen }}
|
||||
iconOnly
|
||||
/>
|
||||
{#if !item.value?.startsWith('datatable://')}
|
||||
<Button
|
||||
{disabled}
|
||||
variant="subtle"
|
||||
size="xs2"
|
||||
wrapperClasses="-mr-2 pl-1 -my-2"
|
||||
btnClasses="hover:bg-surface-tertiary"
|
||||
on:click={() => (resourceEditor?.initEdit?.(item.value ?? ''), close())}
|
||||
startIcon={{ icon: Pen }}
|
||||
iconOnly
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet bottomSnippet({ close })}
|
||||
<div class="flex bg-surface border-t divide-x">
|
||||
@@ -276,7 +298,7 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</Select>
|
||||
{#if value && hovering}
|
||||
{#if value && hovering && !isDatatableSelected}
|
||||
<div class="absolute {disabled ? 'right-2' : 'right-10'} z-20">
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
||||
@@ -429,14 +429,14 @@
|
||||
}
|
||||
|
||||
function jobsFilter(f: 'waiting' | 'suspended') {
|
||||
filters.val.path = undefined
|
||||
filters.val.user = undefined
|
||||
filters.val.folder = undefined
|
||||
filters.val.label = undefined
|
||||
filters.val.concurrency_key = undefined
|
||||
filters.val.tag = undefined
|
||||
filters.val.worker = undefined
|
||||
filters.val.schedule_path = undefined
|
||||
delete filters.val.path
|
||||
delete filters.val.user
|
||||
delete filters.val.folder
|
||||
delete filters.val.label
|
||||
delete filters.val.concurrency_key
|
||||
delete filters.val.tag
|
||||
delete filters.val.worker
|
||||
delete filters.val.schedule_path
|
||||
filters.val.status = filters.val.status == f ? undefined : f
|
||||
filters.val.job_kinds = 'all'
|
||||
}
|
||||
@@ -703,9 +703,7 @@
|
||||
bind:checked={
|
||||
() => filters.val.show_future_jobs !== false,
|
||||
(v) =>
|
||||
v
|
||||
? delete filters.val.show_future_jobs
|
||||
: (filters.val.show_future_jobs = false)
|
||||
v ? delete filters.val.show_future_jobs : (filters.val.show_future_jobs = false)
|
||||
}
|
||||
/>
|
||||
<Clock size={14} />
|
||||
|
||||
@@ -13,38 +13,38 @@
|
||||
DollarSign,
|
||||
Folder,
|
||||
LayoutDashboard,
|
||||
Mail,
|
||||
Route,
|
||||
Unplug
|
||||
} from 'lucide-svelte'
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
kind:
|
||||
| 'script'
|
||||
| 'flow'
|
||||
| 'app'
|
||||
| 'raw_app'
|
||||
| 'resource'
|
||||
| 'variable'
|
||||
| 'resource_type'
|
||||
| 'folder'
|
||||
| 'schedule'
|
||||
| 'trigger'
|
||||
| 'routes'
|
||||
| 'schedules'
|
||||
| 'websockets'
|
||||
| 'postgres'
|
||||
| 'kafka'
|
||||
| 'nats'
|
||||
| 'mqtt'
|
||||
| 'sqs'
|
||||
| 'gcp';
|
||||
kind:
|
||||
| 'script'
|
||||
| 'flow'
|
||||
| 'app'
|
||||
| 'raw_app'
|
||||
| 'resource'
|
||||
| 'variable'
|
||||
| 'resource_type'
|
||||
| 'folder'
|
||||
| 'schedule'
|
||||
| 'trigger'
|
||||
| 'routes'
|
||||
| 'schedules'
|
||||
| 'websockets'
|
||||
| 'postgres'
|
||||
| 'kafka'
|
||||
| 'nats'
|
||||
| 'mqtt'
|
||||
| 'sqs'
|
||||
| 'gcp'
|
||||
| 'emails'
|
||||
/** For 'trigger' kind, specifies the specific trigger type (routes, schedules, etc.) */
|
||||
triggerKind?: string | undefined;
|
||||
triggerKind?: string | undefined
|
||||
}
|
||||
|
||||
let { kind, triggerKind = undefined }: Props = $props();
|
||||
let { kind, triggerKind = undefined }: Props = $props()
|
||||
|
||||
// Use triggerKind if kind is 'trigger' and triggerKind is provided
|
||||
let effectiveKind = $derived(kind === 'trigger' && triggerKind ? triggerKind : kind)
|
||||
@@ -83,6 +83,8 @@
|
||||
<AwsIcon size={16} class="text-gray-400" />
|
||||
{:else if effectiveKind === 'gcp'}
|
||||
<GoogleCloudIcon size={16} />
|
||||
{:else if effectiveKind === 'emails'}
|
||||
<Mail size={16} class="text-gray-400" />
|
||||
{:else if effectiveKind === 'trigger'}
|
||||
<Calendar size={16} class="text-gray-400" />
|
||||
{:else}
|
||||
|
||||
@@ -57,7 +57,7 @@ import { prepareApiSystemMessage, prepareApiUserMessage } from './api/core'
|
||||
import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic'
|
||||
import { getOpenAIResponsesCompletion, parseOpenAIResponsesCompletion } from './openai-responses'
|
||||
import type { ReviewChangesOpts } from './monaco-adapter'
|
||||
import { getCurrentModel, getCombinedCustomPrompt } from '$lib/aiStore'
|
||||
import { getCurrentModel, tryGetCurrentModel, getCombinedCustomPrompt } from '$lib/aiStore'
|
||||
|
||||
// If the estimated token usage is greater than the model context window - the threshold, we delete the oldest message
|
||||
const MAX_TOKENS_THRESHOLD_PERCENTAGE = 0.05
|
||||
@@ -229,11 +229,12 @@ class AIChatManager {
|
||||
closeScriptSettings?: boolean
|
||||
}
|
||||
) {
|
||||
if (mode === AIMode.SCRIPT && !tryGetCurrentModel()) return
|
||||
this.mode = mode
|
||||
this.pendingPrompt = pendingPrompt ?? ''
|
||||
if (mode === AIMode.SCRIPT) {
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
const currentModel = getCurrentModel()
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
const lang = this.scriptEditorOptions?.lang ?? 'bun'
|
||||
const context = this.contextManager.getSelectedContext()
|
||||
this.systemMessage = prepareScriptSystemMessage(currentModel, lang, {}, customPrompt)
|
||||
|
||||
@@ -55,7 +55,7 @@ export const AI_PROVIDERS: Record<AIProvider, AIProviderDetails> = {
|
||||
},
|
||||
anthropic: {
|
||||
label: 'Anthropic',
|
||||
defaultModels: ['claude-sonnet-4-0', 'claude-sonnet-4-0/thinking', 'claude-3-5-haiku-latest']
|
||||
defaultModels: ['claude-sonnet-4-6', 'claude-sonnet-4-6/thinking', 'claude-3-5-haiku-latest']
|
||||
},
|
||||
mistral: {
|
||||
label: 'Mistral',
|
||||
|
||||
@@ -585,7 +585,7 @@
|
||||
let assetNodesResult = $showAssets
|
||||
? computeAssetNodes(
|
||||
newNodes.map((n) => ({
|
||||
data: { assets: n.data?.assets as AssetWithAltAccessType[] },
|
||||
data: { assets: n.data?.assets as AssetWithAltAccessType[], offset: n.data?.offset as number },
|
||||
id: n.id,
|
||||
position: n.position
|
||||
}))
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
let computeAssetNodesCache: [NodeDep[], ReturnType<typeof computeAssetNodes>] | undefined
|
||||
|
||||
type NodeDep = {
|
||||
data: object & { assets?: AssetWithAltAccessType[] | undefined }
|
||||
data: object & { assets?: AssetWithAltAccessType[] | undefined; offset?: number }
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
}
|
||||
@@ -78,13 +78,14 @@
|
||||
width: inputAssetWidth,
|
||||
position: {
|
||||
x:
|
||||
displayedInputAssets.length === 1
|
||||
(node.data.offset ?? 0) +
|
||||
(displayedInputAssets.length === 1
|
||||
? (NODE.width - inputAssetWidth) / 2 - 10 // Ensure we see the edge
|
||||
: (inputAssetWidth + inputAssetXGap) * (i - displayedInputAssets.length / 2) +
|
||||
(NODE.width + inputAssetXGap) / 2 +
|
||||
(overflowedInputAssets.length
|
||||
? (-ASSETS_OVERFLOWED_NODE_WIDTH - inputAssetXGap) / 2
|
||||
: 0),
|
||||
: 0)),
|
||||
y: READ_ASSET_Y_OFFSET
|
||||
},
|
||||
selectable: false
|
||||
@@ -115,13 +116,14 @@
|
||||
width: outputAssetWidth,
|
||||
position: {
|
||||
x:
|
||||
displayedOutputAssets.length === 1
|
||||
(node.data.offset ?? 0) +
|
||||
(displayedOutputAssets.length === 1
|
||||
? (NODE.width - outputAssetWidth) / 2 - 10 // Ensure we see the edge
|
||||
: (outputAssetWidth + outputAssetXGap) * (i - displayedOutputAssets.length / 2) +
|
||||
(NODE.width + outputAssetXGap) / 2 +
|
||||
(overflowedOutputAssets.length
|
||||
? (-ASSETS_OVERFLOWED_NODE_WIDTH - outputAssetXGap) / 2
|
||||
: 0),
|
||||
: 0)),
|
||||
y: WRITE_ASSET_Y_OFFSET
|
||||
},
|
||||
selectable: false
|
||||
@@ -155,7 +157,7 @@
|
||||
parentId: node.id,
|
||||
width: ASSETS_OVERFLOWED_NODE_WIDTH,
|
||||
position: {
|
||||
x: MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14,
|
||||
x: (node.data.offset ?? 0) + MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14,
|
||||
y: READ_ASSET_Y_OFFSET
|
||||
}
|
||||
} satisfies Node & AssetsOverflowedN)
|
||||
@@ -174,7 +176,7 @@
|
||||
parentId: node.id,
|
||||
width: ASSETS_OVERFLOWED_NODE_WIDTH,
|
||||
position: {
|
||||
x: MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14,
|
||||
x: (node.data.offset ?? 0) + MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14,
|
||||
y: WRITE_ASSET_Y_OFFSET
|
||||
}
|
||||
} satisfies Node & AssetsOverflowedN)
|
||||
|
||||
@@ -640,6 +640,7 @@
|
||||
disabled={!can_write}
|
||||
bind:value={postgres_resource_path}
|
||||
resourceType={'postgresql'}
|
||||
datatableAsPgResource
|
||||
/>
|
||||
<CheckPostgresRequirement bind:postgres_resource_path bind:can_write />
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,8 @@
|
||||
`GRANT CREATE ON DATABASE "${dbname}" TO custom_instance_user;\n` +
|
||||
'ALTER DEFAULT PRIVILEGES IN SCHEMA public \n' +
|
||||
' GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES\n TO custom_instance_user;\n' +
|
||||
'ALTER ROLE custom_instance_user CREATEROLE;'
|
||||
'ALTER ROLE custom_instance_user CREATEROLE;\n' +
|
||||
'ALTER ROLE custom_instance_user REPLICATION;'
|
||||
}
|
||||
],
|
||||
status?.error ?? undefined
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { Plus, SettingsIcon } from 'lucide-svelte'
|
||||
|
||||
import Button from '../common/button/Button.svelte'
|
||||
|
||||
@@ -88,10 +88,11 @@ const config = {
|
||||
plugins: [sveltekit(), ...(process.env.HTTPS === 'true' ? [mkcert()] : []), plugin],
|
||||
define: { __pkg__: version },
|
||||
optimizeDeps: {
|
||||
include: ['highlight.js', 'highlight.js/lib/core', 'monaco-vim', 'monaco-editor-wrapper'],
|
||||
include: ['highlight.js', 'highlight.js/lib/core', 'monaco-vim'],
|
||||
exclude: [
|
||||
'@codingame/monaco-vscode-standalone-typescript-language-features',
|
||||
'@codingame/monaco-vscode-standalone-languages',
|
||||
'windmill-client'
|
||||
]
|
||||
},
|
||||
worker: { format: 'es' },
|
||||
|
||||
+79
-2
@@ -199,6 +199,40 @@ cmd_close() {
|
||||
workmux close $name
|
||||
}
|
||||
|
||||
cmd_open_ee() {
|
||||
local name=${1:?Usage: wm-cursor open-ee <name>}
|
||||
|
||||
local wt_path=$(workmux path $name)
|
||||
local main_repo_root="$(cd "$(git -C "$wt_path" rev-parse --git-common-dir 2>/dev/null)/.." && pwd)"
|
||||
|
||||
# Find ee repo (same discovery logic as worktree-env)
|
||||
local ee_repo="" candidate
|
||||
for candidate in \
|
||||
"${main_repo_root:+${main_repo_root}/../windmill-ee-private}" \
|
||||
"${wt_path}/../windmill-ee-private" \
|
||||
"${HOME}/windmill-ee-private" \
|
||||
"${HOME}/projects/windmill-ee-private"; do
|
||||
if [[ -n $candidate ]] && [[ -d $candidate ]]; then
|
||||
ee_repo=${candidate:A}
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z $ee_repo ]]; then
|
||||
print -u2 "Error: Could not find windmill-ee-private repo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local ee_worktree_dir="${ee_repo}__worktrees/${name}"
|
||||
if [[ ! -d $ee_worktree_dir ]]; then
|
||||
print -u2 "Error: EE worktree not found at ${ee_worktree_dir}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
$cursor_bin -n $ee_worktree_dir
|
||||
print "Opened Cursor for EE worktree: ${ee_worktree_dir}"
|
||||
}
|
||||
|
||||
cmd_setup() {
|
||||
local repo_root=${1:?Usage: wm-cursor setup <repo-root>}
|
||||
repo_root=${repo_root:A}
|
||||
@@ -275,7 +309,7 @@ json.dump(json.loads(text), sys.stdout, indent=2)
|
||||
print "Created ${settings_file}"
|
||||
fi
|
||||
|
||||
# --- zsh alias ---
|
||||
# --- zsh alias + completion ---
|
||||
local rc=${ZDOTDIR:-$HOME}/.zshrc
|
||||
local alias_line="alias wmc=${(q)script_path}"
|
||||
|
||||
@@ -286,6 +320,45 @@ json.dump(json.loads(text), sys.stdout, indent=2)
|
||||
print "\n# wm-cursor alias\n${alias_line}" >> $rc
|
||||
print "Added wmc alias to ${rc}"
|
||||
fi
|
||||
|
||||
local eval_line='eval "$(wmc completions)"'
|
||||
if ! grep -qF 'wmc completions' $rc; then
|
||||
print "${eval_line}" >> $rc
|
||||
print "Added completions eval to ${rc}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_completions() {
|
||||
cat <<'COMP'
|
||||
_wmc_worktree_names() {
|
||||
local -a names
|
||||
names=(${(f)"$(git worktree list --porcelain 2>/dev/null | sed -n 's|^worktree .*/||p' | tail -n +2)"})
|
||||
_describe 'worktree' names
|
||||
}
|
||||
|
||||
_wmc() {
|
||||
local -a subcmds=(
|
||||
'add:Create worktree + open Cursor'
|
||||
'open:Open Cursor for existing worktree'
|
||||
'open-ee:Open EE worktree in Cursor'
|
||||
'close:Clean up grouped tmux session'
|
||||
'setup:Set up .vscode settings, tasks + wmc alias'
|
||||
'completions:Print zsh completions'
|
||||
)
|
||||
|
||||
if (( CURRENT == 2 )); then
|
||||
_describe 'subcommand' subcmds
|
||||
else
|
||||
case $words[2] in
|
||||
open|open-ee|close)
|
||||
_wmc_worktree_names
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
compdef _wmc wmc wm-cursor
|
||||
COMP
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
@@ -293,16 +366,20 @@ json.dump(json.loads(text), sys.stdout, indent=2)
|
||||
case ${1-} in
|
||||
add) shift; cmd_add "$@" ;;
|
||||
open) shift; cmd_open "$@" ;;
|
||||
open-ee) shift; cmd_open_ee "$@" ;;
|
||||
close) shift; cmd_close "$@" ;;
|
||||
setup) shift; cmd_setup "$@" ;;
|
||||
completions) cmd_completions ;;
|
||||
*)
|
||||
print -u2 "Usage: wm-cursor <add|open|close|setup> [args...]"
|
||||
print -u2 "Usage: wm-cursor <add|open|open-ee|close|setup|completions> [args...]"
|
||||
print -u2 ""
|
||||
print -u2 "Subcommands:"
|
||||
print -u2 " add [--features <f>] [workmux-add-args...] Create worktree + open Cursor"
|
||||
print -u2 " open <name> [--features <f>] Open Cursor for existing worktree"
|
||||
print -u2 " open-ee <name> Open EE worktree in Cursor"
|
||||
print -u2 " close <name> Clean up grouped tmux session"
|
||||
print -u2 " setup <repo-root> Set up .vscode settings, tasks + wmc alias"
|
||||
print -u2 " completions Print zsh completions (use with eval)"
|
||||
print -u2 ""
|
||||
print -u2 "Options:"
|
||||
print -u2 " --features <features> Cargo features for the backend (e.g. \"enterprise,parquet\")"
|
||||
|
||||
@@ -23,21 +23,47 @@ else
|
||||
echo "[cleanup] No .env.local at $wt_dir/.env.local"
|
||||
fi
|
||||
|
||||
# Drop per-worktree database
|
||||
if [ -n "${WM_DB_NAME:-}" ]; then
|
||||
db_conn="postgres://postgres:changeme@127.0.0.1:5432"
|
||||
if command -v psql &>/dev/null; then
|
||||
psql "$db_conn/postgres" -c "DROP DATABASE IF EXISTS ${WM_DB_NAME} WITH (FORCE)" 2>/dev/null \
|
||||
&& echo "[cleanup] Dropped database $WM_DB_NAME" \
|
||||
|| echo "[cleanup] Warning: Could not drop database $WM_DB_NAME"
|
||||
else
|
||||
echo "[cleanup] psql not found, skipping database cleanup for $WM_DB_NAME"
|
||||
fi
|
||||
else
|
||||
echo "[cleanup] No WM_DB_NAME in .env.local, skipping database cleanup"
|
||||
fi
|
||||
|
||||
# Remove the matching windmill-ee-private worktree if one exists
|
||||
wt_basename=$(basename "$wt_dir")
|
||||
|
||||
# Check parent directory first (sibling to worktree root), then fall back to home
|
||||
# Find ee repo using same discovery logic as worktree-env
|
||||
main_repo_root="$(cd "$(git -C "$wt_dir" rev-parse --git-common-dir 2>/dev/null)/.." && pwd)"
|
||||
parent_dir="$(cd "$wt_dir/.." && pwd)"
|
||||
echo "[cleanup] wt_basename=$wt_basename parent_dir=$parent_dir"
|
||||
if [ -d "${parent_dir}/windmill-ee-private" ]; then
|
||||
ee_repo="${parent_dir}/windmill-ee-private"
|
||||
else
|
||||
ee_repo="${HOME}/windmill-ee-private"
|
||||
echo "[cleanup] wt_basename=$wt_basename main_repo_root=$main_repo_root parent_dir=$parent_dir"
|
||||
|
||||
ee_repo=""
|
||||
for candidate in \
|
||||
"${main_repo_root:+${main_repo_root}/../windmill-ee-private}" \
|
||||
"${parent_dir}/windmill-ee-private" \
|
||||
"${HOME}/windmill-ee-private" \
|
||||
"${HOME}/projects/windmill-ee-private"; do
|
||||
if [ -n "$candidate" ] && [ -d "$candidate" ]; then
|
||||
ee_repo="$(cd "$candidate" && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$ee_repo" ]; then
|
||||
echo "[cleanup] Could not find windmill-ee-private repo, skipping EE worktree cleanup"
|
||||
fi
|
||||
|
||||
ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}"
|
||||
echo "[cleanup] ee_repo=$ee_repo ee_worktree_dir=$ee_worktree_dir exists=$([ -d "$ee_worktree_dir" ] && echo yes || echo no)"
|
||||
if [ -d "$ee_worktree_dir" ]; then
|
||||
ee_worktree_dir="${ee_repo:+${ee_repo}__worktrees/${wt_basename}}"
|
||||
echo "[cleanup] ee_repo=${ee_repo:-<not found>} ee_worktree_dir=${ee_worktree_dir:-<none>} exists=$([ -n "$ee_worktree_dir" ] && [ -d "$ee_worktree_dir" ] && echo yes || echo no)"
|
||||
if [ -n "$ee_worktree_dir" ] && [ -d "$ee_worktree_dir" ]; then
|
||||
git -C "$ee_repo" worktree remove "$ee_worktree_dir" --force 2>/dev/null \
|
||||
&& echo "[cleanup] Removed EE worktree at $ee_worktree_dir" \
|
||||
|| echo "[cleanup] Warning: Could not remove EE worktree at $ee_worktree_dir"
|
||||
|
||||
@@ -52,6 +52,34 @@ fi
|
||||
|
||||
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
|
||||
|
||||
# --- Create per-worktree database ---
|
||||
wt_basename=$(basename "$(pwd)")
|
||||
db_name="windmill_${wt_basename//-/_}"
|
||||
db_conn="postgres://postgres:changeme@127.0.0.1:5432"
|
||||
|
||||
if command -v psql &>/dev/null; then
|
||||
if psql "$db_conn/postgres" -tc "SELECT 1 FROM pg_database WHERE datname = '${db_name}'" 2>/dev/null | grep -q 1; then
|
||||
echo "Database $db_name already exists"
|
||||
else
|
||||
psql "$db_conn/postgres" -c "CREATE DATABASE ${db_name}" 2>/dev/null \
|
||||
&& echo "Created database $db_name" \
|
||||
|| echo "WARNING: Could not create database $db_name (is PostgreSQL running?)" >&2
|
||||
fi
|
||||
db_url="${db_conn}/${db_name}?sslmode=disable"
|
||||
# Run migrations against the new database
|
||||
DATABASE_URL="$db_url" sqlx migrate run --source backend/migrations \
|
||||
&& echo "Migrations applied to $db_name" \
|
||||
|| echo "WARNING: Could not run migrations on $db_name" >&2
|
||||
# Use export so DATABASE_URL overrides the nix devshell value for child processes
|
||||
cat >> .env.local <<EOF
|
||||
export DATABASE_URL=$db_url
|
||||
WM_DB_NAME=$db_name
|
||||
EOF
|
||||
echo "Added DATABASE_URL for database $db_name to .env.local"
|
||||
else
|
||||
echo "WARNING: psql not found, skipping per-worktree database creation" >&2
|
||||
fi
|
||||
|
||||
# --- Copy frontend/node_modules preserving symlinks ---
|
||||
# cp -a preserves .bin/ symlinks that cp -r would dereference, breaking require() paths
|
||||
main_repo_root="$(cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." && pwd)"
|
||||
@@ -66,6 +94,25 @@ if command -v direnv &>/dev/null && [ -f .envrc ]; then
|
||||
echo "direnv allowed"
|
||||
fi
|
||||
|
||||
# --- Trust worktree directory in Claude Code ---
|
||||
claude_json="$HOME/.claude.json"
|
||||
if [ -f "$claude_json" ]; then
|
||||
wt_path="$(pwd)"
|
||||
python3 -c "
|
||||
import json, sys
|
||||
path = '$wt_path'
|
||||
with open('$claude_json', 'r') as f:
|
||||
data = json.load(f)
|
||||
projects = data.setdefault('projects', {})
|
||||
proj = projects.setdefault(path, {})
|
||||
proj['hasTrustDialogAccepted'] = True
|
||||
proj['hasCompletedProjectOnboarding'] = True
|
||||
with open('$claude_json', 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
" && echo "Added $wt_path to Claude Code trusted directories" \
|
||||
|| echo "Warning: Could not update Claude Code trusted directories"
|
||||
fi
|
||||
|
||||
# --- Create matching windmill-ee-private worktree ---
|
||||
# Find ee repo: sibling to the main worktree (git toplevel of the main checkout),
|
||||
# then try parent of cwd, then fall back to home
|
||||
|
||||
@@ -1169,6 +1169,8 @@ interface SlackApprovalOptions {
|
||||
approver?: string;
|
||||
defaultArgsJson?: Record<string, any>;
|
||||
dynamicEnumsJson?: Record<string, any>;
|
||||
resumeButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
}
|
||||
|
||||
interface TeamsApprovalOptions {
|
||||
@@ -1208,6 +1210,8 @@ interface TeamsApprovalOptions {
|
||||
* approver: "approver123",
|
||||
* defaultArgsJson: { key1: "value1", key2: 42 },
|
||||
* dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
|
||||
* resumeButtonText: "Resume",
|
||||
* cancelButtonText: "Cancel",
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
@@ -1220,6 +1224,8 @@ export async function requestInteractiveSlackApproval({
|
||||
approver,
|
||||
defaultArgsJson,
|
||||
dynamicEnumsJson,
|
||||
resumeButtonText,
|
||||
cancelButtonText,
|
||||
}: SlackApprovalOptions): Promise<void> {
|
||||
const workspace = getWorkspace();
|
||||
const flowJobId = getEnv("WM_FLOW_JOB_ID");
|
||||
@@ -1244,6 +1250,8 @@ export async function requestInteractiveSlackApproval({
|
||||
flowStepId: string;
|
||||
defaultArgsJson?: string;
|
||||
dynamicEnumsJson?: string;
|
||||
resumeButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
} = {
|
||||
slackResourcePath,
|
||||
channelId,
|
||||
@@ -1265,6 +1273,13 @@ export async function requestInteractiveSlackApproval({
|
||||
params.dynamicEnumsJson = JSON.stringify(dynamicEnumsJson);
|
||||
}
|
||||
|
||||
if (resumeButtonText) {
|
||||
params.resumeButtonText = resumeButtonText;
|
||||
}
|
||||
if (cancelButtonText) {
|
||||
params.cancelButtonText = cancelButtonText;
|
||||
}
|
||||
|
||||
await JobService.getSlackApprovalPayload({
|
||||
workspace,
|
||||
...params,
|
||||
|
||||
Reference in New Issue
Block a user