Compare commits
40 Commits
rf/strip2
...
rf/debugKa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ece4573076 | ||
|
|
833d0df965 | ||
|
|
1e7b7cc8d6 | ||
|
|
1de8eefe96 | ||
|
|
51a710ffa0 | ||
|
|
ad0c836129 | ||
|
|
be6707b461 | ||
|
|
c227ba2794 | ||
|
|
d60a770eb7 | ||
|
|
30d83a942c | ||
|
|
98bf9355dc | ||
|
|
a3928101af | ||
|
|
6e77757939 | ||
|
|
66c5d6e400 | ||
|
|
b0c184767c | ||
|
|
1165cf6e24 | ||
|
|
a9640107f3 | ||
|
|
73bc13bb7d | ||
|
|
e98966283d | ||
|
|
8ad2344ec6 | ||
|
|
b77c239f30 | ||
|
|
15cb83936b | ||
|
|
e2157ee1c0 | ||
|
|
bdfd50b970 | ||
|
|
4debfd68dc | ||
|
|
410032060a | ||
|
|
22b8fed9d9 | ||
|
|
0172587b12 | ||
|
|
f05f9e4edb | ||
|
|
24b1fa0ae3 | ||
|
|
fedb9fe74c | ||
|
|
5408da829c | ||
|
|
448e45b7e4 | ||
|
|
156f257619 | ||
|
|
7f40373fd6 | ||
|
|
f2ec03ba1b | ||
|
|
9a79c5537e | ||
|
|
cf156b0591 | ||
|
|
832ddabdf2 | ||
|
|
22080312f0 |
@@ -306,6 +306,9 @@ upcoming CLI tool.
|
||||
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
|
||||
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
|
||||
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
|
||||
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
|
||||
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
|
||||
| |
|
||||
|
||||
|
||||
|
||||
|
||||
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -4570,8 +4570,10 @@ dependencies = [
|
||||
"hmac",
|
||||
"hyper",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"magic-crypt",
|
||||
"mime_guess",
|
||||
"prometheus",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"retainer",
|
||||
|
||||
1
backend/migrations/20230126023323_webhook.down.sql
Normal file
1
backend/migrations/20230126023323_webhook.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
5
backend/migrations/20230126023323_webhook.up.sql
Normal file
5
backend/migrations/20230126023323_webhook.up.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE
|
||||
workspace_settings
|
||||
ADD
|
||||
COLUMN webhook text;
|
||||
@@ -104,6 +104,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
|
||||
"bytes" => Typ::Bytes,
|
||||
"datetime" => Typ::Datetime,
|
||||
"datetime.datetime" => Typ::Datetime,
|
||||
"Sql" | "sql" => Typ::Sql,
|
||||
_ => Typ::Resource(id),
|
||||
},
|
||||
_ => Typ::Unknown,
|
||||
|
||||
@@ -548,6 +548,21 @@
|
||||
"name": "auto_invite_operator",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "customer_id",
|
||||
"ordinal": 7,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "plan",
|
||||
"ordinal": 8,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "webhook",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -557,6 +572,9 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
@@ -1219,6 +1237,18 @@
|
||||
},
|
||||
"query": "INSERT INTO usr\n (workspace_id, email, username, is_admin, operator)\n VALUES ($1, $2, $3, $4, $5)"
|
||||
},
|
||||
"33d69b3915ddfde40323ace65c14e39fa4bbc8b5dd50a34e165765eaea1f4966": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1"
|
||||
},
|
||||
"355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1759,23 +1789,6 @@
|
||||
},
|
||||
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2),\n ARRAY['step'], $3)\n WHERE id = $4\n "
|
||||
},
|
||||
"53f2836a652d34aafe6ffb266a092d7390212bc5532dcc226529a61a53deec42": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6)"
|
||||
},
|
||||
"541ebd3bac65431237cf3b882dfdcd61ca97c253d9754d05bba59fda89841067": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1845,6 +1858,21 @@
|
||||
"name": "auto_invite_operator",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "customer_id",
|
||||
"ordinal": 7,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "plan",
|
||||
"ordinal": 8,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "webhook",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -1854,6 +1882,9 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
@@ -2064,6 +2095,26 @@
|
||||
},
|
||||
"query": "DELETE FROM group_ WHERE name = $1 AND workspace_id = $2"
|
||||
},
|
||||
"5ba4b87528ad49f17d72b53c3db30f5ca4b3b0b0afbd5d9721c8b5d692af601b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT COUNT(id) FROM queue WHERE email = $1"
|
||||
},
|
||||
"5c377fffc224a06f693c125f4c13b0a9ccfc217190ba6cf78246294bbc6c93bc": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2683,6 +2734,19 @@
|
||||
},
|
||||
"query": "UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash"
|
||||
},
|
||||
"8292b7b2cce5319575bc09ad18f29b63270872b6e5c6df1f0a326370058f13b0": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2"
|
||||
},
|
||||
"82f3c4cd1c1f6aea86d66f675442587684391bc32be9ab55ae20aab549b7bba5": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -3738,6 +3802,26 @@
|
||||
},
|
||||
"query": "SELECT email FROM usr where username = $1 AND workspace_id = $2"
|
||||
},
|
||||
"a34b79872766941cae2d62c99d80e28b7214dd2fcbb68020a63325bbcb34f417": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "webhook",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT webhook FROM workspace_settings WHERE workspace_id = $1"
|
||||
},
|
||||
"a38059dc3574da498ce986c916b6d385b1f18d5bd659ef13c43fafa9daff6bda": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -4077,6 +4161,26 @@
|
||||
},
|
||||
"query": "\n SELECT resume_id, approver\n FROM resume_job\n WHERE job = $1\n "
|
||||
},
|
||||
"b053117536c067095e2fb2864ce5af33f84b22c24c92fcb870f37f0501f8ea9a": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT COUNT(id) FROM queue WHERE running = true AND email = $1"
|
||||
},
|
||||
"b05c5f62ef4aa21d33369130cced0e9d7d128727eb58a9be7ae69cbb16bcbb27": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -4564,6 +4668,26 @@
|
||||
},
|
||||
"query": "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *"
|
||||
},
|
||||
"c07c9276945663d062cf0ff5b3323be681a0e2cb07a457ea9aede2daeff551cc": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "customer_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT customer_id FROM workspace_settings WHERE workspace_id = $1"
|
||||
},
|
||||
"c1149f74ef0cca4a48be80f523d339d78e8e20e2bfacac9ad1b6b75c5ae370fb": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -5556,6 +5680,24 @@
|
||||
},
|
||||
"query": "DELETE FROM password WHERE email = $1"
|
||||
},
|
||||
"e9c0e331c16312bf086b17c91466c5389d41454fd3f18d73c2e9554845ee9a72": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin, email)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7)"
|
||||
},
|
||||
"ea8ebb8d972fe99c960b5a69f794ee2b57bfb1914bf370c5b10313e45fa9b65f": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
||||
@@ -262,7 +262,7 @@ mod suspend_resume {
|
||||
// print_job(second, &db).await;
|
||||
|
||||
let tx = db.begin().await.unwrap();
|
||||
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100).await.unwrap();
|
||||
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
|
||||
tx.commit().await.unwrap();
|
||||
let secret = reqwest::get(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
|
||||
@@ -365,7 +365,7 @@ mod suspend_resume {
|
||||
let second = completed.next().await.unwrap();
|
||||
|
||||
let tx = db.begin().await.unwrap();
|
||||
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100).await.unwrap();
|
||||
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
|
||||
tx.commit().await.unwrap();
|
||||
let secret = reqwest::get(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
|
||||
@@ -395,7 +395,7 @@ mod suspend_resume {
|
||||
let result = completed_job(flow, &db).await.result.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
json!( {"error": {"name": "InternalErr", "message": "{\"message\":\"Job canceled: approval request disapproved by unknown\",\"name\":\"Canceled\",\"reason\":\"approval request disapproved\",\"canceler\":\"unknown\"}"}}),
|
||||
json!( {"error": {"name": "Canceled", "reason": "approval request disapproved", "message": "Job canceled: approval request disapproved by unknown", "canceler": "unknown"}}),
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,3 +70,5 @@ cookie.workspace = true
|
||||
sha2.workspace = true
|
||||
urlencoding.workspace = true
|
||||
async-stripe.workspace = true
|
||||
lazy_static.workspace = true
|
||||
prometheus.workspace = true
|
||||
|
||||
@@ -883,6 +883,8 @@ paths:
|
||||
type: string
|
||||
customer_id:
|
||||
type: string
|
||||
webhook:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/premium_info:
|
||||
get:
|
||||
@@ -962,6 +964,33 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_webhook:
|
||||
post:
|
||||
summary: edit webhook
|
||||
operationId: editWebhook
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: WorkspaceWebhook
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
webhook:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/users/list:
|
||||
get:
|
||||
summary: list users
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
jobs::script_path_to_payload,
|
||||
users::{require_owner_of_path, Authed, OptAuthed},
|
||||
variables::build_crypt,
|
||||
webhook_util::{WebhookMessage, WebhookUtil},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Json, Path, Query},
|
||||
@@ -310,6 +311,7 @@ async fn get_secret_id(
|
||||
async fn create_app(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path(w_id): Path<String>,
|
||||
Json(app): Json<CreateApp>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
@@ -356,7 +358,12 @@ async fn create_app(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::CreateApp {
|
||||
workspace: w_id.clone(),
|
||||
path: app.path.clone(),
|
||||
});
|
||||
|
||||
Ok((StatusCode::CREATED, app.path))
|
||||
}
|
||||
@@ -395,6 +402,7 @@ pub async fn get_hub_app_by_id(
|
||||
async fn delete_app(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
@@ -418,6 +426,8 @@ async fn delete_app(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook
|
||||
.send_message(WebhookMessage::DeleteApp { workspace: w_id.clone(), path: path.to_owned() });
|
||||
|
||||
Ok(format!("app {} deleted", path))
|
||||
}
|
||||
@@ -425,6 +435,7 @@ async fn delete_app(
|
||||
async fn update_app(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditApp>,
|
||||
@@ -514,6 +525,11 @@ async fn update_app(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::UpdateApp {
|
||||
workspace: w_id.clone(),
|
||||
old_path: path.to_owned(),
|
||||
new_path: npath.clone(),
|
||||
});
|
||||
|
||||
Ok(format!("app {} updated (npath: {:?})", path, npath))
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
schedule::clear_schedule,
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookUtil},
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -181,6 +182,7 @@ async fn check_path_conflict<'c>(
|
||||
async fn create_flow(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path(w_id): Path<String>,
|
||||
Json(nf): Json<NewFlow>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
@@ -221,6 +223,10 @@ async fn create_flow(
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::CreateFlow {
|
||||
workspace: w_id.clone(),
|
||||
path: nf.path.clone(),
|
||||
});
|
||||
|
||||
let tx = user_db.begin(&authed).await?;
|
||||
let (dependency_job_uuid, mut tx) = push(
|
||||
@@ -280,6 +286,7 @@ async fn update_flow(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
||||
Json(nf): Json<NewFlow>,
|
||||
) -> Result<String> {
|
||||
@@ -368,6 +375,11 @@ async fn update_flow(
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::UpdateFlow {
|
||||
workspace: w_id.clone(),
|
||||
old_path: flow_path.to_owned(),
|
||||
new_path: nf.path.clone(),
|
||||
});
|
||||
|
||||
let tx = user_db.begin(&authed).await?;
|
||||
let (dependency_job_uuid, mut tx) = push(
|
||||
@@ -450,6 +462,7 @@ async fn exists_flow_by_path(
|
||||
async fn archive_flow_by_path(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
@@ -474,6 +487,10 @@ async fn archive_flow_by_path(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::ArchiveFlow {
|
||||
workspace: w_id.clone(),
|
||||
path: path.to_owned(),
|
||||
});
|
||||
|
||||
Ok(format!("Flow {path} archived"))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::Authed,
|
||||
webhook_util::{WebhookMessage, WebhookUtil},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -139,6 +140,7 @@ async fn check_name_conflict<'c>(
|
||||
async fn create_folder(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ng): Json<NewFolder>,
|
||||
) -> Result<String> {
|
||||
@@ -193,8 +195,12 @@ async fn create_folder(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::CreateFolder {
|
||||
workspace: w_id.clone(),
|
||||
name: ng.name.clone(),
|
||||
});
|
||||
|
||||
Ok(format!("Created folder {}", ng.name))
|
||||
}
|
||||
|
||||
@@ -245,6 +251,7 @@ pub async fn require_is_owner(
|
||||
async fn update_folder(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(ng): Json<UpdateFolder>,
|
||||
) -> Result<String> {
|
||||
@@ -298,8 +305,12 @@ async fn update_folder(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::UpdateFolder {
|
||||
workspace: w_id.clone(),
|
||||
name: name.to_owned(),
|
||||
});
|
||||
|
||||
Ok(format!("Updated folder {}", name))
|
||||
}
|
||||
|
||||
@@ -416,6 +427,7 @@ async fn get_folder_usage(
|
||||
async fn delete_folder(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
) -> Result<String> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
@@ -440,6 +452,10 @@ async fn delete_folder(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook
|
||||
.send_message(WebhookMessage::DeleteFolder { workspace: w_id.clone(), name: name.clone() });
|
||||
|
||||
Ok(format!("delete folder at name {}", name))
|
||||
}
|
||||
|
||||
@@ -447,6 +463,7 @@ async fn add_owner(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(Owner { owner }): Json<Owner>,
|
||||
) -> Result<String> {
|
||||
@@ -477,6 +494,10 @@ async fn add_owner(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook
|
||||
.send_message(WebhookMessage::UpdateFolder { workspace: w_id.clone(), name: name.clone() });
|
||||
|
||||
Ok(format!("Added {} to folder {}", owner, name))
|
||||
}
|
||||
|
||||
@@ -510,6 +531,7 @@ async fn remove_owner(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(Owner { owner }): Json<Owner>,
|
||||
) -> Result<String> {
|
||||
@@ -540,5 +562,9 @@ async fn remove_owner(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook
|
||||
.send_message(WebhookMessage::UpdateFolder { workspace: w_id.clone(), name: name.clone() });
|
||||
|
||||
Ok(format!("Removed {} to folder {}", owner, name))
|
||||
}
|
||||
|
||||
@@ -103,7 +103,9 @@ async fn get_result_by_id(
|
||||
Query(ResultByIdQuery { skip_direct }): Query<ResultByIdQuery>,
|
||||
Path((w_id, flow_id, node_id)): Path<(String, String, String)>,
|
||||
) -> windmill_common::error::JsonResult<serde_json::Value> {
|
||||
tracing::error!("get_result_by_id_bef: {:?} {:?}", flow_id, node_id);
|
||||
let res = windmill_queue::get_result_by_id(db, skip_direct, w_id, flow_id, node_id).await?;
|
||||
tracing::error!("get_result_by_id: {:?}", res);
|
||||
Ok(Json(res))
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::{
|
||||
oauth2::{build_oauth_clients, SlackVerifier},
|
||||
tracing_init::{MyMakeSpan, MyOnResponse},
|
||||
users::{Authed, OptAuthed},
|
||||
webhook_util::{WebhookShared, WebhookUtil},
|
||||
};
|
||||
|
||||
mod apps;
|
||||
@@ -42,6 +43,7 @@ mod tracing_init;
|
||||
mod users;
|
||||
mod utils;
|
||||
mod variables;
|
||||
mod webhook_util;
|
||||
mod worker_ping;
|
||||
mod workspaces;
|
||||
|
||||
@@ -106,7 +108,8 @@ pub async fn run_server(
|
||||
std::env::var("COOKIE_DOMAIN").ok(),
|
||||
))))
|
||||
.layer(Extension(http_client))
|
||||
.layer(CookieManagerLayer::new());
|
||||
.layer(CookieManagerLayer::new())
|
||||
.layer(Extension(WebhookShared::new(rx.resubscribe())));
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.nest(
|
||||
@@ -147,7 +150,8 @@ pub async fn run_server(
|
||||
.nest("/flows", flows::workspaced_service())
|
||||
.nest("/capture", capture::workspaced_service())
|
||||
.nest("/favorites", favorite::workspaced_service())
|
||||
.nest("/folders", folders::workspaced_service()),
|
||||
.nest("/folders", folders::workspaced_service())
|
||||
.route_layer(from_extractor::<WebhookUtil>()),
|
||||
)
|
||||
.nest("/workspaces", workspaces::global_service())
|
||||
.nest(
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookUtil},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -263,6 +264,7 @@ async fn check_path_conflict<'c>(
|
||||
async fn create_resource(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path(w_id): Path<String>,
|
||||
Json(resource): Json<CreateResource>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
@@ -293,6 +295,11 @@ async fn create_resource(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::CreateResource {
|
||||
workspace: w_id.clone(),
|
||||
path: resource.path.clone(),
|
||||
});
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
format!("resource {} created", resource.path),
|
||||
@@ -302,6 +309,7 @@ async fn create_resource(
|
||||
async fn delete_resource(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
@@ -333,12 +341,18 @@ async fn delete_resource(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::DeleteResource {
|
||||
workspace: w_id.clone(),
|
||||
path: path.to_owned(),
|
||||
});
|
||||
|
||||
Ok(format!("resource {} deleted", path))
|
||||
}
|
||||
|
||||
async fn update_resource(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditResource>,
|
||||
@@ -400,6 +414,12 @@ async fn update_resource(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::UpdateResource {
|
||||
workspace: w_id.clone(),
|
||||
old_path: path.to_owned(),
|
||||
new_path: npath.clone(),
|
||||
});
|
||||
|
||||
Ok(format!("resource {} updated (npath: {:?})", path, npath))
|
||||
}
|
||||
|
||||
@@ -411,6 +431,7 @@ struct UpdateResource {
|
||||
async fn update_resource_value(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(nv): Json<UpdateResource>,
|
||||
) -> Result<String> {
|
||||
@@ -436,6 +457,11 @@ async fn update_resource_value(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::UpdateResource {
|
||||
workspace: w_id.clone(),
|
||||
old_path: path.to_owned(),
|
||||
new_path: path.to_owned(),
|
||||
});
|
||||
|
||||
Ok(format!("value of resource {} updated", path))
|
||||
}
|
||||
@@ -513,6 +539,7 @@ async fn exists_resource_type(
|
||||
async fn create_resource_type(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path(w_id): Path<String>,
|
||||
Json(resource_type): Json<CreateResourceType>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
@@ -543,6 +570,8 @@ async fn create_resource_type(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::CreateResourceType { name: resource_type.name.clone() });
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
format!("resource_type {} created", resource_type.name),
|
||||
@@ -574,6 +603,7 @@ async fn check_rt_path_conflict<'c>(
|
||||
async fn delete_resource_type(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
@@ -598,6 +628,7 @@ async fn delete_resource_type(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::DeleteResourceType { name: name.clone() });
|
||||
|
||||
Ok(format!("resource_type {} deleted", name))
|
||||
}
|
||||
@@ -605,6 +636,7 @@ async fn delete_resource_type(
|
||||
async fn update_resource_type(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(ns): Json<EditResourceType>,
|
||||
) -> Result<String> {
|
||||
@@ -634,6 +666,7 @@ async fn update_resource_type(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::UpdateResourceType { name: name.clone() });
|
||||
|
||||
Ok(format!("resource_type {} updated", name))
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
schedule::clear_schedule,
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookUtil},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -184,6 +185,7 @@ fn hash_script(ns: &NewScript) -> i64 {
|
||||
async fn create_script(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ns): Json<NewScript>,
|
||||
@@ -400,6 +402,11 @@ async fn create_script(
|
||||
Some([("hash", hash.to_string().as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
webhook.send_message(WebhookMessage::UpdateScript {
|
||||
workspace: w_id.clone(),
|
||||
path: ns.path.clone(),
|
||||
hash: hash.to_string(),
|
||||
});
|
||||
} else {
|
||||
audit_log(
|
||||
&mut tx,
|
||||
@@ -417,6 +424,11 @@ async fn create_script(
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
webhook.send_message(WebhookMessage::CreateScript {
|
||||
workspace: w_id.clone(),
|
||||
path: ns.path.clone(),
|
||||
hash: hash.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
@@ -600,6 +612,7 @@ async fn get_deployment_status(
|
||||
|
||||
async fn archive_script_by_path(
|
||||
authed: Authed,
|
||||
webhook: WebhookUtil,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
@@ -626,6 +639,10 @@ async fn archive_script_by_path(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(WebhookMessage::DeleteScript {
|
||||
workspace: w_id.clone(),
|
||||
hash: hash.to_string(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -633,6 +650,7 @@ async fn archive_script_by_path(
|
||||
async fn archive_script_by_hash(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
) -> JsonResult<Script> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
@@ -657,12 +675,18 @@ async fn archive_script_by_hash(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::DeleteScript {
|
||||
workspace: w_id.clone(),
|
||||
hash: hash.to_string(),
|
||||
});
|
||||
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
async fn delete_script_by_hash(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
) -> JsonResult<Script> {
|
||||
@@ -691,6 +715,11 @@ async fn delete_script_by_hash(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::DeleteScript {
|
||||
workspace: w_id.clone(),
|
||||
hash: hash.to_string(),
|
||||
});
|
||||
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
oauth2::{AllClients, _refresh_token},
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookUtil},
|
||||
BaseUrl,
|
||||
};
|
||||
/*
|
||||
@@ -224,6 +225,7 @@ async fn check_path_conflict<'c>(
|
||||
async fn create_variable(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path(w_id): Path<String>,
|
||||
Json(variable): Json<CreateVariable>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
@@ -265,6 +267,11 @@ async fn create_variable(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::CreateVariable {
|
||||
workspace: w_id.clone(),
|
||||
path: variable.path.clone(),
|
||||
});
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
format!("variable {} created", variable.path),
|
||||
@@ -274,6 +281,7 @@ async fn create_variable(
|
||||
async fn delete_variable(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
@@ -306,6 +314,11 @@ async fn delete_variable(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::DeleteVariable {
|
||||
workspace: w_id.clone(),
|
||||
path: path.to_owned(),
|
||||
});
|
||||
|
||||
Ok(format!("variable {} deleted", path))
|
||||
}
|
||||
|
||||
@@ -320,6 +333,7 @@ struct EditVariable {
|
||||
async fn update_variable(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
webhook: WebhookUtil,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditVariable>,
|
||||
@@ -405,6 +419,12 @@ async fn update_variable(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(WebhookMessage::UpdateVariable {
|
||||
workspace: w_id.clone(),
|
||||
old_path: path.to_owned(),
|
||||
new_path: npath.clone(),
|
||||
});
|
||||
|
||||
Ok(format!("variable {} updated (npath: {:?})", path, npath))
|
||||
}
|
||||
|
||||
|
||||
156
backend/windmill-api/src/webhook_util.rs
Normal file
156
backend/windmill-api/src/webhook_util.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRequestParts, OriginalUri},
|
||||
http::request::Parts,
|
||||
Extension,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use serde::Serialize;
|
||||
use tokio::{select, sync::mpsc};
|
||||
|
||||
use crate::db::DB;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// TODO: these aren't synced, they should be moved into the queue abstraction once/if that happens.
|
||||
static ref WEBHOOK_REQUEST_COUNT: prometheus::Histogram = prometheus::register_histogram!(
|
||||
"webhook_request",
|
||||
"Histogram of webhook requests made"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum WebhookMessage {
|
||||
// See https://serde.rs/enum-representations.html#internally-tagged for how this looks in JSON
|
||||
CreateApp { workspace: String, path: String },
|
||||
DeleteApp { workspace: String, path: String },
|
||||
UpdateApp { workspace: String, old_path: String, new_path: String },
|
||||
CreateFlow { workspace: String, path: String },
|
||||
UpdateFlow { workspace: String, old_path: String, new_path: String },
|
||||
ArchiveFlow { workspace: String, path: String },
|
||||
CreateFolder { workspace: String, name: String },
|
||||
UpdateFolder { workspace: String, name: String },
|
||||
DeleteFolder { workspace: String, name: String },
|
||||
DeleteResource { workspace: String, path: String },
|
||||
CreateResource { workspace: String, path: String },
|
||||
UpdateResource { workspace: String, old_path: String, new_path: String },
|
||||
CreateResourceType { name: String },
|
||||
DeleteResourceType { name: String },
|
||||
UpdateResourceType { name: String },
|
||||
CreateScript { workspace: String, path: String, hash: String },
|
||||
UpdateScript { workspace: String, path: String, hash: String },
|
||||
DeleteScript { workspace: String, hash: String },
|
||||
CreateVariable { workspace: String, path: String },
|
||||
UpdateVariable { workspace: String, old_path: String, new_path: String },
|
||||
DeleteVariable { workspace: String, path: String },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebhookShared {
|
||||
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
|
||||
}
|
||||
|
||||
impl WebhookShared {
|
||||
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>) -> Self {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<(String, WebhookMessage)>();
|
||||
let _process = tokio::spawn(async move {
|
||||
let client = reqwest::Client::builder()
|
||||
// TODO: investigate pool timeouts and such if TCP load is high
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = shutdown_rx.recv() => break,
|
||||
r = rx.recv() => match r {
|
||||
Some((url, message)) => {
|
||||
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
|
||||
let _ = client.post(url).json(&message).send().await;
|
||||
timer.stop_and_record();
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self { channel: tx }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebhookUtil {
|
||||
webhook: Option<String>,
|
||||
shared: Extension<WebhookShared>,
|
||||
}
|
||||
|
||||
impl WebhookUtil {
|
||||
pub fn send_message(&self, message: WebhookMessage) {
|
||||
let Some(webhook) = &self.webhook else {
|
||||
return;
|
||||
};
|
||||
let _ = self.shared.channel.send((webhook.clone(), message));
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for WebhookUtil
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, String);
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &S,
|
||||
) -> std::result::Result<Self, Self::Rejection> {
|
||||
let original_uri = OriginalUri::from_request_parts(parts, state)
|
||||
.await
|
||||
.ok()
|
||||
.map(|x| x.0)
|
||||
.unwrap_or_default();
|
||||
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
|
||||
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
|
||||
Some(path_vec[3].to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let webhook = sqlx::query_scalar!(
|
||||
"SELECT webhook FROM workspace_settings WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(
|
||||
&Extension::<DB>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not aquire DB while retrieving webhook".to_owned(),
|
||||
)
|
||||
})?
|
||||
.0,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Could not execute DB query {:?}", e),
|
||||
)
|
||||
})?;
|
||||
let shared = Extension::<WebhookShared>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not aquire shared process while retrieving webhook".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self { webhook, shared })
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/delete_invite", post(delete_invite))
|
||||
.route("/get_settings", get(get_settings))
|
||||
.route("/edit_slack_command", post(edit_slack_command))
|
||||
.route("/edit_webhook", post(edit_webhook))
|
||||
.route("/edit_auto_invite", post(edit_auto_invite))
|
||||
.route("/tarball", get(tarball_workspace))
|
||||
.route("/premium_info", get(premium_info))
|
||||
@@ -90,6 +91,7 @@ pub struct WorkspaceSettings {
|
||||
pub auto_invite_operator: Option<bool>,
|
||||
pub customer_id: Option<String>,
|
||||
pub plan: Option<String>,
|
||||
pub webhook: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -117,6 +119,11 @@ struct EditAutoInvite {
|
||||
operator: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditWebhook {
|
||||
webhook: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateWorkspace {
|
||||
id: String,
|
||||
@@ -509,6 +516,48 @@ async fn edit_auto_invite(
|
||||
))
|
||||
}
|
||||
|
||||
async fn edit_webhook(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Authed { is_admin, username, .. }: Authed,
|
||||
Json(ew): Json<EditWebhook>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
if let Some(webhook) = &ew.webhook {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2",
|
||||
webhook,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1",
|
||||
&w_id,
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"workspaces.edit_webhook",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("webhook", &format!("{:?}", ew.webhook)[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Edit webhook for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
async fn list_workspaces_as_super_admin(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
|
||||
@@ -58,6 +58,8 @@ pub enum Error {
|
||||
ExitStatus(i32),
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
#[error("Error: {0:#?}")]
|
||||
JsonErr(serde_json::Value),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Pool, Postgres, Transaction};
|
||||
use tracing::{instrument, Instrument};
|
||||
@@ -44,6 +45,7 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
const MAX_FREE_EXECS: i32 = 1000;
|
||||
const MAX_FREE_CONCURRENT_RUNS: i32 = 3;
|
||||
|
||||
pub async fn cancel_job<'c>(
|
||||
username: &str,
|
||||
@@ -317,10 +319,38 @@ pub async fn push<'c>(
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
if !is_super_admin && usage > MAX_FREE_EXECS {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
|
||||
if !is_super_admin {
|
||||
if usage > MAX_FREE_EXECS {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"User {email} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
let in_queue =
|
||||
sqlx::query_scalar!("SELECT COUNT(id) FROM queue WHERE email = $1", email)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
if in_queue > MAX_FREE_EXECS.into() {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"User {email} has exceeded the jobs in queue limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
|
||||
let concurrent_runs = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM queue WHERE running = true AND email = $1",
|
||||
email
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
if concurrent_runs > MAX_FREE_CONCURRENT_RUNS.into() {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"User {email} has exceeded the concurrent runs limit of {MAX_FREE_CONCURRENT_RUNS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,7 +381,9 @@ pub async fn push<'c>(
|
||||
)
|
||||
}
|
||||
JobPayload::ScriptHub { path } => {
|
||||
let script = get_hub_script(path.clone(), email).await?;
|
||||
let script = get_hub_script(path.clone(), email)
|
||||
.await
|
||||
.context("error fetching hub script")?;
|
||||
(
|
||||
None,
|
||||
Some(path),
|
||||
|
||||
@@ -72,6 +72,12 @@ mount {
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/import_map.json"
|
||||
dst: "/tmp/import_map.json"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/lock.json"
|
||||
dst: "/tmp/lock.json"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use sqlx::{Pool, Postgres, Transaction};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
@@ -27,16 +26,6 @@ pub async fn add_completed_job_error(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn error_to_result<E: ToString + std::fmt::Debug>(
|
||||
output_map: &mut Map<String, Value>,
|
||||
err: &E,
|
||||
) {
|
||||
output_map.insert(
|
||||
"error".to_string(),
|
||||
serde_json::Value::String(err.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn flatten_jobs(modules: Vec<FlowStatusModule>) -> Vec<Uuid> {
|
||||
modules
|
||||
.into_iter()
|
||||
|
||||
@@ -307,6 +307,19 @@ async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyho
|
||||
let node_id = &args[4];
|
||||
|
||||
let client = windmill_api_client::create_client(base_url, token.clone());
|
||||
let err = client
|
||||
.result_by_id(workspace, flow_job_id, node_id, Some(true))
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
let res = match err {
|
||||
windmill_api_client::Error::UnexpectedResponse(e) => {
|
||||
tracing::error!("{:?}", e.text().await);
|
||||
anyhow::anyhow!("bar")
|
||||
}
|
||||
_ => anyhow::anyhow!("foo"),
|
||||
};
|
||||
tracing::error!("{:?}", res);
|
||||
let result = client
|
||||
.result_by_id(workspace, flow_job_id, node_id, Some(true))
|
||||
.await
|
||||
|
||||
@@ -49,7 +49,7 @@ use futures::{
|
||||
use async_recursion::async_recursion;
|
||||
|
||||
use crate::{
|
||||
jobs::{add_completed_job, add_completed_job_error, error_to_result},
|
||||
jobs::{add_completed_job, add_completed_job_error},
|
||||
worker_flow::{
|
||||
handle_flow, update_flow_status_after_job_completion, update_flow_status_in_progress,
|
||||
},
|
||||
@@ -234,6 +234,7 @@ pub async fn create_token_for_owner<'c>(
|
||||
owner: &str,
|
||||
label: &str,
|
||||
expires_in: i32,
|
||||
email: &str,
|
||||
) -> error::Result<(Transaction<'c, Postgres>, String)> {
|
||||
// TODO: Bad implementation. We should not have access to this DB here.
|
||||
let token: String = rd_string(30);
|
||||
@@ -245,14 +246,15 @@ pub async fn create_token_for_owner<'c>(
|
||||
|
||||
sqlx::query_scalar!(
|
||||
"INSERT INTO token
|
||||
(workspace_id, token, owner, label, expiration, super_admin)
|
||||
VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6)",
|
||||
(workspace_id, token, owner, label, expiration, super_admin, email)
|
||||
VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7)",
|
||||
&w_id,
|
||||
token,
|
||||
owner,
|
||||
label,
|
||||
expires_in.to_string(),
|
||||
is_super_admin
|
||||
is_super_admin,
|
||||
email
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -414,10 +416,17 @@ pub async fn run_worker(
|
||||
let pip_index_url = std::env::var("PIP_INDEX_URL").ok();
|
||||
let pip_extra_index_url = std::env::var("PIP_EXTRA_INDEX_URL").ok();
|
||||
let pip_trusted_host = std::env::var("PIP_TRUSTED_HOST").ok();
|
||||
let deno_auth_tokens = std::env::var("DENO_AUTH_TOKENS")
|
||||
.ok()
|
||||
.map(|x| format!(";{x}"))
|
||||
.unwrap_or_else(|| String::new());
|
||||
let max_log_size = std::env::var("MAX_LOG_SIZE")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<i64>().ok())
|
||||
.unwrap_or(500000);
|
||||
let deno_flags = std::env::var("DENO_FLAGS")
|
||||
.ok()
|
||||
.map(|x| x.split(' ').map(|x| x.to_string()).collect());
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
let tar_cache_rate = std::env::var("TAR_CACHE_RATE")
|
||||
@@ -436,6 +445,8 @@ pub async fn run_worker(
|
||||
pip_extra_index_url,
|
||||
pip_trusted_host,
|
||||
max_log_size,
|
||||
deno_flags,
|
||||
deno_auth_tokens,
|
||||
};
|
||||
WORKER_STARTED.inc();
|
||||
|
||||
@@ -570,10 +581,11 @@ pub async fn run_worker(
|
||||
&job.permissioned_as,
|
||||
"ephemeral-script",
|
||||
timeout * 2,
|
||||
&job.email,
|
||||
)
|
||||
.await.expect("could not create job token");
|
||||
tx.commit().await.expect("could not commit job token");
|
||||
let job_client = windmill_api_client::create_client(&worker_config.base_url, token.clone());
|
||||
let job_client = windmill_api_client::create_client(&worker_config.base_internal_url, token.clone());
|
||||
let is_flow = job.job_kind == JobKind::Flow || job.job_kind == JobKind::FlowPreview || job.job_kind == JobKind::FlowDependencies;
|
||||
|
||||
if let Some(err) = handle_queued_job(
|
||||
@@ -649,24 +661,29 @@ async fn handle_job_error(
|
||||
keep_job_dir: bool,
|
||||
base_internal_url: &str,
|
||||
) {
|
||||
let _ = add_completed_job_error(
|
||||
db,
|
||||
&job,
|
||||
format!("Unexpected error during job execution:\n{err}"),
|
||||
json!({"message": err.to_string(), "name": "InternalErr"}),
|
||||
metrics.clone(),
|
||||
)
|
||||
.await;
|
||||
let err = match err {
|
||||
Error::JsonErr(err) => err,
|
||||
_ => json!({"message": err.to_string(), "name": "InternalErr"}),
|
||||
};
|
||||
|
||||
let update_job_future = || {
|
||||
add_completed_job_error(
|
||||
db,
|
||||
&job,
|
||||
format!("Unexpected error during job execution:\n{err}"),
|
||||
err.clone(),
|
||||
metrics.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
if job.is_flow_step || job.job_kind == JobKind::FlowPreview || job.job_kind == JobKind::Flow {
|
||||
let (flow, job_status_to_update) = if let Some(parent_job_id) = job.parent_job {
|
||||
let _ = update_job_future().await;
|
||||
(parent_job_id, job.id)
|
||||
} else {
|
||||
(job.id, Uuid::nil())
|
||||
};
|
||||
|
||||
let mut output_map = serde_json::Map::new();
|
||||
error_to_result(&mut output_map, &err);
|
||||
let updated_flow = update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
@@ -674,7 +691,7 @@ async fn handle_job_error(
|
||||
&job_status_to_update,
|
||||
&job.workspace_id,
|
||||
false,
|
||||
serde_json::Value::Object(output_map),
|
||||
json!({ "error": err }),
|
||||
metrics.clone(),
|
||||
unrecoverable,
|
||||
same_worker_tx,
|
||||
@@ -684,9 +701,8 @@ async fn handle_job_error(
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = updated_flow {
|
||||
println!("error updating flow status: {}", err);
|
||||
|
||||
if let Err(err) = updated_flow {
|
||||
if let Some(parent_job_id) = job.parent_job {
|
||||
if let Ok(mut tx) = db.begin().await {
|
||||
if let Ok(Some(parent_job)) =
|
||||
@@ -705,7 +721,10 @@ async fn handle_job_error(
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::error!(job_id = %job.id, err = err.alt(), "error handling job: {} {} {}", job.id, job.workspace_id, job.created_by);
|
||||
if job.parent_job.is_none() {
|
||||
let _ = update_job_future().await;
|
||||
}
|
||||
tracing::error!(job_id = %job.id, "error handling job: {err:#?} {} {} {}", job.id, job.workspace_id, job.created_by);
|
||||
}
|
||||
|
||||
async fn insert_initial_ping(
|
||||
@@ -735,6 +754,8 @@ struct Envs {
|
||||
pip_index_url: Option<String>,
|
||||
pip_extra_index_url: Option<String>,
|
||||
pip_trusted_host: Option<String>,
|
||||
deno_auth_tokens: String,
|
||||
deno_flags: Option<Vec<String>>,
|
||||
max_log_size: i64,
|
||||
}
|
||||
|
||||
@@ -758,9 +779,7 @@ async fn handle_queued_job(
|
||||
base_internal_url: &str,
|
||||
) -> windmill_common::error::Result<()> {
|
||||
if job.canceled {
|
||||
return Err(Error::ExecutionErr(
|
||||
canceled_job_to_result(&job).to_string(),
|
||||
))?;
|
||||
return Err(Error::JsonErr(canceled_job_to_result(&job)))?;
|
||||
}
|
||||
if let Some(e) = job.pre_run_error {
|
||||
return Err(Error::ExecutionErr(e));
|
||||
@@ -886,7 +905,7 @@ async fn handle_queued_job(
|
||||
}
|
||||
}
|
||||
err @ _ => {
|
||||
json!({"message": format!("error before termination: {err:#?}"), "name": "ExecutionErr"})
|
||||
json!({"message": format!("error during execution of the script:\n{}", err), "name": "ExecutionErr"})
|
||||
}
|
||||
};
|
||||
|
||||
@@ -932,6 +951,7 @@ async fn write_file(dir: &str, path: &str, content: &str) -> error::Result<File>
|
||||
|
||||
#[async_recursion]
|
||||
async fn transform_json_value(
|
||||
name: &str,
|
||||
client: &windmill_api_client::Client,
|
||||
workspace: &str,
|
||||
v: Value,
|
||||
@@ -942,7 +962,7 @@ async fn transform_json_value(
|
||||
let v = client
|
||||
.get_variable(workspace, path, Some(true))
|
||||
.await
|
||||
.map_err(|_| Error::NotFound(format!("Variable {path} not found")))
|
||||
.map_err(|_| Error::NotFound(format!("Variable {path} not found for `{name}`")))
|
||||
.map(|v| v.into_inner())?
|
||||
.value
|
||||
.unwrap_or_else(|| String::new());
|
||||
@@ -951,20 +971,23 @@ async fn transform_json_value(
|
||||
Value::String(y) if y.starts_with("$res:") => {
|
||||
let path = y.strip_prefix("$res:").unwrap();
|
||||
if path.split("/").count() < 2 {
|
||||
return Err(Error::InternalErr(
|
||||
format!("invalid resource path: {path}",),
|
||||
));
|
||||
return Err(Error::InternalErr(format!(
|
||||
"Argument `{name}` is an invalid resource path: {path}",
|
||||
)));
|
||||
}
|
||||
let v = client
|
||||
.get_resource_value(workspace, path)
|
||||
.await
|
||||
.map_err(|_| Error::NotFound(format!("Resource {path} not found")))?
|
||||
.map_err(|_| Error::NotFound(format!("Resource {path} not found for `{name}`")))?
|
||||
.into_inner();
|
||||
transform_json_value(client, workspace, v).await
|
||||
transform_json_value(name, client, workspace, v).await
|
||||
}
|
||||
Value::Object(mut m) => {
|
||||
for (a, b) in m.clone().into_iter() {
|
||||
m.insert(a, transform_json_value(client, workspace, b).await?);
|
||||
m.insert(
|
||||
a.clone(),
|
||||
transform_json_value(&a, client, workspace, b).await?,
|
||||
);
|
||||
}
|
||||
Ok(Value::Object(m))
|
||||
}
|
||||
@@ -1411,7 +1434,7 @@ fn capitalize(s: &str) -> String {
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_deno_job(
|
||||
WorkerConfig { base_internal_url, base_url, disable_nuser, disable_nsjail, .. }: &WorkerConfig,
|
||||
Envs { nsjail_path, deno_path, path_env, max_log_size, .. }: &Envs,
|
||||
Envs { nsjail_path, deno_path, path_env, max_log_size, deno_auth_tokens, deno_flags, .. }: &Envs,
|
||||
logs: &mut String,
|
||||
job: &QueuedJob,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
@@ -1461,12 +1484,23 @@ run().catch(async (e) => {{
|
||||
"#,
|
||||
);
|
||||
write_file(job_dir, "main.ts", &wrapper_content).await?;
|
||||
let w_id = job.workspace_id.clone();
|
||||
let import_map = format!(
|
||||
r#"{{
|
||||
"imports": {{
|
||||
"/": "{base_internal_url}/api/w/{w_id}/scripts/raw/p/",
|
||||
"./": "./"
|
||||
}}
|
||||
}}"#
|
||||
);
|
||||
write_file(job_dir, "import_map.json", &import_map).await?;
|
||||
let mut reserved_variables = get_reserved_variables(job, &token, &base_url, db).await?;
|
||||
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
|
||||
|
||||
let hostname_base = base_url.split("://").last().unwrap_or("localhost");
|
||||
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
|
||||
let deno_auth_tokens = format!("{token}@{hostname_base};{token}@{hostname_internal}");
|
||||
let deno_auth_tokens =
|
||||
format!("{token}@{hostname_base};{token}@{hostname_internal}{deno_auth_tokens}",);
|
||||
let child = async {
|
||||
Ok(if !disable_nsjail {
|
||||
let _ = write_file(
|
||||
@@ -1488,9 +1522,16 @@ run().catch(async (e) => {{
|
||||
if lockfile.is_some() {
|
||||
args.push("--lock=/tmp/lock.json");
|
||||
}
|
||||
args.push("--import-map");
|
||||
args.push("/tmp/import_map.json");
|
||||
args.push("--unstable");
|
||||
args.push("--v8-flags=--max-heap-size=2048");
|
||||
args.push("-A");
|
||||
if let Some(deno_flags) = deno_flags {
|
||||
for flag in deno_flags {
|
||||
args.push(flag);
|
||||
}
|
||||
} else {
|
||||
args.push("-A");
|
||||
}
|
||||
args.push("/tmp/main.ts");
|
||||
|
||||
Command::new(nsjail_path)
|
||||
@@ -1507,13 +1548,18 @@ run().catch(async (e) => {{
|
||||
} else {
|
||||
let mut args = Vec::new();
|
||||
let script_path = format!("{job_dir}/main.ts");
|
||||
let import_map_path = format!("{job_dir}/import_map.json");
|
||||
args.push("run");
|
||||
if lockfile.is_some() {
|
||||
args.push("--lock=/tmp/lock.json");
|
||||
}
|
||||
args.push("--import-map");
|
||||
args.push(&import_map_path);
|
||||
args.push("--unstable");
|
||||
args.push("--v8-flags=--max-heap-size=2048");
|
||||
args.push("-A");
|
||||
if let Some(deno_flags) = deno_flags {
|
||||
for flag in deno_flags {
|
||||
args.push(flag);
|
||||
}
|
||||
} else {
|
||||
args.push("-A");
|
||||
}
|
||||
args.push(&script_path);
|
||||
Command::new(deno_path)
|
||||
.current_dir(job_dir)
|
||||
@@ -1543,7 +1589,7 @@ async fn create_args_and_out_file(
|
||||
job_dir: &str,
|
||||
) -> Result<(), Error> {
|
||||
let args = if let Some(args) = &job.args {
|
||||
Some(transform_json_value(client, &job.workspace_id, args.clone()).await?)
|
||||
Some(transform_json_value("args", client, &job.workspace_id, args.clone()).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -2595,6 +2641,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, timeout: i32, base_url: &str) {
|
||||
&job.permissioned_as,
|
||||
"ephemeral-zombie-jobs",
|
||||
timeout * 2,
|
||||
&job.email,
|
||||
)
|
||||
.await
|
||||
.expect("could not create job token");
|
||||
|
||||
@@ -427,8 +427,10 @@ pub async fn update_flow_status_after_job_completion(
|
||||
"Flow job canceled".to_string()
|
||||
} else if stop_early {
|
||||
format!("Flow job stopped early because of a stop early predicate returning true")
|
||||
} else if success {
|
||||
"Flow job completed with success".to_string()
|
||||
} else {
|
||||
"Flow job completed".to_string()
|
||||
"Flow job completed with error".to_string()
|
||||
};
|
||||
if flow_job.canceled {
|
||||
add_completed_job_error(
|
||||
@@ -986,7 +988,7 @@ async fn push_next_flow_job(
|
||||
let success = false;
|
||||
let skipped = false;
|
||||
let logs = "Timed out waiting to be resumed".to_string();
|
||||
let result = json!({ "error": logs });
|
||||
let result = json!({ "error": {"message": logs, "name": "SuspendedTimeout"}});
|
||||
let _uuid =
|
||||
add_completed_job(db, &flow_job, success, skipped, result, logs).await?;
|
||||
|
||||
@@ -1881,6 +1883,7 @@ async fn get_transform_context(
|
||||
&flow_job.permissioned_as,
|
||||
"transform-input",
|
||||
10,
|
||||
&flow_job.email,
|
||||
)
|
||||
.await?;
|
||||
//we need to commit asap otherwise the token won't be valid for auth to check outside of this transaction
|
||||
|
||||
@@ -7,6 +7,12 @@ export {
|
||||
UserService, WorkspaceService
|
||||
} from './windmill-api/index.ts'
|
||||
|
||||
|
||||
// @ts-ignore: Otherwise BigInt is not supported for export
|
||||
BigInt.prototype.toJSON = function () {
|
||||
return this.toString();
|
||||
};
|
||||
|
||||
export { pgSql, pgClient } from './pg.ts'
|
||||
export { mySql, mysqlClient } from './mysql.ts'
|
||||
|
||||
|
||||
169
frontend/package-lock.json
generated
169
frontend/package-lock.json
generated
@@ -10,11 +10,11 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@leeoniya/ufuzzy": "^0.9.0",
|
||||
"@leeoniya/ufuzzy": "^0.9.1",
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.7.6",
|
||||
"async-mutex": "^0.4.0",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-zoom": "^2.0.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"highlight.js": "^11.7.0",
|
||||
@@ -37,7 +37,7 @@
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@windmill-labs/svelvet": "^4.0.20",
|
||||
"@zerodevx/svelte-toast": "^0.8.1",
|
||||
@@ -709,9 +709,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@leeoniya/ufuzzy": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-0.9.0.tgz",
|
||||
"integrity": "sha512-p2zWsX0GwO1x723Yhb3KLAoSwp1geQvzRPHgIoOR/0qn8Ptpsb3b01+W47iAYR/NWo0pX36XQoTU0alVRykMAg=="
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-0.9.1.tgz",
|
||||
"integrity": "sha512-xlL3HMhHdZX/UVTyjiLB0hpUa71HERhegvrfkUD9NxGnYh9kk9+/llynEWS+NmGRB2KlRKoY+HmWZzt/c0whJQ=="
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
@@ -1001,14 +1001,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz",
|
||||
"integrity": "sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz",
|
||||
"integrity": "sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.46.1",
|
||||
"@typescript-eslint/type-utils": "5.46.1",
|
||||
"@typescript-eslint/utils": "5.46.1",
|
||||
"@typescript-eslint/scope-manager": "5.49.0",
|
||||
"@typescript-eslint/type-utils": "5.49.0",
|
||||
"@typescript-eslint/utils": "5.49.0",
|
||||
"debug": "^4.3.4",
|
||||
"ignore": "^5.2.0",
|
||||
"natural-compare-lite": "^1.4.0",
|
||||
@@ -1135,13 +1135,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz",
|
||||
"integrity": "sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.49.0.tgz",
|
||||
"integrity": "sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.46.1",
|
||||
"@typescript-eslint/visitor-keys": "5.46.1"
|
||||
"@typescript-eslint/types": "5.49.0",
|
||||
"@typescript-eslint/visitor-keys": "5.49.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -1152,13 +1152,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz",
|
||||
"integrity": "sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz",
|
||||
"integrity": "sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "5.46.1",
|
||||
"@typescript-eslint/utils": "5.46.1",
|
||||
"@typescript-eslint/typescript-estree": "5.49.0",
|
||||
"@typescript-eslint/utils": "5.49.0",
|
||||
"debug": "^4.3.4",
|
||||
"tsutils": "^3.21.0"
|
||||
},
|
||||
@@ -1179,9 +1179,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.1.tgz",
|
||||
"integrity": "sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz",
|
||||
"integrity": "sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -1192,13 +1192,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz",
|
||||
"integrity": "sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz",
|
||||
"integrity": "sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.46.1",
|
||||
"@typescript-eslint/visitor-keys": "5.46.1",
|
||||
"@typescript-eslint/types": "5.49.0",
|
||||
"@typescript-eslint/visitor-keys": "5.49.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1219,16 +1219,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.1.tgz",
|
||||
"integrity": "sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz",
|
||||
"integrity": "sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@typescript-eslint/scope-manager": "5.46.1",
|
||||
"@typescript-eslint/types": "5.46.1",
|
||||
"@typescript-eslint/typescript-estree": "5.46.1",
|
||||
"@typescript-eslint/scope-manager": "5.49.0",
|
||||
"@typescript-eslint/types": "5.49.0",
|
||||
"@typescript-eslint/typescript-estree": "5.49.0",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0",
|
||||
"semver": "^7.3.7"
|
||||
@@ -1245,12 +1245,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz",
|
||||
"integrity": "sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.49.0.tgz",
|
||||
"integrity": "sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.46.1",
|
||||
"@typescript-eslint/types": "5.49.0",
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1868,11 +1868,12 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/chartjs-adapter-date-fns": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
|
||||
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^3.0.0"
|
||||
"chart.js": ">=2.8.0",
|
||||
"date-fns": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-plugin-zoom": {
|
||||
@@ -7537,9 +7538,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@leeoniya/ufuzzy": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-0.9.0.tgz",
|
||||
"integrity": "sha512-p2zWsX0GwO1x723Yhb3KLAoSwp1geQvzRPHgIoOR/0qn8Ptpsb3b01+W47iAYR/NWo0pX36XQoTU0alVRykMAg=="
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-0.9.1.tgz",
|
||||
"integrity": "sha512-xlL3HMhHdZX/UVTyjiLB0hpUa71HERhegvrfkUD9NxGnYh9kk9+/llynEWS+NmGRB2KlRKoY+HmWZzt/c0whJQ=="
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
@@ -7755,14 +7756,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz",
|
||||
"integrity": "sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz",
|
||||
"integrity": "sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "5.46.1",
|
||||
"@typescript-eslint/type-utils": "5.46.1",
|
||||
"@typescript-eslint/utils": "5.46.1",
|
||||
"@typescript-eslint/scope-manager": "5.49.0",
|
||||
"@typescript-eslint/type-utils": "5.49.0",
|
||||
"@typescript-eslint/utils": "5.49.0",
|
||||
"debug": "^4.3.4",
|
||||
"ignore": "^5.2.0",
|
||||
"natural-compare-lite": "^1.4.0",
|
||||
@@ -7827,41 +7828,41 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz",
|
||||
"integrity": "sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.49.0.tgz",
|
||||
"integrity": "sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "5.46.1",
|
||||
"@typescript-eslint/visitor-keys": "5.46.1"
|
||||
"@typescript-eslint/types": "5.49.0",
|
||||
"@typescript-eslint/visitor-keys": "5.49.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/type-utils": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz",
|
||||
"integrity": "sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz",
|
||||
"integrity": "sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/typescript-estree": "5.46.1",
|
||||
"@typescript-eslint/utils": "5.46.1",
|
||||
"@typescript-eslint/typescript-estree": "5.49.0",
|
||||
"@typescript-eslint/utils": "5.49.0",
|
||||
"debug": "^4.3.4",
|
||||
"tsutils": "^3.21.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.1.tgz",
|
||||
"integrity": "sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz",
|
||||
"integrity": "sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz",
|
||||
"integrity": "sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz",
|
||||
"integrity": "sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "5.46.1",
|
||||
"@typescript-eslint/visitor-keys": "5.46.1",
|
||||
"@typescript-eslint/types": "5.49.0",
|
||||
"@typescript-eslint/visitor-keys": "5.49.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -7870,28 +7871,28 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.1.tgz",
|
||||
"integrity": "sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz",
|
||||
"integrity": "sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@typescript-eslint/scope-manager": "5.46.1",
|
||||
"@typescript-eslint/types": "5.46.1",
|
||||
"@typescript-eslint/typescript-estree": "5.46.1",
|
||||
"@typescript-eslint/scope-manager": "5.49.0",
|
||||
"@typescript-eslint/types": "5.49.0",
|
||||
"@typescript-eslint/typescript-estree": "5.49.0",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0",
|
||||
"semver": "^7.3.7"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz",
|
||||
"integrity": "sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg==",
|
||||
"version": "5.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.49.0.tgz",
|
||||
"integrity": "sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "5.46.1",
|
||||
"@typescript-eslint/types": "5.49.0",
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
}
|
||||
},
|
||||
@@ -8314,9 +8315,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"chartjs-adapter-date-fns": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
|
||||
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||
"requires": {}
|
||||
},
|
||||
"chartjs-plugin-zoom": {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@windmill-labs/svelvet": "^4.0.20",
|
||||
"@zerodevx/svelte-toast": "^0.8.1",
|
||||
@@ -63,11 +63,11 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@leeoniya/ufuzzy": "^0.9.0",
|
||||
"@leeoniya/ufuzzy": "^0.9.1",
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.7.6",
|
||||
"async-mutex": "^0.4.0",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-zoom": "^2.0.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"highlight.js": "^11.7.0",
|
||||
|
||||
@@ -422,7 +422,6 @@
|
||||
</div>
|
||||
|
||||
<h2 class="mt-4 mb-2">Description</h2>
|
||||
|
||||
<textarea type="text" autocomplete="off" use:autosize bind:value={description} />
|
||||
{:else}
|
||||
<Path
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
>
|
||||
</div>
|
||||
{:else if !forceJson && resultKind == 'error'}<div>
|
||||
<span class="text-red-500 font-semibold text-sm"
|
||||
<span class="text-red-500 font-semibold text-sm whitespace-pre-wrap"
|
||||
>{result.error.name}: {result.error.message}</span
|
||||
>
|
||||
<pre class="text-sm whitespace-pre-wrap text-gray-900">{result.error.stack ?? ''}</pre>
|
||||
|
||||
@@ -411,7 +411,8 @@
|
||||
|
||||
model.updateOptions(updateOptions)
|
||||
editor = meditor.create(divEl as HTMLDivElement, {
|
||||
...editorConfig(model, code, lang, automaticLayout, fixedOverflowWidgets)
|
||||
...editorConfig(model, code, lang, automaticLayout, fixedOverflowWidgets),
|
||||
tabSize: lang == 'python' ? 4 : 2
|
||||
})
|
||||
|
||||
let timeoutModel: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
|
||||
@@ -6,6 +7,7 @@
|
||||
export let logs: string
|
||||
export let col: boolean = false
|
||||
export let noBorder = false
|
||||
export let loading
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -14,7 +16,13 @@
|
||||
>
|
||||
<div class="bg-white max-h-80 h-full p-1 overflow-auto relative">
|
||||
<span class="text-gray-500">Result</span>
|
||||
<DisplayResult {result} />
|
||||
{#if result}
|
||||
<DisplayResult {result} />
|
||||
{:else if loading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
<div class="text-gray-400">No result (result is undefined)</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="overflow-auto max-h-80 h-full relative">
|
||||
<LogViewer content={logs ?? ''} isLoading={false} />
|
||||
|
||||
@@ -149,8 +149,8 @@
|
||||
</div>
|
||||
<FlowProgressBar {job} bind:reset={jobProgressReset} />
|
||||
|
||||
<div class="overflow-y-auto grow flex-col flex divide-y divide-gray-600 ">
|
||||
<div class="max-h-1/2 overflow-auto">
|
||||
<div class="overflow-y-auto grow divide-y divide-gray-600 ">
|
||||
<div class="max-h-1/2 overflow-auto border-b border-gray-700">
|
||||
<SchemaForm
|
||||
noVariablePicker
|
||||
compact
|
||||
@@ -160,8 +160,7 @@
|
||||
bind:args={$previewArgs}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-full pt-4 grow">
|
||||
<div class="pt-4 grow">
|
||||
{#if jobId}
|
||||
<FlowStatusViewer bind:flowState={$flowStateStore} {jobId} bind:job />
|
||||
{:else}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import { FlowGraph, type GraphModuleState } from './graph'
|
||||
import ModuleStatus from './ModuleStatus.svelte'
|
||||
import { displayDate, isOwner, pluralize, truncateRev } from '$lib/utils'
|
||||
import { displayDate, emptyString, isOwner, pluralize, truncateRev } from '$lib/utils'
|
||||
import JobArgs from './JobArgs.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
@@ -180,7 +180,11 @@
|
||||
<FlowPreviewStatus {job} />
|
||||
{#if `result` in job}
|
||||
<div class="w-full h-full">
|
||||
<FlowJobResult result={job.result} logs={job.logs ?? ''} />
|
||||
<FlowJobResult
|
||||
loading={job['running'] == true}
|
||||
result={job.result}
|
||||
logs={job.logs ?? ''}
|
||||
/>
|
||||
</div>
|
||||
{:else if job.flow_status?.modules?.[job?.flow_status?.step]?.type === FlowStatusModule.type.WAITING_FOR_EVENTS}
|
||||
<div class="w-full h-full mt-2 text-sm text-gray-600">
|
||||
@@ -226,14 +230,21 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each innerModules as mod, i (mod.id)}
|
||||
{#if mod.type == FlowStatusModule.type.IN_PROGRESS}
|
||||
{@const rawMod = job.raw_flow?.modules[i]}
|
||||
|
||||
<div
|
||||
><span class="inline-flex gap-1"
|
||||
><Badge color="indigo">{mod.id}</Badge>
|
||||
{#if job.raw_flow?.modules[i]?.summary}
|
||||
<span class="font-medium text-gray-900">
|
||||
{job.raw_flow?.modules[i]?.summary ?? ''}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="font-medium text-gray-900">
|
||||
{#if !emptyString(rawMod?.summary)}
|
||||
{rawMod?.summary ?? ''}
|
||||
{:else if rawMod?.value.type == 'script'}
|
||||
{rawMod.value.path ?? ''}
|
||||
{:else if rawMod?.value.type}
|
||||
{rawMod?.value.type}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<Loader2 class="animate-spin" /></span
|
||||
></div
|
||||
>
|
||||
@@ -444,7 +455,13 @@
|
||||
{#if selectedNode}
|
||||
{@const node = localFlowModuleStates[selectedNode]}
|
||||
{#if selectedNode == 'end'}
|
||||
<FlowJobResult noBorder col result={job['result'] ?? {}} logs={job.logs ?? ''} />
|
||||
<FlowJobResult
|
||||
loading={job['running']}
|
||||
noBorder
|
||||
col
|
||||
result={job['result']}
|
||||
logs={job.logs ?? ''}
|
||||
/>
|
||||
{:else if selectedNode == 'start'}
|
||||
{#if job.args}
|
||||
<div class="p-2">
|
||||
@@ -471,7 +488,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<FlowJobResult noBorder col result={node.result ?? {}} logs={node.logs ?? ''} />
|
||||
<FlowJobResult
|
||||
loading={job['running'] == true}
|
||||
noBorder
|
||||
col
|
||||
result={node.result}
|
||||
logs={node.logs ?? ''}
|
||||
/>
|
||||
{:else}
|
||||
<p class="p-2 text-gray-600 italic"
|
||||
>The execution of this node has no information attached to it. The job likely did
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import Select from 'svelte-select'
|
||||
import AppConnect from './AppConnect.svelte'
|
||||
import ResourceEditor from './ResourceEditor.svelte'
|
||||
import { truncate } from '$lib/utils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let resources: Resource[] = []
|
||||
|
||||
@@ -43,23 +43,30 @@
|
||||
|
||||
function removeExtraKey() {
|
||||
Object.keys(args ?? {}).forEach((key) => {
|
||||
if (!Object.keys(schema?.properties ?? {}).includes(key)) {
|
||||
if (!keys.includes(key)) {
|
||||
delete args[key]
|
||||
delete inputCheck[key]
|
||||
console.log('DELETED', key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$: schema?.properties && removeExtraKey()
|
||||
|
||||
let pickForField: string | undefined
|
||||
let itemPicker: ItemPicker | undefined = undefined
|
||||
let variableEditor: VariableEditor | undefined = undefined
|
||||
let keys: string[] = []
|
||||
$: {
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
|
||||
keys = lkeys
|
||||
removeExtraKey()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 gap-y-2' : ''}">
|
||||
{#if Object.keys(schema?.properties ?? {}).length > 0}
|
||||
{#each Object.keys(schema?.properties ?? {}) as argName, i (argName)}
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
{#if !filter || filter.includes(argName)}
|
||||
<div transition:slide|local>
|
||||
{#if inputTransform}
|
||||
|
||||
@@ -60,9 +60,15 @@
|
||||
localStorage.removeItem(script.path)
|
||||
|
||||
script.schema = script.schema ?? emptySchema()
|
||||
if (!script.schema) {
|
||||
try {
|
||||
await inferArgs(script.language, script.content, script.schema)
|
||||
} catch (error) {
|
||||
sendUserToast(
|
||||
`Impossible to infer the schema. Assuming this is a script without main function`,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
const newHash = await ScriptService.createScript({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
@@ -87,7 +93,13 @@
|
||||
async function changeStep(step: number) {
|
||||
if (step > 1) {
|
||||
script.schema = script.schema ?? emptySchema()
|
||||
await inferArgs(script.language, script.content, script.schema)
|
||||
try {
|
||||
await inferArgs(script.language, script.content, script.schema)
|
||||
} catch (error) {
|
||||
console.info(
|
||||
'Impossible to infer the schema. Assuming this is a script without main function'
|
||||
)
|
||||
}
|
||||
}
|
||||
goto(`?step=${step}`)
|
||||
}
|
||||
|
||||
16
frontend/src/lib/components/Slider.svelte
Normal file
16
frontend/src/lib/components/Slider.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown, ChevronUp } from 'lucide-svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import Button from './common/button/Button.svelte'
|
||||
|
||||
export let text: string
|
||||
export let view = false
|
||||
</script>
|
||||
|
||||
<Button color="light" on:click={() => (view = !view)} variant="border"
|
||||
>{text}
|
||||
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
|
||||
>
|
||||
{#if view}
|
||||
<div class="my-4 px-2" transition:slide><slot /></div>
|
||||
{/if}
|
||||
@@ -54,7 +54,6 @@
|
||||
|
||||
<RunnableWrapper
|
||||
defaultUserInput
|
||||
noMinH
|
||||
bind:runnableComponent
|
||||
bind:componentInput
|
||||
{id}
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
>
|
||||
<RunnableWrapper
|
||||
defaultUserInput
|
||||
noMinH
|
||||
bind:runnableComponent
|
||||
bind:componentInput
|
||||
{id}
|
||||
|
||||
@@ -21,13 +21,21 @@
|
||||
export let autoRefresh: boolean = true
|
||||
export let result: any = undefined
|
||||
export let forceSchemaDisplay: boolean = false
|
||||
export let noMinH = false
|
||||
export let defaultUserInput = false
|
||||
export let flexWrap = false
|
||||
export let wrapperClass = ''
|
||||
|
||||
const { worldStore, runnableComponents, workspace, appPath, isEditor, jobs, noBackend } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
const {
|
||||
worldStore,
|
||||
runnableComponents,
|
||||
workspace,
|
||||
appPath,
|
||||
isEditor,
|
||||
jobs,
|
||||
noBackend,
|
||||
errorByComponent,
|
||||
mode
|
||||
} = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
onMount(() => {
|
||||
if (autoRefresh) {
|
||||
@@ -41,7 +49,6 @@
|
||||
let args: Record<string, any> = {}
|
||||
let testIsLoading = false
|
||||
let runnableInputValues: Record<string, any> = {}
|
||||
|
||||
let executeTimeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
function setDebouncedExecute() {
|
||||
@@ -245,7 +252,19 @@
|
||||
export async function runComponent() {
|
||||
await executeComponent()
|
||||
}
|
||||
|
||||
let lastStartedAt: number = Date.now()
|
||||
|
||||
function recordError(error: string) {
|
||||
if (testJob) {
|
||||
$errorByComponent[testJob.id] = {
|
||||
error: error,
|
||||
componentId: id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: result?.error && recordError(result.error)
|
||||
</script>
|
||||
|
||||
{#each Object.entries(fields ?? {}) as [key, v]}
|
||||
@@ -268,6 +287,15 @@
|
||||
lastStartedAt = startedAt
|
||||
outputs.result?.set(testJob?.result)
|
||||
result = testJob.result
|
||||
|
||||
const previousJobId = Object.keys($errorByComponent).find(
|
||||
(key) => $errorByComponent[key].componentId === id
|
||||
)
|
||||
|
||||
if (previousJobId && !result?.error) {
|
||||
delete $errorByComponent[previousJobId]
|
||||
$errorByComponent = $errorByComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -299,19 +327,21 @@
|
||||
<Alert type="warning" size="xs" class="mt-2 px-1" title="Missing runnable">
|
||||
Please select a runnable
|
||||
</Alert>
|
||||
{:else if result?.error}
|
||||
{:else if result?.error && $mode === 'preview'}
|
||||
<div class="p-2">
|
||||
<Alert type="error" title="Error during execution">
|
||||
See "Debug Runs" on the top right for more details
|
||||
<pre
|
||||
title={JSON.stringify(result.error, null, 4)}
|
||||
class=" mt-2 text-2xs whitespace-pre-wrap">{JSON.stringify(result.error, null, 4)}</pre
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
An error occured, please contact the app author.
|
||||
<span class="font-semibold">Job id: {testJob?.id}</span>
|
||||
<pre class=" whitespace-pre-wrap text-gray-900 bg-white border w-full p-4 text-xs"
|
||||
>{JSON.stringify(result.error, null, 4)}
|
||||
</pre>
|
||||
</div>
|
||||
</Alert>
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="block w-full h-full">
|
||||
<div class="block grow max-h-full">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
export let componentInput: AppInput | undefined
|
||||
export let id: string
|
||||
export let result: any = undefined
|
||||
export let noMinH = false
|
||||
|
||||
export let extraQueryParams: Record<string, any> = {}
|
||||
export let autoRefresh: boolean = true
|
||||
@@ -47,7 +46,6 @@
|
||||
{id}
|
||||
{extraQueryParams}
|
||||
{forceSchemaDisplay}
|
||||
{noMinH}
|
||||
wrapperClass={runnableClass}
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import Select from 'svelte-select'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
@@ -46,7 +46,7 @@
|
||||
e?.stopPropagation()
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
}}
|
||||
{items}
|
||||
items={Array.isArray(items) ? items : []}
|
||||
{value}
|
||||
placeholder="Select an item"
|
||||
on:click={() => {
|
||||
|
||||
@@ -19,7 +19,13 @@
|
||||
export let configuration: Record<string, AppInput>
|
||||
export let actionButtons: (BaseAppComponent & ButtonComponent)[]
|
||||
|
||||
export const staticOutputs: string[] = ['selectedRow', 'loading', 'result', 'search']
|
||||
export const staticOutputs: string[] = [
|
||||
'selectedRow',
|
||||
'loading',
|
||||
'result',
|
||||
'search',
|
||||
'selectedRowIndex'
|
||||
]
|
||||
|
||||
type T = Record<string, any>
|
||||
|
||||
@@ -52,6 +58,7 @@
|
||||
if (selectedRowIndex !== rowIndex) {
|
||||
selectedRowIndex = rowIndex
|
||||
outputs?.selectedRow.set(row.original)
|
||||
outputs?.selectedRowIndex.set(rowIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +121,7 @@
|
||||
$: search === 'By Component' && (filteredResult = searchInResult(result ?? [], searchValue))
|
||||
$: (search === 'By Runnable' || search === 'Disabled') && (filteredResult = result ?? [])
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
selectedRowIndex: Output<number>
|
||||
selectedRow: Output<any>
|
||||
search: Output<string>
|
||||
}
|
||||
@@ -203,18 +211,20 @@
|
||||
|
||||
{#if actionButtons.length > 0}
|
||||
<td
|
||||
class="flex w-full flex-row gap-2 p-4"
|
||||
class="p-2 "
|
||||
on:click={() => toggleRow(row, rowIndex)}
|
||||
>
|
||||
{#each actionButtons as actionButton, actionIndex (actionIndex)}
|
||||
<AppButton
|
||||
noWFull
|
||||
{...actionButton}
|
||||
extraQueryParams={{ row: row.original }}
|
||||
bind:componentInput={actionButton.componentInput}
|
||||
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
|
||||
/>
|
||||
{/each}
|
||||
<div class="center-center h-full w-full flex-wrap gap-1">
|
||||
{#each actionButtons as actionButton, actionIndex (actionIndex)}
|
||||
<AppButton
|
||||
noWFull
|
||||
{...actionButton}
|
||||
extraQueryParams={{ row: row.original }}
|
||||
bind:componentInput={actionButton.componentInput}
|
||||
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
})
|
||||
|
||||
const runnableComponents = writable<Record<string, () => Promise<void>>>({})
|
||||
const errorByComponent = writable<Record<string, { error: string; componentId: string }>>({})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
worldStore,
|
||||
@@ -72,7 +73,9 @@
|
||||
isEditor: true,
|
||||
jobs: writable([]),
|
||||
staticExporter: writable({}),
|
||||
noBackend: false
|
||||
noBackend: false,
|
||||
errorByComponent,
|
||||
openDebugRun: writable(undefined)
|
||||
})
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
@@ -41,7 +41,13 @@
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { appToHubUrl, classNames, copyToClipboard, sendUserToast } from '../../../utils'
|
||||
import type { AppInput } from '../inputType'
|
||||
import type {
|
||||
AppInput,
|
||||
ConnectedAppInput,
|
||||
RowAppInput,
|
||||
StaticAppInput,
|
||||
UserAppInput
|
||||
} from '../inputType'
|
||||
import type { AppComponent, AppEditorContext } from '../types'
|
||||
import { toStatic } from '../utils'
|
||||
import AppExportButton from './AppExportButton.svelte'
|
||||
@@ -57,13 +63,34 @@
|
||||
|
||||
export let policy: Policy
|
||||
|
||||
const { app, summary, mode, breakpoint, appPath, jobs, staticExporter } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
const {
|
||||
app,
|
||||
summary,
|
||||
mode,
|
||||
breakpoint,
|
||||
appPath,
|
||||
jobs,
|
||||
staticExporter,
|
||||
errorByComponent,
|
||||
openDebugRun
|
||||
} = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
const loading = {
|
||||
publish: false,
|
||||
save: false
|
||||
}
|
||||
|
||||
$: if ($openDebugRun == undefined) {
|
||||
$openDebugRun = (componentId: string) => {
|
||||
jobsDrawerOpen = true
|
||||
|
||||
const job = $jobs.find((job) => job.component === componentId)
|
||||
if (job) {
|
||||
selectedJobId = job.job
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let newPath: string = ''
|
||||
let pathError: string | undefined = undefined
|
||||
|
||||
@@ -77,6 +104,17 @@
|
||||
saveDrawerOpen = false
|
||||
}
|
||||
|
||||
function collectStaticFields(
|
||||
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(fields ?? {})
|
||||
.filter(([k, v]) => v.type == 'static')
|
||||
.map(([k, v]) => {
|
||||
return [k, v['value']]
|
||||
})
|
||||
)
|
||||
}
|
||||
async function computeTriggerables() {
|
||||
const allTriggers = await Promise.all(
|
||||
$app.grid
|
||||
@@ -90,13 +128,7 @@
|
||||
})
|
||||
.map(async (input) => {
|
||||
if (input?.type == 'runnable') {
|
||||
const staticInputs = Object.fromEntries(
|
||||
Object.entries(input.fields ?? {})
|
||||
.filter(([k, v]) => v.type == 'static')
|
||||
.map(([k, v]) => {
|
||||
return [k, v['value']]
|
||||
})
|
||||
)
|
||||
const staticInputs = collectStaticFields(input.fields)
|
||||
if (input.runnable?.type == 'runnableByName') {
|
||||
let hex = await hash(input.runnable.inlineScript?.content)
|
||||
return [`rawscript/${hex}`, staticInputs]
|
||||
@@ -108,8 +140,16 @@
|
||||
}
|
||||
return []
|
||||
})
|
||||
.concat(
|
||||
Object.values($app.hiddenInlineScripts).map(async (v) => {
|
||||
let hex = await hash(v.inlineScript?.content)
|
||||
const staticInputs = collectStaticFields(v.fields)
|
||||
return [`rawscript/${hex}`, staticInputs]
|
||||
})
|
||||
)
|
||||
)
|
||||
policy.triggerables = Object.fromEntries(allTriggers)
|
||||
policy.triggerables = Object.fromEntries(allTriggers.filter((x) => x.length > 0))
|
||||
|
||||
policy.on_behalf_of = `u/${$userStore?.username}`
|
||||
policy.on_behalf_of_email = $userStore?.email
|
||||
}
|
||||
@@ -183,6 +223,7 @@
|
||||
let testIsLoading = false
|
||||
|
||||
$: selectedJobId && testJobLoader?.watchJob(selectedJobId)
|
||||
$: hasErrors = Object.keys($errorByComponent).length > 0
|
||||
</script>
|
||||
|
||||
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job />
|
||||
@@ -218,10 +259,13 @@
|
||||
{#each $jobs ?? [] as { job, component } (job)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="{classNames(
|
||||
class={classNames(
|
||||
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
|
||||
selectedJobId == job ? 'bg-blue-100 text-blue-600' : ''
|
||||
)},"
|
||||
$errorByComponent[job] ? 'border border-red-500 bg-red-100' : '',
|
||||
selectedJobId == job && !$errorByComponent[component]
|
||||
? 'bg-blue-100 text-blue-600'
|
||||
: ''
|
||||
)}
|
||||
on:click={() => (selectedJobId = job)}
|
||||
>
|
||||
<span class="text-xs truncate">{job}</span>
|
||||
@@ -243,12 +287,14 @@
|
||||
{:else}
|
||||
<div class="flex flex-col h-full w-full gap-4 mb-4">
|
||||
{#if job?.['running']}
|
||||
<div class="flex flex-row-reverse w-full"
|
||||
><Button
|
||||
<div class="flex flex-row-reverse w-full">
|
||||
<Button
|
||||
color="red"
|
||||
variant="border"
|
||||
on:click={() => testJobLoader?.cancelJob()}>Cancel</Button
|
||||
on:click={() => testJobLoader?.cancelJob()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="p-2">
|
||||
@@ -436,7 +482,7 @@
|
||||
<span class="hidden md:inline">
|
||||
<Button
|
||||
on:click={() => (jobsDrawerOpen = true)}
|
||||
color="light"
|
||||
color={hasErrors ? 'red' : 'light'}
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faBug }}
|
||||
|
||||
@@ -59,7 +59,9 @@
|
||||
isEditor,
|
||||
jobs: writable([]),
|
||||
staticExporter: writable({}),
|
||||
noBackend
|
||||
noBackend,
|
||||
errorByComponent: writable({}),
|
||||
openDebugRun: writable(undefined)
|
||||
})
|
||||
|
||||
let mounted = false
|
||||
@@ -74,8 +76,10 @@
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<div class="{$$props.class} {lockedClasses} h-full max-h-[calc(100%-41px)] overflow-auto
|
||||
w-full {app.fullscreen ? '' : 'max-w-6xl'} mx-auto">
|
||||
<div
|
||||
class="{$$props.class} {lockedClasses} h-full max-h-[calc(100%-41px)] overflow-auto
|
||||
w-full {app.fullscreen ? '' : 'max-w-6xl'} mx-auto"
|
||||
>
|
||||
{#if $appStore.grid}
|
||||
<div class={classNames('mx-auto pb-4', width)}>
|
||||
<GridEditor {policy} />
|
||||
@@ -86,10 +90,10 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
transition:fade|local={{ duration: 200, easing: cubicOut }}
|
||||
on:click={() => isLocked = false}
|
||||
on:click={() => (isLocked = false)}
|
||||
class="absolute inset-0 center-center bg-black/20 z-50 backdrop-blur-[1px] cursor-pointer"
|
||||
>
|
||||
<Button on:click={() => isLocked = false}>
|
||||
<Button on:click={() => (isLocked = false)}>
|
||||
Unlock preview
|
||||
<Unlock size={18} class="ml-1" strokeWidth={2.5} />
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { AppComponent } from '../types'
|
||||
import { Anchor, Move } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { AppComponent, AppEditorContext } from '../types'
|
||||
import { Anchor, Bug, Move } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { Alert, Button } from '$lib/components/common'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -11,6 +13,16 @@
|
||||
export let hover: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const { errorByComponent, openDebugRun } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
$: error = Object.values($errorByComponent).find((e) => e.componentId === component.id)
|
||||
|
||||
function openDebugRuns() {
|
||||
if ($openDebugRun) {
|
||||
$openDebugRun(component.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<span
|
||||
@@ -56,3 +68,32 @@
|
||||
<Move size={14} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
{@const json = JSON.parse(JSON.stringify(error.error))}
|
||||
<span
|
||||
title="Error"
|
||||
class={classNames(
|
||||
'text-red-500 px-1 text-2xs py-0.5 font-bold w-fit absolute border border-red-500 -bottom-1 shadow left-1/2 transform -translate-x-1/2 z-50 cursor-pointer',
|
||||
'bg-red-100/80'
|
||||
)}
|
||||
>
|
||||
<Popover notClickable placement="bottom" popupClass="!bg-white border w-96">
|
||||
<Bug size={14} />
|
||||
<span slot="text">
|
||||
<div class="bg-white">
|
||||
<Alert type="error" title={`${json?.name}: ${json?.message}`}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
<pre class=" whitespace-pre-wrap text-gray-900 bg-white border w-full p-4 text-xs"
|
||||
>{json?.stack ?? ''}
|
||||
</pre>
|
||||
</div>
|
||||
<Button color="red" variant="border" on:click={openDebugRuns}>Open Debug Runs</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</span>
|
||||
</Popover>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -172,6 +172,8 @@ export type AppEditorContext = {
|
||||
isEditor: boolean
|
||||
jobs: Writable<{ job: string; component: string }[]>
|
||||
noBackend: boolean
|
||||
errorByComponent: Writable<Record<string, { error: string; componentId: string }>>
|
||||
openDebugRun: Writable<((componentID: string) => void) | undefined>
|
||||
}
|
||||
|
||||
export type EditorMode = 'dnd' | 'preview'
|
||||
|
||||
@@ -39,12 +39,14 @@
|
||||
editor?.insertAtCursor(detail)
|
||||
}}
|
||||
>
|
||||
<SimpleEditor
|
||||
bind:this={editor}
|
||||
lang="javascript"
|
||||
bind:code={branch.expr}
|
||||
class="small-editor"
|
||||
shouldBindKey={false}
|
||||
extraLib={stepPropPicker.extraLib}
|
||||
/>
|
||||
<div class="border border-gray-400">
|
||||
<SimpleEditor
|
||||
bind:this={editor}
|
||||
lang="javascript"
|
||||
bind:code={branch.expr}
|
||||
class="small-editor border "
|
||||
shouldBindKey={false}
|
||||
extraLib={stepPropPicker.extraLib}
|
||||
/>
|
||||
</div>
|
||||
</PropPickerWrapper>
|
||||
|
||||
@@ -27,133 +27,143 @@
|
||||
</script>
|
||||
|
||||
<div class="p-4 h-full flex flex-col">
|
||||
{#if !failureModule}
|
||||
<div class="center-center">
|
||||
<div class="max-w-min">
|
||||
<ToggleButtonGroup bind:selected={kind}>
|
||||
<ToggleButton position="left" value="script" size="sm" startIcon={{ icon: faCode }}>
|
||||
Action <Tooltip>
|
||||
An action script is simply a script that is neither a trigger nor an approval script.
|
||||
Those are the majority of the scripts.
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
{#if !shouldDisableTriggerScripts}
|
||||
<ToggleButton position="center" value="trigger" size="sm" startIcon={{ icon: faBolt }}>
|
||||
Trigger <Tooltip>
|
||||
Used as a first step most commonly with a state and a schedule to watch for changes
|
||||
on an external system, compute the diff since last time, set the new state. The
|
||||
diffs are then treated one by one with a for-loop.
|
||||
{#if summary == 'Terminate flow'}
|
||||
<Alert role="info" title="The flow stops here"
|
||||
>This is an identity step with an early stop that has 'true' for expression</Alert
|
||||
>
|
||||
{:else}{#if !failureModule}
|
||||
<div class="center-center">
|
||||
<div class="max-w-min">
|
||||
<ToggleButtonGroup bind:selected={kind}>
|
||||
<ToggleButton position="left" value="script" size="sm" startIcon={{ icon: faCode }}>
|
||||
Action <Tooltip>
|
||||
An action script is simply a script that is neither a trigger nor an approval
|
||||
script. Those are the majority of the scripts.
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
{/if}
|
||||
<ToggleButton position="right" value="approval" size="sm" startIcon={{ icon: faCheck }}>
|
||||
Approval <Tooltip>
|
||||
An approval step will suspend the execution of a flow until it has been approved
|
||||
through the resume endpoints or the approval page by and solely by the recipients of
|
||||
those secret urls. Use `wmill.getResumeUrls()` in Typescript or
|
||||
`wmill.get_resume_urls()` in Python from the wmill client to generate those URLs.
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
{#if !shouldDisableTriggerScripts}
|
||||
<ToggleButton
|
||||
position="center"
|
||||
value="trigger"
|
||||
size="sm"
|
||||
startIcon={{ icon: faBolt }}
|
||||
>
|
||||
Trigger <Tooltip>
|
||||
Used as a first step most commonly with a state and a schedule to watch for
|
||||
changes on an external system, compute the diff since last time, set the new
|
||||
state. The diffs are then treated one by one with a for-loop.
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
{/if}
|
||||
<ToggleButton position="right" value="approval" size="sm" startIcon={{ icon: faCheck }}>
|
||||
Approval <Tooltip>
|
||||
An approval step will suspend the execution of a flow until it has been approved
|
||||
through the resume endpoints or the approval page by and solely by the recipients of
|
||||
those secret urls. Use `wmill.getResumeUrls()` in Typescript or
|
||||
`wmill.get_resume_urls()` in Python from the wmill client to generate those URLs.
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if kind == 'trigger'}
|
||||
<div class="mt-2" />
|
||||
<Alert title="Trigger script automatic schedule" role="info">
|
||||
A schedule will be automatically attached to this flow to run every 15 minutes. Adjust
|
||||
frequency in 'Settings -> Schedule'</Alert
|
||||
>
|
||||
{/if}
|
||||
<h3 class="pb-2 pt-4">
|
||||
Inline new <span class="text-blue-500">{kind == 'script' ? 'action' : kind}</span> script
|
||||
<Tooltip>
|
||||
Embed a script directly inside a flow instead of saving the script into your workspace for
|
||||
reuse. You can always save an inline script to your workspace later.
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
<FlowScriptPicker
|
||||
label="Typescript"
|
||||
lang={Script.language.DENO}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: RawScript.language.DENO,
|
||||
kind,
|
||||
subkind: 'flow'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<FlowScriptPicker
|
||||
label="Python"
|
||||
lang={Script.language.PYTHON3}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: RawScript.language.PYTHON3,
|
||||
kind,
|
||||
subkind: 'flow'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if kind != 'approval'}
|
||||
{/if}
|
||||
{#if kind == 'trigger'}
|
||||
<div class="mt-2" />
|
||||
<Alert title="Trigger script automatic schedule" role="info">
|
||||
A schedule will be automatically attached to this flow to run every 15 minutes. Adjust
|
||||
frequency in 'Settings -> Schedule'</Alert
|
||||
>
|
||||
{/if}
|
||||
<h3 class="pb-2 pt-4">
|
||||
Inline new <span class="text-blue-500">{kind == 'script' ? 'action' : kind}</span> script
|
||||
<Tooltip>
|
||||
Embed a script directly inside a flow instead of saving the script into your workspace for
|
||||
reuse. You can always save an inline script to your workspace later.
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
<FlowScriptPicker
|
||||
label="Go"
|
||||
lang={Script.language.GO}
|
||||
label="Typescript"
|
||||
lang={Script.language.DENO}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: RawScript.language.GO,
|
||||
kind,
|
||||
subkind: 'flow'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if kind == 'script'}
|
||||
<FlowScriptPicker
|
||||
label="Bash"
|
||||
lang={Script.language.BASH}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: RawScript.language.BASH,
|
||||
language: RawScript.language.DENO,
|
||||
kind,
|
||||
subkind: 'flow'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if !failureModule}
|
||||
<FlowScriptPicker
|
||||
label={`PostgreSQL`}
|
||||
lang="pgsql"
|
||||
on:click={() =>
|
||||
dispatch('new', { language: RawScript.language.DENO, kind, subkind: 'pgsql' })}
|
||||
/>
|
||||
<FlowScriptPicker
|
||||
label="Python"
|
||||
lang={Script.language.PYTHON3}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: RawScript.language.PYTHON3,
|
||||
kind,
|
||||
subkind: 'flow'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if kind != 'approval'}
|
||||
<FlowScriptPicker
|
||||
label={`MySQL`}
|
||||
lang="mysql"
|
||||
on:click={() =>
|
||||
dispatch('new', { language: RawScript.language.DENO, kind, subkind: 'mysql' })}
|
||||
label="Go"
|
||||
lang={Script.language.GO}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: RawScript.language.GO,
|
||||
kind,
|
||||
subkind: 'flow'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mb-2 mt-6"
|
||||
>Use pre-made <span class="text-blue-500">{kind == 'script' ? 'action' : kind}</span> script</h3
|
||||
>
|
||||
{#if pick_existing == 'hub'}
|
||||
<PickHubScript bind:filter {kind} on:pick>
|
||||
<ToggleHubWorkspace bind:selected={pick_existing} />
|
||||
</PickHubScript>
|
||||
{:else}
|
||||
<WorkspaceScriptPicker displayLock bind:filter {kind} on:pick>
|
||||
<ToggleHubWorkspace bind:selected={pick_existing} />
|
||||
</WorkspaceScriptPicker>
|
||||
{#if kind == 'script'}
|
||||
<FlowScriptPicker
|
||||
label="Bash"
|
||||
lang={Script.language.BASH}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: RawScript.language.BASH,
|
||||
kind,
|
||||
subkind: 'flow'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if !failureModule}
|
||||
<FlowScriptPicker
|
||||
label={`PostgreSQL`}
|
||||
lang="pgsql"
|
||||
on:click={() =>
|
||||
dispatch('new', { language: RawScript.language.DENO, kind, subkind: 'pgsql' })}
|
||||
/>
|
||||
|
||||
<FlowScriptPicker
|
||||
label={`MySQL`}
|
||||
lang="mysql"
|
||||
on:click={() =>
|
||||
dispatch('new', { language: RawScript.language.DENO, kind, subkind: 'mysql' })}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mb-2 mt-6"
|
||||
>Use pre-made <span class="text-blue-500">{kind == 'script' ? 'action' : kind}</span> script</h3
|
||||
>
|
||||
{#if pick_existing == 'hub'}
|
||||
<PickHubScript bind:filter {kind} on:pick>
|
||||
<ToggleHubWorkspace bind:selected={pick_existing} />
|
||||
</PickHubScript>
|
||||
{:else}
|
||||
<WorkspaceScriptPicker displayLock bind:filter {kind} on:pick>
|
||||
<ToggleHubWorkspace bind:selected={pick_existing} />
|
||||
</WorkspaceScriptPicker>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -14,12 +14,20 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import autosize from 'svelte-autosize'
|
||||
import Slider from '$lib/components/Slider.svelte'
|
||||
import { page } from '$app/stores'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
export let initialPath: string
|
||||
|
||||
let topHeight = 0
|
||||
|
||||
$: url = `${$page.url.hostname}/api/w/${$workspaceStore}/jobs/run/f/${$flowStore?.path}`
|
||||
</script>
|
||||
|
||||
<div class="h-full overflow-hidden">
|
||||
@@ -32,34 +40,78 @@
|
||||
<Tab value="settings-graph">Graph</Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="settings-metadata" class="p-4">
|
||||
<Path bind:path={$flowStore.path} {initialPath} namePlaceholder="my_flow" kind="flow" />
|
||||
|
||||
<label class="block my-4">
|
||||
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$flowStore.summary}
|
||||
placeholder="A short summary of the flow displayed when the flow is listed"
|
||||
id="flow-summary"
|
||||
<TabContent value="settings-metadata" class="p-4 h-full">
|
||||
<div class="overflow-auto h-full">
|
||||
<Path
|
||||
bind:path={$flowStore.path}
|
||||
{initialPath}
|
||||
namePlaceholder="my_flow"
|
||||
kind="flow"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="block my-4" for="inp">
|
||||
<span class="text-gray-700 text-sm">
|
||||
Description
|
||||
<Required required={false} />
|
||||
<textarea
|
||||
use:autosize
|
||||
<label class="block my-4">
|
||||
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
|
||||
<input
|
||||
type="text"
|
||||
class="text-sm"
|
||||
id="inp"
|
||||
bind:value={$flowStore.description}
|
||||
placeholder="A description to help users understand what this flow does and how to use it."
|
||||
rows="3"
|
||||
bind:value={$flowStore.summary}
|
||||
placeholder="A short summary of the flow displayed when the flow is listed"
|
||||
id="flow-summary"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</label>
|
||||
|
||||
<label class="block my-4" for="inp">
|
||||
<span class="text-gray-700 text-sm">
|
||||
Description
|
||||
<Required required={false} />
|
||||
<textarea
|
||||
use:autosize
|
||||
type="text"
|
||||
class="text-sm"
|
||||
id="inp"
|
||||
bind:value={$flowStore.description}
|
||||
placeholder="A description to help users understand what this flow does and how to use it."
|
||||
rows="3"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<Slider text="How to trigger from external events?">
|
||||
<div class="text-sm text-gray-600 border p-4">
|
||||
There are 2 ways to trigger a flow based on external events:
|
||||
<ul class="pt-4">
|
||||
<li
|
||||
>1. Send a webhook after each event: <a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(url)
|
||||
}}
|
||||
href={$page.url.protocol + '//' + url}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{url}
|
||||
<span class="text-gray-700 ml-2">
|
||||
<Icon data={faClipboard} />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
2. Use a trigger script and schedule this flow to run as frequently as
|
||||
needed and compare a state persisted in Windmill to the state of the
|
||||
external system. If a difference is detected, then the rest of the flow is
|
||||
triggered. Oftentimes, the second step of a flow is a for-loop that will
|
||||
iterate over every elements. When using a trigger, a default schedule will
|
||||
be created.
|
||||
<img
|
||||
class="shadow-lg border rounded"
|
||||
alt="static button"
|
||||
src="/trigger_button.png"
|
||||
/>
|
||||
</div>
|
||||
</li></ul
|
||||
>
|
||||
</div>
|
||||
</Slider>
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="settings-schedule" class="p-4">
|
||||
<Alert type="info" title="Primary Schedule">
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FlowModuleSchemaMap bind:modules={branch.modules} />
|
||||
<FlowModuleSchemaMap parentType="branchall" bind:modules={branch.modules} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<FlowModuleSchemaMap bind:modules={module.value.default} />
|
||||
<FlowModuleSchemaMap parentType="branchone" bind:modules={module.value.default} />
|
||||
</div>
|
||||
|
||||
{#each module.value.branches ?? [] as branch, branchIndex (branchIndex)}
|
||||
@@ -108,7 +108,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FlowModuleSchemaMap bind:modules={branch.modules} />
|
||||
<FlowModuleSchemaMap parentType="branchone" bind:modules={branch.modules} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -23,13 +23,14 @@
|
||||
|
||||
export let root: boolean = false
|
||||
export let modules: FlowModule[] | undefined
|
||||
export let parentType: 'flow' | 'forloop' | 'branchall' | 'branchone' | undefined = undefined
|
||||
|
||||
let indexToRemove: number | undefined = undefined
|
||||
const { select, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
async function insertNewModuleAtIndex(
|
||||
index: number,
|
||||
kind: 'script' | 'forloop' | 'branchone' | 'branchall' | 'flow' | 'trigger' | 'approval'
|
||||
kind: 'script' | 'forloop' | 'branchone' | 'branchall' | 'flow' | 'trigger' | 'approval' | 'end'
|
||||
): Promise<void> {
|
||||
await idMutex.runExclusive(async () => {
|
||||
var module = emptyModule(kind == 'flow')
|
||||
@@ -50,6 +51,9 @@
|
||||
flowModule.summary = 'Trigger'
|
||||
} else if (kind == 'approval') {
|
||||
flowModule.summary = 'Approval'
|
||||
} else if (kind == 'end') {
|
||||
flowModule.summary = 'Terminate flow'
|
||||
flowModule.stop_after_if = { skip_if_stopped: false, expr: 'true' }
|
||||
}
|
||||
select(flowModule.id)
|
||||
})
|
||||
@@ -136,10 +140,15 @@
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<InsertModuleButton
|
||||
trigger={modules?.length == 0}
|
||||
on:new={(e) => insertNewModuleAtIndex(modules?.length ?? 0, e.detail)}
|
||||
/>
|
||||
{#if !modules || modules[modules?.length - 1]?.summary != 'Terminate flow'}
|
||||
<InsertModuleButton
|
||||
stop={!root && parentType == 'branchone'}
|
||||
trigger={root && modules?.length == 0}
|
||||
on:new={(e) => insertNewModuleAtIndex(modules?.length ?? 0, e.detail)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="my-2" />
|
||||
{/if}
|
||||
</ul>
|
||||
{#if root}
|
||||
<div class="sticky bottom-0 bg-gray-50 flex-none px-4 py-1 pb-2 border-t">
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Repeat } from 'lucide-svelte'
|
||||
import { Repeat, Square } from 'lucide-svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
export let trigger = false
|
||||
export let stop = false
|
||||
</script>
|
||||
|
||||
<Menu noMinW placement="bottom-start" let:close>
|
||||
@@ -113,5 +114,18 @@
|
||||
<Icon data={faBarsStaggered} scale={0.8} class="mr-2" />
|
||||
Flow
|
||||
</button>
|
||||
{#if stop}
|
||||
<button
|
||||
class="w-full text-left p-2 hover:bg-gray-100 inline-flex gap-2.5"
|
||||
on:click={() => {
|
||||
close()
|
||||
dispatch('new', 'end')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Square size={14} />
|
||||
End Flow
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</Menu>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
|
||||
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
|
||||
import { Building, Repeat } from 'lucide-svelte'
|
||||
import { Building, Repeat, Square } from 'lucide-svelte'
|
||||
|
||||
export let mod: FlowModule
|
||||
export let index: number
|
||||
@@ -107,6 +107,8 @@
|
||||
<div slot="icon">
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
<LanguageIcon lang={mod.value.language} width={16} height={16} />
|
||||
{:else if mod.summary == 'Terminate flow'}
|
||||
<Square size={16} />
|
||||
{:else if mod.value.type === 'identity'}
|
||||
<Icon data={faLongArrowDown} scale={1.1} />
|
||||
{:else if mod.value.type === 'script'}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
faBarsStaggered
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { CalendarClock } from 'lucide-svelte'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { check } from 'svelte-awesome/icons'
|
||||
import { Badge } from '../common'
|
||||
@@ -32,6 +33,24 @@
|
||||
|
||||
export let job: Job
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
let time = Date.now()
|
||||
let interval
|
||||
onMount(() => {
|
||||
interval = setInterval(() => {
|
||||
time = Date.now()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
interval && clearInterval(interval)
|
||||
})
|
||||
|
||||
function endedDate(started_at: string, duration_ms: number): string {
|
||||
const started = new Date(started_at)
|
||||
started.setMilliseconds(started.getMilliseconds() + duration_ms)
|
||||
return displayDaysAgo(started.toString())
|
||||
}
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
@@ -152,9 +171,17 @@
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-1.5"
|
||||
>
|
||||
Started {displayDaysAgo(job.started_at ?? '')}</span
|
||||
>
|
||||
</div>
|
||||
<span>
|
||||
{#if job?.['duration_ms']}
|
||||
Ended {#key time}
|
||||
{endedDate(job.started_at, job?.['duration_ms'])}{/key}
|
||||
{:else}
|
||||
Started {#key time}
|
||||
{displayDaysAgo(job.started_at ?? '')}{/key}
|
||||
{/if}</span
|
||||
>
|
||||
</span></div
|
||||
>
|
||||
{/if}
|
||||
{#if 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="inline-flex gap-1">
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { Badge } from '../common'
|
||||
import { NEVER_TESTED_THIS_FAR } from '../flows/utils'
|
||||
import { getTypeAsString } from '../flows/utils'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { computeKey } from './utils'
|
||||
import WarningMessage from './WarningMessage.svelte'
|
||||
|
||||
@@ -18,6 +17,7 @@
|
||||
export let rawKey = false
|
||||
export let topBrackets = false
|
||||
export let topLevelNode = false
|
||||
export let allowCopy = true
|
||||
|
||||
const collapsedSymbol = '...'
|
||||
let keys: string | any[]
|
||||
@@ -39,7 +39,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function selectProp(key: string, value: any) {
|
||||
if (pureViewer) {
|
||||
if (pureViewer && allowCopy) {
|
||||
copyToClipboard(value)
|
||||
}
|
||||
dispatch('select', rawKey ? key : computeKey(key, isArray, currentPath))
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
const EMPTY_STRING = ''
|
||||
let search = ''
|
||||
|
||||
const { propPickerConfig, clearFocus } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
|
||||
const { propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
|
||||
|
||||
$: flowInputsFiltered =
|
||||
search === EMPTY_STRING
|
||||
@@ -89,6 +89,7 @@
|
||||
</div>
|
||||
<div class="overflow-y-auto mb-2">
|
||||
<ObjectViewer
|
||||
allowCopy={false}
|
||||
pureViewer={!$propPickerConfig}
|
||||
json={flowInputsFiltered}
|
||||
on:select={(e) => {
|
||||
@@ -100,6 +101,7 @@
|
||||
<span class="font-bold text-sm">Error</span>
|
||||
<div class="overflow-y-auto mb-2">
|
||||
<ObjectViewer
|
||||
allowCopy={false}
|
||||
pureViewer={!$propPickerConfig}
|
||||
json={{
|
||||
error: {
|
||||
@@ -116,6 +118,7 @@
|
||||
<span class="font-bold text-sm">Previous Result</span>
|
||||
<div class="overflow-y-auto mb-2">
|
||||
<ObjectViewer
|
||||
allowCopy={false}
|
||||
topLevelNode
|
||||
pureViewer={!$propPickerConfig}
|
||||
json={Object.fromEntries(
|
||||
@@ -131,6 +134,7 @@
|
||||
<span class="font-bold text-sm">Resume payloads</span>
|
||||
<div class="overflow-y-auto mb-2">
|
||||
<ObjectViewer
|
||||
allowCopy={false}
|
||||
topLevelNode
|
||||
pureViewer={!$propPickerConfig}
|
||||
json={{
|
||||
@@ -148,6 +152,7 @@
|
||||
<span class="font-bold text-sm">All Results</span>
|
||||
<div class="overflow-y-auto mb-2">
|
||||
<ObjectViewer
|
||||
allowCopy={false}
|
||||
topLevelNode
|
||||
pureViewer={!$propPickerConfig}
|
||||
collapsed={true}
|
||||
@@ -173,6 +178,7 @@
|
||||
}}>-</Button
|
||||
>
|
||||
<ObjectViewer
|
||||
allowCopy={false}
|
||||
pureViewer={!$propPickerConfig}
|
||||
rawKey={true}
|
||||
json={variables}
|
||||
@@ -200,6 +206,7 @@
|
||||
}}>-</Button
|
||||
>
|
||||
<ObjectViewer
|
||||
allowCopy={false}
|
||||
pureViewer={!$propPickerConfig}
|
||||
rawKey={true}
|
||||
json={resources}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
<div class="w-full px-2">
|
||||
<span class="font-bold text-sm">Result</span>
|
||||
<div class="overflow-y-auto mb-2 w-full">
|
||||
<ObjectViewer json={{ result }} on:select />
|
||||
<ObjectViewer allowCopy={false} json={{ result }} on:select />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
{#if isCloudHosted()}
|
||||
{#if isCloudHosted() && $premiumStore}
|
||||
{#if !$premiumStore.premium}
|
||||
<div class="py-1" role="none">
|
||||
<span class="text-gray-700 block w-full text-left px-4 py-2 text-sm"
|
||||
|
||||
@@ -72,9 +72,9 @@ if (browser) {
|
||||
} catch (e) {
|
||||
console.error('Could not persist workspace to local storage', e)
|
||||
}
|
||||
|
||||
userStore.set(await getUserExt(workspace));
|
||||
if (isCloudHosted()) {
|
||||
const user = await getUserExt(workspace)
|
||||
userStore.set(user);
|
||||
if (isCloudHosted() && user?.is_admin) {
|
||||
premiumStore.set((await WorkspaceService.getPremiumInfo({ workspace })));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
defaultIfEmptyString,
|
||||
flowToHubUrl,
|
||||
copyToClipboard,
|
||||
emptyString
|
||||
emptyString,
|
||||
encodeState
|
||||
} from '$lib/utils'
|
||||
import {
|
||||
faPlay,
|
||||
@@ -173,7 +174,7 @@
|
||||
</Button>
|
||||
{#if !$userStore?.operator}
|
||||
<Button
|
||||
href="/flows/edit/{path}?nodraft=true"
|
||||
href="/flows/edit/{path}?nodraft=true&args={encodeState(args)}"
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="md"
|
||||
|
||||
@@ -168,7 +168,11 @@
|
||||
{@const editHref = `${stem}/edit/${route}${
|
||||
isScript
|
||||
? `?step=2${job?.args ? `&args=${encodeURIComponent(encodeState(job?.args))}` : ''}`
|
||||
: `${job?.args ? `?args=${encodeURIComponent(encodeState(job?.args))}` : ''}`
|
||||
: `${
|
||||
job?.args
|
||||
? `?args=${encodeURIComponent(encodeState(job?.args))}&nodraft=true`
|
||||
: '?nodraft=true'
|
||||
}`
|
||||
}`}
|
||||
{@const isRunning = job && 'running' in job && job.running}
|
||||
{@const viewHref = `${stem}/get/${isScript ? job?.script_hash : job?.script_path}`}
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<td>
|
||||
<div class="inline-flex gap-2">
|
||||
<Button
|
||||
href={`/runs/${path}`}
|
||||
href={`/runs/${script_path}`}
|
||||
size="xs"
|
||||
startIcon={{ icon: faList }}
|
||||
color="light"
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
defaultIfEmptyString,
|
||||
scriptToHubUrl,
|
||||
copyToClipboard,
|
||||
emptyString
|
||||
emptyString,
|
||||
encodeState
|
||||
} from '$lib/utils'
|
||||
import {
|
||||
faPlay,
|
||||
@@ -207,7 +208,7 @@
|
||||
</Button>
|
||||
{#if !$userStore?.operator}
|
||||
<Button
|
||||
href={`/scripts/edit/${script.hash}?step=2`}
|
||||
href={`/scripts/edit/${script.hash}?step=2&args=${encodeState(args)}`}
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faEdit }}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte'
|
||||
import AddUser from '$lib/components/AddUser.svelte'
|
||||
import { page } from '$app/stores'
|
||||
import Slider from '$lib/components/Slider.svelte'
|
||||
|
||||
let users: User[] | undefined = undefined
|
||||
let invites: WorkspaceInvite[] = []
|
||||
@@ -40,9 +41,14 @@
|
||||
let nbDisplayed = 30
|
||||
let plan: string | undefined = undefined
|
||||
let customer_id: string | undefined = undefined
|
||||
let tab: 'users' | 'slack' | 'premium' | 'export_delete' =
|
||||
($page.url.searchParams.get('tab') as 'users' | 'slack' | 'premium' | 'export_delete') ??
|
||||
'users'
|
||||
let webhook: string | undefined = undefined
|
||||
let tab =
|
||||
($page.url.searchParams.get('tab') as
|
||||
| 'users'
|
||||
| 'slack'
|
||||
| 'premium'
|
||||
| 'export_delete'
|
||||
| 'webhook') ?? 'users'
|
||||
|
||||
// function getDropDownItems(username: string): DropdownItem[] {
|
||||
// return [
|
||||
@@ -77,6 +83,14 @@
|
||||
sendUserToast(`slack command script set to ${scriptPath}`)
|
||||
}
|
||||
|
||||
async function editWebhook(): Promise<void> {
|
||||
await WorkspaceService.editWebhook({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { webhook }
|
||||
})
|
||||
sendUserToast(`webhook set to ${webhook}`)
|
||||
}
|
||||
|
||||
async function loadSettings(): Promise<void> {
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
team_name = settings.slack_name
|
||||
@@ -86,6 +100,7 @@
|
||||
plan = settings.plan
|
||||
customer_id = settings.customer_id
|
||||
initialPath = scriptPath
|
||||
webhook = settings.webhook
|
||||
}
|
||||
|
||||
async function listUsers(): Promise<void> {
|
||||
@@ -142,23 +157,26 @@
|
||||
'<b>1 000</b> free global executions per-user per month'
|
||||
],
|
||||
Team: [
|
||||
`<b>$10/month</b> per user in the workspace.`,
|
||||
`Executions are not accounted for in the global user's
|
||||
quotas but are accounted for in the workspace's quota.`,
|
||||
`Every user in the workspace increases the pooled workspace quota by <b>10k</b> executions.`,
|
||||
`<div class="text-lg mt-4"><b>10k executions/user</b></div>`,
|
||||
`$0.001 per additional execution (1$ per 1000 executions)`
|
||||
`<b>$10/mo</b>, includes 1 user/author + 10k computations`,
|
||||
`<b>+ $8/mo</b> per extra user/author in the workspace (1 included)`,
|
||||
`<b>+ $4/mo</b> per extra operators in the workspace.`,
|
||||
`<b>$0.001</b> per additional computation (10k included)`,
|
||||
`Google/Github/Microsoft/Gitlab <b>SSO</b>`,
|
||||
`<b>Unlimited</b> variables/resources/scripts/apps/flows`,
|
||||
`<b>Support 24/7 with 48h response time</b>`
|
||||
],
|
||||
Enterprise: [
|
||||
`<b>$50/month</b> per user in the workspace.`,
|
||||
`Executions are not accounted for in the global user's
|
||||
quotas but are accounted for in the workspace's quota.`,
|
||||
`Every user in the workspace increases the pooled workspace quota by <b>50k</b> executions.`,
|
||||
`<b>Dedicated workers and database</b>`,
|
||||
`<b>SAML support</b>`,
|
||||
`<b>Priority support including an automation engineer</b>`,
|
||||
`<div class="text-lg mt-4"><b>50k executions/user</b><div>`,
|
||||
`$0.001 per additional execution (1$ per 1000 executions)`
|
||||
`<b>$200/mo</b>, includes 1 user/author + 10k computations`,
|
||||
`<b>+ $32/mo</b> per extra user/author in the workspace (1 included)`,
|
||||
`<b>+ $16/mo</b> per extra operators in the workspace.`,
|
||||
`<b>$0.004</b> per additional computation (10k included)`,
|
||||
`<b>Dedicated</b> and isolated database and workers available for <b>+400$/mo</b> (EU/US)`,
|
||||
`<b>Dedicated</b> entire cluster available for <b>+4000$/mo</b> (EU/US)`,
|
||||
`<b>SAML</b> support`,
|
||||
`<b>SLA</b>`,
|
||||
`<b>Priority Support 24/7 with 3h response time and automation engineer assistance</b>`,
|
||||
`<b>Design partners for Roadmap</b>`,
|
||||
`<div class="mt-4">(Self-hosted enterprise licenses also available at 50% discount)</div>`
|
||||
]
|
||||
}
|
||||
</script>
|
||||
@@ -189,6 +207,9 @@
|
||||
<Tab size="md" value="export_delete">
|
||||
<div class="flex gap-2 items-center my-1"> Export & Delete Workspace </div>
|
||||
</Tab>
|
||||
<Tab size="md" value="webhook">
|
||||
<div class="flex gap-2 items-center my-1">Webhook for CLI Sync</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{#if tab == 'users'}
|
||||
<PageHeader title="Members ({users?.length ?? ''})" primary={false} />
|
||||
@@ -471,12 +492,21 @@
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<Alert type="info" title="What is an execution">
|
||||
The single credit-unit is called an "execution". An execution corresponds to a single
|
||||
job whose duration is less than 1s. For any additional seconds of execution, an
|
||||
additional execution is accounted for. Jobs are executed on powerful cpus. Most jobs
|
||||
will take less than 200ms to execute.
|
||||
</Alert>
|
||||
<Slider text="What is a computation ?">
|
||||
<Alert type="info" title="A computation is 1s of execution">
|
||||
The single credit-unit is called a "computation". An computation corresponds to a
|
||||
single job whose duration is less than 1s. For any additional seconds of computation,
|
||||
an additional computation is accounted for. Jobs are executed on one powerful virtual
|
||||
CPU with 2Gb of memory. Most jobs will take less than 200ms to execute.
|
||||
</Alert>
|
||||
</Slider>
|
||||
|
||||
<Slider text="Operator vs Author">
|
||||
<Alert type="info" title="Operator vs Author"
|
||||
>An author can write scripts/flows/apps/variables/resources. An operator can only
|
||||
run/view them.</Alert
|
||||
>
|
||||
</Slider>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -639,6 +669,29 @@
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if tab == 'webhook'}
|
||||
<PageHeader title="Webhook for CLI Sync" primary={false} />
|
||||
|
||||
<div class="mt-2"
|
||||
><Alert type="info" title="Send events to an external service"
|
||||
>Connect your windmill workspace to an external service to sync or get notified about any
|
||||
changes</Alert
|
||||
></div
|
||||
>
|
||||
|
||||
<h3 class="mt-5 text-gray-700"
|
||||
>URL to send requests to<Tooltip>
|
||||
This URL will be POSTed to with a JSON body depending on the type of event. The type is
|
||||
indicated by the <pre>type</pre> field. The other fields are dependent on the type.
|
||||
</Tooltip>
|
||||
</h3>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input class="justify-start" type="text" bind:value={webhook} />
|
||||
<Button color="blue" btnClasses="justify-end" size="md" on:click={editWebhook}
|
||||
>Set Webhook</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4" role="alert">
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
'Stopping the server timed out',
|
||||
'Canceled',
|
||||
'Missing service editorService',
|
||||
'Unexpected usage'
|
||||
'Unexpected usage',
|
||||
'NetworkError when attempting to fetch resource.'
|
||||
]
|
||||
|
||||
async function loadUser() {
|
||||
|
||||
BIN
frontend/static/trigger_button.png
Normal file
BIN
frontend/static/trigger_button.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/static/trigger_button.png~
Normal file
BIN
frontend/static/trigger_button.png~
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,4 +1,4 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import { readFileSync } from 'fs'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -8,27 +8,24 @@ const version = JSON.parse(json)
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
preview: {
|
||||
port: 3000,
|
||||
},
|
||||
plugins: [sveltekit()],
|
||||
define: {
|
||||
__pkg__: version
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'highlight.js',
|
||||
'highlight.js/lib/core',
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
path: "path-browserify"
|
||||
},
|
||||
},
|
||||
};
|
||||
server: {
|
||||
port: 3000
|
||||
},
|
||||
preview: {
|
||||
port: 3000
|
||||
},
|
||||
plugins: [sveltekit()],
|
||||
define: {
|
||||
__pkg__: version
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['highlight.js', 'highlight.js/lib/core']
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
path: 'path-browserify'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
||||
Reference in New Issue
Block a user