mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-25 16:03:47 +00:00
Merge branch 'main' into python-uv
This commit is contained in:
@@ -1,5 +1,64 @@
|
||||
# Changelog
|
||||
|
||||
## [1.414.1](https://github.com/windmill-labs/windmill/compare/v1.414.0...v1.414.1) (2024-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** enable text selection on aggrid tables by default ([b0b9180](https://github.com/windmill-labs/windmill/commit/b0b9180fb907c92b95a48ff286eb1dae59bb4981))
|
||||
* **apps:** public apps can take full height ([703db7d](https://github.com/windmill-labs/windmill/commit/703db7d4412795b4323a2eefcc39ae3cf43bc748))
|
||||
* **bun:** handle bun lockfile created with windows ([#4602](https://github.com/windmill-labs/windmill/issues/4602)) ([dcf5e2f](https://github.com/windmill-labs/windmill/commit/dcf5e2f03f977e241f6785530dad61a70c5bdd79))
|
||||
* **frontend:** make script and schema scrollable on script detail page ([6e222b3](https://github.com/windmill-labs/windmill/commit/6e222b3b1a419e5543fdf35420a7413699688b41))
|
||||
* **frontend:** new approval steps default to timeout 1800 ([b86de62](https://github.com/windmill-labs/windmill/commit/b86de6280e03e014e8ddf85b2b5f8fd030d0467a))
|
||||
|
||||
## [1.414.0](https://github.com/windmill-labs/windmill/compare/v1.413.2...v1.414.0) (2024-10-29)
|
||||
|
||||
* Issue with previous release, re-releasing
|
||||
|
||||
## [1.413.2](https://github.com/windmill-labs/windmill/compare/v1.413.1...v1.413.2) (2024-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** in flows, workspace scripts should use their set tags instead of the default one ([5b7c6d7](https://github.com/windmill-labs/windmill/commit/5b7c6d7d62dcfd09fec374e781bdf5c5bafe4a9d))
|
||||
* **cli:** fix wmill instance pull --instance ([3c62f5e](https://github.com/windmill-labs/windmill/commit/3c62f5ea83d1da8bd3705468969d56e2fe680751))
|
||||
* **frontend:** fix script and flow renaming ([d743e00](https://github.com/windmill-labs/windmill/commit/d743e0056353a4fca445a7089e3afc1fd4e8c219))
|
||||
|
||||
## [1.413.1](https://github.com/windmill-labs/windmill/compare/v1.413.0...v1.413.1) (2024-10-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** fix wmill instance push --base-url and --instance ([8298710](https://github.com/windmill-labs/windmill/commit/82987105a6fd6ec272c170fb094453a0267143be))
|
||||
|
||||
## [1.413.0](https://github.com/windmill-labs/windmill/compare/v1.412.0...v1.413.0) (2024-10-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* autoscaling v0 ([#4593](https://github.com/windmill-labs/windmill/issues/4593)) ([fe7d044](https://github.com/windmill-labs/windmill/commit/fe7d044a66e8ec223a337cb704d3e58942dd1502))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add run immediately popover to run again ([e54d253](https://github.com/windmill-labs/windmill/commit/e54d25368541dc6109a0f99022a120d28455f9bd))
|
||||
* **docs:** smtp setup documentation link ([#4590](https://github.com/windmill-labs/windmill/issues/4590)) ([bac3205](https://github.com/windmill-labs/windmill/commit/bac32057259d893140d649c9dfec2ca75e395ad4))
|
||||
|
||||
## [1.412.0](https://github.com/windmill-labs/windmill/compare/v1.411.1...v1.412.0) (2024-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Spotify oauth provider ([#4581](https://github.com/windmill-labs/windmill/issues/4581)) ([a46aa64](https://github.com/windmill-labs/windmill/commit/a46aa644b096e71e75b59507224ed92f7d2f99ba))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **app builder:** date input default value improvements ([9f43d5d](https://github.com/windmill-labs/windmill/commit/9f43d5dcd92ddcd0c0baeaa5271779520af652f4))
|
||||
* **bash:** correctly propagate sigterm for cancelled bash scripts ([134cfdb](https://github.com/windmill-labs/windmill/commit/134cfdb30eb8d29c2ecb1b78b8fabae2b6e10700))
|
||||
* do not update created_at of scripts on lockfile generation ([d1a28eb](https://github.com/windmill-labs/windmill/commit/d1a28eb7cac5f465e2b07f870a507b0cc5cc722a))
|
||||
* initialize empty smtp settings correctly ([84e0524](https://github.com/windmill-labs/windmill/commit/84e05249505c3d2f7eb7a4917f6bca503df81017))
|
||||
|
||||
## [1.411.1](https://github.com/windmill-labs/windmill/compare/v1.411.0...v1.411.1) (2024-10-22)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ ENV GO_PATH=/usr/local/go/bin/go
|
||||
RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.4.18/uv-installer.sh | sh && mv /root/.cargo/bin/uv /usr/local/bin/uv
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -
|
||||
RUN apt-get -y update && apt-get install -y curl nodejs awscli && apt-get clean \
|
||||
RUN apt-get -y update && apt-get install -y curl procps nodejs awscli && apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# go build is slower the first time it is ran, so we prewarm it in the build
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT tag, count(*) as count FROM queue WHERE\n scheduled_for <= now() - ('3 seconds')::interval AND running = false\n GROUP BY tag",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "02b516dac764662194db1bc33e365c01f40bae70af3683f1f09748f6020f0d49"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT created_at FROM metrics WHERE id = 'telemetry' ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "19f0abd79372698f378cb6deea3ee6d098a2758d16ede000809bd9a09660b604"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email, login_type::text, verified, super_admin, name, company, username from password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2",
|
||||
"query": "SELECT email, login_type::text, verified, super_admin, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -37,6 +37,11 @@
|
||||
"ordinal": 6,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -52,8 +57,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c2060e8cacef6c3b5ce51ed203a2dbafc18d66f2924d1fe518c6728997647db2"
|
||||
"hash": "1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO autoscaling_event (worker_group, event_type, desired_workers, reason) VALUES ($1, $2, $3, $4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "autoscaling_event_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"full_scaleout",
|
||||
"scalein",
|
||||
"scaleout"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int4",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1f5f0858909eb5bac63c4e3b1add95226bd94ca3facb92a620ffa59dacad6705"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username FROM password WHERE email = $1",
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -37,6 +37,11 @@
|
||||
"ordinal": 6,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -51,8 +56,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8813665f8adfcab0daefbac2cc6b50e427dfd1c12d12895451affd307dc59c37"
|
||||
"hash": "2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542"
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC\n LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "login_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "super_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef"
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT event_type::AUTOSCALING_EVENT_TYPE AS \"event_type: _\", EXTRACT(EPOCH FROM (NOW() - applied_at))::int as seconds_ago FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "event_type: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "autoscaling_event_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"full_scaleout",
|
||||
"scalein",
|
||||
"scaleout"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "seconds_ago",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "4eca060026a0cb19c5794cd56ace89fc04765191f251945d14bbe78718714f6e"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT pg_try_advisory_xact_lock($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "pg_try_advisory_xact_lock",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f"
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, worker_group, event_type::text, desired_workers, reason, applied_at FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT 5",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "worker_group",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "event_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "desired_workers",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "applied_at",
|
||||
"type_info": "Timestamp"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6d134b137ae81534e145fc5b6474cf963ee26a3ad3a0a3d8dc064cb14c8fd9a6"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT created_at FROM metrics WHERE id = 'license_key_renewal' ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9ed77e78e6295c62745ac3ac3b7e5f544f654d7cddc13acb5d3f4fdc12a8875f"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE script SET lock = $1, created_at = now() WHERE hash = $2 AND workspace_id = $3",
|
||||
"query": "UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -12,5 +12,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ec47955683d811b12e82ff3e6aeafa0df46a320c436bad5eb1acc127df138a61"
|
||||
"hash": "d697b7311430e7bd5375ec5494179f4071e3bfe123d9600249cfa80c1103edd8"
|
||||
}
|
||||
Generated
+185
-168
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.411.1"
|
||||
version = "1.414.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -13,6 +13,7 @@ members = [
|
||||
"./windmill-common",
|
||||
"./windmill-audit",
|
||||
"./windmill-git-sync",
|
||||
"./windmill-autoscaling",
|
||||
"./windmill-indexer",
|
||||
"./windmill-macros",
|
||||
"./parsers/windmill-parser",
|
||||
@@ -28,7 +29,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.411.1"
|
||||
version = "1.414.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -45,7 +46,7 @@ lto = "thin"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise", "windmill-indexer/enterprise"]
|
||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise", "windmill-indexer/enterprise"]
|
||||
enterprise_saml = ["windmill-api/enterprise_saml"]
|
||||
stripe = ["windmill-api/stripe"]
|
||||
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark", "windmill-common/benchmark"]
|
||||
@@ -72,6 +73,7 @@ windmill-git-sync.workspace = true
|
||||
windmill-api = { workspace = true, default-features = false }
|
||||
windmill-worker.workspace = true
|
||||
windmill-indexer = { workspace = true, optional = true }
|
||||
windmill-autoscaling = { workspace = true, optional = true }
|
||||
futures.workspace = true
|
||||
tracing.workspace = true
|
||||
sqlx.workspace = true
|
||||
@@ -116,6 +118,7 @@ windmill-worker = { path = "./windmill-worker" }
|
||||
windmill-common = { path = "./windmill-common", default-features = false }
|
||||
windmill-audit = { path = "./windmill-audit" }
|
||||
windmill-git-sync = { path = "./windmill-git-sync" }
|
||||
windmill-autoscaling = { path = "./windmill-autoscaling" }
|
||||
windmill-indexer = {path = "./windmill-indexer"}
|
||||
windmill-macros = {path = "./windmill-macros"}
|
||||
windmill-parser = { path = "./parsers/windmill-parser" }
|
||||
|
||||
@@ -1 +1 @@
|
||||
d61c163e0a311ecd86d1398aff883eaea8d0b09a
|
||||
f667fcebdb5a2d1c22c1928d6a316ba57bb43d72
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE autoscaling_event;
|
||||
DROP TYPE autoscaling_event_type;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Add up migration script here
|
||||
CREATE TYPE AUTOSCALING_EVENT_TYPE AS ENUM ('full_scaleout', 'scalein', 'scaleout');
|
||||
|
||||
CREATE TABLE autoscaling_event (
|
||||
id SERIAL PRIMARY KEY,
|
||||
worker_group TEXT NOT NULL,
|
||||
event_type AUTOSCALING_EVENT_TYPE NOT NULL,
|
||||
desired_workers INTEGER NOT NULL,
|
||||
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
reason TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX autoscaling_event_worker_group_idx ON autoscaling_event (worker_group, applied_at);
|
||||
@@ -152,5 +152,24 @@
|
||||
"vismanet_erp_interactive_api:read",
|
||||
"vismanet_erp_interactive_api:update"
|
||||
]
|
||||
},
|
||||
"spotify": {
|
||||
"auth_url": "https://accounts.spotify.com/authorize",
|
||||
"token_url": "https://accounts.spotify.com/api/token",
|
||||
"scopes": [
|
||||
"user-read-playback-state",
|
||||
"user-modify-playback-state",
|
||||
"user-read-currently-playing",
|
||||
"playlist-read-private",
|
||||
"playlist-read-collaborative",
|
||||
"playlist-modify-private",
|
||||
"playlist-modify-public",
|
||||
"user-follow-read",
|
||||
"user-read-playback-position",
|
||||
"user-read-recently-played",
|
||||
"user-top-read",
|
||||
"user-library-modify",
|
||||
"user-library-read"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -188,9 +188,15 @@ pub struct AnsiblePlaybookOptions {
|
||||
pub force_handlers: Option<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ResourceOrVariablePath {
|
||||
Resource(String),
|
||||
Variable(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileResource {
|
||||
pub resource_path: String,
|
||||
pub resource_path: ResourceOrVariablePath,
|
||||
pub target_path: String,
|
||||
}
|
||||
|
||||
@@ -309,7 +315,7 @@ pub fn parse_ansible_reqs(
|
||||
}
|
||||
}
|
||||
}
|
||||
Yaml::String(key) if key == "file_resources" => {
|
||||
Yaml::String(key) if key == "files" || key == "file_resources" => {
|
||||
if let Yaml::Array(file_resources) = value {
|
||||
let resources: anyhow::Result<Vec<FileResource>> =
|
||||
file_resources.iter().map(parse_file_resource).collect();
|
||||
@@ -440,7 +446,24 @@ fn parse_file_resource(yaml: &Yaml) -> anyhow::Result<FileResource> {
|
||||
"No `target` provided for file resource {}. Please input a target relative path for the ansible playbook to see this file.",
|
||||
resource_path
|
||||
))?;
|
||||
return Ok(FileResource { resource_path: resource_path.clone(), target_path });
|
||||
return Ok(FileResource {
|
||||
resource_path: ResourceOrVariablePath::Resource(resource_path.clone()),
|
||||
target_path,
|
||||
});
|
||||
}
|
||||
if let Some(Yaml::String(resource_path)) = f.get(&Yaml::String("variable".to_string())) {
|
||||
let target_path = f
|
||||
.get(&Yaml::String("target".to_string()))
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|x| x.to_string())
|
||||
.ok_or(anyhow!(
|
||||
"No `target` provided for file resource {}. Please input a target relative path for the ansible playbook to see this file.",
|
||||
resource_path
|
||||
))?;
|
||||
return Ok(FileResource {
|
||||
resource_path: ResourceOrVariablePath::Variable(resource_path.clone()),
|
||||
target_path,
|
||||
});
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"File resource should have a `resource` field, linking to a text file resource"
|
||||
|
||||
+2
-10
@@ -1,16 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::error::{Error, Result};
|
||||
|
||||
pub async fn set_license_key(_license_key: String) -> anyhow::Result<()> {
|
||||
pub async fn set_license_key(_license_key: String) -> () {
|
||||
// Implementation is not open source
|
||||
Err(anyhow!("License cannot be set in Windmill CE"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn verify_license_key() -> Result<()> {
|
||||
pub async fn verify_license_key() -> () {
|
||||
// Implementation is not open source
|
||||
Err(Error::InternalErr(
|
||||
"License always invalid in Windmill CE".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
+75
-13
@@ -27,7 +27,7 @@ use uuid::Uuid;
|
||||
use windmill_api::HTTP_CLIENT;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::ee::schedule_key_renewal;
|
||||
use windmill_common::ee::{maybe_renew_license_key_on_start, LICENSE_KEY_ID, LICENSE_KEY_VALID};
|
||||
|
||||
use windmill_common::{
|
||||
global_settings::{
|
||||
@@ -307,7 +307,8 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
Mode::Standalone
|
||||
});
|
||||
|
||||
let num_workers = if mode == Mode::Server || mode == Mode::Indexer {
|
||||
#[allow(unused_mut)]
|
||||
let mut num_workers = if mode == Mode::Server || mode == Mode::Indexer {
|
||||
0
|
||||
} else {
|
||||
std::env::var("NUM_WORKERS")
|
||||
@@ -422,6 +423,50 @@ Windmill Community Edition {GIT_VERSION}
|
||||
|
||||
display_config(&ENV_SETTINGS);
|
||||
|
||||
if let Err(e) = reload_base_url_setting(&db).await {
|
||||
tracing::error!("Error loading base url: {:?}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = reload_critical_error_channels_setting(&db).await {
|
||||
tracing::error!("Could loading critical error emails setting: {:?}", e);
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
// load the license key and check if it's valid
|
||||
// if not valid and not server mode just quit
|
||||
// if not expired and server mode then force renewal
|
||||
// if key still invalid and num_workers > 0, set to 0
|
||||
if let Err(err) = reload_license_key(&db).await {
|
||||
tracing::error!("Failed to reload license key: {err:#}");
|
||||
}
|
||||
let valid_key = *LICENSE_KEY_VALID.read().await;
|
||||
if !valid_key && !server_mode {
|
||||
panic!("Invalid license key, workers require a valid license key");
|
||||
}
|
||||
if server_mode {
|
||||
// only force renewal if invalid but not empty (= expired)
|
||||
let renewed_now = maybe_renew_license_key_on_start(
|
||||
&HTTP_CLIENT,
|
||||
&db,
|
||||
!valid_key && !LICENSE_KEY_ID.read().await.is_empty(),
|
||||
)
|
||||
.await;
|
||||
if renewed_now {
|
||||
if let Err(err) = reload_license_key(&db).await {
|
||||
tracing::error!("Failed to reload license key: {err:#}");
|
||||
}
|
||||
}
|
||||
if num_workers > 0 {
|
||||
let valid_key = *LICENSE_KEY_VALID.read().await;
|
||||
if !valid_key {
|
||||
tracing::warn!("License key invalid, setting num_workers to 0");
|
||||
num_workers = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let worker_mode = num_workers > 0;
|
||||
|
||||
if server_mode || worker_mode || indexer_mode {
|
||||
@@ -448,7 +493,16 @@ Windmill Community Edition {GIT_VERSION}
|
||||
|
||||
initial_load(&db, killpill_tx.clone(), worker_mode, server_mode, is_agent).await;
|
||||
|
||||
monitor_db(&db, &base_internal_url, rsmq.clone(), server_mode, true).await;
|
||||
monitor_db(
|
||||
&db,
|
||||
&base_internal_url,
|
||||
rsmq.clone(),
|
||||
server_mode,
|
||||
worker_mode,
|
||||
true,
|
||||
killpill_tx.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
monitor_pool(&db).await;
|
||||
|
||||
@@ -548,8 +602,11 @@ Windmill Community Edition {GIT_VERSION}
|
||||
rx.recv().await?;
|
||||
}
|
||||
}
|
||||
tracing::info!("Starting phase 2 of shutdown");
|
||||
killpill_phase2_tx.send(())?;
|
||||
if killpill_phase2_tx.receiver_count() > 0 {
|
||||
tracing::info!("Starting phase 2 of shutdown");
|
||||
killpill_phase2_tx.send(())?;
|
||||
tracing::info!("Phase 2 of shutdown completed");
|
||||
}
|
||||
Ok(()) as anyhow::Result<()>
|
||||
};
|
||||
|
||||
@@ -575,7 +632,9 @@ Windmill Community Edition {GIT_VERSION}
|
||||
&base_internal_url,
|
||||
rsmq.clone(),
|
||||
server_mode,
|
||||
false
|
||||
worker_mode,
|
||||
false,
|
||||
tx.clone(),
|
||||
)
|
||||
.await;
|
||||
},
|
||||
@@ -618,7 +677,15 @@ Windmill Community Edition {GIT_VERSION}
|
||||
},
|
||||
LICENSE_KEY_SETTING => {
|
||||
if let Err(e) = reload_license_key(&db).await {
|
||||
tracing::error!(error = %e, "Could not reload license key setting");
|
||||
tracing::error!("Failed to reload license key: {e:#}");
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
if worker_mode {
|
||||
let valid_key = *LICENSE_KEY_VALID.read().await;
|
||||
if !valid_key {
|
||||
tracing::error!("Invalid license key, exiting...");
|
||||
tx.send(()).expect("send");
|
||||
}
|
||||
}
|
||||
},
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING => {
|
||||
@@ -764,15 +831,10 @@ Windmill Community Edition {GIT_VERSION}
|
||||
Ok(()) as anyhow::Result<()>
|
||||
};
|
||||
|
||||
if mode == Mode::Server || mode == Mode::Standalone {
|
||||
if server_mode {
|
||||
schedule_stats(&db, &HTTP_CLIENT).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
if mode == Mode::Server || mode == Mode::Standalone {
|
||||
schedule_key_renewal(&HTTP_CLIENT, &db).await;
|
||||
}
|
||||
|
||||
futures::try_join!(
|
||||
shutdown_signal,
|
||||
workers_f,
|
||||
|
||||
+29
-42
@@ -27,7 +27,7 @@ use windmill_api::{
|
||||
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
|
||||
};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::ee::{worker_groups_alerts, jobs_waiting_alerts};
|
||||
use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts, LICENSE_KEY_VALID};
|
||||
use windmill_common::{
|
||||
auth::JWT_SECRET,
|
||||
ee::CriticalErrorChannel,
|
||||
@@ -76,9 +76,6 @@ use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::ee::verify_license_key;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::ee::LICENSE_KEY_VALID;
|
||||
|
||||
use crate::ee::set_license_key;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -152,18 +149,10 @@ pub async fn initial_load(
|
||||
tracing::error!("Error reloading custom tags: {:?}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = reload_base_url_setting(db).await {
|
||||
tracing::error!("Error reloading base url: {:?}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = reload_hub_base_url_setting(db, server_mode).await {
|
||||
tracing::error!("Error reloading hub base url: {:?}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = reload_critical_error_channels_setting(&db).await {
|
||||
tracing::error!("Could not reload critical error emails setting: {:?}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = reload_jwt_secret_setting(&db).await {
|
||||
tracing::error!("Could not reload jwt secret setting: {:?}", e);
|
||||
}
|
||||
@@ -182,11 +171,6 @@ pub async fn initial_load(
|
||||
reload_scim_token_setting(&db).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
if let Err(e) = reload_license_key(&db).await {
|
||||
tracing::error!("Error reloading license key: {:?}", e)
|
||||
}
|
||||
|
||||
if worker_mode {
|
||||
reload_extra_pip_index_url_setting(&db).await;
|
||||
reload_pip_index_url_setting(&db).await;
|
||||
@@ -854,8 +838,10 @@ pub async fn reload_request_size(db: &DB) {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_license_key(db: &DB) -> error::Result<()> {
|
||||
let q = load_value_from_global_settings(db, LICENSE_KEY_SETTING).await?;
|
||||
pub async fn reload_license_key(db: &DB) -> anyhow::Result<()> {
|
||||
let q = load_value_from_global_settings(db, LICENSE_KEY_SETTING)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("Error reloading license key: {}", err.to_string()))?;
|
||||
|
||||
let mut value = std::env::var("LICENSE_KEY")
|
||||
.ok()
|
||||
@@ -873,9 +859,7 @@ pub async fn reload_license_key(db: &DB) -> error::Result<()> {
|
||||
tracing::error!("Could not parse LICENSE_KEY found: {:#?}", &q);
|
||||
}
|
||||
};
|
||||
|
||||
set_license_key(value).await?;
|
||||
|
||||
set_license_key(value).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1014,7 +998,9 @@ pub async fn monitor_db(
|
||||
base_internal_url: &str,
|
||||
rsmq: Option<MultiplexedRsmq>,
|
||||
server_mode: bool,
|
||||
_worker_mode: bool,
|
||||
initial_load: bool,
|
||||
_killpill_tx: tokio::sync::broadcast::Sender<()>,
|
||||
) {
|
||||
let zombie_jobs_f = async {
|
||||
if server_mode && !initial_load {
|
||||
@@ -1035,15 +1021,14 @@ pub async fn monitor_db(
|
||||
|
||||
let verify_license_key_f = async {
|
||||
#[cfg(feature = "enterprise")]
|
||||
if let Err(e) = verify_license_key().await {
|
||||
tracing::error!("Error verifying license key: {:?}", e);
|
||||
let mut l = LICENSE_KEY_VALID.write().await;
|
||||
*l = false;
|
||||
} else {
|
||||
let is_valid = LICENSE_KEY_VALID.read().await.clone();
|
||||
if !is_valid {
|
||||
let mut l = LICENSE_KEY_VALID.write().await;
|
||||
*l = true;
|
||||
if !initial_load {
|
||||
verify_license_key().await;
|
||||
if _worker_mode {
|
||||
let valid_key = *LICENSE_KEY_VALID.read().await;
|
||||
if !valid_key {
|
||||
tracing::error!("Invalid license key, exiting...");
|
||||
_killpill_tx.send(()).expect("send");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1068,6 +1053,15 @@ pub async fn monitor_db(
|
||||
}
|
||||
};
|
||||
|
||||
let apply_autoscaling_f = async {
|
||||
#[cfg(feature = "enterprise")]
|
||||
if server_mode && !initial_load {
|
||||
if let Err(e) = windmill_autoscaling::apply_all_autoscaling(db).await {
|
||||
tracing::error!("Error applying autoscaling: {:?}", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
join!(
|
||||
expired_items_f,
|
||||
zombie_jobs_f,
|
||||
@@ -1075,6 +1069,7 @@ pub async fn monitor_db(
|
||||
verify_license_key_f,
|
||||
worker_groups_alerts_f,
|
||||
jobs_waiting_alerts_f,
|
||||
apply_autoscaling_f,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1092,19 +1087,11 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
|
||||
.unwrap_or(true);
|
||||
|
||||
if metrics_enabled || save_metrics {
|
||||
let queue_counts = sqlx::query!(
|
||||
"SELECT tag, count(*) as count FROM queue WHERE
|
||||
scheduled_for <= now() - ('3 seconds')::interval AND running = false
|
||||
GROUP BY tag"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_else(|| vec![]);
|
||||
let queue_counts = windmill_common::queue::get_queue_counts(db).await;
|
||||
|
||||
for q in queue_counts {
|
||||
let count = q.count.unwrap_or(0);
|
||||
let tag = q.tag;
|
||||
let count = q.1;
|
||||
let tag = q.0;
|
||||
if metrics_enabled {
|
||||
let metric = (*QUEUE_COUNT).with_label_values(&[&tag]);
|
||||
metric.set(count as i64);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.411.1
|
||||
version: 1.414.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -1467,6 +1467,11 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: active_only
|
||||
in: query
|
||||
description: filter only active users
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: user
|
||||
@@ -8198,6 +8203,29 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/Config"
|
||||
|
||||
/configs/list_autoscaling_events/{worker_group}:
|
||||
get:
|
||||
summary: List autoscaling events
|
||||
operationId: listAutoscalingEvents
|
||||
tags:
|
||||
- config
|
||||
parameters:
|
||||
- name: worker_group
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: List of autoscaling events
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AutoscalingEvent"
|
||||
|
||||
|
||||
/w/{workspace}/acls/get/{kind}/{path}:
|
||||
get:
|
||||
summary: get granular acls
|
||||
@@ -11768,6 +11796,8 @@ components:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
operator_only:
|
||||
type: boolean
|
||||
|
||||
required:
|
||||
- email
|
||||
@@ -12469,3 +12499,21 @@ components:
|
||||
properties:
|
||||
dancer:
|
||||
type: string
|
||||
|
||||
AutoscalingEvent:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
worker_group:
|
||||
type: string
|
||||
event_type:
|
||||
type: string
|
||||
desired_workers:
|
||||
type: integer
|
||||
reason:
|
||||
type: string
|
||||
applied_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
@@ -29,6 +29,10 @@ pub fn global_service() -> Router {
|
||||
.route("/update/:name", post(update_config).delete(delete_config))
|
||||
.route("/get/:name", get(get_config))
|
||||
.route("/list", get(list_configs))
|
||||
.route(
|
||||
"/list_autoscaling_events/:worker_group",
|
||||
get(list_autoscaling_events),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow)]
|
||||
@@ -177,6 +181,30 @@ async fn delete_config(
|
||||
Ok(format!("Deleted config {name}"))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow)]
|
||||
struct AutoscalingEvent {
|
||||
id: i64,
|
||||
worker_group: String,
|
||||
event_type: Option<String>,
|
||||
desired_workers: i32,
|
||||
reason: Option<String>,
|
||||
applied_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
async fn list_autoscaling_events(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(worker_group): Path<String>,
|
||||
) -> error::JsonResult<Vec<AutoscalingEvent>> {
|
||||
let events = sqlx::query_as!(
|
||||
AutoscalingEvent,
|
||||
"SELECT id, worker_group, event_type::text, desired_workers, reason, applied_at FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT 5",
|
||||
worker_group
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
Ok(Json(events))
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn list_configs(
|
||||
authed: ApiAuthed,
|
||||
|
||||
@@ -173,17 +173,6 @@ pub async fn migrate(db: &DB) -> Result<(), Error> {
|
||||
let migrator = db.acquire().await?;
|
||||
let mut custom_migrator = CustomMigrator { inner: migrator };
|
||||
|
||||
if let Err(err) = fix_flow_versioning_migration(&mut custom_migrator, db).await {
|
||||
tracing::error!("Could not apply flow versioning fix migration: {err:#}");
|
||||
}
|
||||
|
||||
let db2 = db.clone();
|
||||
let _ = tokio::task::spawn(async move {
|
||||
if let Err(err) = fix_job_completed_index(&db2).await {
|
||||
tracing::error!("Could not apply job completed index fix migration: {err:#}");
|
||||
}
|
||||
});
|
||||
|
||||
match sqlx::migrate!("../migrations")
|
||||
.run_direct(&mut custom_migrator)
|
||||
.await
|
||||
@@ -199,6 +188,17 @@ pub async fn migrate(db: &DB) -> Result<(), Error> {
|
||||
Err(err) => Err(err),
|
||||
}?;
|
||||
|
||||
if let Err(err) = fix_flow_versioning_migration(&mut custom_migrator, db).await {
|
||||
tracing::error!("Could not apply flow versioning fix migration: {err:#}");
|
||||
}
|
||||
|
||||
let db2 = db.clone();
|
||||
let _ = tokio::task::spawn(async move {
|
||||
if let Err(err) = fix_job_completed_index(&db2).await {
|
||||
tracing::error!("Could not apply job completed index fix migration: {err:#}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ macro_rules! run_windmill_migration {
|
||||
.await?;
|
||||
tracing::info!("Finished applying {migration_job_name} migration");
|
||||
} else {
|
||||
tracing::info!("migration {migration_job_name} already done");
|
||||
tracing::debug!("migration {migration_job_name} already done");
|
||||
}
|
||||
|
||||
let _ = sqlx::query("SELECT pg_advisory_unlock(4242)")
|
||||
@@ -311,7 +311,7 @@ macro_rules! run_windmill_migration {
|
||||
tx.commit().await?;
|
||||
tracing::info!("released lock for {migration_job_name}");
|
||||
} else {
|
||||
tracing::info!("migration {migration_job_name} already done");
|
||||
tracing::debug!("migration {migration_job_name} already done");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<String> {
|
||||
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<(String, bool)> {
|
||||
// Implementation is not open source
|
||||
Err(anyhow!("License can't be validated in Windmill CE"))
|
||||
}
|
||||
|
||||
@@ -544,7 +544,6 @@ async fn get_latest_version(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<Option<FlowVersion>> {
|
||||
|
||||
let path = path.to_path();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
@@ -1333,7 +1332,6 @@ mod tests {
|
||||
},
|
||||
"type": "script",
|
||||
"path": "test",
|
||||
"tag_override": Option::<String>::None,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1377,14 +1375,12 @@ mod tests {
|
||||
"input_transforms": {},
|
||||
"type": "script",
|
||||
"path": "test",
|
||||
"tag_override": Option::<String>::None,
|
||||
},
|
||||
"stop_after_if": {
|
||||
"expr": "previous.isEmpty()",
|
||||
"skip_if_stopped": false
|
||||
}
|
||||
},
|
||||
"preprocessor_module": Option::<String>::None
|
||||
});
|
||||
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));
|
||||
}
|
||||
|
||||
@@ -2815,7 +2815,6 @@ pub async fn run_flow_by_path_inner(
|
||||
let flow_path = flow_path.to_path();
|
||||
check_scopes(&authed, || format!("run:flow/{flow_path}"))?;
|
||||
|
||||
|
||||
let (tag, dedicated_worker, has_preprocessor) = sqlx::query!(
|
||||
"SELECT tag, dedicated_worker, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor
|
||||
FROM flow
|
||||
@@ -2910,7 +2909,6 @@ pub async fn restart_flow(
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
check_license_key_valid().await?;
|
||||
|
||||
|
||||
let completed_job = sqlx::query_as::<_, CompletedJob>(
|
||||
"SELECT *, result->'wm_labels' as labels from completed_job WHERE id = $1 and workspace_id = $2",
|
||||
)
|
||||
@@ -3010,7 +3008,6 @@ pub async fn run_script_by_path_inner(
|
||||
|
||||
check_scopes(&authed, || format!("run:script/{script_path}"))?;
|
||||
|
||||
|
||||
let (job_payload, tag, _delete_after_use, timeout) =
|
||||
script_path_to_payload(script_path, &db, &w_id, run_query.skip_preprocessor).await?;
|
||||
let scheduled_for = run_query.get_scheduled_for(&db).await?;
|
||||
@@ -3018,7 +3015,6 @@ pub async fn run_script_by_path_inner(
|
||||
let tag = run_query.tag.clone().or(tag);
|
||||
check_tag_available_for_workspace(&w_id, &tag).await?;
|
||||
|
||||
|
||||
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
|
||||
|
||||
let (uuid, tx) = push(
|
||||
@@ -3067,7 +3063,6 @@ pub async fn run_workflow_as_code(
|
||||
Query(wkflow_query): Query<WorkflowAsCodeQuery>,
|
||||
Json(task): Json<WorkflowTask>,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
|
||||
let mut i = 1;
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
@@ -3079,23 +3074,18 @@ pub async fn run_workflow_as_code(
|
||||
check_license_key_valid().await?;
|
||||
check_tag_available_for_workspace(&w_id, &run_query.tag).await?;
|
||||
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
|
||||
let job = get_queued_job(&job_id, &w_id, &db).await?;
|
||||
|
||||
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
|
||||
let job = not_found_if_none(job, "Queued Job", &job_id.to_string())?;
|
||||
let (job_payload, tag, _delete_after_use, timeout) = match job.job_kind {
|
||||
JobKind::Preview => (
|
||||
@@ -3118,18 +3108,12 @@ pub async fn run_workflow_as_code(
|
||||
run_query.timeout,
|
||||
),
|
||||
JobKind::Script => {
|
||||
script_path_to_payload(
|
||||
job.script_path(),
|
||||
&db,
|
||||
&w_id,
|
||||
run_query.skip_preprocessor,
|
||||
)
|
||||
.await?
|
||||
script_path_to_payload(job.script_path(), &db, &w_id, run_query.skip_preprocessor)
|
||||
.await?
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!("Not supported").into()),
|
||||
};
|
||||
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
i += 1;
|
||||
@@ -3143,22 +3127,18 @@ pub async fn run_workflow_as_code(
|
||||
|
||||
let tag = run_query.tag.clone().or(tag).or(Some(job.tag));
|
||||
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
|
||||
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
|
||||
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
|
||||
let (uuid, mut tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
@@ -3185,7 +3165,6 @@ pub async fn run_workflow_as_code(
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
i += 1;
|
||||
@@ -3203,16 +3182,13 @@ pub async fn run_workflow_as_code(
|
||||
tracing::info!("Skipping update of flow status for job {job_id} in workspace {w_id}");
|
||||
}
|
||||
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
}
|
||||
@@ -3866,7 +3842,6 @@ pub async fn run_wait_result_flow_by_path_internal(
|
||||
let flow_path = flow_path.to_path();
|
||||
check_scopes(&authed, || format!("run:flow/{flow_path}"))?;
|
||||
|
||||
|
||||
let scheduled_for = run_query.get_scheduled_for(&db).await?;
|
||||
|
||||
let (tag, dedicated_worker, early_return, has_preprocessor) = sqlx::query!(
|
||||
@@ -4362,7 +4337,6 @@ async fn add_batch_jobs(
|
||||
}
|
||||
}
|
||||
"flow" => {
|
||||
|
||||
let mut uuids: Vec<Uuid> = Vec::new();
|
||||
let payload = if let Some(ref fv) = batch_info.flow_value {
|
||||
JobPayload::RawFlow { value: fv.clone(), path: None, restarted_from: None }
|
||||
@@ -4580,7 +4554,6 @@ pub async fn run_job_by_hash_inner(
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
|
||||
|
||||
let hash = script_hash.0;
|
||||
let (
|
||||
path,
|
||||
|
||||
@@ -456,9 +456,13 @@ async fn ee_license() -> &'static str {
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn ee_license() -> String {
|
||||
use windmill_common::ee::LICENSE_KEY_ID;
|
||||
use windmill_common::ee::{LICENSE_KEY_ID, LICENSE_KEY_VALID};
|
||||
|
||||
LICENSE_KEY_ID.read().await.clone()
|
||||
if *LICENSE_KEY_VALID.read().await {
|
||||
LICENSE_KEY_ID.read().await.clone()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
async fn openapi() -> &'static str {
|
||||
|
||||
@@ -163,8 +163,13 @@ pub async fn test_license_key(
|
||||
Json(TestKey { license_key }): Json<TestKey>,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
validate_license_key(license_key).await?;
|
||||
Ok("Sent test email".to_string())
|
||||
let (_, expired) = validate_license_key(license_key).await?;
|
||||
|
||||
if expired {
|
||||
Err(error::Error::BadRequest("Expired license key".to_string()))
|
||||
} else {
|
||||
Ok("Valid license key".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_local_settings(
|
||||
@@ -301,7 +306,6 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
|
||||
windmill_common::stats_ee::send_stats(
|
||||
&HTTP_CLIENT,
|
||||
&db,
|
||||
true,
|
||||
windmill_common::stats_ee::SendStatsReason::Manual,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "windmill-autoscaling"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "windmill_autoscaling"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[features]
|
||||
enterprise = ["windmill-queue/enterprise", "windmill-common/enterprise"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
uuid.workspace = true
|
||||
serde.workspace = true
|
||||
sqlx.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
windmill-common = { workspace = true, default-features = false }
|
||||
windmill-queue.workspace = true
|
||||
rsmq_async.workspace = true
|
||||
anyhow.workspace = true
|
||||
@@ -0,0 +1,6 @@
|
||||
use windmill_common::DB;
|
||||
|
||||
pub async fn apply_all_autoscaling(_db: &DB) -> anyhow::Result<()> {
|
||||
// Autoscaling is an ee feature
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod autoscaling_ee;
|
||||
pub use autoscaling_ee::*;
|
||||
@@ -48,8 +48,13 @@ pub async fn send_critical_alert(
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
|
||||
pub async fn maybe_renew_license_key_on_start(
|
||||
_http_client: &reqwest::Client,
|
||||
_db: &crate::db::DB,
|
||||
force_renew_now: bool,
|
||||
) -> bool {
|
||||
// Implementation is not open source
|
||||
force_renew_now
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
|
||||
@@ -101,6 +101,7 @@ pub struct FlowValue {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub failure_module: Option<Box<FlowModule>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub preprocessor_module: Option<Box<FlowModule>>,
|
||||
#[serde(default)]
|
||||
@@ -423,6 +424,7 @@ pub enum FlowModuleValue {
|
||||
path: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
hash: Option<ScriptHash>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tag_override: Option<String>,
|
||||
},
|
||||
Flow {
|
||||
|
||||
@@ -17,6 +17,7 @@ use scripts::ScriptLang;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
pub mod apps;
|
||||
pub mod auth;
|
||||
#[cfg(feature = "benchmark")]
|
||||
pub mod bench;
|
||||
pub mod db;
|
||||
@@ -32,9 +33,8 @@ pub mod job_s3_helpers_ee;
|
||||
pub mod jobs;
|
||||
pub mod more_serde;
|
||||
pub mod oauth2;
|
||||
pub mod queue;
|
||||
pub mod s3_helpers;
|
||||
|
||||
pub mod auth;
|
||||
pub mod schedule;
|
||||
pub mod scripts;
|
||||
pub mod server;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
pub async fn get_queue_counts(db: &Pool<Postgres>) -> HashMap<String, u32> {
|
||||
sqlx::query_as::<_, (String, i64)>(
|
||||
"SELECT tag, count(*) as count FROM queue WHERE
|
||||
scheduled_for <= now() - ('3 seconds')::interval AND running = false
|
||||
GROUP BY tag",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()
|
||||
.map(|v| v.into_iter().map(|(k, v)| (k, v as u32)).collect())
|
||||
.unwrap_or_else(|| HashMap::new())
|
||||
}
|
||||
@@ -28,7 +28,6 @@ pub enum SendStatsReason {
|
||||
pub async fn send_stats(
|
||||
_http_client: &reqwest::Client,
|
||||
_db: &DB,
|
||||
_skip_job_usage: bool,
|
||||
_reason: SendStatsReason,
|
||||
) -> Result<()> {
|
||||
// stats details are closed source
|
||||
|
||||
@@ -206,6 +206,14 @@ pub async fn get_uid<'c, E: sqlx::Executor<'c, Database = Postgres>>(db: E) -> R
|
||||
Ok(uid)
|
||||
}
|
||||
|
||||
pub fn map_string_to_number(s: &str, max_number: u64) -> u64 {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = DefaultHasher::new();
|
||||
s.hash(&mut hasher);
|
||||
hasher.finish() % (max_number + 1)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Mode {
|
||||
|
||||
@@ -23,7 +23,7 @@ use windmill_common::{
|
||||
jobs::QueuedJob,
|
||||
worker::{to_raw_value, write_file, write_file_at_user_defined_location, WORKER_CONFIG},
|
||||
};
|
||||
use windmill_parser_yaml::AnsibleRequirements;
|
||||
use windmill_parser_yaml::{AnsibleRequirements, ResourceOrVariablePath};
|
||||
use windmill_queue::{append_logs, CanceledBy};
|
||||
|
||||
use crate::{
|
||||
@@ -186,7 +186,7 @@ async fn install_galaxy_collections(
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
fn check_ansible_exists() -> Result<(), error::Error> {
|
||||
if !Path::new(ANSIBLE_PLAYBOOK_PATH.as_str()).exists() {
|
||||
let msg = format!("Couldn't find ansible-playbook at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full` for your instance in order to run rust jobs.", ANSIBLE_PLAYBOOK_PATH.as_str());
|
||||
let msg = format!("Couldn't find ansible-playbook at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full` for your instance in order to run Ansible jobs.", ANSIBLE_PLAYBOOK_PATH.as_str());
|
||||
return Err(error::Error::NotFound(msg));
|
||||
}
|
||||
Ok(())
|
||||
@@ -195,7 +195,7 @@ fn check_ansible_exists() -> Result<(), error::Error> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
fn check_ansible_exists() -> Result<(), error::Error> {
|
||||
if !Path::new(ANSIBLE_PLAYBOOK_PATH.as_str()).exists() {
|
||||
let msg = format!("Couldn't find ansible-playbook at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full-ee` for your instance in order to run rust jobs.", ANSIBLE_PLAYBOOK_PATH.as_str());
|
||||
let msg = format!("Couldn't find ansible-playbook at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-ee-full` for your instance in order to run Ansible jobs.", ANSIBLE_PLAYBOOK_PATH.as_str());
|
||||
return Err(error::Error::NotFound(msg));
|
||||
}
|
||||
Ok(())
|
||||
@@ -557,20 +557,17 @@ async fn create_file_resources(
|
||||
}
|
||||
|
||||
for file_res in &r.file_resources {
|
||||
let r = client
|
||||
.get_resource_value_interpolated::<serde_json::Value>(
|
||||
&file_res.resource_path,
|
||||
Some(job_id.to_string()),
|
||||
)
|
||||
.await?;
|
||||
let r = 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.get("content").and_then(|v| v.as_str()).ok_or(anyhow!(
|
||||
"Invalid text file resource {}, `content` field absent or invalid",
|
||||
&file_res.resource_path
|
||||
))?,
|
||||
&r,
|
||||
)
|
||||
.map_err(|e| anyhow!("Couldn't write text file at {}: {}", path, e))?;
|
||||
|
||||
@@ -580,7 +577,7 @@ async fn create_file_resources(
|
||||
);
|
||||
|
||||
logs.push_str(&format!(
|
||||
"\nCreated {} from {}",
|
||||
"\nCreated {} from {:?}",
|
||||
file_res.target_path, file_res.resource_path
|
||||
));
|
||||
}
|
||||
@@ -588,3 +585,25 @@ async fn create_file_resources(
|
||||
|
||||
Ok(nsjail_mounts)
|
||||
}
|
||||
|
||||
async fn get_resource_or_variable_content(
|
||||
client: &crate::AuthedClient,
|
||||
path: &ResourceOrVariablePath,
|
||||
job_id: String,
|
||||
) -> anyhow::Result<String> {
|
||||
Ok(match path {
|
||||
ResourceOrVariablePath::Resource(p) => {
|
||||
let r = client
|
||||
.get_resource_value_interpolated::<serde_json::Value>(&p, Some(job_id))
|
||||
.await?;
|
||||
|
||||
r.get("content").and_then(|v| v.as_str()).ok_or(anyhow!(
|
||||
"Invalid text file resource {}, `content` field absent or invalid",
|
||||
p
|
||||
))?.to_string()
|
||||
}
|
||||
ResourceOrVariablePath::Variable(p) => {
|
||||
client.get_variable_value(&p).await?
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,11 +59,42 @@ pub async fn handle_bash_job(
|
||||
append_logs(&job.id, &job.workspace_id, logs1, db).await;
|
||||
|
||||
write_file(job_dir, "main.sh", &format!("set -e\n{content}"))?;
|
||||
write_file(
|
||||
job_dir,
|
||||
"wrapper.sh",
|
||||
&format!("set -o pipefail\nset -e\nmkfifo bp\ncat bp | tail -1 > ./result2.out &\n {bash} ./main.sh \"$@\" 2>&1 | tee bp\nwait $!", bash = BIN_BASH.as_str()),
|
||||
)?;
|
||||
let script = format!(
|
||||
r#"
|
||||
set -o pipefail
|
||||
set -e
|
||||
|
||||
# Function to kill child processes
|
||||
cleanup() {{
|
||||
echo "Terminating child processes..."
|
||||
|
||||
# Ignore SIGTERM and SIGINT
|
||||
trap '' SIGTERM SIGINT
|
||||
|
||||
# Kill the process group of the script (negative PID value)
|
||||
pkill -P $$
|
||||
exit
|
||||
}}
|
||||
|
||||
|
||||
# Trap SIGTERM (or other signals) and call cleanup function
|
||||
trap cleanup SIGTERM SIGINT
|
||||
|
||||
# Create a named pipe
|
||||
mkfifo bp
|
||||
|
||||
# Start background processes
|
||||
cat bp | tail -1 >> ./result2.out &
|
||||
|
||||
# Run main.sh in the same process group
|
||||
{bash} ./main.sh "$@" 2>&1 | tee bp &
|
||||
|
||||
# Wait for all background processes to finish
|
||||
wait
|
||||
"#,
|
||||
bash = BIN_BASH.as_str(),
|
||||
);
|
||||
write_file(job_dir, "wrapper.sh", &script)?;
|
||||
|
||||
let token = client.get_token().await;
|
||||
let mut reserved_variables = get_reserved_variables(job, &token, db).await?;
|
||||
|
||||
@@ -63,8 +63,26 @@ const RELATIVE_BUN_BUILDER: &str = include_str!("../loader_builder.bun.js");
|
||||
const NSJAIL_CONFIG_RUN_BUN_CONTENT: &str = include_str!("../nsjail/run.bun.config.proto");
|
||||
|
||||
pub const BUN_LOCKB_SPLIT: &str = "\n//bun.lockb\n";
|
||||
pub const BUN_LOCKB_SPLIT_WINDOWS: &str = "\r\n//bun.lockb\r\n";
|
||||
|
||||
pub const EMPTY_FILE: &str = "<empty>";
|
||||
|
||||
fn split_lockfile(lockfile: &str) -> (&str, Option<&str>, bool) {
|
||||
if let Some(index) = lockfile.find(BUN_LOCKB_SPLIT) {
|
||||
// Split using "\n//bun.lockb\n"
|
||||
let (before, after_with_sep) = lockfile.split_at(index);
|
||||
let after = &after_with_sep[BUN_LOCKB_SPLIT.len()..];
|
||||
(before, Some(after), after == EMPTY_FILE)
|
||||
} else if let Some(index) = lockfile.find(BUN_LOCKB_SPLIT_WINDOWS) {
|
||||
// Split using "\r\n//bun.lockb\r\n"
|
||||
let (before, after_with_sep) = lockfile.split_at(index);
|
||||
let after = &after_with_sep[BUN_LOCKB_SPLIT_WINDOWS.len()..];
|
||||
(before, Some(after), after == EMPTY_FILE)
|
||||
} else {
|
||||
(lockfile, None, false)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn gen_bun_lockfile(
|
||||
mem_peak: &mut i32,
|
||||
canceled_by: &mut Option<CanceledBy>,
|
||||
@@ -180,7 +198,12 @@ pub async fn gen_bun_lockfile(
|
||||
file.read_to_string(&mut content).await?;
|
||||
}
|
||||
if !npm_mode {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
content.push_str(BUN_LOCKB_SPLIT);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
content.push_str(BUN_LOCKB_SPLIT_WINDOWS);
|
||||
|
||||
{
|
||||
let file = format!("{job_dir}/bun.lockb");
|
||||
if !empty_deps && tokio::fs::metadata(&file).await.is_ok() {
|
||||
@@ -762,12 +785,15 @@ async fn compute_bundle_local_and_remote_path(
|
||||
}
|
||||
|
||||
pub async fn prepare_job_dir(reqs: &str, job_dir: &str) -> Result<()> {
|
||||
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
|
||||
let _ = write_file(job_dir, "package.json", &splitted[0])?;
|
||||
let (pkg, lock, empty) = split_lockfile(reqs);
|
||||
let _ = write_file(job_dir, "package.json", pkg)?;
|
||||
|
||||
if splitted[1] != EMPTY_FILE {
|
||||
let _ = write_lockb(splitted[1], job_dir).await?;
|
||||
if !empty {
|
||||
if let Some(lock) = lock {
|
||||
let _ = write_lockb(lock, job_dir).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn write_lockb(splitted_lockb_2: &str, job_dir: &str) -> Result<()> {
|
||||
@@ -874,22 +900,23 @@ pub async fn handle_bun_job(
|
||||
} else if let Some(codebase) = codebase.as_ref() {
|
||||
pull_codebase(&job.workspace_id, codebase, job_dir).await?;
|
||||
} else if let Some(reqs) = requirements_o.as_ref() {
|
||||
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
|
||||
if splitted.len() != 2 && !annotation.npm {
|
||||
let (pkg, lock, empty) = split_lockfile(reqs);
|
||||
|
||||
if lock.is_none() && !annotation.npm {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
format!("Invalid requirements, expected to find //bun.lockb split pattern in reqs. Found: |{reqs}|")
|
||||
));
|
||||
}
|
||||
|
||||
let _ = write_file(job_dir, "package.json", &splitted[0])?;
|
||||
let lockb = if annotation.npm { "" } else { splitted[1] };
|
||||
if lockb != EMPTY_FILE {
|
||||
let _ = write_file(job_dir, "package.json", pkg)?;
|
||||
let lockb = if annotation.npm { "" } else { lock.unwrap() };
|
||||
if !empty {
|
||||
let mut skip_install = false;
|
||||
let mut create_buntar = false;
|
||||
let mut buntar_path = "".to_string();
|
||||
|
||||
if !annotation.npm {
|
||||
let _ = write_lockb(&splitted[1], job_dir).await?;
|
||||
let _ = write_lockb(lockb, job_dir).await?;
|
||||
|
||||
let mut sha_path = sha2::Sha256::new();
|
||||
sha_path.update(lockb.as_bytes());
|
||||
@@ -1561,20 +1588,20 @@ pub async fn start_worker(
|
||||
if let Some(codebase) = codebase.as_ref() {
|
||||
pull_codebase(w_id, codebase, job_dir).await?;
|
||||
} else if let Some(reqs) = requirements_o {
|
||||
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
|
||||
if splitted.len() != 2 {
|
||||
let (pkg, lock, empty) = split_lockfile(&reqs);
|
||||
if lock.is_none() {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
format!("Invalid requirements, expected to find //bun.lockb split pattern in reqs. Found: |{reqs}|")
|
||||
));
|
||||
}
|
||||
let _ = write_file(job_dir, "package.json", &splitted[0])?;
|
||||
let lockb = splitted[1];
|
||||
if lockb != EMPTY_FILE {
|
||||
let _ = write_file(job_dir, "package.json", pkg)?;
|
||||
let lockb = lock.unwrap();
|
||||
if !empty {
|
||||
let _ = write_file_binary(
|
||||
job_dir,
|
||||
"bun.lockb",
|
||||
&base64::engine::general_purpose::STANDARD
|
||||
.decode(&splitted[1])
|
||||
.decode(lockb)
|
||||
.map_err(|_| {
|
||||
error::Error::InternalErr("Could not decode bun.lockb".to_string())
|
||||
})?,
|
||||
|
||||
@@ -85,7 +85,7 @@ fn do_postgresql_inner<'a>(
|
||||
let arg_t = arg
|
||||
.otyp
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing otyp for pg arg"))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing otzyp for pg arg"))?;
|
||||
let typ = &arg.typ;
|
||||
let param = convert_val(value, arg_t, typ)?;
|
||||
query_params.push(param);
|
||||
|
||||
@@ -143,7 +143,7 @@ fn check_php_exists() -> error::Result<()> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
fn check_php_exists() -> error::Result<()> {
|
||||
if !Path::new(PHP_PATH.as_str()).exists() {
|
||||
let msg = format!("Couldn't find php at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full-ee` for your instance in order to run php jobs.", PHP_PATH.as_str());
|
||||
let msg = format!("Couldn't find php at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-ee-full` for your instance in order to run php jobs.", PHP_PATH.as_str());
|
||||
return Err(error::Error::NotFound(msg));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -281,7 +281,7 @@ fn check_cargo_exists() -> Result<(), Error> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
fn check_cargo_exists() -> Result<(), Error> {
|
||||
if !Path::new(CARGO_PATH.as_str()).exists() {
|
||||
let msg = format!("Couldn't find cargo at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full-ee` for your instance in order to run rust jobs.", CARGO_PATH.as_str());
|
||||
let msg = format!("Couldn't find cargo at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-ee-full` for your instance in order to run rust jobs.", CARGO_PATH.as_str());
|
||||
return Err(Error::NotFound(msg));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -2403,15 +2403,15 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
}
|
||||
tracing::debug!(id = %flow_job.id, root_id = %job_root, "pushing job {i} of {len}");
|
||||
let payload_tag = match &job_payloads {
|
||||
ContinuePayload::SingleJob(payload) => payload.clone(),
|
||||
ContinuePayload::BranchAllJobs(payloads) => payloads[i].clone(),
|
||||
ContinuePayload::SingleJob(payload) => payload,
|
||||
ContinuePayload::BranchAllJobs(payloads) => &payloads[i],
|
||||
ContinuePayload::ForloopJobs { flow_value, delete_after_use, .. } => {
|
||||
let mut fv = flow_value.clone();
|
||||
|
||||
if let Some(failure_module) = fv.failure_module.as_mut() {
|
||||
failure_module.id_append(&format!("{}-{i}", &status.step.to_string()));
|
||||
}
|
||||
JobPayloadWithTag {
|
||||
&JobPayloadWithTag {
|
||||
payload: JobPayload::RawFlow {
|
||||
value: fv,
|
||||
path: Some(format!("{}/forloop-{i}", flow_job.script_path())),
|
||||
@@ -2590,13 +2590,17 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
};
|
||||
|
||||
tracing::debug!(id = %flow_job.id, root_id = %job_root, "computed perms for job {i} of {len}");
|
||||
|
||||
let tag = if flow_job.tag == "flow" || flow_job.tag == format!("flow-{}", flow_job.workspace_id) {
|
||||
payload_tag.tag.clone()
|
||||
} else {
|
||||
Some(flow_job.tag.clone())
|
||||
};
|
||||
let tx2 = PushIsolationLevel::Transaction(tx);
|
||||
let (uuid, mut inner_tx) = push(
|
||||
&db,
|
||||
tx2,
|
||||
&flow_job.workspace_id,
|
||||
payload_tag.payload,
|
||||
payload_tag.payload.clone(),
|
||||
push_args,
|
||||
&flow_job.created_by,
|
||||
&flow_job.email,
|
||||
@@ -2610,11 +2614,7 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
continue_on_same_worker,
|
||||
err,
|
||||
flow_job.visible_to_owner,
|
||||
if flow_job.tag == "flow" || flow_job.tag == format!("flow-{}", flow_job.workspace_id) {
|
||||
payload_tag.tag
|
||||
} else {
|
||||
Some(flow_job.tag.clone())
|
||||
},
|
||||
tag,
|
||||
payload_tag.timeout,
|
||||
Some(module.id.clone()),
|
||||
new_job_priority_override,
|
||||
@@ -3761,6 +3761,11 @@ async fn script_to_payload(
|
||||
module: &FlowModule,
|
||||
tag_override: &Option<String>,
|
||||
) -> Result<JobPayloadWithTag, Error> {
|
||||
let tag_override = if tag_override.as_ref().is_some_and(|x| x.trim().is_empty()) {
|
||||
None
|
||||
} else {
|
||||
tag_override.clone()
|
||||
};
|
||||
let (payload, tag, delete_after_use, script_timeout) = if script_hash.is_none() {
|
||||
let (jp, tag, delete_after_use, script_timeout) =
|
||||
script_path_to_payload(script_path, db, &flow_job.workspace_id, Some(true)).await?;
|
||||
|
||||
@@ -291,7 +291,7 @@ pub async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync +
|
||||
let hash = job.script_hash.unwrap_or(ScriptHash(0));
|
||||
let w_id = &job.workspace_id;
|
||||
sqlx::query!(
|
||||
"UPDATE script SET lock = $1, created_at = now() WHERE hash = $2 AND workspace_id = $3",
|
||||
"UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3",
|
||||
&content,
|
||||
&hash.0,
|
||||
w_id
|
||||
|
||||
+1
-1
@@ -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.411.1";
|
||||
export const VERSION = "v1.414.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+5
-6
@@ -650,7 +650,10 @@ const command = new Command()
|
||||
.option("--skip-configs", "Skip pulling configs (worker groups and SMTP)")
|
||||
.option("--skip-groups", "Skip pulling instance groups")
|
||||
.option("--include-workspaces", "Also pull workspaces")
|
||||
|
||||
.option(
|
||||
"--instance <instance:string>",
|
||||
"Name of the instance to push to, override the active instance"
|
||||
)
|
||||
.action(instancePull as any)
|
||||
.command("push")
|
||||
.description(
|
||||
@@ -663,13 +666,9 @@ const command = new Command()
|
||||
.option("--skip-groups", "Skip pushing instance groups")
|
||||
.option("--include-workspaces", "Also push workspaces")
|
||||
.option(
|
||||
"--instance",
|
||||
"--instance <instance:string>",
|
||||
"Name of the instance to push to, override the active instance"
|
||||
)
|
||||
.option(
|
||||
"--base-url",
|
||||
"If used with --token, will be used as the base url for the instance"
|
||||
)
|
||||
.action(instancePush as any)
|
||||
.command("whoami")
|
||||
.description("Display information about the currently logged-in user")
|
||||
|
||||
+85
-87
@@ -3,102 +3,100 @@ import crypto from "node:crypto";
|
||||
|
||||
// Helper function to convert strings to Uint8Array (binary)
|
||||
function encode(input: string): Uint8Array {
|
||||
return new TextEncoder().encode(input);
|
||||
}
|
||||
|
||||
// Helper function to convert Uint8Array (binary) to base64
|
||||
function toBase64(arr: Uint8Array): string {
|
||||
return btoa(String.fromCharCode(...arr));
|
||||
}
|
||||
|
||||
// Helper function to convert base64 to Uint8Array (binary)
|
||||
function fromBase64(base64: string): Uint8Array {
|
||||
return new Uint8Array(
|
||||
atob(base64).split("").map((char) => char.charCodeAt(0))
|
||||
);
|
||||
}
|
||||
|
||||
// Function to derive a 256-bit key from any input string using SHA-256
|
||||
async function deriveKey(keyString: string): Promise<CryptoKey> {
|
||||
const keyMaterial = encode(keyString);
|
||||
const keyHash = await crypto.subtle.digest("SHA-256", keyMaterial); // Generate SHA-256 hash
|
||||
// Import the hash as a CryptoKey for AES-GCM
|
||||
return crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyHash,
|
||||
{ name: "AES-GCM" },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
return new TextEncoder().encode(input);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Encrypt function
|
||||
export async function encrypt(
|
||||
plaintext: string,
|
||||
keyString: string
|
||||
): Promise<string> {
|
||||
const key = await deriveKey(keyString); // Derive a 256-bit AES key from any input string
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12)); // AES-GCM needs a 12-byte IV
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
encode(plaintext) // convert plaintext to binary
|
||||
);
|
||||
|
||||
// Concatenate IV and encrypted data
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
combined.set(iv, 0); // first part is the IV
|
||||
combined.set(new Uint8Array(encrypted), iv.length); // second part is the ciphertext
|
||||
|
||||
// Convert to base64 for storage/transmission
|
||||
return toBase64(combined);
|
||||
}
|
||||
|
||||
// Decrypt function
|
||||
export async function decrypt(
|
||||
combinedCiphertext: string,
|
||||
keyString: string
|
||||
): Promise<string> {
|
||||
const key = await deriveKey(keyString); // Derive the same 256-bit AES key from the input string
|
||||
const combined = fromBase64(combinedCiphertext); // decode base64 to binary
|
||||
|
||||
// Split the IV and the ciphertext
|
||||
const iv = combined.slice(0, 12); // First 12 bytes are the IV
|
||||
const ciphertext = combined.slice(12); // The rest is the encrypted data
|
||||
console.log()
|
||||
// Helper function to convert Uint8Array (binary) to base64
|
||||
function toBase64(arr: Uint8Array): string {
|
||||
return btoa(String.fromCharCode(...arr));
|
||||
}
|
||||
|
||||
// Helper function to convert base64 to Uint8Array (binary)
|
||||
function fromBase64(base64: string): Uint8Array {
|
||||
return new Uint8Array(
|
||||
atob(base64)
|
||||
.split("")
|
||||
.map((char) => char.charCodeAt(0))
|
||||
);
|
||||
}
|
||||
|
||||
// Function to derive a 256-bit key from any input string using SHA-256
|
||||
async function deriveKey(
|
||||
keyString: string
|
||||
): Promise<crypto.webcrypto.CryptoKey> {
|
||||
const keyMaterial = encode(keyString);
|
||||
const keyHash = await crypto.subtle.digest("SHA-256", keyMaterial); // Generate SHA-256 hash
|
||||
// Import the hash as a CryptoKey for AES-GCM
|
||||
return crypto.subtle.importKey("raw", keyHash, { name: "AES-GCM" }, false, [
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
]);
|
||||
}
|
||||
|
||||
// Encrypt function
|
||||
export async function encrypt(
|
||||
plaintext: string,
|
||||
keyString: string
|
||||
): Promise<string> {
|
||||
const key = await deriveKey(keyString); // Derive a 256-bit AES key from any input string
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12)); // AES-GCM needs a 12-byte IV
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
tagLength: 128,
|
||||
},
|
||||
key,
|
||||
encode(plaintext) // convert plaintext to binary
|
||||
);
|
||||
|
||||
// Concatenate IV and encrypted data
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
combined.set(iv, 0); // first part is the IV
|
||||
combined.set(new Uint8Array(encrypted), iv.length); // second part is the ciphertext
|
||||
|
||||
// Convert to base64 for storage/transmission
|
||||
return toBase64(combined);
|
||||
}
|
||||
|
||||
// Decrypt function
|
||||
export async function decrypt(
|
||||
combinedCiphertext: string,
|
||||
keyString: string
|
||||
): Promise<string> {
|
||||
const key = await deriveKey(keyString); // Derive the same 256-bit AES key from the input string
|
||||
const combined = fromBase64(combinedCiphertext); // decode base64 to binary
|
||||
|
||||
// Split the IV and the ciphertext
|
||||
const iv = combined.slice(0, 12); // First 12 bytes are the IV
|
||||
const ciphertext = combined.slice(12); // The rest is the encrypted data
|
||||
console.log();
|
||||
|
||||
// log.info({keyString, key, ciphertext})
|
||||
// Perform decryption
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
tagLength: 128,
|
||||
},
|
||||
key,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
// Convert decrypted data from binary to string
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
// log.info({keyString, key, ciphertext})
|
||||
// Perform decryption
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
// Convert decrypted data from binary to string
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
// // Example usage:
|
||||
// const key = "any-length-key-you-want"; // Now can be any length
|
||||
// const message = "This is a secret message.";
|
||||
|
||||
|
||||
// encrypt(message, key).then((combinedCiphertext) => {
|
||||
// console.log("Encrypted message:", combinedCiphertext);
|
||||
|
||||
|
||||
// // Now decrypt it
|
||||
// decrypt(combinedCiphertext, key).then((decryptedMessage) => {
|
||||
// console.log("Decrypted message:", decryptedMessage);
|
||||
// });
|
||||
// });
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.411.1";
|
||||
export const VERSION = "1.414.1";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -11,7 +11,7 @@ RUN apt-get update \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -
|
||||
RUN apt-get -y update && apt-get install -y curl nodejs awscli
|
||||
RUN apt-get -y update && apt-get install -y curl procps nodejs awscli
|
||||
|
||||
ENV TZ=Etc/UTC
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ RUN apt-get update \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -
|
||||
RUN apt-get -y update && apt-get install -y curl nodejs awscli
|
||||
RUN apt-get -y update && apt-get install -y curl procps nodejs awscli
|
||||
|
||||
ENV TZ=Etc/UTC
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.411.1",
|
||||
"version": "1.414.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.411.1",
|
||||
"version": "1.414.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.411.1",
|
||||
"version": "1.414.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -75,16 +75,18 @@
|
||||
}}>{value}</button
|
||||
>
|
||||
{:else if typeof value !== 'object'}
|
||||
{truncate(JSON.stringify(value), 100)}
|
||||
{#if JSON.stringify(value).length > 100}
|
||||
<button
|
||||
class="text-xs text-blue-500"
|
||||
on:click={() => {
|
||||
jsonViewerContent = value
|
||||
jsonViewer.toggleDrawer()
|
||||
}}>See expanded</button
|
||||
>
|
||||
{/if}
|
||||
<span>
|
||||
{truncate(JSON.stringify(value), 80)}
|
||||
{#if JSON.stringify(value).length > 80}
|
||||
<button
|
||||
class="text-xs text-blue-500"
|
||||
on:click={() => {
|
||||
jsonViewerContent = value
|
||||
jsonViewer.toggleDrawer()
|
||||
}}>See expanded</button
|
||||
>
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<div class="relative">
|
||||
{#if JSON.stringify(value).length > 120}
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
<script lang="ts">
|
||||
import type { AutoscalingConfig } from './worker_group'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Section from './Section.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { Button } from './common'
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Label from './Label.svelte'
|
||||
import MultiSelect from 'svelte-multiselect'
|
||||
|
||||
export let config: AutoscalingConfig | undefined
|
||||
export let worker_tags: string[] | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let test_input: number = 3
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row gap-16 pt-2">
|
||||
<div class="space-y-4 flex flex-col gap-1 max-w-xs text-sm">
|
||||
<h5>Rules</h5>
|
||||
<Toggle
|
||||
checked={config?.enabled ?? false}
|
||||
options={{ right: 'Enabled' }}
|
||||
on:change={(e) => {
|
||||
dispatch('dirty')
|
||||
if (e.detail) {
|
||||
if (!config) {
|
||||
config = {
|
||||
enabled: true,
|
||||
min_workers: 3,
|
||||
max_workers: 10,
|
||||
integration: { type: 'dryrun' }
|
||||
}
|
||||
} else {
|
||||
config.enabled = true
|
||||
}
|
||||
} else {
|
||||
config = {
|
||||
...(config ?? {
|
||||
min_workers: 3,
|
||||
max_workers: 10,
|
||||
integration: { type: 'dryrun' }
|
||||
}),
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Min # of Workers
|
||||
{#if config !== undefined}
|
||||
<input on:input={() => dispatch('dirty')} type="number" bind:value={config.min_workers} />
|
||||
{#if config.min_workers !== undefined && config.min_workers != undefined && config.min_workers > config.max_workers}
|
||||
<div class="text-red-600 text-xs whitespace-nowrap"
|
||||
>Minimum cannot be {'>'} to Maximum</div
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<input type="number" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Max # of Workers
|
||||
{#if config !== undefined}
|
||||
<input on:input={() => dispatch('dirty')} type="number" bind:value={config.max_workers} />
|
||||
{:else}
|
||||
<input type="number" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
<div class="p-2">
|
||||
<Section label="Advanced" small collapsable={true}>
|
||||
<div class="flex flex-col gap-2 text-2xs">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Cooldown seconds after an incremental scale-in/out
|
||||
{#if config !== undefined}
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="number"
|
||||
step="1"
|
||||
min="30"
|
||||
placeholder="300"
|
||||
bind:value={config.cooldown_seconds}
|
||||
/>
|
||||
{:else}
|
||||
<input type="number" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Cooldown seconds after a full scale out
|
||||
{#if config !== undefined}
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="number"
|
||||
step="1"
|
||||
min="30"
|
||||
placeholder="1500"
|
||||
bind:value={config.full_scale_cooldown_seconds}
|
||||
/>
|
||||
{:else}
|
||||
<input type="number" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Num jobs waiting to trigger an incremental scale-out
|
||||
{#if config !== undefined}
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="number"
|
||||
bind:value={config.inc_scale_num_jobs_waiting}
|
||||
placeholder="1"
|
||||
/>
|
||||
{:else}
|
||||
<input type="number" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Num jobs waiting to trigger a full scale out <Tooltip
|
||||
>Default: max_workers, full scale out = scale out to max workers</Tooltip
|
||||
>
|
||||
{#if config !== undefined}
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="number"
|
||||
placeholder="max workers"
|
||||
bind:value={config.full_scale_jobs_waiting}
|
||||
/>
|
||||
{:else}
|
||||
<input type="number" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Occupancy rate % threhsold to go below to trigger a scale-in (decrease) <Tooltip
|
||||
>Default: 25%, need to go below average of all of 15s, 5m and 30m occupancy rates</Tooltip
|
||||
>
|
||||
{#if config !== undefined}
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
placeholder="25"
|
||||
bind:value={config.dec_scale_occupancy_rate}
|
||||
/>
|
||||
{:else}
|
||||
<input type="number" step="0.01" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Occupancy rate threshold to exceed to trigger an incremental scale-out (increase) <Tooltip
|
||||
>Default: 75%, need to exceed average of all of 15s, 5m and 30m occupancy rates</Tooltip
|
||||
>
|
||||
{#if config !== undefined}
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
placeholder="75"
|
||||
bind:value={config.inc_scale_occupancy_rate}
|
||||
/>
|
||||
{:else}
|
||||
<input type="number" step="0.01" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label>
|
||||
Num workers to scale-in/out by when incremental <Tooltip
|
||||
>Default: (max_workers - min_workers) / 5</Tooltip
|
||||
>
|
||||
{#if config !== undefined}
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
placeholder="(max_workers - min_workers) / 5"
|
||||
bind:value={config.inc_num_workers}
|
||||
/>
|
||||
{:else}
|
||||
<input type="number" disabled />
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<Label label="Custom tags to autoscale on">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>
|
||||
By default, autoscaling will apply to the tags the worker group is assigned to but
|
||||
you can override this here.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
{#if config}
|
||||
{#if config.custom_tags}
|
||||
<MultiSelect
|
||||
outerDivClass="text-secondary !bg-surface-disabled !border-0"
|
||||
selected={config.custom_tags}
|
||||
on:change={(e) => {
|
||||
console.log(e.detail.type, config?.custom_tags)
|
||||
if (e.detail && config?.custom_tags) {
|
||||
if (e.detail.type === 'add') {
|
||||
config.custom_tags = [...config.custom_tags, e.detail.option]
|
||||
} else if (e.detail.type === 'remove') {
|
||||
config.custom_tags = config.custom_tags.filter((t) => t !== e.detail.option)
|
||||
if (config?.custom_tags && config.custom_tags.length == 0) {
|
||||
config.custom_tags = undefined
|
||||
}
|
||||
} else if (e.detail.type === 'removeAll') {
|
||||
config.custom_tags = undefined
|
||||
} else {
|
||||
console.error(
|
||||
`Priority tags multiselect - unknown event type: '${e.detail.type}'`
|
||||
)
|
||||
}
|
||||
dispatch('dirty')
|
||||
}
|
||||
}}
|
||||
on:clear={() => {
|
||||
if (config) {
|
||||
config.custom_tags = undefined
|
||||
}
|
||||
dispatch('dirty')
|
||||
}}
|
||||
options={worker_tags ?? []}
|
||||
selectedOptionsDraggable={false}
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
placeholder="Tags"
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
on:click={() => {
|
||||
if (config) {
|
||||
config.custom_tags = []
|
||||
dispatch('dirty')
|
||||
}
|
||||
}}>Add custom tags</Button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</Label>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 max-w-xs text-sm">
|
||||
<h5>Integration</h5>
|
||||
{#if config?.integration}
|
||||
<ToggleButtonGroup
|
||||
on:selected={(e) => dispatch('dirty')}
|
||||
bind:selected={config.integration.type}
|
||||
class="mb-4 mt-2"
|
||||
>
|
||||
<ToggleButton
|
||||
value="dryrun"
|
||||
size="sm"
|
||||
label="Dry run"
|
||||
tooltip="See autoscaling events but not actual scaling actions will be performed"
|
||||
/>
|
||||
<ToggleButton
|
||||
value="script"
|
||||
size="sm"
|
||||
label="Custom Script"
|
||||
tooltip="Run a custom script to scale your worker group"
|
||||
/>
|
||||
<ToggleButton position="center" disabled value="ecs" size="sm" label="ECS (soon)" />
|
||||
<ToggleButton position="right" disabled value="nomad" size="sm" label="Nomad (soon)" />
|
||||
<ToggleButton
|
||||
position="right"
|
||||
disabled
|
||||
value="kubernetes"
|
||||
size="sm"
|
||||
label="Kubernetes (soon)"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{#if config.integration.type === 'script'}
|
||||
<label>
|
||||
Script path on the 'admins' workspace
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="text"
|
||||
bind:value={config.integration.path}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Custom tag for executing script (optional)
|
||||
{#if config.integration.tag}
|
||||
<input
|
||||
on:input={() => dispatch('dirty')}
|
||||
type="text"
|
||||
bind:value={config.integration.tag}
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
on:click={() => {
|
||||
if (config?.integration?.type === 'script') {
|
||||
config.integration.tag = 'bash'
|
||||
dispatch('dirty')
|
||||
}
|
||||
}}>Set tag</Button
|
||||
>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<div class="flex mt-6 gap-2">
|
||||
<Button
|
||||
color="dark"
|
||||
target="_blank"
|
||||
endIcon={{ icon: ExternalLink }}
|
||||
href="/scripts/add?hub=hub%2F9204%2Fhelper%2FScale%20a%20worker%20group%20deployed%20as%20a%20kubernetes%20service&workspace=admins"
|
||||
>Create from template</Button
|
||||
>
|
||||
<Button
|
||||
color="dark"
|
||||
target="_blank"
|
||||
href={`/runs/${config.integration.path}?workspace=admins`}
|
||||
endIcon={{ icon: ExternalLink }}
|
||||
>
|
||||
See jobs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 mt-4">
|
||||
<Button color="light" size="xs" variant="contained">Test scaling</Button>
|
||||
<div class="flex text-xs flex-row gap-2 items-center">
|
||||
<input class="!w-16" type="number" bind:value={test_input} />
|
||||
workers
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2">
|
||||
<ToggleButton value="dryrun" size="sm" label="Dry run" />
|
||||
<ToggleButton value="script" size="sm" label="Custom Script" />
|
||||
<ToggleButton position="center" value="ecs" size="sm" label="ECS (soon)" />
|
||||
<ToggleButton position="right" value="nomad" size="sm" label="Nomad (soon)" />
|
||||
<ToggleButton position="right" value="kubernetes" size="sm" label="Kubernetes (soon)" />
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<label>
|
||||
Script path on the 'admins' workspace
|
||||
<input type="text" disabled />
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { ConfigService, type AutoscalingEvent } from '$lib/gen'
|
||||
import { LoaderIcon, RefreshCw } from 'lucide-svelte'
|
||||
import { Button, Skeleton } from './common'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import TimeAgo from './TimeAgo.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
|
||||
export let worker_group: string
|
||||
|
||||
let loading = true
|
||||
let events: AutoscalingEvent[] | undefined = undefined
|
||||
|
||||
$: worker_group && loadEvents()
|
||||
|
||||
async function loadEvents() {
|
||||
loading = true
|
||||
try {
|
||||
events = await ConfigService.listAutoscalingEvents({ workerGroup: worker_group })
|
||||
} catch (e) {
|
||||
events = []
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h6
|
||||
class={!$enterpriseLicense || (events != undefined && events.length == 0)
|
||||
? 'text-tertiary'
|
||||
: ''}
|
||||
>Autoscaling events {#if $enterpriseLicense}<span class="text-xs text-tertiary">(5 last)</span>
|
||||
<span class="inline-flex ml-6">
|
||||
<Button
|
||||
startIcon={{
|
||||
icon: loading ? LoaderIcon : RefreshCw,
|
||||
classes: twMerge(
|
||||
loading ? 'animate-spin text-blue-800' : '',
|
||||
'transition-all text-gray-500 dark:text-white'
|
||||
)
|
||||
}}
|
||||
color="light"
|
||||
size="xs2"
|
||||
btnClasses={twMerge(loading ? ' bg-blue-100 dark:bg-blue-400' : '', 'transition-all')}
|
||||
on:click={() => loadEvents()}
|
||||
iconOnly
|
||||
/>
|
||||
</span>{/if}
|
||||
</h6>
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="text-xs pt-2 text-tertiary">Autoscaling is an EE feature</div>
|
||||
{:else if loading}
|
||||
<Skeleton layout={[[12], 1]} />
|
||||
{:else if events}
|
||||
{#if events.length == 0}
|
||||
<div class="text-xs pt-2 text-tertiary"
|
||||
>No events, is autoscaling set in the worker group config?</div
|
||||
>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2 text-xs text-tertiary pt-4">
|
||||
{#each events as event}
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="text-primary">{event.event_type} to {event.desired_workers}</div>
|
||||
<div class="text-secondary">{event.reason}</div>
|
||||
<div class="text-tertiary"><TimeAgo date={event.applied_at ?? ''} /></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -9,6 +9,7 @@
|
||||
export let disappearTimeout = 100
|
||||
export let appearTimeout = 300
|
||||
export let style: string | undefined = undefined
|
||||
export let noPadding = false
|
||||
|
||||
export let focusEl: HTMLElement | undefined = undefined
|
||||
|
||||
@@ -73,7 +74,7 @@
|
||||
use:popperContent={popperOptions}
|
||||
on:mouseenter={open}
|
||||
on:mouseleave={close}
|
||||
class="z-[5001] border rounded-lg shadow-lg p-4 bg-surface"
|
||||
class="z-[5001] border rounded-lg shadow-lg {noPadding ? '' : 'p-4'} bg-surface"
|
||||
>
|
||||
<slot name="overlay" />
|
||||
</div>
|
||||
|
||||
@@ -119,13 +119,21 @@
|
||||
if (version === undefined) {
|
||||
return
|
||||
}
|
||||
const flowVersion = await FlowService.getFlowLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: $pathStore
|
||||
})
|
||||
|
||||
onLatest = version === flowVersion?.id
|
||||
try {
|
||||
if (initialPath && initialPath != '') {
|
||||
const flowVersion = await FlowService.getFlowLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
|
||||
onLatest = version === flowVersion?.id
|
||||
} else {
|
||||
onLatest = true
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error comparing versions', err)
|
||||
onLatest = true
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -277,9 +285,8 @@
|
||||
}
|
||||
|
||||
async function handleSaveFlow(deploymentMsg?: string) {
|
||||
|
||||
await compareVersions();
|
||||
if (onLatest) {
|
||||
await compareVersions()
|
||||
if (onLatest || initialPath == '' || savedFlow?.draft_only) {
|
||||
// Handle directly
|
||||
await saveFlow(deploymentMsg)
|
||||
} else {
|
||||
@@ -294,28 +301,27 @@
|
||||
open = true
|
||||
}
|
||||
}
|
||||
async function syncWithDeployed(){
|
||||
const flow = await FlowService.getFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: $pathStore,
|
||||
withStarredInfo: true
|
||||
})
|
||||
deployedValue = {
|
||||
...flow,
|
||||
starred: undefined,
|
||||
id: undefined,
|
||||
edited_at: undefined,
|
||||
edited_by: undefined,
|
||||
workspace_id: undefined,
|
||||
archived: undefined,
|
||||
same_worker: undefined,
|
||||
visible_to_runner_only: undefined,
|
||||
ws_error_handler_muted: undefined,
|
||||
}
|
||||
deployedBy = flow.edited_by
|
||||
async function syncWithDeployed() {
|
||||
const flow = await FlowService.getFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
withStarredInfo: true
|
||||
})
|
||||
deployedValue = {
|
||||
...flow,
|
||||
starred: undefined,
|
||||
id: undefined,
|
||||
edited_at: undefined,
|
||||
edited_by: undefined,
|
||||
workspace_id: undefined,
|
||||
archived: undefined,
|
||||
same_worker: undefined,
|
||||
visible_to_runner_only: undefined,
|
||||
ws_error_handler_muted: undefined
|
||||
}
|
||||
deployedBy = flow.edited_by
|
||||
}
|
||||
|
||||
|
||||
async function saveFlow(deploymentMsg?: string): Promise<void> {
|
||||
loadingSave = true
|
||||
try {
|
||||
|
||||
@@ -133,8 +133,10 @@
|
||||
Error handler
|
||||
{:else if stepDetail.id === 'preprocessor'}
|
||||
Preprocessor
|
||||
{:else}
|
||||
Anonymous step
|
||||
{:else if stepDetail.value.type == 'rawscript'}
|
||||
Inline {stepDetail.value.language} script
|
||||
{:else if stepDetail.value.type == 'script'}
|
||||
Workspace script
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -102,8 +102,8 @@
|
||||
if (values['base_url'] == undefined) {
|
||||
values['base_url'] = 'http://localhost'
|
||||
}
|
||||
if (values['smtp_connect'] == undefined) {
|
||||
values['smtp_connect'] = {}
|
||||
if (values['smtp_settings'] == undefined) {
|
||||
values['smtp_settings'] = {}
|
||||
}
|
||||
loading = false
|
||||
|
||||
@@ -206,7 +206,8 @@
|
||||
'basecamp',
|
||||
'linkedin',
|
||||
'quickbooks',
|
||||
'visma'
|
||||
'visma',
|
||||
'spotify'
|
||||
]
|
||||
|
||||
let oauth_name = undefined
|
||||
@@ -274,22 +275,34 @@
|
||||
<TabContent value={category}>
|
||||
{#if category == 'SMTP'}
|
||||
<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></div
|
||||
>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
|
||||
></div
|
||||
>
|
||||
{:else if category == 'Registries'}
|
||||
<div class="text-secondary pb-4 text-xs">
|
||||
Add private registries for Pip, Bun and npm. <a target="_blank" href="https://www.windmill.dev/docs/advanced/imports">Learn more</a>
|
||||
Add private registries for Pip, Bun and npm. <a
|
||||
target="_blank"
|
||||
href="https://www.windmill.dev/docs/advanced/imports">Learn more</a
|
||||
>
|
||||
</div>
|
||||
{:else if category == 'Slack'}
|
||||
<div class="text-secondary pb-4 text-xs">
|
||||
Connecting your instance to a Slack workspace enables critical alerts to be sent to a Slack channel.
|
||||
<a target="_blank" href="https://www.windmill.dev/docs/misc/saml_and_scim">Learn more</a>
|
||||
Connecting your instance to a Slack workspace enables critical alerts to be sent to a
|
||||
Slack channel.
|
||||
<a target="_blank" href="https://www.windmill.dev/docs/misc/saml_and_scim"
|
||||
>Learn more</a
|
||||
>
|
||||
</div>
|
||||
{:else if category == 'SCIM/SAML'}
|
||||
<div class="text-secondary pb-4 text-xs">
|
||||
Setting up SAML and SCIM allows you to authenticate users using your identity provider.
|
||||
<a target="_blank" href="https://www.windmill.dev/docs/advanced/instance_settings#slack">Learn more</a>
|
||||
Setting up SAML and SCIM allows you to authenticate users using your identity
|
||||
provider.
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://www.windmill.dev/docs/advanced/instance_settings#slack">Learn more</a
|
||||
>
|
||||
</div>
|
||||
{:else if category == 'Debug'}
|
||||
<div class="text-secondary pb-4 text-xs">
|
||||
@@ -310,9 +323,11 @@
|
||||
</ul>
|
||||
</div>
|
||||
{#if $enterpriseLicense}
|
||||
<div class="text-secondary pb-4 text-xs">
|
||||
On Enterprise Edition, you must send data to check that usage is in line with the terms of the subscription. You can either enable telemetry or regularly send usage data by clicking the button below.
|
||||
</div>
|
||||
<div class="text-secondary pb-4 text-xs">
|
||||
On Enterprise Edition, you must send data to check that usage is in line with the
|
||||
terms of the subscription. You can either enable telemetry or regularly send usage
|
||||
data by clicking the button below.
|
||||
</div>
|
||||
<Button
|
||||
on:click={sendStats}
|
||||
variant="border"
|
||||
@@ -340,12 +355,17 @@
|
||||
{/if}
|
||||
|
||||
<div class="py-1" />
|
||||
<div class="mb-2">
|
||||
<span class="text-primary text-sm">When at least one of the below options is set, users will be able to login to
|
||||
Windmill via their third-party account.
|
||||
<br /> To test SSO, the recommended workflow is to to save the settings and try to
|
||||
login in an incognito window. <a target="_blank" href="https://www.windmill.dev/docs/misc/setup_oauth#sso">Learn more</a></span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="text-primary text-sm"
|
||||
>When at least one of the below options is set, users will be able to login to
|
||||
Windmill via their third-party account.
|
||||
<br /> To test SSO, the recommended workflow is to to save the settings and try
|
||||
to login in an incognito window.
|
||||
<a target="_blank" href="https://www.windmill.dev/docs/misc/setup_oauth#sso"
|
||||
>Learn more</a
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 py-4">
|
||||
<OAuthSetting name="google" bind:value={oauths['google']} />
|
||||
<OAuthSetting name="microsoft" bind:value={oauths['microsoft']} />
|
||||
@@ -374,7 +394,11 @@
|
||||
<div class="p-2 border rounded">
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Custom Name</span>
|
||||
<input type="text" placeholder="Custom Name" bind:value={oauths[k]['display_name']} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Custom Name"
|
||||
bind:value={oauths[k]['display_name']}
|
||||
/>
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Client Id</span>
|
||||
@@ -429,12 +453,18 @@
|
||||
</div>
|
||||
{:else if ssoOrOauth === 'oauth'}
|
||||
<div class="mb-2">
|
||||
<span class="text-primary text-sm">When one of the below options is set, you will be able to create a specific
|
||||
<span class="text-primary text-sm"
|
||||
>When one of the below options is set, you will be able to create a specific
|
||||
resource containing a token automatically generated by the third-party provider.
|
||||
<br />
|
||||
To test it after setting an oauth client, go to the Resources menu and create a new
|
||||
one of the type of your oauth client (i.e. a 'github' resource if you set Github OAuth).
|
||||
<br /><a target="_blank" href="https://www.windmill.dev/docs/misc/setup_oauth#oauth">Learn more</a></span>
|
||||
one of the type of your oauth client (i.e. a 'github' resource if you set Github
|
||||
OAuth).
|
||||
<br /><a
|
||||
target="_blank"
|
||||
href="https://www.windmill.dev/docs/misc/setup_oauth#oauth">Learn more</a
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="py-1" />
|
||||
<OAuthSetting login={false} name="slack" bind:value={oauths['slack']} />
|
||||
@@ -621,24 +651,33 @@
|
||||
{@const attemptedAt = new Date(
|
||||
latestKeyRenewalAttempt.attempted_at
|
||||
).toLocaleString()}
|
||||
{@const isTrial =
|
||||
latestKeyRenewalAttempt.result.startsWith('error: trial:')}
|
||||
<div class="relative">
|
||||
<Popover notClickable>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
{#if latestKeyRenewalAttempt.result === 'success'}
|
||||
<BadgeCheck class="text-green-600" size={12} />
|
||||
{:else}
|
||||
<BadgeX class="text-red-600" size={12} />
|
||||
<BadgeX
|
||||
class={isTrial ? 'text-yellow-600' : 'text-red-600'}
|
||||
size={12}
|
||||
/>
|
||||
{/if}
|
||||
<span
|
||||
class={classNames(
|
||||
'text-xs',
|
||||
latestKeyRenewalAttempt.result === 'success'
|
||||
? 'text-green-600'
|
||||
: isTrial
|
||||
? 'text-yellow-600'
|
||||
: 'text-red-600'
|
||||
)}
|
||||
>
|
||||
{latestKeyRenewalAttempt.result === 'success'
|
||||
? 'Latest key renewal succeeded'
|
||||
: isTrial
|
||||
? 'Latest key renewal ignored because in trial'
|
||||
: 'Latest key renewal failed'}
|
||||
on {attemptedAt}
|
||||
</span>
|
||||
@@ -648,6 +687,10 @@
|
||||
<span class="text-green-300">
|
||||
Latest key renewal succeeded on {attemptedAt}
|
||||
</span>
|
||||
{:else if isTrial}
|
||||
<span class="text-yellow-300">
|
||||
License key cannot be renewed during trial ({attemptedAt})
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-red-300">
|
||||
Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt.result.replace(
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import Row from './table/Row.svelte'
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { isWindmillTooBigObject } from './job_args'
|
||||
|
||||
export let id: string | undefined = undefined
|
||||
export let args: any
|
||||
@@ -63,7 +64,7 @@ ${Object.entries(args)
|
||||
<div class="relative">
|
||||
<DataTable size="sm">
|
||||
<Head>
|
||||
<tr>
|
||||
<tr class="w-full">
|
||||
<Cell head first>{argLabel ?? 'Arg'}</Cell>
|
||||
<Cell head last>Value</Cell>
|
||||
</tr>
|
||||
@@ -79,7 +80,7 @@ ${Object.entries(args)
|
||||
</svelte:fragment>
|
||||
</Head>
|
||||
|
||||
<tbody class="divide-y">
|
||||
<tbody class="divide-y w-full">
|
||||
{#if args && Object.keys(args).length > 0}
|
||||
{#each Object.entries(args).sort((a, b) => a[0].localeCompare(b[0])) as [arg, value]}
|
||||
<Row>
|
||||
@@ -137,7 +138,7 @@ ${Object.entries(args)
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
{#if jsonStr.length > 100000 || (id && workspace && args && typeof args === 'object' && deepEqual( Object.keys(args), ['reason'] ) && args['reason'] == 'WINDMILL_TOO_BIG')}
|
||||
{#if jsonStr.length > 100000 || (id && workspace && args && isWindmillTooBigObject(args))}
|
||||
<div class="text-sm mb-2 text-tertiary">
|
||||
<a
|
||||
download="windmill-args.json"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
// Test
|
||||
let scriptProgress = undefined;
|
||||
let scriptProgress = undefined
|
||||
let testJobLoader: TestJobLoader
|
||||
let testIsLoading = false
|
||||
let testJob: Job | undefined = undefined
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
export async function runTest(args: any) {
|
||||
// Not defined if JobProgressBar not loaded
|
||||
if (jobProgressReset) jobProgressReset();
|
||||
if (jobProgressReset) jobProgressReset()
|
||||
|
||||
const val = mod.value
|
||||
// let jobId: string | undefined = undefined
|
||||
@@ -75,7 +75,7 @@
|
||||
script.content,
|
||||
script.language,
|
||||
args,
|
||||
$flowStore?.tag ?? script.tag
|
||||
$flowStore?.tag ?? (val.tag_override ? val.tag_override : script.tag)
|
||||
)
|
||||
} else if (val.type == 'flow') {
|
||||
await testJobLoader?.abstractRun(() =>
|
||||
@@ -103,8 +103,6 @@
|
||||
let forceJson = false
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<TestJobLoader
|
||||
toastError={noEditor}
|
||||
on:done={() => jobDone()}
|
||||
@@ -158,9 +156,14 @@
|
||||
/>
|
||||
</Pane>
|
||||
<Pane size={50} minSize={10} class="text-sm text-tertiary">
|
||||
{#if scriptProgress}
|
||||
<JobProgressBar job={testJob} bind:scriptProgress bind:reset={jobProgressReset} compact={true} />
|
||||
{/if}
|
||||
{#if scriptProgress}
|
||||
<JobProgressBar
|
||||
job={testJob}
|
||||
bind:scriptProgress
|
||||
bind:reset={jobProgressReset}
|
||||
compact={true}
|
||||
/>
|
||||
{/if}
|
||||
{#if testJob != undefined && 'result' in testJob && testJob.result != undefined}
|
||||
<div class="break-words relative h-full p-2">
|
||||
<DisplayResult
|
||||
@@ -186,7 +189,7 @@
|
||||
<div class="p-2">
|
||||
{#if testIsLoading}
|
||||
{#if !scriptProgress}
|
||||
<Loader2 class="animate-spin" />
|
||||
<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
{:else}
|
||||
Test to see the result here
|
||||
|
||||
@@ -66,10 +66,12 @@
|
||||
>
|
||||
{#if enabled}
|
||||
<div class="p-2 rounded border">
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Custom Name</span>
|
||||
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
|
||||
</label>
|
||||
{#if name != 'slack'}
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Custom Name</span>
|
||||
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
|
||||
</label>
|
||||
{/if}
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Client Id</span>
|
||||
<input type="text" placeholder="Client Id" bind:value={value['id']} />
|
||||
|
||||
@@ -9,6 +9,18 @@
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
|
||||
function updateChangesMade() {
|
||||
changesMade = JSON.stringify(alerts) !== JSON.stringify(originalAlerts)
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
const target = event.target
|
||||
console.log(target)
|
||||
if (target.tagName.toLowerCase() === 'input') {
|
||||
updateChangesMade()
|
||||
}
|
||||
}
|
||||
|
||||
type Alert = {
|
||||
name: string
|
||||
tags_to_monitor: string[]
|
||||
@@ -70,16 +82,20 @@
|
||||
}
|
||||
|
||||
function startEditing(index) {
|
||||
if (editingIndex !== -1) {
|
||||
const success = saveAlert(editingIndex)
|
||||
if (!success) return
|
||||
}
|
||||
editingIndex = index
|
||||
updateWorkerTags()
|
||||
}
|
||||
|
||||
function saveAlert(index) {
|
||||
function saveAlert(index): boolean {
|
||||
const newAlert = alerts[index]
|
||||
|
||||
if (newAlert.tags_to_monitor.length === 0) {
|
||||
sendUserToast('Please add at least one tag before saving.', true)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -88,7 +104,7 @@
|
||||
newAlert.alert_time_threshold_seconds <= 0
|
||||
) {
|
||||
sendUserToast('All numeric values must be strictly positive.', true)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
const alertExists = originalAlerts.some(
|
||||
@@ -99,13 +115,15 @@
|
||||
)
|
||||
|
||||
if (alertExists) {
|
||||
sendUserToast('An identical alert already exists.', true)
|
||||
return
|
||||
sendUserToast('You can only define one alert per identical set of tags', true)
|
||||
return false
|
||||
}
|
||||
|
||||
editingIndex = -1
|
||||
changesMade = JSON.stringify(alerts) !== JSON.stringify(originalAlerts)
|
||||
updateChangesMade()
|
||||
|
||||
stagedNewAlert = false
|
||||
return true
|
||||
}
|
||||
|
||||
function stageDeleteAlert(index) {
|
||||
@@ -126,16 +144,18 @@
|
||||
}
|
||||
newTag = ''
|
||||
filteredTags = []
|
||||
updateChangesMade()
|
||||
}
|
||||
|
||||
function removeTag(alertIndex, tag) {
|
||||
alerts[alertIndex].tags_to_monitor = alerts[alertIndex].tags_to_monitor.filter((t) => t !== tag)
|
||||
updateChangesMade()
|
||||
}
|
||||
|
||||
async function applyConfig() {
|
||||
if (stagedNewAlert) {
|
||||
sendUserToast('New alert staged. Please save or cancel the changes before applying.', true)
|
||||
return
|
||||
if (editingIndex !== -1) {
|
||||
const success = saveAlert(editingIndex)
|
||||
if (!success) return
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -146,6 +166,7 @@
|
||||
removedAlerts = []
|
||||
changesMade = false
|
||||
stagedNewAlert = false
|
||||
editingIndex = -1
|
||||
} catch (error) {
|
||||
console.error('Failed to update config:', error)
|
||||
}
|
||||
@@ -167,7 +188,7 @@
|
||||
}
|
||||
|
||||
const newAlert = {
|
||||
name: 'New Alert',
|
||||
name: 'Job Queue Alert',
|
||||
tags_to_monitor: [],
|
||||
jobs_num_threshold: 3,
|
||||
alert_cooldown_seconds: 600,
|
||||
@@ -177,6 +198,7 @@
|
||||
alerts = [...alerts, newAlert]
|
||||
editingIndex = alerts.length - 1
|
||||
stagedNewAlert = true
|
||||
updateChangesMade()
|
||||
updateWorkerTags()
|
||||
}
|
||||
|
||||
@@ -188,28 +210,35 @@
|
||||
alerts[alertIndex].tags_to_monitor = [
|
||||
...new Set([...alerts[alertIndex].tags_to_monitor, ...workerTags])
|
||||
]
|
||||
alerts = alerts // Trigger reactivity
|
||||
alerts = [...alerts]
|
||||
updateChangesMade()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size="800px">
|
||||
<DrawerContent title="Queue Metrics" on:close={drawer.closeDrawer}>
|
||||
<DrawerContent
|
||||
title="Queues"
|
||||
on:close={drawer.closeDrawer}
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups#queue-metrics"
|
||||
>
|
||||
<Section
|
||||
label="Queue Alert Settings"
|
||||
collapsable={true}
|
||||
tooltip="Manage queue alerts for monitoring queue metrics. These alerts will trigger a critical alert notification if the queue metrics are met."
|
||||
tooltip="A critical alert is triggered when the number of jobs in the queue exceeds the set threshold and they have been waiting for at least the specified time. After an alert, no new alerts will be triggered during the cooldown period."
|
||||
eeOnly={true}
|
||||
>
|
||||
{#if $enterpriseLicense}
|
||||
{#if changesMade}
|
||||
<div class="text-red-600 text-xs whitespace-nowrap pb-2">Non applied changes</div>
|
||||
<div class="flex gap-2 pb-2">
|
||||
<Button color="blue" size="xs" on:click={applyConfig}>
|
||||
<SaveIcon size={16} /> Apply Config
|
||||
</Button>
|
||||
<Button color="light" size="xs" on:click={cancelChanges}>Cancel</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-2 pb-2">
|
||||
<Button color="blue" size="xs" on:click={applyConfig} disabled={!changesMade}>
|
||||
<SaveIcon size={16} /> Apply Config
|
||||
</Button>
|
||||
<Button color="light" size="xs" on:click={cancelChanges} disabled={!changesMade}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if alerts.length > 0}
|
||||
<div>
|
||||
@@ -217,12 +246,6 @@
|
||||
<table class="w-full border-collapse mb-2 text-xs table-auto">
|
||||
<thead class="bg-gray-200 dark:bg-slate-600 text-left text-xs">
|
||||
<tr>
|
||||
<th class="p-2 min-w-[120px]">
|
||||
Name
|
||||
<Tooltip
|
||||
markdownTooltip="The name of the alert. This will be used in the crticial alert notification and help identify the alert."
|
||||
/>
|
||||
</th>
|
||||
<th class="p-2 w-full">
|
||||
Queue Tags to Monitor
|
||||
<Tooltip markdownTooltip="Queue tags to monitor for this alert." />
|
||||
@@ -230,38 +253,31 @@
|
||||
<th class="p-2 min-w-[65px]">
|
||||
Jobs
|
||||
<Tooltip
|
||||
markdownTooltip="Number of jobs threshold. An alert will be triggerd if more than this number of jobs are in the queue for at least the time threshold."
|
||||
markdownTooltip="Number of jobs threshold: An alert will be triggered if the number of jobs in the queue exceeds this threshold and they have been waiting for at least the specified time threshold."
|
||||
/>
|
||||
</th>
|
||||
<th class="p-2 min-w-[115px]">
|
||||
Cooldown (s)
|
||||
<Tooltip
|
||||
markdownTooltip="Cooldown period in seconds. The cooldown period is the time after an alert is triggered that no new alerts will be triggered."
|
||||
markdownTooltip="Cooldown period in seconds: This defines the time interval after an alert is triggered during which no additional alerts will be sent."
|
||||
/>
|
||||
</th>
|
||||
<th class="p-2 min-w-[105px]">
|
||||
Time (s)
|
||||
<Tooltip
|
||||
markdownTooltip="Time threshold in seconds. An alert will be triggered if more than 'n jobs threshold' have been in the queue for at least this amount of time."
|
||||
markdownTooltip="Time threshold in seconds: An alert will be triggered if the number of jobs in the queue exceeds the job threshold and they have remained in the queue for at least this duration."
|
||||
/>
|
||||
</th>
|
||||
<th class="p-2 min-w-[100px]"> Actions </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody on:input={handleInput}>
|
||||
{#each alerts as alert, index}
|
||||
<tr
|
||||
class={removedAlerts.includes(alert)
|
||||
? 'bg-red-100 dark:bg-red-900 pointer-events-none opacity-50'
|
||||
: ''}
|
||||
>
|
||||
<td class="border p-2">
|
||||
{#if editingIndex === index}
|
||||
<input type="text" bind:value={alert.name} class="w-full p-1" />
|
||||
{:else}
|
||||
{alert.name}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="border p-2">
|
||||
{#if editingIndex === index}
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
@@ -282,8 +298,9 @@
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newTag}
|
||||
placeholder="Add tag"
|
||||
placeholder="{workerTags.length === alert.tags_to_monitor.length ? 'All tags already added' : 'Add tag from dropdown' }"
|
||||
on:input={(e) => filterTags(e)}
|
||||
disabled={workerTags.length === alert.tags_to_monitor.length}
|
||||
class="p-1 flex-grow mr-1"
|
||||
/>
|
||||
<button on:click={() => addTag(index, newTag)} aria-label="Add Tag">
|
||||
@@ -294,6 +311,7 @@
|
||||
<button
|
||||
on:click={() => addAllTags(index)}
|
||||
class="text-xs hover:bg-gray-200 dark:hover:bg-gray-700 rounded px-2 py-1 mt-1"
|
||||
disabled={workerTags.length === alert.tags_to_monitor.length}
|
||||
>
|
||||
Add All Tags
|
||||
</button>
|
||||
@@ -302,6 +320,7 @@
|
||||
class="autocomplete-list border max-h-36 overflow-y-auto absolute z-50"
|
||||
>
|
||||
{#each filteredTags as tag}
|
||||
{#if !alert.tags_to_monitor.includes(tag)}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
@@ -311,6 +330,7 @@
|
||||
{tag}
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
@@ -401,6 +421,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
</Section>
|
||||
<h1 class="pt-4">Queue Metrics</h1>
|
||||
<div class="p-8">
|
||||
<QueueMetricsDrawerInner />
|
||||
</div>
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
encodeState,
|
||||
formatCron,
|
||||
orderedJsonStringify,
|
||||
|
||||
type Value
|
||||
|
||||
} from '$lib/utils'
|
||||
import Path from './Path.svelte'
|
||||
import ScriptEditor from './ScriptEditor.svelte'
|
||||
@@ -92,7 +90,6 @@
|
||||
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
|
||||
let open: boolean = false // Is confirmation modal open
|
||||
|
||||
|
||||
let metadataOpen =
|
||||
!neverShowMeta &&
|
||||
(showMeta ||
|
||||
@@ -251,62 +248,74 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEditScript(stay: boolean, deployMsg?: string): Promise<void>{
|
||||
// Fetch latest version and fetch entire script after if needed
|
||||
let actual_parent_hash = (await ScriptService.getScriptLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: script.path,
|
||||
}))?.script_hash;
|
||||
async function handleEditScript(stay: boolean, deployMsg?: string): Promise<void> {
|
||||
// Fetch latest version and fetch entire script after if needed
|
||||
let actual_parent_hash: string | undefined = undefined
|
||||
|
||||
|
||||
// Usually when we create new script, we put current hash as a parent_hash
|
||||
// But if we specify parent_hash that is already used, than we get error
|
||||
// In order to fix it we make sure that client's understanding of parent_hash
|
||||
// is aligns with understanding of backend.
|
||||
if (script.parent_hash == actual_parent_hash) {
|
||||
// Handle directly
|
||||
await editScript(stay, actual_parent_hash, deployMsg);
|
||||
} else {
|
||||
|
||||
// Fetch entire script, since we need it to show Diff
|
||||
await syncWithDeployed()
|
||||
|
||||
// Handle through confirmation modal
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
await editScript(stay, actual_parent_hash, deployMsg);
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
try {
|
||||
if (initialPath && initialPath != '') {
|
||||
actual_parent_hash = (
|
||||
await ScriptService.getScriptLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
)?.script_hash
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
|
||||
// Usually when we create new script, we put current hash as a parent_hash
|
||||
// But if we specify parent_hash that is already used, than we get error
|
||||
// In order to fix it we make sure that client's understanding of parent_hash
|
||||
// is aligns with understanding of backend.
|
||||
if (actual_parent_hash == undefined || script.parent_hash == actual_parent_hash) {
|
||||
// Handle directly
|
||||
await editScript(stay, script.parent_hash!, deployMsg)
|
||||
} else {
|
||||
// Fetch entire script, since we need it to show Diff
|
||||
await syncWithDeployed()
|
||||
|
||||
// Handle through confirmation modal
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
await editScript(stay, actual_parent_hash, deployMsg)
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
}
|
||||
|
||||
async function syncWithDeployed(){
|
||||
const latestScript = await ScriptService.getScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: script.path,
|
||||
withStarredInfo: true
|
||||
});
|
||||
async function syncWithDeployed() {
|
||||
const latestScript = await ScriptService.getScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
withStarredInfo: true
|
||||
})
|
||||
|
||||
deployedValue = {
|
||||
...latestScript,
|
||||
starred: undefined,
|
||||
workspace_id: undefined,
|
||||
archived: undefined,
|
||||
created_at: undefined,
|
||||
created_by: undefined,
|
||||
deleted: undefined,
|
||||
extra_perms: undefined,
|
||||
is_template: undefined,
|
||||
lock: undefined,
|
||||
lock_error_logs: undefined,
|
||||
parent_hashes: undefined,
|
||||
};
|
||||
deployedValue = {
|
||||
...latestScript,
|
||||
starred: undefined,
|
||||
workspace_id: undefined,
|
||||
archived: undefined,
|
||||
created_at: undefined,
|
||||
created_by: undefined,
|
||||
deleted: undefined,
|
||||
extra_perms: undefined,
|
||||
is_template: undefined,
|
||||
lock: undefined,
|
||||
lock_error_logs: undefined,
|
||||
parent_hashes: undefined
|
||||
}
|
||||
|
||||
deployedBy = latestScript.created_by;
|
||||
deployedBy = latestScript.created_by
|
||||
}
|
||||
|
||||
async function editScript(stay: boolean, parentHash: string, deploymentMsg?: string, ): Promise<void> {
|
||||
async function editScript(
|
||||
stay: boolean,
|
||||
parentHash: string,
|
||||
deploymentMsg?: string
|
||||
): Promise<void> {
|
||||
loadingSave = true
|
||||
try {
|
||||
try {
|
||||
@@ -354,7 +363,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
console.log('initialPath', initialPath)
|
||||
const scheduleExists =
|
||||
initialPath != '' &&
|
||||
(await ScheduleService.existsSchedule({
|
||||
|
||||
@@ -22,11 +22,12 @@
|
||||
import ChangeInstanceUsername from './ChangeInstanceUsername.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import InstanceNameEditor from './InstanceNameEditor.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
let drawer: Drawer
|
||||
let filter = ''
|
||||
|
||||
export function openDrawer() {
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
drawer?.openDrawer?.()
|
||||
}
|
||||
|
||||
@@ -45,11 +46,14 @@
|
||||
let users: GlobalUserInfo[] = []
|
||||
let filteredUsers: GlobalUserInfo[] = []
|
||||
let deleteConfirmedCallback: (() => void) | undefined = undefined
|
||||
let activeOnly = false
|
||||
|
||||
async function listUsers(): Promise<void> {
|
||||
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000 })
|
||||
async function listUsers(activeOnly: boolean): Promise<void> {
|
||||
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000, activeOnly: activeOnly })
|
||||
}
|
||||
|
||||
$: listUsers(activeOnly)
|
||||
|
||||
let tab: 'users' | string = 'users'
|
||||
|
||||
let nbDisplayed = 50
|
||||
@@ -70,7 +74,7 @@
|
||||
})
|
||||
getAutomateUsernameCreationSetting()
|
||||
sendUserToast('Automatic username creation enabled')
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
}
|
||||
|
||||
async function updateName(name: string | undefined, email: string) {
|
||||
@@ -82,7 +86,7 @@
|
||||
}
|
||||
})
|
||||
sendUserToast('User updated')
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
} catch (e) {
|
||||
sendUserToast('Error updating user', true)
|
||||
}
|
||||
@@ -96,7 +100,12 @@
|
||||
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
|
||||
/>
|
||||
|
||||
<Drawer bind:this={drawer} on:open={listUsers} size="1200px" on:close={removeHash}>
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
on:open={() => listUsers(activeOnly)}
|
||||
size="1200px"
|
||||
on:close={removeHash}
|
||||
>
|
||||
<DrawerContent overflow_y={true} title="Instance settings" on:close={closeDrawer}>
|
||||
<div class="flex flex-col h-full w-full">
|
||||
<div>
|
||||
@@ -130,11 +139,16 @@
|
||||
<div class="h-full">
|
||||
{#if !automateUsernameCreation && !isCloudHosted()}
|
||||
<div class="mb-4">
|
||||
<h3 class="mb-2">
|
||||
Automatic username creation
|
||||
</h3>
|
||||
<h3 class="mb-2"> Automatic username creation </h3>
|
||||
<div class="mb-2">
|
||||
<span class="text-primary text-sm">Automatically create a username for new users based on their email, shared across workspaces. <a target="_blank" href="https://www.windmill.dev/docs/advanced/instance_settings#automatic-username-creation">Learn more</a></span>
|
||||
<span class="text-primary text-sm"
|
||||
>Automatically create a username for new users based on their email, shared
|
||||
across workspaces. <a
|
||||
target="_blank"
|
||||
href="https://www.windmill.dev/docs/advanced/instance_settings#automatic-username-creation"
|
||||
>Learn more</a
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<Button
|
||||
btnClasses="w-auto"
|
||||
@@ -164,10 +178,20 @@
|
||||
{/if}
|
||||
|
||||
<div class="py-2 mb-4">
|
||||
<InviteGlobalUser on:new={listUsers} />
|
||||
<InviteGlobalUser on:new={() => listUsers(activeOnly)} />
|
||||
</div>
|
||||
|
||||
<h3>All instance users</h3>
|
||||
<div class="flex flex-row justify-between">
|
||||
<h3>All instance users</h3>
|
||||
<Toggle
|
||||
bind:checked={activeOnly}
|
||||
options={{
|
||||
left: 'Show active users only',
|
||||
leftTooltip:
|
||||
'An active user is a user who has performed at least one action in the last 30 days'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="pb-1" />
|
||||
<div>
|
||||
<input placeholder="Search users" bind:value={filter} class="input mt-1" />
|
||||
@@ -181,12 +205,15 @@
|
||||
{#if automateUsernameCreation}
|
||||
<th>username</th>
|
||||
{/if}
|
||||
{#if activeOnly}
|
||||
<th>kind</th>
|
||||
{/if}
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body" class="overflow-y-auto w-full h-full max-h-full">
|
||||
{#if filteredUsers && users}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username } (email)}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username, operator_only } (email)}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{login_type}</td>
|
||||
@@ -203,13 +230,22 @@
|
||||
{email}
|
||||
isConflict
|
||||
on:renamed={() => {
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
{#if activeOnly}
|
||||
<td>
|
||||
{#if operator_only}
|
||||
Operator only
|
||||
{:else}
|
||||
Developer
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
<td>
|
||||
<ToggleButtonGroup
|
||||
selected={super_admin}
|
||||
@@ -217,7 +253,7 @@
|
||||
console.log('BAR')
|
||||
if (email == $userStore?.email) {
|
||||
sendUserToast('You cannot demote yourself', true)
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
return
|
||||
}
|
||||
await UserService.globalUserUpdate({
|
||||
@@ -227,7 +263,7 @@
|
||||
}
|
||||
})
|
||||
sendUserToast('User updated')
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
>
|
||||
<ToggleButton value={false} size="xs" label="User" />
|
||||
@@ -244,7 +280,7 @@
|
||||
updateName(e.detail, email)
|
||||
}}
|
||||
on:renamed={() => {
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
{automateUsernameCreation}
|
||||
/>
|
||||
@@ -258,7 +294,7 @@
|
||||
deleteConfirmedCallback = async () => {
|
||||
await UserService.globalUserDelete({ email })
|
||||
sendUserToast(`User ${email} removed`)
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
+24
-2
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Copy, Plus, RefreshCcwIcon, Settings, Trash, X } from 'lucide-svelte'
|
||||
import { Alert, Button, Drawer } from './common'
|
||||
import { Alert, Badge, Button, Drawer } from './common'
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
@@ -18,6 +18,8 @@
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import YAML from 'yaml'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import type { AutoscalingConfig } from './worker_group'
|
||||
import AutoscalingConfigEditor from './AutoscalingConfigEditor.svelte'
|
||||
|
||||
export let name: string
|
||||
export let config:
|
||||
@@ -31,6 +33,7 @@
|
||||
additional_python_paths?: string[]
|
||||
pip_local_dependencies?: string[]
|
||||
min_alive_workers_alert_threshold?: number
|
||||
autoscaling?: AutoscalingConfig
|
||||
}
|
||||
export let activeWorkers: number
|
||||
export let customTags: string[] | undefined
|
||||
@@ -65,6 +68,7 @@
|
||||
additional_python_paths?: string[]
|
||||
pip_local_dependencies?: string[]
|
||||
min_alive_workers_alert_threshold?: number
|
||||
autoscaling?: AutoscalingConfig
|
||||
} = {}
|
||||
|
||||
function loadNConfig() {
|
||||
@@ -438,7 +442,7 @@
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<Multiselect
|
||||
outerDivClass="text-secondary !bg-surface-disabled"
|
||||
outerDivClass="text-secondary !bg-surface-disabled !border-0"
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:selected={selectedPriorityTags}
|
||||
on:change={(e) => {
|
||||
@@ -452,6 +456,9 @@
|
||||
delete nconfig.priority_tags[e.detail.option]
|
||||
}
|
||||
dirty = true
|
||||
} else if (e.detail.type === 'removeAll') {
|
||||
nconfig.priority_tags = undefined
|
||||
dirty = true
|
||||
} else {
|
||||
console.error(
|
||||
`Priority tags multiselect - unknown event type: '${e.detail.type}'`
|
||||
@@ -782,6 +789,21 @@
|
||||
</Section>
|
||||
<div class="mt-8" />
|
||||
|
||||
<Section label="Autoscaling" collapsable>
|
||||
<div slot="header" class="ml-4 flex flex-row gap-2 items-center">
|
||||
<Badge>Beta</Badge>
|
||||
{#if nconfig.autoscaling?.enabled}
|
||||
<Badge color="green">Enabled</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<AutoscalingConfigEditor
|
||||
on:dirty={() => (dirty = true)}
|
||||
worker_tags={nconfig.worker_tags}
|
||||
bind:config={nconfig.autoscaling}
|
||||
/>
|
||||
</Section>
|
||||
<div class="mt-8" />
|
||||
|
||||
<Section
|
||||
label="Init Script"
|
||||
tooltip="Bash scripts run at start of the workers. More lightweight than having to require the worker images at the cost of being run on every start."
|
||||
@@ -27,28 +27,29 @@ export type ColumnMetadata = {
|
||||
export type TableMetadata = ColumnMetadata[]
|
||||
|
||||
export type ColumnDef = {
|
||||
minWidth: number
|
||||
hide: boolean
|
||||
flex: number
|
||||
sort: 'asc' | 'desc'
|
||||
sortIndex: number
|
||||
aggFunc: string
|
||||
pivot: boolean
|
||||
pivotIndex: number
|
||||
pinned: 'left' | 'right' | boolean
|
||||
rowGroup: boolean
|
||||
rowGroupIndex: number
|
||||
valueFormatter: string
|
||||
valueParser: string
|
||||
field: string
|
||||
headerName: string
|
||||
minWidth?: number
|
||||
hide?: boolean
|
||||
flex?: number
|
||||
sort?: 'asc' | 'desc'
|
||||
sortIndex?: number
|
||||
aggFunc?: string
|
||||
pivot?: boolean
|
||||
pivotIndex?: number
|
||||
pinned?: 'left' | 'right' | boolean
|
||||
rowGroup?: boolean
|
||||
rowGroupIndex?: number
|
||||
valueFormatter?: string
|
||||
valueParser?: string
|
||||
field?: string
|
||||
headerName?: string
|
||||
children?: ColumnDef[]
|
||||
// DBExplorer
|
||||
ignored: boolean
|
||||
hideInsert: boolean
|
||||
editable: boolean
|
||||
overrideDefaultValue: boolean
|
||||
defaultUserValue: any
|
||||
defaultValueNull: boolean
|
||||
ignored?: boolean
|
||||
hideInsert?: boolean
|
||||
editable?: boolean
|
||||
overrideDefaultValue?: boolean
|
||||
defaultUserValue?: any
|
||||
defaultValueNull?: boolean
|
||||
} & ColumnMetadata
|
||||
|
||||
export async function loadTableMetaData(
|
||||
|
||||
+18
-3
@@ -150,7 +150,7 @@
|
||||
outputs?.inputs.set(inputs, true)
|
||||
},
|
||||
onRemove: (id, rowIndex) => {
|
||||
if (inputs?.[id] == undefined) {
|
||||
if (inputs?.[id] == undefined) {
|
||||
return
|
||||
}
|
||||
delete inputs[id][rowIndex]
|
||||
@@ -267,10 +267,24 @@
|
||||
|
||||
// Validate each column definition
|
||||
columnDefs.forEach((colDef, index) => {
|
||||
let noField = !colDef.field || typeof colDef.field !== 'string' || colDef.field.trim() === ''
|
||||
|
||||
// Check if 'field' property exists and is a non-empty string
|
||||
if (!colDef.field || typeof colDef.field !== 'string' || colDef.field.trim() === '') {
|
||||
if (noField && !(colDef.children && Array.isArray(colDef.children))) {
|
||||
isValid = false
|
||||
errors.push(`Column at index ${index} is missing a valid 'field' property.`)
|
||||
errors.push(
|
||||
`Column at index ${index} is missing a valid 'field' property nor having any children.`
|
||||
)
|
||||
}
|
||||
|
||||
if (colDef.children && Array.isArray(colDef.children)) {
|
||||
const { isValid: isChildrenValid, errors: childrenErrors } = validateColumnDefs(
|
||||
colDef.children
|
||||
)
|
||||
if (!isChildrenValid) {
|
||||
isValid = false
|
||||
errors.push(...childrenErrors.map((err) => `Error in children at index ${index}: ${err}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -310,6 +324,7 @@
|
||||
: false,
|
||||
initialState: state,
|
||||
suppressRowDeselection: true,
|
||||
enableCellTextSelection: true,
|
||||
...(resolvedConfig?.extraConfig ?? {}),
|
||||
onViewportChanged: (e) => {
|
||||
firstRow = e.firstRow
|
||||
|
||||
@@ -334,6 +334,7 @@
|
||||
initialState: state,
|
||||
suppressRowDeselection: true,
|
||||
suppressDragLeaveHidesColumns: true,
|
||||
enableCellTextSelection: true,
|
||||
...(resolvedConfig?.extraConfig ?? {}),
|
||||
onStateUpdated: (e) => {
|
||||
state = e?.api?.getState()
|
||||
|
||||
@@ -368,18 +368,17 @@
|
||||
}
|
||||
|
||||
async function handleUpdateApp(npath: string) {
|
||||
|
||||
// We have to make sure there is no updates when we clicked the button
|
||||
await compareVersions();
|
||||
await compareVersions()
|
||||
|
||||
if (onLatest) {
|
||||
// Handle directly
|
||||
await updateApp(npath)
|
||||
} else {
|
||||
// There is onLatest, but we need more information while deploying
|
||||
// We need it to show diff
|
||||
// There is onLatest, but we need more information while deploying
|
||||
// We need it to show diff
|
||||
// Handle through confirmation modal
|
||||
await syncWithDeployed();
|
||||
await syncWithDeployed()
|
||||
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
@@ -628,13 +627,18 @@
|
||||
if (version === undefined) {
|
||||
return
|
||||
}
|
||||
const appVersion = await AppService.getAppLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: appPath
|
||||
})
|
||||
onLatest = version === appVersion?.version
|
||||
|
||||
try {
|
||||
const appVersion = await AppService.getAppLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: appPath
|
||||
})
|
||||
onLatest = version === appVersion?.version
|
||||
} catch (e) {
|
||||
console.error('Error comparing versions', e)
|
||||
onLatest = true
|
||||
}
|
||||
}
|
||||
|
||||
$: saveDrawerOpen && compareVersions()
|
||||
|
||||
let selectedJobId: string | undefined = undefined
|
||||
@@ -764,7 +768,7 @@
|
||||
}
|
||||
|
||||
// deployedValue should be syncronized when we open Diff
|
||||
await syncWithDeployed();
|
||||
await syncWithDeployed()
|
||||
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
@@ -955,7 +959,7 @@
|
||||
return
|
||||
}
|
||||
// deployedValue should be syncronized when we open Diff
|
||||
await syncWithDeployed();
|
||||
await syncWithDeployed()
|
||||
|
||||
saveDrawerOpen = false
|
||||
diffDrawer?.openDrawer()
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
|
||||
<svelte:window on:hashchange={hashchange} on:resize={resizeWindow} />
|
||||
|
||||
<div class="relative h-full grow" bind:clientHeight={appHeight}>
|
||||
<div class="relative min-h-full grow" bind:clientHeight={appHeight}>
|
||||
<div id="app-editor-top-level-drawer" />
|
||||
<div id="app-editor-select" />
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Tabs, Tab, TabContent } from '$lib/components/common'
|
||||
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import FlowViewerInner from '../FlowViewerInner.svelte'
|
||||
import DetailPageTriggerPanel from './DetailPageTriggerPanel.svelte'
|
||||
@@ -22,51 +21,48 @@
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<Splitpanes horizontal class="h-full">
|
||||
<Pane size={100}>
|
||||
<Tabs bind:selected>
|
||||
<Tab value="saved_inputs">Saved Inputs</Tab>
|
||||
{#if !isOperator}
|
||||
<Tab value="triggers">Triggers</Tab>
|
||||
{/if}
|
||||
{#if flow_json}
|
||||
<Tab value="raw">Export</Tab>
|
||||
{:else}
|
||||
<Tab value="script">Script</Tab>
|
||||
{/if}
|
||||
{#if selected == 'flow_step'}
|
||||
<Tab value="flow_step">Step</Tab>
|
||||
{/if}
|
||||
<Tabs bind:selected>
|
||||
<Tab value="saved_inputs">Saved Inputs</Tab>
|
||||
{#if !isOperator}
|
||||
<Tab value="triggers">Triggers</Tab>
|
||||
{/if}
|
||||
{#if flow_json}
|
||||
<Tab value="raw">Export</Tab>
|
||||
{:else}
|
||||
<Tab value="script">Script</Tab>
|
||||
{/if}
|
||||
{#if selected == 'flow_step'}
|
||||
<Tab value="flow_step">Step</Tab>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<div class="overflow-hidden" style="height:calc(100% - 32px);">
|
||||
<TabContent value="saved_inputs" class="flex flex-col flex-1 h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="schema" class="flex flex-col flex-1 h-full">
|
||||
<slot name="schema" />
|
||||
</TabContent>
|
||||
<TabContent value="script" class="flex flex-col flex-1 h-full">
|
||||
<slot name="script" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full pt-2">
|
||||
<DetailPageTriggerPanel bind:triggerSelected>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="websockets" name="websockets" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
</DetailPageTriggerPanel>
|
||||
</TabContent>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
<div class="h-full overflow-auto">
|
||||
<TabContent value="saved_inputs" class="h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="script" class="h-full">
|
||||
<slot name="script" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="h-full pt-2">
|
||||
<DetailPageTriggerPanel bind:triggerSelected>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="websockets" name="websockets" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
</DetailPageTriggerPanel>
|
||||
</TabContent>
|
||||
{#if flow_json}
|
||||
<TabContent value="raw" class="flex flex-col flex-1 h-full overflow-auto p-2">
|
||||
<FlowViewerInner flow={flow_json} />
|
||||
</TabContent>
|
||||
<TabContent value="flow_step" class="flex flex-col flex-1 h-full">
|
||||
<slot name="flow_step" />
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</SplitPanesWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="h-full">
|
||||
<div class="h-full w-full">
|
||||
<slot name="header" />
|
||||
<Tabs bind:selected={mobileTab}>
|
||||
<Tab value="form">Run form</Tab>
|
||||
@@ -74,27 +74,29 @@
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="form" class="flex flex-col flex-1 h-full">
|
||||
<slot name="form" />
|
||||
</TabContent>
|
||||
<div class="h-full">
|
||||
<TabContent value="form" class="flex flex-col flex-1 h-full">
|
||||
<slot name="form" />
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="saved_inputs" class="flex flex-col flex-1 h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full">
|
||||
<DetailPageTriggerPanel bind:triggerSelected={$selectedTriggerStore}>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="script" name="script" />
|
||||
<slot slot="websockets" name="websockets" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
</DetailPageTriggerPanel>
|
||||
</TabContent>
|
||||
<TabContent value="script" class="flex flex-col flex-1 h-full">
|
||||
<slot name="script" />
|
||||
</TabContent>
|
||||
<TabContent value="saved_inputs" class="flex flex-col flex-1 h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full">
|
||||
<DetailPageTriggerPanel bind:triggerSelected={$selectedTriggerStore}>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="script" name="script" />
|
||||
<slot slot="websockets" name="websockets" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
</DetailPageTriggerPanel>
|
||||
</TabContent>
|
||||
<TabContent value="script" class="flex flex-col flex-1 h-full">
|
||||
<slot name="script" />
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{#if tag}
|
||||
<option value="">reset to default</option>
|
||||
{:else}
|
||||
<option value="" disabled selected>Tag</option>
|
||||
<option value={undefined} disabled selected>Tag</option>
|
||||
{/if}
|
||||
{#each $workerTags ?? [] as tag (tag)}
|
||||
<option value={tag}>{tag}</option>
|
||||
|
||||
@@ -441,7 +441,7 @@
|
||||
}}
|
||||
options={{
|
||||
right: `Label as high priority`,
|
||||
rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. ${
|
||||
rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. Higher priority numbers are executed first. ${
|
||||
!$enterpriseLicense ? 'This is a feature only available on enterprise edition.' : ''
|
||||
}`
|
||||
}}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
}
|
||||
|
||||
if (kind == 'approval') {
|
||||
module.suspend = { required_events: 1 }
|
||||
module.suspend = { required_events: 1, timeout: 1800 }
|
||||
} else if (kind == 'trigger') {
|
||||
module.stop_after_if = {
|
||||
expr: '!result || (Array.isArray(result) && result.length == 0)',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let height = '24px'
|
||||
export let width = '24px'
|
||||
</script>
|
||||
|
||||
<svg {width} {height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
|
||||
<path fill="#1ed760" d="M248 8C111.1 8 0 119.1 0 256s111.1 248 248 248 248-111.1 248-248S384.9 8 248 8Z"/>
|
||||
<path d="M406.6 231.1c-5.2 0-8.4-1.3-12.9-3.9-71.2-42.5-198.5-52.7-280.9-29.7-3.6 1-8.1 2.6-12.9 2.6-13.2 0-23.3-10.3-23.3-23.6 0-13.6 8.4-21.3 17.4-23.9 35.2-10.3 74.6-15.2 117.5-15.2 73 0 149.5 15.2 205.4 47.8 7.8 4.5 12.9 10.7 12.9 22.6 0 13.6-11 23.3-23.2 23.3zm-31 76.2c-5.2 0-8.7-2.3-12.3-4.2-62.5-37-155.7-51.9-238.6-29.4-4.8 1.3-7.4 2.6-11.9 2.6-10.7 0-19.4-8.7-19.4-19.4s5.2-17.8 15.5-20.7c27.8-7.8 56.2-13.6 97.8-13.6 64.9 0 127.6 16.1 177 45.5 8.1 4.8 11.3 11 11.3 19.7-.1 10.8-8.5 19.5-19.4 19.5zm-26.9 65.6c-4.2 0-6.8-1.3-10.7-3.6-62.4-37.6-135-39.2-206.7-24.5-3.9 1-9 2.6-11.9 2.6-9.7 0-15.8-7.7-15.8-15.8 0-10.3 6.1-15.2 13.6-16.8 81.9-18.1 165.6-16.5 237 26.2 6.1 3.9 9.7 7.4 9.7 16.5s-7.1 15.4-15.2 15.4z"/>
|
||||
</svg>
|
||||
@@ -91,6 +91,7 @@ import VismaIcon from './VismaIcon.svelte'
|
||||
import JumpCloudIcon from './JumpCloudIcon.svelte'
|
||||
import KeycloakIcon from './KeycloakIcon.svelte'
|
||||
import ZitadelIcon from './ZitadelIcon.svelte'
|
||||
import SpotifyIcon from './SpotifyIcon.svelte'
|
||||
|
||||
export const APP_TO_ICON_COMPONENT = {
|
||||
postgresql: PostgresIcon,
|
||||
@@ -188,7 +189,8 @@ export const APP_TO_ICON_COMPONENT = {
|
||||
visma: VismaIcon,
|
||||
jumpcloud: JumpCloudIcon,
|
||||
keycloak: KeycloakIcon,
|
||||
zitadel: ZitadelIcon
|
||||
zitadel: ZitadelIcon,
|
||||
spotify: SpotifyIcon
|
||||
} as const
|
||||
|
||||
export {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
export function isWindmillTooBigObject(obj: any): boolean {
|
||||
return (
|
||||
typeof obj === 'object' &&
|
||||
deepEqual(Object.keys(obj), ['reason']) &&
|
||||
obj['reason'] == 'WINDMILL_TOO_BIG'
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// enabled: bool,
|
||||
// min_workers: usize,
|
||||
// max_workers: usize,
|
||||
// cooldown_seconds: usize,
|
||||
// inc_scale_jobs_waiting: usize,
|
||||
// full_scale_cooldown_seconds: usize,
|
||||
// full_scale_jobs_waiting: usize,
|
||||
// dec_scale_occupancy_rate: u8, // occupancy rate of 30s, 5m, 30m to scale down
|
||||
// inc_scale_occupancy_rate: u8, // occupancy rate of 30s, 5m to scale up
|
||||
// inc_percent: usize,
|
||||
// integration: Option<AutoscalingIntegration>,
|
||||
|
||||
export type AutoscalingConfig = {
|
||||
enabled: boolean
|
||||
min_workers: number
|
||||
max_workers: number
|
||||
cooldown_seconds?: number
|
||||
inc_scale_num_jobs_waiting?: number
|
||||
full_scale_cooldown_seconds?: number
|
||||
full_scale_jobs_waiting?: number
|
||||
dec_scale_occupancy_rate?: number
|
||||
inc_scale_occupancy_rate?: number
|
||||
inc_num_workers?: number
|
||||
integration?: AutoscalingIntegration
|
||||
custom_tags?: string[]
|
||||
}
|
||||
|
||||
|
||||
export type AutoscalingIntegration = AutoscaleScript | AutoscaleDryRun
|
||||
|
||||
export type AutoscaleScript = {
|
||||
type: 'script'
|
||||
tag?: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export type AutoscaleDryRun = {
|
||||
type: 'dryrun'
|
||||
}
|
||||
@@ -635,9 +635,11 @@ inventory:
|
||||
|
||||
# File resources will be written in the relative \`target\` location before
|
||||
# running the playbook
|
||||
# file_resources:
|
||||
# files:
|
||||
# - resource: u/user/fabulous_jinja_template
|
||||
# target: ./config_template.j2
|
||||
# - variable: u/user/ssh_key
|
||||
# target: ./ssh_key
|
||||
|
||||
# Define the arguments of the windmill script
|
||||
extra_vars:
|
||||
|
||||
@@ -472,7 +472,7 @@ export async function copyToClipboard(value?: string, sendToast = true): Promise
|
||||
}
|
||||
|
||||
export function pluralize(quantity: number, word: string, customPlural?: string) {
|
||||
if (quantity <= 1) {
|
||||
if (quantity == 1) {
|
||||
return `${quantity} ${word}`
|
||||
} else if (customPlural) {
|
||||
return `${quantity} ${customPlural}}`
|
||||
@@ -498,7 +498,7 @@ export function isObject(obj: any) {
|
||||
|
||||
export function debounce(func: (...args: any[]) => any, wait: number) {
|
||||
let timeout: any
|
||||
return function(...args: any[]) {
|
||||
return function (...args: any[]) {
|
||||
// @ts-ignore
|
||||
const context = this
|
||||
clearTimeout(timeout)
|
||||
@@ -508,7 +508,7 @@ export function debounce(func: (...args: any[]) => any, wait: number) {
|
||||
|
||||
export function throttle<T>(func: (...args: any[]) => T, wait: number) {
|
||||
let timeout: any
|
||||
return function(...args: any[]) {
|
||||
return function (...args: any[]) {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
type WorkflowStatus,
|
||||
type NewScript,
|
||||
ConcurrencyGroupsService,
|
||||
MetricsService
|
||||
MetricsService,
|
||||
type ScriptArgs
|
||||
} from '$lib/gen'
|
||||
import {
|
||||
canWrite,
|
||||
@@ -91,6 +92,8 @@
|
||||
import HighlightTheme from '$lib/components/HighlightTheme.svelte'
|
||||
import PreprocessedArgsDisplay from '$lib/components/runs/PreprocessedArgsDisplay.svelte'
|
||||
import ExecutionDuration from '$lib/components/ExecutionDuration.svelte'
|
||||
import CustomPopover from '$lib/components/CustomPopover.svelte'
|
||||
import { isWindmillTooBigObject } from '$lib/components/job_args'
|
||||
|
||||
let job: Job | undefined
|
||||
let jobUpdateLastFetch: Date | undefined
|
||||
@@ -306,6 +309,46 @@
|
||||
}
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
let runImmediatelyLoading = false
|
||||
async function runImmediately() {
|
||||
runImmediatelyLoading = true
|
||||
try {
|
||||
let args = job?.args as ScriptArgs
|
||||
if (isWindmillTooBigObject(args)) {
|
||||
args = (await JobService.getJobArgs({
|
||||
workspace: $workspaceStore!,
|
||||
id: job?.id!
|
||||
})) as ScriptArgs
|
||||
}
|
||||
|
||||
const commonArgs = {
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: args
|
||||
}
|
||||
if (job?.job_kind == 'script' || job?.job_kind == 'flow') {
|
||||
let id
|
||||
|
||||
if (job?.job_kind == 'script') {
|
||||
id = await JobService.runScriptByHash({
|
||||
...commonArgs,
|
||||
hash: job.script_hash!
|
||||
})
|
||||
} else {
|
||||
id = await JobService.runFlowByPath({
|
||||
...commonArgs,
|
||||
path: job.script_path!
|
||||
})
|
||||
}
|
||||
|
||||
await goto('/run/' + id + '?workspace=' + $workspaceStore)
|
||||
} else {
|
||||
sendUserToast('Cannot run this job immediately', true)
|
||||
}
|
||||
} finally {
|
||||
runImmediatelyLoading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -595,14 +638,23 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
|
||||
<Button
|
||||
on:click|once={() => {
|
||||
goto(viewHref + `#${computeSharableHash(job?.args)}`)
|
||||
}}
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: RefreshCw }}>Run again</Button
|
||||
>
|
||||
<CustomPopover noPadding appearTimeout={0}>
|
||||
<Button
|
||||
on:click|once={() => {
|
||||
goto(viewHref + `#${computeSharableHash(job?.args)}`)
|
||||
}}
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: RefreshCw }}>Run again</Button
|
||||
>
|
||||
<svelte:fragment slot="overlay">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button size="xs" loading={runImmediatelyLoading} on:click={() => runImmediately()}>
|
||||
Run immediately with same args
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CustomPopover>
|
||||
{/if}
|
||||
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
|
||||
{#if !$userStore?.operator}
|
||||
|
||||
@@ -742,7 +742,7 @@
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="script">
|
||||
<div>
|
||||
<div class="h-full">
|
||||
<Skeleton {loading} layout={[[20]]} />
|
||||
|
||||
<Tabs selected="code">
|
||||
@@ -751,7 +751,7 @@
|
||||
<Tab value="schema" size="xs">Schema</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="code">
|
||||
<div class="p-2 w-full overflow-auto">
|
||||
<div class="p-2 w-full">
|
||||
<HighlightCode
|
||||
language={script.language}
|
||||
code={script.content}
|
||||
@@ -785,7 +785,7 @@
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="schema">
|
||||
<div class="p-1 relative">
|
||||
<div class="p-1 relative h-full">
|
||||
<button
|
||||
on:click={() => copyToClipboard(JSON.stringify(script?.schema, null, 4))}
|
||||
class="absolute top-2 right-2"
|
||||
|
||||
@@ -13,17 +13,18 @@
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
import Head from '$lib/components/table/Head.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import WorkspaceGroup from '$lib/components/WorkspaceGroup.svelte'
|
||||
import WorkspaceGroup from '$lib/components/WorkerGroup.svelte'
|
||||
import { WorkerService, type WorkerPing, ConfigService, SettingService } from '$lib/gen'
|
||||
import { enterpriseLicense, superadmin } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { displayDate, groupBy, pluralize, truncate } from '$lib/utils'
|
||||
import { AlertTriangle, CopyIcon, LineChart, List, Plus, Search } from 'lucide-svelte'
|
||||
import { AlertTriangle, LineChart, List, Plus, Search } from 'lucide-svelte'
|
||||
import { getContext, onDestroy, onMount } from 'svelte'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
|
||||
import YAML from 'yaml'
|
||||
import { DEFAULT_TAGS_WORKSPACES_SETTING } from '$lib/consts'
|
||||
import AutoscalingEvents from '$lib/components/AutoscalingEvents.svelte'
|
||||
|
||||
let workers: WorkerPing[] | undefined = undefined
|
||||
let workerGroups: Record<string, any> | undefined = undefined
|
||||
@@ -326,7 +327,7 @@
|
||||
<p>No workers seem to be available</p>
|
||||
{/if}
|
||||
|
||||
<div class="py-4 w-full flex justify-between"
|
||||
<div class="pt-4 pb-8 w-full flex justify-between items-center"
|
||||
><h4
|
||||
>{groupWorkers?.length} Worker Groups <Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
|
||||
@@ -336,6 +337,7 @@
|
||||
</Tooltip></h4
|
||||
>
|
||||
<div />
|
||||
|
||||
{#if $superadmin}
|
||||
<div class="flex flex-row items-center">
|
||||
<Popup
|
||||
@@ -344,31 +346,30 @@
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
color="light"
|
||||
startIcon={{ icon: CopyIcon }}
|
||||
on:click={() => {
|
||||
if (!workerGroups) {
|
||||
return sendUserToast('No worker groups found', true)
|
||||
}
|
||||
|
||||
const workersConfig = Object.entries(workerGroups).map(([name, config]) => ({
|
||||
name,
|
||||
...config
|
||||
}))
|
||||
navigator.clipboard.writeText(YAML.stringify(workersConfig))
|
||||
sendUserToast('Worker groups config copied to clipboard as YAML')
|
||||
}}
|
||||
>
|
||||
<span class="hidden md:block">Copy groups config</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: Plus }}
|
||||
nonCaptureEvent
|
||||
disabled={!$enterpriseLicense}
|
||||
dropdownItems={$enterpriseLicense
|
||||
? [
|
||||
{
|
||||
label: 'Copy groups config as YAML',
|
||||
onClick: () => {
|
||||
if (!workerGroups) {
|
||||
return sendUserToast('No worker groups found', true)
|
||||
}
|
||||
|
||||
const workersConfig = Object.entries(workerGroups).map(
|
||||
([name, config]) => ({
|
||||
name,
|
||||
...config
|
||||
})
|
||||
)
|
||||
navigator.clipboard.writeText(YAML.stringify(workersConfig))
|
||||
sendUserToast('Worker groups config copied to clipboard as YAML')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Import groups config from YAML',
|
||||
onClick: () => {
|
||||
@@ -378,7 +379,9 @@
|
||||
]
|
||||
: undefined}
|
||||
>
|
||||
<span class="hidden md:block">New group config</span>
|
||||
<span class="hidden md:block"
|
||||
>New group config {!$enterpriseLicense ? '(EE)' : ''}</span
|
||||
>
|
||||
|
||||
<Tooltip light>
|
||||
Worker Group configs are propagated to every workers in the worker group
|
||||
@@ -644,7 +647,8 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pb-4" />
|
||||
<div class="pb-20" />
|
||||
<AutoscalingEvents worker_group={selectedTab} />
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
{#each new Array(4) as _}
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
{#key app}
|
||||
<div
|
||||
class={twMerge(
|
||||
'min-h-screen h-full w-full',
|
||||
'min-h-screen h-full w-full flex',
|
||||
app?.value?.['css']?.['app']?.['viewer']?.class,
|
||||
'wm-app-viewer'
|
||||
)}
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.411.1"
|
||||
wmill_pg = ">=1.411.1"
|
||||
wmill = ">=1.414.1"
|
||||
wmill_pg = ">=1.414.1"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.411.1
|
||||
version: 1.414.1
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.411.1'
|
||||
ModuleVersion = '1.414.1'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.411.1"
|
||||
version = "1.414.1"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill-pg"
|
||||
version = "1.411.1"
|
||||
version = "1.414.1"
|
||||
description = "An extension client for the wmill client library focused on pg"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.411.1",
|
||||
"version": "1.414.1",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.411.1",
|
||||
"version": "1.414.1",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.411.1
|
||||
1.414.1
|
||||
|
||||
Reference in New Issue
Block a user