feat: add instance setting to enforce workspace prefix for HTTP routes (#8528)
* feat: add instance-level setting to enforce workspace prefix for HTTP routes
Add `http_route_workspaced_route` instance setting that forces all HTTP routes
to use workspace prefix (`/api/r/{workspace_id}/{route}`), mirroring the existing
`app_workspaced_route` setting for apps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: bump http trigger version on setting change to invalidate route cache
The route cache is version-based, not TTL-based. Without bumping the
version sequence when the instance setting changes, cached routes would
continue serving with the old prefix behavior until a route is
created/updated/deleted or the server restarts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: immediately refresh HTTP routers on setting change
The route cache polls every 60 seconds, but bumping the version sequence
only makes the next poll pick up changes. Explicitly call refresh_routers
after the setting reload so routes are rebuilt immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
backend/.sqlx/query-87ee10d8ba5ba281781f23e5390190fb90df980a19c900452f9b1a19c3620e30.json
generated
Normal file
32
backend/.sqlx/query-87ee10d8ba5ba281781f23e5390190fb90df980a19c900452f9b1a19c3620e30.json
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n route_path,\n workspace_id,\n http_method::TEXT AS \"http_method!\"\n FROM\n http_trigger\n WHERE\n workspaced_route IS FALSE\n AND route_path_key IN (\n SELECT\n route_path_key\n FROM\n http_trigger\n WHERE\n workspaced_route IS FALSE\n GROUP BY\n route_path_key, http_method\n HAVING COUNT(*) > 1\n )\n ORDER BY route_path_key\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "http_method!",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "87ee10d8ba5ba281781f23e5390190fb90df980a19c900452f9b1a19c3620e30"
|
||||
}
|
||||
@@ -44,8 +44,8 @@ use windmill_common::{
|
||||
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
|
||||
INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
HTTP_ROUTE_WORKSPACED_ROUTE_SETTING, HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING,
|
||||
INDEXER_SETTING, INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MAVEN_SETTINGS_XML_SETTING,
|
||||
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
|
||||
@@ -104,10 +104,10 @@ use crate::monitor::{
|
||||
reload_base_url_setting, reload_bunfig_install_scopes_setting,
|
||||
reload_critical_alert_mute_ui_setting, reload_critical_alerts_on_token_expiry_setting,
|
||||
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
|
||||
reload_hub_api_secret_setting, reload_hub_base_url_setting,
|
||||
reload_instance_events_webhook_setting, reload_job_default_timeout_setting,
|
||||
reload_job_isolation_setting, reload_jwt_secret_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_otel_tracing_proxy_setting,
|
||||
reload_http_route_workspaced_route_setting, reload_hub_api_secret_setting,
|
||||
reload_hub_base_url_setting, reload_instance_events_webhook_setting,
|
||||
reload_job_default_timeout_setting, reload_job_isolation_setting, reload_jwt_secret_setting,
|
||||
reload_license_key, reload_npm_config_registry_setting, reload_otel_tracing_proxy_setting,
|
||||
reload_pip_index_url_setting, reload_retention_period_setting, reload_scim_token_setting,
|
||||
reload_smtp_config, reload_uv_index_strategy_setting, reload_worker_config, MonitorIteration,
|
||||
};
|
||||
@@ -1814,6 +1814,23 @@ async fn process_notify_event(
|
||||
tracing::error!(error = %e, "Could not reload app workspaced route setting");
|
||||
}
|
||||
}
|
||||
HTTP_ROUTE_WORKSPACED_ROUTE_SETTING => {
|
||||
if let Err(e) = reload_http_route_workspaced_route_setting(db).await {
|
||||
tracing::error!(error = %e, "Could not reload http route workspaced route setting");
|
||||
}
|
||||
#[cfg(feature = "http_trigger")]
|
||||
match windmill_api::triggers::http::refresh_routers(db).await {
|
||||
Ok((true, _)) => {
|
||||
tracing::info!(
|
||||
"Refreshed HTTP routers (http workspaced route setting change)"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error refreshing HTTP routers (http workspaced route setting change): {err:#}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
AI_CONFIG_SETTING => {
|
||||
tracing::info!("AI config setting changed, bumping instance AI cache revision");
|
||||
bump_instance_ai_config_revision();
|
||||
|
||||
@@ -88,7 +88,13 @@ use windmill_common::{
|
||||
MONITOR_LOGS_ON_OBJECT_STORE, OTEL_LOGS_ENABLED, OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED,
|
||||
SERVICE_LOG_RETENTION_SECS,
|
||||
};
|
||||
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
|
||||
use windmill_common::{
|
||||
client::AuthedClient,
|
||||
global_settings::{
|
||||
APP_WORKSPACED_ROUTE_SETTING, HTTP_ROUTE_WORKSPACED_ROUTE,
|
||||
HTTP_ROUTE_WORKSPACED_ROUTE_SETTING,
|
||||
},
|
||||
};
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_object_store::reload_object_store_setting;
|
||||
use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload};
|
||||
@@ -296,6 +302,10 @@ pub async fn initial_load(
|
||||
if let Err(e) = reload_app_workspaced_route_setting(db).await {
|
||||
tracing::error!("Error reloading app workspaced route: {:?}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = reload_http_route_workspaced_route_setting(db).await {
|
||||
tracing::error!("Error reloading http route workspaced route: {:?}", e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -3390,6 +3400,39 @@ pub async fn reload_app_workspaced_route_setting(conn: &DB) -> error::Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_http_route_workspaced_route_setting(conn: &DB) -> error::Result<()> {
|
||||
let http_route_workspaced_route =
|
||||
load_value_from_global_settings(conn, HTTP_ROUTE_WORKSPACED_ROUTE_SETTING).await?;
|
||||
|
||||
let ws_route = match http_route_workspaced_route {
|
||||
Some(serde_json::Value::Bool(ws_route)) => ws_route,
|
||||
None => false,
|
||||
_ => {
|
||||
tracing::error!(
|
||||
"Expected {} to be a boolean got: {:?}. Defaulting to false",
|
||||
HTTP_ROUTE_WORKSPACED_ROUTE_SETTING,
|
||||
http_route_workspaced_route
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mut l = HTTP_ROUTE_WORKSPACED_ROUTE.write().await;
|
||||
|
||||
if *l != ws_route {
|
||||
*l = ws_route;
|
||||
drop(l);
|
||||
// Bump the HTTP trigger version so the route cache is rebuilt with
|
||||
// the updated workspaced_route behavior on the next request.
|
||||
sqlx::query!("SELECT nextval('http_trigger_version_seq')")
|
||||
.fetch_one(conn)
|
||||
.await?;
|
||||
} else {
|
||||
*l = ws_route;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_critical_alerts_on_db_oversize(conn: &DB) -> error::Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct DBOversize {
|
||||
|
||||
@@ -45,8 +45,8 @@ use windmill_common::{
|
||||
global_settings::{
|
||||
AI_CONFIG_SETTING, APP_WORKSPACED_ROUTE_SETTING, AUTOMATE_USERNAME_CREATION_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, DISABLE_HUB_SETTING,
|
||||
EMAIL_DOMAIN_SETTING, ENV_SETTINGS, HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
WS_BASE_URL_SETTING,
|
||||
EMAIL_DOMAIN_SETTING, ENV_SETTINGS, HTTP_ROUTE_WORKSPACED_ROUTE_SETTING,
|
||||
HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING, WS_BASE_URL_SETTING,
|
||||
},
|
||||
instance_config::{self, ApplyMode, InstanceConfig},
|
||||
server::Smtp,
|
||||
@@ -424,6 +424,74 @@ async fn run_setting_pre_write_hook(
|
||||
}
|
||||
}
|
||||
}
|
||||
HTTP_ROUTE_WORKSPACED_ROUTE_SETTING => {
|
||||
let serde_json::Value::Bool(workspaced_route) = value else {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"{} setting expected to be boolean",
|
||||
HTTP_ROUTE_WORKSPACED_ROUTE_SETTING
|
||||
)));
|
||||
};
|
||||
|
||||
if !*workspaced_route {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[allow(unused)]
|
||||
struct DuplicateRoute {
|
||||
route_path: String,
|
||||
workspace_id: String,
|
||||
http_method: String,
|
||||
}
|
||||
let duplicate_routes = sqlx::query_as!(
|
||||
DuplicateRoute,
|
||||
r#"
|
||||
SELECT
|
||||
route_path,
|
||||
workspace_id,
|
||||
http_method::TEXT AS "http_method!"
|
||||
FROM
|
||||
http_trigger
|
||||
WHERE
|
||||
workspaced_route IS FALSE
|
||||
AND route_path_key IN (
|
||||
SELECT
|
||||
route_path_key
|
||||
FROM
|
||||
http_trigger
|
||||
WHERE
|
||||
workspaced_route IS FALSE
|
||||
GROUP BY
|
||||
route_path_key, http_method
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
ORDER BY route_path_key
|
||||
"#
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
if !duplicate_routes.is_empty() {
|
||||
tracing::error!(
|
||||
"Cannot disable {} setting as duplicate http routes were found: {:?}",
|
||||
HTTP_ROUTE_WORKSPACED_ROUTE_SETTING,
|
||||
&duplicate_routes
|
||||
);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
details: Vec<DuplicateRoute>,
|
||||
}
|
||||
|
||||
let error_response = ErrorResponse {
|
||||
error: "Duplicate HTTP route paths detected".to_string(),
|
||||
details: duplicate_routes,
|
||||
};
|
||||
|
||||
return Err(error::Error::JsonErr(
|
||||
serde_json::to_value(error_response).unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
@@ -541,6 +609,7 @@ pub async fn get_global_setting(
|
||||
&& key != DISABLE_HUB_SETTING
|
||||
&& key != EMAIL_DOMAIN_SETTING
|
||||
&& key != APP_WORKSPACED_ROUTE_SETTING
|
||||
&& key != HTTP_ROUTE_WORKSPACED_ROUTE_SETTING
|
||||
&& key != WS_BASE_URL_SETTING
|
||||
{
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
@@ -58,12 +58,20 @@ pub const EMAIL_DOMAIN_SETTING: &str = "email_domain";
|
||||
pub const OTEL_SETTING: &str = "otel";
|
||||
pub const OTEL_TRACING_PROXY_SETTING: &str = "otel_tracing_proxy";
|
||||
pub const APP_WORKSPACED_ROUTE_SETTING: &str = "app_workspaced_route";
|
||||
pub const HTTP_ROUTE_WORKSPACED_ROUTE_SETTING: &str = "http_route_workspaced_route";
|
||||
pub const SECRET_BACKEND_SETTING: &str = "secret_backend";
|
||||
pub const MIN_KEEP_ALIVE_VERSION_SETTING: &str = "min_keep_alive_version";
|
||||
pub const GITHUB_ENTERPRISE_APP_SETTING: &str = "github_enterprise_app";
|
||||
pub const INSTANCE_EVENTS_WEBHOOK_SETTING: &str = "instance_events_webhook";
|
||||
pub const WORKSPACE_REGISTRIES_SETTING: &str = "workspace_registries";
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref HTTP_ROUTE_WORKSPACED_ROUTE: Arc<RwLock<bool>> = Arc::new(RwLock::new(false));
|
||||
}
|
||||
|
||||
pub const ENV_SETTINGS: &[&str] = &[
|
||||
"DISABLE_NSJAIL",
|
||||
"MODE",
|
||||
|
||||
@@ -237,6 +237,8 @@ pub struct GlobalSettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub app_workspaced_route: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub http_route_workspaced_route: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_default_maven: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_tags_per_workspace: Option<bool>,
|
||||
|
||||
@@ -8,6 +8,7 @@ use sqlx::PgConnection;
|
||||
use std::collections::HashSet;
|
||||
use windmill_api_auth::ApiAuthed;
|
||||
use windmill_audit::{audit_oss::audit_log, ActionKind};
|
||||
use windmill_common::global_settings::HTTP_ROUTE_WORKSPACED_ROUTE;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, Result},
|
||||
@@ -61,11 +62,12 @@ pub async fn route_path_key_exists(
|
||||
.await?
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
let route_path_key = match workspaced_route {
|
||||
Some(true) => {
|
||||
std::borrow::Cow::Owned(format!("{}/{}", w_id, route_path_key.trim_matches('/')))
|
||||
}
|
||||
_ => std::borrow::Cow::Borrowed(route_path_key),
|
||||
let http_route_workspaced = *HTTP_ROUTE_WORKSPACED_ROUTE.read().await;
|
||||
let effective_workspaced = workspaced_route.unwrap_or(false) || http_route_workspaced;
|
||||
let route_path_key = if effective_workspaced {
|
||||
std::borrow::Cow::Owned(format!("{}/{}", w_id, route_path_key.trim_matches('/')))
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(route_path_key)
|
||||
};
|
||||
|
||||
sqlx::query_scalar!(
|
||||
@@ -146,6 +148,10 @@ pub async fn insert_new_trigger_into_db(
|
||||
) -> Result<()> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let http_route_workspaced = *HTTP_ROUTE_WORKSPACED_ROUTE.read().await;
|
||||
let effective_workspaced =
|
||||
trigger.config.workspaced_route.unwrap_or(false) || http_route_workspaced;
|
||||
|
||||
let request_type = trigger.config.request_type;
|
||||
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
|
||||
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
|
||||
@@ -186,7 +192,7 @@ pub async fn insert_new_trigger_into_db(
|
||||
trigger.base.path,
|
||||
trigger.config.route_path,
|
||||
route_path_key,
|
||||
trigger.config.workspaced_route.unwrap_or(false),
|
||||
effective_workspaced,
|
||||
trigger.config.authentication_resource_path,
|
||||
trigger.config.wrap_body.unwrap_or(false),
|
||||
trigger.config.raw_string.unwrap_or(false),
|
||||
@@ -445,6 +451,10 @@ impl TriggerCrud for HttpTrigger {
|
||||
let route_path_key =
|
||||
check_if_route_exist(db, &trigger.config, workspace_id, Some(path)).await?;
|
||||
|
||||
let http_route_workspaced = *HTTP_ROUTE_WORKSPACED_ROUTE.read().await;
|
||||
let effective_workspaced =
|
||||
trigger.config.workspaced_route.unwrap_or(false) || http_route_workspaced;
|
||||
|
||||
let request_type = trigger.config.request_type;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -481,7 +491,7 @@ impl TriggerCrud for HttpTrigger {
|
||||
"#,
|
||||
route_path,
|
||||
&route_path_key,
|
||||
trigger.config.workspaced_route,
|
||||
Some(effective_workspaced),
|
||||
trigger.config.wrap_body,
|
||||
trigger.config.raw_string,
|
||||
trigger.config.authentication_resource_path,
|
||||
|
||||
@@ -7,6 +7,7 @@ use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use windmill_common::{
|
||||
error::{Error, Result},
|
||||
flows::Retry,
|
||||
global_settings::HTTP_ROUTE_WORKSPACED_ROUTE,
|
||||
utils::ExpiringCacheEntry,
|
||||
worker::CLOUD_HOSTED,
|
||||
DB,
|
||||
@@ -273,13 +274,15 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
|
||||
.await?;
|
||||
|
||||
let mut router = matchit::Router::new();
|
||||
let http_route_workspaced = *HTTP_ROUTE_WORKSPACED_ROUTE.read().await;
|
||||
|
||||
for trigger in triggers {
|
||||
let full_path = if trigger.workspaced_route || *CLOUD_HOSTED {
|
||||
format!("/{}/{}", trigger.workspace_id, trigger.route_path)
|
||||
} else {
|
||||
format!("/{}", trigger.route_path)
|
||||
};
|
||||
let full_path =
|
||||
if trigger.workspaced_route || *CLOUD_HOSTED || http_route_workspaced {
|
||||
format!("/{}/{}", trigger.workspace_id, trigger.route_path)
|
||||
} else {
|
||||
format!("/{}", trigger.route_path)
|
||||
};
|
||||
|
||||
if trigger.is_static_website {
|
||||
router
|
||||
|
||||
@@ -196,6 +196,16 @@ export const settings: Record<string, Setting[]> = {
|
||||
ee_only: '',
|
||||
hideInQuickSetup: true
|
||||
},
|
||||
{
|
||||
label: 'HTTP route workspace prefix',
|
||||
description:
|
||||
'When enabled HTTP routes will be accessible at /api/r/{workspace_id}/{route} instead of /api/r/{route} allowing you to define same route path in different workspaces without conflict',
|
||||
key: 'http_route_workspaced_route',
|
||||
fieldType: 'boolean',
|
||||
storage: 'setting',
|
||||
ee_only: '',
|
||||
hideInQuickSetup: true
|
||||
},
|
||||
{
|
||||
label: 'Audit log retention (days)',
|
||||
key: 'audit_log_retention_days',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { HttpTriggerService } from '$lib/gen'
|
||||
import { HttpTriggerService, SettingService } from '$lib/gen'
|
||||
// import { page } from '$app/state'
|
||||
import { getHttpRoute } from './utils'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
@@ -106,6 +106,26 @@
|
||||
|
||||
let userIsAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
let userCanEditConfig = $derived(userIsAdmin || isDraftOnly) // User can edit config if they are admin or if the trigger is a draft which will not be saved
|
||||
|
||||
let globalHttpWorkspacedRoute = $state(false)
|
||||
|
||||
async function loadGlobalHttpWorkspacedRouteSetting() {
|
||||
try {
|
||||
const setting = await SettingService.getGlobal({ key: 'http_route_workspaced_route' })
|
||||
globalHttpWorkspacedRoute = (setting as boolean) ?? false
|
||||
} catch (error) {
|
||||
globalHttpWorkspacedRoute = false
|
||||
}
|
||||
}
|
||||
|
||||
loadGlobalHttpWorkspacedRouteSetting()
|
||||
|
||||
$effect.pre(() => {
|
||||
if (globalHttpWorkspacedRoute && !workspaced_route) {
|
||||
workspaced_route = true
|
||||
dirtyRoutePath = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -172,13 +192,15 @@
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={workspaced_route}
|
||||
disabled={!can_write || !userCanEditConfig}
|
||||
disabled={!can_write || !userCanEditConfig || globalHttpWorkspacedRoute}
|
||||
on:change={() => {
|
||||
workspaced_route = !workspaced_route
|
||||
dirtyRoutePath = true
|
||||
}}
|
||||
options={{
|
||||
right: 'Prefix with workspace',
|
||||
right: globalHttpWorkspacedRoute
|
||||
? 'Prefix with workspace (enforced by instance setting)'
|
||||
: 'Prefix with workspace',
|
||||
rightTooltip:
|
||||
'Prefixes the route with the workspace ID (e.g., {base_url}/api/r/{workspace_id}/{route}). Note: deploying the HTTP trigger to another workspace updates the route workspace prefix accordingly.',
|
||||
rightDocumentationLink:
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { HttpTriggerService, type EditHttpTrigger, type HttpTrigger, type NewHttpTrigger } from '$lib/gen'
|
||||
import {
|
||||
HttpTriggerService,
|
||||
SettingService,
|
||||
type EditHttpTrigger,
|
||||
type HttpTrigger,
|
||||
type NewHttpTrigger
|
||||
} from '$lib/gen'
|
||||
import { Pen, Save } from 'lucide-svelte'
|
||||
import Button from '../../common/button/Button.svelte'
|
||||
import ToggleButton from '../../common/toggleButton-v2/ToggleButton.svelte'
|
||||
@@ -24,6 +30,19 @@
|
||||
|
||||
let { closeFn }: Props = $props()
|
||||
|
||||
let globalHttpWorkspacedRoute = $state(false)
|
||||
|
||||
async function loadGlobalHttpWorkspacedRouteSetting() {
|
||||
try {
|
||||
const setting = await SettingService.getGlobal({ key: 'http_route_workspaced_route' })
|
||||
globalHttpWorkspacedRoute = (setting as boolean) ?? false
|
||||
} catch {
|
||||
globalHttpWorkspacedRoute = false
|
||||
}
|
||||
}
|
||||
|
||||
loadGlobalHttpWorkspacedRouteSetting()
|
||||
|
||||
let routeEditor: RouteEditor
|
||||
let routesGenerator: Drawer
|
||||
|
||||
@@ -288,7 +307,7 @@
|
||||
<div>
|
||||
<div class="text-primary">
|
||||
{httpTrigger.http_method.toUpperCase()}
|
||||
{isCloudHosted() || httpTrigger.workspaced_route
|
||||
{isCloudHosted() || httpTrigger.workspaced_route || globalHttpWorkspacedRoute
|
||||
? $workspaceStore! + '/' + httpTrigger.route_path
|
||||
: httpTrigger.route_path}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import {
|
||||
HttpTriggerService,
|
||||
SettingService,
|
||||
WorkspaceService,
|
||||
type HttpTrigger,
|
||||
type TriggerMode,
|
||||
@@ -71,6 +72,18 @@
|
||||
let routesGenerator: RoutesGenerator | undefined = $state()
|
||||
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
|
||||
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
|
||||
let globalHttpWorkspacedRoute = $state(false)
|
||||
|
||||
async function loadGlobalHttpWorkspacedRouteSetting() {
|
||||
try {
|
||||
const setting = await SettingService.getGlobal({ key: 'http_route_workspaced_route' })
|
||||
globalHttpWorkspacedRoute = (setting as boolean) ?? false
|
||||
} catch {
|
||||
globalHttpWorkspacedRoute = false
|
||||
}
|
||||
}
|
||||
|
||||
loadGlobalHttpWorkspacedRouteSetting()
|
||||
|
||||
async function getDeployUiSettings() {
|
||||
if (!$enterpriseLicense) {
|
||||
@@ -355,7 +368,7 @@
|
||||
{summary}
|
||||
{:else}
|
||||
{http_method.toUpperCase()}
|
||||
/{isCloudHosted() || workspaced_route
|
||||
/{isCloudHosted() || workspaced_route || globalHttpWorkspacedRoute
|
||||
? workspace_id + '/' + route_path
|
||||
: route_path}
|
||||
{/if}
|
||||
@@ -399,7 +412,12 @@
|
||||
<Button
|
||||
on:click={() =>
|
||||
copyToClipboard(
|
||||
getHttpRoute('r', route_path, workspaced_route ?? false, workspace_id)
|
||||
getHttpRoute(
|
||||
'r',
|
||||
route_path,
|
||||
(workspaced_route ?? false) || globalHttpWorkspacedRoute,
|
||||
workspace_id
|
||||
)
|
||||
)}
|
||||
variant="subtle"
|
||||
unifiedSize="md"
|
||||
|
||||
Reference in New Issue
Block a user