Files
windmill/frontend/src/lib/components/SupabaseConnect.svelte
T
Diego ImbertandClaude Opus 4.6 5d79f33590 Final Svelte 5 migration (#8211)
* 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>
2026-03-05 18:11:40 +01:00

190 lines
5.4 KiB
Svelte

<script lang="ts">
import { run } from 'svelte/legacy'
import { Loader2, RotateCwIcon } from 'lucide-svelte'
import { Button, DrawerContent } from './common'
import Drawer from './common/drawer/Drawer.svelte'
import Path from './Path.svelte'
import { sendUserToast } from '$lib/toast'
import { Highlight } from 'svelte-highlight'
import { json } from 'svelte-highlight/languages'
import autosize from '$lib/autosize'
import { ResourceService, VariableService } from '$lib/gen'
import { oauthStore, workspaceStore } from '$lib/stores'
import Password from './Password.svelte'
import { createEventDispatcher } from 'svelte'
import HighlightTheme from './HighlightTheme.svelte'
let drawer: Drawer | undefined = $state()
let token: undefined | string = $state(undefined)
export async function open() {
token = $oauthStore?.access_token ?? ''
drawer?.openDrawer?.()
step = 'init'
description = ''
}
let step: 'init' | 'resource' = $state('init')
async function listDatabases() {
if (!token) return
databases = undefined
const res = await fetch('/api/oauth/list_supabase', {
headers: {
'Content-Type': 'application/json',
'X-Supabase-Token': token
}
})
databases = await res.json()
}
type Database = {
name: string
database?: { host: string }
region: string
id: string
}
let databases: undefined | Database[] = $state(undefined)
run(() => {
token != undefined && listDatabases()
})
let selectedDatabase: undefined | Database = $state(undefined)
let description = $state('')
let pathError = $state('')
let password = $state('')
let path: string | undefined = $state(undefined)
/**
* https://github.com/orgs/supabase/discussions/17817
* host is in the format of `aws-0-${region}.pooler.supabase.com`
* user is in the format of `postgres.${id}`
*/
let resourceValue = $derived.by(() => ({
host: `aws-0-${selectedDatabase?.region}.pooler.supabase.com`,
user: `postgres.${selectedDatabase?.id}`,
port: 5432,
dbname: 'postgres',
sslmode: 'prefer',
password: `$var:${path}`
}))
let disabled = $derived(path == undefined || pathError != '' || path == '')
const dispatch = createEventDispatcher()
async function save() {
if (!path) return
await VariableService.createVariable({
workspace: $workspaceStore!,
requestBody: {
path,
value: password,
is_secret: true,
description: 'Password for supabase postgres database',
is_oauth: false
}
})
await ResourceService.createResource({
workspace: $workspaceStore!,
requestBody: {
resource_type: 'postgresql',
path,
value: resourceValue,
description
}
})
sendUserToast('Saved postgres resource')
dispatch('refresh')
drawer?.closeDrawer?.()
}
</script>
<HighlightTheme />
<Drawer bind:this={drawer} size="800px">
<DrawerContent title="Add a Supabase Database" on:close={drawer.closeDrawer}>
{#if step === 'init' || selectedDatabase == undefined}
<h2
>Connect an existing database <div class="inline-block ml-2"
><Button variant="default" wrapperClasses="self-stretch" on:click={listDatabases}
><RotateCwIcon size={12} /></Button
></div
>
</h2>
<div class="mt-6"></div>
{#if databases == undefined}
<Loader2 class="animate-spin" />
{:else}
<div class=" flex flex-col gap-y-2"></div>
{#each databases as database}
<button
class="btn btn-outline-primary mt-2 border p-2 w-full border-secondary-inverse hover:border-secondary rounded"
onclick={() => {
selectedDatabase = database
step = 'resource'
}}
>
<div class="flex flex-row items-center">
<div class="flex-grow">
<h3 class="text-lg font-semibold">{database.name}</h3>
<p class="text-sm text-secondary">id: {database.id} - region: {database.region}</p>
</div>
</div>
</button>
{/each}
{/if}
<h3 class="mt-8 mb-2">Create a new database</h3>
<p class="text-sm text-secondary"
><a href="https://supabase.com/dashboard/projects" target="_blank" rel="noopener noreferrer"
>Create a new database in your Supabase account
</a>
</p>
{:else if step === 'resource'}
<Path
bind:error={pathError}
bind:path
initialPath=""
fullNamePlaceholder={'supabase_' +
selectedDatabase?.name?.replace(/\s+/g, '').replace(/[^\w\s]/gi, '')}
kind="resource"
/>
<h2 class="mt-8 mb-2">Database Password</h2>
<p class="text-sm text-secondary mb-1"
>For security reasons from supabase, the password of the database cannot be retrieved
automatically. In a future update, a dedicated role for windmill will be created and the
password for it will be generated automatically. The password of the database is shown
during the project creation.</p
>
<Password required bind:password />
<h3 class="mt-6 mb-2">Description</h3>
<textarea autocomplete="off" use:autosize bind:value={description}></textarea>
<div class="mt-12"></div>
<p class="my-1 text-sm text-secondary"
>A resource and a variable will be created at path: {path}. The content of the resource will
be:</p
>
<Highlight language={json} code={JSON.stringify(resourceValue, null, 4)} />
{/if}
{#snippet actions()}
<div class="flex gap-1">
{#if step == 'resource' && selectedDatabase != undefined}
<Button variant="default" on:click={() => (step = 'init')}>Back</Button>
<Button {disabled} on:click={save} variant="accent">Save</Button>
{/if}
</div>
{/snippet}
</DrawerContent>
</Drawer>