From 43a89ee1ccbeeac11211277f8005d85541c71dc3 Mon Sep 17 00:00:00 2001 From: Faton Ramadani Date: Mon, 5 Aug 2024 11:06:42 +0200 Subject: [PATCH 1/8] fix(frontend): fix style panel for compoentn without custom css (#4182) --- frontend/src/lib/components/apps/editor/appUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/apps/editor/appUtils.ts b/frontend/src/lib/components/apps/editor/appUtils.ts index e9330b5ae0..a18d5a7eca 100644 --- a/frontend/src/lib/components/apps/editor/appUtils.ts +++ b/frontend/src/lib/components/apps/editor/appUtils.ts @@ -366,7 +366,10 @@ export function appComponentFromType( tabs: init.tabs, conditions: init.conditions, nodes: init.nodes, - customCss: deepMergeWithPriority(ccomponents[type].customCss as any, override?.customCss), + customCss: deepMergeWithPriority( + ccomponents[type].customCss as any, + override?.customCss ?? {} + ), recomputeIds: init.recomputeIds ? [] : undefined, actionButtons: init.actionButtons ? [] : undefined, actions: [], From cc111ba7dcd7fe6c4280d394d4eb0e723e9d8b19 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 5 Aug 2024 12:46:01 +0200 Subject: [PATCH 2/8] feat: improve indices of completed_runs for faster load --- backend/windmill-api/src/db.rs | 137 +++++++++++++++++- .../src/lib/components/runs/JobLoader.svelte | 13 +- 2 files changed, 140 insertions(+), 10 deletions(-) diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index 70a9413f87..ffbb018122 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -327,13 +327,13 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> { .unwrap_or(false); if !has_done_migration { - sqlx::query( - "CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_completed_job_workspace_id_created_at_new_2 ON completed_job (workspace_id, job_kind, success, is_skipped, is_flow_step, created_at DESC)" - ).execute(db).await?; + // sqlx::query( + // "CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_completed_job_workspace_id_created_at_new_2 ON completed_job (workspace_id, job_kind, success, is_skipped, is_flow_step, created_at DESC)" + // ).execute(db).await?; - sqlx::query( - "CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_completed_job_workspace_id_started_at_new ON completed_job (workspace_id, job_kind, success, is_skipped, is_flow_step, started_at DESC)" - ).execute(db).await?; + // sqlx::query( + // "CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_completed_job_workspace_id_started_at_new ON completed_job (workspace_id, job_kind, success, is_skipped, is_flow_step, started_at DESC)" + // ).execute(db).await?; sqlx::query( "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at", @@ -436,6 +436,131 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> { tracing::info!("released lock for {migration_job_name}"); } + let migration_job_name = "fix_job_completed_index_4"; + let has_done_migration = sqlx::query_scalar!( + "SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = $1)", + migration_job_name + ) + .fetch_one(db) + .await? + .unwrap_or(false); + if !has_done_migration { + tracing::info!("Applying {migration_job_name} migration"); + let mut tx = db.begin().await?; + let mut r = false; + while !r { + r = sqlx::query_scalar!("SELECT pg_try_advisory_lock(4242)") + .fetch_one(&mut *tx) + .await + .map_err(|e| { + tracing::error!("Error acquiring {migration_job_name} lock: {e:#}"); + sqlx::migrate::MigrateError::Execute(e) + })? + .unwrap_or(false); + if !r { + tracing::info!("PG {migration_job_name} lock already acquired by another server or worker, retrying in 5s. (look for the advisory lock in pg_lock with granted = true)"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } + tracing::info!("acquired lock for {migration_job_name}"); + + let has_done_migration = sqlx::query_scalar!( + "SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = $1)", + migration_job_name + ) + .fetch_one(db) + .await? + .unwrap_or(false); + + if !has_done_migration { + let mut i = 1; + tracing::info!("step {i} of {migration_job_name} migration"); + sqlx::query("create index concurrently if not exists ix_completed_job_workspace_id_created_at_new_3 ON completed_job (workspace_id, created_at DESC)") + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("create index concurrently if not exists ix_completed_job_workspace_id_created_at_new_8 ON completed_job (workspace_id, created_at DESC) where job_kind in ('deploymentcallback') AND parent_job IS NULL") + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("create index concurrently if not exists ix_completed_job_workspace_id_created_at_new_9 ON completed_job (workspace_id, created_at DESC) where job_kind in ('dependencies', 'flowdependencies', 'appdependencies') AND parent_job IS NULL") + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("create index concurrently if not exists ix_completed_job_workspace_id_created_at_new_5 ON completed_job (workspace_id, created_at DESC) where job_kind in ('preview', 'flowpreview') AND parent_job IS NULL") + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("create index concurrently if not exists ix_completed_job_workspace_id_created_at_new_6 ON completed_job (workspace_id, created_at DESC) where job_kind in ('script', 'flow') AND parent_job IS NULL") + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("create index concurrently if not exists ix_completed_job_workspace_id_created_at_new_7 ON completed_job (workspace_id, success, created_at DESC) where job_kind in ('script', 'flow') AND parent_job IS NULL") + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("create index concurrently if not exists ix_completed_job_workspace_id_started_at_new_2 ON completed_job (workspace_id, started_at DESC)") + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("create index concurrently if not exists root_job_index_by_path_2 ON completed_job (workspace_id, script_path, created_at desc) WHERE parent_job IS NULL") + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query( + "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_2", + ) + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query( + "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_started_at_new", + ) + .execute(db) + .await?; + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("DROP INDEX CONCURRENTLY IF EXISTS root_job_index_by_path") + .execute(db) + .await?; + + sqlx::query!( + "INSERT INTO windmill_migrations (name) VALUES ($1) ON CONFLICT DO NOTHING", + migration_job_name + ) + .execute(&mut *tx) + .await?; + tracing::info!("Finished applying {migration_job_name} migration"); + } else { + tracing::info!("migration {migration_job_name} already done"); + } + + let _ = sqlx::query("SELECT pg_advisory_unlock(4242)") + .execute(&mut *tx) + .await?; + tx.commit().await?; + tracing::info!("released lock for {migration_job_name}"); + } + Ok(()) } diff --git a/frontend/src/lib/components/runs/JobLoader.svelte b/frontend/src/lib/components/runs/JobLoader.svelte index 57364e0153..dc1cf7d228 100644 --- a/frontend/src/lib/components/runs/JobLoader.svelte +++ b/frontend/src/lib/components/runs/JobLoader.svelte @@ -122,21 +122,26 @@ ): Promise { loadingFetch = true try { + let scriptPathStart = folder === null || folder === '' ? undefined : `f/${folder}/` + let scriptPathExact = path === null || path === '' ? undefined : path return JobService.listJobs({ workspace: $workspaceStore!, createdOrStartedBefore: startedBefore, createdOrStartedAfter: startedAfter, createdOrStartedAfterCompletedJobs: startedAfterCompletedJobs, schedulePath, - scriptPathExact: path === null || path === '' ? undefined : path, + scriptPathExact, createdBy: user === null || user === '' ? undefined : user, - scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`, + scriptPathStart: scriptPathStart, jobKinds, success: success == 'success' ? true : success == 'failure' ? false : undefined, running: success == 'running' ? true : undefined, isSkipped: isSkipped ? undefined : false, - isFlowStep: jobKindsCat != 'all' ? false : undefined, - hasNullParent: jobKindsCat != 'all' ? false : undefined, + // isFlowStep: jobKindsCat != 'all' ? false : undefined, + hasNullParent: + scriptPathExact != undefined || scriptPathStart != undefined || jobKinds != 'all' + ? true + : undefined, label: label === null || label === '' ? undefined : label, isNotSchedule: showSchedules == false ? true : undefined, scheduledForBeforeNow: showFutureJobs == false ? true : undefined, From d771e38bfd00b956d36bc9fe7eb168c72a7f8a94 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 5 Aug 2024 13:12:02 +0200 Subject: [PATCH 3/8] add global created_at index --- backend/windmill-api/src/db.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index ffbb018122..dc8a37149f 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -520,6 +520,14 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> { sqlx::query("create index concurrently if not exists root_job_index_by_path_2 ON completed_job (workspace_id, script_path, created_at desc) WHERE parent_job IS NULL") .execute(db) .await?; + + i += 1; + tracing::info!("step {i} of {migration_job_name} migration"); + + sqlx::query("create index concurrently if not exists ix_completed_job_created_at ON completed_job (created_at DESC)") + .execute(db) + .await?; + i += 1; tracing::info!("step {i} of {migration_job_name} migration"); From 704e75e893a94e71091c840aa5608837c0617526 Mon Sep 17 00:00:00 2001 From: Faton Ramadani Date: Mon, 5 Aug 2024 13:20:56 +0200 Subject: [PATCH 4/8] fix(frontend): fix tutorial for apps with the new topbar (#4186) --- .../tutorials/app/AppTutorial.svelte | 6 +- .../src/lib/components/tutorials/utils.ts | 172 +++++++++++++++++- 2 files changed, 174 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/tutorials/app/AppTutorial.svelte b/frontend/src/lib/components/tutorials/app/AppTutorial.svelte index a4b6fa9313..f824df0c80 100644 --- a/frontend/src/lib/components/tutorials/app/AppTutorial.svelte +++ b/frontend/src/lib/components/tutorials/app/AppTutorial.svelte @@ -84,7 +84,7 @@ description: 'Click on a component to add it to your app. Here we will add a display component.', onNextClick: () => { - if (!$selectedComponent?.includes('b')) { + if (!$selectedComponent?.includes('e')) { addComponent('displaycomponent') } @@ -216,7 +216,7 @@ }, { - element: '#connect-output-a', + element: '#connect-output-d', popover: { title: 'Select the output', description: ' ', @@ -265,7 +265,7 @@ description: 'We can now type in the text input and see the result in the display component', onNextClick: () => { - connectInlineRunnableInputToComponentOutput($app, 'b', 'x', 'a', 'result') + connectInlineRunnableInputToComponentOutput($app, 'e', 'x', 'd', 'result', 'integer') $app = $app diff --git a/frontend/src/lib/components/tutorials/utils.ts b/frontend/src/lib/components/tutorials/utils.ts index d9ced283ae..73d9ccb6a0 100644 --- a/frontend/src/lib/components/tutorials/utils.ts +++ b/frontend/src/lib/components/tutorials/utils.ts @@ -1,4 +1,5 @@ import type { FlowModule, OpenFlow } from '$lib/gen' +import { deepEqual } from 'fast-equals' import { findGridItem } from '../apps/editor/appUtils' import type { App } from '../apps/types' @@ -61,8 +62,177 @@ export function isFlowTainted(flow: OpenFlow) { ) } +const emptyApp = { + grid: [ + { + '3': { + fixed: false, + x: 0, + y: 0, + fullHeight: false, + w: 6, + h: 2 + }, + '12': { + fixed: false, + x: 0, + y: 0, + fullHeight: false, + w: 12, + h: 2 + }, + data: { + type: 'containercomponent', + configuration: {}, + customCss: { + container: { + class: '!p-0', + style: '' + } + }, + actions: [], + numberOfSubgrids: 1, + id: 'a' + }, + id: 'a' + } + ], + fullscreen: false, + unusedInlineScripts: [], + hiddenInlineScripts: [], + theme: { + type: 'path', + path: 'f/app_themes/theme_0' + }, + subgrids: { + 'a-0': [ + { + '3': { + fixed: false, + x: 0, + y: 0, + fullHeight: false, + w: 6, + h: 1 + }, + '12': { + fixed: false, + x: 0, + y: 0, + fullHeight: false, + w: 6, + h: 1 + }, + data: { + type: 'textcomponent', + configuration: { + style: { + type: 'static', + value: 'Body' + }, + copyButton: { + type: 'static', + value: false + }, + tooltip: { + type: 'evalv2', + value: '', + fieldType: 'text', + expr: '`Author: ${ctx.author}`', + connections: [ + { + componentId: 'ctx', + id: 'author' + } + ] + }, + disableNoText: { + type: 'static', + value: true, + fieldType: 'boolean' + } + }, + componentInput: { + type: 'templatev2', + fieldType: 'template', + eval: '${ctx.summary}', + connections: [ + { + id: 'summary', + componentId: 'ctx' + } + ] + }, + customCss: { + text: { + class: 'text-xl font-semibold whitespace-nowrap truncate', + style: '' + }, + container: { + class: '', + style: '' + } + }, + actions: [], + horizontalAlignment: 'left', + verticalAlignment: 'center', + id: 'b' + }, + id: 'b' + }, + { + '3': { + fixed: false, + x: 0, + y: 1, + fullHeight: false, + w: 3, + h: 1 + }, + '12': { + fixed: false, + x: 6, + y: 0, + fullHeight: false, + w: 6, + h: 1 + }, + data: { + type: 'recomputeallcomponent', + configuration: {}, + actions: [], + menuItems: [], + horizontalAlignment: 'right', + verticalAlignment: 'center', + id: 'c' + }, + id: 'c' + } + ] + }, + hideLegacyTopBar: true, + norefreshbar: false +} + export function isAppTainted(app: App) { - return !(app.grid.length === 0 && app.hiddenInlineScripts?.length === 0) + if (app.hideLegacyTopBar === true) { + // An empty app should have only have a topbar and no hidden inline scripts + + if (Array.isArray(app.hiddenInlineScripts) && app.hiddenInlineScripts?.length > 0) { + return true + } + + // New apps have only a single component which is the topbar + if (Array.isArray(app.grid) && app.grid.length > 1) { + return true + } + + // Check if the current app is different from an empty app + return !deepEqual(app, emptyApp) + } else { + // For older apps, + return !(app.grid.length === 0 && app.hiddenInlineScripts?.length === 0) + } } export function updateFlowModuleById( From f1fa6bca8477bb16fa1b6915982648590a289227 Mon Sep 17 00:00:00 2001 From: Henri Courdent <122811744+hcourdent@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:21:14 +0200 Subject: [PATCH 5/8] Changelog add native runtime support of npm packages (#4185) --- frontend/src/lib/components/sidebar/changelogs.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/lib/components/sidebar/changelogs.ts b/frontend/src/lib/components/sidebar/changelogs.ts index 432de3177f..895fbc79ed 100644 --- a/frontend/src/lib/components/sidebar/changelogs.ts +++ b/frontend/src/lib/components/sidebar/changelogs.ts @@ -5,6 +5,11 @@ export type Changelog = { } const changelogs: Changelog[] = [ + { + label: 'Nativets runtime supports npm packages and relative imports', + href: 'https://www.windmill.dev/changelog/native-runtime-imports', + date: '2024-07-29' + }, { label: 'App bar as components', href: 'https://www.windmill.dev/changelog/app-bar-components', From f2f8bbe1d4364540854fef3bf11df5bb4252c17d Mon Sep 17 00:00:00 2001 From: wendrul <53628737+wendrul@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:27:13 +0200 Subject: [PATCH 6/8] feat: deployment UI filter deployable items (#4183) * Add column to workspace settings table * Add endpoint to change the deploy UI settings * Make frontend page to edit UI settings * Prepare sqlx * Add deployment restrictions to frontend * Fix function name * Change dependency to minimatch, CE compatibility * Remove default include_path * Remove picomatch types (old dep) * Use empty list instead of globstar as default * All deployable if config is null --- ...c61296a3ff7489ae12f52a19f9543173ac597.json | 6 + ...76be8aaa74ec3d32596c76a2041769478e61e.json | 14 ++ ...dcc40f463cbc52d94ed9315cf9a547d4c89f2.json | 6 + ...7f5033b9c9afc344d9c3e385ba20a3ad2197a.json | 2 +- ...0cb549a34b96554ae1872355b90304f5dcb76.json | 4 +- ...f47a3b108bbf61c9f4afd364ed869deac75a5.json | 15 ++ .../20240804220655_deploy_ui_config.down.sql | 2 + .../20240804220655_deploy_ui_config.up.sql | 2 + backend/windmill-api/openapi.yaml | 47 ++++ backend/windmill-api/src/workspaces.rs | 76 ++++++- backend/windmill-common/src/workspaces.rs | 6 + frontend/package-lock.json | 203 +++++++++++++++--- frontend/package.json | 1 + .../src/lib/components/DeployToSetting.svelte | 180 +++++++++++++++- .../lib/components/common/table/AppRow.svelte | 30 ++- .../components/common/table/FlowRow.svelte | 27 ++- .../components/common/table/RawAppRow.svelte | 22 +- .../components/common/table/ScriptRow.svelte | 32 ++- frontend/src/lib/components/home/Item.svelte | 20 ++ frontend/src/lib/utils.ts | 44 +++- .../(logged)/flows/get/[...path]/+page.svelte | 44 +++- .../(root)/(logged)/resources/+page.svelte | 38 +++- .../scripts/get/[...hash]/+page.svelte | 50 +++-- .../(root)/(logged)/variables/+page.svelte | 36 +++- .../(logged)/workspace_settings/+page.svelte | 30 ++- 25 files changed, 809 insertions(+), 128 deletions(-) create mode 100644 backend/.sqlx/query-26b3e1f531909a2d841cfed49bb76be8aaa74ec3d32596c76a2041769478e61e.json create mode 100644 backend/.sqlx/query-c3364a5cd3fb7d43a6a6f484009f47a3b108bbf61c9f4afd364ed869deac75a5.json create mode 100644 backend/migrations/20240804220655_deploy_ui_config.down.sql create mode 100644 backend/migrations/20240804220655_deploy_ui_config.up.sql diff --git a/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json b/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json index 93d5dbac1e..16a701fdff 100644 --- a/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json +++ b/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json @@ -112,6 +112,11 @@ "ordinal": 21, "name": "default_scripts", "type_info": "Jsonb" + }, + { + "ordinal": 22, + "name": "deploy_ui", + "type_info": "Jsonb" } ], "parameters": { @@ -141,6 +146,7 @@ true, true, false, + true, true ] }, diff --git a/backend/.sqlx/query-26b3e1f531909a2d841cfed49bb76be8aaa74ec3d32596c76a2041769478e61e.json b/backend/.sqlx/query-26b3e1f531909a2d841cfed49bb76be8aaa74ec3d32596c76a2041769478e61e.json new file mode 100644 index 0000000000..64d01d5058 --- /dev/null +++ b/backend/.sqlx/query-26b3e1f531909a2d841cfed49bb76be8aaa74ec3d32596c76a2041769478e61e.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace_settings SET deploy_ui = NULL WHERE workspace_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "26b3e1f531909a2d841cfed49bb76be8aaa74ec3d32596c76a2041769478e61e" +} diff --git a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json index 0e29ef4745..5f4f137cd9 100644 --- a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json +++ b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json @@ -112,6 +112,11 @@ "ordinal": 21, "name": "default_scripts", "type_info": "Jsonb" + }, + { + "ordinal": 22, + "name": "deploy_ui", + "type_info": "Jsonb" } ], "parameters": { @@ -141,6 +146,7 @@ true, true, false, + true, true ] }, diff --git a/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json b/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json index 75b8108281..1fa370e682 100644 --- a/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json +++ b/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json @@ -5,7 +5,7 @@ "columns": [ { "ordinal": 0, - "name": "?column?", + "name": "bool", "type_info": "Bool" } ], diff --git a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json index 99269c9851..54e94cfb8f 100644 --- a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json +++ b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json @@ -18,8 +18,8 @@ "Left": [] }, "nullable": [ - true, - false + false, + true ] }, "hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76" diff --git a/backend/.sqlx/query-c3364a5cd3fb7d43a6a6f484009f47a3b108bbf61c9f4afd364ed869deac75a5.json b/backend/.sqlx/query-c3364a5cd3fb7d43a6a6f484009f47a3b108bbf61c9f4afd364ed869deac75a5.json new file mode 100644 index 0000000000..1b49e9e986 --- /dev/null +++ b/backend/.sqlx/query-c3364a5cd3fb7d43a6a6f484009f47a3b108bbf61c9f4afd364ed869deac75a5.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace_settings SET deploy_ui = $1 WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Jsonb", + "Text" + ] + }, + "nullable": [] + }, + "hash": "c3364a5cd3fb7d43a6a6f484009f47a3b108bbf61c9f4afd364ed869deac75a5" +} diff --git a/backend/migrations/20240804220655_deploy_ui_config.down.sql b/backend/migrations/20240804220655_deploy_ui_config.down.sql new file mode 100644 index 0000000000..92c6564ae1 --- /dev/null +++ b/backend/migrations/20240804220655_deploy_ui_config.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER TABLE workspace_settings DROP COLUMN deploy_ui; diff --git a/backend/migrations/20240804220655_deploy_ui_config.up.sql b/backend/migrations/20240804220655_deploy_ui_config.up.sql new file mode 100644 index 0000000000..6c59270932 --- /dev/null +++ b/backend/migrations/20240804220655_deploy_ui_config.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TABLE workspace_settings ADD COLUMN IF NOT EXISTS deploy_ui JSONB; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 49a5a5c4c9..bad9db1281 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1507,6 +1507,8 @@ paths: $ref: "#/components/schemas/LargeFileStorage" git_sync: $ref: "#/components/schemas/WorkspaceGitSyncSettings" + deploy_ui: + $ref: "#/components/schemas/WorkspaceDeployUISettings" default_app: type: string default_scripts: @@ -1896,6 +1898,32 @@ paths: application/json: schema: {} + /w/{workspace}/workspaces/edit_deploy_ui_config: + post: + summary: edit workspace deploy ui settings + operationId: editWorkspaceDeployUISettings + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: Workspace deploy UI settings + required: true + content: + application/json: + schema: + type: object + properties: + deploy_ui_settings: + $ref: "#/components/schemas/WorkspaceDeployUISettings" + + responses: + "200": + description: status + content: + application/json: + schema: {} + /w/{workspace}/workspaces/edit_default_app: post: summary: edit default app for workspace @@ -11078,6 +11106,25 @@ components: items: $ref: "#/components/schemas/GitRepositorySettings" + WorkspaceDeployUISettings: + type: object + properties: + include_path: + type: array + items: + type: string + include_type: + type: array + items: + type: string + enum: + - script + - flow + - app + - resource + - variable + - secret + WorkspaceDefaultScripts: type: object properties: diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index b3ae17a1c7..08509680cb 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -42,7 +42,7 @@ use windmill_common::schedule::Schedule; use windmill_common::users::username_to_permissioned_as; use windmill_common::variables::build_crypt; use windmill_common::worker::{to_raw_value, CLOUD_HOSTED}; -use windmill_common::workspaces::WorkspaceGitSyncSettings; +use windmill_common::workspaces::{WorkspaceDeploymentUISettings, WorkspaceGitSyncSettings}; use windmill_common::{ error::{to_anyhow, Error, JsonResult, Result}, flows::Flow, @@ -97,6 +97,7 @@ pub fn workspaced_service() -> Router { post(edit_large_file_storage_config), ) .route("/edit_git_sync_config", post(edit_git_sync_config)) + .route("/edit_deploy_ui_config", post(edit_deploy_ui_config)) .route("/edit_default_app", post(edit_default_app)) .route("/default_app", get(get_default_app)) .route( @@ -167,8 +168,9 @@ pub struct WorkspaceSettings { pub error_handler: Option, pub error_handler_extra_args: Option, pub error_handler_muted_on_cancel: Option, - pub large_file_storage: Option, // effectively: DatasetsStorage - pub git_sync: Option, // effectively: WorkspaceGitSyncSettings + pub large_file_storage: Option, // effectively: DatasetsStorage + pub git_sync: Option, // effectively: WorkspaceGitSyncSettings + pub deploy_ui: Option, // effectively: WorkspaceDeploymentUISettings pub default_app: Option, pub automatic_billing: bool, pub default_scripts: Option, @@ -1052,6 +1054,74 @@ async fn edit_git_sync_config( Ok(format!("Edit git sync config for workspace {}", &w_id)) } +#[derive(Deserialize)] +struct EditDeployUIConfig { + deploy_ui_settings: Option, +} + +#[cfg(not(feature = "enterprise"))] +async fn edit_deploy_ui_config( + _authed: ApiAuthed, + Extension(_db): Extension, + Path(_w_id): Path, + Json(_new_config): Json, +) -> Result { + return Err(Error::BadRequest( + "Deployment UI is only available on Windmill Enterprise Edition".to_string(), + )); +} + + +#[cfg(feature = "enterprise")] +async fn edit_deploy_ui_config( + authed: ApiAuthed, + Extension(db): Extension, + Path(w_id): Path, + ApiAuthed { is_admin, username, .. }: ApiAuthed, + Json(new_config): Json, +) -> Result { + require_admin(is_admin, &username)?; + + let mut tx = db.begin().await?; + + let args_for_audit = format!("{:?}", new_config.deploy_ui_settings); + audit_log( + &mut *tx, + &authed, + "workspaces.edit_deploy_ui_config", + ActionKind::Update, + &w_id, + Some(&authed.email), + Some([("deployment_ui_settings", args_for_audit.as_str())].into()), + ) + .await?; + + if let Some(deploy_ui_settings) = new_config.deploy_ui_settings { + let serialized_config = serde_json::to_value::(deploy_ui_settings) + .map_err(|err| Error::InternalErr(err.to_string()))?; + + sqlx::query!( + "UPDATE workspace_settings SET deploy_ui = $1 WHERE workspace_id = $2", + serialized_config, + &w_id + ) + .execute(&mut *tx) + .await?; + } else { + sqlx::query!( + "UPDATE workspace_settings SET deploy_ui = NULL WHERE workspace_id = $1", + &w_id, + ) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; + + Ok(format!("Edit deployment UI config for workspace {}", &w_id)) +} + + + #[derive(Deserialize)] pub struct EditDefaultApp { pub default_app_path: Option, diff --git a/backend/windmill-common/src/workspaces.rs b/backend/windmill-common/src/workspaces.rs index 756b920bf4..d674285479 100644 --- a/backend/windmill-common/src/workspaces.rs +++ b/backend/windmill-common/src/workspaces.rs @@ -7,6 +7,12 @@ pub struct WorkspaceGitSyncSettings { pub repositories: Vec, } +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct WorkspaceDeploymentUISettings { + pub include_path: Vec, + pub include_type: Vec, +} + #[derive(Serialize, Deserialize, PartialEq, Debug)] #[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))] pub enum ObjectType { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 381cdfb06b..ece4e299d1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,7 @@ "hash-sum": "^2.0.0", "highlight.js": "^11.8.0", "lucide-svelte": "^0.293.0", + "minimatch": "^10.0.1", "monaco-editor": "npm:@codingame/monaco-editor-treemended@>=1.83.5 <1.84.0", "monaco-graphql": "^1.5.1", "monaco-languageclient": "~7.0.1", @@ -935,6 +936,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", @@ -1026,6 +1049,28 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1289,6 +1334,18 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz", @@ -2386,6 +2443,18 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -2560,13 +2629,11 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "devOptional": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -4061,6 +4128,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -4098,6 +4175,18 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/esm-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", @@ -4607,6 +4696,28 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -6535,6 +6646,18 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -6597,15 +6720,17 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -7349,18 +7474,6 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/picomatch-browser": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/picomatch-browser/-/picomatch-browser-2.2.6.tgz", @@ -8298,6 +8411,18 @@ "node": ">=8.10.0" } }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/redent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", @@ -9104,6 +9229,16 @@ "node": ">=8" } }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/sucrase/node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -9133,6 +9268,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10159,14 +10306,6 @@ "vscode": "^1.82.0" } }, - "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/vscode-languageclient/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 99e707e464..541318d95f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -114,6 +114,7 @@ "hash-sum": "^2.0.0", "highlight.js": "^11.8.0", "lucide-svelte": "^0.293.0", + "minimatch": "^10.0.1", "monaco-editor": "npm:@codingame/monaco-editor-treemended@>=1.83.5 <1.84.0", "monaco-graphql": "^1.5.1", "monaco-languageclient": "~7.0.1", diff --git a/frontend/src/lib/components/DeployToSetting.svelte b/frontend/src/lib/components/DeployToSetting.svelte index 3fe5081023..462ed6a67c 100644 --- a/frontend/src/lib/components/DeployToSetting.svelte +++ b/frontend/src/lib/components/DeployToSetting.svelte @@ -1,13 +1,88 @@

Workspace to link to

@@ -36,3 +111,104 @@ {/each} +

Deployable items

+
+
+ {#if Array.isArray(deployUiSettings?.include_path)} +

Filter on path + Only scripts, flows and apps with their path matching one of those filters will be allowed + to be deployed in the deploy UI. The filters allow '*'' and '**' characters, with '*'' + matching any character allowed in paths until the next slash (/) and '**' matching + anything including slashes. +

+ {#each deployUiSettings.include_path ?? [] as regexpPath, idx} +
+ + +
+ {/each} + {/if} +
+ +
+
+ +
+

Filter on type + You can filter which types of item can be deployed to the production workspace. By default + everything is deployable. +

+
+ + + + +
+ { + if (!ev.detail) { + deployUiSettings.include_type.secrets = false + } + }} + options={{ right: 'Variables ' }} + /> + - + +
+
+
+
+{#if $enterpriseLicense} +
+ +
+{/if} diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index f52b7fdae6..93afe04815 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -4,7 +4,12 @@ import type MoveDrawer from '$lib/components/MoveDrawer.svelte' import SharedBadge from '$lib/components/SharedBadge.svelte' import type ShareModal from '$lib/components/ShareModal.svelte' - import { AppService, DraftService, type ListableApp } from '$lib/gen' + import { + AppService, + DraftService, + type ListableApp, + type WorkspaceDeployUISettings + } from '$lib/gen' import { userStore, workspaceStore } from '$lib/stores' import { createEventDispatcher } from 'svelte' import Button from '../button/Button.svelte' @@ -28,7 +33,7 @@ import { goto as gotoUrl } from '$app/navigation' import { page } from '$app/stores' import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' - import { DELETE, copyToClipboard } from '$lib/utils' + import { DELETE, copyToClipboard, isDeployable } from '$lib/utils' import AppDeploymentHistory from '$lib/components/apps/editor/AppDeploymentHistory.svelte' import AppJsonEditor from '$lib/components/apps/editor/AppJsonEditor.svelte' @@ -41,6 +46,7 @@ export let deleteConfirmedCallback: (() => void) | undefined export let depth: number = 0 export let menuOpen: boolean = false + export let deployUiSettings: WorkspaceDeployUISettings | undefined = undefined const dispatch = createEventDispatcher() @@ -163,14 +169,18 @@ disabled: !canWrite, hide: $userStore?.operator }, - { - displayName: 'Deploy to staging/prod', - icon: Globe, - action: () => { - deploymentDrawer.openDrawer(path, 'app') - }, - hide: $userStore?.operator - }, + ...(isDeployable('app', path, deployUiSettings) + ? [ + { + displayName: 'Deploy to staging/prod', + icon: Globe, + action: () => { + deploymentDrawer.openDrawer(path, 'app') + }, + hide: $userStore?.operator + } + ] + : []), { displayName: $userStore?.operator ? 'View JSON' : 'View/Edit JSON', icon: FileJson, diff --git a/frontend/src/lib/components/common/table/FlowRow.svelte b/frontend/src/lib/components/common/table/FlowRow.svelte index c667b8bcff..ab73ba060e 100644 --- a/frontend/src/lib/components/common/table/FlowRow.svelte +++ b/frontend/src/lib/components/common/table/FlowRow.svelte @@ -6,7 +6,7 @@ import ScheduleEditor from '$lib/components/ScheduleEditor.svelte' import SharedBadge from '$lib/components/SharedBadge.svelte' import type ShareModal from '$lib/components/ShareModal.svelte' - import { FlowService, type Flow, DraftService } from '$lib/gen' + import { FlowService, type Flow, DraftService, type WorkspaceDeployUISettings } from '$lib/gen' import { userStore, workspaceStore } from '$lib/stores' import { createEventDispatcher } from 'svelte' import Badge from '../badge/Badge.svelte' @@ -14,7 +14,7 @@ import Row from './Row.svelte' import DraftBadge from '$lib/components/DraftBadge.svelte' import { sendUserToast } from '$lib/toast' - import { DELETE, copyToClipboard, isOwner } from '$lib/utils' + import { DELETE, copyToClipboard, isDeployable, isOwner } from '$lib/utils' import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' import { Pen, @@ -42,6 +42,7 @@ export let errorHandlerMuted: boolean export let depth: number = 0 export let menuOpen: boolean = false + export let deployUiSettings: WorkspaceDeployUISettings | undefined = undefined const dispatch = createEventDispatcher() @@ -190,15 +191,19 @@ copyToClipboard(path) } }, - { - displayName: 'Deploy to staging/prod', - icon: Globe, - action: () => { - deploymentDrawer.openDrawer(path, 'flow') - }, - disabled: archived, - hide: $userStore?.operator - }, + ...(isDeployable('flow', path, deployUiSettings) + ? [ + { + displayName: 'Deploy to staging/prod', + icon: Globe, + action: () => { + deploymentDrawer.openDrawer(path, 'flow') + }, + disabled: archived, + hide: $userStore?.operator + } + ] + : []), { displayName: 'Deployments', icon: HistoryIcon, diff --git a/frontend/src/lib/components/common/table/RawAppRow.svelte b/frontend/src/lib/components/common/table/RawAppRow.svelte index a7751c36eb..615de50e64 100644 --- a/frontend/src/lib/components/common/table/RawAppRow.svelte +++ b/frontend/src/lib/components/common/table/RawAppRow.svelte @@ -4,7 +4,7 @@ import type MoveDrawer from '$lib/components/MoveDrawer.svelte' import SharedBadge from '$lib/components/SharedBadge.svelte' import type ShareModal from '$lib/components/ShareModal.svelte' - import { RawAppService, type ListableRawApp } from '$lib/gen' + import { RawAppService, type ListableRawApp, type WorkspaceDeployUISettings } from '$lib/gen' import { userStore, workspaceStore } from '$lib/stores' import { createEventDispatcher } from 'svelte' import Button from '../button/Button.svelte' @@ -15,6 +15,7 @@ import { goto } from '$lib/navigation' import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' import { FileUp, Globe, Pen, Share, Trash } from 'lucide-svelte' + import { isDeployable } from '$lib/utils' export let app: ListableRawApp & { canWrite: boolean } export let marked: string | undefined @@ -25,6 +26,7 @@ export let deploymentDrawer: DeployWorkspaceDrawer export let depth: number = 0 export let menuOpen: boolean = false + export let deployUiSettings: WorkspaceDeployUISettings | undefined = undefined let updateAppDrawer: Drawer @@ -98,13 +100,17 @@ }, disabled: !canWrite }, - { - displayName: 'Deploy to prod/staging', - icon: Globe, - action: () => { - deploymentDrawer.openDrawer(path, 'raw_app') - } - }, + ...(isDeployable('app', path, deployUiSettings) + ? [ + { + displayName: 'Deploy to prod/staging', + icon: Globe, + action: () => { + deploymentDrawer.openDrawer(path, 'raw_app') + } + } + ] + : []), { displayName: canWrite ? 'Share' : 'See Permissions', icon: Share, diff --git a/frontend/src/lib/components/common/table/ScriptRow.svelte b/frontend/src/lib/components/common/table/ScriptRow.svelte index 450836af77..12acd2774c 100644 --- a/frontend/src/lib/components/common/table/ScriptRow.svelte +++ b/frontend/src/lib/components/common/table/ScriptRow.svelte @@ -7,7 +7,12 @@ import SharedBadge from '$lib/components/SharedBadge.svelte' import type ShareModal from '$lib/components/ShareModal.svelte' - import { ScriptService, type Script, DraftService } from '$lib/gen' + import { + ScriptService, + type Script, + DraftService, + type WorkspaceDeployUISettings + } from '$lib/gen' import { userStore, workspaceStore } from '$lib/stores' import { createEventDispatcher } from 'svelte' @@ -16,7 +21,7 @@ import Row from './Row.svelte' import DraftBadge from '$lib/components/DraftBadge.svelte' import { sendUserToast } from '$lib/toast' - import { copyToClipboard, DELETE, isOwner } from '$lib/utils' + import { copyToClipboard, DELETE, isDeployable, isOwner } from '$lib/utils' import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' import { LanguageIcon } from '../languageIcons' import { @@ -49,6 +54,7 @@ export let showCode: (path: string, summary: string) => void export let depth: number = 0 export let menuOpen: boolean = false + export let deployUiSettings: WorkspaceDeployUISettings | undefined = undefined const dispatch = createEventDispatcher() @@ -211,15 +217,19 @@ disabled: !owner || script.archived, hide: $userStore?.operator }, - { - displayName: 'Deploy to staging/prod', - icon: FileUp, - action: () => { - deploymentDrawer.openDrawer(script.path, 'script') - }, - disabled: script.archived, - hide: $userStore?.operator - }, + ...(isDeployable('script', script.path, deployUiSettings) + ? [ + { + displayName: 'Deploy to staging/prod', + icon: FileUp, + action: () => { + deploymentDrawer.openDrawer(script.path, 'script') + }, + disabled: script.archived, + hide: $userStore?.operator + } + ] + : []), { displayName: 'View runs', icon: List, diff --git a/frontend/src/lib/components/home/Item.svelte b/frontend/src/lib/components/home/Item.svelte index 91ef63179d..2e7b3c6967 100644 --- a/frontend/src/lib/components/home/Item.svelte +++ b/frontend/src/lib/components/home/Item.svelte @@ -10,6 +10,9 @@ import ShareModal from '../ShareModal.svelte' import { createEventDispatcher } from 'svelte' import { ArrowBigUp } from 'lucide-svelte' + import { enterpriseLicense, workspaceStore } from '$lib/stores' + import { WorkspaceService, type WorkspaceDeployUISettings } from '$lib/gen' + import { ALL_DEPLOYABLE } from '$lib/utils' export let item export let depth: number = 0 @@ -23,6 +26,19 @@ let menuOpen: boolean = false export let showCode: (path: string, summary: string) => void + + let deployUiSettings: WorkspaceDeployUISettings | undefined = undefined + + async function getDeployUiSettings() { + if (!$enterpriseLicense) { + deployUiSettings = ALL_DEPLOYABLE + return + } + let settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! }) + deployUiSettings = settings.deploy_ui ?? ALL_DEPLOYABLE + } + getDeployUiSettings() + {#if item.type == 'script'} @@ -42,6 +58,7 @@ {depth} bind:menuOpen {showCode} + {deployUiSettings} /> {:else if item.type == 'flow'} {:else if item.type == 'app'} {:else if item.type == 'raw_app'} {/if} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 554301c203..876d60bce2 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -11,9 +11,10 @@ import { deepEqual } from 'fast-equals' import YAML from 'yaml' import type { UserExt } from './stores' import { sendUserToast } from './toast' -import type { Script } from './gen' +import type { Script, WorkspaceDeployUISettings } from './gen' import type { EnumType, SchemaProperty } from './common' import type { Schema } from './common' +import { minimatch } from 'minimatch' export { sendUserToast } export function validateUsername(username: string): string { @@ -494,7 +495,7 @@ export function isObject(obj: any) { export function debounce(func: (...args: any[]) => any, wait: number) { let timeout: any - return function (...args: any[]) { + return function(...args: any[]) { // @ts-ignore const context = this clearTimeout(timeout) @@ -504,7 +505,7 @@ export function debounce(func: (...args: any[]) => any, wait: number) { export function throttle(func: (...args: any[]) => T, wait: number) { let timeout: any - return function (...args: any[]) { + return function(...args: any[]) { if (!timeout) { timeout = setTimeout(() => { timeout = null @@ -720,7 +721,7 @@ export async function tryEvery({ try { await tryCode() break - } catch (err) {} + } catch (err) { } i++ } if (i >= times) { @@ -937,3 +938,38 @@ export function getSchemaFromProperties(properties: { [name: string]: SchemaProp order: Object.keys(properties).filter((k) => k !== 'label') } } + +type DeployUIType = 'script' | 'flow' | 'app' | 'resource' | 'variable' | 'secret' + +export function isDeployable( + type: DeployUIType, + path: string, + deployUiSettings: WorkspaceDeployUISettings | undefined +) { + if (deployUiSettings == undefined) { + return false + } + + if ( + deployUiSettings.include_type != undefined && + !deployUiSettings.include_type.includes(type) + ) { + return false + } + + if ( + deployUiSettings.include_path != undefined && + deployUiSettings.include_path.length != 0 && + deployUiSettings.include_path.every((x) => !minimatch(path, x)) + ) { + return false + } + + return true +} + +export const ALL_DEPLOYABLE: WorkspaceDeployUISettings = { + include_path: [], + include_type: ['script', 'flow', 'app', 'resource', 'variable', 'secret'] +} + diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index 95fad52d5e..76973ddeac 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -1,7 +1,14 @@ { - deploymentDrawer.openDrawer(path, 'resource') - } - }, + ...(isDeployable('resource', path, deployUiSettings) + ? [ + { + displayName: 'Deploy to prod/staging', + icon: FileUp, + action: () => { + deploymentDrawer.openDrawer(path, 'resource') + } + } + ] + : []), { displayName: 'Delete', disabled: !canWrite, diff --git a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte index 87a6009419..bdf1ffca5a 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -1,11 +1,20 @@