mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 08:03:16 +00:00
feat: public app rate limiting + fork hub raw apps + raw apps publish to hub button
This commit is contained in:
Generated
+1
@@ -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",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE workspace_settings
|
||||
DROP COLUMN IF EXISTS public_app_execution_limit_per_minute;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE workspace_settings
|
||||
ADD COLUMN IF NOT EXISTS public_app_execution_limit_per_minute INTEGER DEFAULT NULL;
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<i32>,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> JsonResult<Box<serde_json::value::RawValue>> {
|
||||
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<DB>,
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<DashMap<String, RateLimitEntry>> =
|
||||
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(())
|
||||
}
|
||||
@@ -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_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub success_handler: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub public_app_execution_limit_per_minute: Option<i32>,
|
||||
}
|
||||
|
||||
/// #[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<i32>,
|
||||
}
|
||||
|
||||
async fn edit_public_app_rate_limit(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
authed: ApiAuthed,
|
||||
Json(req): Json<EditPublicAppRateLimitRequest>,
|
||||
) -> Result<String> {
|
||||
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<Option<i32>> {
|
||||
let result: Option<Option<i32>> = 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)]
|
||||
|
||||
Generated
+88
-2
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:open={publishToHubDrawerOpen} size="600px">
|
||||
<DrawerContent title="Publish to Hub" on:close={() => (publishToHubDrawerOpen = false)}>
|
||||
{#snippet actions()}
|
||||
<Button
|
||||
loading={publishingToHub}
|
||||
disabled={!app}
|
||||
on:click={publishToHub}
|
||||
variant="accent"
|
||||
startIcon={{ icon: Download }}
|
||||
>
|
||||
Download & open hub
|
||||
</Button>
|
||||
{/snippet}
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-secondary text-sm">
|
||||
This will download a zip file containing your raw app bundle and open the Windmill Hub
|
||||
submission page.
|
||||
</p>
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold mb-2">The zip file will contain:</p>
|
||||
<ul class="list-disc list-inside text-secondary space-y-1">
|
||||
<li
|
||||
><code class="text-xs bg-surface-secondary px-1 rounded">app.yaml</code> - App configuration</li
|
||||
>
|
||||
<li
|
||||
><code class="text-xs bg-surface-secondary px-1 rounded">bundle.js</code> - JavaScript bundle</li
|
||||
>
|
||||
<li
|
||||
><code class="text-xs bg-surface-secondary px-1 rounded">bundle.css</code> - CSS styles</li
|
||||
>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<AppJobsDrawer
|
||||
bind:open={jobsDrawerOpen}
|
||||
on:clear={() => {
|
||||
|
||||
+10
-2
@@ -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
|
||||
|
||||
@@ -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<string, string> = $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, [
|
||||
{
|
||||
|
||||
@@ -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<AIConfig['providers'], undefined> = $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 @@
|
||||
<Tab
|
||||
small
|
||||
value="default_app"
|
||||
aiId="workspace-settings-default-app"
|
||||
aiDescription="Default app workspace settings"
|
||||
label="Default App"
|
||||
aiId="workspace-settings-apps"
|
||||
aiDescription="Apps workspace settings"
|
||||
label="Apps"
|
||||
/>
|
||||
|
||||
<Tab
|
||||
@@ -1405,6 +1424,33 @@ export async function main(
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<hr class="border-t my-8" />
|
||||
<Section
|
||||
label="Public App Rate Limiting"
|
||||
description="Limit the number of public (anonymous) app executions per minute per server. Set to 0 or leave empty to disable. This is a per-server limit, not a global limit."
|
||||
class="flex flex-col gap-6"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<TextInput
|
||||
inputProps={{ type: 'number', placeholder: '0 (disabled)' }}
|
||||
bind:value={publicAppRateLimitPerMinute}
|
||||
class="w-48"
|
||||
/>
|
||||
<span class="text-secondary text-sm">executions per minute per server</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={publicAppRateLimitPerMinute === initialPublicAppRateLimitPerMinute}
|
||||
size="sm"
|
||||
on:click={editPublicAppRateLimit}
|
||||
variant="default"
|
||||
startIcon={{ icon: Save }}
|
||||
btnClasses="w-fit"
|
||||
>
|
||||
Save rate limit
|
||||
</Button>
|
||||
</Section>
|
||||
{:else if tab == 'native_triggers'}
|
||||
{#if $workspaceStore}
|
||||
{#await import('$lib/components/workspaceSettings/WorkspaceIntegrations.svelte') then { default: WorkspaceIntegrations }}
|
||||
|
||||
Reference in New Issue
Block a user