Merge branch 'main' into python-uv

This commit is contained in:
pyranota
2024-10-23 09:37:29 +00:00
committed by GitHub
106 changed files with 3860 additions and 811 deletions
+2 -2
View File
@@ -40,11 +40,11 @@ RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VER
RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=oven/bun:1.1.30 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.31 /usr/local/bin/bun /usr/bin/bun
ARG TARGETPLATFORM
RUN curl -Lsf https://github.com/denoland/deno/releases/download/v2.0.0/deno-x86_64-unknown-linux-gnu.zip -o deno.zip
RUN curl -Lsf https://github.com/denoland/deno/releases/download/v2.0.2/deno-x86_64-unknown-linux-gnu.zip -o deno.zip
# RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v2.0.0/deno-aarch64-unknown-linux-gnu.zip -o deno.zip || true
RUN unzip deno.zip && rm deno.zip && mv deno /usr/bin/deno
+3 -1
View File
@@ -26,6 +26,7 @@ permissions: write-all
jobs:
build:
runs-on: ubicloud
if: (github.event_name != 'issue_comment') || (contains(github.event.comment.body, '/buildimage_all') || contains(github.event.comment.body, '/buildimage_base'))
steps:
- uses: actions/checkout@v4
with:
@@ -87,6 +88,7 @@ jobs:
build_ee:
runs-on: ubicloud
if: (github.event_name != 'issue_comment') || (contains(github.event.comment.body, '/buildimage_ee') || contains(github.event.comment.body, '/buildimage_nsjail')) || contains(github.event.comment.body, '/buildimage_all')
steps:
- uses: actions/checkout@v4
with:
@@ -436,7 +438,7 @@ jobs:
build_ee_nsjail:
needs: [build_ee]
runs-on: ubicloud
if: github.event_name != 'pull_request'
if: (github.event_name != 'issue_comment') || (github.event_name != 'pull_request') || (contains(github.event.comment.body, '/buildimage_nsjail') || contains(github.event.comment.body, '/buildimage_all'))
steps:
- uses: actions/checkout@v4
with:
+72
View File
@@ -1,5 +1,77 @@
# Changelog
## [1.411.1](https://github.com/windmill-labs/windmill/compare/v1.411.0...v1.411.1) (2024-10-22)
### Bug Fixes
* update bun to 1.1.32 ([#4568](https://github.com/windmill-labs/windmill/issues/4568)) ([0586446](https://github.com/windmill-labs/windmill/commit/058644667129f0d79ec147aacdda449142ae0ab9))
## [1.411.0](https://github.com/windmill-labs/windmill/compare/v1.410.3...v1.411.0) (2024-10-21)
### Features
* **cli:** encrypt sensitive instance settings ([#4561](https://github.com/windmill-labs/windmill/issues/4561)) ([b8a6a11](https://github.com/windmill-labs/windmill/commit/b8a6a116354b10f5977e54edb365d6711e160538))
### Bug Fixes
* Do not ignore file resources with json file ext ([#4562](https://github.com/windmill-labs/windmill/issues/4562)) ([2079b2e](https://github.com/windmill-labs/windmill/commit/2079b2e7e19aa2fe327f2ae66d1b5eba988b9b0a))
* update bun to 1.1.31 and deno to 2.0.2 ([0d90396](https://github.com/windmill-labs/windmill/commit/0d9039641b3348e75599937188c35dd89a000584))
## [1.410.3](https://github.com/windmill-labs/windmill/compare/v1.410.2...v1.410.3) (2024-10-20)
### Bug Fixes
* **go-client:** reduce runtime dependencies by bumping oai-codeen to v2.4.1 ([87f5c07](https://github.com/windmill-labs/windmill/commit/87f5c078dd63cd3e0ba3e719c2da7b83f1458e2c))
## [1.410.1](https://github.com/windmill-labs/windmill/compare/v1.410.0...v1.410.1) (2024-10-19)
### Bug Fixes
* **cli:** improve wmill init behavior ([26a40d1](https://github.com/windmill-labs/windmill/commit/26a40d19441aa816ee711ce30f3435dddd3542a7))
* **frontend:** improve display of error handlers ([a92a2fd](https://github.com/windmill-labs/windmill/commit/a92a2fd6fd67c11c04554a90b8ae6d7a0dd9067c))
## [1.410.1](https://github.com/windmill-labs/windmill/compare/v1.410.0...v1.410.1) (2024-10-19)
### Bug Fixes
* **cli:** improve wmill init behavior ([26a40d1](https://github.com/windmill-labs/windmill/commit/26a40d19441aa816ee711ce30f3435dddd3542a7))
## [1.410.0](https://github.com/windmill-labs/windmill/compare/v1.409.4...v1.410.0) (2024-10-18)
### Features
* **typescript-bun:** support relative imports without the .ts extension ([248fdc2](https://github.com/windmill-labs/windmill/commit/248fdc24a61aca946902b20b9d8187101a9b7bfa))
* websocket triggers ([#4505](https://github.com/windmill-labs/windmill/issues/4505)) ([8807e99](https://github.com/windmill-labs/windmill/commit/8807e99f06caf3e26c526eefdc83dbc2f7aa93ee))
### Bug Fixes
* cache js static assets by default ([08595c6](https://github.com/windmill-labs/windmill/commit/08595c6f14a89df1559974e8835e63881a1a9601))
* **frontend:** add back script lockfile to script details pae ([549b11d](https://github.com/windmill-labs/windmill/commit/549b11dcfb39a60acd6d3899fce51b31149f8594))
* improve cancelling of jobs on public apps for anonymous users ([d7cf5ea](https://github.com/windmill-labs/windmill/commit/d7cf5ea37db313e78865708bc22e87a1feea9e0d))
## [1.409.4](https://github.com/windmill-labs/windmill/compare/v1.409.3...v1.409.4) (2024-10-17)
### Bug Fixes
* fix flow viewer renderer outside of flow details and editor context ([59a1e67](https://github.com/windmill-labs/windmill/commit/59a1e67465cd21ded5539cb5b829bc0d4e7169ed))
## [1.409.3](https://github.com/windmill-labs/windmill/compare/v1.409.2...v1.409.3) (2024-10-17)
### Bug Fixes
* do not delete primary schedule of script/flow on redeploy even if schedule wasn't loaded ([c3c2fe4](https://github.com/windmill-labs/windmill/commit/c3c2fe462c52f48795a55025414b16f8d3d98fe0))
* **nsjail:** improve memory reading when using nsjail ([b7ad19b](https://github.com/windmill-labs/windmill/commit/b7ad19bb75bd885a254ebac778349c7d88af8326))
## [1.409.2](https://github.com/windmill-labs/windmill/compare/v1.409.1...v1.409.2) (2024-10-16)
+2 -2
View File
@@ -175,9 +175,9 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=builder /frontend/build /static_frontend
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=denoland/deno:2.0.0 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=denoland/deno:2.0.2 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.1.30 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET server_id = $1, last_server_ping = now() WHERE enabled IS TRUE AND workspace_id = $2 AND path = $3 AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') RETURNING true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "02424907504848e983bfa89eec343061932dc5b4b17cf13d5cf8d833aedbe6d5"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET enabled = $1, email = $2, edited_by = $3, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL\n WHERE path = $4 AND workspace_id = $5 RETURNING 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Bool",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "12a86755706ce030a0a9142da78d035d0c7d361240b60005cc864e4345eb0bc7"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1)",
"query": "SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE path = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
@@ -11,6 +11,7 @@
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
@@ -18,5 +19,5 @@
null
]
},
"hash": "9ebb9c16948a695a053068d9a1df0152691271ac33d8c82db47be11746ccbcef"
"hash": "2238aaed46031f71b17c1cabcef935a0d3b2ca6d057f446ed9b02d4386e2ddd9"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM websocket_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Bool",
"Text"
]
},
"nullable": [
null
]
},
"hash": "2e6165543e34216dfaedf6e10729f733cff812e62dea1c1bce8cefd3a3979b14"
}
@@ -0,0 +1,108 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now()) RETURNING *",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "url",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "enabled",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "filters",
"type_info": "JsonbArray"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Bool",
"JsonbArray",
"Varchar",
"Varchar"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false
]
},
"hash": "4e9668a46bad9e82baa51422946d373b18b6577198df7545c94bd19be3446775"
}
@@ -0,0 +1,98 @@
{
"db_name": "PostgreSQL",
"query": "SELECT *\n FROM websocket_trigger\n WHERE enabled IS TRUE AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "url",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "enabled",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "filters",
"type_info": "JsonbArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false
]
},
"hash": "5303cb9dd5903aa4791ef8e5e5881a50a832e65c8c9632e2e12cd9c2747f2fc7"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, edited_by = $6, email = $7, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL\n WHERE workspace_id = $8 AND path = $9",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Bool",
"JsonbArray",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "acbf74cf3302bfcf7615285070d3f8958932bb8a2dda715f1b9152ab44442780"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM websocket_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "d54840373df5da9662ed11ed0a605cddac517bdc06dca0a8be071330338e948a"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as \"websocket_used!\", EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as \"http_routes_used!\"",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "websocket_used!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "http_routes_used!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null
]
},
"hash": "db24f1f6ef1e26b7de7926bd3f32d1fa673c4970c25ff9ad98f0fdf199801b53"
}
@@ -0,0 +1,101 @@
{
"db_name": "PostgreSQL",
"query": "SELECT *\n FROM websocket_trigger\n WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "url",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "enabled",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "filters",
"type_info": "JsonbArray"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false
]
},
"hash": "f2baee15e6d1fecd6d2d7b39fda1b50a15ec8bd349f1081af54da5dc2f5e3021"
}
@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET last_server_ping = now(), error = $1 WHERE workspace_id = $2 AND path = $3 AND server_id = $4 AND enabled IS TRUE RETURNING 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "febd70e6d5304ad912c232d3d8fc3f8ce0133d8a1a9e5ca1124eff9a988dc2d7"
}
+173 -136
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.409.2"
version = "1.411.1"
authors.workspace = true
edition.workspace = true
@@ -28,7 +28,7 @@ members = [
]
[workspace.package]
version = "1.409.2"
version = "1.411.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -219,7 +219,7 @@ serde_derive = "1.0.147"
const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] }
dyn-iter = "0.2.0"
rsa = "0.7.2"
async-stripe = { version = "0.34.1", features = [
async-stripe = { version = "0.39.1", features = [
"runtime-tokio-hyper",
"checkout",
"billing",
@@ -291,3 +291,4 @@ syn = { version = "2.0.74", features = ["full"] }
quote = "1.0.36"
regex-lite = "0.1.6"
yaml-rust = "0.4.5"
tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] }
+1 -1
View File
@@ -1 +1 @@
0428068e4fbbd1380a4d8bbaab5c8e7955decdb8
d61c163e0a311ecd86d1398aff883eaea8d0b09a
@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE websocket_trigger;
@@ -0,0 +1,67 @@
-- Add up migration script here
CREATE TABLE websocket_trigger (
path VARCHAR(255) NOT NULL,
url VARCHAR(255) NOT NULL,
script_path VARCHAR(255) NOT NULL,
is_flow BOOLEAN NOT NULL,
workspace_id VARCHAR(50) NOT NULL,
edited_by VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
extra_perms JSONB NOT NULL DEFAULT '{}',
server_id VARCHAR(50) NULL,
last_server_ping TIMESTAMPTZ NULL,
error TEXT NULL,
enabled BOOLEAN NOT NULL,
filters JSONB[] NOT NULL DEFAULT '{}',
PRIMARY KEY (path, workspace_id)
);
GRANT ALL ON websocket_trigger TO windmill_user;
GRANT ALL ON websocket_trigger TO windmill_admin;
ALTER TABLE websocket_trigger ENABLE ROW LEVEL SECURITY;
CREATE POLICY admin_policy ON websocket_trigger FOR ALL TO windmill_admin USING (true);
CREATE POLICY see_folder_extra_perms_user_select ON websocket_trigger FOR SELECT TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'f' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_insert ON websocket_trigger FOR INSERT TO windmill_user
WITH CHECK (SPLIT_PART(websocket_trigger.path, '/', 1) = 'f' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_update ON websocket_trigger FOR UPDATE TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'f' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_delete ON websocket_trigger FOR DELETE TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'f' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_own ON websocket_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'u' AND SPLIT_PART(websocket_trigger.path, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member ON websocket_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'g' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
CREATE POLICY see_extra_perms_user_select ON websocket_trigger FOR SELECT TO windmill_user
USING (extra_perms ? CONCAT('u/', current_setting('session.user')));
CREATE POLICY see_extra_perms_user_insert ON websocket_trigger FOR INSERT TO windmill_user
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_update ON websocket_trigger FOR UPDATE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_delete ON websocket_trigger FOR DELETE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_groups_select ON websocket_trigger FOR SELECT TO windmill_user
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]);
CREATE POLICY see_extra_perms_groups_insert ON websocket_trigger FOR INSERT TO windmill_user
WITH CHECK (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
CREATE POLICY see_extra_perms_groups_update ON websocket_trigger FOR UPDATE TO windmill_user
USING (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
CREATE POLICY see_extra_perms_groups_delete ON websocket_trigger FOR DELETE TO windmill_user
USING (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
+1
View File
@@ -95,6 +95,7 @@ openidconnect = { workspace = true, optional = true}
url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit.workspace = true
tokio-tungstenite.workspace = true
pin-project.workspace = true
http.workspace = true
+363 -7
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.409.2
version: 1.411.1
title: Windmill API
contact:
@@ -2162,6 +2162,30 @@ paths:
schema:
type: number
/w/{workspace}/workspaces/used_triggers:
get:
summary: get used triggers
operationId: getUsedTriggers
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: status
content:
application/json:
schema:
type: object
properties:
http_routes_used:
type: boolean
websocket_used:
type: boolean
required:
- http_routes_used
- websocket_used
/w/{workspace}/users/list:
get:
summary: list users
@@ -4092,6 +4116,26 @@ paths:
items:
$ref: "#/components/schemas/ScriptHistory"
/w/{workspace}/scripts/get_latest_version/{path}:
get:
summary: get scripts's latest version (hash)
operationId: getScriptLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- script
responses:
"200":
description: Script version/hash
content:
application/json:
required: false
schema:
$ref: "#/components/schemas/ScriptHistory"
/w/{workspace}/scripts/history_update/h/{hash}/p/{path}:
post:
summary: update history of a script
@@ -4580,6 +4624,25 @@ paths:
items:
$ref: "#/components/schemas/FlowVersion"
/w/{workspace}/flows/get_latest_version/{path}:
get:
summary: get flow's latest version
operationId: getFlowLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- flow
responses:
"200":
description: Flow version
content:
application/json:
required: false
schema:
$ref: "#/components/schemas/FlowVersion"
/w/{workspace}/flows/get/v/{version}/p/{path}:
get:
summary: get flow version
@@ -5134,6 +5197,24 @@ paths:
items:
$ref: "#/components/schemas/AppHistory"
/w/{workspace}/apps/get_latest_version/{path}:
get:
summary: get apps's latest version
operationId: getAppLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- app
responses:
"200":
description: App version
content:
application/json:
required: false
schema:
$ref: "#/components/schemas/AppHistory"
/w/{workspace}/apps/history_update/a/{id}/v/{version}:
post:
summary: update app history
@@ -7152,22 +7233,170 @@ paths:
schema:
type: boolean
/w/{workspace}/http_triggers/used:
get:
summary: whether http triggers are used
operationId: used
/w/{workspace}/websocket_triggers/create:
post:
summary: create websocket trigger
operationId: createWebsocketTrigger
tags:
- http_trigger
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new websocket trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewWebsocketTrigger"
responses:
"201":
description: websocket trigger created
content:
text/plain:
schema:
type: string
/w/{workspace}/websocket_triggers/update/{path}:
post:
summary: update websocket trigger
operationId: updateWebsocketTrigger
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EditWebsocketTrigger"
responses:
"200":
description: whether http triggers are used
description: websocket trigger updated
content:
text/plain:
schema:
type: string
/w/{workspace}/websocket_triggers/delete/{path}:
delete:
summary: delete websocket trigger
operationId: deleteWebsocketTrigger
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: websocket trigger deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/websocket_triggers/get/{path}:
get:
summary: get websocket trigger
operationId: getWebsocketTrigger
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: websocket trigger deleted
content:
application/json:
schema:
$ref: "#/components/schemas/WebsocketTrigger"
/w/{workspace}/websocket_triggers/list:
get:
summary: list websocket triggers
operationId: listWebsocketTriggers
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
required: true
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: path
description: filter by path
in: query
schema:
type: string
- name: is_flow
in: query
schema:
type: boolean
- name: path_start
in: query
schema:
type: string
responses:
"200":
description: websocket trigger list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WebsocketTrigger"
/w/{workspace}/websocket_triggers/exists/{path}:
get:
summary: does websocket trigger exists
operationId: existsWebsocketTrigger
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: websocket trigger exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/websocket_triggers/setenabled/{path}:
post:
summary: set enabled websocket trigger
operationId: setWebsocketTriggerEnabled
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated websocket trigger enable
required: true
content:
application/json:
schema:
type: object
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: websocket trigger enabled set
content:
text/plain:
schema:
type: string
/groups/list:
get:
summary: list instance groups
@@ -7995,6 +8224,7 @@ paths:
app,
raw_app,
http_trigger,
websocket_trigger,
]
responses:
"200":
@@ -8032,6 +8262,7 @@ paths:
app,
raw_app,
http_trigger,
websocket_trigger,
]
requestBody:
description: acl to add
@@ -8080,6 +8311,7 @@ paths:
app,
raw_app,
http_trigger,
websocket_trigger,
]
requestBody:
description: acl to add
@@ -10503,6 +10735,7 @@ components:
- "users.delete"
- "users.update"
- "users.login"
- "users.login_failure"
- "users.logout"
- "users.accept_invite"
- "users.decline_invite"
@@ -10514,6 +10747,7 @@ components:
- "users.impersonate"
- "users.leave_workspace"
- "oauth.login"
- "oauth.login_failure"
- "oauth.signup"
- "variables.create"
- "variables.delete"
@@ -11213,6 +11447,128 @@ components:
type: number
email_count:
type: number
websocket_count:
type: number
WebsocketTrigger:
type: object
properties:
path:
type: string
edited_by:
type: string
edited_at:
type: string
format: date-time
script_path:
type: string
url:
type: string
is_flow:
type: boolean
extra_perms:
type: object
additionalProperties:
type: boolean
email:
type: string
workspace_id:
type: string
server_id:
type: string
last_server_ping:
type: string
format: date-time
error:
type: string
enabled:
type: boolean
filters:
type: array
items:
type: object
properties:
key:
type: string
value: {}
required:
- key
- value
required:
- path
- edited_by
- edited_at
- script_path
- url
- extra_perms
- is_flow
- email
- workspace_id
- enabled
- filters
NewWebsocketTrigger:
type: object
properties:
path:
type: string
script_path:
type: string
is_flow:
type: boolean
url:
type: string
enabled:
type: boolean
filters:
type: array
items:
type: object
properties:
key:
type: string
value: {}
required:
- key
- value
required:
- path
- script_path
- url
- is_flow
- filters
EditWebsocketTrigger:
type: object
properties:
url:
type: string
path:
type: string
script_path:
type: string
is_flow:
type: boolean
filters:
type: array
items:
type: object
properties:
key:
type: string
value: {}
required:
- key
- value
required:
- path
- script_path
- url
- is_flow
- filters
Group:
type: object
+33
View File
@@ -62,6 +62,7 @@ pub fn workspaced_service() -> Router {
.route("/delete/*path", delete(delete_app))
.route("/create", post(create_app))
.route("/history/p/*path", get(get_app_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route("/history_update/a/:id/v/:version", post(update_app_history))
}
@@ -427,6 +428,38 @@ async fn get_app_history(
return Ok(Json(result));
}
async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<AppHistory>> {
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg
FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version
WHERE a.workspace_id = $1 AND a.path = $2
ORDER BY created_at DESC",
w_id,
path.to_path(),
).fetch_optional(&mut *tx).await?;
tx.commit().await?;
if let Some(row) = row {
let result = AppHistory {
app_id: row.app_id,
version: row.version_id,
deployment_msg: row.deployment_msg,
};
return Ok(Json(Some(result)));
} else {
return Ok(Json(None));
}
}
async fn update_app_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+26
View File
@@ -63,6 +63,7 @@ pub fn workspaced_service() -> Router {
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
.route("/history/p/*path", get(get_flow_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route(
"/history_update/v/:version/p/*path",
post(update_flow_history),
@@ -538,6 +539,31 @@ async fn get_flow_history(
Ok(Json(flows))
}
async fn get_latest_version(
authed: ApiAuthed,
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?;
let version = sqlx::query_as!(
FlowVersion,
"SELECT flow_version.id, flow_version.created_at, deployment_metadata.deployment_msg FROM flow_version
LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version
WHERE flow_version.path = $1 AND flow_version.workspace_id = $2
ORDER BY flow_version.created_at DESC",
path,
w_id
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(version))
}
async fn get_flow_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+1 -37
View File
@@ -12,10 +12,8 @@ use std::collections::HashMap;
use tower_http::cors::CorsLayer;
use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_common::{
auth::fetch_authed_from_permissioned_as,
db::UserDB,
error::{self, JsonResult},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
@@ -27,7 +25,7 @@ use crate::{
run_flow_by_path_inner, run_script_by_path_inner, run_wait_result_flow_by_path_internal,
run_wait_result_script_by_path_internal, RunJobQuery,
},
users::OptAuthed,
users::{fetch_api_authed, OptAuthed},
};
lazy_static::lazy_static! {
@@ -66,7 +64,6 @@ pub fn workspaced_service() -> Router {
.route("/update/*path", post(update_trigger))
.route("/delete/*path", delete(delete_trigger))
.route("/exists/*path", get(exists_trigger))
.route("/used", get(used))
.route("/route_exists", post(exists_route))
}
@@ -346,17 +343,6 @@ async fn delete_trigger(
Ok(format!("HTTP trigger {path} deleted"))
}
async fn used(Extension(db): Extension<DB>, Path(w_id): Path<String>) -> JsonResult<bool> {
let used = sqlx::query_scalar!(
r#"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1)"#,
w_id,
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(used))
}
async fn exists_trigger(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -421,28 +407,6 @@ struct TriggerRoute {
http_method: HttpMethod,
}
async fn fetch_api_authed(
username: String,
email: String,
w_id: &str,
db: &DB,
username_override: String,
) -> error::Result<ApiAuthed> {
let permissioned_as = username_to_permissioned_as(username.as_str());
let authed =
fetch_authed_from_permissioned_as(permissioned_as, email.clone(), w_id, db).await?;
Ok(ApiAuthed {
username: username,
email: email,
is_admin: authed.is_admin,
is_operator: authed.is_operator,
groups: authed.groups,
folders: authed.folders,
scopes: authed.scopes,
username_override: Some(username_override),
})
}
async fn get_http_route_trigger(
route_path: &str,
opt_authed: Option<ApiAuthed>,
+4 -2
View File
@@ -70,7 +70,10 @@ use windmill_common::{
oauth2::HmacSha256,
scripts::{ScriptHash, ScriptLang},
users::username_to_permissioned_as,
utils::{not_found_if_none, now_from_db, paginate, paginate_without_limits, require_admin, Pagination, StripPath},
utils::{
not_found_if_none, now_from_db, paginate, paginate_without_limits, require_admin,
Pagination, StripPath,
},
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
@@ -4751,7 +4754,6 @@ async fn get_job_update(
&w_id,
job_id,
"progress_perc"
)
.fetch_optional(&db)
.await?.and_then(|inner| inner)
+13 -3
View File
@@ -39,7 +39,7 @@ use tower_http::{
trace::TraceLayer,
};
use windmill_common::db::UserDB;
use windmill_common::worker::ALL_TAGS;
use windmill_common::worker::{ALL_TAGS, CLOUD_HOSTED};
use windmill_common::{BASE_URL, INSTANCE_NAME};
use crate::scim_ee::has_scim_token;
@@ -87,6 +87,7 @@ mod users;
mod utils;
mod variables;
mod webhook_util;
mod websocket_triggers;
mod workers;
mod workspaces;
@@ -225,7 +226,7 @@ pub async fn run_server(
db: db.clone(),
user_db: user_db,
auth_cache: auth_cache.clone(),
rsmq: rsmq,
rsmq: rsmq.clone(),
base_internal_url: base_internal_url.clone(),
});
if let Err(err) = smtp_server.start_listener_thread(addr).await {
@@ -245,6 +246,11 @@ pub async fn run_server(
}
};
if !*CLOUD_HOSTED {
let ws_killpill_rx = rx.resubscribe();
websocket_triggers::start_websockets(db.clone(), rsmq, ws_killpill_rx).await;
}
// build our application with a route
let app = Router::new()
.nest(
@@ -285,7 +291,11 @@ pub async fn run_server(
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_ee::workspaced_service())
.nest("/http_triggers", http_triggers::workspaced_service()),
.nest("/http_triggers", http_triggers::workspaced_service())
.nest(
"/websocket_triggers",
websocket_triggers::workspaced_service(),
),
)
.nest("/workspaces", workspaces::global_service())
.nest(
+33
View File
@@ -152,6 +152,7 @@ pub fn workspaced_service() -> Router {
post(toggle_workspace_error_handler),
)
.route("/history/p/*path", get(get_script_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route(
"/history_update/h/:hash/p/*path",
post(update_script_history),
@@ -948,6 +949,38 @@ async fn get_script_history(
return Ok(Json(result));
}
async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<ScriptHistory>> {
let mut tx = user_db.begin(&authed).await?;
let row_o = sqlx::query!(
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash
WHERE s.workspace_id = $1 AND s.path = $2
ORDER by created_at DESC",
w_id,
path.to_path(),
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
if let Some(row) = row_o {
let result = ScriptHistory {
script_hash: ScriptHash(row.hash),
deployment_msg: row.deployment_msg, //
};
return Ok(Json(Some(result)));
} else {
return Ok(Json(None));
}
}
async fn update_script_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+4 -1
View File
@@ -48,7 +48,10 @@ fn serve_path(path: &str) -> Response<Body> {
let mut res = Response::builder()
.header(header::CONTENT_TYPE, mime.as_ref())
.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*");
if mime.as_ref() == mime::APPLICATION_JAVASCRIPT || path.ends_with(".wasm") {
if mime.as_ref() == mime::APPLICATION_JAVASCRIPT
|| mime.as_ref() == mime::TEXT_JAVASCRIPT
|| path.ends_with(".wasm")
{
res = res.header(header::CACHE_CONTROL, "max-age=31536000");
} else if (mime.type_(), mime.subtype()) == (mime::TEXT, mime::CSS) {
res = res.header(header::CACHE_CONTROL, "max-age=31536000");
+12
View File
@@ -17,6 +17,7 @@ pub struct TriggersCount {
http_routes_count: i64,
webhook_count: i64,
email_count: i64,
websocket_count: i64,
}
pub(crate) async fn get_triggers_count_internal(
db: &DB,
@@ -53,6 +54,16 @@ pub(crate) async fn get_triggers_count_internal(
.await?
.unwrap_or(0);
let websocket_count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM websocket_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
path,
is_flow,
w_id
)
.fetch_one(db)
.await?
.unwrap_or(0);
let webhook_count = (if is_flow {
sqlx::query_scalar!(
"SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
@@ -93,6 +104,7 @@ pub(crate) async fn get_triggers_count_internal(
http_routes_count,
webhook_count,
email_count,
websocket_count,
}))
}
@@ -0,0 +1,684 @@
use axum::{
extract::{Path, Query},
routing::{delete, get, post},
Extension, Json, Router,
};
use futures::StreamExt;
use http::StatusCode;
use itertools::Itertools;
use rand::seq::SliceRandom;
use serde::{
de::{self, MapAccess, Visitor},
Deserialize, Deserializer, Serialize,
};
use serde_json::Value;
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::prelude::FromRow;
use std::{collections::HashMap, fmt};
use tokio_tungstenite::connect_async;
use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_common::{
db::UserDB,
error::{self, JsonResult},
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
INSTANCE_NAME,
};
use windmill_queue::PushArgsOwned;
use crate::{
db::{ApiAuthed, DB},
jobs::{
run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery,
},
users::fetch_api_authed,
};
pub fn workspaced_service() -> Router {
Router::new()
.route("/create", post(create_websocket_trigger))
.route("/list", get(list_websocket_triggers))
.route("/get/*path", get(get_websocket_trigger))
.route("/update/*path", post(update_websocket_trigger))
.route("/delete/*path", delete(delete_websocket_trigger))
.route("/exists/*path", get(exists_websocket_trigger))
.route("/setenabled/*path", post(set_enabled))
}
#[derive(Deserialize)]
struct NewWebsocketTrigger {
path: String,
url: String,
script_path: String,
is_flow: bool,
enabled: Option<bool>,
filters: Vec<serde_json::Value>,
}
#[derive(FromRow, Serialize, Clone)]
pub struct WebsocketTrigger {
workspace_id: String,
path: String,
url: String,
script_path: String,
is_flow: bool,
edited_by: String,
email: String,
edited_at: chrono::DateTime<chrono::Utc>,
server_id: Option<String>,
last_server_ping: Option<chrono::DateTime<chrono::Utc>>,
extra_perms: serde_json::Value,
error: Option<String>,
enabled: bool,
filters: Vec<serde_json::Value>,
}
#[derive(Deserialize)]
struct EditWebsocketTrigger {
path: String,
url: String,
script_path: String,
is_flow: bool,
filters: Vec<serde_json::Value>,
}
#[derive(Deserialize)]
pub struct ListWebsocketTriggerQuery {
pub page: Option<usize>,
pub per_page: Option<usize>,
pub path: Option<String>,
pub is_flow: Option<bool>,
pub path_start: Option<String>,
}
async fn list_websocket_triggers(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(lst): Query<ListWebsocketTriggerQuery>,
) -> error::JsonResult<Vec<WebsocketTrigger>> {
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
let mut sqlb = SqlBuilder::select_from("websocket_trigger")
.field("*")
.order_by("edited_at", true)
.and_where("workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
if let Some(path) = lst.path {
sqlb.and_where_eq("script_path", "?".bind(&path));
}
if let Some(is_flow) = lst.is_flow {
sqlb.and_where_eq("is_flow", "?".bind(&is_flow));
}
if let Some(path_start) = &lst.path_start {
sqlb.and_where_like_left("path", path_start);
}
let sql = sqlb
.sql()
.map_err(|e| error::Error::InternalErr(e.to_string()))?;
let rows = sqlx::query_as::<_, WebsocketTrigger>(&sql)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(rows))
}
async fn get_websocket_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::JsonResult<WebsocketTrigger> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
let trigger = sqlx::query_as!(
WebsocketTrigger,
r#"SELECT *
FROM websocket_trigger
WHERE workspace_id = $1 AND path = $2"#,
w_id,
path,
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
let trigger = not_found_if_none(trigger, "Trigger", path)?;
Ok(Json(trigger))
}
async fn create_websocket_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ct): Json<NewWebsocketTrigger>,
) -> error::Result<(StatusCode, String)> {
if *CLOUD_HOSTED {
return Err(error::Error::BadRequest(
"Websocket triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
));
}
require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query_as!(
WebsocketTrigger,
"INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now()) RETURNING *",
w_id,
ct.path,
ct.url,
ct.script_path,
ct.is_flow,
ct.enabled.unwrap_or(true),
&ct.filters,
&authed.username,
&authed.email
)
.fetch_one(&mut *tx).await?;
audit_log(
&mut *tx,
&authed,
"websocket_triggers.create",
ActionKind::Create,
&w_id,
Some(ct.path.as_str()),
None,
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, format!("{}", ct.path)))
}
async fn update_websocket_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ct): Json<EditWebsocketTrigger>,
) -> error::Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
// important to update server_id, last_server_ping and error to NULL to stop current websocket listener
sqlx::query!(
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, edited_by = $6, email = $7, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL
WHERE workspace_id = $8 AND path = $9",
ct.url,
ct.script_path,
ct.path,
ct.is_flow,
&ct.filters,
&authed.username,
&authed.email,
w_id,
path,
)
.execute(&mut *tx).await?;
audit_log(
&mut *tx,
&authed,
"websocket_triggers.update",
ActionKind::Create,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
Ok(path.to_string())
}
#[derive(Deserialize)]
pub struct SetEnabled {
pub enabled: bool,
}
pub async fn set_enabled(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<SetEnabled>,
) -> error::Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
// important to set server_id, last_server_ping and error to NULL to stop current websocket listener
let one_o = sqlx::query_scalar!(
"UPDATE websocket_trigger SET enabled = $1, email = $2, edited_by = $3, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL
WHERE path = $4 AND workspace_id = $5 RETURNING 1",
payload.enabled,
&authed.email,
&authed.username,
path,
w_id,
).fetch_optional(&mut *tx).await?;
not_found_if_none(one_o.flatten(), "Websocket trigger", path)?;
audit_log(
&mut *tx,
&authed,
"websocket_triggers.setenabled",
ActionKind::Update,
&w_id,
Some(path),
Some([("enabled", payload.enabled.to_string().as_ref())].into()),
)
.await?;
tx.commit().await?;
Ok(format!(
"succesfully updated websocket trigger at path {} to status {}",
path, payload.enabled
))
}
async fn delete_websocket_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM websocket_trigger WHERE workspace_id = $1 AND path = $2",
w_id,
path,
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"websocket_triggers.delete",
ActionKind::Delete,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Websocket trigger {path} deleted"))
}
async fn exists_websocket_trigger(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE path = $1 AND workspace_id = $2)",
path,
w_id,
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
async fn listen_to_unlistened_websockets(
db: &DB,
rsmq: &Option<rsmq_async::MultiplexedRsmq>,
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
) -> () {
match sqlx::query_as!(
WebsocketTrigger,
r#"SELECT *
FROM websocket_trigger
WHERE enabled IS TRUE AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')"#
)
.fetch_all(db)
.await
{
Ok(mut triggers) => {
triggers.shuffle(&mut rand::thread_rng());
for trigger in triggers {
maybe_listen_to_websocket(trigger, db.clone(), rsmq.clone(), killpill_rx.resubscribe()).await;
}
}
Err(err) => {
tracing::error!("Error fetching websocket triggers: {:?}", err);
}
};
}
pub async fn start_websockets(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
tokio::spawn(async move {
listen_to_unlistened_websockets(&db, &rsmq, &killpill_rx).await;
loop {
tokio::select! {
biased;
_ = killpill_rx.recv() => {
return;
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(15)) => {
listen_to_unlistened_websockets(&db, &rsmq, &killpill_rx).await;
}
}
}
});
}
async fn maybe_listen_to_websocket(
ws_trigger: WebsocketTrigger,
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
match sqlx::query_scalar!(
"UPDATE websocket_trigger SET server_id = $1, last_server_ping = now() WHERE enabled IS TRUE AND workspace_id = $2 AND path = $3 AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') RETURNING true",
*INSTANCE_NAME,
ws_trigger.workspace_id,
ws_trigger.path,
).fetch_optional(&db).await {
Ok(has_lock) => {
if has_lock.flatten().unwrap_or(false) {
tokio::spawn(listen_to_websocket(ws_trigger, db, rsmq, killpill_rx));
} else {
tracing::info!("Websocket {} already being listened to", ws_trigger.url);
}
},
Err(err) => {
tracing::error!("Error acquiring lock for websocket {}: {:?}", ws_trigger.path, err);
}
};
}
struct SupersetVisitor<'a> {
key: &'a str,
value_to_check: &'a Value,
}
impl<'de, 'a> Visitor<'de> for SupersetVisitor<'a> {
type Value = bool;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a JSON object with a specific key at the top level")
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
while let Some(key) = map.next_key::<String>()? {
if key == self.key {
// Deserialize the value for the key and check if it's a superset
let json_value: Value = map.next_value()?;
tracing::info!("json_value: {:?}", json_value);
tracing::info!("value_to_check: {:?}", self.value_to_check);
return Ok(is_superset(&json_value, self.value_to_check));
} else {
// Skip the value if it's not the one we're interested in
let _ = map.next_value::<de::IgnoredAny>()?;
}
}
// If the key was not found, return false
Ok(false)
}
}
// Function to check if json_value is a superset of value_to_check
fn is_superset(json_value: &Value, value_to_check: &Value) -> bool {
match (json_value, value_to_check) {
(Value::Object(json_map), Value::Object(check_map)) => {
// Check that all keys and values in check_map exist and match in json_map
check_map.iter().all(|(k, v)| {
json_map
.get(k)
.map_or(false, |json_val| is_superset(json_val, v))
})
}
(Value::Array(json_array), Value::Array(check_array)) => {
// Check that all elements in check_array exist in json_array
check_array.iter().all(|check_item| {
json_array
.iter()
.any(|json_item| is_superset(json_item, check_item))
})
}
_ => json_value == value_to_check,
}
}
// A function to deserialize and check if the value at the given key is a superset of a passed value
fn is_value_superset<'a, 'de, D>(
deserializer: D,
key: &'a str,
value_to_check: &'a Value,
) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(SupersetVisitor { key, value_to_check })
}
async fn listen_to_websocket(
ws_trigger: WebsocketTrigger,
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
async fn update_ping(db: DB, ws_trigger: &WebsocketTrigger, error: Option<&str>) -> Option<()> {
match sqlx::query_scalar!(
"UPDATE websocket_trigger SET last_server_ping = now(), error = $1 WHERE workspace_id = $2 AND path = $3 AND server_id = $4 AND enabled IS TRUE RETURNING 1",
error,
ws_trigger.workspace_id,
ws_trigger.path,
*INSTANCE_NAME
).fetch_optional(&db).await {
Ok(updated) => {
if updated.flatten().is_none() {
tracing::info!("Websocket {} changed, disabled, or deleted, stopping...", ws_trigger.url);
return None;
}
},
Err(err) => {
tracing::warn!("Error updating ping of websocket {}: {:?}", ws_trigger.url, err);
}
};
Some(())
}
let url = ws_trigger.url.as_str();
#[derive(Deserialize)]
struct JsonFilter {
key: String,
value: serde_json::Value,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Filter {
JsonFilter(JsonFilter),
}
let filters: Vec<Filter> = ws_trigger
.filters
.iter()
.filter_map(|m| serde_json::from_value(m.clone()).ok())
.collect_vec();
loop {
tokio::select! {
biased;
_ = killpill_rx.recv() => {
return;
},
connection = connect_async(url) => {
match connection {
Ok((ws_stream, _)) => {
tracing::info!("Listening to websocket {}", url);
if let None = update_ping(db.clone(), &ws_trigger, None).await {
return;
}
let mut last_ping = tokio::time::Instant::now();
let (_, mut read) = ws_stream.split();
loop {
tokio::select! {
biased;
_ = killpill_rx.recv() => {
return;
}
msg = read.next() => {
if let Some(msg) = msg {
if last_ping.elapsed() > tokio::time::Duration::from_secs(5) {
if let None = update_ping(db.clone(), &ws_trigger, None).await {
return;
}
last_ping = tokio::time::Instant::now();
}
match msg {
Ok(msg) => {
match msg {
tokio_tungstenite::tungstenite::Message::Text(text) => {
let mut should_handle = true;
for filter in &filters {
match filter {
Filter::JsonFilter(JsonFilter { key, value }) => {
let mut deserializer = serde_json::Deserializer::from_str(text.as_str());
should_handle = match is_value_superset(&mut deserializer, key, &value) {
Ok(filter_match) => {
filter_match
},
Err(err) => {
tracing::warn!("Error deserializing filter for websocket {}: {:?}", url, err);
false
}
};
}
}
if !should_handle {
break;
}
}
if should_handle {
let db_ = db.clone();
let rsmq_ = rsmq.clone();
let ws_trigger_ = ws_trigger.clone();
tokio::spawn(async move {
let url = ws_trigger_.url.clone();
if let Err(err) = run_job(db_, rsmq_, ws_trigger_, text).await {
tracing::error!("Error running job on websocket {}: {:?}", url, err);
};
});
}
},
_ => {}
}
},
Err(err) => {
tracing::error!("Error reading from websocket {}: {:?}", url, err);
}
}
} else {
tracing::error!("Websocket {} closed", url);
if let None =
update_ping(db.clone(), &ws_trigger, Some("Websocket closed")).await
{
return;
}
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
break;
}
},
_ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {
if let None = update_ping(db.clone(), &ws_trigger, None).await {
return;
}
last_ping = tokio::time::Instant::now();
},
}
}
}
Err(err) => {
tracing::error!("Error connecting to websocket {}: {:?}", url, err);
if let None =
update_ping(db.clone(), &ws_trigger, Some(err.to_string().as_str())).await
{
return;
}
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
}
}
}
}
}
}
async fn run_job(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
trigger: WebsocketTrigger,
msg: String,
) -> anyhow::Result<()> {
let args = PushArgsOwned {
args: HashMap::from([("msg".to_string(), to_raw_value(&msg))]),
extra: Some(HashMap::from([(
"wm_trigger".to_string(),
to_raw_value(&serde_json::json!({"kind": "websocket"})),
)])),
};
let label_prefix = Some(format!("ws-{}-", trigger.path));
let authed = fetch_api_authed(
trigger.edited_by.clone(),
trigger.email.clone(),
&trigger.workspace_id,
&db,
"anonymous".to_string(),
)
.await?;
let user_db = UserDB::new(db.clone());
let run_query = RunJobQuery::default();
if trigger.is_flow {
run_wait_result_flow_by_path_internal(
db,
run_query,
StripPath(trigger.script_path.to_owned()),
authed,
rsmq,
user_db,
args,
trigger.workspace_id.clone(),
label_prefix,
)
.await?;
} else {
run_wait_result_script_by_path_internal(
db,
run_query,
StripPath(trigger.script_path.to_owned()),
authed,
rsmq,
user_db,
trigger.workspace_id.clone(),
args,
label_prefix,
)
.await?;
}
Ok(())
}
+26 -1
View File
@@ -116,7 +116,8 @@ pub fn workspaced_service() -> Router {
.route("/get_workspace_name", get(get_workspace_name))
.route("/change_workspace_name", post(change_workspace_name))
.route("/change_workspace_id", post(change_workspace_id))
.route("/usage", get(get_usage));
.route("/usage", get(get_usage))
.route("/used_triggers", get(get_used_triggers));
#[cfg(feature = "stripe")]
{
@@ -1488,6 +1489,30 @@ async fn set_encryption_key(
return Ok(());
}
#[derive(Serialize)]
struct UsedTriggers {
pub websocket_used: bool,
pub http_routes_used: bool,
}
async fn get_used_triggers(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<UsedTriggers> {
let mut tx = user_db.begin(&authed).await?;
let websocket_used = sqlx::query_as!(
UsedTriggers,
r#"SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as "websocket_used!", EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as "http_routes_used!""#,
w_id,
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(websocket_used))
}
async fn list_workspaces_as_super_admin(
authed: ApiAuthed,
Extension(db): Extension<DB>,
-2
View File
@@ -640,9 +640,7 @@ pub async fn get_logs_from_store(
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
tracing::debug!("Getting logs from store: {file_index:?}");
if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
tracing::debug!("object store client present, streaming from there");
let logs = logs.to_string();
let stream = async_stream::stream! {
+53 -16
View File
@@ -8,29 +8,65 @@ const p = {
"localhost",
"127.0.0.1"
);
const w_id = "W_ID";
const current_path = "CURRENT_PATH";
const token = "TOKEN";
const cdir = resolve("./");
const cdirNoPrivate = cdir.replace(/^\/private/, ""); // for macos
const filterResolve = new RegExp(
`^(?!\\.\/main\\.ts)(?!${cdir}\/main\\.ts)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.mjs).*\\.ts$`
);
let cdirNodeModules = `${cdir}/node_modules/`;
const filterLoad = new RegExp(`^${cdir}\/main\\.ts$`);
const transpiler = new Bun.Transpiler({
loader: "tsx",
});
function replaceRelativeImports(code) {
const imports = transpiler.scanImports(code);
for (const imp of imports) {
if (imp.kind == "import-statement") {
if (imp.path.startsWith(".") && !imp.path.endsWith(".ts")) {
code = code.replaceAll(imp.path, imp.path + ".ts");
}
}
}
return {
contents: code,
};
}
build.onLoad({ filter: filterLoad }, async (args) => {
const code = readFileSync(args.path, "utf8");
return replaceRelativeImports(code);
});
build.onLoad({ filter: /.*\.url$/ }, async (args) => {
const url = readFileSync(args.path, "utf8");
const contents = await (
await fetch(url, {
method: "GET",
headers: { Authorization: "Bearer TOKEN" },
})
).text();
const req = await fetch(url, {
method: "GET",
headers: {
Authorization: "Bearer " + token,
},
});
if (!req.ok) {
throw new Error(
`Failed to find relative import at ${url}`,
req.statusText
);
}
const contents = await req.text();
return {
contents,
contents: replaceRelativeImports(contents).contents,
loader: "tsx",
};
});
const cdir = resolve("./");
const cdirNoPrivate = cdir.replace(/^\/private/, ""); // for macos
const filter = new RegExp(
`^(?!\\.\/main\\.ts)(?!${cdir}\/main\\.ts)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.mjs).*\\.ts$`
);
let cdirNodeModules = `${cdir}/node_modules/`;
build.onResolve({ filter }, (args) => {
build.onResolve({ filter: filterResolve }, (args) => {
if (args.importer?.startsWith(cdirNodeModules)) {
return undefined;
}
@@ -41,9 +77,10 @@ const p = {
const isRelative = !args.path.startsWith("/");
let endExt = args.path.endsWith(".ts") ? "" : ".ts";
const url = isRelative
? `${base_internal_url}/api/w/${w_id}/scripts/RAW_GET_ENDPOINT/p/${file_path}/../${args.path}`
: `${base_internal_url}/api/w/${w_id}/scripts/RAW_GET_ENDPOINT/p/${args.path}`;
? `${base_internal_url}/api/w/${w_id}/scripts/raw_unpinned/p/${file_path}/../${args.path}${endExt}`
: `${base_internal_url}/api/w/${w_id}/scripts/raw_unpinned/p/${args.path}${endExt}`;
const file = isRelative
? resolve("./" + file_path + "/../" + args.path + ".url")
: resolve("./" + args.path + ".url");
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.409.2";
export const VERSION = "v1.411.1";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+104
View File
@@ -0,0 +1,104 @@
import { log } from "./deps.ts";
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"]
);
}
// 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);
}
// // 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);
// });
// });
+11 -5
View File
@@ -60,7 +60,7 @@ export {
// }
// });
export const VERSION = "1.409.2";
export const VERSION = "1.411.1";
const command = new Command()
.name("wmill")
@@ -101,9 +101,13 @@ const command = new Command()
"wmill.yaml",
yamlStringify({
defaultTs: "bun",
includes: ["**"],
includes: ["f/**"],
excludes: [],
codebases: [],
skipVariables: true,
skipResources: true,
skipSecrets: true,
includeSchedules: false,
})
);
log.info(colors.green("wmill.yaml created"));
@@ -213,9 +217,11 @@ function isMain() {
const isMain = import.meta.main;
if (isMain) {
if (!Deno.args.includes("completions")) {
log.warn(
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli"
);
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
log.warn(
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true"
);
}
}
}
return isMain;
+63 -18
View File
@@ -9,6 +9,7 @@ import { deepEqual } from "./utils.ts";
import * as wmill from "./gen/services.gen.ts";
import { Config, GlobalSetting } from "./gen/types.gen.ts";
import { removeWorkerPrefix } from "./worker_groups.ts";
import process from "node:process";
export interface SimplifiedSettings {
// slack_team_id?: string;
@@ -112,10 +113,10 @@ export async function pushWorkspaceSettings(
workspace,
requestBody: localSettings.auto_invite_enabled
? {
operator: localSettings.auto_invite_as === "operator",
invite_all: true,
auto_add: localSettings.auto_invite_mode === "add",
}
operator: localSettings.auto_invite_as === "operator",
invite_all: true,
auto_add: localSettings.auto_invite_mode === "add",
}
: {},
});
} catch (_) {
@@ -127,10 +128,10 @@ export async function pushWorkspaceSettings(
workspace,
requestBody: localSettings.auto_invite_enabled
? {
operator: localSettings.auto_invite_as === "operator",
invite_all: false,
auto_add: localSettings.auto_invite_mode === "add",
}
operator: localSettings.auto_invite_as === "operator",
invite_all: false,
auto_add: localSettings.auto_invite_mode === "add",
}
: {},
});
}
@@ -155,7 +156,7 @@ export async function pushWorkspaceSettings(
settings.error_handler_extra_args
) ||
localSettings.error_handler_muted_on_cancel !==
settings.error_handler_muted_on_cancel
settings.error_handler_muted_on_cancel
) {
log.debug(`Updating error handler...`);
await wmill.editErrorHandler({
@@ -259,27 +260,69 @@ export async function pushWorkspaceKey(
}
}
const INSTANCE_SETTINGS_PATH = "instance_settings.yaml";
export async function readInstanceSettings() {
let localSettings: GlobalSetting[] = [];
try {
localSettings = (await yamlParseFile(
"instance_settings.yaml"
)) as GlobalSetting[];
localSettings = (await yamlParseFile(INSTANCE_SETTINGS_PATH)) as GlobalSetting[];
} catch {
log.warn("No instance_settings.yaml found");
log.warn(`No ${INSTANCE_SETTINGS_PATH} found`);
}
return localSettings;
}
import { decrypt, encrypt } from "./local_encryption.ts";
const SENSITIVE_FIELD: string[] = ["license_key", "jwt_secret"]
async function processInstanceSettings(settings: GlobalSetting[], mode: "encode" | "decode"): Promise<GlobalSetting[]> {
const encKey = process.env.WMILL_INSTANCE_LOCAL_ENCRYPTION_KEY;
if (encKey) {
const res: GlobalSetting[] = []
for (const s of settings) {
if (SENSITIVE_FIELD.includes(s.name) && typeof s.value === "string") {
res.push(await processField(s, "value", encKey, mode) as GlobalSetting);
} else if (s.name == "oauths") {
if (typeof s.value === "object") {
const oauths = s.value as { [key: string]: any };
for (const [k, v] of Object.entries(oauths)) {
oauths[k] = await processField(v, "secret", encKey, mode);
}
res.push(s);
} else {
log.warn(`Unexpected oauths value type: ${typeof s.value}`);
res.push(s);
}
} else {
res.push(s);
}
}
return res;
} else {
log.warn("No encryption key found, skipping encryption. Recommend setting WMILL_INSTANCE_LOCAL_ENCRYPTION_KEY");
}
return settings;
}
async function processField(obj: { [key: string]: any }, field: string, encKey: string, mode: "encode" | "decode"): Promise<{ [key: string]: any }> {
return {
...obj,
[field]: mode === "encode" ? await encrypt(obj[field], encKey) : await decrypt(obj[field], encKey) as any,
}
}
export async function pullInstanceSettings(preview = false) {
const remoteSettings = await wmill.listGlobalSettings();
if (preview) {
const localSettings: GlobalSetting[] = await readInstanceSettings();
const processedSettings = await processInstanceSettings(remoteSettings, "encode");
return compareInstanceObjects(
remoteSettings,
processedSettings,
localSettings,
"name",
"setting"
@@ -287,12 +330,13 @@ export async function pullInstanceSettings(preview = false) {
} else {
log.info("Pulling settings from instance");
const processedSettings = await processInstanceSettings(remoteSettings, "encode");
await Deno.writeTextFile(
"instance_settings.yaml",
yamlStringify(remoteSettings as any)
INSTANCE_SETTINGS_PATH,
yamlStringify(processedSettings)
);
log.info(colors.green("Settings written to instance_settings.yaml"));
log.info(colors.green(`Settings written to ${INSTANCE_SETTINGS_PATH}`));
}
}
@@ -302,6 +346,7 @@ export async function pushInstanceSettings(
) {
const remoteSettings = await wmill.listGlobalSettings();
let localSettings: GlobalSetting[] = await readInstanceSettings();
localSettings = await processInstanceSettings(localSettings, "decode");
if (baseUrl) {
localSettings = localSettings.filter((s) => s.name !== "base_url");
+2 -2
View File
@@ -618,8 +618,8 @@ export async function elementsToMap(
for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
if (entry.isDirectory || entry.ignored) continue;
const path = entry.path;
if (json && path.endsWith(".yaml")) continue;
if (!json && path.endsWith(".json")) continue;
if (json && path.endsWith(".yaml") && !isFileResource(path)) continue;
if (!json && path.endsWith(".json") && !isFileResource(path)) continue;
const ext = json ? ".json" : ".yaml";
if (!skips.includeSchedules && path.endsWith(".schedule" + ext)) continue;
if (!skips.includeUsers && path.endsWith(".user" + ext)) continue;
+1 -1
View File
@@ -20,7 +20,7 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
# Install UV
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
COPY --from=oven/bun:1.1.27 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
# add the docker client to call docker from a worker if enabled
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
+1 -1
View File
@@ -19,7 +19,7 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
# Install UV
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
COPY --from=oven/bun:1.1.27 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
# add the docker client to call docker from a worker if enabled
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.409.2",
"version": "1.411.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.409.2",
"version": "1.411.1",
"license": "AGPL-3.0",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.409.2",
"version": "1.411.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
+8 -3
View File
@@ -120,11 +120,16 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => {
? f.raw
: '/' + config.scriptPath + (f.raw.startsWith('../') ? '/../' : '/.') + f.raw
let url = config.root + path
// console.log('FOO', config.scriptPath, path, f.raw)
console.log('fetching local file', url, f.raw)
let localPath = f.raw
if (f.raw.startsWith('.') && !f.raw.endsWith('.ts')) {
url += '.ts'
localPath += '.ts'
}
console.log('fetching local file', url, f.raw, localPath)
const res = await fetch(url)
if (res.ok) {
config.delegate.localFile?.(await res.text(), f.raw)
config.delegate.localFile?.(await res.text(), localPath)
}
})
}
+9 -9
View File
@@ -112,26 +112,26 @@
// If using the basic editor, set the cron string based on the selected options
if (executeEvery === 'second') {
if (seconds > 0) {
nschedule = `*/${seconds} * * * *`
nschedule = `*/${seconds} * * * * *`
} else {
nschedule = `* * * * *`
nschedule = `* * * * * *`
}
} else if (executeEvery === 'minute') {
if (minutes > 0) {
nschedule = `0 */${minutes} * * *`
nschedule = `0 */${minutes} * * * *`
} else {
nschedule = `* * * * *`
nschedule = `* * * * * *`
}
} else if (executeEvery === 'hour') {
if (hours > 0) {
nschedule = `0 0 */${hours} * *`
nschedule = `0 0 */${hours} * * *`
} else {
nschedule = `* * * * *`
nschedule = `* * * * * *`
}
} else if (executeEvery === 'day-month') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} *`
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} * *`
} else if (executeEvery === 'month') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} ${s_months}`
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} ${s_months} *`
} else if (executeEvery === 'day-week') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} * * ${s_daysOfWeek}`
}
@@ -214,7 +214,7 @@
type="text"
id="cron-schedule"
name="cron-schedule"
placeholder="*/30 * * * *"
placeholder="0 0 */1 * * *"
bind:value={schedule}
{disabled}
/>
+3 -3
View File
@@ -475,9 +475,9 @@
const testStepStore = writable<Record<string, any>>({})
const selectedIdStore = writable('settings-metadata')
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
>('webhooks')
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const triggersCount = writable<TriggersCount | undefined>(undefined)
+91 -12
View File
@@ -25,10 +25,12 @@
encodeState,
formatCron,
orderedJsonStringify,
sleep
sleep,
type Value
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { Drawer } from '$lib/components/common'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import { setContext, tick, type ComponentType } from 'svelte'
import { writable, type Writable } from 'svelte/store'
@@ -102,9 +104,30 @@
export let disableAi: boolean = false
export let disabledFlowInputs = false
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
export let version: number | undefined = undefined
// Used by multiplayer deploy collision warning
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
$: setContext('customUi', customUi)
let onLatest = true
async function compareVersions() {
if (version === undefined) {
return
}
const flowVersion = await FlowService.getFlowLatestVersion({
workspace: $workspaceStore!,
path: $pathStore
})
onLatest = version === flowVersion?.id
}
const dispatch = createEventDispatcher()
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
@@ -253,6 +276,46 @@
)
}
async function handleSaveFlow(deploymentMsg?: string) {
await compareVersions();
if (onLatest) {
// Handle directly
await saveFlow(deploymentMsg)
} else {
// We need it for diff
await syncWithDeployed()
// Handle through confirmation modal
confirmCallback = async () => {
await saveFlow(deploymentMsg)
}
// Open confirmation modal
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 saveFlow(deploymentMsg?: string): Promise<void> {
loadingSave = true
try {
@@ -330,7 +393,7 @@
requestBody: { enabled }
})
}
} else if (scheduleExists) {
} else if (scheduleExists && !$triggersCount?.primary_schedule) {
await ScheduleService.deleteSchedule({
workspace: $workspaceStore ?? '',
path: $pathStore
@@ -398,9 +461,9 @@
}
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
>('webhooks')
export function getSelectedId() {
return $selectedIdStore
@@ -420,7 +483,9 @@
selectedIdStore.set(selectedId)
}
function selectTrigger(selectedTrigger: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes') {
function selectTrigger(
selectedTrigger: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
) {
selectedTriggerStore.set(selectedTrigger)
}
@@ -1122,6 +1187,15 @@
<slot />
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={$flowStore}
/>
{#key renderCount}
{#if !$userStore?.operator}
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
@@ -1286,14 +1360,17 @@
color="light"
variant="border"
size="xs"
on:click={() => {
on:click={async () => {
if (!savedFlow) {
return
}
await syncWithDeployed()
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedFlow,
deployed: deployedValue ?? savedFlow,
draft: savedFlow['draft'],
current: { ...$flowStore, path: $pathStore }
})
@@ -1335,7 +1412,9 @@
loading={loadingSave}
size="xs"
startIcon={{ icon: Save }}
on:click={() => saveFlow()}
on:click={async () => {
await handleSaveFlow()
}}
dropdownItems={!newFlow ? dropdownItems : undefined}
>
Deploy
@@ -1346,16 +1425,16 @@
type="text"
placeholder="Deployment message"
bind:value={deploymentMsg}
on:keydown={(e) => {
on:keydown={async (e) => {
if (e.key === 'Enter') {
saveFlow(deploymentMsg)
await handleSaveFlow(deploymentMsg)
}
}}
bind:this={msgInput}
/>
<Button
size="xs"
on:click={() => saveFlow(deploymentMsg)}
on:click={async () => await handleSaveFlow(deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>
@@ -20,7 +20,7 @@
export let noSide = false
export let download = false
export let noGraph = false
export let triggerNode = false
export let stepDetail: FlowModule | string | undefined = undefined
const dispatch = createEventDispatcher()
@@ -33,6 +33,7 @@
class:overflow-auto={overflowAuto}
>
<FlowGraphV2
{triggerNode}
path={flow?.path}
{download}
minHeight={400}
@@ -197,6 +197,7 @@
args: job?.args,
tag: job?.tag
}
setModuleState(mod.id ?? '', newState)
})
.catch((e) => {
@@ -207,7 +208,6 @@
(mod.type == 'Success' || mod.type == 'Failure') &&
!['Success', 'Failure'].includes($localModuleStates?.[mod.id ?? '']?.type)
) {
// console.log(mod.id, 'FOO')
setModuleState(
mod.id ?? '',
{
@@ -423,6 +423,11 @@
started_at
})
} else {
const parent_module = mod['parent_module']
// Delete existing failure node attached to the same parent module
removeFailureNode(mod.id, parent_module)
setModuleState(
mod.id,
{
@@ -432,7 +437,7 @@
result: job['result'],
job_id: job.id,
tag: job.tag,
parent_module: mod['parent_module'],
parent_module,
duration_ms: job['duration_ms'],
started_at: started_at,
flow_jobs: mod.flow_jobs,
@@ -444,6 +449,7 @@
},
force
)
setDurationStatusByJob(mod.id, job.id, {
created_at: job.created_at ? new Date(job.created_at).getTime() : undefined,
started_at,
@@ -608,6 +614,23 @@
let storedListJobs: Record<number, Job> = {}
let wrapperHeight: number = 0
function removeFailureNode(id: string, parent_module: any) {
if (id?.startsWith('failure-') && parent_module) {
;[...globalModuleStates, localModuleStates].forEach((stateMapStore) => {
stateMapStore.update((stateMap) => {
if (id) {
Object.keys(stateMap).forEach((key) => {
if (stateMap[key]?.parent_module == parent_module) {
delete stateMap[key]
}
})
}
return stateMap
})
})
}
}
</script>
{#if notAnonynmous}
@@ -857,7 +880,7 @@
{childFlow}
globalModuleStates={[localModuleStates, ...globalModuleStates]}
globalDurationStatuses={[localDurationStatuses, ...globalDurationStatuses]}
render={failedRetry == retry_selected}
render={failedRetry == retry_selected && render}
reducedPolling={false}
{workspaceId}
jobId={failedRetry}
@@ -945,6 +968,7 @@
</div>
<FlowGraphV2
triggerNode={true}
download={!hideDownloadInGraph}
minHeight={wrapperHeight}
success={jobId != undefined && isSuccess(job?.['success'])}
@@ -6,6 +6,7 @@
import Skeleton from './common/skeleton/Skeleton.svelte'
import TableCustom from './TableCustom.svelte'
import { sendUserToast } from '$lib/toast'
import { Loader2 } from 'lucide-svelte'
export let name: string
@@ -57,7 +58,12 @@
>
</div>
</div>
<h2>Members ({members?.length ?? 0})</h2>
<h2
>Members ({#if members?.length != undefined}{members?.length ?? 0}{:else}<Loader2
class="animate-spin"
size={10}
/>{/if})</h2
>
<div class="flex items-start">
<input type="text" placeholder="john@acme.com" bind:value={email} />
<Button
@@ -3,6 +3,7 @@
import CollapseLink from './CollapseLink.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
import { onMount } from 'svelte'
export let name: string
export let value: any
@@ -13,6 +14,21 @@
let tenant: string = ''
$: name == 'microsoft' && changeTenantId(tenant)
onMount(() => {
try {
if (
name == 'microsoft' &&
value &&
value['login_config'] &&
typeof value['login_config']['auth_url'] == 'string'
) {
tenant = value['login_config']['auth_url'].split('/')[3]
}
} catch (e) {
console.error('Could not set tenantId', e)
}
})
function changeTenantId(tenant: string) {
if (value && tenant) {
if (tenant != '') {
@@ -7,10 +7,10 @@
export let primary: boolean = true
</script>
<div class="flex flex-row flex-wrap justify-between pt-6 pb-2 my-4">
<div class="flex flex-row flex-wrap justify-between pb-2 my-4 mr-2">
{#if primary}
<span class="flex items-center gap-2">
<h1 class="!text-2xl font-semibold leading-6 tracking-tight">{title}</h1>
<h1 class="!text-2xl font-semibold whitespace-nowrap leading-6 tracking-tight">{title}</h1>
{#if tooltip != '' || documentationLink}
<Tooltip {documentationLink}>
{tooltip}
+12 -5
View File
@@ -13,7 +13,8 @@
ScheduleService,
ScriptService,
HttpTriggerService,
VariableService
VariableService,
WebsocketTriggerService
} from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher, getContext } from 'svelte'
@@ -35,6 +36,7 @@
| 'app'
| 'raw_app'
| 'http_trigger'
| 'websocket_trigger'
let meta: Meta | undefined = undefined
export let fullNamePlaceholder: string | undefined = undefined
export let namePlaceholder = ''
@@ -218,6 +220,11 @@
workspace: $workspaceStore!,
path: path
})
} else if (kind == 'websocket_trigger') {
return await WebsocketTriggerService.existsWebsocketTrigger({
workspace: $workspaceStore!,
path: path
})
} else {
return false
}
@@ -231,10 +238,10 @@
error = 'This name is not valid'
return false
} else if (meta.owner == '' && meta.ownerKind == 'folder') {
error = 'Folder need to be chosen'
error = 'Folder needs to be chosen'
return false
} else if (meta.owner == '' && meta.ownerKind == 'group') {
error = 'Group need to be chosen'
error = 'Group needs to be chosen'
return false
} else {
return true
@@ -455,10 +462,10 @@
/>
<!-- <span class="font-mono text-sm break-all">{path}</span> -->
</div>
<div class="text-red-600 dark:text-red-400 text-2xs">{error}</div>
<div class="text-red-600 dark:text-red-400 text-2xs mt-1.5">{error}</div>
</div>
{#if kind != 'app' && kind != 'schedule' && kind != 'http_trigger' && initialPath != '' && initialPath != undefined && initialPath != path}
{#if kind != 'app' && kind != 'schedule' && kind != 'http_trigger' && kind != 'websocket_trigger' && initialPath != '' && initialPath != undefined && initialPath != path}
<Alert type="warning" class="mt-4" title="Moving may break other items relying on it">
You are renaming an item that may be depended upon by other items. This may break apps, flows
or resources. Find if it used elsewhere using the content search. Note that linked variables
@@ -15,7 +15,7 @@
const dispatch = createEventDispatcher()
$: workspace = workspaceOverride ?? $workspaceStore
$: workspace = workspaceOverride ?? $workspaceStore!
let syncIteration: number = 0
let errorIteration = 0
@@ -76,7 +76,7 @@
return abstractRun(
() =>
JobService.runScriptByPath({
workspace: $workspaceStore!,
workspace: workspace,
path: path,
requestBody: args,
skipPreprocessor: true
@@ -93,7 +93,7 @@
return abstractRun(
() =>
JobService.runScriptByHash({
workspace: $workspaceStore!,
workspace: workspace,
hash: hash,
requestBody: args,
skipPreprocessor: true
@@ -110,7 +110,7 @@
return abstractRun(
() =>
JobService.runFlowByPath({
workspace: $workspaceStore!,
workspace: workspace,
path: path ?? '',
requestBody: args,
skipPreprocessor: true
@@ -130,7 +130,7 @@
return abstractRun(
() =>
JobService.runScriptPreview({
workspace: $workspaceStore!,
workspace: workspace,
requestBody: {
path,
content: code,
@@ -154,7 +154,7 @@
currentId = undefined
try {
await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
workspace: workspace ?? '',
id,
requestBody: {}
})
@@ -218,7 +218,7 @@
$primarySchedule = {
summary: '',
args: {},
cron: '0 0 0 /1 * * *',
cron: '0 0 */1 * * *',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
enabled: true
}
@@ -17,7 +17,10 @@
emptyString,
encodeState,
formatCron,
orderedJsonStringify
orderedJsonStringify,
type Value
} from '$lib/utils'
import Path from './Path.svelte'
import ScriptEditor from './ScriptEditor.svelte'
@@ -63,6 +66,7 @@
import CustomPopover from './CustomPopover.svelte'
import Summary from './Summary.svelte'
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import TriggersEditor from './triggers/TriggersEditor.svelte'
import type { ScheduleTrigger, TriggerContext } from './triggers'
@@ -83,6 +87,12 @@
export let customUi: ScriptBuilderWhitelabelCustomUi = {}
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
let metadataOpen =
!neverShowMeta &&
(showMeta ||
@@ -99,9 +109,9 @@
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
: undefined
)
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
>('webhooks')
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
primaryScheduleStore.set(schedule)
@@ -241,7 +251,62 @@
}
}
async function editScript(stay: boolean, deploymentMsg?: string): Promise<void> {
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;
// 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
}
}
async function syncWithDeployed(){
const latestScript = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: script.path,
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,
};
deployedBy = latestScript.created_by;
}
async function editScript(stay: boolean, parentHash: string, deploymentMsg?: string, ): Promise<void> {
loadingSave = true
try {
try {
@@ -265,7 +330,7 @@
summary: script.summary,
description: script.description ?? '',
content: script.content,
parent_hash: script.parent_hash,
parent_hash: parentHash,
schema: script.schema,
is_template: script.is_template,
language: script.language,
@@ -331,7 +396,7 @@
} else if (enabled) {
await createSchedule(script.path)
}
} else if (scheduleExists) {
} else if (scheduleExists && !$triggersCount?.primary_schedule) {
await ScheduleService.deleteSchedule({
workspace: $workspaceStore ?? '',
path: script.path
@@ -472,7 +537,7 @@
{
label: 'Deploy & Stay here',
onClick: () => {
editScript(true)
handleEditScript(true)
}
},
{
@@ -530,6 +595,14 @@
<svelte:window on:keydown={onKeyDown} />
<slot />
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={script}
/>
{#if !$userStore?.operator}
<Drawer
placement="right"
@@ -1094,7 +1167,7 @@
</div>
<div class="gap-4 flex">
{#if $primaryScheduleStore && $primaryScheduleStore?.enabled}
{#if $primaryScheduleStore != undefined ? $primaryScheduleStore && $primaryScheduleStore?.enabled : $triggersCount?.primary_schedule}
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: Calendar }}
@@ -1107,7 +1180,11 @@
$selectedTriggerStore = 'schedules'
}}
>
{$primaryScheduleStore?.cron ?? ''}
{$primaryScheduleStore != undefined
? $primaryScheduleStore
? $primaryScheduleStore?.cron
: ''
: $triggersCount?.primary_schedule?.schedule}
</Button>
{/if}
{#if customUi?.topBar?.path != false}
@@ -1150,14 +1227,16 @@
color="light"
variant="border"
size="xs"
on:click={() => {
on:click={async () => {
if (!savedScript) {
return
}
await syncWithDeployed()
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedScript,
deployed: deployedValue ?? savedScript,
draft: savedScript['draft'],
current: script
})
@@ -1202,7 +1281,7 @@
size="xs"
disabled={!fullyLoaded}
startIcon={{ icon: Save }}
on:click={() => editScript(false)}
on:click={() => handleEditScript(false)}
dropdownItems={computeDropdownItems(initialPath)}
>
Deploy
@@ -1216,13 +1295,13 @@
bind:this={msgInput}
on:keydown={(e) => {
if (e.key === 'Enter') {
editScript(false, deploymentMsg)
handleEditScript(false, deploymentMsg)
}
}}
/>
<Button
size="xs"
on:click={() => editScript(false, deploymentMsg)}
on:click={() => handleEditScript(false, deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>
@@ -25,6 +25,7 @@
| 'app'
| 'raw_app'
| 'http_trigger'
| 'websocket_trigger'
let kind: Kind
let path: string = ''
@@ -38,7 +38,7 @@
result: undefined as string | undefined
})
$: !value && handleDefault(resolvedConfig.defaultValue)
$: handleDefault(resolvedConfig.defaultValue)
function formatDate(dateString: string, formatString: string = 'dd.MM.yyyy') {
if (formatString === '') {
@@ -40,7 +40,8 @@
cleanValueProperties,
copyToClipboard,
truncateRev,
orderedJsonStringify
orderedJsonStringify,
type Value
} from '../../../utils'
import type {
AppInput,
@@ -85,6 +86,7 @@
import Summary from '$lib/components/Summary.svelte'
import ToggleEnable from '$lib/components/common/toggleButton-v2/ToggleEnable.svelte'
import HideButton from './settingsPanel/HideButton.svelte'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
async function hash(message) {
try {
@@ -121,6 +123,11 @@
export let rightPanelHidden: boolean = false
export let bottomPanelHidden: boolean = false
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
const {
app,
summary,
@@ -360,6 +367,50 @@
}
}
async function handleUpdateApp(npath: string) {
// We have to make sure there is no updates when we clicked the button
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
// Handle through confirmation modal
await syncWithDeployed();
confirmCallback = async () => {
open = false
await updateApp(npath)
}
// Open confirmation modal
open = true
}
}
async function syncWithDeployed() {
const deployedApp = await AppService.getAppByPath({
workspace: $workspaceStore!,
path: appPath,
withStarredInfo: true
})
deployedBy = deployedApp.created_by
// Strip off extra information
deployedValue = {
...deployedApp,
starred: undefined,
id: undefined,
created_at: undefined,
created_by: undefined,
versions: undefined,
extra_perms: undefined //
}
}
async function updateApp(npath: string) {
await computeTriggerables()
await AppService.updateApp({
@@ -577,11 +628,12 @@
if (version === undefined) {
return
}
const appHistory = await AppService.getAppHistoryByPath({
const appVersion = await AppService.getAppLatestVersion({
workspace: $workspaceStore!,
path: appPath
})
onLatest = version === appHistory[0]?.version
onLatest = version === appVersion?.version
}
$: saveDrawerOpen && compareVersions()
@@ -706,14 +758,18 @@
{
displayName: 'Diff',
icon: DiffIcon,
action: () => {
action: async () => {
if (!savedApp) {
return
}
// deployedValue should be syncronized when we open Diff
await syncWithDeployed();
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedApp,
deployed: deployedValue ?? savedApp,
draft: savedApp.draft,
current: {
summary: $summary,
@@ -766,6 +822,20 @@
}}
/>
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={{
summary: $summary,
value: $app,
path: newPath || savedApp?.draft?.path || savedApp?.path,
policy
}}
/>
{#if appPath == ''}
<Drawer bind:open={draftDrawerOpen} size="800px">
<DrawerContent title="Initial draft save" on:close={() => closeDraftDrawer()}>
@@ -823,8 +893,8 @@
<Drawer bind:open={saveDrawerOpen} size="800px">
<DrawerContent title="Deploy" on:close={() => closeSaveDrawer()}>
{#if !onLatest}
<Alert title="You're not on the latest app version" type="warning">
By deploying, you may overwrite changes made by other users.
<Alert title="You're not on the latest app version. " type="warning">
By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff.
</Alert>
<div class="py-2" />
{/if}
@@ -880,15 +950,18 @@
variant="border"
color="light"
disabled={!savedApp || savedApp.draft_only}
on:click={() => {
on:click={async () => {
if (!savedApp) {
return
}
// deployedValue should be syncronized when we open Diff
await syncWithDeployed();
saveDrawerOpen = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedApp,
deployed: deployedValue ?? savedApp,
draft: savedApp.draft,
current: {
summary: $summary,
@@ -902,7 +975,7 @@
if (appPath == '') {
createApp(newPath)
} else {
updateApp(newPath)
handleUpdateApp(newPath)
}
}
}
@@ -921,7 +994,7 @@
if (appPath == '') {
createApp(newPath)
} else {
updateApp(newPath)
handleUpdateApp(newPath)
}
}}
>
@@ -241,6 +241,7 @@
USERS_SETPASSWORD: 'users.setpassword',
USERS_UPDATE: 'users.update',
USERS_LOGIN: 'users.login',
USERS_LOGIN_FAILURE: 'users.login_failure',
USERS_LOGOUT: 'users.logout',
USERS_ACCEPT_INVITE: 'users.accept_invite',
USERS_DECLINE_INVITE: 'users.decline_invite',
@@ -251,6 +252,7 @@
USERS_IMPERSONATE: 'users.impersonate',
USERS_LEAVE_WORKSPACE: 'users.leave_workspace',
OAUTH_LOGIN: 'oauth.login',
OAUTH_LOGIN_FAILURE: 'oauth.login_failure',
OAUTH_SIGNUP: 'oauth.signup',
VARIABLES_CREATE: 'variables.create',
VARIABLES_DELETE: 'variables.delete',
@@ -0,0 +1,53 @@
<script lang="ts">
import ConfirmationModal from './ConfirmationModal.svelte'
import Button from '../button/Button.svelte'
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
import { type Value } from '$lib/utils'
export let deployedValue: Value | undefined = undefined
export let currentValue: Value | undefined = undefined
export let diffDrawer: DiffDrawer | undefined = undefined
export let confirmCallback: () => void
export let deployedBy : string | undefined = undefined
export let open = false
</script>
<ConfirmationModal
{open}
title={"New version deployed by " + deployedBy}
confirmationText="Override"
on:canceled={() => {
open = false
}}
on:confirmed={() => confirmCallback()}
>
<div class="flex flex-col w-full space-y-4">
<span>There was deployed new version while you were editing this one.</span>
{#if diffDrawer}
<Button
wrapperClasses="self-start"
color="light"
variant="border"
size="xs"
on:click={() => {
if (!deployedValue || !currentValue) {
return
}
open = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'simple',
original: deployedValue,
current: currentValue,
title: 'Deployed <> Current',
button: {
text: 'Override anyway',
onClick: () => confirmCallback()
}
})
}}
>Show diff
</Button>
{/if}
</div>
</ConfirmationModal>
@@ -6,7 +6,13 @@
import FlowViewerInner from '../FlowViewerInner.svelte'
import DetailPageTriggerPanel from './DetailPageTriggerPanel.svelte'
export let triggerSelected: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' = 'webhooks'
export let triggerSelected:
| 'webhooks'
| 'emails'
| 'schedules'
| 'cli'
| 'routes'
| 'websockets' = 'webhooks'
export let flow_json: any | undefined = undefined
export let isOperator: boolean = false
@@ -25,13 +31,12 @@
{/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}
{#if !flow_json && !isOperator}
<Tab value="schema">Schema</Tab>
{/if}
<svelte:fragment slot="content">
<div class="overflow-hidden" style="height:calc(100% - 32px);">
@@ -41,10 +46,14 @@
<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" />
@@ -19,9 +19,9 @@
let clientWidth = window.innerWidth
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
>('webhooks')
setContext<TriggerContext>('TriggerContext', {
selectedTrigger: selectedTriggerStore,
@@ -48,13 +48,13 @@
>
<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" />
<slot slot="details" name="details" />
<slot slot="script" name="script" />
<slot slot="save_inputs" name="save_inputs" />
<slot slot="flow_step" name="flow_step" />
<slot slot="schema" name="schema" />
</DetailPageDetailPanel>
</Pane>
</Splitpanes>
@@ -69,6 +69,10 @@
{#if !isOperator}
<Tab value="triggers">Triggers</Tab>
{/if}
{#if !flow_json}
<Tab value="script">Script</Tab>
{/if}
<svelte:fragment slot="content">
<TabContent value="form" class="flex flex-col flex-1 h-full">
<slot name="form" />
@@ -81,11 +85,16 @@
<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>
</svelte:fragment>
</Tabs>
</div>
@@ -1,42 +1,54 @@
<script lang="ts">
import { Tabs, Tab } from '$lib/components/common'
import { CalendarCheck2, MailIcon, Route, Terminal, Webhook } from 'lucide-svelte'
import { CalendarCheck2, MailIcon, Route, Terminal, Webhook, Unplug } from 'lucide-svelte'
import HighlightTheme from '../HighlightTheme.svelte'
export let triggerSelected: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' = 'webhooks'
export let triggerSelected:
| 'webhooks'
| 'emails'
| 'schedules'
| 'cli'
| 'routes'
| 'websockets' = 'webhooks'
</script>
<HighlightTheme />
<Tabs bind:selected={triggerSelected}>
<Tab value="webhooks">
<span class="flex flex-row gap-2 items-center">
<Webhook size={14} />
<span class="flex flex-row gap-2 items-center text-xs">
<Webhook size={12} />
Webhooks
</span>
</Tab>
<Tab value="schedules">
<span class="flex flex-row gap-2 items-center">
<CalendarCheck2 size={14} />
<span class="flex flex-row gap-2 items-center text-xs">
<CalendarCheck2 size={12} />
Schedules
</span>
</Tab>
<Tab value="routes">
<span class="flex flex-row gap-2 items-center">
<Route size={14} />
<span class="flex flex-row gap-2 items-center text-xs">
<Route size={12} />
HTTP
</span>
</Tab>
<Tab value="websockets">
<span class="flex flex-row gap-2 items-center text-xs">
<Unplug size={12} />
Websockets
</span>
</Tab>
<Tab value="emails">
<span class="flex flex-row gap-2 items-center">
<MailIcon size={14} />
<span class="flex flex-row gap-2 items-center text-xs">
<MailIcon size={12} />
Email
</span>
</Tab>
<Tab value="cli">
<span class="flex flex-row gap-2 items-center">
<Terminal size={14} />
<span class="flex flex-row gap-2 items-center text-xs">
<Terminal size={12} />
CLI
</span>
</Tab>
@@ -52,6 +64,8 @@
<slot name="emails" />
{:else if triggerSelected === 'schedules'}
<slot name="schedules" />
{:else if triggerSelected === 'websockets'}
<slot name="websockets" />
{:else if triggerSelected === 'cli'}
<slot name="cli" />
{/if}
@@ -1,15 +1,33 @@
<script lang="ts">
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte'
import { FlowService, type Flow } from '$lib/gen'
import type { ScheduleTrigger, TriggerContext } from '$lib/components/triggers'
import { FlowService, type Flow, type TriggersCount } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
export let path: string
export let noSide = false
let flow: Flow | undefined = undefined
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const triggersCount = writable<TriggersCount | undefined>(undefined)
setContext<TriggerContext>('TriggerContext', {
primarySchedule: primaryScheduleStore,
selectedTrigger: selectedTriggerStore,
triggersCount: triggersCount
})
async function loadFlow(path: string) {
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
triggersCount.set(
await FlowService.getTriggersCountOfFlow({ workspace: $workspaceStore!, path })
)
}
$: path && loadFlow(path)
@@ -17,7 +35,7 @@
<div class="flex flex-col flex-1 h-full overflow-auto">
{#if flow}
<FlowGraphViewer {noSide} {flow} />
<FlowGraphViewer triggerNode={true} {noSide} {flow} />
{:else}
<Skeleton layout={[[40]]} />
{/if}
@@ -320,6 +320,7 @@
<div class="z-10 flex-auto grow bg-surface-secondary" bind:clientHeight={minHeight}>
<FlowGraphV2
triggerNode={true}
path={$pathStore}
{newFlow}
{disableAi}
@@ -37,6 +37,7 @@
import { Alert, Drawer } from '../common'
import Button from '../common/button/Button.svelte'
import FlowYamlEditor from '../flows/header/FlowYamlEditor.svelte'
import BranchOneEndNode from './renderers/nodes/branchOneEndNode.svelte'
export let success: boolean | undefined = undefined
export let modules: FlowModule[] | undefined = []
export let failureModule: FlowModule | undefined = undefined
@@ -61,6 +62,7 @@
export let flowInputsStore: Writable<FlowInput | undefined> = writable<FlowInput | undefined>(
undefined
)
export let triggerNode = false
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
@@ -178,9 +180,11 @@
$useDataflow,
$selectedId,
moving,
{
path
}
triggerNode
? {
path
}
: undefined
)
const nodes = writable<Node[]>([])
@@ -212,7 +216,7 @@
whileLoopStart: ForLoopStartNode,
whileLoopEnd: ForLoopEndNode,
branchOneStart: BranchOneStart,
branchOneEnd: BranchAllEndNode,
branchOneEnd: BranchOneEndNode,
noBranch: NoBranchNode,
trigger: TriggersNode
} as any
@@ -263,7 +267,7 @@
<div style={`height: ${height}px; max-height: ${maxHeight}px;`} bind:clientWidth={width}>
{#if graph?.error}
<div class="center-center">
<div class="center-center p-2">
<Alert title="Error parsing the flow" type="error" class="max-w-1/2">
{graph.error}
@@ -36,14 +36,15 @@ export default function graphBuilder(
} {
const nodes: Node[] = []
const edges: Edge[] = []
try {
if (!modules) {
return { nodes, edges }
}
function addNode(module: FlowModule, offset: number, type: string, subModules?: FlowModule[]) {
if (nodes.some((n) => n.id === module.id)) {
const duplicated = nodes.find((n) => n.id === module.id)
if (duplicated) {
console.log('Duplicated node detected: ', module, duplicated)
throw new Error(`Duplicated node detected: ${module.id}`)
}
@@ -155,7 +156,7 @@ export default function graphBuilder(
}
}
if (extra.path) {
if (extra.path && triggerProps != undefined) {
const triggerNode: Node = {
id: 'Trigger',
position: { x: -1, y: -1 },
@@ -199,7 +200,8 @@ export default function graphBuilder(
nextNode: Node,
currentOffset = 0,
disableMoveIds: string[] = [],
parentIndex?: string
parentIndex?: string,
branchChosen?: boolean
) {
let previousId: string | undefined = undefined
@@ -428,13 +430,15 @@ export default function graphBuilder(
addEdge(module.id, defaultBranch.id, { type: 'empty' })
const branchChosen = extra.flowModuleStates?.[module.id]?.branchChosen
processModules(
module.value.default,
defaultBranch,
endNode,
currentOffset,
localDisableMoveIds,
parentIndex ? `${parentIndex}-${index}` : index.toString()
parentIndex ? `${parentIndex}-${index}` : index.toString(),
branchChosen == 0
)
module.value.branches.forEach((branch, branchIndex) => {
@@ -466,7 +470,8 @@ export default function graphBuilder(
endNode,
currentOffset,
localDisableMoveIds,
parentIndex ? `${parentIndex}-${index}` : index.toString()
parentIndex ? `${parentIndex}-${index}` : index.toString(),
branchChosen == branchIndex + 1
)
})
@@ -475,6 +480,8 @@ export default function graphBuilder(
addNode(module, currentOffset, 'module', modules)
previousId = module.id
}
if (index === 0) {
@@ -492,27 +499,29 @@ export default function graphBuilder(
}
})
if (failureModule) {
const id = parentIndex ? `failure-${parentIndex}` : 'failure'
const failureState = extra.flowModuleStates?.[id] as GraphModuleState | undefined
if (failureState && failureState.parent_module) {
addNode(
{
...failureModule,
id: id
},
0,
'module'
)
addEdge(failureState.parent_module, id, { type: 'empty' })
}
}
}
}
processModules(modules, inputNode, resultNode)
if (failureModule) {
let toAdd: Record<string, string> = {}
Object.keys(extra.flowModuleStates ?? {}).forEach((id) => {
if (id.startsWith('failure-')) {
const failureState = extra.flowModuleStates?.[id] as GraphModuleState | undefined
if (failureState?.parent_module) {
toAdd[failureState.parent_module] = id
}
}
})
Object.entries(toAdd).forEach((x) => {
addNode({ ...failureModule, id: x[1] }, 0, 'module')
addEdge(x[0], x[1], { type: 'empty' })
})
}
if (preprocessorModule) {
addNode(preprocessorModule, 0, 'module')
const id = JSON.parse(JSON.stringify(preprocessorModule.id))
@@ -16,7 +16,7 @@
<NodeWrapper let:darkMode offset={data.offset}>
<VirtualItem
label={'Collect result from all branches'}
label={'Collect result from chosen branch'}
id={data.id}
selectable={true}
selected={false}
@@ -1,5 +1,5 @@
<script lang="ts">
import { Calendar, Mail, Webhook } from 'lucide-svelte'
import { Calendar, Mail, Webhook, Unplug } from 'lucide-svelte'
import TriggerButton from './TriggerButton.svelte'
import Popover from '$lib/components/Popover.svelte'
@@ -89,6 +89,22 @@
</Popover>
{/if}
{#if !showOnlyWithCount || ($triggersCount?.websocket_count ?? 0) > 0}
<Popover>
<svelte:fragment slot="text">Websockets</svelte:fragment>
<TriggerButton
on:click={() => {
$selectedTrigger = 'websockets'
dispatch('select')
}}
selected={selected && $selectedTrigger === 'websockets'}
>
<TriggerCount count={$triggersCount?.websocket_count} />
<Unplug size={12} />
</TriggerButton>
</Popover>
{/if}
{#if !showOnlyWithCount || ($triggersCount?.email_count ?? 0) > 0}
<Popover>
<svelte:fragment slot="text">Emails</svelte:fragment>
@@ -98,7 +98,9 @@
async function removeAllInvitesFromDomain() {
await Promise.all(
invites
.filter((x) => (isCloudHosted() ? x.email.endsWith('@' + auto_invite_domain ?? '') : true))
.filter((x) =>
isCloudHosted() ? x.email.endsWith('@' + (auto_invite_domain ?? '')) : true
)
.map(({ email, is_admin, operator }) =>
WorkspaceService.deleteInvite({
workspace: $workspaceStore ?? '',
@@ -137,9 +139,11 @@
</div>
</div>
</div>
<div class="flex flex-row justify-between items-center">
<div class="flex flex-row justify-between items-center pt-2">
<PageHeader
title="Members ({filteredUsers?.length ?? users?.length ?? ''})"
title="Members {(filteredUsers?.length ?? users?.length) != undefined
? `(${filteredUsers?.length ?? users?.length})`
: ''}"
primary={true}
tooltip="Manage users manually or enable SSO authentication."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification"
@@ -17,7 +17,7 @@
if (href === `${base}/`) {
isSelected = $page.url.pathname === href
} else {
isSelected = $page.url.pathname.includes(href)
isSelected = $page.url.pathname.startsWith(href)
}
})
</script>
@@ -21,7 +21,8 @@
ServerCog,
Settings,
UserCog,
Plus
Plus,
Unplug
} from 'lucide-svelte'
import Menu from '../common/menu/MenuV2.svelte'
import MenuButton from './MenuButton.svelte'
@@ -77,6 +78,13 @@
icon: Route,
disabled: $userStore?.operator,
kind: 'http'
},
{
label: 'Websockets',
href: '/websocket_triggers',
icon: Unplug,
disabled: $userStore?.operator,
kind: 'ws'
}
]
+1 -1
View File
@@ -10,7 +10,7 @@ export type ScheduleTrigger = {
}
export type TriggerContext = {
selectedTrigger: Writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>
selectedTrigger: Writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'>
primarySchedule: Writable<ScheduleTrigger | undefined | false>
triggersCount: Writable<TriggersCount | undefined>
}
@@ -39,6 +39,7 @@
itemKind = isFlow ? 'flow' : 'script'
edit = true
dirtyPath = false
dirtyRoutePath = false
await loadTrigger()
} catch (err) {
sendUserToast(`Could not load route: ${err}`, true)
@@ -58,6 +59,7 @@
requires_auth = false
initialRoutePath = ''
route_path = ''
dirtyRoutePath = false
http_method = 'post'
initialScriptPath = ''
fixedScriptPath = fixedScriptPath_ ?? ''
@@ -73,6 +75,7 @@
let path: string = ''
let pathError = ''
let routeError = ''
let dirtyRoutePath = false
let is_async = false
let requires_auth = false
let initialRoutePath = ''
@@ -239,6 +242,9 @@
class={routeError === ''
? ''
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
on:input={() => {
dirtyRoutePath = true
}}
/>
</label>
@@ -272,7 +278,10 @@
}}
/>
</div>
<div class="text-red-600 dark:text-red-400 text-2xs">{routeError}</div>
<div class="text-red-600 dark:text-red-400 text-2xs mt-1.5"
>{dirtyRoutePath ? routeError : ''}</div
>
</div>
</div>
</Section>
@@ -10,6 +10,7 @@
import FlowCard from '../flows/common/FlowCard.svelte'
import { getContext } from 'svelte'
import type { TriggerContext } from '$lib/components/triggers'
import WebsocketTriggersPanel from './WebsocketTriggersPanel.svelte'
export let noEditor: boolean
export let newItem = false
@@ -27,6 +28,7 @@
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">Routes</Tab>
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
<svelte:fragment slot="content">
@@ -60,6 +62,12 @@
</div>
{/if}
{#if $selectedTrigger === 'websockets'}
<div class="p-4">
<WebsocketTriggersPanel {newItem} path={currentPath} {isFlow} />
</div>
{/if}
{#if $selectedTrigger === 'schedules'}
<div class="p-2">
<RunPageSchedules
@@ -0,0 +1,23 @@
<script lang="ts">
import { tick } from 'svelte'
import WebsocketTriggerEditorInner from './WebsocketTriggerEditorInner.svelte'
let open = false
export async function openEdit(ePath: string, isFlow: boolean) {
open = true
await tick()
drawer?.openEdit(ePath, isFlow)
}
export async function openNew(is_flow: boolean, initial_script_path?: string) {
open = true
await tick()
drawer?.openNew(is_flow, initial_script_path)
}
let drawer: WebsocketTriggerEditorInner
</script>
{#if open}
<WebsocketTriggerEditorInner on:update bind:this={drawer} />
{/if}
@@ -0,0 +1,338 @@
<script lang="ts">
import { Alert, Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import Path from '$lib/components/Path.svelte'
import Required from '$lib/components/Required.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import { WebsocketTriggerService } from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import Section from '$lib/components/Section.svelte'
import { Loader2, Save, X, Plus } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
import Toggle from '../Toggle.svelte'
import { fade } from 'svelte/transition'
import JsonEditor from '../apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
let drawer: Drawer
let is_flow: boolean = false
let initialPath = ''
let edit = true
let itemKind: 'flow' | 'script' = 'script'
let script_path = ''
let initialScriptPath = ''
let fixedScriptPath = ''
let path: string = ''
let pathError = ''
let url = ''
let urlError = ''
let dirtyUrl = false
let enabled = false
let filters: {
key: string
value: any
}[] = []
let dirtyPath = false
let can_write = true
let drawerLoading = true
const dispatch = createEventDispatcher()
$: is_flow = itemKind === 'flow'
export async function openEdit(ePath: string, isFlow: boolean) {
drawerLoading = true
try {
drawer?.openDrawer()
initialPath = ePath
itemKind = isFlow ? 'flow' : 'script'
edit = true
dirtyPath = false
dirtyUrl = false
await loadTrigger()
} catch (err) {
sendUserToast(`Could not load websocket trigger: ${err}`, true)
} finally {
drawerLoading = false
}
}
export async function openNew(nis_flow: boolean, fixedScriptPath_?: string) {
drawerLoading = true
try {
drawer?.openDrawer()
is_flow = nis_flow
edit = false
itemKind = nis_flow ? 'flow' : 'script'
url = ''
dirtyUrl = false
initialScriptPath = ''
fixedScriptPath = fixedScriptPath_ ?? ''
script_path = fixedScriptPath
path = ''
initialPath = ''
filters = []
dirtyPath = false
} finally {
drawerLoading = false
}
}
async function loadTrigger(): Promise<void> {
const s = await WebsocketTriggerService.getWebsocketTrigger({
workspace: $workspaceStore!,
path: initialPath
})
script_path = s.script_path
initialScriptPath = s.script_path
is_flow = s.is_flow
path = s.path
url = s.url
enabled = s.enabled
filters = s.filters
can_write = canWrite(s.path, s.extra_perms, $userStore)
}
async function updateTrigger(): Promise<void> {
if (edit) {
await WebsocketTriggerService.updateWebsocketTrigger({
workspace: $workspaceStore!,
path: initialPath,
requestBody: {
path,
script_path,
is_flow,
url,
filters
}
})
sendUserToast(`Route ${path} updated`)
} else {
await WebsocketTriggerService.createWebsocketTrigger({
workspace: $workspaceStore!,
requestBody: {
path,
script_path,
is_flow,
url,
enabled: true,
filters
}
})
sendUserToast(`Route ${path} created`)
}
if (!$usedTriggerKinds.includes('ws')) {
$usedTriggerKinds = [...$usedTriggerKinds, 'ws']
}
dispatch('update')
drawer.closeDrawer()
}
let validateTimeout: NodeJS.Timeout | undefined = undefined
function validateUrl(url: string) {
urlError = ''
if (validateTimeout) {
clearTimeout(validateTimeout)
}
validateTimeout = setTimeout(() => {
if (/^(ws:|wss:)\/\/[^\s]+$/.test(url) === false) {
urlError = 'Invalid websocket URL'
}
validateTimeout = undefined
}, 500)
}
$: validateUrl(url)
</script>
<Drawer size="700px" bind:this={drawer}>
<DrawerContent
title={edit
? can_write
? `Edit WS trigger ${initialPath}`
: `WS trigger ${initialPath}`
: 'New WS trigger'}
on:close={drawer.closeDrawer}
>
<svelte:fragment slot="actions">
{#if !drawerLoading && can_write}
{#if edit}
<div class="mr-8 center-center -mt-1">
<Toggle
disabled={!can_write}
checked={enabled}
options={{ right: 'enable', left: 'disable' }}
on:change={async (e) => {
await WebsocketTriggerService.setWebsocketTriggerEnabled({
path: initialPath,
workspace: $workspaceStore ?? '',
requestBody: { enabled: e.detail }
})
sendUserToast(
`${e.detail ? 'enabled' : 'disabled'} websocket trigger ${initialPath}`
)
}}
/>
</div>
{/if}
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' || urlError != '' || emptyString(script_path) || !can_write}
on:click={updateTrigger}
>
Save
</Button>
{/if}
</svelte:fragment>
{#if drawerLoading}
<Loader2 class="animate-spin" />
{:else}
<Alert title="Info" type="info">
{#if edit}
Changes can take up to 30 seconds to take effect.
{:else}
New websocket triggers can take up to 30 seconds to start listening.
{/if}
</Alert>
<div class="flex flex-col gap-12 mt-6">
<div class="flex flex-col gap-4">
<Label label="Path">
<Path
bind:dirty={dirtyPath}
bind:error={pathError}
bind:path
{initialPath}
checkInitialPathExistence={!edit}
namePlaceholder="ws_trigger"
kind="websocket_trigger"
disabled={!can_write}
/>
</Label>
</div>
<Section label="Websocket">
<div class="flex flex-col w-full gap-4">
<label class="block grow w-full">
<div class="text-secondary text-sm flex items-center gap-1 w-full justify-between">
<div>
URL
<Required required={true} />
</div>
</div>
<input
type="text"
autocomplete="off"
bind:value={url}
disabled={!can_write}
on:input={() => {
dirtyUrl = true
}}
class={urlError === ''
? ''
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
/>
<div class="text-red-600 dark:text-red-400 text-2xs mt-1.5">
{dirtyUrl ? urlError : ''}
</div>
</label>
</div>
</Section>
<Section label="Runnable">
<p class="text-xs mb-1 text-tertiary">
Pick a script or flow to be triggered<Required required={true} />
</p>
<div class="flex flex-row mb-2">
<ScriptPicker
disabled={fixedScriptPath != '' || !can_write}
initialPath={fixedScriptPath || initialScriptPath}
kinds={['script']}
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
allowRefresh
/>
</div>
</Section>
<Section label="Filters">
<p class="text-xs mb-1 text-tertiary">
Filters will limit the execution of the trigger to only messages that match all
criteria.<br />
The JSON filter checks if the value at the key is equal or a superset of the filter value.
</p>
<div class="flex flex-col gap-4 mt-1">
{#each filters as v, i}
<div class="flex w-full gap-4 items-center">
<div class="w-full flex flex-col gap-2">
<div class="flex flex-row gap-2 w-full">
<label class="flex flex-col w-full">
<div class="text-secondary text-sm">Type</div>
<select
class="w-20"
on:change={(e) => {
if (e.target?.['value']) {
filters[i] = {
key: '',
value: ''
}
}
}}
value={'json'}
>
<option value="json">JSON</option>
</select>
</label>
</div>
<label class="flex flex-col w-full">
<div class="text-secondary text-sm">Key</div>
<input type="text" bind:value={v.key} />
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="flex flex-col w-full">
<div class="text-secondary text-sm">Value</div>
<JsonEditor bind:value={v.value} code={JSON.stringify(v.value)} />
</label>
</div>
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
aria-label="Clear"
on:click={() => {
filters = filters.filter((_, index) => index !== i)
}}
>
<X size={14} />
</button>
</div>
{/each}
<div class="flex items-baseline">
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
if (filters == undefined || !Array.isArray(filters)) {
filters = []
}
filters = filters.concat({
key: '',
value: ''
})
}}
startIcon={{ icon: Plus }}
>
Add item
</Button>
</div>
</div>
</Section>
</div>
{/if}
</DrawerContent>
</Drawer>
@@ -0,0 +1,108 @@
<script lang="ts">
import { Button } from '../common'
import { userStore, workspaceStore } from '$lib/stores'
import { WebsocketTriggerService, type WebsocketTrigger } from '$lib/gen'
import { UnplugIcon } from 'lucide-svelte'
import Skeleton from '../common/skeleton/Skeleton.svelte'
import { canWrite } from '$lib/utils'
import Alert from '../common/alert/Alert.svelte'
import type { TriggerContext } from '../triggers'
import { getContext } from 'svelte'
import WebsocketTriggerEditor from './WebsocketTriggerEditor.svelte'
import { isCloudHosted } from '$lib/cloud'
export let isFlow: boolean
export let path: string
export let newItem: boolean = false
let wsTriggerEditor: WebsocketTriggerEditor
$: path && loadTriggers()
const { triggersCount } = getContext<TriggerContext>('TriggerContext')
let wsTriggers: (WebsocketTrigger & { canWrite: boolean })[] | undefined = undefined
export async function loadTriggers() {
try {
wsTriggers = (
await WebsocketTriggerService.listWebsocketTriggers({
workspace: $workspaceStore ?? '',
path,
isFlow
})
).map((x) => {
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
})
$triggersCount = { ...($triggersCount ?? {}), websocket_count: wsTriggers?.length }
} catch (e) {
console.error('impossible to load WS triggers', e)
}
}
</script>
<WebsocketTriggerEditor
on:update={() => {
loadTriggers()
}}
bind:this={wsTriggerEditor}
/>
<div class="flex flex-col gap-4">
{#if !newItem}
{#if isCloudHosted()}
<Alert title="Not compatible with multi-tenant cloud" type="warning">
Websocket triggers are disabled in the multi-tenant cloud.
</Alert>
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
<Button
on:click={() => wsTriggerEditor?.openNew(isFlow, path)}
variant="border"
color="light"
size="xs"
startIcon={{ icon: UnplugIcon }}
>
New WS Trigger
</Button>
{:else}
<Alert title="Only workspace admins can create routes" type="warning" size="xs" />
{/if}
{/if}
{#if wsTriggers}
{#if wsTriggers.length == 0}
<div class="text-xs text-secondary"> No WS triggers </div>
{:else}
<div class="flex flex-col divide-y pt-2">
{#each wsTriggers as wsTriggers (wsTriggers.path)}
<div class="grid grid-cols-5 text-2xs items-center py-2">
<div class="col-span-2 truncate">{wsTriggers.path}</div>
<div class="col-span-2 truncate">
{wsTriggers.url}
</div>
<div class="flex justify-end">
<button
on:click={() => wsTriggerEditor?.openEdit(wsTriggers.path, isFlow)}
class="px-2"
>
{#if wsTriggers.canWrite}
Edit
{:else}
View
{/if}
</button>
</div>
</div>
{/each}
</div>
{/if}
{:else}
<Skeleton layout={[[8]]} />
{/if}
{#if newItem}
<Alert title="Triggers disabled" type="warning" size="xs">
Deploy the {isFlow ? 'flow' : 'script'} to add WS triggers.
</Alert>
{/if}
</div>
+1 -1
View File
@@ -26,5 +26,5 @@ export function approximateFindPythonRelativePath(code: string) {
// Use match to find all matches in the code
const matches = code.match(regex)
return [...(matches?.entries() ?? [])] || []
return [...(matches?.entries() ?? [])]
}
+3 -3
View File
@@ -514,7 +514,7 @@ export async function main(approver?: string) {
const BUN_PREPROCESSOR_MODULE_CODE = `
export async function preprocessor(
wm_trigger: {
kind: 'http' | 'email' | 'webhook',
kind: 'http' | 'email' | 'webhook' | 'websocket',
http?: {
route: string // The route path, e.g. "/users/:id"
path: string // The actual path called, e.g. "/users/123"
@@ -535,7 +535,7 @@ export async function preprocessor(
const DENO_PREPROCESSOR_MODULE_CODE = `
export async function preprocessor(
wm_trigger: {
kind: 'http' | 'email' | 'wehbook',
kind: 'http' | 'email' | 'wehbook' | 'websocket',
http?: {
route: string // The route path, e.g. "/users/:id"
path: string // The actual path called, e.g. "/users/123"
@@ -591,7 +591,7 @@ class Http(TypedDict):
headers: dict[str, str]
class WmTrigger(TypedDict):
kind: Literal["http", "email", "webhook"]
kind: Literal["http", "email", "webhook", "websocket"]
http: Http | None
def preprocessor(
@@ -8,7 +8,6 @@
RawAppService,
ScriptService,
SettingService,
HttpTriggerService,
UserService,
WorkspaceService
} from '$lib/gen'
@@ -187,8 +186,17 @@
}
async function loadUsedTriggerKinds() {
const httpUsed = await HttpTriggerService.used({ workspace: $workspaceStore ?? '' })
$usedTriggerKinds = httpUsed ? ['http'] : []
let usedKinds: string[] = []
const { http_routes_used, websocket_used } = await WorkspaceService.getUsedTriggers({
workspace: $workspaceStore ?? ''
})
if (http_routes_used) {
usedKinds.push('http')
}
if (websocket_used) {
usedKinds.push('ws')
}
$usedTriggerKinds = usedKinds
}
function pathInAppMode(pathname: string | undefined): boolean {
@@ -15,6 +15,7 @@
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import type { ScheduleTrigger } from '$lib/components/triggers'
let version: undefined | number = undefined;
let nodraft = $page.url.searchParams.get('nodraft')
const initialState = nodraft ? undefined : localStorage.getItem(`flow-${$page.params.path}`)
let stateLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
@@ -65,6 +66,13 @@
let flow: Flow
let statePath = stateLoadedFromUrl?.path
if (stateLoadedFromUrl != undefined && statePath == $page.params.path) {
// Currently there is no way to get version of flow with flow.
// So we have to request it here
version = (await FlowService.getFlowLatestVersion({
workspace: $workspaceStore!,
path: statePath
})).id;
savedFlow = await FlowService.getFlowByPathWithDraft({
workspace: $workspaceStore!,
path: statePath
@@ -103,6 +111,13 @@
])
}
} else {
// Currently there is no way to get version of flow with flow.
// So we have to request it here
version = (await FlowService.getFlowLatestVersion({
workspace: $workspaceStore!,
path: $page.params.path
})).id;
const flowWithDraft = await FlowService.getFlowByPathWithDraft({
workspace: $workspaceStore!,
path: $page.params.path
@@ -230,6 +245,7 @@
bind:savedFlow
{diffDrawer}
{savedPrimarySchedule}
bind:version
>
<UnsavedConfirmationModal {diffDrawer} savedValue={savedFlow} modifiedValue={$flowStore} />
</FlowBuilder>
@@ -60,6 +60,7 @@
import json from 'svelte-highlight/languages/json'
import { writable } from 'svelte/store'
import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte'
import WebsocketTriggersPanel from '$lib/components/triggers/WebsocketTriggersPanel.svelte'
let flow: Flow | undefined
let can_write = false
@@ -473,6 +474,7 @@
</div>
<div class="mt-8">
<FlowGraphViewer
triggerNode={true}
download
{flow}
overflowAuto
@@ -532,6 +534,11 @@
<RoutesPanel path={flow.path ?? ''} isFlow />
</div>
</svelte:fragment>
<svelte:fragment slot="websockets">
<div class="p-2">
<WebsocketTriggersPanel path={flow.path ?? ''} isFlow />
</div>
</svelte:fragment>
<svelte:fragment slot="emails">
<div class="p-2">
<EmailTriggerPanel
@@ -54,7 +54,7 @@
const TRIGGER_PATH_KIND_FILTER_SETTING = 'filter_path_of'
const FILTER_USER_FOLDER_SETTING_NAME = 'user_and_folders_only'
let selectedFilterKind =
(getLocalSetting(TRIGGER_PATH_KIND_FILTER_SETTING) as 'route' | 'script_flow') ?? 'route'
(getLocalSetting(TRIGGER_PATH_KIND_FILTER_SETTING) as 'trigger' | 'script_flow') ?? 'trigger'
let filterUserFolders = getLocalSetting(FILTER_USER_FOLDER_SETTING_NAME) == 'true'
$: storeLocalSetting(TRIGGER_PATH_KIND_FILTER_SETTING, selectedFilterKind)
@@ -62,12 +62,12 @@
function filterItemsPathsBaseOnUserFilters(
item: TriggerW,
selectedFilterKind: 'route' | 'script_flow',
selectedFilterKind: 'trigger' | 'script_flow',
filterUserFolders: boolean
) {
if ($workspaceStore == 'admins') return true
if (filterUserFolders) {
if (selectedFilterKind === 'route') {
if (selectedFilterKind === 'trigger') {
return (
!item.path.startsWith('u/') || item.path.startsWith('u/' + $userStore?.username + '/')
)
@@ -84,15 +84,15 @@
$: preFilteredItems =
ownerFilter != undefined
? selectedFilterKind === 'route'
? selectedFilterKind === 'trigger'
? triggers?.filter(
(x) =>
x.path.startsWith(ownerFilter + '/' ?? '') &&
x.path.startsWith(ownerFilter + '/') &&
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders)
)
: triggers?.filter(
(x) =>
x.script_path.startsWith(ownerFilter + '/' ?? '') &&
x.script_path.startsWith(ownerFilter + '/') &&
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders)
)
: triggers?.filter((x) =>
@@ -104,7 +104,7 @@
}
$: owners =
selectedFilterKind === 'route'
selectedFilterKind === 'trigger'
? Array.from(
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort()
@@ -133,7 +133,7 @@
let queryFilterKind = url.searchParams.get(TRIGGER_PATH_KIND_FILTER_SETTING)
let queryFilterUserFolders = url.searchParams.get(FILTER_USER_FOLDER_SETTING_NAME)
if (queryFilterKind) {
selectedFilterKind = queryFilterKind as 'route' | 'script_flow'
selectedFilterKind = queryFilterKind as 'trigger' | 'script_flow'
}
if (queryFilterUserFolders) {
filterUserFolders = queryFilterUserFolders == 'true'
@@ -174,7 +174,7 @@
<div class="flex flex-row items-center gap-2 mt-6">
<div class="text-sm shrink-0"> Filter by path of </div>
<ToggleButtonGroup bind:selected={selectedFilterKind}>
<ToggleButton small value="route" label="Route" icon={Route} />
<ToggleButton small value="trigger" label="Route" icon={Route} />
<ToggleButton small value="script_flow" label="Script/Flow" icon={Code} />
</ToggleButtonGroup>
</div>
@@ -172,13 +172,13 @@
? selectedFilterKind === 'schedule'
? schedules?.filter(
(x) =>
x.path.startsWith(ownerFilter + '/' ?? '') &&
x.path.startsWith(ownerFilter + '/') &&
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders) &&
filterItemsBasedOnEnabledDisabled(x, filterEnabledDisabled)
)
: schedules?.filter(
(x) =>
x.script_path.startsWith(ownerFilter + '/' ?? '') &&
x.script_path.startsWith(ownerFilter + '/') &&
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders) &&
filterItemsBasedOnEnabledDisabled(x, filterEnabledDisabled)
)
@@ -21,7 +21,6 @@
import { enterpriseLicense, hubBaseUrlStore, userStore, workspaceStore } from '$lib/stores'
import { isDeployable, ALL_DEPLOYABLE } from '$lib/utils_deployable'
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
import { onDestroy } from 'svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import {
@@ -84,6 +83,7 @@
import json from 'svelte-highlight/languages/json'
import { writable } from 'svelte/store'
import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte'
import WebsocketTriggersPanel from '$lib/components/triggers/WebsocketTriggersPanel.svelte'
let script: Script | undefined
let topHash: string | undefined
@@ -685,17 +685,6 @@
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="schema">
<div class="p-1 relative">
<button
on:click={() => copyToClipboard(JSON.stringify(script?.schema, null, 4))}
class="absolute top-2 right-2"
>
<ClipboardCopy size={14} />
</button>
<Highlight language={json} code={JSON.stringify(script?.schema, null, 4)} />
</div>
</svelte:fragment>
<svelte:fragment slot="save_inputs">
{#if args}
<SavedInputs
@@ -727,6 +716,11 @@
<RoutesPanel path={script.path ?? ''} isFlow={false} />
</div>
</svelte:fragment>
<svelte:fragment slot="websockets">
<div class="p-2">
<WebsocketTriggersPanel path={script.path ?? ''} isFlow={false} />
</div>
</svelte:fragment>
<svelte:fragment slot="emails">
<div class="p-2">
<EmailTriggerPanel
@@ -747,26 +741,14 @@
/>
</div>
</svelte:fragment>
<svelte:fragment slot="details">
<svelte:fragment slot="script">
<div>
<Skeleton {loading} layout={[[20]]} />
<Tabs selected="code">
<Tab value="code" size="xs">Code</Tab>
<Tab value="dependencies" size="xs">Lockfile</Tab>
<Tab value="arguments" size="xs">
<span class="inline-flex items-center gap-1">
Inputs
<Tooltip>
The jsonschema defines the constraints that the payload must respect to be
compatible with the input parameters of this script. The UI form is generated
automatically from the script jsonschema. See
<a href="https://json-schema.org/" class="text-blue-500">
jsonschema documentation
</a>
</Tooltip>
</span>
</Tab>
<Tab value="schema" size="xs">Schema</Tab>
<svelte:fragment slot="content">
<TabContent value="code">
<div class="p-2 w-full overflow-auto">
@@ -802,9 +784,15 @@
{/if}
</div>
</TabContent>
<TabContent value="arguments">
<div class="p-2">
<SchemaViewer schema={script.schema} />
<TabContent value="schema">
<div class="p-1 relative">
<button
on:click={() => copyToClipboard(JSON.stringify(script?.schema, null, 4))}
class="absolute top-2 right-2"
>
<ClipboardCopy size={14} />
</button>
<Highlight language={json} code={JSON.stringify(script?.schema, null, 4)} />
</div>
</TabContent>
</svelte:fragment>
@@ -0,0 +1,5 @@
export function load() {
return {
stuff: { title: 'WS triggers' }
}
}
@@ -0,0 +1,422 @@
<script lang="ts">
import { WebsocketTriggerService, type WebsocketTrigger } from '$lib/gen'
import {
canWrite,
displayDate,
getLocalSetting,
sendUserToast,
storeLocalSetting
} from '$lib/utils'
import { base } from '$app/paths'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Button, Skeleton } from '$lib/components/common'
import Dropdown from '$lib/components/DropdownV2.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import ShareModal from '$lib/components/ShareModal.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { Unplug, Code, Eye, Pen, Plus, Share, Trash, Circle } from 'lucide-svelte'
import { goto } from '$lib/navigation'
import SearchItems from '$lib/components/SearchItems.svelte'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import ListFilters from '$lib/components/home/ListFilters.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { setQuery } from '$lib/navigation'
import { onDestroy, onMount } from 'svelte'
import WebsocketTriggerEditor from '$lib/components/triggers/WebsocketTriggerEditor.svelte'
import Popover from '$lib/components/Popover.svelte'
import { isCloudHosted } from '$lib/cloud'
type TriggerW = WebsocketTrigger & { canWrite: boolean }
let triggers: TriggerW[] = []
let shareModal: ShareModal
let loading = true
async function loadTriggers(): Promise<void> {
triggers = (
await WebsocketTriggerService.listWebsocketTriggers({ workspace: $workspaceStore! })
).map((x) => {
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
})
loading = false
}
let status: {
[path: string]: { error: string | undefined; last_server_ping: string | undefined }
} = {}
let interval = setInterval(async () => {
try {
status = (
await WebsocketTriggerService.listWebsocketTriggers({
workspace: $workspaceStore!
})
).reduce((acc, x) => {
acc[x.path] = x
return acc
}, {})
} catch (err) {
console.error(err)
}
}, 5000)
onDestroy(() => {
clearInterval(interval)
})
async function setTriggerEnabled(path: string, enabled: boolean): Promise<void> {
try {
await WebsocketTriggerService.setWebsocketTriggerEnabled({
path,
workspace: $workspaceStore!,
requestBody: { enabled }
})
} catch (err) {
sendUserToast(
`Cannot ` + (enabled ? 'enable' : 'disable') + ` websocket trigger: ${err.body}`,
true
)
} finally {
loadTriggers()
}
}
$: {
if ($workspaceStore && $userStore) {
loadTriggers()
}
}
let websocketTriggerEditor: WebsocketTriggerEditor
let filteredItems: (TriggerW & { marked?: any })[] | undefined = []
let items: typeof filteredItems | undefined = []
let preFilteredItems: typeof filteredItems | undefined = []
let filter = ''
let ownerFilter: string | undefined = undefined
let nbDisplayed = 15
const TRIGGER_PATH_KIND_FILTER_SETTING = 'filter_path_of'
const FILTER_USER_FOLDER_SETTING_NAME = 'user_and_folders_only'
let selectedFilterKind =
(getLocalSetting(TRIGGER_PATH_KIND_FILTER_SETTING) as 'trigger' | 'script_flow') ?? 'trigger'
let filterUserFolders = getLocalSetting(FILTER_USER_FOLDER_SETTING_NAME) == 'true'
$: storeLocalSetting(TRIGGER_PATH_KIND_FILTER_SETTING, selectedFilterKind)
$: storeLocalSetting(FILTER_USER_FOLDER_SETTING_NAME, filterUserFolders ? 'true' : undefined)
function filterItemsPathsBaseOnUserFilters(
item: TriggerW,
selectedFilterKind: 'trigger' | 'script_flow',
filterUserFolders: boolean
) {
if ($workspaceStore == 'admins') return true
if (filterUserFolders) {
if (selectedFilterKind === 'trigger') {
return (
!item.path.startsWith('u/') || item.path.startsWith('u/' + $userStore?.username + '/')
)
} else {
return (
!item.script_path.startsWith('u/') ||
item.script_path.startsWith('u/' + $userStore?.username + '/')
)
}
} else {
return true
}
}
$: preFilteredItems =
ownerFilter != undefined
? selectedFilterKind === 'trigger'
? triggers?.filter(
(x) =>
x.path.startsWith(ownerFilter + '/') &&
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders)
)
: triggers?.filter(
(x) =>
x.script_path.startsWith(ownerFilter + '/') &&
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders)
)
: triggers?.filter((x) =>
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders)
)
$: if ($workspaceStore) {
ownerFilter = undefined
}
$: owners =
selectedFilterKind === 'trigger'
? Array.from(
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort()
: Array.from(
new Set(filteredItems?.map((x) => x.script_path.split('/').slice(0, 2).join('/')) ?? [])
).sort()
$: items = filter !== '' ? filteredItems : preFilteredItems
function updateQueryFilters(selectedFilterKind, filterUserFolders) {
setQuery(
new URL(window.location.href),
TRIGGER_PATH_KIND_FILTER_SETTING,
selectedFilterKind
).then(() => {
setQuery(
new URL(window.location.href),
FILTER_USER_FOLDER_SETTING_NAME,
String(filterUserFolders)
)
})
}
function loadQueryFilters() {
let url = new URL(window.location.href)
let queryFilterKind = url.searchParams.get(TRIGGER_PATH_KIND_FILTER_SETTING)
let queryFilterUserFolders = url.searchParams.get(FILTER_USER_FOLDER_SETTING_NAME)
if (queryFilterKind) {
selectedFilterKind = queryFilterKind as 'trigger' | 'script_flow'
}
if (queryFilterUserFolders) {
filterUserFolders = queryFilterUserFolders == 'true'
}
}
onMount(() => {
loadQueryFilters()
})
$: updateQueryFilters(selectedFilterKind, filterUserFolders)
</script>
<WebsocketTriggerEditor on:update={loadTriggers} bind:this={websocketTriggerEditor} />
<SearchItems
{filter}
items={preFilteredItems}
bind:filteredItems
f={(x) => (x.summary ?? '') + ' ' + x.path + ' (' + x.script_path + ')'}
/>
<CenteredPage>
<PageHeader
title="Websocket triggers"
tooltip="Windmill can listen to websocket events and trigger scripts or flows based on them."
>
<Button
size="md"
startIcon={{ icon: Plus }}
on:click={() => websocketTriggerEditor.openNew(false)}
>
New&nbsp;WS trigger
</Button>
</PageHeader>
{#if isCloudHosted()}
<Alert title="Not compatible with multi-tenant cloud" type="warning">
Websocket triggers are disabled in the multi-tenant cloud.
</Alert>
<div class="py-4" />
{/if}
<div class="w-full h-full flex flex-col">
<div class="w-full pb-4 pt-6">
<input type="text" placeholder="Search WS triggers" bind:value={filter} class="search-item" />
<div class="flex flex-row items-center gap-2 mt-6">
<div class="text-sm shrink-0"> Filter by path of </div>
<ToggleButtonGroup bind:selected={selectedFilterKind}>
<ToggleButton small value="trigger" label="WS Trigger" icon={Unplug} />
<ToggleButton small value="script_flow" label="Script/Flow" icon={Code} />
</ToggleButtonGroup>
</div>
<ListFilters syncQuery bind:selectedFilter={ownerFilter} filters={owners} />
<div class="flex flex-row items-center justify-end gap-4">
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
<Toggle
size="xs"
bind:checked={filterUserFolders}
options={{ right: `Only u/${$userStore.username} and f/*` }}
/>
{/if}
</div>
</div>
{#if loading}
{#each new Array(6) as _}
<Skeleton layout={[[6], 0.4]} />
{/each}
{:else if !triggers?.length}
<div class="text-center text-sm text-tertiary mt-2"> No websocket triggers </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, url, is_flow, extra_perms, canWrite, marked, error, last_server_ping, enabled } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const wsStatus = status[path] ?? { error, last_server_ping }}
<div
class="hover:bg-surface-hover w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0
first-of-type:rounded-t-md last-of-type:rounded-b-md flex flex-col"
>
<div class="w-full flex gap-5 items-center">
<RowIcon kind={is_flow ? 'flow' : 'script'} />
<a
href="#{path}"
on:click={() => websocketTriggerEditor?.openEdit(path, is_flow)}
class="min-w-0 grow hover:underline decoration-gray-400"
>
<div class="text-primary flex-wrap text-left text-md font-semibold mb-1 truncate">
{#if marked}
<span class="text-xs">
{@html marked}
</span>
{:else}
{url}
{/if}
</div>
<div class="text-secondary text-xs truncate text-left font-light">
{path}
</div>
<div class="text-secondary text-xs truncate text-left font-light">
runnable: {script_path}
</div>
</a>
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
</div>
<div class="w-10">
{#if enabled}
{@const ping = wsStatus.last_server_ping
? new Date(wsStatus.last_server_ping)
: undefined}
{#if !ping || ping.getTime() < new Date().getTime() - 15 * 1000 || wsStatus.error}
<Popover notClickable>
<span class="flex h-4 w-4">
<Circle
class="text-red-600 animate-ping absolute inline-flex fill-current"
size={12}
/>
<Circle class="text-red-600 relative inline-flex fill-current" size={12} />
</span>
<div slot="text">
Websocket is not connected{wsStatus.error ? ': ' + wsStatus.error : ''}
</div>
</Popover>
{:else}
<Popover notClickable>
<span class="flex h-4 w-4">
<Circle
class="text-green-600 relative inline-flex fill-current"
size={12}
/>
</span>
<div slot="text"> Websocket is connected </div>
</Popover>
{/if}
{/if}
</div>
<Toggle
checked={enabled}
disabled={!canWrite}
on:change={(e) => {
setTriggerEnabled(path, e.detail)
}}
/>
<div class="flex gap-2 items-center justify-end">
<Button
on:click={() => websocketTriggerEditor?.openEdit(path, is_flow)}
size="xs"
startIcon={canWrite
? { icon: Pen }
: {
icon: Eye
}}
color="dark"
>
{canWrite ? 'Edit' : 'View'}
</Button>
<Dropdown
items={[
{
displayName: `View ${is_flow ? 'Flow' : 'Script'}`,
icon: Eye,
action: () => {
goto(href)
}
},
{
displayName: 'Delete',
type: 'delete',
icon: Trash,
disabled: !canWrite,
action: async () => {
await WebsocketTriggerService.deleteWebsocketTrigger({
workspace: $workspaceStore ?? '',
path
})
loadTriggers()
}
},
{
displayName: canWrite ? 'Edit' : 'View',
icon: canWrite ? Pen : Eye,
action: () => {
websocketTriggerEditor?.openEdit(path, is_flow)
}
},
{
displayName: 'Audit logs',
icon: Eye,
href: `${base}/audit_logs?resource=${path}`
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: Share,
action: () => {
shareModal.openDrawer(path, 'websocket_trigger')
}
}
]}
/>
</div>
</div>
<div class="w-full flex justify-between items-baseline">
<div
class="flex flex-wrap text-[0.7em] text-tertiary gap-1 items-center justify-end truncate pr-2"
><div class="truncate">edited by {edited_by}</div><div class="truncate"
>the {displayDate(edited_at)}</div
></div
></div
>
</div>
{/each}
</div>
{:else}
<NoItemFound />
{/if}
</div>
{#if items && items?.length > 15 && nbDisplayed < items.length}
<span class="text-xs"
>{nbDisplayed} items out of {items.length}
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button></span
>
{/if}
</CenteredPage>
<ShareModal
bind:this={shareModal}
on:change={() => {
loadTriggers()
}}
/>
@@ -1,6 +1,5 @@
<script lang="ts">
import { goto } from '$lib/navigation'
import { replaceState } from '$app/navigation'
import { page } from '$app/stores'
import { isCloudHosted } from '$lib/cloud'
import CenteredPage from '$lib/components/CenteredPage.svelte'
@@ -37,7 +36,8 @@
X,
Plus,
Loader2,
Save
Save,
ExternalLink
} from 'lucide-svelte'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
@@ -140,30 +140,6 @@
let usingOpenaiClientCredentialsOauth = false
const latestGitSyncHubScript = hubPaths.gitSync
// function getDropDownItems(username: string): DropdownItem[] {
// return [
// {
// displayName: 'Manage user',
// href: `/admin/user/manage/${username}`
// },
// {
// displayName: 'Delete',
// action: () => deleteUser(username)
// }
// ];
// }
// async function deleteUser(username: string): Promise<void> {
// try {
// await UserService.deleteUser({ workspace: $workspaceStore!, username });
// users = await UserService.listUsers({ workspace: $workspaceStore! });
// fuse?.setCollection(users);
// sendUserToast(`User ${username} has been removed`);
// } catch (err) {
// console.error(err);
// sendUserToast(`Cannot delete user: ${err}`, true);
// }
// }
async function editSlackCommand(): Promise<void> {
initialPath = scriptPath
@@ -520,11 +496,7 @@
}
}
$: {
if ($workspaceStore) {
loadSettings()
}
}
$: $workspaceStore && loadSettings()
async function editErrorHandler() {
if (errorHandlerScriptPath) {
@@ -602,31 +574,6 @@
timeout: 5000
})
}
let debounced: NodeJS.Timeout | undefined = undefined
export function setQueryWithoutLoad(
url: URL,
args: { key: string; value: string | null | undefined }[],
bounceTime?: number
): void {
debounced && clearTimeout(debounced)
debounced = setTimeout(() => {
const nurl = new URL(url.toString())
for (const { key, value } of args) {
if (value) {
nurl.searchParams.set(key, value)
} else {
nurl.searchParams.delete(key)
}
}
try {
replaceState(nurl.toString(), $page.state)
} catch (e) {
console.error(e)
}
}, bounceTime ?? 200)
}
</script>
<Portal name="workspace-settings">
@@ -652,7 +599,9 @@
<Tabs
bind:selected={tab}
on:selected={() => {
setQueryWithoutLoad($page.url, [{ key: 'tab', value: tab }], 0)
// setQueryWithoutLoad($page.url, [{ key: 'tab', value: tab }], 0)
$page.url.searchParams.set('tab', tab)
goto(`?${$page.url.searchParams.toString()}`)
}}
>
<Tab size="xs" value="users">
@@ -933,6 +882,7 @@
Workspace error handler is a Windmill EE feature. It enables using your current Slack
connection or a custom script to send notifications anytime any job would fail.
</Alert>
<div class="pb-2" />
{/if}
<div class="flex flex-col gap-4 my-8">
<div class="flex flex-col gap-1">
@@ -1261,26 +1211,33 @@
<div class="mb-2" />
{/if}
{#if gitSyncSettings != undefined}
{#if $enterpriseLicense}
<div class="flex mt-5 mb-5 gap-1">
<Button
color="blue"
disabled={gitSyncSettings?.repositories?.some((elmt) =>
<div class="flex mt-5 mb-5 gap-8">
<Button
color="blue"
disabled={!$enterpriseLicense ||
gitSyncSettings?.repositories?.some((elmt) =>
emptyString(elmt.git_repo_resource_path)
)}
on:click={() => {
editWindmillGitSyncSettings()
console.log('Saving git sync settings', gitSyncSettings)
}}>Save Git sync settings</Button
>
</div>
{/if}
on:click={() => {
editWindmillGitSyncSettings()
console.log('Saving git sync settings', gitSyncSettings)
}}>Save git sync settings {!$enterpriseLicense ? '(ee only)' : ''}</Button
>
<Button
color="dark"
target="_blank"
endIcon={{ icon: ExternalLink }}
href={`/runs?job_kinds=deploymentcallbacks&workspace=${$workspaceStore}`}
>See sync jobs</Button
>
</div>
<div class="flex flex-wrap gap-20">
<div class="max-w-md w-full">
{#if Array.isArray(gitSyncSettings?.include_path)}
<h4 class="flex gap-2 mb-4"
>Filter on path<Tooltip>
>Path filters<Tooltip>
Only scripts, flows and apps with their path matching one of those filters will be
synced to the Git repositories below. The filters allow '*'' and '**' characters,
with '*'' matching any character allowed in paths until the next slash (/) and
@@ -1320,11 +1277,16 @@
Add filter
</Button>
</div>
<div class="pt-2" />
<Alert type="info" title="Only new updates trigger git sync">
Only new changes matching the filters will trigger a git sync. You still need to
initalize the repo to the desired state first.
</Alert>
</div>
<div class="max-w-md w-full">
<h4 class="flex gap-2 mb-4"
>Filter on type<Tooltip>
>Type filters<Tooltip>
On top of the filter path above, you can include only certain type of object to be
synced with the Git repository.
<br />By default everything is synced.
@@ -1645,13 +1607,15 @@
<br />
. For the git repo to be representative of the entire workspace, it is recommended to
set it up using the Windmill CLI before turning this option on.
For the git repo to be representative of the entire workspace, it is recommended to set
it up using the Windmill CLI before turning this option on.
<br /><br />
Not familiar with Windmill CLI?
<a href="https://www.windmill.dev/docs/advanced/cli">Check out the docs</a>
<a href="https://www.windmill.dev/docs/advanced/cli" class="text-primary"
>Check out the docs</a
>
<br /><br />
@@ -1662,9 +1626,11 @@
<pre class="overflow-auto max-h-screen"
><code
>wmill workspace add {$workspaceStore} {$workspaceStore} {`${$page.url.protocol}//${$page.url.hostname}/`}
echo 'includes: ["f/**"]' > wmill.yaml
wmill sync pull --raw --skip-variables --skip-secrets --skip-resources
>npm install -g windmill-cli
wmill workspace add {$workspaceStore} {$workspaceStore} {`${$page.url.protocol}//${$page.url.hostname}/`}
wmill init
# adjust wmill.yaml file configuraton as needed
wmill sync pull
git add -A
git commit -m 'Initial commit'
git push</code
@@ -287,6 +287,7 @@
<h2 class="mt-10">Flow details</h2>
<div class="border border-gray-700">
<FlowGraphV2
triggerNode={false}
modules={job.raw_flow?.modules}
failureModule={job.raw_flow?.failure_module}
preprocessorModule={job.raw_flow?.preprocessor_module}
+3 -3
View File
@@ -73,9 +73,9 @@
const selectedIdStore = writable('settings-metadata')
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const triggersCount = writable<TriggersCount | undefined>(undefined)
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
>('webhooks')
setContext<TriggerContext>('TriggerContext', {
primarySchedule: primaryScheduleStore,
selectedTrigger: selectedTriggerStore,
+1 -1
View File
@@ -8,7 +8,7 @@
: { modules: [], failureModule: undefined, preprocessorModule: undefined }
</script>
<FlowGraphV2 {modules} {failureModule} {preprocessorModule} />
<FlowGraphV2 triggerNode={false} {modules} {failureModule} {preprocessorModule} />
<a
download="flow.json"
href={'data:text/json;charset=utf-8,' +
+7 -6
View File
@@ -4,17 +4,18 @@ set -e
mkdir openapi
cp ../backend/windmill-api/openapi.yaml openapi/openapi.yaml
sed -i '/ enum:/ { N; / \[/ { N; /"S3Storage",/ { N; /"AzureBlobStorage",/ { N; /"AzureWorkloadIdentity",/ { N; /"S3AwsOidc",/ { N; / \]/d; }}}}}}' openapi/openapi.yaml
sed -z 's/ extra_params:\n additionalProperties:\n type: string/ extra_params: {}/' openapi/openapi.yaml > openapi/openapi1.yaml
sed -z 's/ enum: \[script, failure, trigger, command, approval\]//' openapi/openapi1.yaml > openapi/openapi2.yaml
sed -z 's/ enum: \[deno, python3, go, bash\]//' openapi/openapi2.yaml > openapi/openapi3.yaml
# sed -i '/ enum:/ { N; / \[/ { N; /"S3Storage",/ { N; /"AzureBlobStorage",/ { N; /"AzureWorkloadIdentity",/ { N; /"S3AwsOidc",/ { N; / \]/d; }}}}}}' openapi/openapi.yaml
# sed -z 's/ extra_params:\n additionalProperties:\n type: string/ extra_params: {}/' openapi/openapi.yaml > openapi/openapi3.yaml
# sed -z 's/ enum: \[script, failure, trigger, command, approval\]//' openapi/openapi1.yaml > openapi/openapi2.yaml
# sed -z 's/ enum: \[deno, python3, go, bash\]//' openapi/openapi2.yaml > openapi/openapi3.yaml
npx @redocly/openapi-cli@latest bundle openapi/openapi3.yaml --ext json > openapi-bundled.json
npx @redocly/openapi-cli@latest bundle openapi/openapi.yaml --ext json > openapi-bundled.json
cat openapi-bundled.json | jq 'del(.paths."/oauth/list_connects")' > openapi-bundled-cleaned.json
cat openapi-bundled.json | jq 'del(.paths."/oauth/list_connects")' | jq 'del(.components.schemas.Job.discriminator)' > openapi-bundled-cleaned.json
rm -rf api/ || true
mkdir -p api
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.4.1
~/go/bin/oapi-codegen -old-config-style --package=windmill_api --generate=types,client openapi-bundled-cleaned.json > api/windmill_api.gen.go
rm -rf openapi/
rm openapi*
+4 -67
View File
@@ -3,79 +3,16 @@ module github.com/windmill-labs/windmill-go-client
go 1.19
require (
github.com/deepmap/oapi-codegen v1.15.0
github.com/google/uuid v1.5.0
github.com/oapi-codegen/runtime v1.1.1
github.com/stretchr/testify v1.8.4
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
github.com/Joker/jade v1.1.3 // indirect
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bytedance/sonic v1.10.0-rc3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/flosch/pongo2/v4 v4.0.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/iris-contrib/schema v0.0.6 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kataras/blocks v0.0.7 // indirect
github.com/kataras/golog v0.1.9 // indirect
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 // indirect
github.com/kataras/pio v0.0.12 // indirect
github.com/kataras/sitemap v0.0.6 // indirect
github.com/kataras/tunnel v0.0.4 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/labstack/echo/v4 v4.11.1 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/tdewolff/minify/v2 v2.12.9 // indirect
github.com/tdewolff/parse/v2 v2.6.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/yosssi/ace v0.0.5 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+4 -221
View File
@@ -1,246 +1,29 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME=
github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4=
github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0=
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=
github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepmap/oapi-codegen v1.15.0 h1:SQqViaeb4k2vMul8gx12oDOIadEtoRqTdLkxjzqtQ90=
github.com/deepmap/oapi-codegen v1.15.0/go.mod h1:a6KoHV7lMRwsPoEg2C6NDHiXYV3EQfiFocOlJ8dgJQE=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E=
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go=
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4=
github.com/kataras/blocks v0.0.7/go.mod h1:UJIU97CluDo0f+zEjbnbkeMRlvYORtmc1304EeyXf4I=
github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk=
github.com/kataras/golog v0.1.9/go.mod h1:jlpk/bOaYCyqDqH18pgDHdaJab72yBE6i0O3s30hpWY=
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 h1:Vx8kDVhO2qepK8w44lBtp+RzN3ld743i+LYPzODJSpQ=
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9/go.mod h1:ldkoR3iXABBeqlTibQ3MYaviA1oSlPvim6f55biwBh4=
github.com/kataras/pio v0.0.12 h1:o52SfVYauS3J5X08fNjlGS5arXHjW/ItLkyLcKjoH6w=
github.com/kataras/pio v0.0.12/go.mod h1:ODK/8XBhhQ5WqrAhKy+9lTPS7sBf6O3KcLhc9klfRcY=
github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY=
github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4=
github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA=
github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tdewolff/minify/v2 v2.12.9 h1:dvn5MtmuQ/DFMwqf5j8QhEVpPX6fi3WGImhv8RUB4zA=
github.com/tdewolff/minify/v2 v2.12.9/go.mod h1:qOqdlDfL+7v0/fyymB+OP497nIxJYSvX4MQWA8OoiXU=
github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvFMA=
github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0=
github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
-1
View File
@@ -8,7 +8,6 @@ import (
"math/rand"
"net/http"
"os"
"github.com/google/uuid"
api "github.com/windmill-labs/windmill-go-client/api"
)
+1 -1
View File
@@ -26,7 +26,7 @@ ENV GOBIN=/usr/local/go/bin
RUN /usr/local/go/bin/go install -v golang.org/x/tools/gopls@latest
RUN pip3 install tornado python-lsp-jsonrpc ruff-lsp
COPY --from=denoland/deno:2.0.0 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=denoland/deno:2.0.2 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY Pipfile .
+2 -2
View File
@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.409.2"
wmill_pg = ">=1.409.2"
wmill = ">=1.411.1"
wmill_pg = ">=1.411.1"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.409.2
version: 1.411.1
title: OpenFlow Spec
contact:
name: Ruben Fiszel

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