feat(datatables): configure the external instance cluster and pick its databases from the UI

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-09-17 15:45:22 +02:00
co-authored by Claude Opus 5
parent b62d8b246a
commit aebae79397
8 changed files with 594 additions and 19 deletions
@@ -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 @@
/>
<p class="text-xs text-tertiary">
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 (<code>example.com</code> matches
subdomains; <code>.example.com</code> 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 (<code>example.com</code> matches subdomains;
<code>.example.com</code> matches subdomains only).
</p>
</div>
<div class="flex flex-col gap-1">
@@ -872,6 +873,8 @@
<WsConnectivityTest {values} />
{:else if setting.fieldType == 'instance_banner'}
<InstanceBannerSetting {values} disabled={loading} />
{:else if setting.fieldType == 'external_instance_pg'}
<ExternalInstancePgSettings {values} disabled={loading} />
{/if}
{#if hasError}
<span class="text-red-600 dark:text-red-400 text-xs">
@@ -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'}
<SettingsPageHeader
title="External Postgres"
description="Store data tables and Ducklake catalogs on a PostgreSQL cluster outside Windmill's own database. Save the connection, set the cluster up, then create databases for workspaces to use."
/>
{:else if category == 'DB Health'}
<SettingsPageHeader
title="DB Health"
@@ -71,6 +71,7 @@ export interface Setting {
| 'ws_connectivity'
| 'retention_overrides'
| 'instance_banner'
| 'external_instance_pg'
storage: SettingStorage
advancedToggle?: {
label: string
@@ -718,6 +719,17 @@ export const settings: Record<string, Setting[]> = {
}
],
'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<string, string> = {
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<string, string> = {
'GitHub App': 'github_enterprise_app',
WebSocket: 'websocket',
'DB Health': 'db_health',
'External Postgres': 'external_postgres',
LSP: 'lsp'
}
@@ -0,0 +1,400 @@
<script lang="ts">
import { Alert, Button } from '$lib/components/common'
import TextInput from '../text_input/TextInput.svelte'
import Password from '../Password.svelte'
import Select from '../select/Select.svelte'
import DataTable from '../table/DataTable.svelte'
import Head from '../table/Head.svelte'
import Row from '../table/Row.svelte'
import Cell from '../table/Cell.svelte'
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
import { createAsyncConfirmationModal } from '../common/confirmationModal/asyncConfirmationModal.svelte'
import {
SettingService,
type CustomInstanceDbTag,
type ExternalInstancePgSetupReport
} from '$lib/gen'
import { instanceSettingsSaved } from '../instanceSettings'
import { enterpriseLicense } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { resource } from 'runed'
import { deepEqual } from 'fast-equals'
import {
CircleCheck,
CircleX,
KeyRound,
Plus,
Trash2,
TriangleAlert,
Wrench
} from 'lucide-svelte'
import type { Writable } from 'svelte/store'
interface Props {
values: Writable<Record<string, any>>
disabled?: boolean
}
let { values, disabled = false }: Props = $props()
const KEY = 'external_instance_pg'
const SSLMODES = ['verify-full', 'verify-ca', 'require', 'prefer', 'disable']
let isDisabled = $derived(disabled || !$enterpriseLicense)
function field(key: string): any {
return $values[KEY]?.[key]
}
// Empty inputs are removed rather than sent: the backend reads every field as optional, and an
// empty string would be a real (and invalid) host, port or sslmode.
function setField(key: string, value: any) {
const next = { ...($values[KEY] ?? {}) }
if (value === '' || value === undefined || value === null) {
delete next[key]
} else {
next[key] = key === 'port' ? Number(value) : value
}
$values[KEY] = Object.keys(next).length > 0 ? next : undefined
}
// Setup reads the saved settings, never the form, so it waits until the form is saved.
const saved = resource(
() => $instanceSettingsSaved,
async () => {
try {
return (await SettingService.getGlobal({ key: KEY })) ?? undefined
} catch {
return undefined
}
}
)
let unsaved = $derived(!saved.loading && !deepEqual(saved.current ?? undefined, $values[KEY]))
let refreshKey = $state(0)
const status = resource(
() => [$instanceSettingsSaved, refreshKey],
async () => {
try {
return await SettingService.getExternalInstancePgStatus()
} catch {
return undefined
}
}
)
const databases = resource(
() => [$instanceSettingsSaved, refreshKey],
async () => {
try {
return await SettingService.listExternalInstancePgDatabases()
} catch {
return {}
}
}
)
let freshReport: ExternalInstancePgSetupReport | undefined = $state(undefined)
let report = $derived(freshReport ?? status.current?.last_setup)
let runningSetup: 'setup' | 'rotate' | undefined = $state(undefined)
const confirmationModal = createAsyncConfirmationModal()
async function runSetup(rotate: boolean) {
if (rotate) {
const ok = await confirmationModal.ask({
title: 'Rotate passwords',
children:
'Windmill generates new passwords for its two roles on the external cluster. Jobs that connect afterwards use the new ones.',
confirmationText: 'Rotate'
})
if (!ok) return
}
runningSetup = rotate ? 'rotate' : 'setup'
try {
freshReport = await SettingService.setupExternalInstancePg({
requestBody: { rotate_passwords: rotate }
})
sendUserToast(
freshReport.success ? 'External cluster is set up' : 'Setup failed, see the report below',
!freshReport.success
)
} catch (e) {
sendUserToast(e?.body ?? e?.message ?? String(e), true)
} finally {
runningSetup = undefined
refreshKey++
}
}
let newDbName = $state('')
let newDbTag: CustomInstanceDbTag = $state('datatable')
let creating = $state(false)
async function createDatabase() {
creating = true
try {
await SettingService.createExternalInstancePgDatabase({
name: newDbName.trim(),
requestBody: { tag: newDbTag }
})
sendUserToast(`Created database ${newDbName.trim()}`)
newDbName = ''
} catch (e) {
sendUserToast(e?.body ?? e?.message ?? String(e), true)
} finally {
creating = false
refreshKey++
}
}
async function dropDatabase(name: string) {
const ok = await confirmationModal.ask({
title: `Drop database ${name}`,
children:
'The database and everything in it is deleted from the external cluster. This cannot be undone.',
confirmationText: 'Drop database'
})
if (!ok) return
try {
await SettingService.dropExternalInstancePgDatabase({ name })
sendUserToast(`Dropped database ${name}`)
} catch (e) {
sendUserToast(e?.body ?? e?.message ?? String(e), true)
} finally {
refreshKey++
}
}
let databaseEntries = $derived(Object.entries(databases.current ?? {}))
let setUp = $derived(!!status.current?.last_setup?.success)
</script>
<div class="flex flex-col gap-6">
{#if !$enterpriseLicense}
<Alert
type="info"
title="External instance databases are an Enterprise Edition feature"
size="xs"
/>
{/if}
<div class="grid grid-cols-2 gap-x-2 gap-y-4">
<div class="flex flex-col gap-1">
<label for="external_pg_host" class="text-xs font-semibold text-emphasis">Host</label>
<TextInput
inputProps={{ id: 'external_pg_host', placeholder: 'db.example.com', disabled: isDisabled }}
bind:value={() => field('host'), (v) => setField('host', v)}
/>
</div>
<div class="flex flex-col gap-1">
<label for="external_pg_port" class="text-xs font-semibold text-emphasis">Port</label>
<TextInput
inputProps={{
id: 'external_pg_port',
type: 'number',
placeholder: '5432',
disabled: isDisabled
}}
bind:value={() => field('port'), (v) => setField('port', v)}
/>
</div>
<div class="flex flex-col gap-1">
<label for="external_pg_user" class="text-xs font-semibold text-emphasis">Admin user</label>
<TextInput
inputProps={{ id: 'external_pg_user', placeholder: 'windmill_admin', disabled: isDisabled }}
bind:value={() => field('user'), (v) => setField('user', v)}
/>
</div>
<div class="flex flex-col gap-1">
<label for="external_pg_password" class="text-xs font-semibold text-emphasis">Password</label>
<Password
id="external_pg_password"
small
disabled={isDisabled}
bind:password={() => field('password'), (v) => setField('password', v)}
/>
</div>
<div class="flex flex-col gap-1">
<label for="external_pg_dbname" class="text-xs font-semibold text-emphasis">
Maintenance database
</label>
<TextInput
inputProps={{ id: 'external_pg_dbname', placeholder: 'postgres', disabled: isDisabled }}
bind:value={() => field('dbname'), (v) => setField('dbname', v)}
/>
</div>
<div class="flex flex-col gap-1">
<label for="external_pg_sslmode" class="text-xs font-semibold text-emphasis">SSL mode</label>
<Select
id="external_pg_sslmode"
items={SSLMODES.map((m) => ({ value: m, label: m }))}
placeholder="verify-full (default)"
clearable
disabled={isDisabled}
bind:value={() => field('sslmode'), (v) => setField('sslmode', v)}
/>
</div>
<div class="col-span-2 flex flex-col gap-1">
<label for="external_pg_root_cert" class="text-xs font-semibold text-emphasis">
Root certificate (PEM)
</label>
<TextInput
underlyingInputEl="textarea"
inputProps={{
id: 'external_pg_root_cert',
placeholder: '-----BEGIN CERTIFICATE-----',
rows: 3,
disabled: isDisabled
}}
bind:value={() => field('root_certificate_pem'), (v) => setField('root_certificate_pem', v)}
/>
<span class="text-2xs text-secondary">
Leave empty to verify against the system trust store.
</span>
</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Wrench }}
disabled={isDisabled || unsaved || !saved.current || !!runningSetup}
loading={runningSetup === 'setup'}
onclick={() => runSetup(false)}
>
Set up cluster
</Button>
<Button
unifiedSize="md"
variant="default"
startIcon={{ icon: KeyRound }}
disabled={isDisabled || unsaved || !saved.current || !setUp || !!runningSetup}
loading={runningSetup === 'rotate'}
onclick={() => runSetup(true)}
>
Rotate passwords
</Button>
{#if unsaved}
<span class="text-xs text-secondary">Save the settings before setting the cluster up.</span>
{:else if !saved.current}
<span class="text-xs text-secondary">Fill in the connection and save to set it up.</span>
{/if}
</div>
{#if report}
<div class="flex flex-col gap-1 rounded-md border p-3 bg-surface-secondary">
<div class="flex items-center justify-between text-xs">
<span class="font-semibold text-emphasis">
{report.success ? 'Last setup succeeded' : 'Last setup failed'}
</span>
<span class="text-secondary">{new Date(report.finished_at).toLocaleString()}</span>
</div>
<ul class="flex flex-col gap-1.5 mt-1">
{#each report.steps as step, i (i)}
<li class="flex gap-2 text-xs">
{#if step.status === 'ok'}
<CircleCheck size={14} class="text-green-600 dark:text-green-400 shrink-0 mt-0.5" />
{:else if step.status === 'warning'}
<TriangleAlert
size={14}
class="text-yellow-600 dark:text-yellow-400 shrink-0 mt-0.5"
/>
{:else}
<CircleX size={14} class="text-red-600 dark:text-red-400 shrink-0 mt-0.5" />
{/if}
<span class="font-mono text-secondary shrink-0">{step.name}</span>
<span class="text-primary break-words">{step.message}</span>
</li>
{/each}
</ul>
</div>
{/if}
</div>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-0.5">
<span class="text-xs font-semibold text-emphasis">Databases</span>
<span class="text-xs text-secondary">
Databases Windmill created on this cluster. Workspaces use them by picking the
<span class="font-semibold">External instance</span> type in their data table or Ducklake settings.
</span>
</div>
<DataTable>
<Head>
<tr>
<Cell head first>Name</Cell>
<Cell head>Used for</Cell>
<Cell head>Used by</Cell>
<Cell head last></Cell>
</tr>
</Head>
<tbody class="divide-y bg-surface-tertiary">
{#if databaseEntries.length === 0}
<Row>
<Cell colspan={4} class="text-center text-xs text-secondary py-4">No database yet</Cell>
</Row>
{/if}
{#each databaseEntries as [name, db] (name)}
<Row>
<Cell first class="font-mono text-xs">{name}</Cell>
<Cell class="text-xs">{db.tag === 'ducklake' ? 'Ducklake' : 'Data table'}</Cell>
<Cell class="text-xs">
{(db.used_by_workspaces ?? []).join(', ') || '—'}
</Cell>
<Cell last class="text-right">
<Button
unifiedSize="sm"
variant="subtle"
startIcon={{ icon: Trash2 }}
iconOnly
disabled={isDisabled || (db.used_by_workspaces ?? []).length > 0}
title={(db.used_by_workspaces ?? []).length > 0
? 'Still used by a workspace'
: `Drop ${name}`}
onclick={() => dropDatabase(name)}
/>
</Cell>
</Row>
{/each}
</tbody>
</DataTable>
<div class="flex items-center gap-2">
<TextInput
class="flex-1"
inputProps={{
id: 'external_pg_new_db',
placeholder: 'New database name',
disabled: isDisabled || !setUp
}}
bind:value={newDbName}
/>
<Select
id="external_pg_new_db_tag"
class="w-36"
items={[
{ value: 'datatable', label: 'Data table' },
{ value: 'ducklake', label: 'Ducklake' }
]}
disabled={isDisabled || !setUp}
bind:value={newDbTag}
/>
<Button
unifiedSize="md"
variant="default"
startIcon={{ icon: Plus }}
disabled={isDisabled || !setUp || !newDbName.trim() || creating}
loading={creating}
onclick={createDatabase}
>
Create database
</Button>
</div>
{#if !setUp}
<span class="text-xs text-secondary">Set the cluster up before creating databases.</span>
{/if}
</div>
</div>
<ConfirmationModal {...confirmationModal.props} />
@@ -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
</Tooltip>
{:else if dataTable.database.resource_type === 'external_instance'}
<Tooltip
wrapperClass="absolute mt-[0.6rem] right-2 z-20"
placement="bottom-start"
>
Use a database on the external PostgreSQL cluster set in instance settings
</Tooltip>
{/if}
<Select
items={[
@@ -433,6 +442,12 @@
: isCloudHosted()
? 'Not available on cloud'
: 'Superadmin only'
},
{
value: 'external_instance',
label: 'External instance',
disabled: !!$externalInstanceDbUnavailableReason,
subtitle: $externalInstanceDbUnavailableReason
}
]}
bind:value={
@@ -446,16 +461,22 @@
}
}
id="database-type-select"
class="w-28"
class="w-40"
/>
</div>
<div class="flex items-center gap-1 w-80 relative">
{#if dataTable.database.resource_type !== 'instance'}
{#if dataTable.database.resource_type === 'postgresql'}
<ResourcePicker
class="flex-1"
bind:value={dataTable.database.resource_path}
resourceType={dataTable.database.resource_type}
/>
{:else if dataTable.database.resource_type === 'external_instance'}
<ExternalInstanceDbSelect
class="flex-1"
bind:value={dataTable.database.resource_path}
tag="datatable"
/>
{:else}
<CustomInstanceDbSelect
class="flex-1"
@@ -13,7 +13,7 @@
ducklakes: {
name: string
catalog: {
resource_type: 'postgresql' | 'mysql' | 'instance'
resource_type: 'postgresql' | 'mysql' | 'instance' | 'external_instance'
resource_path?: string // Name of the database when resource_type is instance
}
storage: {
@@ -105,7 +105,12 @@
import Popover from '../meltComponents/Popover.svelte'
import TextInput from '../text_input/TextInput.svelte'
import { slide } from 'svelte/transition'
import { isCustomInstanceDbEnabled, getUnusedInstanceDbName } from './utils.svelte'
import {
isCustomInstanceDbEnabled,
getUnusedInstanceDbName,
externalInstanceDbUnavailableReason
} from './utils.svelte'
import ExternalInstanceDbSelect from './ExternalInstanceDbSelect.svelte'
import { resource } from 'runed'
import CustomInstanceDbSelect from './CustomInstanceDbSelect.svelte'
import Label from '../Label.svelte'
@@ -283,10 +288,9 @@
This workspace is a fork, and these settings are its own copy. Lakes marked
<span class="font-semibold">isolated</span> read the parent's tables through defer views and
write to a fork-scoped namespace that is cleaned up when the fork is deleted. Lakes marked
<span class="font-semibold">shared with parent</span> read and write the parent's physical
lake directly — editing their catalog or storage here repoints the shared lake for this
fork's jobs. The choice is made per lake when the fork is created and cannot be changed
here.
<span class="font-semibold">shared with parent</span> read and write the parent's physical lake
directly — editing their catalog or storage here repoints the shared lake for this fork's jobs.
The choice is made per lake when the fork is created and cannot be changed here.
</Alert>
</div>
{/if}
@@ -359,8 +363,8 @@
isolated
</span>
<Tooltip>
Writes go to a fork-scoped namespace; reads of tables not yet materialized in
this fork defer to the parent. Deleting the fork cleans the namespace up.
Writes go to a fork-scoped namespace; reads of tables not yet materialized in this
fork defer to the parent. Deleting the fork cleans the namespace up.
</Tooltip>
{/if}
</div>
@@ -373,6 +377,11 @@
<Tooltip wrapperClass="absolute mt-[0.6rem] right-2 z-20" placement="bottom-start">
Use Windmill's PostgreSQL instance as a catalog
</Tooltip>
{:else if ducklake.catalog.resource_type === 'external_instance'}
<Tooltip wrapperClass="absolute mt-[0.6rem] right-2 z-20" placement="bottom-start">
Use a database on the external PostgreSQL cluster set in instance settings as a
catalog
</Tooltip>
{/if}
<Select
items={[
@@ -382,6 +391,12 @@
value: 'instance',
label: 'Instance',
subtitle: $isCustomInstanceDbEnabled ? undefined : 'Superadmin only'
},
{
value: 'external_instance',
label: 'External instance',
disabled: !!$externalInstanceDbUnavailableReason,
subtitle: $externalInstanceDbUnavailableReason
}
]}
bind:value={
@@ -394,16 +409,22 @@
}
}
}
class="w-24"
class="w-40"
/>
</div>
<div class="flex flex-1">
{#if ducklake.catalog.resource_type !== 'instance'}
{#if ducklake.catalog.resource_type === 'postgresql' || ducklake.catalog.resource_type === 'mysql'}
<ResourcePicker
class="flex-1 min-w-32"
bind:value={ducklake.catalog.resource_path}
resourceType={ducklake.catalog.resource_type}
/>
{:else if ducklake.catalog.resource_type === 'external_instance'}
<ExternalInstanceDbSelect
class="flex-1 min-w-32"
bind:value={ducklake.catalog.resource_path}
tag="ducklake"
/>
{:else}
<CustomInstanceDbSelect
class="flex-1 min-w-32"
@@ -0,0 +1,84 @@
<script lang="ts">
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++
}
}
</script>
<div class="flex items-center gap-1 {className}">
<Select
class="flex-1"
bind:value
onCreateItem={(i) => (value = i)}
placeholder="Search or create..."
showPlaceholderOnOpen
{items}
id="external-instance-db-select"
disabled={!$isExternalInstanceDbEnabled}
/>
{#if value && !databases.loading && !exists}
<Button
unifiedSize="sm"
variant="default"
startIcon={{ icon: Plus }}
loading={creating}
disabled={!$isExternalInstanceDbEnabled}
title="Create this database on the external cluster"
onclick={create}
>
Create
</Button>
{/if}
</div>
@@ -1,6 +1,6 @@
import { isCloudHosted } from '$lib/cloud'
import { superadmin } from '$lib/stores'
import { enterpriseLicense, superadmin } from '$lib/stores'
import { getLocalSetting } from '$lib/utils'
import { derived } from 'svelte/store'
@@ -20,6 +20,24 @@ export let isCustomInstanceDbEnabled = derived(
([superadmin_]) => superadmin_ && !isCloudHosted()
)
export let isExternalInstanceDbEnabled = derived(
[superadmin, enterpriseLicense],
([superadmin_, enterpriseLicense_]) => superadmin_ && !!enterpriseLicense_ && !isCloudHosted()
)
/** Why the External instance option cannot be picked, or undefined when it can. */
export let externalInstanceDbUnavailableReason = derived(
[superadmin, enterpriseLicense],
([superadmin_, enterpriseLicense_]) =>
isCloudHosted()
? 'Not available on cloud'
: !enterpriseLicense_
? 'Enterprise Edition only'
: !superadmin_
? 'Superadmin only'
: undefined
)
// Postgres caps identifiers at 63 bytes; the backend rejects longer db names.
const MAX_INSTANCE_DB_NAME_LEN = 63