mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-25 16:03:47 +00:00
* Remove $$props.field usage * Rename slots to ensure no hyphen * _props * _trigger * OnSelectedIteration type correct capitalization * rename _content * Remove afterUpdate * Migrate everything to svelte 5 * array bind * Fix popover * type never * nit fixes * Fixed many trivial errors * onClick * Fix errors * use let: * nit typing * fix: wrap state_referenced_locally vars with untrack() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add untrack import * Fix all syntax errors due to untrack migration * Fix undefined errors * Fix more undefined errors * untrack(() => initialOpen) * svelte-ignore * Fix state_descriptors_fixed error in Chart.svelte Use $state.snapshot() to pass plain copies of data/options to Chart.js instead of $state proxies. Chart.js's listenArrayEvents tries to define property descriptors on data arrays, which Svelte 5 proxies reject. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * nit typing * Merge issue * Fix "path is not set" error in resource picker / editor * Fix InputTransformForm error when rerunning some flows * fix npm run check --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
303 lines
9.1 KiB
Svelte
303 lines
9.1 KiB
Svelte
<script lang="ts" module>
|
|
export type DataTableSettingsType = {
|
|
dataTables: {
|
|
name: string
|
|
database: {
|
|
resource_type: 'postgresql' | 'instance'
|
|
resource_path?: string | undefined
|
|
}
|
|
}[]
|
|
}
|
|
|
|
export function convertDataTableSettingsFromBackend(
|
|
settings: GetSettingsResponse['datatable']
|
|
): DataTableSettingsType {
|
|
const s: DataTableSettingsType = { dataTables: [] }
|
|
if (settings?.datatables) {
|
|
for (const [name, rest] of Object.entries(settings.datatables)) {
|
|
s.dataTables.push({ name, ...rest })
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
export function convertDataTableSettingsToBackend(
|
|
settings: DataTableSettingsType
|
|
): NonNullable<GetSettingsResponse['datatable']> {
|
|
const s: GetSettingsResponse['datatable'] = { datatables: {} }
|
|
for (const dataTable of settings.dataTables) {
|
|
const database = dataTable.database
|
|
if (dataTable.name in s.datatables)
|
|
throw 'Settings contain duplicate dataTable name: ' + dataTable.name
|
|
if (!database.resource_path) throw 'No resource selected for ' + dataTable.name
|
|
if (database.resource_type === 'instance' && database.resource_path === 'windmill')
|
|
throw dataTable.name + ' database cannot be called "windmill"'
|
|
|
|
s.datatables[dataTable.name] = {
|
|
database: dataTable.database
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
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 ResourcePicker from '../ResourcePicker.svelte'
|
|
import SettingsPageHeader from '../settings/SettingsPageHeader.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'
|
|
import { SettingService, WorkspaceService, type GetSettingsResponse } from '$lib/gen'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { createAsyncConfirmationModal } from '../common/confirmationModal/asyncConfirmationModal.svelte'
|
|
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
|
import { resource } from 'runed'
|
|
import CustomInstanceDbSelect from './CustomInstanceDbSelect.svelte'
|
|
import { Popover } from '../meltComponents'
|
|
import ExploreAssetButton from '../ExploreAssetButton.svelte'
|
|
import { deepEqual } from 'fast-equals'
|
|
import { clone } from '$lib/utils'
|
|
import SettingsFooter from './SettingsFooter.svelte'
|
|
|
|
type Props = {
|
|
dataTableSettings: DataTableSettingsType
|
|
}
|
|
|
|
let { dataTableSettings = $bindable() }: Props = $props()
|
|
|
|
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: DataTableSettingsType = $derived.by(() => {
|
|
let s = $state($state.snapshot(dataTableSettings))
|
|
return s
|
|
})
|
|
|
|
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
|
|
}
|
|
})
|
|
}
|
|
|
|
const customInstanceDbs = resource([], SettingService.listCustomInstanceDbs)
|
|
|
|
async function onSave() {
|
|
try {
|
|
if (
|
|
$isCustomInstanceDbEnabled &&
|
|
tempSettings.dataTables.some(
|
|
(d) =>
|
|
d.database.resource_type === 'instance' &&
|
|
!customInstanceDbs.current?.[d.database.resource_path ?? '']?.success
|
|
)
|
|
) {
|
|
let confirm = await confirmationModal.ask({
|
|
title: 'Some databases are not setup',
|
|
children: 'Are you sure you want to save without setting them up ?',
|
|
confirmationText: 'Save anyway'
|
|
})
|
|
if (!confirm) return
|
|
}
|
|
const settings = convertDataTableSettingsToBackend(tempSettings)
|
|
await WorkspaceService.editDataTableConfig({
|
|
workspace: $workspaceStore!,
|
|
requestBody: { settings }
|
|
})
|
|
dataTableSettings = clone(tempSettings)
|
|
sendUserToast('Data table settings saved successfully')
|
|
} catch (e) {
|
|
sendUserToast(e, true)
|
|
console.error('Error saving data table settings', e)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
let confirmationModal = createAsyncConfirmationModal()
|
|
let dirtyMap = $derived.by(() => {
|
|
const map: Record<string, boolean> = {}
|
|
for (let i = 0; i < tempSettings.dataTables.length; i++) {
|
|
let temp = tempSettings.dataTables[i]
|
|
let dt = dataTableSettings.dataTables.find((d) => d.name === temp.name)
|
|
map[temp.name] = !deepEqual(dt, temp)
|
|
}
|
|
return map
|
|
})
|
|
|
|
function onDiscard() {
|
|
tempSettings.dataTables = $state.snapshot(dataTableSettings.dataTables)
|
|
}
|
|
|
|
export function discard() {
|
|
onDiscard()
|
|
}
|
|
|
|
export function unsavedChanges(): { savedValue: any; modifiedValue: any } {
|
|
return { savedValue: dataTableSettings, modifiedValue: tempSettings }
|
|
}
|
|
|
|
let hasUnsavedChanges = $derived.by(() => {
|
|
return !deepEqual(dataTableSettings, tempSettings)
|
|
})
|
|
</script>
|
|
|
|
<SettingsPageHeader
|
|
title="Data tables"
|
|
description="Store relational data out of the box. Interact with a fully managed PostgreSQL database directly from the Windmill SDK."
|
|
link="https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables"
|
|
/>
|
|
|
|
<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-tertiary">
|
|
{#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', id: '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
|
|
}
|
|
}
|
|
}
|
|
id="database-type-select"
|
|
class="w-28"
|
|
/>
|
|
</div>
|
|
<div class="flex items-center gap-1 w-80 relative">
|
|
{#if dataTable.database.resource_type !== 'instance'}
|
|
<ResourcePicker
|
|
class="flex-1"
|
|
bind:value={dataTable.database.resource_path}
|
|
resourceType={dataTable.database.resource_type}
|
|
/>
|
|
{:else}
|
|
<CustomInstanceDbSelect
|
|
class="flex-1"
|
|
{confirmationModal}
|
|
{customInstanceDbs}
|
|
bind:value={dataTable.database.resource_path}
|
|
tag="datatable"
|
|
/>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</Cell>
|
|
<Cell class="w-12">
|
|
{#if dirtyMap[dataTable.name]}
|
|
<Popover
|
|
openOnHover
|
|
contentClasses="p-2 text-sm text-secondary italic"
|
|
class="cursor-not-allowed"
|
|
>
|
|
{#snippet trigger()}
|
|
<ExploreAssetButton
|
|
class="h-9"
|
|
asset={{ kind: 'datatable', path: dataTable.name }}
|
|
disabled
|
|
/>
|
|
{/snippet}
|
|
{#snippet content()}
|
|
Please save settings first
|
|
{/snippet}
|
|
</Popover>
|
|
{:else}
|
|
<ExploreAssetButton class="h-9" asset={{ kind: 'datatable', path: dataTable.name }} />
|
|
{/if}
|
|
</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>
|
|
|
|
<SettingsFooter
|
|
class="mt-8"
|
|
{hasUnsavedChanges}
|
|
{onSave}
|
|
{onDiscard}
|
|
saveLabel="Save data table settings"
|
|
/>
|
|
|
|
<ConfirmationModal {...confirmationModal.props} />
|