From 48e4ae6561bfac725bc9ddee2697b44af48ad3a8 Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Thu, 6 Feb 2025 17:38:12 +0100 Subject: [PATCH] feat: serve static websites (#5218) * feat: serve static websites * nit * sqlx * nit * nits * nits * nits * nits --- ...4dac7f747585d98aa8bcf60dcfb4c8124028.json} | 13 +- ...4f58c1b3edfcc1e863ec4316c9742a1ac7c8.json} | 12 +- ...9ebb6ec02aa083d7e175c1a02a38935a4024.json} | 7 +- ...297af93b5b83ffdfb3ec91eba5df7ad3fd95.json} | 5 +- ...f4c7b2c91094036d9b5037ef9a4ac986ee53.json} | 13 +- ...b5c555c1187b276f1353dae1b2ac0670a92a.json} | 7 +- ...662539d5e4af1dd0b5923d8ee17b772c5677.json} | 7 +- ...2996dede4de49c5bd9cca2928209dc40b8f1.json} | 5 +- ...250205131513_http_static_websites.down.sql | 11 + ...20250205131513_http_static_websites.up.sql | 12 + backend/windmill-api/openapi.yaml | 12 +- backend/windmill-api/src/http_triggers.rs | 237 ++++++++---- backend/windmill-api/src/tracing_init.rs | 2 +- .../src/lib/components/S3FilePicker.svelte | 10 +- .../common/fileInput/FileInput.svelte | 97 +++-- .../common/fileUpload/FileUpload.svelte | 61 +++- .../components/triggers/CaptureWrapper.svelte | 2 - .../triggers/TriggersWrapper.svelte | 2 - .../http/RouteEditorConfigSection.svelte | 28 +- .../triggers/http/RouteEditorInner.svelte | 270 ++++++++------ .../(root)/(logged)/routes/+page.svelte | 343 +++++++++--------- 21 files changed, 731 insertions(+), 425 deletions(-) rename backend/.sqlx/{query-a76be7f4e9e8b8c81afe50fcbf1f3d393ed0bb73314e5c430f32541c43bcac52.json => query-1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028.json} (84%) rename backend/.sqlx/{query-f904702536c106b0e5da8facae119c6af887c49a29ae44b3a95350ff27fb1ccf.json => query-197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8.json} (86%) rename backend/.sqlx/{query-16c4fb122ceb30722f54c92db97427b0ddd36f495d549f8cd23d659d2c866a63.json => query-63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024.json} (77%) rename backend/.sqlx/{query-7113d7cc72e44e4b7e01b69cc18cbe7b0399cf8ec0e9e6d2b05ceef589c432df.json => query-89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json} (80%) rename backend/.sqlx/{query-28a389a93a3d2472b13d956ec55eb357c6147e186e00e371f7374d166903ef64.json => query-8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53.json} (82%) rename backend/.sqlx/{query-867e0f8299e22a6b8e02d21adeb0281b1a10593715db823a5adeeac3ab1058cd.json => query-a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a.json} (75%) rename backend/.sqlx/{query-333b484ffa030dee08e7b1161fcbc48af411377d2d9f58f92fc9d5eacdf0fba1.json => query-b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677.json} (79%) rename backend/.sqlx/{query-487d377e2df67fc3ea39d183ba9f99d45828d7c8e0ff10c5d74c454472e0493c.json => query-ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1.json} (82%) create mode 100644 backend/migrations/20250205131513_http_static_websites.down.sql create mode 100644 backend/migrations/20250205131513_http_static_websites.up.sql diff --git a/backend/.sqlx/query-a76be7f4e9e8b8c81afe50fcbf1f3d393ed0bb73314e5c430f32541c43bcac52.json b/backend/.sqlx/query-1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028.json similarity index 84% rename from backend/.sqlx/query-a76be7f4e9e8b8c81afe50fcbf1f3d393ed0bb73314e5c430f32541c43bcac52.json rename to backend/.sqlx/query-1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028.json index fb1cea135a..418be57757 100644 --- a/backend/.sqlx/query-a76be7f4e9e8b8c81afe50fcbf1f3d393ed0bb73314e5c430f32541c43bcac52.json +++ b/backend/.sqlx/query-1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as \"static_asset_config: _\" FROM http_trigger WHERE workspace_id = $1 AND http_method = $2", + "query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger WHERE http_method = $1", "describe": { "columns": [ { @@ -52,11 +52,15 @@ "ordinal": 9, "name": "static_asset_config: _", "type_info": "Jsonb" + }, + { + "ordinal": 10, + "name": "is_static_website", + "type_info": "Bool" } ], "parameters": { "Left": [ - "Text", { "Custom": { "name": "http_method", @@ -83,8 +87,9 @@ false, false, false, - true + true, + false ] }, - "hash": "a76be7f4e9e8b8c81afe50fcbf1f3d393ed0bb73314e5c430f32541c43bcac52" + "hash": "1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028" } diff --git a/backend/.sqlx/query-f904702536c106b0e5da8facae119c6af887c49a29ae44b3a95350ff27fb1ccf.json b/backend/.sqlx/query-197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8.json similarity index 86% rename from backend/.sqlx/query-f904702536c106b0e5da8facae119c6af887c49a29ae44b3a95350ff27fb1ccf.json rename to backend/.sqlx/query-197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8.json index 08592a3f04..bfc823c911 100644 --- a/backend/.sqlx/query-f904702536c106b0e5da8facae119c6af887c49a29ae44b3a95350ff27fb1ccf.json +++ b/backend/.sqlx/query-197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as \"static_asset_config: _\"\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2", + "query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as \"static_asset_config: _\", is_static_website\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2", "describe": { "columns": [ { @@ -85,6 +85,11 @@ "ordinal": 13, "name": "static_asset_config: _", "type_info": "Jsonb" + }, + { + "ordinal": 14, + "name": "is_static_website", + "type_info": "Bool" } ], "parameters": { @@ -107,8 +112,9 @@ false, false, false, - true + true, + false ] }, - "hash": "f904702536c106b0e5da8facae119c6af887c49a29ae44b3a95350ff27fb1ccf" + "hash": "197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8" } diff --git a/backend/.sqlx/query-16c4fb122ceb30722f54c92db97427b0ddd36f495d549f8cd23d659d2c866a63.json b/backend/.sqlx/query-63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024.json similarity index 77% rename from backend/.sqlx/query-16c4fb122ceb30722f54c92db97427b0ddd36f495d549f8cd23d659d2c866a63.json rename to backend/.sqlx/query-63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024.json index 00361e6e95..4d914355db 100644 --- a/backend/.sqlx/query-16c4fb122ceb30722f54c92db97427b0ddd36f495d549f8cd23d659d2c866a63.json +++ b/backend/.sqlx/query-63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2)", + "query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2 AND ($3::TEXT IS NULL OR path != $3))", "describe": { "columns": [ { @@ -25,12 +25,13 @@ ] } } - } + }, + "Text" ] }, "nullable": [ null ] }, - "hash": "16c4fb122ceb30722f54c92db97427b0ddd36f495d549f8cd23d659d2c866a63" + "hash": "63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024" } diff --git a/backend/.sqlx/query-7113d7cc72e44e4b7e01b69cc18cbe7b0399cf8ec0e9e6d2b05ceef589c432df.json b/backend/.sqlx/query-89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json similarity index 80% rename from backend/.sqlx/query-7113d7cc72e44e4b7e01b69cc18cbe7b0399cf8ec0e9e6d2b05ceef589c432df.json rename to backend/.sqlx/query-89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json index da3503cf35..a463eafdb7 100644 --- a/backend/.sqlx/query-7113d7cc72e44e4b7e01b69cc18cbe7b0399cf8ec0e9e6d2b05ceef589c432df.json +++ b/backend/.sqlx/query-89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now() \n WHERE workspace_id = $10 AND path = $11", + "query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now(), is_static_website = $10\n WHERE workspace_id = $11 AND path = $12", "describe": { "columns": [], "parameters": { @@ -27,11 +27,12 @@ "Varchar", "Bool", "Bool", + "Bool", "Text", "Text" ] }, "nullable": [] }, - "hash": "7113d7cc72e44e4b7e01b69cc18cbe7b0399cf8ec0e9e6d2b05ceef589c432df" + "hash": "89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95" } diff --git a/backend/.sqlx/query-28a389a93a3d2472b13d956ec55eb357c6147e186e00e371f7374d166903ef64.json b/backend/.sqlx/query-8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53.json similarity index 82% rename from backend/.sqlx/query-28a389a93a3d2472b13d956ec55eb357c6147e186e00e371f7374d166903ef64.json rename to backend/.sqlx/query-8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53.json index a8a68c9237..de933d99e1 100644 --- a/backend/.sqlx/query-28a389a93a3d2472b13d956ec55eb357c6147e186e00e371f7374d166903ef64.json +++ b/backend/.sqlx/query-8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as \"static_asset_config: _\" FROM http_trigger WHERE http_method = $1", + "query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger WHERE workspace_id = $1 AND http_method = $2", "describe": { "columns": [ { @@ -52,10 +52,16 @@ "ordinal": 9, "name": "static_asset_config: _", "type_info": "Jsonb" + }, + { + "ordinal": 10, + "name": "is_static_website", + "type_info": "Bool" } ], "parameters": { "Left": [ + "Text", { "Custom": { "name": "http_method", @@ -82,8 +88,9 @@ false, false, false, - true + true, + false ] }, - "hash": "28a389a93a3d2472b13d956ec55eb357c6147e186e00e371f7374d166903ef64" + "hash": "8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53" } diff --git a/backend/.sqlx/query-867e0f8299e22a6b8e02d21adeb0281b1a10593715db823a5adeeac3ab1058cd.json b/backend/.sqlx/query-a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a.json similarity index 75% rename from backend/.sqlx/query-867e0f8299e22a6b8e02d21adeb0281b1a10593715db823a5adeeac3ab1058cd.json rename to backend/.sqlx/query-a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a.json index ec6143fe39..060d87a941 100644 --- a/backend/.sqlx/query-867e0f8299e22a6b8e02d21adeb0281b1a10593715db823a5adeeac3ab1058cd.json +++ b/backend/.sqlx/query-a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND workspace_id = $2 AND http_method = $3)", + "query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND workspace_id = $2 AND http_method = $3 AND ($4::TEXT IS NULL OR path != $4))", "describe": { "columns": [ { @@ -26,12 +26,13 @@ ] } } - } + }, + "Text" ] }, "nullable": [ null ] }, - "hash": "867e0f8299e22a6b8e02d21adeb0281b1a10593715db823a5adeeac3ab1058cd" + "hash": "a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a" } diff --git a/backend/.sqlx/query-333b484ffa030dee08e7b1161fcbc48af411377d2d9f58f92fc9d5eacdf0fba1.json b/backend/.sqlx/query-b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677.json similarity index 79% rename from backend/.sqlx/query-333b484ffa030dee08e7b1161fcbc48af411377d2d9f58f92fc9d5eacdf0fba1.json rename to backend/.sqlx/query-b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677.json index bb21c2a9db..8475175c69 100644 --- a/backend/.sqlx/query-333b484ffa030dee08e7b1161fcbc48af411377d2d9f58f92fc9d5eacdf0fba1.json +++ b/backend/.sqlx/query-b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now())", + "query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at, is_static_website) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13)", "describe": { "columns": [], "parameters": { @@ -29,10 +29,11 @@ }, "Jsonb", "Varchar", - "Varchar" + "Varchar", + "Bool" ] }, "nullable": [] }, - "hash": "333b484ffa030dee08e7b1161fcbc48af411377d2d9f58f92fc9d5eacdf0fba1" + "hash": "b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677" } diff --git a/backend/.sqlx/query-487d377e2df67fc3ea39d183ba9f99d45828d7c8e0ff10c5d74c454472e0493c.json b/backend/.sqlx/query-ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1.json similarity index 82% rename from backend/.sqlx/query-487d377e2df67fc3ea39d183ba9f99d45828d7c8e0ff10c5d74c454472e0493c.json rename to backend/.sqlx/query-ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1.json index 7cd37b4c50..f4bc3f2174 100644 --- a/backend/.sqlx/query-487d377e2df67fc3ea39d183ba9f99d45828d7c8e0ff10c5d74c454472e0493c.json +++ b/backend/.sqlx/query-ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now() \n WHERE workspace_id = $12 AND path = $13", + "query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now(), is_static_website = $12\n WHERE workspace_id = $13 AND path = $14", "describe": { "columns": [], "parameters": { @@ -29,11 +29,12 @@ "Varchar", "Bool", "Bool", + "Bool", "Text", "Text" ] }, "nullable": [] }, - "hash": "487d377e2df67fc3ea39d183ba9f99d45828d7c8e0ff10c5d74c454472e0493c" + "hash": "ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1" } diff --git a/backend/migrations/20250205131513_http_static_websites.down.sql b/backend/migrations/20250205131513_http_static_websites.down.sql new file mode 100644 index 0000000000..72a7ae8a15 --- /dev/null +++ b/backend/migrations/20250205131513_http_static_websites.down.sql @@ -0,0 +1,11 @@ +ALTER TABLE http_trigger DROP COLUMN is_static_website; + +CREATE OR REPLACE FUNCTION prevent_route_path_change() +RETURNS TRIGGER AS $$ +BEGIN + IF CURRENT_USER <> 'windmill_admin' AND NEW.route_path <> OLD.route_path THEN + RAISE EXCEPTION 'Modification of route_path is only allowed by admins'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/backend/migrations/20250205131513_http_static_websites.up.sql b/backend/migrations/20250205131513_http_static_websites.up.sql new file mode 100644 index 0000000000..e4edc0255d --- /dev/null +++ b/backend/migrations/20250205131513_http_static_websites.up.sql @@ -0,0 +1,12 @@ +ALTER TABLE http_trigger ADD COLUMN is_static_website BOOLEAN NOT NULL DEFAULT FALSE; + + +CREATE OR REPLACE FUNCTION prevent_route_path_change() +RETURNS TRIGGER AS $$ +BEGIN + IF CURRENT_USER = 'windmill_user' AND NEW.route_path <> OLD.route_path THEN + RAISE EXCEPTION 'Modification of route_path is only allowed by admins'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0b865086c8..df32bd33ae 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -7781,8 +7781,9 @@ paths: http_method: type: string enum: ["get", "post", "put", "delete", "patch"] + trigger_path: + type: string required: - - kind - route_path - http_method responses: @@ -12998,6 +12999,8 @@ components: type: boolean requires_auth: type: boolean + is_static_website: + type: boolean required: - path @@ -13012,6 +13015,7 @@ components: - is_async - requires_auth - http_method + - is_static_website NewHttpTrigger: type: object @@ -13047,6 +13051,8 @@ components: type: boolean requires_auth: type: boolean + is_static_website: + type: boolean required: - path @@ -13056,6 +13062,7 @@ components: - is_async - requires_auth - http_method + - is_static_website EditHttpTrigger: type: object @@ -13091,6 +13098,8 @@ components: type: boolean requires_auth: type: boolean + is_static_website: + type: boolean required: - path - script_path @@ -13099,6 +13108,7 @@ components: - is_async - requires_auth - http_method + - is_static_website TriggersCount: type: object diff --git a/backend/windmill-api/src/http_triggers.rs b/backend/windmill-api/src/http_triggers.rs index d0a0b43698..68e21d17ed 100644 --- a/backend/windmill-api/src/http_triggers.rs +++ b/backend/windmill-api/src/http_triggers.rs @@ -36,7 +36,8 @@ use windmill_common::{ }; lazy_static::lazy_static! { - static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/:\w+").unwrap(); + static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/?:[-\w]+").unwrap(); + static ref VALID_ROUTE_PATH_RE: regex::Regex = regex::Regex::new(r"^:?[-\w]+(/:?[-\w]+)*$").unwrap(); } pub fn routes_global_service() -> Router { @@ -109,10 +110,11 @@ struct NewTrigger { requires_auth: bool, http_method: HttpMethod, static_asset_config: Option>, + is_static_website: bool, } #[derive(FromRow, Serialize)] -struct Trigger { +struct HttpTrigger { workspace_id: String, path: String, route_path: String, @@ -127,6 +129,7 @@ struct Trigger { requires_auth: bool, http_method: HttpMethod, static_asset_config: Option>, + is_static_website: bool, } #[derive(Deserialize)] @@ -139,6 +142,7 @@ struct EditTrigger { requires_auth: bool, http_method: HttpMethod, static_asset_config: Option>, + is_static_website: bool, } #[derive(Deserialize)] @@ -155,7 +159,7 @@ async fn list_triggers( Extension(user_db): Extension, Path(w_id): Path, Query(lst): Query, -) -> error::JsonResult> { +) -> error::JsonResult> { 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("http_trigger") @@ -177,7 +181,7 @@ async fn list_triggers( let sql = sqlb .sql() .map_err(|e| error::Error::internal_err(e.to_string()))?; - let rows = sqlx::query_as::<_, Trigger>(&sql) + let rows = sqlx::query_as::<_, HttpTrigger>(&sql) .fetch_all(&mut *tx) .await?; tx.commit().await?; @@ -189,12 +193,12 @@ async fn get_trigger( authed: ApiAuthed, Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, -) -> error::JsonResult { +) -> error::JsonResult { let mut tx = user_db.begin(&authed).await?; let path = path.to_path(); let trigger = sqlx::query_as!( - Trigger, - r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as "static_asset_config: _" + HttpTrigger, + r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as "static_asset_config: _", is_static_website FROM http_trigger WHERE workspace_id = $1 AND path = $2"#, w_id, @@ -211,17 +215,38 @@ async fn get_trigger( async fn create_trigger( authed: ApiAuthed, + Extension(db): Extension, Extension(user_db): Extension, Path(w_id): Path, Json(ct): Json, ) -> error::Result<(StatusCode, String)> { require_admin(authed.is_admin, &authed.username)?; + if !VALID_ROUTE_PATH_RE.is_match(&ct.route_path) { + return Err(error::Error::BadRequest("Invalid route path".to_string())); + } + + // route path key is extracted from the route path to check for uniqueness + // it replaces /?:{key} with :key + // it will also remove the leading / if present, not an issue as we only allow : after slashes let route_path_key = ROUTE_PATH_KEY_RE.replace_all(ct.route_path.as_str(), ":key"); + let exists = route_path_key_exists(&route_path_key, &ct.http_method, &w_id, None, &db).await?; + if exists { + return Err(error::Error::BadRequest( + "A route already exists with this path".to_string(), + )); + } + + if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) { + return Err(error::Error::BadRequest( + "Static website and static asset are not supported on cloud".to_string(), + )); + } + let mut tx = user_db.begin(&authed).await?; sqlx::query!( - "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now())", + "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at, is_static_website) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13)", w_id, ct.path, ct.route_path, @@ -233,7 +258,8 @@ async fn create_trigger( ct.http_method as _, ct.static_asset_config as _, &authed.username, - &authed.email + &authed.email, + ct.is_static_website, ) .execute(&mut *tx).await?; @@ -256,27 +282,48 @@ async fn create_trigger( async fn update_trigger( authed: ApiAuthed, Extension(user_db): Extension, + Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ct): Json, ) -> error::Result { let path = path.to_path(); - let mut tx = user_db.begin(&authed).await?; + if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) { + return Err(error::Error::BadRequest( + "Static website and static asset are not supported on cloud".to_string(), + )); + } + + let mut tx; if authed.is_admin { - if ct.route_path.is_none() { + let Some(route_path) = ct.route_path else { return Err(error::Error::BadRequest( "route_path is required".to_string(), )); + }; + + if !VALID_ROUTE_PATH_RE.is_match(&route_path) { + return Err(error::Error::BadRequest("Invalid route path".to_string())); } - let route_path_key = - ROUTE_PATH_KEY_RE.replace_all(ct.route_path.as_ref().unwrap().as_str(), ":key"); + let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&route_path, ":key"); + + let exists = + route_path_key_exists(&route_path_key, &ct.http_method, &w_id, Some(&path), &db) + .await?; + if exists { + return Err(error::Error::BadRequest( + "A route already exists with this path".to_string(), + )); + } + + tx = user_db.begin(&authed).await?; sqlx::query!( "UPDATE http_trigger - SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now() - WHERE workspace_id = $12 AND path = $13", - ct.route_path, + SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now(), is_static_website = $12 + WHERE workspace_id = $13 AND path = $14", + route_path, &route_path_key, ct.script_path, ct.path, @@ -287,14 +334,16 @@ async fn update_trigger( &authed.email, ct.is_async, ct.requires_auth, + ct.is_static_website, w_id, path, ) .execute(&mut *tx).await?; } else { + tx = user_db.begin(&authed).await?; sqlx::query!( - "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now() - WHERE workspace_id = $10 AND path = $11", + "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now(), is_static_website = $10 + WHERE workspace_id = $11 AND path = $12", ct.script_path, ct.path, ct.is_flow, @@ -304,6 +353,7 @@ async fn update_trigger( &authed.email, ct.is_async, ct.requires_auth, + ct.is_static_website, w_id, path, ) @@ -378,34 +428,57 @@ async fn exists_trigger( struct RouteExists { route_path: String, http_method: HttpMethod, + trigger_path: Option, +} + +async fn route_path_key_exists( + route_path_key: &str, + http_method: &HttpMethod, + w_id: &str, + trigger_path: Option<&str>, + db: &DB, +) -> error::Result { + let exists = if *CLOUD_HOSTED { + sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND workspace_id = $2 AND http_method = $3 AND ($4::TEXT IS NULL OR path != $4))", + &route_path_key, + w_id, + http_method as &HttpMethod, + trigger_path + ) + .fetch_one(db) + .await? + .unwrap_or(false) + } else { + sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2 AND ($3::TEXT IS NULL OR path != $3))", + &route_path_key, + http_method as &HttpMethod, + trigger_path + ) + .fetch_one(db) + .await? + .unwrap_or(false) + }; + Ok(exists) } async fn exists_route( Extension(db): Extension, Path(w_id): Path, - Json(RouteExists { route_path, http_method }): Json, + Json(RouteExists { route_path, http_method, trigger_path }): Json, ) -> JsonResult { let route_path_key = ROUTE_PATH_KEY_RE.replace_all(route_path.as_str(), ":key"); - let exists = if *CLOUD_HOSTED { - sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND workspace_id = $2 AND http_method = $3)", - &route_path_key, - w_id, - http_method as HttpMethod - ) - .fetch_one(&db) - .await? - .unwrap_or(false) - } else { - sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2)", - &route_path_key, - http_method as HttpMethod - ) - .fetch_one(&db) - .await? - .unwrap_or(false) - }; + + let exists = route_path_key_exists( + &route_path_key, + &http_method, + &w_id, + trigger_path.as_deref(), + &db, + ) + .await?; + Ok(Json(exists)) } @@ -420,6 +493,7 @@ struct TriggerRoute { edited_by: String, email: String, static_asset_config: Option>, + is_static_website: bool, } async fn get_http_route_trigger( @@ -439,7 +513,7 @@ async fn get_http_route_trigger( let route_path = StripPath(splitted.collect::>().join("/")); let triggers = sqlx::query_as!( TriggerRoute, - r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as "static_asset_config: _" FROM http_trigger WHERE workspace_id = $1 AND http_method = $2"#, + r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as "static_asset_config: _", is_static_website FROM http_trigger WHERE workspace_id = $1 AND http_method = $2"#, w_id, http_method as HttpMethod ) @@ -449,7 +523,7 @@ async fn get_http_route_trigger( } else { let triggers = sqlx::query_as!( TriggerRoute, - r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as "static_asset_config: _" FROM http_trigger WHERE http_method = $1"#, + r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as "static_asset_config: _", is_static_website FROM http_trigger WHERE http_method = $1"#, http_method as HttpMethod ) .fetch_all(db) @@ -461,6 +535,17 @@ async fn get_http_route_trigger( for (idx, trigger) in triggers.iter().enumerate() { let route_path = trigger.route_path.clone(); + if trigger.is_static_website { + router + .insert(format!("{}/*wm_subpath", route_path), idx) + .unwrap_or_else(|e| { + tracing::warn!( + "Failed to consider http trigger route {}: {:?}", + route_path, + e, + ); + }); + } router.insert(route_path.as_str(), idx).unwrap_or_else(|e| { tracing::warn!( "Failed to consider http trigger route {}: {:?}", @@ -565,7 +650,7 @@ async fn route_job( headers: HeaderMap, args: WebhookArgs, ) -> impl IntoResponse { - let route_path = route_path.to_path(); + let route_path = route_path.to_path().trim_end_matches("/"); let (trigger, called_path, params, authed) = match get_http_route_trigger( route_path, &auth_cache, @@ -612,11 +697,37 @@ async fn route_job( "No files storage resource defined at the workspace level".to_string(), ))?; let s3_client = build_object_store_client(&s3_resource).await?; - let path = object_store::path::Path::from(config.s3); - let s3_object = s3_client.get(&path).await.map_err(|err| { + + let path = if trigger.is_static_website { + let subpath = params + .get("wm_subpath") + .cloned() + .unwrap_or("index.html".to_string()); + tracing::info!("subpath: {}", subpath); + format!("{}/{}", config.s3.trim_end_matches('/'), subpath) + } else { + config.s3.clone() + }; + let path = object_store::path::Path::from(path); + let s3_object = s3_client.get(&path).await; + + let s3_object = match s3_object { + Err(object_store::Error::NotFound { .. }) if trigger.is_static_website => { + // fallback to index.html if the file is not found + let path = object_store::path::Path::from(format!( + "{}/index.html", + config.s3.trim_end_matches('/') + )); + s3_client.get(&path).await + } + r => r, + }; + + let s3_object = s3_object.map_err(|err| { tracing::warn!("Error retrieving file from S3: {:?}", err); error::Error::internal_err(format!("Error retrieving file: {}", err.to_string())) })?; + let mut response_headers = http::HeaderMap::new(); if let Some(ref e_tag) = s3_object.meta.e_tag { if let Some(if_none_match) = headers.get(IF_NONE_MATCH) { @@ -641,24 +752,26 @@ async fn route_job( .flatten() .unwrap_or("application/octet-stream".parse().unwrap()), ); - response_headers.insert( - "content-disposition", - config.filename.as_ref().map_or_else( - || { - s3_object - .attributes - .get(&object_store::Attribute::ContentDisposition) - .map(|s| s.parse().ok()) - .flatten() - .unwrap_or("inline".parse().unwrap()) - }, - |filename| { - format!("inline; filename=\"{}\"", filename) - .parse() - .unwrap_or("inline".parse().unwrap()) - }, - ), - ); + if !trigger.is_static_website { + response_headers.insert( + "content-disposition", + config.filename.as_ref().map_or_else( + || { + s3_object + .attributes + .get(&object_store::Attribute::ContentDisposition) + .map(|s| s.parse().ok()) + .flatten() + .unwrap_or("inline".parse().unwrap()) + }, + |filename| { + format!("inline; filename=\"{}\"", filename) + .parse() + .unwrap_or("inline".parse().unwrap()) + }, + ), + ); + } let body_stream = axum::body::Body::from_stream(s3_object.into_stream()); Ok::<_, error::Error>((StatusCode::OK, response_headers, body_stream)) diff --git a/backend/windmill-api/src/tracing_init.rs b/backend/windmill-api/src/tracing_init.rs index d099eb1598..9f34e93592 100644 --- a/backend/windmill-api/src/tracing_init.rs +++ b/backend/windmill-api/src/tracing_init.rs @@ -31,7 +31,7 @@ impl OnResponse for MyOnResponse { if *LOG_REQUESTS { let latency = latency.as_millis(); let status = response.status().as_u16(); - if response.status().is_success() { + if response.status().is_success() || response.status().is_redirection() { tracing::info!(latency = latency, status = status, "response") } else { tracing::error!(latency = latency, status = status, "response") diff --git a/frontend/src/lib/components/S3FilePicker.svelte b/frontend/src/lib/components/S3FilePicker.svelte index 3ee14c1986..8bfaa82763 100644 --- a/frontend/src/lib/components/S3FilePicker.svelte +++ b/frontend/src/lib/components/S3FilePicker.svelte @@ -43,6 +43,7 @@ export let initialFileKey: { s3: string } | undefined = undefined let initialFileKeyInternalCopy: { s3: string } export let selectedFileKey: { s3: string } | undefined = undefined + export let folderOnly = false let csvSeparatorChar: string = ',' let csvHasHeader: boolean = true @@ -369,6 +370,11 @@ let item_key = displayedFileKeys[index] let item = allFilesByKey[item_key] if (item.type === 'folder') { + if (folderOnly) { + selectedFileKey = { + s3: item_key + } + } if (toggleCollapsed) { item.collapsed = !item.collapsed } @@ -709,7 +715,9 @@ > {#if !fromWorkspaceSettings} {/if} diff --git a/frontend/src/lib/components/common/fileInput/FileInput.svelte b/frontend/src/lib/components/common/fileInput/FileInput.svelte index b123fbb3a4..0212f5f534 100644 --- a/frontend/src/lib/components/common/fileInput/FileInput.svelte +++ b/frontend/src/lib/components/common/fileInput/FileInput.svelte @@ -20,30 +20,24 @@ export let submittedText: string | undefined = undefined export let defaultFile: string | undefined = undefined export let disabled: boolean | undefined = undefined + export let folderOnly = false const dispatch = createEventDispatcher() let input: HTMLInputElement - export let files: File[] | undefined = undefined + type FileWithPath = File & { path?: string } + export let files: FileWithPath[] | undefined = undefined - async function onChange(fileList: FileList | null) { + async function onChange(fileList: FileWithPath[] | null) { if (!fileList || !fileList.length) { files = undefined dispatch('change', files) return } - if (!multiple || !files) { - files = [] - } - for (let i = 0; i < fileList.length; i++) { - const file = fileList.item(i) - if (file) { - files.push(file) - } - } - - if (!files.length) { - files = undefined + if (multiple && files) { + files = [...files, ...fileList] + } else { + files = fileList } // Needs to be reset so the same file can be selected @@ -86,15 +80,70 @@ } } - function handleDrop(event: DragEvent) { + async function handleFile(fileEntry: FileSystemFileEntry): Promise { + return new Promise((resolve, reject) => { + fileEntry.file(resolve, reject) + }) + } + + async function handleDirectory( + dirEntry: FileSystemDirectoryEntry, + path: string + ): Promise { + const files: FileWithPath[] = [] + const dirReader = dirEntry.createReader() + + async function readEntries() { + return new Promise((resolve) => { + dirReader.readEntries(async (entries) => { + if (entries.length === 0) { + resolve(files) + return + } + + const filePromises = entries.map(async (entry) => { + return traverseFileTree(entry, path + dirEntry.name + '/') + }) + const nestedFiles = await Promise.all(filePromises) + files.push(...nestedFiles.flat()) + // readEntries only return up to 100 files + // continue reading if more files exist + resolve(await readEntries()) + }) + }) + } + + return readEntries() + } + + async function traverseFileTree(entry: FileSystemEntry, path = ''): Promise { + if (entry.isFile) { + const file = await handleFile(entry as FileSystemFileEntry) + file.path = path + file.name + return [file] + } else if (entry.isDirectory) { + return handleDirectory(entry as FileSystemDirectoryEntry, path) + } + return [] + } + + async function handleDrop(event: DragEvent) { event.preventDefault() if (event.dataTransfer) { - if (event.dataTransfer.files && event.dataTransfer.files.length) { - if (!multiple && event.dataTransfer.files.length > 1) { - sendUserToast('Only one file can be uploaded at a time') - return - } else { - onChange(event.dataTransfer.files) + if (folderOnly) { + const item = event.dataTransfer.items[0]?.webkitGetAsEntry() + if (item) { + const files = await traverseFileTree(item, '') + onChange(files) + } + } else { + if (event.dataTransfer.files && event.dataTransfer.files.length) { + if (!multiple && event.dataTransfer.files.length > 1) { + sendUserToast('Only one file can be uploaded at a time') + return + } else { + onChange(Array.from(event.dataTransfer.files)) + } } } } @@ -174,22 +223,22 @@ {:else} - Drag and drop {multiple ? 'files' : 'a file'} + Drag and drop {folderOnly ? 'a folder' : multiple ? 'files' : 'a file'} {/if} 1 ? 's' : ''} chosen` : 'No file chosen'} bind:this={input} on:change={({ currentTarget }) => { - onChange(currentTarget.files) + onChange(currentTarget.files ? Array.from(currentTarget.files) : null) }} {accept} {multiple} {...$$restProps} /> - {#if defaultFile}
Default file: {defaultFile} diff --git a/frontend/src/lib/components/common/fileUpload/FileUpload.svelte b/frontend/src/lib/components/common/fileUpload/FileUpload.svelte index dccdbed4a4..09d6e639ac 100644 --- a/frontend/src/lib/components/common/fileUpload/FileUpload.svelte +++ b/frontend/src/lib/components/common/fileUpload/FileUpload.svelte @@ -14,7 +14,10 @@ export let acceptedFileTypes: string[] | undefined = ['*'] export let allowMultiple: boolean = true - export let containerText: string = allowMultiple + export let folderOnly = false + export let containerText: string = folderOnly + ? 'Drag and drop a folder here or click to browse' + : allowMultiple ? 'Drag and drop files here or click to browse' : 'Drag and drop a file here or click to browse' export let customResourcePath: string | undefined = undefined @@ -53,20 +56,61 @@ } async function handleChange(files: File[] | undefined) { - for (const file of files ?? []) { - uploadFileToS3(file, file.name) + if (folderOnly) { + uniqueFolderPrefix = getRandomFolderPrefix() + uploadedFolderRoot = undefined + await Promise.all( + files?.map(async (file) => { + await uploadFileToS3(file, file.name) + }) ?? [] + ) + if (uploadedFolderRoot) { + dispatch('addition', { + path: uniqueFolderPrefix + uploadedFolderRoot, + filename: undefined + }) + sendUserToast('Folder uploaded!') + } + } else { + for (const file of files ?? []) { + uploadFileToS3(file, file.name) + } } } let activeUploads: { xhr: XMLHttpRequest; fileName: string }[] = [] - async function uploadFileToS3(fileToUpload: File, fileToUploadKey: string) { + function getRandomFolderPrefix(): string { + const now = Date.now() + const randomNum = Math.floor(Math.random() * 65536) + return `windmill_uploads/upload_${now}_${randomNum}/` + } + + let uniqueFolderPrefix = getRandomFolderPrefix() + let uploadedFolderRoot: string | undefined = undefined + + async function uploadFileToS3(fileToUpload: File & { path?: string }, fileToUploadKey: string) { if (fileToUpload === undefined || fileToUploadKey === undefined) { return } let path: string | undefined = undefined let fileExtension: string | undefined = undefined - if (randomFileKey) { + if (folderOnly) { + const relativePath = fileToUpload.webkitRelativePath || fileToUpload.path + if (!relativePath) { + throw new Error('Missing file relative path') + } + const rootFolder = relativePath.split('/')[0] + if (!rootFolder) { + throw new Error('Missing root folder') + } + if (uploadedFolderRoot && rootFolder !== uploadedFolderRoot) { + throw new Error('Uploading a file in a different root folder than the previous files') + } else { + uploadedFolderRoot = rootFolder + } + path = uniqueFolderPrefix + relativePath + } else if (randomFileKey) { fileExtension = fileToUpload.name.split('.').pop() if (emptyString(fileExtension)) { fileExtension = undefined @@ -219,8 +263,10 @@ }) return } - dispatch('addition', { path: uploadData.path, filename: fileToUpload.name }) - sendUserToast('File upload finished!') + if (!folderOnly) { + dispatch('addition', { path: uploadData.path, filename: fileToUpload.name }) + sendUserToast('File uploaded!') + } uploadData.progress = 100 $fileUploads = $fileUploads.map((fileUpload) => { @@ -442,6 +488,7 @@
{:else} {:else if captureType === 'http'} {:else if triggerType === 'email'} { if (validateTimeout) { clearTimeout(validateTimeout) } validateTimeout = setTimeout(async () => { - if (!path || !method || !/^[\w-:]+(\/[\w-:]+)*$/.test(path)) { + if (!routePath || !method || !/^:?[-\w]+(\/:?[-\w]+)*$/.test(routePath)) { routeError = 'Endpoint not valid' - } else if (initialRoutePath !== path && (await routeExists(path, method))) { + } else if (await routeExists(routePath, method)) { routeError = 'Endpoint already taken' } else { routeError = '' @@ -59,14 +57,15 @@ workspace: $workspaceStore!, requestBody: { route_path, - http_method: method + http_method: method, + trigger_path: initialTriggerPath } }) } - $: captureURL = `${location.origin}${base}/api/w/${$workspaceStore}/capture_u/http/${ - isFlow ? 'flow' : 'script' - }/${path.replaceAll('/', '.')}/${route_path}` + $: validateRoute(route_path, http_method) + + $: isValid = routeError === '' function getHttpRoute(route_path: string | undefined) { return `${location.origin}${base}/api/r/${isCloudHosted() ? $workspaceStore + '/' : ''}${ @@ -74,10 +73,6 @@ }` } - $: validateRoute(route_path, http_method) - - $: isValid = routeError === '' - $: fullRoute = getHttpRoute(route_path) $: !http_method && (http_method = 'post') @@ -86,6 +81,9 @@
{#if showCapture && captureInfo} + {@const captureURL = `${location.origin}${base}/api/w/${$workspaceStore}/capture_u/http/${ + captureInfo.isFlow ? 'flow' : 'script' + }/${captureInfo.path.replaceAll('/', '.')}/${route_path}`} {#if static_asset_config} { s3Editor?.setCode(JSON.stringify(static_asset_config, null, 2)) @@ -220,102 +227,125 @@
- { - if (ev.detail === 'static_asset') { - static_asset_config = { s3: '' } - script_path = '' - initialScriptPath = '' - is_flow = false - http_method = 'get' - } else { - static_asset_config = undefined - } - }} - > - - - + {#if !isCloudHosted()} + { + if (ev.detail === 'static_asset' || ev.detail === 'static_website') { + static_asset_config = { s3: '' } + s3Editor?.setCode(JSON.stringify(static_asset_config, null, 2)) + script_path = '' + initialScriptPath = '' + is_flow = false + http_method = 'get' + is_async = false + is_static_website = ev.detail === 'static_website' + if (is_static_website) { + requires_auth = false + } + } else if (ev.detail === 'runnable') { + static_asset_config = undefined + } + }} + > + + + + + {/if} {#if static_asset_config} -
- {#if can_write} - - {/if} - {#if s3FileUploadRawMode} + {#if is_static_website} +

+ Upload or specify a folder on S3. All its files will be served under the path + above. Use this full path as the base URL of your website to ensure relative imports + work correctly. +

+ {/if} +
+
{#if can_write} - { - dispatch('focus') - }} - on:blur={(e) => { - dispatch('blur') - }} - code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)} - bind:value={static_asset_config} - /> - {:else} - - {/if} - {#if can_write} - + options={{ left: 'Existing file' }} + disabled={!can_write} + /> {/if} - {:else} - { - static_asset_config = { - s3: evt.detail?.path ?? '', - filename: evt.detail?.filename ?? undefined - } - s3FileUploadRawMode = true - }} - on:deletion={(evt) => { - static_asset_config = { - s3: '' - } - }} - /> - {/if} + {#if s3FileUploadRawMode} + {#if can_write} + { + dispatch('focus') + }} + on:blur={(e) => { + dispatch('blur') + }} + code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)} + bind:value={static_asset_config} + /> + {:else} + + {/if} + {#if can_write} + + {/if} + {:else} + {#key is_static_website} + { + static_asset_config = { + s3: evt.detail?.path ?? '', + filename: evt.detail?.filename ?? undefined + } + s3FileUploadRawMode = true + }} + on:deletion={(evt) => { + static_asset_config = { + s3: '' + } + }} + /> + {/key} + {/if} +
{:else} -

+

Pick a script or flow to be triggered
To handle headers, query or path parameters, add a preprocessor to your runnable.

@@ -346,48 +376,52 @@ {/if}
-
-
-
-
-
+ + {/if} {/if} diff --git a/frontend/src/routes/(root)/(logged)/routes/+page.svelte b/frontend/src/routes/(root)/(logged)/routes/+page.svelte index bd12e6d5eb..395264f987 100644 --- a/frontend/src/routes/(root)/(logged)/routes/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/routes/+page.svelte @@ -109,7 +109,11 @@ 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('/')) ?? []) + new Set( + filteredItems + ?.filter((x) => x.script_path) + .map((x) => x.script_path.split('/').slice(0, 2).join('/')) ?? [] + ) ).sort() $: items = filter !== '' ? filteredItems : preFilteredItems @@ -156,176 +160,177 @@ f={(x) => (x.summary ?? '') + ' ' + x.path + ' (' + x.script_path + ')'} /> -{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find(_ => _.id === $workspaceStore)?.operator_settings?.triggers} - -{:else} - - - {#if $userStore?.is_admin || $userStore?.is_super_admin} - - {/if} - -
-
- -
-
Filter by path of
- - - - -
- - -
- {#if $userStore?.is_super_admin && $userStore.username.includes('@')} - - {:else if $userStore?.is_admin || $userStore?.is_super_admin} - - {/if} -
-
- {#if loading} - {#each new Array(6) as _} - - {/each} - {:else if !triggers?.length} -
No routes
- {:else if items?.length} -
- {#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, route_path, is_flow, extra_perms, canWrite, marked, http_method, static_asset_config } (path)} - {@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`} - -
-
- - - routeEditor?.openEdit(path, is_flow)} - class="min-w-0 grow hover:underline decoration-gray-400" - > -
- {#if marked} - - {@html marked} - - {:else} - {http_method.toUpperCase()} /{route_path} - {/if} -
-
- {path} -
-
- {#if static_asset_config} - file: {static_asset_config.s3} - {:else} - runnable: {script_path} - {/if} -
-
- - - -
- - { - goto(href) - } - }, - { - displayName: 'Delete', - type: 'delete', - icon: Trash, - disabled: !canWrite || !($userStore?.is_admin || $userStore?.is_super_admin), - action: async () => { - await HttpTriggerService.deleteHttpTrigger({ - workspace: $workspaceStore ?? '', - path - }) - loadTriggers() - } - }, - { - displayName: canWrite ? 'Edit' : 'View', - icon: canWrite ? Pen : Eye, - action: () => { - routeEditor?.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, 'http_trigger') - } - } - ]} - /> -
-
-
-
-
edited by {edited_by}
-
at {displayDate(edited_at)}
-
-
-
- {/each} -
- {:else} - - {/if} +{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.triggers} + - {#if items && items?.length > 15 && nbDisplayed < items.length} - {nbDisplayed} items out of {items.length} - + - {/if} - + {#if $userStore?.is_admin || $userStore?.is_super_admin} + + {/if} + +
+
+ +
+
Filter by path of
+ + + + +
+ + +
+ {#if $userStore?.is_super_admin && $userStore.username.includes('@')} + + {:else if $userStore?.is_admin || $userStore?.is_super_admin} + + {/if} +
+
+ {#if loading} + {#each new Array(6) as _} + + {/each} + {:else if !triggers?.length} +
No routes
+ {:else if items?.length} +
+ {#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, route_path, is_flow, extra_perms, canWrite, marked, http_method, static_asset_config } (path)} + {@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`} + +
+
+ + + routeEditor?.openEdit(path, is_flow)} + class="min-w-0 grow hover:underline decoration-gray-400" + > +
+ {#if marked} + + {@html marked} + + {:else} + {http_method.toUpperCase()} /{route_path} + {/if} +
+
+ {path} +
+
+ {#if static_asset_config} + file: {static_asset_config.s3} + {:else} + runnable: {script_path} + {/if} +
+
+ + + +
+ + { + goto(href) + } + }, + { + displayName: 'Delete', + type: 'delete', + icon: Trash, + disabled: + !canWrite || !($userStore?.is_admin || $userStore?.is_super_admin), + action: async () => { + await HttpTriggerService.deleteHttpTrigger({ + workspace: $workspaceStore ?? '', + path + }) + loadTriggers() + } + }, + { + displayName: canWrite ? 'Edit' : 'View', + icon: canWrite ? Pen : Eye, + action: () => { + routeEditor?.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, 'http_trigger') + } + } + ]} + /> +
+
+
+
+
edited by {edited_by}
+
at {displayDate(edited_at)}
+
+
+
+ {/each} +
+ {:else} + + {/if} +
+ {#if items && items?.length > 15 && nbDisplayed < items.length} + {nbDisplayed} items out of {items.length} + + {/if} + {/if}