diff --git a/backend/Cargo.lock b/backend/Cargo.lock index d6a8b7307f..b917f87599 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -15587,6 +15587,7 @@ dependencies = [ "constant_time_eq 0.3.1", "cookie 0.17.0", "cron", + "dashmap 6.1.0", "datafusion", "deno_core", "deno_error", diff --git a/backend/migrations/20260204120403_add_public_app_rate_limit.down.sql b/backend/migrations/20260204120403_add_public_app_rate_limit.down.sql new file mode 100644 index 0000000000..d81d2f4169 --- /dev/null +++ b/backend/migrations/20260204120403_add_public_app_rate_limit.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE workspace_settings +DROP COLUMN IF EXISTS public_app_execution_limit_per_minute; diff --git a/backend/migrations/20260204120403_add_public_app_rate_limit.up.sql b/backend/migrations/20260204120403_add_public_app_rate_limit.up.sql new file mode 100644 index 0000000000..05cc67078a --- /dev/null +++ b/backend/migrations/20260204120403_add_public_app_rate_limit.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE workspace_settings +ADD COLUMN IF NOT EXISTS public_app_execution_limit_per_minute INTEGER DEFAULT NULL; diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index 16a0bc3a16..f0672666d0 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -169,6 +169,7 @@ tar.workspace = true flate2.workspace = true backon = {workspace = true, optional = true} strum = { workspace = true, optional = true } +dashmap.workspace = true [build-dependencies] deno_core = { workspace = true, optional = true } diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 958218d854..6ba9e8935f 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2383,6 +2383,9 @@ paths: type: string operator_settings: $ref: "#/components/schemas/OperatorSettings" + public_app_execution_limit_per_minute: + type: integer + description: Rate limit for public app executions per minute per server. NULL or 0 means disabled. /w/{workspace}/workspaces/get_deploy_to: get: @@ -4145,6 +4148,35 @@ paths: type: string example: "Updated mute critical alert UI settings for workspace: workspace_id" + /w/{workspace}/workspaces/public_app_rate_limit: + post: + summary: Set public app rate limit for this workspace + operationId: setPublicAppRateLimit + tags: + - setting + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: Public app rate limit configuration + required: true + content: + application/json: + schema: + type: object + properties: + public_app_execution_limit_per_minute: + type: integer + description: Rate limit for public app executions per minute per server. NULL or 0 to disable. + example: 100 + responses: + "200": + description: Successfully updated public app rate limit settings. + content: + application/json: + schema: + type: string + example: "Updated public app rate limit for workspace: workspace_id" + /oauth/login_callback/{client_name}: post: security: [] @@ -5396,6 +5428,34 @@ paths: required: - app + /apps/hub/get_raw/{id}: + get: + summary: get hub raw app by id + operationId: getHubRawAppById + tags: + - app + parameters: + - $ref: "#/components/parameters/PathId" + responses: + "200": + description: raw app + content: + application/json: + schema: + type: object + properties: + app: + type: object + properties: + summary: + type: string + value: {} + required: + - summary + - value + required: + - app + /apps_u/public_app_by_custom_path/{custom_path}: get: summary: get public app by custom path diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index cf26dfd8a0..9d2d6d9db3 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -126,6 +126,7 @@ pub fn global_service() -> Router { Router::new() .route("/hub/list", get(list_hub_apps)) .route("/hub/get/:id", get(get_hub_app_by_id)) + .route("/hub/get_raw/:id", get(get_hub_raw_app_by_id)) } #[derive(FromRow, Deserialize, Serialize)] @@ -1312,6 +1313,24 @@ pub async fn get_hub_app_by_id( Ok(Json(value)) } +pub async fn get_hub_raw_app_by_id( + Path(id): Path, + Extension(db): Extension, +) -> JsonResult> { + let value = http_get_from_hub( + &HTTP_CLIENT, + &format!("{}/raw_apps/{}/json", *HUB_BASE_URL.read().await, id), + false, + None, + Some(&db), + ) + .await? + .json() + .await + .map_err(to_anyhow)?; + Ok(Json(value)) +} + async fn delete_app( authed: ApiAuthed, Extension(db): Extension, @@ -1910,6 +1929,15 @@ async fn execute_component( } }; + // Check rate limit for anonymous (public) executions + if matches!(policy.execution_mode, ExecutionMode::Anonymous) && opt_authed.is_none() { + if let Some(limit) = crate::workspaces::get_public_app_rate_limit(&db, &w_id).await? { + if limit > 0 { + crate::public_app_rate_limit::check_and_increment(&w_id, limit)?; + } + } + } + // Execution is publisher and an user is authenticated: check if the user is authorized to // execute the app. if let (ExecutionMode::Publisher, Some(authed)) = (policy.execution_mode, opt_authed.as_ref()) { diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 8a44ee8360..a78b6fa2a3 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -167,6 +167,7 @@ mod teams_approvals_oss; #[cfg(feature = "native_trigger")] pub mod native_triggers; mod public_app_layer; +mod public_app_rate_limit; mod static_assets; #[cfg(all(feature = "stripe", feature = "enterprise", feature = "private"))] pub mod stripe_ee; diff --git a/backend/windmill-api/src/public_app_rate_limit.rs b/backend/windmill-api/src/public_app_rate_limit.rs new file mode 100644 index 0000000000..bff89d694b --- /dev/null +++ b/backend/windmill-api/src/public_app_rate_limit.rs @@ -0,0 +1,48 @@ +/* + * Author: Windmill Labs, Inc + * Copyright: Windmill Labs, Inc 2024 + * This file and its contents are licensed under the AGPLv3 License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use chrono::Utc; +use dashmap::DashMap; +use hyper::StatusCode; +use std::sync::LazyLock; +use windmill_common::error::{Error, Result}; + +struct RateLimitEntry { + count: i32, + minute_bucket: i64, +} + +static RATE_LIMIT_COUNTER: LazyLock> = + LazyLock::new(DashMap::new); + +pub fn check_and_increment(workspace_id: &str, limit: i32) -> Result<()> { + let current_minute = Utc::now().timestamp() / 60; + + let mut entry = RATE_LIMIT_COUNTER + .entry(workspace_id.to_string()) + .or_insert(RateLimitEntry { count: 0, minute_bucket: current_minute }); + + if entry.minute_bucket != current_minute { + entry.count = 0; + entry.minute_bucket = current_minute; + } + + if entry.count >= limit { + return Err(Error::Generic( + StatusCode::TOO_MANY_REQUESTS, + format!( + "Rate limit exceeded for public app executions in workspace '{}'. \ + Limit: {} per minute per server.", + workspace_id, limit + ), + )); + } + + entry.count += 1; + Ok(()) +} diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 4b2d937625..da56b0dcd2 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -176,6 +176,10 @@ pub fn workspaced_service() -> Router { post(acknowledge_all_critical_alerts), ) .route("/critical_alerts/mute", post(mute_critical_alerts)) + .route( + "/public_app_rate_limit", + post(edit_public_app_rate_limit), + ) .route("/operator_settings", post(update_operator_settings)) .route( "/create_workspace_fork_branch", @@ -287,6 +291,8 @@ pub struct WorkspaceSettings { pub error_handler: Option, #[serde(skip_serializing_if = "Option::is_none")] pub success_handler: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub public_app_execution_limit_per_minute: Option, } /// #[derive(sqlx::Type, Serialize, Deserialize, Debug)] @@ -625,7 +631,8 @@ async fn get_settings( git_app_installations, auto_invite, error_handler, - success_handler + success_handler, + public_app_execution_limit_per_minute FROM workspace_settings WHERE @@ -4376,6 +4383,55 @@ pub async fn mute_critical_alerts() -> Error { Error::NotFound("Critical Alerts require EE".to_string()) } +#[derive(Deserialize)] +pub struct EditPublicAppRateLimitRequest { + pub public_app_execution_limit_per_minute: Option, +} + +async fn edit_public_app_rate_limit( + Extension(db): Extension, + Path(w_id): Path, + authed: ApiAuthed, + Json(req): Json, +) -> Result { + require_admin(authed.is_admin, &authed.username)?; + + sqlx::query!( + "UPDATE workspace_settings SET public_app_execution_limit_per_minute = $1 WHERE workspace_id = $2", + req.public_app_execution_limit_per_minute, + &w_id + ) + .execute(&db) + .await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &w_id, + DeployedObject::Settings { setting_type: "public_app_rate_limit".to_string() }, + None, + false, + None, + ) + .await?; + + Ok(format!( + "Updated public app rate limit for workspace: {}", + &w_id + )) +} + +pub async fn get_public_app_rate_limit(db: &DB, w_id: &str) -> Result> { + let result: Option> = sqlx::query_scalar( + "SELECT public_app_execution_limit_per_minute FROM workspace_settings WHERE workspace_id = $1", + ) + .bind(w_id) + .fetch_optional(db) + .await?; + Ok(result.flatten()) +} + #[derive(Deserialize, Serialize)] struct ChangeOperatorSettings { #[serde(default)] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4e7d2b4eb1..54fa07dbdb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -47,6 +47,7 @@ "hash-sum": "^2.0.0", "highlight.js": "^11.8.0", "idb": "^8.0.2", + "jszip": "^3.10.1", "lru-cache": "^11.1.0", "lucide-svelte": "^0.540.0", "minimatch": "^10.0.1", @@ -4308,6 +4309,12 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -6926,7 +6933,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "devOptional": true, "license": "ISC" }, "node_modules/ini": { @@ -7129,6 +7135,12 @@ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", "license": "MIT" }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7331,6 +7343,54 @@ "node": ">=0.10.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7572,6 +7632,21 @@ "url": "https://github.com/sponsors/dmonad" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lie/node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -10781,6 +10856,12 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -11591,6 +11672,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13351,7 +13438,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true, "license": "MIT" }, "node_modules/validate-npm-package-license": { diff --git a/frontend/package.json b/frontend/package.json index 6d006737a8..cc63f84d8f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -117,6 +117,7 @@ "hash-sum": "^2.0.0", "highlight.js": "^11.8.0", "idb": "^8.0.2", + "jszip": "^3.10.1", "lru-cache": "^11.1.0", "lucide-svelte": "^0.540.0", "minimatch": "^10.0.1", diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index b732af8979..83cf84a793 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -4,13 +4,17 @@ import UndoRedo from '$lib/components/common/button/UndoRedo.svelte' import { AppService, DraftService, type Policy } from '$lib/gen' - import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores' + import { rawAppToHubUrl } from '$lib/hub' + import { enterpriseLicense, hubBaseUrlStore, userStore, workspaceStore } from '$lib/stores' + import JSZip from 'jszip' + import YAML from 'yaml' import { Bug, DiffIcon, + Download, EllipsisVertical, FileJson, - FileUp, + Globe, History, Pen, Save, @@ -143,8 +147,40 @@ let draftDrawerOpen = $state(false) let saveDrawerOpen = $state(false) let historyBrowserDrawerOpen = $state(false) + let publishToHubDrawerOpen = $state(false) + let publishingToHub = $state(false) let deploymentMsg: string | undefined = $state(undefined) + async function publishToHub() { + if (!app) return + publishingToHub = true + try { + const { js, css } = await getBundle() + const zip = new JSZip() + zip.file('app.yaml', YAML.stringify(app)) + zip.file('bundle.js', js) + zip.file('bundle.css', css) + const blob = await zip.generateAsync({ type: 'blob' }) + + // Download the zip + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${(appPath || 'raw-app').replaceAll('/', '__')}.zip` + a.click() + setTimeout(() => URL.revokeObjectURL(url), 100) + + // Open hub page + const hubUrl = rawAppToHubUrl( + $hubBaseUrlStore, + summary || appPath.split('/').pop()?.replace('_', ' ') || 'my raw app' + ) + window.open(hubUrl.toString(), '_blank') + } finally { + publishingToHub = false + } + } + function closeSaveDrawer() { saveDrawerOpen = false } @@ -538,11 +574,10 @@ } }, { - displayName: 'Hub compatible JSON', - icon: FileUp, + displayName: 'Publish to Hub', + icon: Globe, action: () => { - sendUserToast('todo') - // appExport.open(toStatic(app, $staticExporter, summary).app) + publishToHubDrawerOpen = true } }, { @@ -728,6 +763,42 @@ + + (publishToHubDrawerOpen = false)}> + {#snippet actions()} + + {/snippet} +
+

+ This will download a zip file containing your raw app bundle and open the Windmill Hub + submission page. +

+
+

The zip file will contain:

+
    +
  • app.yaml - App configuration
  • +
  • bundle.js - JavaScript bundle
  • +
  • bundle.css - CSS styles
  • +
+
+
+
+
+ { diff --git a/frontend/src/lib/hub.ts b/frontend/src/lib/hub.ts index 78a617397b..fcaa2ab832 100644 --- a/frontend/src/lib/hub.ts +++ b/frontend/src/lib/hub.ts @@ -1,7 +1,7 @@ import type { Schema } from './common' import { AppService, FlowService, type Flow, type Script } from './gen' import { encodeState } from './utils' -import rawHubPaths from './hubPaths.json?raw' +import hubPathsData from './hubPaths.json' import { replacePlaceholderForSignatureScriptTemplate, SIGNATURE_TEMPLATE_FLOW_HUB_ID, @@ -83,6 +83,14 @@ export function appToHubUrl(staticApp: any, hubBaseUrl: string): URL { return url } +export function rawAppToHubUrl(hubBaseUrl: string, summary?: string): URL { + const url = new URL(hubBaseUrl + '/raw_apps/add') + if (summary) { + url.searchParams.append('summary', summary) + } + return url +} + type HubPaths = { gitSync: string gitSyncTest: string @@ -101,4 +109,4 @@ type HubPaths = { appReport: string } -export const hubPaths = JSON.parse(rawHubPaths) as HubPaths +export const hubPaths = hubPathsData as HubPaths diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte index bd694080e7..9b1cb497ce 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte @@ -39,13 +39,14 @@ let nodraft = $page.url.searchParams.get('nodraft') const templatePath = $page.url.searchParams.get('template') const templateId = $page.url.searchParams.get('template_id') + const hubId = $page.url.searchParams.get('hub') const importRaw = $importStore if ($importStore) { $importStore = undefined } - const appState = nodraft ? undefined : localStorage.getItem('rawapp') + const appState = nodraft || hubId ? undefined : localStorage.getItem('rawapp') let summary = $state('') let files: Record = $state(react19Template) @@ -143,7 +144,18 @@ console.log('App loaded from template id') sendUserToast('App loaded from template') goto('?', { replaceState: true }) - } else if (!templatePath && appState) { + } else if (hubId) { + const hub = await AppService.getHubRawAppById({ id: Number(hubId) }) + if (hub.app?.value) { + extractValue(hub.app.value) + } + if (hub.app?.summary) { + summary = hub.app.summary + } + console.log('App loaded from Hub') + sendUserToast('App loaded from Hub') + goto('?', { replaceState: true }) + } else if (!templatePath && !hubId && appState) { console.log('App loaded from browser stored autosave') sendUserToast('App restored from browser stored autosave', false, [ { diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 18ff4a394a..b59dcaaccf 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -102,6 +102,8 @@ let successHandlerScriptPath: string | undefined = $state(undefined) let criticalAlertUIMuted: boolean | undefined = $state(undefined) let initialCriticalAlertUIMuted: boolean | undefined = $state(undefined) + let publicAppRateLimitPerMinute: number | undefined = $state(undefined) + let initialPublicAppRateLimitPerMinute: number | undefined = $state(undefined) let aiProviders: Exclude = $state({}) let codeCompletionModel: string | undefined = $state(undefined) @@ -347,6 +349,8 @@ errorHandlerMutedOnUserPath = errorHandler?.muted_on_user_path criticalAlertUIMuted = settings.mute_critical_alerts initialCriticalAlertUIMuted = settings.mute_critical_alerts + publicAppRateLimitPerMinute = settings.public_app_execution_limit_per_minute ?? undefined + initialPublicAppRateLimitPerMinute = settings.public_app_execution_limit_per_minute ?? undefined if (emptyString($enterpriseLicense)) { errorHandlerSelected = 'custom' } else { @@ -561,6 +565,21 @@ }, 3000) } + async function editPublicAppRateLimit() { + await SettingService.setPublicAppRateLimit({ + workspace: $workspaceStore!, + requestBody: { + public_app_execution_limit_per_minute: publicAppRateLimitPerMinute + } + }) + initialPublicAppRateLimitPerMinute = publicAppRateLimitPerMinute + sendUserToast( + publicAppRateLimitPerMinute + ? `Public app rate limit set to ${publicAppRateLimitPerMinute} per minute per server` + : `Public app rate limit disabled` + ) + } + // Function to check if there are unsaved changes in AI settings function getAiSettingsInitialAndModifiedValues() { // Only check for unsaved changes when on the AI tab @@ -764,9 +783,9 @@ {/key} + +
+
+
+ + executions per minute per server +
+ + +
{:else if tab == 'native_triggers'} {#if $workspaceStore} {#await import('$lib/components/workspaceSettings/WorkspaceIntegrations.svelte') then { default: WorkspaceIntegrations }}