From aebae7939707922b014591ac8b09ad04fe23ae61 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Thu, 17 Sep 2026 15:45:22 +0200 Subject: [PATCH] feat(datatables): configure the external instance cluster and pick its databases from the UI Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/components/InstanceSetting.svelte | 11 +- .../lib/components/InstanceSettings.svelte | 6 + .../src/lib/components/instanceSettings.ts | 22 + .../ExternalInstancePgSettings.svelte | 400 ++++++++++++++++++ .../DataTableSettings.svelte | 29 +- .../workspaceSettings/DucklakeSettings.svelte | 41 +- .../ExternalInstanceDbSelect.svelte | 84 ++++ .../workspaceSettings/utils.svelte.ts | 20 +- 8 files changed, 594 insertions(+), 19 deletions(-) create mode 100644 frontend/src/lib/components/instanceSettings/ExternalInstancePgSettings.svelte create mode 100644 frontend/src/lib/components/workspaceSettings/ExternalInstanceDbSelect.svelte diff --git a/frontend/src/lib/components/InstanceSetting.svelte b/frontend/src/lib/components/InstanceSetting.svelte index 1e67652f60..9ad1a2119b 100644 --- a/frontend/src/lib/components/InstanceSetting.svelte +++ b/frontend/src/lib/components/InstanceSetting.svelte @@ -27,6 +27,7 @@ import WebhookBaseUrlSetting from './instanceSettings/WebhookBaseUrlSetting.svelte' import WsConnectivityTest from './instanceSettings/WsConnectivityTest.svelte' import InstanceBannerSetting from './instanceSettings/InstanceBannerSetting.svelte' + import ExternalInstancePgSettings from './instanceSettings/ExternalInstancePgSettings.svelte' import IndexerMemorySettings from './instanceSettings/IndexerMemorySettings.svelte' import IndexerJobIndexSettings from './instanceSettings/IndexerJobIndexSettings.svelte' import IndexerLogIndexSettings from './instanceSettings/IndexerLogIndexSettings.svelte' @@ -781,10 +782,10 @@ />

Comma-separated host/IP patterns the proxy still traces but for which it skips - upstream TLS certificate verification. Use for internal endpoints with - self-signed or otherwise untrusted certificates — unlike NO_PROXY above, these - requests stay traced. Same matching as NO_PROXY (example.com matches - subdomains; .example.com matches subdomains only). + upstream TLS certificate verification. Use for internal endpoints with self-signed + or otherwise untrusted certificates — unlike NO_PROXY above, these requests stay + traced. Same matching as NO_PROXY (example.com matches subdomains; + .example.com matches subdomains only).

@@ -872,6 +873,8 @@ {:else if setting.fieldType == 'instance_banner'} + {:else if setting.fieldType == 'external_instance_pg'} + {/if} {#if hasError} diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index abe37c3537..fe5b498b39 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -730,6 +730,7 @@ secret_backend: ['token'], object_store_cache_config: ['secret_key', 'serviceAccountKey'], custom_instance_pg_databases: ['user_pwd'], + external_instance_pg: ['password'], rsa_keys: ['private_key'], github_enterprise_app: ['private_key'] } @@ -1234,6 +1235,11 @@ description="Configure a self-managed GitHub App for git sync on GitHub.com, GHE Cloud or GitHub Enterprise Server." link="https://www.windmill.dev/docs/integrations/git_repository#self-managed-github-app" /> + {:else if category == 'External Postgres'} + {:else if category == 'DB Health'} = { } ], 'DB Health': [], + 'External Postgres': [ + { + label: 'External instance cluster', + description: + 'A PostgreSQL cluster Windmill administers for data tables and Ducklake catalogs of the External instance type. The admin user needs CREATEDB and CREATEROLE, plus REPLICATION for Postgres triggers. Windmill creates its own roles and databases there and leaves everything else on the cluster alone.', + key: 'external_instance_pg', + fieldType: 'external_instance_pg', + storage: 'setting', + ee_only: 'External instance databases are an Enterprise Edition feature' + } + ], Registries: [ { label: 'Instance Python Version', @@ -1241,6 +1253,14 @@ export const instanceSettingsNavigationGroups = [ aiId: 'instance-settings-object-storage', aiDescription: 'Instance object storage settings', isEE: true + }, + { + id: 'external_postgres', + label: 'External Postgres', + aiId: 'instance-settings-external-postgres', + aiDescription: + 'External PostgreSQL cluster Windmill manages for external instance data tables and Ducklake catalogs', + isEE: true } ] }, @@ -1365,6 +1385,7 @@ export const tabToCategoryMap: Record = { github_enterprise_app: 'GitHub App', websocket: 'WebSocket', db_health: 'DB Health', + external_postgres: 'External Postgres', lsp: 'LSP' } @@ -1401,6 +1422,7 @@ export const categoryToTabMap: Record = { 'GitHub App': 'github_enterprise_app', WebSocket: 'websocket', 'DB Health': 'db_health', + 'External Postgres': 'external_postgres', LSP: 'lsp' } diff --git a/frontend/src/lib/components/instanceSettings/ExternalInstancePgSettings.svelte b/frontend/src/lib/components/instanceSettings/ExternalInstancePgSettings.svelte new file mode 100644 index 0000000000..251b6dcb65 --- /dev/null +++ b/frontend/src/lib/components/instanceSettings/ExternalInstancePgSettings.svelte @@ -0,0 +1,400 @@ + + +
+ {#if !$enterpriseLicense} + + {/if} + +
+
+ + field('host'), (v) => setField('host', v)} + /> +
+
+ + field('port'), (v) => setField('port', v)} + /> +
+
+ + field('user'), (v) => setField('user', v)} + /> +
+
+ + field('password'), (v) => setField('password', v)} + /> +
+
+ + field('dbname'), (v) => setField('dbname', v)} + /> +
+
+ + + +
+ {#if !setUp} + Set the cluster up before creating databases. + {/if} +
+
+ + diff --git a/frontend/src/lib/components/workspaceSettings/DataTableSettings.svelte b/frontend/src/lib/components/workspaceSettings/DataTableSettings.svelte index ebc4bd8a7f..961b048f33 100644 --- a/frontend/src/lib/components/workspaceSettings/DataTableSettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/DataTableSettings.svelte @@ -9,7 +9,7 @@ id: string name: string database: { - resource_type: 'postgresql' | 'instance' + resource_type: 'postgresql' | 'instance' | 'external_instance' resource_path?: string | undefined } /** Set on a fork's entry: it names the workspace whose data table governs this one, and @@ -81,8 +81,10 @@ import { isCustomInstanceDbEnabled, getUnusedInstanceDbName, - isDataTableWizardEnabled + isDataTableWizardEnabled, + externalInstanceDbUnavailableReason } from './utils.svelte' + import ExternalInstanceDbSelect from './ExternalInstanceDbSelect.svelte' import { random_adj } from '../random_positive_adjetive' import { sendUserToast } from '$lib/toast' import { @@ -420,6 +422,13 @@ > Use Windmill's PostgreSQL instance + {:else if dataTable.database.resource_type === 'external_instance'} + + Use a database on the external PostgreSQL cluster set in instance settings + {/if}
- {#if ducklake.catalog.resource_type !== 'instance'} + {#if ducklake.catalog.resource_type === 'postgresql' || ducklake.catalog.resource_type === 'mysql'} + {:else if ducklake.catalog.resource_type === 'external_instance'} + {:else} + import { SettingService, type CustomInstanceDbTag } from '$lib/gen' + import { resource } from 'runed' + import Select from '../select/Select.svelte' + import { safeSelectItems } from '../select/utils.svelte' + import Button from '../common/button/Button.svelte' + import { sendUserToast } from '$lib/toast' + import { isExternalInstanceDbEnabled } from './utils.svelte' + import { Plus } from 'lucide-svelte' + + type Props = { + value: string | undefined + tag: CustomInstanceDbTag + class?: string + } + let { value = $bindable(), tag, class: className }: Props = $props() + + let refreshKey = $state(0) + const databases = resource( + () => refreshKey, + async () => { + try { + return await SettingService.listExternalInstancePgDatabases() + } catch { + return {} + } + } + ) + + // Every database Windmill created is offered, whatever it was created for: the tag only + // sorts the ones made for this kind of storage first. + let items = $derived( + safeSelectItems( + Object.entries(databases.current ?? {}) + .sort(([, a], [, b]) => Number(b.tag === tag) - Number(a.tag === tag)) + .map(([name]) => name) + ) + ) + let exists = $derived(!!value && !!databases.current?.[value]) + let creating = $state(false) + + async function create() { + if (!value) return + creating = true + try { + await SettingService.createExternalInstancePgDatabase({ + name: value, + requestBody: { tag } + }) + sendUserToast(`Created database ${value} on the external cluster`) + } catch (e) { + sendUserToast(e?.body ?? e?.message ?? String(e), true) + } finally { + creating = false + refreshKey++ + } + } + + +
+