Compare commits

..

40 Commits

Author SHA1 Message Date
Ruben Fiszel
ece4573076 all 2023-02-01 19:46:48 +01:00
Kai Jellinghaus
833d0df965 Add webhook request histogram 2023-01-26 07:03:49 +01:00
Kai Jellinghaus
1e7b7cc8d6 Update SQLX 2023-01-26 06:59:29 +01:00
Kai Jellinghaus
1de8eefe96 Add workspace webhook 2023-01-26 06:42:10 +01:00
Ruben Fiszel
51a710ffa0 do not copy necessarily for the flow object viewer 2023-01-26 03:20:01 +01:00
Ruben Fiszel
ad0c836129 add instructions for triggers on flows 2023-01-26 01:06:53 +01:00
Ruben Fiszel
be6707b461 for python change default editor tab to 4 2023-01-25 20:40:40 +01:00
Ruben Fiszel
c227ba2794 loader for flowjobresult 2023-01-25 20:33:33 +01:00
Ruben Fiszel
d60a770eb7 flow preview status viewer display more info 2023-01-25 14:14:14 +01:00
Ruben Fiszel
30d83a942c restrict number of jobs in the queue for free users 2023-01-25 13:34:08 +01:00
Ruben Fiszel
98bf9355dc implement MAX_FREE_CONCURRENT_RUNS 2023-01-25 12:58:29 +01:00
Ruben Fiszel
a3928101af worker client should use internal url 2023-01-25 00:10:37 +01:00
Ruben Fiszel
6e77757939 nodraft for editing flow from run 2023-01-24 22:57:34 +01:00
Ruben Fiszel
66c5d6e400 fix arg inputs 2023-01-24 22:44:52 +01:00
Ruben Fiszel
b0c184767c update pricing page 2023-01-24 21:58:44 +01:00
Ruben Fiszel
1165cf6e24 UX improvements for flows 2023-01-24 21:53:41 +01:00
Ruben Fiszel
a9640107f3 background scripts now part of the policy 2023-01-24 15:09:27 +01:00
Faton Ramadani
73bc13bb7d feat(frontend): Open debug runs from component (#1155)
* feat(frontend): Add debug runs

* feat(frontend): Fix build

* feat(frontend): revert implementation

* feat(frontend): Fix error displa

* feat(frontend): Fix error display

* feat(frontend): Fix preview mode

* feat(frontend): Fix error display
2023-01-24 14:34:19 +01:00
Ádám Kovács
e98966283d fix(frontend): App table action button cell (#1149)
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-01-24 12:20:27 +01:00
Ruben Fiszel
8ad2344ec6 improve cancel approval error message 2023-01-24 12:11:39 +01:00
Ruben Fiszel
b77c239f30 fix: improvements for error handling as first step of flow 2023-01-24 11:16:44 +01:00
Ruben Fiszel
15cb83936b error handler improvements 2023-01-24 07:30:07 +01:00
Ruben Fiszel
e2157ee1c0 fix listing runs from schedule 2023-01-24 06:55:11 +01:00
dependabot[bot]
bdfd50b970 chore(deps): bump @leeoniya/ufuzzy from 0.9.0 to 0.9.1 in /frontend (#1153)
Bumps [@leeoniya/ufuzzy](https://github.com/leeoniya/uFuzzy) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/leeoniya/uFuzzy/releases)
- [Commits](https://github.com/leeoniya/uFuzzy/compare/0.9.0...0.9.1)

---
updated-dependencies:
- dependency-name: "@leeoniya/ufuzzy"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:04:34 +01:00
dependabot[bot]
4debfd68dc chore(deps-dev): bump @typescript-eslint/eslint-plugin in /frontend (#1151)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.46.1 to 5.49.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.49.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:03:59 +01:00
dependabot[bot]
410032060a chore(deps): bump chartjs-adapter-date-fns in /frontend (#1154)
Bumps [chartjs-adapter-date-fns](https://github.com/chartjs/chartjs-adapter-date-fns) from 2.0.0 to 3.0.0.
- [Release notes](https://github.com/chartjs/chartjs-adapter-date-fns/releases)
- [Commits](https://github.com/chartjs/chartjs-adapter-date-fns/compare/v2.0.0...v3.0.0)

---
updated-dependencies:
- dependency-name: chartjs-adapter-date-fns
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:03:32 +01:00
Faton Ramadani
22b8fed9d9 feat(frontend): Display error as an icon in order to avoid clutter wh… (#1143)
* feat(frontend): Display error as an icon in order to avoid clutter when an error occures

* feat(frontend): Fix build

* feat(frontend): Fix error icon position
2023-01-23 18:03:31 +01:00
Ruben Fiszel
0172587b12 feat: add sql as a valid type in Python 2023-01-23 15:58:33 +01:00
Faton Ramadani
f05f9e4edb feat(frontend): add selectedRowIndex to the table outputs (#1145) 2023-01-23 15:39:15 +01:00
Faton Ramadani
24b1fa0ae3 fix(frontend): Make sure AppSelect items are an array (#1144)
* fix(frontend): Make sure AppSelect items are an array

* fix(frontend): restore placeholder
2023-01-23 05:37:38 -08:00
Ruben Fiszel
fedb9fe74c update pricing page 2023-01-22 18:34:05 +01:00
Ruben Fiszel
5408da829c update pricing page 2023-01-22 18:20:06 +01:00
Ruben Fiszel
448e45b7e4 fix privilege on cloud 2023-01-22 00:53:29 +01:00
Ruben Fiszel
156f257619 add import_map to nsjail 2023-01-21 23:28:11 +01:00
Ruben Fiszel
7f40373fd6 feat(deno): allow overriding deno sandboxing with DENO_FLAGS' 2023-01-21 22:54:14 +01:00
Ruben Fiszel
f2ec03ba1b implement relative imports for deno 2023-01-21 19:12:22 +01:00
Ruben Fiszel
9a79c5537e fix compile 2023-01-21 18:49:26 +01:00
Ruben Fiszel
cf156b0591 support BigInt serialization 2023-01-21 18:41:58 +01:00
Ruben Fiszel
832ddabdf2 feat(deno): add support for DENO_AUTH_TOKENS 2023-01-21 18:36:17 +01:00
Ruben Fiszel
22080312f0 description at bottom 2023-01-21 17:33:23 +01:00
76 changed files with 1478 additions and 466 deletions

View File

@@ -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
View File

@@ -4570,8 +4570,10 @@ dependencies = [
"hmac",
"hyper",
"itertools",
"lazy_static",
"magic-crypt",
"mime_guess",
"prometheus",
"rand 0.8.5",
"reqwest",
"retainer",

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,5 @@
-- Add up migration script here
ALTER TABLE
workspace_settings
ADD
COLUMN webhook text;

View File

@@ -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,

View File

@@ -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": [],

View File

@@ -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
);
}

View File

@@ -70,3 +70,5 @@ cookie.workspace = true
sha2.workspace = true
urlencoding.workspace = true
async-stripe.workspace = true
lazy_static.workspace = true
prometheus.workspace = true

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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"))
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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(

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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))
}

View 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 })
}
}

View File

@@ -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>,

View File

@@ -58,6 +58,8 @@ pub enum Error {
ExitStatus(i32),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error("Error: {0:#?}")]
JsonErr(serde_json::Value),
}
impl Error {

View File

@@ -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),

View File

@@ -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"

View File

@@ -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()

View File

@@ -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

View File

@@ -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");

View File

@@ -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

View File

@@ -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'

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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} />

View File

@@ -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}

View File

@@ -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

View File

@@ -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[] = []

View File

@@ -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}

View File

@@ -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}`)
}

View 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}

View File

@@ -54,7 +54,6 @@
<RunnableWrapper
defaultUserInput
noMinH
bind:runnableComponent
bind:componentInput
{id}

View File

@@ -89,7 +89,6 @@
>
<RunnableWrapper
defaultUserInput
noMinH
bind:runnableComponent
bind:componentInput
{id}

View File

@@ -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}

View File

@@ -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 />

View File

@@ -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={() => {

View File

@@ -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>

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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'

View File

@@ -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>

View File

@@ -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 &nbsp;<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 &nbsp;<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 &nbsp;<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 &nbsp;<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 &nbsp;<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 &nbsp;<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>

View File

@@ -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">

View File

@@ -85,7 +85,7 @@
</div>
<div>
<FlowModuleSchemaMap bind:modules={branch.modules} />
<FlowModuleSchemaMap parentType="branchall" bind:modules={branch.modules} />
</div>
</div>
{/each}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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'}

View File

@@ -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">

View File

@@ -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))

View File

@@ -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}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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}`}

View File

@@ -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"

View File

@@ -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 }}

View File

@@ -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">

View File

@@ -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() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -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