mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 16:01:42 +00:00
data tables settings ui
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user