Merge branch 'main' into alp/slack_interactive_approvals

This commit is contained in:
Alexander Petric
2024-12-05 14:56:30 -05:00
132 changed files with 2867 additions and 1537 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i '' -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i '' -e "/^ModuleVersion =/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
sed -i '' -e "/^[[:space:]]*ModuleVersion[[:space:]]*=/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
# sed -i '' -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml
sed -i '' -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i '' -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
+1 -1
View File
@@ -17,7 +17,7 @@ sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/pac
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i -e "/^ModuleVersion =/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
sed -i -e "/^[[:space:]]*ModuleVersion[[:space:]]*=/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
# sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
+33
View File
@@ -1,5 +1,38 @@
# Changelog
## [1.435.2](https://github.com/windmill-labs/windmill/compare/v1.435.1...v1.435.2) (2024-12-05)
### Bug Fixes
* job search toast on error ([#4851](https://github.com/windmill-labs/windmill/issues/4851)) ([a99e63f](https://github.com/windmill-labs/windmill/commit/a99e63f5435725c42e38b46933ff3caa345002b5))
## [1.435.1](https://github.com/windmill-labs/windmill/compare/v1.435.0...v1.435.1) (2024-12-05)
### Bug Fixes
* improve critical alerts filters ([548cfcf](https://github.com/windmill-labs/windmill/commit/548cfcfbde23ac7ae129e10f6e92d57516e61f20))
## [1.435.0](https://github.com/windmill-labs/windmill/compare/v1.434.2...v1.435.0) (2024-12-05)
### Features
* app custom paths ([#4828](https://github.com/windmill-labs/windmill/issues/4828)) ([1ec6c6f](https://github.com/windmill-labs/windmill/commit/1ec6c6f765904361e641d89495890bc87e8544aa))
### Bug Fixes
* pass USERPROFILE on windows ([5404ec9](https://github.com/windmill-labs/windmill/commit/5404ec9b48e8d7a0cb27b2319d54f04c94ec6fd0))
## [1.434.2](https://github.com/windmill-labs/windmill/compare/v1.434.1...v1.434.2) (2024-12-04)
### Bug Fixes
* custom http routes auth ([#4835](https://github.com/windmill-labs/windmill/issues/4835)) ([02611e4](https://github.com/windmill-labs/windmill/commit/02611e42e1b5c81d9bc57e3b46795e4c8cad67f7))
## [1.434.1](https://github.com/windmill-labs/windmill/compare/v1.434.0...v1.434.1) (2024-11-29)
@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "bool",
"name": "?column?",
"type_info": "Bool"
}
],
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET last_ping = now() WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "062859f1d0e5cfba3115f4241115753b86a4ad239708851c998ff5620ebca5b8"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*)\n FROM alerts\n WHERE COALESCE(acknowledged, false) = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Bool"
]
},
"nullable": [
null
]
},
"hash": "0a46f1f3047d15227f82ae24ad2113eb91d65b98927eaaba427cbde27dd79bfe"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow as \"flow!: sqlx::types::Json<Box<RawValue>>\" FROM flow_node WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "flow!: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true
]
},
"hash": "113b120ae10ea4469ec3575dc3506aaa6d6a8940017a1172403ca9851d0f13a7"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock, code AS \"code!: String\" FROM flow_node WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "code!: String",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
true
]
},
"hash": "15a5db0202c61ed187d9594311cb32cbde4240e143df1c980843dcdfeef3499e"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "1ec97e1bf7c6edfa82b7e64585171ca897dcfa9e82618ce8f11afb08a39e3b20"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*)\n FROM alerts\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "372eb162ace15a4f07162bb46706eaca91a3616c9bfbd78a408b7079ca9706d4"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow AS \"flow!: Json<Box<JsonRawValue>>\" FROM flow_node WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "flow!: Json<Box<JsonRawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true
]
},
"hash": "3d77e5b691dab38b3e39477ed980560bacd55c6ae9fafcbb0239163a3d7f3c0c"
}
@@ -1,27 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH existing AS (\n SELECT id FROM flow_node\n WHERE hash = $1 AND path = $2 AND workspace_id = $3 AND code = $4 AND lock = $5 AND (flow IS NOT DISTINCT FROM $6)\n LIMIT 1\n ),\n inserted AS (\n INSERT INTO flow_node (hash, path, workspace_id, code, lock, flow)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT DO NOTHING\n RETURNING id\n )\n SELECT id FROM existing\n UNION ALL\n SELECT id FROM inserted\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Text",
"Text",
"Jsonb"
]
},
"nullable": [
null
]
},
"hash": "5ac7c9eb3ce21e46f90e23435562b5f818a4274ba223f905e68280b86b24fa25"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE alerts\n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true\n ELSE acknowledged_workspace\n END\n WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": []
},
"hash": "65da41c7ded54cdee8d33211561c068b72294cc99ff44ed0a13179df508ebc6a"
}
@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "bool",
"name": "?column?",
"type_info": "Bool"
}
],
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app\n (workspace_id, path, summary, policy, versions, draft_only)\n VALUES ($1, $2, $3, $4, '{}', $5) RETURNING id",
"query": "INSERT INTO app\n (workspace_id, path, summary, policy, versions, draft_only, custom_path)\n VALUES ($1, $2, $3, $4, '{}', $5, $6) RETURNING id",
"describe": {
"columns": [
{
@@ -15,12 +15,13 @@
"Varchar",
"Varchar",
"Jsonb",
"Bool"
"Bool",
"Text"
]
},
"nullable": [
false
]
},
"hash": "75e880f9d9fbda36c2314706923cef36e4667d930fb8ee1876dd9ce1c92396b2"
"hash": "6b53f7c4bb73177316d6134698f3979f51b53dcd4d8ec50d312c9e7fe31ad5f5"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE alerts \n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "7c32176755c6ea2b6ae531860d436caae3fa256fc0803749ec5107632669adb3"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE alerts \n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2 THEN\n CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n ELSE true\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "80864158f61adaad8df934acc54ba523c9f17d106298d8781885134d28553d36"
}
@@ -0,0 +1,27 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO flow_node (path, workspace_id, hash_v2, lock, code, flow)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (hash_v2) DO UPDATE SET path = EXCLUDED.path -- trivial update to return the id\n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bpchar",
"Text",
"Text",
"Jsonb"
]
},
"nullable": [
false
]
},
"hash": "83cc9e432aea1450f79e9fce04eca0e75f30faa8c39ad4754bd568ae6f210806"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock, code AS \"code!: String\" FROM flow_node WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "code!: String",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
true
]
},
"hash": "8653373c2c8301c902e21e8bf5a11480ff040bb987a48b6f66522bc9b7e2c1d8"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*)\n FROM alerts",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "951c053cf756f00e1da26b06edb3d0193a0ee707e6482a892db58915e0c8a27f"
}
@@ -1,27 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH existing AS (\n SELECT id FROM flow_node\n WHERE hash = $1 AND path = $2 AND workspace_id = $3 AND code = $4 AND lock = $5 AND flow = $6\n LIMIT 1\n ),\n inserted AS (\n INSERT INTO flow_node (hash, path, workspace_id, code, lock, flow)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT DO NOTHING\n RETURNING id\n )\n SELECT id FROM existing\n UNION ALL\n SELECT id FROM inserted\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Text",
"Text",
"Jsonb"
]
},
"nullable": [
null
]
},
"hash": "aa98ab0e4b9a0eb41a804ab047a120444af65b1dc43a234821cde5bf6bf8b74f"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*)\n FROM alerts\n WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Bool"
]
},
"nullable": [
null
]
},
"hash": "b9ed42d4b795942251baafd016b4361e75257c0e5faf8795274ec86236a27413"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE alerts\n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $3 THEN\n CASE\n WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true\n ELSE acknowledged_workspace\n END\n ELSE true\n END\n WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "cd4067e68b375461a495f402d05976da6c6e331d5748bf6f8d59f9f75c027fe8"
}
@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "bool",
"name": "?column?",
"type_info": "Bool"
}
],
@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2) AND NOT (path = $3 AND workspace_id = $4))",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "fb1399c1dc171ec6bb24fee3477a1d606d9725e20e9c2d76a0887fadfd87f8df"
}
+199 -188
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.434.1"
version = "1.435.2"
authors.workspace = true
edition.workspace = true
@@ -29,7 +29,7 @@ members = [
]
[workspace.package]
version = "1.434.1"
version = "1.435.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -284,6 +284,7 @@ tikv-jemalloc-sys = { version = "^0.5" }
tikv-jemalloc-ctl = { version = "^0.5" }
triomphe = "^0"
pin-project-lite = "^0"
tantivy = "0.22.0"
+1 -1
View File
@@ -1 +1 @@
aefbc1e2188fea312996fcfc30a29d8fb5315316
8606d98a692d11b09a387c5efbd6b4335c533fd3
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE app DROP COLUMN custom_path;
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE app ADD COLUMN custom_path TEXT CHECK (custom_path ~ '^[\w-]+(\/[\w-]+)*$');
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,3 @@
-- Add up migration script here
GRANT ALL ON concurrency_key TO windmill_admin;
GRANT ALL ON concurrency_key TO windmill_user;
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE flow_node DROP COLUMN hash_v2;
@@ -0,0 +1,4 @@
-- Add up migration script here
CREATE SEQUENCE IF NOT EXISTS flow_node_hash_seq;
ALTER TABLE flow_node ALTER COLUMN hash DROP NOT NULL;
ALTER TABLE flow_node ADD COLUMN hash_v2 CHAR(64) NOT NULL UNIQUE DEFAULT to_hex(nextval('flow_node_hash_seq'));
+2 -2
View File
@@ -85,12 +85,12 @@ lazy_static::lazy_static! {
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or_else(|| "30".to_string());
.unwrap_or_else(|| "60".to_string());
static ref FLOW_ZOMBIE_TRANSITION_TIMEOUT: String = std::env::var("FLOW_ZOMBIE_TRANSITION_TIMEOUT")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or_else(|| "30".to_string());
.unwrap_or_else(|| "60".to_string());
pub static ref RESTART_ZOMBIE_JOBS: bool = std::env::var("RESTART_ZOMBIE_JOBS")
+79 -7
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.434.1
version: 1.435.2
title: Windmill API
contact:
@@ -872,9 +872,20 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CriticalAlert'
type: object
properties:
alerts:
type: array
items:
$ref: '#/components/schemas/CriticalAlert'
total_rows:
type: integer
description: Total number of rows matching the query.
example: 100
total_pages:
type: integer
description: Total number of pages based on the page size.
example: 10
/settings/critical_alerts/{id}/acknowledge:
post:
@@ -2757,9 +2768,20 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CriticalAlert'
type: object
properties:
alerts:
type: array
items:
$ref: '#/components/schemas/CriticalAlert'
total_rows:
type: integer
description: Total number of rows matching the query.
example: 100
total_pages:
type: integer
description: Total number of pages based on the page size.
example: 10
/w/{workspace}/workspaces/critical_alerts/{id}/acknowledge:
post:
@@ -3733,6 +3755,27 @@ paths:
required:
- app
/apps_u/public_app_by_custom_path/{custom_path}:
get:
summary: get public app by custom path
operationId: getPublicAppByCustomPath
tags:
- app
parameters:
- $ref: "#/components/parameters/CustomPath"
responses:
"200":
description: app details
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/AppWithLastVersion"
- type: object
properties:
workspace_id:
type: string
/scripts/hub/get/{path}:
get:
summary: get hub script content by path
@@ -5371,6 +5414,8 @@ paths:
type: boolean
deployment_message:
type: string
custom_path:
type: string
required:
- path
- value
@@ -5696,6 +5741,8 @@ paths:
$ref: "#/components/schemas/Policy"
deployment_message:
type: string
custom_path:
type: string
responses:
"200":
description: app updated
@@ -5704,6 +5751,23 @@ paths:
schema:
type: string
/w/{workspace}/apps/custom_path_exists/{custom_path}:
get:
summary: check if custom path exists
operationId: customPathExists
tags:
- app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/CustomPath"
responses:
"200":
description: custom path exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/apps_u/execute_component/{path}:
post:
summary: executeComponent
@@ -10222,6 +10286,12 @@ components:
required: true
schema:
type: string
CustomPath:
name: custom_path
in: path
required: true
schema:
type: string
PathId:
name: id
in: path
@@ -12860,6 +12930,8 @@ components:
draft_only:
type: boolean
draft: {}
custom_path:
type: string
AppHistory:
type: object
+93 -16
View File
@@ -56,10 +56,10 @@ use windmill_common::{
jobs::{get_payload_tag_from_prefixed_path, JobPayload, RawCode},
users::username_to_permissioned_as,
utils::{
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, Pagination, StripPath,
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath
},
variables::{build_crypt, build_crypt_with_key_suffix},
worker::to_raw_value,
worker::{to_raw_value, CLOUD_HOSTED},
HUB_BASE_URL,
};
@@ -81,6 +81,7 @@ pub fn workspaced_service() -> Router {
.route("/history/p/*path", get(get_app_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route("/history_update/a/:id/v/:version", post(update_app_history))
.route("/custom_path_exists/*custom_path", get(custom_path_exists))
}
pub fn unauthed_service() -> Router {
@@ -90,13 +91,17 @@ pub fn unauthed_service() -> Router {
.route("/public_app/:secret", get(get_public_app_by_secret))
.route("/public_resource/*path", get(get_public_resource))
}
pub fn global_service() -> Router {
Router::new()
.route("/hub/list", get(list_hub_apps))
.route("/hub/get/:id", get(get_hub_app_by_id))
}
#[cfg(not(feature = "enterprise"))]
pub fn global_unauthed_service() -> Router {
Router::new()
}
#[derive(FromRow, Deserialize, Serialize)]
pub struct ListableApp {
pub id: i64,
@@ -147,21 +152,26 @@ pub struct AppWithLastVersionAndStarred {
pub starred: Option<bool>,
}
#[cfg(feature = "enterprise")]
#[derive(Serialize, FromRow)]
pub struct AppWithLastVersionAndWorkspace {
#[sqlx(flatten)]
#[serde(flatten)]
pub app: AppWithLastVersion,
pub workspace_id: String,
}
#[derive(Serialize, Deserialize, FromRow)]
pub struct AppWithLastVersionAndDraft {
pub id: i64,
pub path: String,
pub summary: String,
pub policy: sqlx::types::Json<Box<RawValue>>,
pub versions: Vec<i64>,
pub value: sqlx::types::Json<Box<RawValue>>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub extra_perms: serde_json::Value,
#[sqlx(flatten)]
#[serde(flatten)]
pub app: AppWithLastVersion,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft: Option<sqlx::types::Json<Box<RawValue>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_path: Option<String>,
}
#[derive(Serialize)]
@@ -229,6 +239,7 @@ pub struct CreateApp {
pub policy: Policy,
pub draft_only: Option<bool>,
pub deployment_message: Option<String>,
pub custom_path: Option<String>,
}
#[derive(Deserialize)]
@@ -238,6 +249,7 @@ pub struct EditApp {
pub value: Option<sqlx::types::Json<Box<RawValue>>>,
pub policy: Option<Policy>,
pub deployment_message: Option<String>,
pub custom_path: Option<String>,
}
#[derive(Serialize, FromRow)]
@@ -408,7 +420,7 @@ async fn get_app_w_draft(
let app_o = sqlx::query_as::<_, AppWithLastVersionAndDraft>(
r#"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app.extra_perms, app_version.value, app.custom_path,
app_version.created_at, app_version.created_by,
app.draft_only, draft.value as "draft"
from app
@@ -515,6 +527,22 @@ async fn update_app_history(
return Ok(());
}
async fn custom_path_exists(
Extension(db): Extension<DB>,
Path((w_id, custom_path)): Path<(String, String)>,
) -> JsonResult<bool> {
let exists =
sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
custom_path,
if *CLOUD_HOSTED { Some(&w_id) } else { None }
)
.fetch_one(&db)
.await?.unwrap_or(false);
Ok(Json(exists))
}
async fn get_app_by_id(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -598,6 +626,7 @@ async fn get_public_app_by_secret(
Ok(Json(app))
}
async fn get_public_resource(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -680,6 +709,26 @@ async fn create_app(
)));
}
if let Some(custom_path) = &app.custom_path {
require_admin(authed.is_admin, &authed.username)?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
custom_path,
if *CLOUD_HOSTED { Some(&w_id) } else { None }
)
.fetch_one(&mut *tx)
.await?.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with custom path {} already exists",
custom_path
)));
}
}
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
&app.path,
@@ -690,13 +739,14 @@ async fn create_app(
let id = sqlx::query_scalar!(
"INSERT INTO app
(workspace_id, path, summary, policy, versions, draft_only)
VALUES ($1, $2, $3, $4, '{}', $5) RETURNING id",
(workspace_id, path, summary, policy, versions, draft_only, custom_path)
VALUES ($1, $2, $3, $4, '{}', $5, $6) RETURNING id",
w_id,
app.path,
app.summary,
json!(app.policy),
app.draft_only,
app.custom_path,
)
.fetch_one(&mut *tx)
.await?;
@@ -899,7 +949,11 @@ async fn update_app(
let mut tx = user_db.clone().begin(&authed).await?;
let npath = if ns.policy.is_some() || ns.path.is_some() || ns.summary.is_some() {
let npath = if ns.policy.is_some()
|| ns.path.is_some()
|| ns.summary.is_some()
|| ns.custom_path.is_some()
{
let mut sqlb = SqlBuilder::update_table("app");
sqlb.and_where_eq("path", "?".bind(&path));
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
@@ -932,6 +986,29 @@ async fn update_app(
sqlb.set_str("summary", nsummary);
}
if let Some(ncustom_path) = &ns.custom_path {
require_admin(authed.is_admin, &authed.username)?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2) AND NOT (path = $3 AND workspace_id = $4))",
ncustom_path,
if *CLOUD_HOSTED { Some(&w_id) } else { None },
path,
w_id
)
.fetch_one(&mut *tx)
.await?.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with custom path {} already exists",
ncustom_path
)));
}
sqlb.set_str("custom_path", ncustom_path);
}
if let Some(mut npolicy) = ns.policy {
npolicy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
npolicy.on_behalf_of_email = Some(authed.email.clone());
+5
View File
@@ -0,0 +1,5 @@
use axum::Router;
pub fn global_unauthed_service() -> Router {
Router::new()
}
+13
View File
@@ -64,6 +64,8 @@ mod indexer_ee;
mod inputs;
mod integration;
#[cfg(feature = "enterprise")]
mod apps_ee;
#[cfg(feature = "parquet")]
mod job_helpers_ee;
pub mod job_metrics;
@@ -343,6 +345,17 @@ pub async fn run_server(
)
.nest("/concurrency_groups", concurrency_groups::global_service())
.nest("/scripts_u", scripts::global_unauthed_service())
.nest("/apps_u", {
#[cfg(feature = "enterprise")]
{
apps_ee::global_unauthed_service()
}
#[cfg(not(feature = "enterprise"))]
{
Router::new()
}
})
.nest(
"/w/:workspace_id/apps_u",
apps::unauthed_service()
+21 -8
View File
@@ -1439,14 +1439,27 @@ async fn delete_script_by_path(
require_admin(authed.is_admin, &authed.username)?;
}
let script = sqlx::query_scalar!(
"DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.fetch_one(&db)
.await
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e:#}")))?;
let script = if !draft_only {
require_admin(authed.is_admin, &authed.username)?;
sqlx::query_scalar!(
"DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.fetch_one(&db)
.await
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e:#}")))?
} else {
// If the script is draft only, we can delete it without admin permissions but we still need write permissions
sqlx::query_scalar!(
"DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.fetch_one(&mut *tx)
.await
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e:#}")))?
};
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
+1 -1
View File
@@ -448,7 +448,7 @@ pub async fn get_critical_alerts(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Query(params): Query<crate::utils::AlertQueryParams>,
) -> JsonResult<Vec<crate::utils::CriticalAlert>> {
) -> JsonResult<serde_json::Value> {
require_devops_role(&db, &authed.email).await?;
crate::utils::get_critical_alerts(db, params, None).await
+70 -8
View File
@@ -10,6 +10,7 @@ use axum::{body::Body, response::Response};
use regex::Regex;
use serde::Deserialize;
use sqlx::{Postgres, Transaction};
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
auth::{is_devops_email, is_super_admin_email},
error::{self, Error},
@@ -206,11 +207,55 @@ pub async fn get_critical_alerts(
db: DB,
params: AlertQueryParams,
workspace_id: Option<String>,
) -> JsonResult<Vec<CriticalAlert>> {
) -> JsonResult<serde_json::Value> {
// Returning total rows and total pages
let page = params.page.unwrap_or(1).max(1);
let page_size = params.page_size.unwrap_or(10).min(100) as i64;
let offset = ((page - 1) * page_size as i32) as i64;
// Count total rows
let total_rows = if let Some(workspace_id) = &workspace_id {
if params.acknowledged.is_none() {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE workspace_id = $1",
workspace_id
)
.fetch_one(&db)
.await?
} else {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2",
workspace_id,
params.acknowledged
)
.fetch_one(&db)
.await?
}
} else {
if params.acknowledged.is_none() {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts"
)
.fetch_one(&db)
.await?
} else {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE COALESCE(acknowledged, false) = $1",
params.acknowledged
)
.fetch_one(&db)
.await?
}
};
// Fetch paginated rows
let alerts = if let Some(workspace_id) = workspace_id {
// `workspace_id` is provided => workspace admin
if params.acknowledged.is_none() {
@@ -278,7 +323,14 @@ pub async fn get_critical_alerts(
}
};
Ok(Json(alerts))
let total_rows = total_rows.unwrap_or(0);
let total_pages = ((total_rows as f64) / (page_size as f64)).ceil() as i64;
Ok(Json(serde_json::json!({
"alerts": alerts,
"total_rows": total_rows,
"total_pages": total_pages
})))
}
#[cfg(feature = "enterprise")]
@@ -292,12 +344,17 @@ pub async fn acknowledge_critical_alert(
SET
acknowledged = true,
acknowledged_workspace = CASE
WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true
ELSE acknowledged_workspace
WHEN $3 THEN
CASE
WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true
ELSE acknowledged_workspace
END
ELSE true
END
WHERE id = $1",
id,
workspace_id
workspace_id,
*CLOUD_HOSTED
)
.execute(&db)
.await?;
@@ -320,12 +377,17 @@ pub async fn acknowledge_all_critical_alerts(
SET
acknowledged = true,
acknowledged_workspace = CASE
WHEN $1::text IS NOT NULL THEN true
ELSE acknowledged_workspace
WHEN $2 THEN
CASE
WHEN $1::text IS NOT NULL THEN true
ELSE acknowledged_workspace
END
ELSE true
END
WHERE ($1::text IS NOT NULL AND workspace_id = $1)
OR ($1::text IS NULL)",
workspace_id
workspace_id,
*CLOUD_HOSTED
)
.execute(&db)
.await?;
+1 -1
View File
@@ -3111,7 +3111,7 @@ pub async fn get_critical_alerts(
Path(w_id): Path<String>,
authed: ApiAuthed,
Query(params): Query<crate::utils::AlertQueryParams>,
) -> JsonResult<Vec<crate::utils::CriticalAlert>> {
) -> JsonResult<serde_json::Value> {
require_admin_or_devops(authed.is_admin, &authed.username, &authed.email, &db).await?;
crate::utils::get_critical_alerts(db, params, Some(w_id)).await
+1
View File
@@ -62,6 +62,7 @@ windmill-macros.workspace = true
semver.workspace = true
croner = "2.0.6"
quick_cache.workspace = true
pin-project-lite.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemalloc-ctl = { optional = true, workspace = true }
+108 -69
View File
@@ -34,24 +34,36 @@ pub mod flow {
/// If not present, import from the file-system cache or fetch it from the database and write
/// it to the file system and cache.
/// This should be preferred over fetching the database directly.
pub async fn fetch_script(e: impl PgExecutor<'_>, node: FlowNodeId)
-> error::Result<(Option<String>, String)>
{
fetch(e, node).await.and_then(|Val { lock, code, .. }| Ok((lock, code.ok_or_else(|| {
error::Error::InternalErr(format!("Flow node ({:x}) isn't a script node.", node.0))
})?)))
pub async fn fetch_script(
e: impl PgExecutor<'_>,
node: FlowNodeId,
) -> error::Result<(Option<String>, String)> {
fetch(e, node).await.and_then(|Val { lock, code, .. }| {
Ok((
lock,
code.ok_or_else(|| {
error::Error::InternalErr(format!(
"Flow node ({:x}) isn't a script node.",
node.0
))
})?,
))
})
}
/// Fetch the flow node flow value referenced by `node` from the cache.
/// If not present, import from the file-system cache or fetch it from the database and write
/// it to the file system and cache.
/// This should be preferred over fetching the database directly.
pub async fn fetch_flow(e: impl PgExecutor<'_>, node: FlowNodeId)
-> error::Result<FlowValue>
{
fetch(e, node).await.and_then(|Val { flow, .. }| flow.ok_or_else(|| {
error::Error::InternalErr(format!("Flow node ({:x}) isn't a flow value node.", node.0))
}))
pub async fn fetch_flow(e: impl PgExecutor<'_>, node: FlowNodeId) -> error::Result<FlowValue> {
fetch(e, node).await.and_then(|Val { flow, .. }| {
flow.ok_or_else(|| {
error::Error::InternalErr(format!(
"Flow node ({:x}) isn't a flow value node.",
node.0
))
})
})
}
/// Fetch the flow node referenced by `node` from the cache.
@@ -62,32 +74,40 @@ pub mod flow {
// If not present, `get_or_insert_async` will lock the key until the future completes,
// so only one thread will be able to fetch the data from the database and write it to
// the file system and cache, hence no race on the file system.
CACHE.get_or_insert_async(
&node,
fs::import_or_insert_with(CACHE_DIR, node.0 as u64, async {
sqlx::query!(
"SELECT \
CACHE
.get_or_insert_async(
&node,
fs::import_or_insert_with(CACHE_DIR, node.0 as u64, async {
sqlx::query!(
"SELECT \
lock AS \"lock: String\", \
code AS \"code: String\", \
flow::text AS \"flow: Box<str>\" \
FROM flow_node WHERE id = $1 LIMIT 1",
node.0,
)
.fetch_one(e)
.await
.map_err(Into::into)
.and_then(|r| Ok(Val {
lock: r.lock.and_then(|x| if x.is_empty() { None } else { Some(x) }),
code: r.code,
flow: match r.flow {
None => None,
Some(flow) => serde_json::from_str(&flow).map_err(|err| {
error::Error::InternalErr(format!("Unable to parse flow value: {err:?}"))
})?,
}
}))
})
).await
node.0,
)
.fetch_one(e)
.await
.map_err(Into::into)
.and_then(|r| {
Ok(Val {
lock: r
.lock
.and_then(|x| if x.is_empty() { None } else { Some(x) }),
code: r.code,
flow: match r.flow {
None => None,
Some(flow) => serde_json::from_str(&flow).map_err(|err| {
error::Error::InternalErr(format!(
"Unable to parse flow value: {err:?}"
))
})?,
},
})
})
}),
)
.await
}
// ----------------------------------------------------------------------------------------------
@@ -130,7 +150,11 @@ pub mod flow {
match item {
Item::Lock => Ok(self.lock.as_ref().map(|s| s.as_bytes().to_vec())),
Item::Code => Ok(self.code.as_ref().map(|s| s.as_bytes().to_vec())),
Item::Flow => Ok(self.flow.as_ref().map(|f| serde_json::to_vec(f)).transpose()?),
Item::Flow => Ok(self
.flow
.as_ref()
.map(|f| serde_json::to_vec(f))
.transpose()?),
}
}
}
@@ -164,39 +188,44 @@ pub mod script {
/// If not present, import from the file-system cache or fetch it from the database and write
/// it to the file system and cache.
/// This should be preferred over fetching the database directly.
pub async fn fetch(e: impl PgExecutor<'_>, hash: ScriptHash, workspace_id: &str)
-> error::Result<Val>
{
pub async fn fetch(
e: impl PgExecutor<'_>,
hash: ScriptHash,
workspace_id: &str,
) -> error::Result<Val> {
// If not present, `get_or_insert_async` will lock the key until the future completes,
// so only one thread will be able to fetch the data from the database and write it to
// the file system and cache, hence no race on the file system.
CACHE.get_or_insert_async(
&hash,
fs::import_or_insert_with(CACHE_DIR, hash.0 as u64, async {
sqlx::query!(
"SELECT \
CACHE
.get_or_insert_async(
&hash,
fs::import_or_insert_with(CACHE_DIR, hash.0 as u64, async {
sqlx::query!(
"SELECT \
lock AS \"lock: String\", \
content AS \"code!: String\",
language AS \"language: Option<ScriptLang>\", \
envs AS \"envs: Vec<String>\", \
codebase AS \"codebase: String\" \
FROM script WHERE hash = $1 AND workspace_id = $2 LIMIT 1",
hash.0,
workspace_id,
)
.fetch_one(e)
.await
.map_err(Into::into)
.map(|r| Val {
lock: r.lock.and_then(|x| if x.is_empty() { None } else { Some(x) }),
code: r.code,
language: r.language,
envs: r.envs,
codebase: r.codebase,
})
})
)
.await
hash.0,
workspace_id,
)
.fetch_one(e)
.await
.map_err(Into::into)
.map(|r| Val {
lock: r
.lock
.and_then(|x| if x.is_empty() { None } else { Some(x) }),
code: r.code,
language: r.language,
envs: r.envs,
codebase: r.codebase,
})
}),
)
.await
}
// ----------------------------------------------------------------------------------------------
@@ -230,7 +259,9 @@ pub mod script {
match item {
Item::Lock => self.lock = Some(String::from_utf8(data)?),
Item::Code => self.code = String::from_utf8(data)?,
Item::Info => (self.language, self.envs, self.codebase) = serde_json::from_slice(&data)?,
Item::Info => {
(self.language, self.envs, self.codebase) = serde_json::from_slice(&data)?
}
}
Ok(())
}
@@ -239,7 +270,11 @@ pub mod script {
match item {
Item::Lock => Ok(self.lock.as_ref().map(|s| s.as_bytes().to_vec())),
Item::Code => Ok(Some(self.code.as_bytes().to_vec())),
Item::Info => Ok(Some(serde_json::to_vec(&(&self.language, &self.envs, &self.codebase))?)),
Item::Info => Ok(Some(serde_json::to_vec(&(
&self.language,
&self.envs,
&self.codebase,
))?)),
}
}
}
@@ -272,8 +307,7 @@ mod fs {
}
/// Import or insert a bundle within the given combination of `{root}/{key}/`.
pub async fn import_or_insert_with<T, F>(root: &str, key: u64, f: F)
-> error::Result<T>
pub async fn import_or_insert_with<T, F>(root: &str, key: u64, f: F) -> error::Result<T>
where
T: Bundle,
F: Future<Output = error::Result<T>>,
@@ -287,8 +321,9 @@ mod fs {
let mut data = T::default();
for item in T::items() {
let mut buf = vec![];
let Ok(mut file) = OpenOptions::new().read(true).open(item.path(&path))
else { continue };
let Ok(mut file) = OpenOptions::new().read(true).open(item.path(&path)) else {
continue;
};
file.read_to_end(&mut buf)?;
data.import(*item, buf)?;
}
@@ -299,7 +334,7 @@ mod fs {
Ok(data) => return Ok(data),
Err(err) => tracing::warn!(
"Failed to import from file-system, fetch source..: {path:?}: {err:?}"
)
),
}
}
// Cache path doesn't exist or import failed, generate the content.
@@ -308,9 +343,13 @@ mod fs {
fs::create_dir_all(&path)?;
// Write the generated data to the file.
for item in T::items() {
let Some(buf) = data.export(*item)?
else { continue };
let mut file = OpenOptions::new().write(true).create(true).open(item.path(&path))?;
let Some(buf) = data.export(*item)? else {
continue;
};
let mut file = OpenOptions::new()
.write(true)
.create(true)
.open(item.path(&path))?;
file.write_all(&buf)?;
}
tracing::debug!("Exported to file-system: {:?}", path);
+2 -1
View File
@@ -1,4 +1,4 @@
use crate::{db::DB, error};
use crate::{db::DB, error, utils::WarnAfterExt};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -90,6 +90,7 @@ pub async fn register_metric_for_job(
.bind(timeseries_int)
.bind(timeseries_float)
.execute(db)
.warn_after_seconds(1)
.await?;
Ok(metric_id)
+92 -9
View File
@@ -34,8 +34,8 @@ pub const GIT_VERSION: &str =
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
use crate::CRITICAL_ALERT_MUTE_UI_ENABLED;
use std::panic::{self, AssertUnwindSafe, Location};
use std::sync::atomic::Ordering;
use std::panic::{self, AssertUnwindSafe};
use crate::worker::CLOUD_HOSTED;
@@ -78,14 +78,18 @@ pub fn require_admin(is_admin: bool, username: &str) -> Result<()> {
}
}
pub async fn require_admin_or_devops(is_admin: bool, username: &str, email: &str, db: &DB) -> Result<()> {
pub async fn require_admin_or_devops(
is_admin: bool,
username: &str,
email: &str,
db: &DB,
) -> Result<()> {
if !is_admin {
if !is_devops_email(db, email).await? {
return Err(Error::RequireAdmin(username.to_string()));
}
}
Ok(())
}
pub fn hostname() -> String {
@@ -94,7 +98,7 @@ pub fn hostname() -> String {
.to_str()
.map(|x| x.to_string())
.unwrap_or_else(|| rd_string(5))
})
})
}
pub fn paginate(pagination: Pagination) -> (usize, usize) {
@@ -440,9 +444,7 @@ impl ScheduleType {
Some("v2") | Some(_) => {
// Use Croner for v2
let schedule_type_result = panic::catch_unwind(AssertUnwindSafe(|| {
Cron::new(schedule_str)
.with_seconds_optional()
.parse()
Cron::new(schedule_str).with_seconds_optional().parse()
}))
.map_err(|_| {
tracing::error!(
@@ -478,8 +480,14 @@ impl ScheduleType {
}
if let Err(e) = result {
tracing::error!("An error occurred while finding the next occurrence: {:?}", e);
return Err(Error::BadRequest(format!("cron: error during find_next_occurrence: {:?}", e)));
tracing::error!(
"An error occurred while finding the next occurrence: {:?}",
e
);
return Err(Error::BadRequest(format!(
"cron: error during find_next_occurrence: {:?}",
e
)));
}
}
@@ -526,3 +534,78 @@ impl ScheduleType {
Ok(events)
}
}
use std::future::Future;
use std::pin::Pin;
use std::task::{Context as TContext, Poll};
use tokio::time::{self, Duration, Sleep};
use pin_project_lite::pin_project;
pub trait WarnAfterExt: Future + Sized {
/// Warns if the future takes longer than the specified number of seconds to complete.
#[track_caller]
fn warn_after_seconds(self, seconds: u8) -> WarnAfterFuture<Self> {
let caller = Location::caller();
let location = format!("{}:{}", caller.file(), caller.line());
WarnAfterFuture {
future: self,
timeout: time::sleep(Duration::from_secs(seconds as u64)),
warned: false,
start_time: std::time::Instant::now(),
location: location,
seconds,
}
}
}
// Blanket implementation for all futures.
impl<F: Future> WarnAfterExt for F {}
pin_project! {
/// A future that wraps another future and prints a warning if it takes too long.
pub struct WarnAfterFuture<F> {
#[pin]
future: F,
#[pin]
timeout: Sleep,
warned: bool,
location: String,
start_time: std::time::Instant,
seconds: u8,
}
}
impl<F: Future> Future for WarnAfterFuture<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut TContext<'_>) -> Poll<Self::Output> {
let this = self.project();
// Poll the timeout future to check if it has elapsed.
if !*this.warned {
if this.timeout.poll(cx).is_ready() {
tracing::warn!(location = this.location, "SLOW_QUERY: query to db taking longer than expected (> {} seconds). This is a sign the database is under heavy load, query is too heavy or database is undersized",
this.seconds,
);
*this.warned = true;
}
}
// Poll the wrapped future.
match this.future.poll(cx) {
Poll::Ready(output) => {
if *this.warned {
let elapsed = this.start_time.elapsed();
tracing::warn!(
location = this.location,
"SLOW_QUERY: completed with total duration: {:.2?}",
elapsed
);
}
Poll::Ready(output)
}
Poll::Pending => Poll::Pending,
}
}
}
+6 -3
View File
@@ -39,8 +39,8 @@ use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::{
cache,
auth::{fetch_authed_from_permissioned_as, permissioned_as_to_username},
cache,
db::{Authed, UserDB},
error::{self, to_anyhow, Error},
flow_status::{
@@ -57,7 +57,7 @@ use windmill_common::{
schedule::Schedule,
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
utils::{not_found_if_none, report_critical_error, StripPath},
utils::{not_found_if_none, report_critical_error, StripPath, WarnAfterExt},
worker::{
to_raw_value, CLOUD_HOSTED, DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES,
DISABLE_FLOW_SCRIPT, MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432, NO_LOGS,
@@ -318,6 +318,7 @@ pub async fn append_logs(
workspace.as_ref(),
)
.execute(db.borrow())
.warn_after_seconds(1)
.await
{
tracing::error!(%job_id, %err, "error updating logs for large_log job {job_id}: {err}");
@@ -3227,7 +3228,7 @@ pub async fn push<'c, 'd>(
None,
None,
)
},
}
JobPayload::ScriptHub { path } => {
if path == "hub/7771/slack" || path == "hub/7836/slack" {
permissioned_as = SUPERADMIN_NOTIFICATION_EMAIL.to_string();
@@ -3851,6 +3852,7 @@ pub async fn push<'c, 'd>(
tag,
)
.execute(&mut *tx)
.warn_after_seconds(1)
.await?;
let (raw_code, raw_lock, raw_flow) = if !*MIN_VERSION_IS_AT_LEAST_1_427.read().await {
@@ -3901,6 +3903,7 @@ pub async fn push<'c, 'd>(
final_priority,
)
.fetch_one(&mut *tx)
.warn_after_seconds(1)
.await
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id} with tag {tag}, schedule_path {schedule_path:?}, script_path: {script_path:?}, email {email}, workspace_id {workspace_id}: {e:#}")))?;
@@ -433,6 +433,10 @@ fi
.args(cmd_args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
ansible_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
start_child_process(ansible_cmd, ANSIBLE_PLAYBOOK_PATH.as_str()).await?
};
@@ -565,13 +569,9 @@ async fn create_file_resources(
get_resource_or_variable_content(client, &file_res.resource_path, job_id.to_string())
.await?;
let path = file_res.target_path.clone();
let validated_path = write_file_at_user_defined_location(
job_dir,
path.as_str(),
&r,
file_res.mode,
)
.map_err(|e| anyhow!("Couldn't write text file at {}: {}", path, e))?;
let validated_path =
write_file_at_user_defined_location(job_dir, path.as_str(), &r, file_res.mode)
.map_err(|e| anyhow!("Couldn't write text file at {}: {}", path, e))?;
nsjail_mounts.push(
define_nsjail_mount(job_dir, &validated_path)
+2 -1
View File
@@ -522,7 +522,8 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
std::env::var("PATHEXT").unwrap_or_else(|_| {
String::from(".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL")
}),
);
)
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
}
cmd.spawn()?
@@ -1522,6 +1522,15 @@ pub async fn get_common_bun_proc_envs(base_internal_url: Option<&str>) -> HashMa
bun_envs.insert(String::from("NODE_PATH"), node_path.to_string());
}
#[cfg(windows)]
{
bun_envs.insert("SystemRoot".to_string(), crate::SYSTEM_ROOT.to_string());
bun_envs.insert(
"USERPROFILE".to_string(),
crate::USERPROFILE_ENV.to_string(),
);
}
return bun_envs;
}
@@ -84,6 +84,15 @@ async fn get_common_deno_proc_envs(
if DENO_TLS_CA_STORE.len() > 0 {
deno_envs.insert(String::from("DENO_TLS_CA_STORE"), DENO_TLS_CA_STORE.clone());
}
#[cfg(windows)]
{
deno_envs.insert("SystemRoot".to_string(), crate::SYSTEM_ROOT.to_string());
deno_envs.insert(
"USERPROFILE".to_string(),
crate::USERPROFILE_ENV.to_string(),
);
}
return deno_envs;
}
@@ -194,6 +194,10 @@ func Run(req Req) (interface{{}}, error){{
.args(vec!["build", "main.go"])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
build_go_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
let build_go_process = start_child_process(build_go_cmd, GO_PATH.as_str()).await?;
handle_child(
&job.id,
@@ -293,6 +297,9 @@ func Run(req Req) (interface{{}}, error){{
run_go.env("GOPROXY", goproxy);
}
#[cfg(windows)]
run_go.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
run_go.stdout(Stdio::piped()).stderr(Stdio::piped());
start_child_process(run_go, compiled_executable_name).await?
};
+13 -4
View File
@@ -8,7 +8,14 @@ use windmill_common::DB;
use crate::job_logger::CompactLogs;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
pub(crate) async fn s3_storage(_job_id: Uuid, _w_id: &String, _db: &sqlx::Pool<sqlx::Postgres>, _logs: &String, _total_size: &Arc<AtomicU32>, _worker_name: &String) {
pub(crate) async fn s3_storage(
_job_id: Uuid,
_w_id: &String,
_db: &sqlx::Pool<sqlx::Postgres>,
_logs: &String,
_total_size: &Arc<AtomicU32>,
_worker_name: &String,
) {
tracing::info!("Logs length of {job_id} has exceeded a threshold. Implementation to store excess on s3 in not OSS");
}
@@ -24,7 +31,9 @@ pub(crate) async fn default_disk_log_storage(
tracing::info!("Logs length of {job_id} has exceeded a threshold. Implementation to store excess on disk in not OSS");
}
pub(crate) fn process_streaming_log_lines(r: Result<Option<String>, io::Error>, _stderr: bool) -> Option<Result<String, io::Error>> {
pub(crate) fn process_streaming_log_lines(
r: Result<Option<String>, io::Error>,
_stderr: bool,
) -> Option<Result<String, io::Error>> {
r.transpose()
}
}
@@ -749,8 +749,10 @@ mount {{
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
python_cmd.env("SystemRoot", SYSTEM_ROOT.as_str());
#[cfg(windows)] {
python_cmd.env("SystemRoot", SYSTEM_ROOT.as_str());
python_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
}
start_child_process(python_cmd, PYTHON_PATH.as_str()).await?
};
@@ -1277,6 +1279,7 @@ async fn spawn_uv_install(
.envs(envs)
.envs(PROXY_ENVS.clone())
.env("SystemRoot", SYSTEM_ROOT.as_str())
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
.env(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
@@ -14,6 +14,7 @@ use windmill_common::{
add_time,
error::{self, Error},
jobs::{JobKind, QueuedJob},
utils::WarnAfterExt,
worker::{to_raw_value, WORKER_GROUP},
DB,
};
@@ -431,6 +432,7 @@ pub async fn process_completed_job(
#[cfg(feature = "benchmark")]
bench,
)
.warn_after_seconds(10)
.await?;
}
}
@@ -469,6 +471,7 @@ pub async fn process_completed_job(
#[cfg(feature = "benchmark")]
bench,
)
.warn_after_seconds(10)
.await?;
}
}
+5 -1
View File
@@ -209,6 +209,7 @@ pub async fn build_rust_crate(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| "C:\\tmp".to_string()),
);
build_rust_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
}
let build_rust_process = start_child_process(build_rust_cmd, CARGO_PATH.as_str()).await?;
@@ -402,7 +403,10 @@ pub async fn handle_rust_job(
.stderr(Stdio::piped());
#[cfg(windows)]
run_rust.env("SystemRoot", SYSTEM_ROOT.as_str());
{
run_rust.env("SystemRoot", SYSTEM_ROOT.as_str());
run_rust.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
}
start_child_process(run_rust, compiled_executable_name).await?
};
+20 -5
View File
@@ -9,6 +9,7 @@
use windmill_common::{
auth::{fetch_authed_from_permissioned_as, JWTAuthClaims, JobPerms, JWT_SECRET},
scripts::PREVIEW_IS_TAR_CODEBASE_HASH,
utils::WarnAfterExt,
worker::{
get_memory, get_vcpus, get_windmill_memory_usage, get_worker_memory_usage, write_file,
ROOT_CACHE_DIR, TMP_DIR,
@@ -159,6 +160,7 @@ pub async fn create_token_for_owner_in_bg(
&email,
&job_id,
)
.warn_after_seconds(5)
.await
.expect("could not create job token");
*locked = token;
@@ -366,6 +368,7 @@ lazy_static::lazy_static! {
pub static ref NSJAIL_PATH: String = std::env::var("NSJAIL_PATH").unwrap_or_else(|_| "nsjail".to_string());
pub static ref PATH_ENV: String = std::env::var("PATH").unwrap_or_else(|_| String::new());
pub static ref HOME_ENV: String = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
pub static ref NODE_PATH: Option<String> = std::env::var("NODE_PATH").ok();
pub static ref TZ_ENV: String = std::env::var("TZ").unwrap_or_else(|_| String::new());
@@ -419,12 +422,12 @@ lazy_static::lazy_static! {
.and_then(|x| x.parse().ok())
.unwrap_or(false);
}
#[cfg(windows)]
lazy_static::lazy_static! {
pub static ref SYSTEM_ROOT: String = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".to_string());
pub static ref USERPROFILE_ENV: String = std::env::var("USERPROFILE").unwrap_or_else(|_| "/tmp".to_string());
}
//only matter if CLOUD_HOSTED
@@ -1823,7 +1826,9 @@ async fn handle_queued_job(
if job.parent_job.is_none() && job.created_by.starts_with("email-") {
let daily_count = sqlx::query!(
"SELECT value FROM metrics WHERE id = 'email_trigger_usage' AND created_at > NOW() - INTERVAL '1 day' ORDER BY created_at DESC LIMIT 1"
).fetch_optional(db).await?.map(|x| serde_json::from_value::<i64>(x.value).unwrap_or(1));
).fetch_optional(db)
.warn_after_seconds(5)
.await?.map(|x| serde_json::from_value::<i64>(x.value).unwrap_or(1));
if let Some(count) = daily_count {
if count >= 100 {
@@ -1836,6 +1841,7 @@ async fn handle_queued_job(
serde_json::json!(count + 1)
)
.execute(db)
.warn_after_seconds(5)
.await?;
}
} else {
@@ -1843,6 +1849,7 @@ async fn handle_queued_job(
"INSERT INTO metrics (id, value) VALUES ('email_trigger_usage', to_jsonb(1))"
)
.execute(db)
.warn_after_seconds(5)
.await?;
}
}
@@ -1855,6 +1862,7 @@ async fn handle_queued_job(
.ok_or_else(|| Error::InternalErr(format!("expected parent job")))?,
job.id,
)
.warn_after_seconds(5)
.await?;
Some(r)
@@ -1867,6 +1875,7 @@ async fn handle_queued_job(
&job.workspace_id
)
.execute(db)
.warn_after_seconds(5)
.await {
tracing::error!("Could not update parent job started_at flow_status: {}", e);
}
@@ -1883,6 +1892,7 @@ async fn handle_queued_job(
job.workspace_id
)
.fetch_one(db)
.warn_after_seconds(5)
.await
.map(|record| (record.raw_code, record.raw_lock, record.raw_flow))
.unwrap_or_default(),
@@ -1923,6 +1933,7 @@ async fn handle_queued_job(
&job.parent_job.unwrap()
)
.fetch_one(db)
.warn_after_seconds(5)
.await
.map_err(|e| {
Error::InternalErr(format!(
@@ -1957,6 +1968,7 @@ async fn handle_queued_job(
&job.workspace_id,
&cached_res_path,
)
.warn_after_seconds(5)
.await;
if let Some(cached_resource_value) = cached_resource_value_maybe {
{
@@ -1995,6 +2007,7 @@ async fn handle_queued_job(
worker_dir,
job_completed_tx.0.clone(),
)
.warn_after_seconds(10)
.await?;
Ok(true)
} else {
@@ -2285,9 +2298,11 @@ async fn handle_code_execution_job(
.await?
}
JobKind::FlowScript => {
let (lockfile, content) = cache::flow::fetch_script(db, FlowNodeId(
job.script_hash.unwrap_or(ScriptHash(0)).0
)).await?;
let (lockfile, content) = cache::flow::fetch_script(
db,
FlowNodeId(job.script_hash.unwrap_or(ScriptHash(0)).0),
)
.await?;
ContentReqLangEnvs {
content,
lockfile,
+8 -2
View File
@@ -41,6 +41,7 @@ use windmill_common::jobs::{
script_hash_to_tag_and_limits, script_path_to_payload, BranchResults, JobPayload, QueuedJob,
RawCode, ENTRYPOINT_OVERRIDE,
};
use windmill_common::utils::WarnAfterExt;
use windmill_common::worker::to_raw_value;
use windmill_common::{
error::{self, to_anyhow, Error},
@@ -1109,6 +1110,7 @@ pub async fn update_flow_status_after_job_completion_internal(
worker_dir,
job_completed_tx,
)
.warn_after_seconds(10)
.await
{
Err(err) => {
@@ -1510,7 +1512,7 @@ pub async fn handle_flow(
let schedule_path = flow_job.schedule_path.as_ref().unwrap();
let schedule =
get_schedule_opt(&mut tx, &flow_job.workspace_id, schedule_path).await?;
get_schedule_opt(&mut tx, &flow_job.workspace_id, schedule_path).warn_after_seconds(5).await?;
tx.commit().await?;
@@ -1522,6 +1524,7 @@ pub async fn handle_flow(
flow_job.script_path.as_ref().unwrap(),
&flow_job.workspace_id,
)
.warn_after_seconds(5)
.await
{
match err {
@@ -1549,6 +1552,7 @@ pub async fn handle_flow(
worker_dir,
job_completed_tx,
)
.warn_after_seconds(10)
.await?;
Ok(())
}
@@ -2133,6 +2137,7 @@ async fn push_next_flow_job(
.bind(status.step)
.bind(json!(status.retry.failed_jobs))
.execute(db)
.warn_after_seconds(2)
.await
.context("update flow retry")?;
@@ -2558,6 +2563,7 @@ async fn push_next_flow_job(
new_job_priority_override,
job_perms.as_ref(),
)
.warn_after_seconds(2)
.await?;
tracing::debug!(id = %flow_job.id, root_id = %job_root, "pushed next flow job: {uuid}");
@@ -2770,7 +2776,7 @@ async fn push_next_flow_job(
.execute(&mut *tx)
.await?;
tx.commit().await?;
tx.commit().warn_after_seconds(3).await?;
tracing::info!(id = %flow_job.id, root_id = %job_root, "all next flow jobs pushed: {uuids:?}");
if continue_on_same_worker {
+106 -56
View File
@@ -4,6 +4,7 @@ use std::path::{Component, Path, PathBuf};
use async_recursion::async_recursion;
use serde_json::value::RawValue;
use serde_json::{json, Value};
use sha2::Digest;
use sqlx::types::Json;
use uuid::Uuid;
use windmill_common::error::Error;
@@ -606,9 +607,8 @@ pub async fn handle_flow_dependency_job(
occupancy_metrics,
)
.await?;
let new_flow_value = sqlx::types::Json(
serde_json::value::to_raw_value(&flow).map_err(to_anyhow)?
);
let new_flow_value =
sqlx::types::Json(serde_json::value::to_raw_value(&flow).map_err(to_anyhow)?);
// Re-check cancelation to ensure we don't accidentially override a flow.
if sqlx::query_scalar!("SELECT canceled FROM queue WHERE id = $1", job.id)
@@ -648,11 +648,20 @@ pub async fn handle_flow_dependency_job(
// Compute a lite version of the flow value (`RawScript` => `FlowScript`).
let mut value_lite = flow.clone();
tx = reduce(tx, &mut value_lite.modules, &job_path, &job.workspace_id, flow.failure_module.as_ref(), flow.same_worker).await?;
tx = reduce(
tx,
&mut value_lite.modules,
&job_path,
&job.workspace_id,
flow.failure_module.as_ref(),
flow.same_worker,
)
.await?;
sqlx::query!(
"INSERT INTO flow_version_lite (id, value) VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value",
version, sqlx::types::Json(to_raw_value(&value_lite)) as sqlx::types::Json<Box<RawValue>>,
version,
sqlx::types::Json(to_raw_value(&value_lite)) as sqlx::types::Json<Box<RawValue>>,
)
.execute(db)
.await?;
@@ -829,8 +838,12 @@ async fn lock_modules<'c>(
occupancy_metrics,
))
.await?;
e.value =
FlowModuleValue::WhileloopFlow { modules: nmodules, modules_node, skip_failures }.into()
e.value = FlowModuleValue::WhileloopFlow {
modules: nmodules,
modules_node,
skip_failures,
}
.into()
}
FlowModuleValue::BranchOne { branches, default, default_node } => {
let mut nbranches = vec![];
@@ -878,8 +891,12 @@ async fn lock_modules<'c>(
occupancy_metrics,
))
.await?;
e.value = FlowModuleValue::BranchOne { branches: nbranches, default: ndefault, default_node }
.into();
e.value = FlowModuleValue::BranchOne {
branches: nbranches,
default: ndefault,
default_node,
}
.into();
}
_ => (),
};
@@ -1009,41 +1026,32 @@ async fn insert_flow_node<'c>(
flow: Option<&Json<Box<RawValue>>>,
) -> Result<(sqlx::Transaction<'c, sqlx::Postgres>, FlowNodeId)> {
let hash = {
use std::hash::{DefaultHasher, Hasher, Hash};
let mut hasher = DefaultHasher::new();
code.hash(&mut hasher);
lock.hash(&mut hasher);
flow.inspect(|flow| flow.get().hash(&mut hasher));
hasher.finish() as i64
let mut hasher = sha2::Sha256::new();
hasher.update(path);
hasher.update(workspace_id);
hasher.update(code.unwrap_or(&Default::default()));
hasher.update(lock.unwrap_or(&Default::default()));
hasher.update(flow.unwrap_or(&Default::default()).get());
format!("{:x}", hasher.finalize())
};
// Insert the flow node if it doesn't exist.
let id = sqlx::query_scalar!(
r#"
WITH existing AS (
SELECT id FROM flow_node
WHERE hash = $1 AND path = $2 AND workspace_id = $3
AND (code IS NOT DISTINCT FROM $4)
AND (lock IS NOT DISTINCT FROM $5)
AND (flow IS NOT DISTINCT FROM $6)
LIMIT 1
),
inserted AS (
INSERT INTO flow_node (hash, path, workspace_id, code, lock, flow)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT DO NOTHING
RETURNING id
)
SELECT id FROM existing
UNION ALL
SELECT id FROM inserted
INSERT INTO flow_node (path, workspace_id, hash_v2, lock, code, flow)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (hash_v2) DO UPDATE SET path = EXCLUDED.path -- trivial update to return the id
RETURNING id
"#,
hash, path, workspace_id, code, lock, flow as Option<&Json<Box<RawValue>>>
path,
workspace_id,
hash,
lock,
code,
flow as Option<&Json<Box<RawValue>>>
)
.fetch_one(&mut *tx)
.await?
.ok_or(error::Error::InternalErr("Failed to cache".to_string()))?;
.await?;
Ok((tx, FlowNodeId(id)))
}
@@ -1056,7 +1064,15 @@ async fn insert_flow_modules<'c>(
modules: &mut Vec<FlowModule>,
modules_node: &mut Option<FlowNodeId>,
) -> Result<sqlx::Transaction<'c, sqlx::Postgres>> {
tx = Box::pin(reduce(tx, modules, path, workspace_id, failure_module, same_worker)).await?;
tx = Box::pin(reduce(
tx,
modules,
path,
workspace_id,
failure_module,
same_worker,
))
.await?;
add_virtual_items_if_necessary(modules);
if modules.is_empty() || crate::worker_flow::is_simple_modules(modules, failure_module) {
return Ok(tx);
@@ -1073,7 +1089,7 @@ async fn insert_flow_modules<'c>(
failure_module: failure_module.cloned(),
same_worker,
..Default::default()
})))
}))),
)
.await?;
*modules_node = Some(id);
@@ -1090,8 +1106,13 @@ async fn reduce<'c>(
) -> Result<sqlx::Transaction<'c, sqlx::Postgres>> {
use FlowModuleValue::*;
for module in &mut *modules {
let mut val = serde_json::from_str::<FlowModuleValue>(module.value.get())
.map_err(|err| Error::InternalErr(format!("reduce: Failed to parse flow module value: {}", err)))?;
let mut val =
serde_json::from_str::<FlowModuleValue>(module.value.get()).map_err(|err| {
Error::InternalErr(format!(
"reduce: Failed to parse flow module value: {}",
err
))
})?;
match &mut val {
RawScript { .. } => {
// In order to avoid an unnecessary `.clone()` of `val`, take ownership of it's content
@@ -1107,9 +1128,14 @@ async fn reduce<'c>(
concurrency_time_window_s,
is_trigger,
..
} = std::mem::replace(&mut val, Identity) else { unreachable!() };
} = std::mem::replace(&mut val, Identity)
else {
unreachable!()
};
let id;
(tx, id) = insert_flow_node(tx, path, workspace_id, Some(&content), lock.as_ref(), None).await?;
(tx, id) =
insert_flow_node(tx, path, workspace_id, Some(&content), lock.as_ref(), None)
.await?;
val = FlowScript {
input_transforms,
id,
@@ -1120,32 +1146,56 @@ async fn reduce<'c>(
concurrency_time_window_s,
is_trigger,
};
},
}
ForloopFlow { modules, modules_node, .. }
| WhileloopFlow { modules, modules_node, .. } => {
| WhileloopFlow { modules, modules_node, .. } => {
tx = insert_flow_modules(
tx, path, workspace_id, failure_module, same_worker,
modules, modules_node
).await?;
tx,
path,
workspace_id,
failure_module,
same_worker,
modules,
modules_node,
)
.await?;
}
BranchOne { branches, default, default_node, .. } => {
for branch in branches.iter_mut() {
tx = insert_flow_modules(
tx, path, workspace_id, failure_module, same_worker,
&mut branch.modules, &mut branch.modules_node
).await?;
tx,
path,
workspace_id,
failure_module,
same_worker,
&mut branch.modules,
&mut branch.modules_node,
)
.await?;
}
tx = insert_flow_modules(
tx, path, workspace_id, failure_module, same_worker,
default, default_node
).await?;
tx,
path,
workspace_id,
failure_module,
same_worker,
default,
default_node,
)
.await?;
}
BranchAll { branches, .. } => {
for branch in branches.iter_mut() {
tx = insert_flow_modules(
tx, path, workspace_id, failure_module, same_worker,
&mut branch.modules, &mut branch.modules_node
).await?;
tx,
path,
workspace_id,
failure_module,
same_worker,
&mut branch.modules,
&mut branch.modules_node,
)
.await?;
}
}
_ => {}
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.434.1";
export const VERSION = "v1.435.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -60,7 +60,7 @@ export {
// }
// });
export const VERSION = "1.434.1";
export const VERSION = "1.435.2";
const command = new Command()
.name("wmill")
-6
View File
@@ -129,12 +129,6 @@ services:
- PORT=8001
- DATABASE_URL=${DATABASE_URL}
- MODE=indexer
- TANTIVY_MAX_INDEXED_JOB_LOG_SIZE__MB=1 # job logs bigger than this will be truncated before indexing
- TANTIVY_S3_BACKUP_PERIOD__S=3600 # how often to backup the index into object storage
- TANTIVY_INDEX_WRITER_MEMORY_BUDGET__MB=100 # higher budget for higher indexing throughput
- TANTIVY_REFRESH_INDEX_PERIOD__S=300 #how often to start indexing new jobs
- TANTIVY_DOC_COMMIT_MAX_BATCH_SIZE=100000 #how many documents to batch in one commit
- TANTIVY_SHOW_MEMORY_EVERY=10000 #log memory usage and progress every so many documents indexed
depends_on:
db:
condition: service_healthy
+68 -64
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.434.1",
"version": "1.435.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.434.1",
"version": "1.435.2",
"license": "AGPL-3.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
@@ -87,10 +87,10 @@
"@hey-api/openapi-ts": "^0.43.0",
"@playwright/test": "^1.34.3",
"@rgossiaux/svelte-headlessui": "^2.0.0",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/package": "^2.2.2",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/d3": "^7.4.0",
@@ -130,7 +130,7 @@
"tailwindcss": "^3.4.1",
"tslib": "^2.6.1",
"typescript": "^5.1.3",
"vite": "^5",
"vite": "^5.4.11",
"vite-plugin-circular-dependency": "^0.2.1",
"vite-plugin-mkcert": "^1.17.5",
"yootils": "^0.3.1"
@@ -3584,9 +3584,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.20",
@@ -3897,10 +3897,11 @@
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.24",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz",
"integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==",
"dev": true
"version": "1.0.0-next.28",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
"dev": true,
"license": "MIT"
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
@@ -4214,32 +4215,33 @@
}
},
"node_modules/@sveltejs/adapter-static": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.1.tgz",
"integrity": "sha512-6lMvf7xYEJ+oGeR5L8DFJJrowkefTK6ZgA4JiMqoClMkKq0s6yvsd3FZfCFvX1fQ0tpCD7fkuRVHsnUVgsHyNg==",
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.6.tgz",
"integrity": "sha512-MGJcesnJWj7FxDcB/GbrdYD3q24Uk0PIL4QIX149ku+hlJuj//nxUbb0HxUTpjkecWfHjVveSUnUaQWnPRXlpg==",
"dev": true,
"peerDependencies": {
"@sveltejs/kit": "^2.0.0"
}
},
"node_modules/@sveltejs/kit": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.0.tgz",
"integrity": "sha512-1uyXvzC2Lu1FZa30T4y5jUAC21R309ZMRG0TPt+PPPbNUoDpy8zSmSNVWYaBWxYDqLGQ5oPNWvjvvF2IjJ1jmA==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.9.0.tgz",
"integrity": "sha512-W3E7ed3ChB6kPqRs2H7tcHp+Z7oiTFC6m+lLyAQQuyXeqw6LdNuuwEUla+5VM0OGgqQD+cYD6+7Xq80vVm17Vg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.6.0",
"devalue": "^4.3.2",
"esm-env": "^1.0.0",
"import-meta-resolve": "^4.0.0",
"devalue": "^5.1.0",
"esm-env": "^1.2.1",
"import-meta-resolve": "^4.1.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
"mrmime": "^2.0.0",
"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
"sirv": "^2.0.4",
"sirv": "^3.0.0",
"tiny-glob": "^0.2.9"
},
"bin": {
@@ -4249,9 +4251,9 @@
"node": ">=18.13"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.3"
"vite": "^5.0.3 || ^6.0.0"
}
},
"node_modules/@sveltejs/package": {
@@ -4277,17 +4279,17 @@
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz",
"integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz",
"integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==",
"dev": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^2.0.0",
"@sveltejs/vite-plugin-svelte-inspector": "^2.1.0",
"debug": "^4.3.4",
"deepmerge": "^4.3.1",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
"svelte-hmr": "^0.15.3",
"magic-string": "^0.30.10",
"svelte-hmr": "^0.16.0",
"vitefu": "^0.2.5"
},
"engines": {
@@ -4299,9 +4301,9 @@
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz",
"integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz",
"integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==",
"dev": true,
"dependencies": {
"debug": "^4.3.4"
@@ -6434,10 +6436,11 @@
}
},
"node_modules/devalue": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz",
"integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==",
"dev": true
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"dev": true,
"license": "MIT"
},
"node_modules/devlop": {
"version": "1.1.0",
@@ -6990,9 +6993,10 @@
}
},
"node_modules/esm-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA=="
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
"integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
"license": "MIT"
},
"node_modules/esm-env-robust": {
"version": "0.0.3",
@@ -7932,10 +7936,11 @@
}
},
"node_modules/import-meta-resolve": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz",
"integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
"integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -8608,14 +8613,11 @@
}
},
"node_modules/magic-string": {
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"version": "0.30.14",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
"integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/map-obj": {
@@ -9781,6 +9783,7 @@
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
"integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
@@ -11960,17 +11963,18 @@
"dev": true
},
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz",
"integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">= 10"
"node": ">=18"
}
},
"node_modules/slash": {
@@ -12642,9 +12646,9 @@
}
},
"node_modules/svelte-hmr": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
"integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==",
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz",
"integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==",
"dev": true,
"engines": {
"node": "^12.20 || ^14.13.1 || >= 16"
@@ -13064,6 +13068,7 @@
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -13395,15 +13400,14 @@
}
},
"node_modules/vite": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
"integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
"version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.40",
"rollup": "^4.13.0"
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.434.1",
"version": "1.435.2",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -21,10 +21,10 @@
"@hey-api/openapi-ts": "^0.43.0",
"@playwright/test": "^1.34.3",
"@rgossiaux/svelte-headlessui": "^2.0.0",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/package": "^2.2.2",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/d3": "^7.4.0",
@@ -64,7 +64,7 @@
"tailwindcss": "^3.4.1",
"tslib": "^2.6.1",
"typescript": "^5.1.3",
"vite": "^5",
"vite": "^5.4.11",
"vite-plugin-circular-dependency": "^0.2.1",
"vite-plugin-mkcert": "^1.17.5",
"yootils": "^0.3.1"
+55 -53
View File
@@ -27,61 +27,63 @@
</head>
<body data-sveltekit-preload-code="viewport" class="outline-none focus:outline-none">
<div style="display: contents">
<div id="mainbg" style="position: absolute; bottom: 0; left: 0; width: 100%; height: 100vh">
</div>
<div id="svelte-global-loader">
<div id="mainbg" style="position: absolute; bottom: 0; left: 0; width: 100%; height: 100vh">
</div>
<div style="position: fixed; left: 50%; top: 40%; transform: translate(-50%, -50%)">
<svg
version="1.1"
id="Windmill"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="128px"
height="128px"
viewBox="0 0 256 256"
style="enable-background: new 0 0 256 256"
xml:space="preserve"
>
<g>
<polygon
class="st2"
points="134.78,14.22 114.31,48.21 101.33,69.75 158.22,69.75 177.97,36.95 191.67,14.22 "
/>
<polygon
class="st3"
points="227.55,69.75 186.61,69.75 101.33,69.75 129.78,119.02 158.16,119.02 228.61,119.02 256,119.02 "
/>
<polygon
class="st3"
points="136.93,132.47 116.46,167.93 73.82,241.78 130.71,241.78 144.9,217.2 180.13,156.18 193.82,132.46
<div style="position: fixed; left: 50%; top: 40%; transform: translate(-50%, -50%)">
<svg
version="1.1"
id="Windmill"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="128px"
height="128px"
viewBox="0 0 256 256"
style="enable-background: new 0 0 256 256"
xml:space="preserve"
>
<g>
<polygon
class="st2"
points="134.78,14.22 114.31,48.21 101.33,69.75 158.22,69.75 177.97,36.95 191.67,14.22 "
/>
<polygon
class="st3"
points="227.55,69.75 186.61,69.75 101.33,69.75 129.78,119.02 158.16,119.02 228.61,119.02 256,119.02 "
/>
<polygon
class="st3"
points="136.93,132.47 116.46,167.93 73.82,241.78 130.71,241.78 144.9,217.2 180.13,156.18 193.82,132.46
"
/>
<polygon
class="st3"
points="121.7,131.95 101.23,96.49 58.59,22.63 30.15,71.91 44.34,96.49 79.57,157.5 93.26,181.22 "
/>
<polygon
class="st2"
points="64.81,131.95 25.15,131.21 0,130.74 28.44,180.01 66.73,180.72 93.26,181.21 "
/>
<polygon
class="st2"
points="165.38,181.74 184.58,216.46 196.75,238.47 225.19,189.2 206.66,155.69 193.83,132.46 "
/>
</g>
</svg>
<div
id="loading-text"
style="
text-align: center;
margin-top: 30px;
color: gray;
font-family: Arial, Helvetica, sans-serif;
"
>
Loading...
/>
<polygon
class="st3"
points="121.7,131.95 101.23,96.49 58.59,22.63 30.15,71.91 44.34,96.49 79.57,157.5 93.26,181.22 "
/>
<polygon
class="st2"
points="64.81,131.95 25.15,131.21 0,130.74 28.44,180.01 66.73,180.72 93.26,181.21 "
/>
<polygon
class="st2"
points="165.38,181.74 184.58,216.46 196.75,238.47 225.19,189.2 206.66,155.69 193.83,132.46 "
/>
</g>
</svg>
<div
id="loading-text"
style="
text-align: center;
margin-top: 30px;
color: gray;
font-family: Arial, Helvetica, sans-serif;
"
>
Loading...
</div>
</div>
</div>
%sveltekit.body%
+1 -1
View File
@@ -250,7 +250,7 @@
</Label>
{#if !disabled}
<div class="flex flex-row gap-2">
<div class="flex flex-row gap-2 mb-2">
<CronBuilder let:close>
<div class="w-full flex flex-col">
<div class="w-full flex flex-col gap-1">
@@ -0,0 +1,10 @@
<script lang="ts">
export let link: string | undefined = undefined
</script>
<div class="text-xs text-tertiary font-normal">
<slot />
{#if link}
<a href={link} target="_blank">Learn more</a>
{/if}
</div>
@@ -214,6 +214,7 @@
export let small = false
export let scriptLang: Preview['language'] | 'bunnative'
export let disabled: boolean = false
export let lineNumbersMinChars = 3
const rHash = randomHash()
$: filePath = computePath(path)
@@ -1131,6 +1132,7 @@
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
fontSize: !small ? 14 : 12,
lineNumbersMinChars,
// overflowWidgetsDomNode: widgets,
tabSize: lang == 'python' ? 4 : 2,
folding
@@ -36,6 +36,8 @@
>
<FlowGraphV2
{triggerNode}
earlyStop={flow.value.skip_expr !== undefined}
cache={flow.value.cache_ttl !== undefined}
path={flow?.path}
{download}
minHeight={400}
@@ -1244,6 +1244,8 @@
}
}
}}
earlyStop={job.raw_flow?.skip_expr !== undefined}
cache={job.raw_flow?.cache_ttl !== undefined}
modules={job.raw_flow?.modules ?? []}
failureModule={job.raw_flow?.failure_module}
preprocessorModule={job.raw_flow?.preprocessor_module}
@@ -325,7 +325,7 @@
<div class="text-secondary pb-4 text-xs"
>Setting SMTP unlocks sending emails upon adding new users to the workspace or the
instance or sending critical alerts.
<a target="_blank" href="https://www.windmill.dev/docs/misc/setup_smtp">Learn more</a
<a target="_blank" href="https://www.windmill.dev/docs/advanced/instance_settings#smtp">Learn more</a
></div
>
{:else if category == "Indexer/Search"}
@@ -14,6 +14,7 @@
import Toggle from './Toggle.svelte'
import { loadSchedules, saveSchedule } from './flows/scheduleUtils'
import { type Writable, writable } from 'svelte/store'
import Description from '$lib/components/Description.svelte'
export let schema: any
export let isFlow: boolean
@@ -48,14 +49,18 @@
}
</script>
<ScheduleEditor
on:update={() => {
updateSchedules(true)
}}
bind:this={scheduleEditor}
/>
<div class="flex flex-col gap-4 w-full">
<Description link="https://www.windmill.dev/docs/core_concepts/scheduling">
Run scripts and flows automatically on a recurring basis using cron expressions. Each script or
flow can have multiple schedules, with one designated as primary.
</Description>
<ScheduleEditor
on:update={() => {
updateSchedules(true)
}}
bind:this={scheduleEditor}
/>
{#if $primarySchedule == undefined}
<Skeleton layout={[[12]]} />
{:else if $primarySchedule}
@@ -163,7 +168,7 @@
}}
variant="contained"
color="dark"
size="md"
size="sm"
startIcon={{ icon: Calendar }}
>
Set Primary Schedule
@@ -648,7 +648,7 @@
<Tab value="triggers">
Triggers
<Tooltip
documentationLink="https://www.windmill.dev/docs/getting_started/trigger_scripts"
documentationLink="https://www.windmill.dev/docs/getting_started/triggers"
>
Configure how this script will be triggered.
</Tooltip>
@@ -317,9 +317,10 @@
<SplitPanesWrapper>
<Splitpanes class="!overflow-visible">
<Pane size={60} minSize={10} class="!overflow-visible">
<div class="pl-2 h-full !overflow-visible bg-gray-50 dark:bg-[#272D38]">
<div class="h-full !overflow-visible bg-gray-50 dark:bg-[#272D38]">
{#key lang}
<Editor
lineNumbersMinChars={4}
folding
{path}
bind:code
+1 -3
View File
@@ -48,9 +48,7 @@
{/if}
</h2>
<slot name="action" />
{#if collapsable && collapsed}
<slot name="badge" />
{/if}
<slot name="badge" />
</div>
<div class={collapsable && collapsed ? `hidden ${$$props.class}` : `${$$props.class}`}>
<slot />
@@ -225,6 +225,8 @@
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
lineDecorationsWidth: 6,
lineNumbersMinChars: 2,
// overflowWidgetsDomNode: widgets,
fontSize: small ? 12 : 14
})
@@ -450,10 +450,11 @@
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
// overflowWidgetsDomNode: widgets,
lineNumbers: 'off',
// lineNumbers: 'on',
lineDecorationsWidth: 6,
lineNumbersMinChars: 2,
fontSize,
suggestOnTriggerCharacters: true,
lineDecorationsWidth: 14
suggestOnTriggerCharacters: true
})
editor.onDidFocusEditorText(() => {
+2 -4
View File
@@ -31,7 +31,7 @@
<label
for={id}
class="{$$props.class || ''} z-auto inline-flex items-center duration-50 {disabled
class="{$$props.class || ''} z-auto flex flex-row items-center duration-50 {disabled
? 'grayscale opacity-50'
: 'cursor-pointer'}"
>
@@ -104,9 +104,7 @@
<slot name="right" />
</label>
{#if eeOnly && disabled}
<span
class="inline-flex text-xs text-primary items-center gap-1 !text-yellow-500 whitespace-nowrap ml-8"
>
<span class="inline-flex text-xs items-center gap-1 !text-yellow-500 whitespace-nowrap ml-8">
<AlertTriangle size={16} />
EE only <Tooltip>Enterprise Edition only feature</Tooltip>
</span>
@@ -68,6 +68,7 @@
summary: string
policy: any
draft_only?: boolean
custom_path?: string
}
| undefined = undefined
export let version: number | undefined = undefined
@@ -11,12 +11,11 @@
import Toggle from '$lib/components/Toggle.svelte'
import { AppService, DraftService, type Job, type Policy } from '$lib/gen'
import { redo, undo } from '$lib/history'
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import {
AlignHorizontalSpaceAround,
BellOff,
Bug,
Clipboard,
DiffIcon,
Expand,
FileJson,
@@ -39,7 +38,6 @@
import {
classNames,
cleanValueProperties,
copyToClipboard,
truncateRev,
orderedJsonStringify,
type Value,
@@ -90,6 +88,9 @@
import HideButton from './settingsPanel/HideButton.svelte'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import { computeS3FileInputPolicy, computeWorkspaceS3FileInputPolicy } from './appUtilsS3'
import { isCloudHosted } from '$lib/cloud'
import { base } from '$lib/base'
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
async function hash(message) {
try {
@@ -119,6 +120,7 @@
summary: string
policy: any
draft_only?: boolean
custom_path?: string
}
| undefined = undefined
export let version: number | undefined = undefined
@@ -479,14 +481,16 @@
summary: $summary,
policy,
path: npath,
deployment_message: deploymentMsg
deployment_message: deploymentMsg,
custom_path: $userStore?.is_admin || $userStore?.is_super_admin ? customPath : undefined
}
})
savedApp = {
summary: $summary,
value: structuredClone($app),
path: npath,
policy
policy,
custom_path: customPath
}
const appHistory = await AppService.getAppHistoryByPath({
workspace: $workspaceStore!,
@@ -885,6 +889,37 @@
let priorDarkMode = document.documentElement.classList.contains('dark')
setTheme($app?.darkMode)
let customPath = savedApp?.custom_path
let dirtyCustomPath = false
let customPathError = ''
$: fullCustomUrl = `${window.location.origin}${base}/a/${
isCloudHosted() ? $workspaceStore + '/' : ''
}${customPath}`
async function appExists(customPath: string) {
return await AppService.customPathExists({
workspace: $workspaceStore!,
customPath
})
}
let validateTimeout: NodeJS.Timeout | undefined = undefined
async function validateCustomPath(customPath: string): Promise<void> {
customPathError = ''
if (validateTimeout) {
clearTimeout(validateTimeout)
}
validateTimeout = setTimeout(async () => {
if (!/^[\w-]+(\/[\w-]+)*$/.test(customPath)) {
customPathError = 'Invalid path'
} else if (customPath !== savedApp?.custom_path && (await appExists(customPath))) {
customPathError = 'Path already taken'
} else {
customPathError = ''
}
validateTimeout = undefined
}, 500)
}
$: customPath !== undefined && validateCustomPath(customPath)
</script>
<svelte:window on:keydown={onKeyDown} />
@@ -1071,7 +1106,7 @@
</Button>
<Button
startIcon={{ icon: Save }}
disabled={pathError != ''}
disabled={pathError != '' || customPathError != ''}
on:click={() => {
if ($appPath == '') {
createApp(newEditedPath)
@@ -1121,29 +1156,68 @@
</div>
<div class="my-6 box">
Public url:
<div class="text-secondary">
<div>Public URL</div>
</div>
{#if secretUrl}
{@const url = `${window.location.hostname}/public/${$workspaceStore}/${secretUrl}`}
{@const href = window.location.protocol + '//' + url}
<a
on:click={(e) => {
e.preventDefault()
copyToClipboard(href)
}}
{href}
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1 inline-flex gap-2"
>
{url}
<span class="text-gray-700 ml-2">
<Clipboard />
</span>
</a>
{@const href = `${window.location.origin}${base}/public/${$workspaceStore}/${secretUrl}`}
<ClipboardPanel content={href} size="md" />
{:else}<Loader2 class="animate-spin" />
{/if}
<div class="text-xs text-secondary"
>Share this url directly or embed it using an iframe (if requiring login, top-level domain
of embedding app must be the same as the one of Windmill)</div
>
<div class="text-xs text-secondary mt-1">
Share this url directly or embed it using an iframe (if requiring login, top-level domain
of embedding app must be the same as the one of Windmill)
</div>
<div class="mt-4">
{#if !$enterpriseLicense}
<Alert title="EE Only" type="warning" size="xs">
Custom path is an enterprise only feature.
</Alert>
<div class="mb-2" />
{:else if !($userStore?.is_admin || $userStore?.is_super_admin)}
<Alert type="warning" title="Admin only" size="xs">
Custom path can only be set by workspace admins
</Alert>
<div class="mb-2" />
{/if}
<Toggle
on:change={({ detail }) => {
customPath = detail ? '' : undefined
}}
checked={customPath !== undefined}
options={{
right: 'Use a custom URL'
}}
disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)}
/>
{#if customPath !== undefined}
<div class="text-secondary text-sm flex items-center gap-1 w-full justify-between">
<div>Custom path</div>
</div>
<input
disabled={!($userStore?.is_admin || $userStore?.is_super_admin)}
type="text"
autocomplete="off"
bind:value={customPath}
class={customPathError === ''
? ''
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
on:input={() => {
dirtyCustomPath = true
}}
/>
<div class="text-secondary text-sm flex items-center gap-1 mt-2 w-full justify-between">
<div>Custom public URL</div>
</div>
<ClipboardPanel content={fullCustomUrl} size="md" />
<div class="text-red-600 dark:text-red-400 text-2xs mt-1.5"
>{dirtyCustomPath ? customPathError : ''}
</div>
{/if}
</div>
</div>
<Alert type="info" title="Only latest deployed app is publicly available">
You will still need to deploy the app to make visible the latest changes
@@ -133,8 +133,9 @@
</div>
{/if}
<button
class="border bg-surface absolute top-0.5 right-2 p-0.5"
on:click={() => (fullscreen = true)}><Maximize2 size={12} /></button
title="Open in drawer"
class="border bg-surface absolute hover:text-primary top-0 right-2 p-0.5 text-secondary"
on:click={() => (fullscreen = true)}><Maximize2 size={10} /></button
>
{#if focus}
<div class="relative w-full">
@@ -0,0 +1,16 @@
<script lang="ts">
export let notificationCount = 0
export let notificationLimit: number | undefined = undefined
</script>
{#if notificationCount > 0}
<div
class="bg-red-500 text-white text-[0.6rem] rounded-md w-5 h-5 flex items-center justify-center"
>
{#if notificationLimit && notificationCount > notificationLimit}
{`${notificationLimit}+`}
{:else}
{notificationCount}
{/if}
</div>
{/if}
@@ -0,0 +1,34 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { RefreshCw } from 'lucide-svelte'
import Popover from '$lib/components/Popover.svelte'
export let loading: boolean
let buttonHover = false
</script>
<Popover>
<Button
on:mouseenter={() => (buttonHover = true)}
on:mouseleave={() => (buttonHover = false)}
color="light"
size="xs2"
variant="border"
on:click
>
<RefreshCw class={loading ? 'animate-spin ' : ''} size="14" />
</Button>
<svelte:fragment slot="text">
{#if loading}
{#if buttonHover}
Stop Refreshing
{:else}
Refreshing...
{/if}
{:else}
Refresh
{/if}
</svelte:fragment>
</Popover>
@@ -0,0 +1,38 @@
<script lang="ts">
export let horizontal: boolean = false
export let gap: 'none' | 'sm' | 'md' | 'lg' = 'sm'
export let justify: 'start' | 'center' | 'end' | 'between' = 'start'
export let wFull = true
const gapMap = {
none: '',
sm: 'gap-2',
md: 'gap-4',
lg: 'gap-8'
}
const justifyMap = {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
between: 'justify-between'
}
</script>
{#if horizontal}
<div
class="flex flex-row h-full {wFull ? 'w-full' : ''} {gapMap[gap]} items-center {justifyMap[
justify
]}"
>
<slot />
</div>
{:else}
<div
class="flex flex-col h-full {wFull ? 'w-full' : ''} {gapMap[gap]} items-center {justifyMap[
justify
]}"
>
<slot />
</div>
{/if}
@@ -0,0 +1,3 @@
<div class="w-full flex-grow bg-blue-300">
<slot />
</div>
@@ -70,10 +70,12 @@
>
<div class="flex">
<div class="ml-4 text-left flex-1">
<h3 class="text-lg font-medium text-primary">
{title}
</h3>
<div class="mt-2 text-sm text-tertiary">
<div class="flex flex-row items-center justify-between">
<h3>{title}</h3>
<slot name="settings" />
</div>
<div class="mt-4 text-sm text-tertiary">
<slot />
</div>
</div>
@@ -0,0 +1,110 @@
<script lang="ts">
import Portal from '$lib/components/Portal.svelte'
import { twMerge } from 'tailwind-merge'
import { clickOutside } from '$lib/utils'
import { X } from 'lucide-svelte'
import List from '$lib/components/common/layout/List.svelte'
import { fade } from 'svelte/transition'
export let title: string
export let css: any = {}
export let target: string = ''
export let isOpen = false
export let fixedSize: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' = 'md'
// Add size mapping with custom pixel values
const sizeStyles = {
xs: { width: '400px', height: '250px' },
sm: { width: '600px', height: '400px' },
md: { width: '800px', height: '500px' },
lg: { width: '1400px', height: '720px' },
xl: { width: '1600px', height: '800px' },
xxl: { width: '1600px', height: '1000px' }
}
export function close() {
isOpen = false
}
export function open() {
isOpen = true
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
close()
}
}
function fadeFast(node: HTMLElement) {
return fade(node, { duration: 200 })
}
</script>
<svelte:window on:keydown={handleKeyDown} />
{#if isOpen}
<Portal name="always-mounted" {target}>
<div
class={'fixed top-0 bottom-0 left-0 right-0 transition-all overflow-auto z-[1100] bg-black bg-opacity-60 w-full h-full'}
transition:fadeFast|local
>
<div class="flex min-h-full items-center justify-center p-8">
<div
style={`width: ${sizeStyles[fixedSize].width}; height: ${sizeStyles[fixedSize].height}; ${
css?.popup?.style || ''
}`}
class={twMerge(
'max-h-screen-80 max-w-screen-80 rounded-lg relative bg-surface pt-2 px-4 pb-4',
css?.popup?.class,
'wm-modal-form-popup'
)}
use:clickOutside
on:click_outside={() => {
close()
}}
>
<List gap="md">
<div class="flex w-full">
<List horizontal justify="between">
<h3>{title}</h3>
<div class="grow w-min-0">
<List horizontal justify="between">
<div class="min-w-0 grow">
<slot name="header-left" />
</div>
<div class="min-w-0 grow-0 justify-end">
<List horizontal justify="end">
<slot name="header-right" />
<div class="w-8">
<button
on:click={() => {
isOpen = false
}}
class="hover:bg-surface-hover rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<X class="text-tertiary " />
</button>
</div>
</List>
</div>
</List>
</div>
</List>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="w-full flex grow min-h-0" on:click|stopPropagation={() => {}}>
<slot />
</div>
</List>
</div>
</div>
</div>
</Portal>
{/if}
@@ -17,6 +17,7 @@
export let target: string | HTMLElement | undefined = undefined
export let noTransition = false
export let popupHover = false
export let preventPopupClosingOnClickInside = false
</script>
<Popover on:close class="leading-none">
@@ -28,6 +29,7 @@
<ConditionalPortal condition={shouldUsePortal} {target}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
use:floatingContent
class={`z5000 ${floatingClasses}`}
@@ -37,6 +39,7 @@
on:mouseleave={() => {
popupHover = false
}}
on:click={(e) => preventPopupClosingOnClickInside && e.stopPropagation()}
>
{#if !noTransition}
<Transition
@@ -1,9 +1,11 @@
<script lang="ts">
import { copyToClipboard } from '$lib/utils'
import { Clipboard } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
export let content: string
export let title: string | undefined = undefined
export let size: 'sm' | 'md' = 'sm'
</script>
{#if title !== undefined}
@@ -19,6 +21,8 @@
copyToClipboard(content)
}}
>
<div class="text-xs truncate whitespace-no-wrap grow">{content}</div>
<div class={twMerge('truncate whitespace-no-wrap grow', size === 'sm' ? 'text-xs' : 'text-sm')}
>{content}</div
>
<Clipboard size={12} class="flex-shrink-0" />
</div>
@@ -17,6 +17,7 @@
import Label from '$lib/components/Label.svelte'
import TriggerTokens from '../triggers/TriggerTokens.svelte'
let userSettings: UserSettings
import Description from '$lib/components/Description.svelte'
export let token: string
export let scopes: string[] = []
@@ -79,6 +80,11 @@
/>
<div class="flex flex-col w-full gap-4">
<Description link="https://www.windmill.dev/docs/advanced/email_triggers">
Email triggers execute scripts and flows when emails are sent to specific addresses. Each
trigger has its own unique email address that can be used to invoke the script or flow.
</Description>
{#if loading}
<Skeleton layout={[[18]]} />
{:else}
@@ -121,7 +121,7 @@
By default, adding a trigger will set the schedule to 15 minutes. To see all ways to trigger
a flow, check
<a
href="https://www.windmill.dev/docs/getting_started/trigger_flows"
href="https://www.windmill.dev/docs/getting_started/triggers"
target="_blank"
class="text-blue-400">Triggering Flows</a
>.
@@ -303,6 +303,7 @@
{#if !noEditor}
{#key flowModule.id}
<Editor
folding
path={$pathStore + '/' + flowModule.id}
bind:websocketAlive
@@ -138,7 +138,7 @@
right: 'Worker group tag (queue)',
rightTooltip:
"When a worker group tag is defined at the flow level, any steps inside the flow will run on any worker group that listen to that tag, regardless of the steps tag. If no worker group tags is defined, the flow controls will be executed with the default tag 'flow' and the steps will be executed with their respective tag",
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/worker_groups'
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/worker_groups'
}}
class="py-1"
/>
@@ -185,7 +185,8 @@
}}
options={{
right: 'Cache the results for each possible inputs',
rightTooltip: 'When enabled, the flow will cache the results of the flow for each possible set of inputs.',
rightTooltip:
'When enabled, the flow will cache the results of the flow for each possible set of inputs.',
rightDocumentationLink: 'https://www.windmill.dev/docs/flows/cache#cache-flows'
}}
class="py-1"
@@ -226,7 +227,8 @@
rightTooltip:
'If the inputs meet the predefined condition, the flow will not run.' +
'to decide if the flow should stop early.',
rightDocumentationLink: 'https://www.windmill.dev/docs/flows/early_stop#early-stop-for-flow'
rightDocumentationLink:
'https://www.windmill.dev/docs/flows/early_stop#early-stop-for-flow'
}}
class="py-1"
/>
@@ -316,7 +318,8 @@
'Steps will share a folder at `./shared` in which they can store heavier data and ' +
'pass them to the next step. Beware that the `./shared` folder is not ' +
'preserved across suspends and sleeps.',
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/persistent_storage/within_windmill#shared-directory'
rightDocumentationLink:
'https://www.windmill.dev/docs/core_concepts/persistent_storage/within_windmill#shared-directory'
}}
class="py-1"
/>
@@ -339,7 +342,8 @@
right: 'Make runs invisible to others',
rightTooltip:
'When this option is enabled, manual executions of this script are invisible to users other than the user running it, including the owner(s). This setting can be overridden when this script is run manually from the advanced menu.',
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs#invisible-runs'
rightDocumentationLink:
'https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs#invisible-runs'
}}
class="py-1"
/>
@@ -365,27 +369,29 @@
<!-- Concurrency Section -->
{#if customUi?.settingsTabs?.concurrency != false}
<div>
<Toggle
textClass="font-normal text-sm"
color="nord"
size="xs"
disabled={!$enterpriseLicense}
checked={Boolean($flowStore.value.concurrent_limit)}
on:change={() => {
if ($flowStore.value.concurrent_limit) {
$flowStore.value.concurrent_limit = undefined
} else {
$flowStore.value.concurrent_limit = 1
}
}}
options={{
right: 'Concurrency limits',
rightTooltip: 'Allowed concurrency within a given timeframe',
rightDocumentationLink: 'https://www.windmill.dev/docs/flows/concurrency_limit'
}}
class="py-1"
eeOnly={true}
/>
<div class="flex flex-row items-center gap-2">
<Toggle
textClass="font-normal text-sm"
color="nord"
size="xs"
disabled={!$enterpriseLicense}
checked={Boolean($flowStore.value.concurrent_limit)}
on:change={() => {
if ($flowStore.value.concurrent_limit) {
$flowStore.value.concurrent_limit = undefined
} else {
$flowStore.value.concurrent_limit = 1
}
}}
options={{
right: 'Concurrency limits',
rightTooltip: 'Allowed concurrency within a given timeframe',
rightDocumentationLink: 'https://www.windmill.dev/docs/flows/concurrency_limit'
}}
class="py-1"
eeOnly={true}
/>
</div>
{#if $flowStore.value.concurrent_limit}
<div class="flex flex-col gap-4">
@@ -483,28 +489,30 @@
</Toggle>
<div>
<Toggle
textClass="font-normal text-sm"
color="nord"
size="xs"
disabled={!$enterpriseLicense || isCloudHosted()}
checked={Boolean($flowStore.dedicated_worker)}
on:change={() => {
if ($flowStore.dedicated_worker) {
$flowStore.dedicated_worker = undefined
} else {
$flowStore.dedicated_worker = true
}
}}
options={{
right: 'Flow is run on dedicated workers',
rightTooltip:
'When enabled, the flow will be executed on a dedicated worker.',
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/jobs#high-priority-jobs'
}}
class="py-1"
eeOnly={true}
/>
<div class="flex flex-row items-center gap-2">
<Toggle
textClass="font-normal text-sm"
color="nord"
size="xs"
disabled={!$enterpriseLicense || isCloudHosted()}
checked={Boolean($flowStore.dedicated_worker)}
on:change={() => {
if ($flowStore.dedicated_worker) {
$flowStore.dedicated_worker = undefined
} else {
$flowStore.dedicated_worker = true
}
}}
options={{
right: 'Flow is run on dedicated workers',
rightTooltip: 'When enabled, the flow will be executed on a dedicated worker.',
rightDocumentationLink:
'https://www.windmill.dev/docs/core_concepts/jobs#high-priority-jobs'
}}
class="py-1"
eeOnly={true}
/>
</div>
{#if $flowStore.dedicated_worker}
<div>
@@ -330,6 +330,8 @@
<div class="z-10 flex-auto grow bg-surface-secondary" bind:clientHeight={minHeight}>
<FlowGraphV2
earlyStop={$flowStore.value?.skip_expr !== undefined}
cache={$flowStore.value?.cache_ttl !== undefined}
triggerNode={true}
path={$pathStore}
{newFlow}
@@ -6,6 +6,9 @@
import VirtualItemWrapper from './VirtualItemWrapper.svelte'
import type { PropPickerContext } from '$lib/components/prop_picker'
import FlowPropPicker from '$lib/components/flows/propPicker/FlowPropPicker.svelte'
import Popover from '$lib/components/Popover.svelte'
import { fade } from 'svelte/transition'
import { Database, Square } from 'lucide-svelte'
export let label: string | undefined = undefined
export let bgColor: string = ''
@@ -19,6 +22,8 @@
export let inputJson: Object | undefined = undefined
export let prefix = ''
export let alwaysPluggable: boolean = false
export let cache: boolean = false
export let earlyStop: boolean = false
const { currentStepStore: copilotCurrentStepStore } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
@@ -62,7 +67,30 @@
</div>
{/if}
</div>
<div class="absolute text-sm right-12 -bottom-3 flex flex-row gap-1 z-10">
{#if cache}
<Popover notClickable>
<div
transition:fade|local={{ duration: 200 }}
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
>
<Database size={12} />
</div>
<svelte:fragment slot="text">Cached</svelte:fragment>
</Popover>
{/if}
{#if earlyStop}
<Popover notClickable>
<div
transition:fade|local={{ duration: 200 }}
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
>
<Square size={12} />
</div>
<svelte:fragment slot="text">Early stop if condition met</svelte:fragment>
</Popover>
{/if}
</div>
{#if inputJson && $flowPropPickerConfig && (Object.keys(inputJson).length > 0 || alwaysPluggable)}
<div class="absolute -bottom-[14px] right-[21px] translate-x-[50%] center-center">
<FlowPropPicker json={inputJson} {prefix} />
@@ -57,6 +57,8 @@
export let newFlow: boolean = false
export let insertable = false
export let earlyStop: boolean = false
export let cache: boolean = false
export let scroll = false
export let moving: string | undefined = undefined
@@ -238,7 +240,9 @@
flowModuleStates,
selectedId: $selectedId,
path,
newFlow
newFlow,
cache,
earlyStop
},
failureModule,
preprocessorModule,
@@ -18,6 +18,8 @@
index: number
disableAi: boolean
disableMoveIds: string[]
cache: boolean
earlyStop: boolean
}
const { selectedId } = getContext<{
@@ -83,5 +85,7 @@
inputJson={filteredInput}
prefix="flow_input"
alwaysPluggable
cache={data.cache}
earlyStop={data.earlyStop}
/>
</NodeWrapper>
@@ -33,7 +33,7 @@
import ContentSearchInner from '../ContentSearchInner.svelte'
import { goto } from '$app/navigation'
import QuickMenuItem from '../search/QuickMenuItem.svelte'
import { enterpriseLicense, superadmin, workspaceStore } from '$lib/stores'
import { devopsRole, enterpriseLicense, workspaceStore } from '$lib/stores'
import uFuzzy from '@leeoniya/ufuzzy'
import BarsStaggered from '../icons/BarsStaggered.svelte'
import { scroll_into_view_if_needed_polyfill } from '../multiselect/utils'
@@ -244,7 +244,7 @@
queryParseErrors = searchResults.query_parse_errors
indexMetadata = searchResults.index_metadata
} catch (e) {
sendUserToast(e, true)
sendUserToast(e.body, true)
}
loadingCompletedRuns = false
selectedItem = selectItem(0)
@@ -610,7 +610,7 @@
/>
{:else if tab === 'logs'}
<div class="p-2">
{#if !$superadmin}
{#if !$devopsRole}
<Alert title="Service logs are only available to superadmins" type="warning">
Service logs are only available to superadmins
</Alert>
@@ -1,15 +1,28 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import CriticalAlertModalInner from './CriticalAlertModalInner.svelte'
import { SettingService } from '$lib/gen'
import { SettingService, type CriticalAlert } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import { workspaceStore, isCriticalAlertsUIOpen, devopsRole } from '$lib/stores'
import Modal from '../common/modal/Modal.svelte'
import {
workspaceStore,
isCriticalAlertsUIOpen,
devopsRole,
userStore,
superadmin
} from '$lib/stores'
import Modal2 from '../common/modal/Modal2.svelte'
import { Button, Popup } from '$lib/components/common'
import List from '$lib/components/common/layout/List.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { BellOff, Bell, ExternalLink, Settings } from 'lucide-svelte'
import { base } from '$lib/base'
import Notification from '$lib/components/common/alert/Notification.svelte'
export let open: boolean = false
export let numUnacknowledgedCriticalAlerts: number = 0
export let muteSettings
let workspaceContext = false
let childRef
$: {
setupApiFunctions(workspaceContext)
@@ -68,15 +81,50 @@
clearInterval(checkForNewAlertsInterval)
})
async function saveWorkSpaceMuteSetting() {
await SettingService.workspaceMuteCriticalAlertsUi({
workspace: $workspaceStore!,
requestBody: {
mute_critical_alerts: muteSettings.workspace
}
})
sendUserToast(
`Critical alert UI mute settings changed.\nPlease reload page for UI changes to take effect.`
)
childRef.refreshAlerts()
}
async function saveGlobalMuteSetting() {
await SettingService.setGlobal({
key: 'critical_alert_mute_ui',
requestBody: { value: muteSettings.global }
})
sendUserToast(
`Critical alert UI mute settings changed.\nPlease reload page for UI changes to take effect.`
)
childRef.refreshAlerts()
}
async function updateHasUnacknowledgedCriticalAlerts(sendToast: boolean = false) {
if (checkingForNewAlerts) return
checkingForNewAlerts = true
try {
const unacknowledged = await getCriticalAlerts({
const params = {
page: 1,
pageSize: 10,
pageSize: 1000,
acknowledged: false
})
}
let unacknowledged: CriticalAlert[] = []
if (!$devopsRole && $workspaceStore) {
const res = await SettingService.workspaceGetCriticalAlerts({
...params,
workspace: $workspaceStore
})
unacknowledged = res.alerts ?? []
} else {
const res = await SettingService.getCriticalAlerts(params)
unacknowledged = res.alerts ?? []
}
if (
numUnacknowledgedCriticalAlerts === 0 &&
unacknowledged.length > 0 &&
@@ -116,14 +164,123 @@
}
</script>
<Modal bind:open title="Critical Alerts" cancelText="Close" style="max-width: 66%;">
<Modal2 bind:isOpen={open} title="Critical Alerts" target="#content" fixedSize="lg">
<svelte:fragment slot="header-left">
<Notification notificationCount={numUnacknowledgedCriticalAlerts} notificationLimit={9999} />
</svelte:fragment>
<svelte:fragment slot="header-right">
<List horizontal>
{#if $superadmin || $userStore?.is_admin}
<Popup
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
target="#mute-settings-button"
preventPopupClosingOnClickInside={true}
>
<svelte:fragment slot="button">
<div id="mute-settings-button">
<Button variant="border" color="light" nonCaptureEvent>
{#if muteSettings.global || muteSettings.workspace}
<BellOff size="16" />
{:else}
<Bell size="16" />
{/if}
</Button>
</div>
</svelte:fragment>
<List justify="start">
<div class="w-full">
{#if $superadmin}
<Toggle
on:change={saveGlobalMuteSetting}
bind:checked={muteSettings.global}
options={{
right: 'Automatically acknowledge critical alerts instance wide'
}}
size="xs"
stopPropagation={true}
/>
{/if}
</div>
<div class="w-full">
<Toggle
on:change={saveWorkSpaceMuteSetting}
bind:checked={muteSettings.workspace}
options={{
right: 'Automatically acknowledge critical alerts for current workspace'
}}
size="xs"
stopPropagation={true}
/>
</div>
</List>
</Popup>
{/if}
{#if $superadmin}
<Popup
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
target="#settings-button"
>
<svelte:fragment slot="button">
<div id="settings-button">
<Button variant="border" color="light" nonCaptureEvent>
<Settings size="16" />
</Button>
</div>
</svelte:fragment>
<List justify="start" gap="none">
<div class="w-full">
<Button
size="xs"
color="light"
href="{base}/?workspace=admins#superadmin-settings"
target="_blank"
>
<div class="w-full">
<List horizontal justify="between" gap="sm">
<div>Instance Critical Alert Settings</div>
<ExternalLink size="16" />
</List>
</div>
</Button>
</div>
<div class="w-full">
<Button
size="xs"
color="light"
href="{base}/workspace_settings?tab=error_handler"
target="_blank"
>
Workspace Critical Alert Settings <ExternalLink size="16" />
</Button>
</div>
</List>
</Popup>
{:else}
<Button
size="xs"
color="light"
variant="border"
href="{base}/workspace_settings?tab=error_handler"
target="_blank"
>
<List horizontal justify="between" gap="sm">
<Settings size="16" />
<ExternalLink size="16" />
</List>
</Button>
{/if}
</List>
</svelte:fragment>
<CriticalAlertModalInner
bind:workspaceContext
{numUnacknowledgedCriticalAlerts}
{updateHasUnacknowledgedCriticalAlerts}
{getCriticalAlerts}
{acknowledgeCriticalAlert}
{acknowledgeAllCriticalAlerts}
{muteSettings}
bind:workspaceContext
bind:this={childRef}
/>
</Modal>
</Modal2>

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