data tables settings ui

This commit is contained in:
Diego Imbert
2025-11-14 11:27:56 +01:00
parent 77316cbd0e
commit efe5bec74a
4 changed files with 299 additions and 100 deletions
@@ -0,0 +1,181 @@
<script lang="ts" module>
export type DataTablesSettingsStruct = {
dataTables: {
name: string
database: {
resource_type: 'postgresql' | 'mysql' | 'instance'
resource_path: string | undefined
}
}[]
}
let DEFAULT_DATATABLE_DB_NAME = 'datatable_db'
</script>
<script lang="ts">
import { Plus } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import CloseButton from '../common/CloseButton.svelte'
import Description from '../Description.svelte'
import ResourcePicker from '../ResourcePicker.svelte'
import Select from '../select/Select.svelte'
import Cell from '../table/Cell.svelte'
import DataTable from '../table/DataTable.svelte'
import Head from '../table/Head.svelte'
import Row from '../table/Row.svelte'
import TextInput from '../text_input/TextInput.svelte'
import Tooltip from '../Tooltip.svelte'
import { isCustomInstanceDbEnabled } from './utils.svelte'
import { random_adj } from '../random_positive_adjetive'
import { sendUserToast } from '$lib/toast'
let tableHeadNames = ['Name', 'Database', '', ''] as const
let tableHeadTooltips: Partial<Record<(typeof tableHeadNames)[number], string | undefined>> = {
Name: 'Data tables are referenced by their name. main is a special name that can be used as the default data table.',
Database: 'The database where the data is stored.'
}
let tempSettings: DataTablesSettingsStruct = $state({
dataTables: []
})
function removeDataTable(index: number) {
tempSettings.dataTables.splice(index, 1)
}
function onNewDataTable() {
const name = tempSettings.dataTables.some((d) => d.name === 'main')
? `${random_adj()}_datatable`
: 'main'
tempSettings.dataTables.push({
name,
database: {
resource_type: $isCustomInstanceDbEnabled ? 'instance' : 'postgresql',
resource_path: $isCustomInstanceDbEnabled ? DEFAULT_DATATABLE_DB_NAME : undefined
}
})
}
async function onSave() {
try {
sendUserToast('Data table settings saved successfully')
} catch (e) {
sendUserToast(e, true)
console.error('Error saving data table settings', e)
}
}
</script>
<div class="flex flex-col gap-4 my-8">
<div class="flex flex-col gap-1">
<div class="text-sm font-semibold text-emphasis">Data tables</div>
<Description link="https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables">
Store relational data out of the box. Interact with a fully managed PostgreSQL database
directly from the windmill SDK.
</Description>
</div>
</div>
<DataTable>
<Head>
<tr>
{#each tableHeadNames as name, i}
<Cell head first={i == 0} last={i == tableHeadNames.length - 1}>
{name}
{#if tableHeadTooltips[name]}
<Tooltip>
{@html tableHeadTooltips[name]}
</Tooltip>
{/if}
</Cell>
{/each}
</tr>
</Head>
<tbody class="divide-y bg-surface">
{#if tempSettings.dataTables.length == 0}
<Row>
<Cell colspan={tableHeadNames.length} class="text-center py-6">
No data table in this workspace yet
</Cell>
</Row>
{/if}
{#each tempSettings.dataTables as dataTable, dataTableIndex}
<Row>
<Cell first class="w-48 relative">
<TextInput bind:value={dataTable.name} inputProps={{ placeholder: 'Name' }} />
</Cell>
<Cell>
<div class="flex gap-2">
<div class="relative">
{#if dataTable.database.resource_type === 'instance'}
<Tooltip wrapperClass="absolute mt-[0.6rem] right-2 z-20" placement="bottom-start">
Use Windmill's PostgreSQL instance
</Tooltip>
{/if}
<Select
items={[
{ value: 'postgresql', label: 'PostgreSQL' },
{
value: 'instance',
label: 'Instance',
subtitle: $isCustomInstanceDbEnabled ? undefined : 'Superadmin only'
}
]}
bind:value={
() => dataTable.database.resource_type,
(resource_type) => {
dataTable.database = {
resource_type,
resource_path:
resource_type === 'instance' ? DEFAULT_DATATABLE_DB_NAME : undefined
}
}
}
class="w-28"
/>
</div>
<div class="flex items-center gap-1 w-80 relative">
{#if dataTable.database.resource_type !== 'instance'}
<ResourcePicker
bind:value={dataTable.database.resource_path}
resourceType={dataTable.database.resource_type}
/>
{:else}
<!-- TODO -->
<Select
class="flex-1"
inputClass="pr-20"
bind:value={dataTable.database.resource_path}
onCreateItem={(i) => (dataTable.database.resource_path = i)}
placeholder="PostgreSQL database name"
items={[] as { value: string; label: string }[]}
disabled={!$isCustomInstanceDbEnabled}
/>
{/if}
</div>
</div>
</Cell>
<Cell class="w-12">
<!-- Explore button -->
</Cell>
<Cell class="w-12">
<CloseButton small on:close={() => removeDataTable(dataTableIndex)} />
</Cell>
</Row>
{/each}
<Row class="!border-0">
<Cell colspan={tableHeadNames.length} class="pt-0 pb-2">
<div class="flex justify-center">
<Button size="sm" btnClasses="max-w-fit" variant="default" on:click={onNewDataTable}>
<Plus /> New Data Table
</Button>
</div>
</Cell>
</Row>
</tbody>
</DataTable>
<Button wrapperClasses="mt-4 mb-16 max-w-fit" on:click={onSave}>Save</Button>
@@ -65,12 +65,11 @@
import { SettingService, WorkspaceService, type DucklakeInstanceCatalogDbStatus } from '$lib/gen'
import { type GetSettingsResponse } from '$lib/gen'
import { superadmin, workspaceStore } from '$lib/stores'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import ExploreAssetButton from '../ExploreAssetButton.svelte'
import DbManagerDrawer from '../DBManagerDrawer.svelte'
import Tooltip from '../Tooltip.svelte'
import { isCloudHosted } from '$lib/cloud'
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
import { createAsyncConfirmationModal } from '../common/confirmationModal/asyncConfirmationModal.svelte'
import { clone } from '$lib/utils'
@@ -81,6 +80,7 @@
import LoggedWizardResult, { firstEmptyStepIsError } from '../wizards/LoggedWizardResult.svelte'
import { safeSelectItems } from '../select/utils.svelte'
import { slide } from 'svelte/transition'
import { isCustomInstanceDbEnabled } from './utils.svelte'
const DEFAULT_DUCKLAKE_CATALOG_NAME = 'ducklake_catalog'
@@ -95,8 +95,6 @@
onSave: onSaveProp = undefined
}: Props = $props()
let isInstanceCatalogEnabled = $derived($superadmin && !isCloudHosted())
function onNewDucklake() {
const name = ducklakeSettings.ducklakes.some((d) => d.name === 'main')
? `${random_adj()}_ducklake`
@@ -104,8 +102,8 @@
ducklakeSettings.ducklakes.push({
name,
catalog: {
resource_type: isInstanceCatalogEnabled ? 'instance' : 'postgresql',
resource_path: isInstanceCatalogEnabled ? DEFAULT_DUCKLAKE_CATALOG_NAME : undefined
resource_type: $isCustomInstanceDbEnabled ? 'instance' : 'postgresql',
resource_path: $isCustomInstanceDbEnabled ? DEFAULT_DUCKLAKE_CATALOG_NAME : undefined
},
storage: {
storage: undefined,
@@ -135,7 +133,7 @@
async function onSave() {
try {
if (
isInstanceCatalogEnabled &&
$isCustomInstanceDbEnabled &&
ducklakeSettings.ducklakes.some(
(d) =>
d.catalog.resource_type === 'instance' &&
@@ -223,7 +221,7 @@
<tbody class="divide-y bg-surface">
{#if ducklakeSettings.ducklakes.length == 0}
<Row>
<Cell colspan={tableHeadNames.length} class="text-center">
<Cell colspan={tableHeadNames.length} class="text-center py-6">
No ducklake in this workspace yet
</Cell>
</Row>
@@ -255,7 +253,7 @@
{
value: 'instance',
label: 'Instance',
subtitle: isInstanceCatalogEnabled ? undefined : 'Superadmin only'
subtitle: $isCustomInstanceDbEnabled ? undefined : 'Superadmin only'
}
]}
bind:value={
@@ -276,8 +274,6 @@
<ResourcePicker
bind:value={ducklake.catalog.resource_path}
resourceType={ducklake.catalog.resource_type}
selectInputClass="min-h-9"
class="min-h-9"
/>
{:else}
{@const status =
@@ -289,7 +285,7 @@
onCreateItem={(i) => (ducklake.catalog.resource_path = i)}
placeholder="PostgreSQL database name"
items={safeSelectItems(Object.keys(instanceCatalogStatuses.value ?? {}))}
disabled={!isInstanceCatalogEnabled}
disabled={!$isCustomInstanceDbEnabled}
/>
<Popover
@@ -481,7 +477,7 @@
<Button
wrapperClasses="flex-1"
size="sm"
disabled={!isInstanceCatalogEnabled}
disabled={!$isCustomInstanceDbEnabled}
onClick={async () => {
if (instanceCatalogSetupIsRunning) return
@@ -516,7 +512,7 @@
}}
loading={instanceCatalogSetupIsRunning}
>
{#if !isInstanceCatalogEnabled}
{#if !$isCustomInstanceDbEnabled}
Only superadmins can setup instance catalogs
{:else if status?.success}
Check again
@@ -532,7 +528,7 @@
asset={{ kind: 'resource', path: 'INSTANCE_DUCKLAKE_CATALOG/' + dbname }}
_resourceMetadata={{ resource_type: 'postgresql' }}
{dbManagerDrawer}
disabled={!isInstanceCatalogEnabled}
disabled={!$isCustomInstanceDbEnabled}
onClick={() => instanceCatalogPopover?.close()}
/>
{/if}
@@ -0,0 +1,9 @@
import { isCloudHosted } from '$lib/cloud'
import { superadmin } from '$lib/stores'
import { derived } from 'svelte/store'
export let isCustomInstanceDbEnabled = derived(
[superadmin],
(superadmin_) => superadmin_ && !isCloudHosted()
)
@@ -59,6 +59,7 @@
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
import CollapseLink from '$lib/components/CollapseLink.svelte'
import DataTableSettings from '$lib/components/workspaceSettings/DataTableSettings.svelte'
let slackInitialPath: string = $state('')
let slackScriptPath: string = $state('')
@@ -131,6 +132,7 @@
| 'deploy_to'
| 'error_handler'
| 'ai'
| 'windmill_data_tables'
| 'windmill_lfs'
| 'git_sync'
| 'default_app'
@@ -339,7 +341,9 @@
if (!$workspaceStore) return
try {
const config = await WorkspaceService.getWorkspaceSlackOauthConfig({ workspace: $workspaceStore })
const config = await WorkspaceService.getWorkspaceSlackOauthConfig({
workspace: $workspaceStore
})
useCustomSlackApp = !!config.slack_oauth_client_id
slackOAuthClientId = config.slack_oauth_client_id || ''
slackOAuthClientSecret = config.slack_oauth_client_secret || ''
@@ -662,6 +666,13 @@
aiDescription="Windmill AI workspace settings"
label="Windmill AI"
/>
<Tab
small
value="windmill_data_tables"
aiId="workspace-settings-windmill-data-tables"
aiDescription="Data tables workspace settings"
label="Data Tables"
/>
<Tab
small
value="windmill_lfs"
@@ -761,93 +772,93 @@
>
{#snippet workspaceConfig()}
<!-- Workspace OAuth Configuration Section -->
<div class="flex flex-col">
{#if slackOAuthConfigLoaded}
<!-- Show saved config with delete button -->
<div class="flex flex-col gap-1 w-fit">
<div class="text-sm text-primary font-medium">Workspace specific Slack app</div>
<div class="p-2 rounded-md border border-gray-200 dark:border-gray-700 bg-surface-secondary">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<span class="text-sm text-primary">Client ID:</span>
<span class="text-xs text-secondary font-mono pt-1">{slackOAuthClientId}</span>
</div>
<Button
size="xs"
onclick={deleteSlackOAuthConfig}
btnClasses="w-fit"
<div class="flex flex-col">
{#if slackOAuthConfigLoaded}
<!-- Show saved config with delete button -->
<div class="flex flex-col gap-1 w-fit">
<div class="text-sm text-primary font-medium">Workspace specific Slack app</div>
<div
class="p-2 rounded-md border border-gray-200 dark:border-gray-700 bg-surface-secondary"
>
<Trash2 size={14} class="mr-1" />
Delete
</Button>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<span class="text-sm text-primary">Client ID:</span>
<span class="text-xs text-secondary font-mono pt-1"
>{slackOAuthClientId}</span
>
</div>
<Button size="xs" onclick={deleteSlackOAuthConfig} btnClasses="w-fit">
<Trash2 size={14} class="mr-1" />
Delete
</Button>
</div>
</div>
</div>
</div>
{:else}
<!-- Show toggle and form to create config -->
<label class="text-sm flex gap-2 items-center font-medium text-primary">
<Toggle bind:checked={useCustomSlackApp} size="sm" />
<span class="text-xs text-secondary">Use workspace specific Slack app</span>
</label>
{#if useCustomSlackApp}
<div class="p-2 rounded border border-gray-200 dark:border-gray-700">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client ID</span>
<input
class="windmill-input"
type="text"
placeholder="1234567890.1234567890"
bind:value={slackOAuthClientId}
/>
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client secret</span>
<input
class="windmill-input"
type="password"
placeholder="Enter client secret"
bind:value={slackOAuthClientSecret}
/>
</label>
<CollapseLink text="Instructions">
<div class="text-xs text-secondary p-2">
Create a Slack app at{' '}
<a
href="https://api.slack.com/apps"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:underline"
>
Slack API
</a>. Set the redirect URI to:{' '}
<code class="bg-gray-100 dark:bg-gray-800 px-1 py-0.5 rounded">
{window.location.origin}{base}/oauth/callback_slack
</code>
</div>
</CollapseLink>
<div class="pt-2">
<Button
size="xs"
variant="accent"
onclick={saveAndConnectSlack}
disabled={!slackOAuthClientId || !slackOAuthClientSecret}
startIcon={{ icon: Slack }}
btnClasses="w-fit"
>
Connect to Slack
</Button>
</div>
</div>
{/if}
{/if}
</div>
{:else}
<!-- Show toggle and form to create config -->
<label class="text-sm flex gap-2 items-center font-medium text-primary">
<Toggle bind:checked={useCustomSlackApp} size="sm" />
<span class="text-xs text-secondary">Use workspace specific Slack app</span>
</label>
{#if useCustomSlackApp}
<div class="p-2 rounded border border-gray-200 dark:border-gray-700">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client ID</span>
<input
class="windmill-input"
type="text"
placeholder="1234567890.1234567890"
bind:value={slackOAuthClientId}
/>
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client secret</span>
<input
class="windmill-input"
type="password"
placeholder="Enter client secret"
bind:value={slackOAuthClientSecret}
/>
</label>
<CollapseLink text="Instructions">
<div class="text-xs text-secondary p-2">
Create a Slack app at{' '}
<a
href="https://api.slack.com/apps"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:underline"
>
Slack API
</a>. Set the redirect URI to:{' '}
<code class="bg-gray-100 dark:bg-gray-800 px-1 py-0.5 rounded">
{window.location.origin}{base}/oauth/callback_slack
</code>
</div>
</CollapseLink>
<div class="pt-2">
<Button
size="xs"
variant="accent"
onclick={saveAndConnectSlack}
disabled={!slackOAuthClientId || !slackOAuthClientSecret}
startIcon={{ icon: Slack }}
btnClasses="w-fit"
>
Connect to Slack
</Button>
</div>
</div>
{/if}
{/if}
</div>
{/snippet}
</ConnectionSection>
{:else if slack_tabs === 'teams_commands'}
{/snippet}
</ConnectionSection>
{:else if slack_tabs === 'teams_commands'}
{#if !$enterpriseLicense}
<div class="pt-4"></div>
<Alert type="warning" title="Workspace Teams commands is an EE feature">
@@ -1102,6 +1113,8 @@
initialMaxTokensPerModel = clone(maxTokensPerModel)
}}
/>
{:else if tab == 'windmill_data_tables'}
<DataTableSettings />
{:else if tab == 'windmill_lfs'}
<StorageSettings
bind:s3ResourceSettings