fix(frontend): tidy the resource drawer around the Supabase entry point

Connect Supabase was a hand-styled anchor carrying Supabase's brand hex values
rather than a Button, and it sat in a row whose other controls had settled on
unifiedSize md. Making it a Button meant SupabaseIcon had to satisfy IconType, so it
now takes `size` (deriving height/width from it) alongside the string props its other
callers pass.

The manual resource form spaced every field 32px apart and WhitelistIp added another
16px of its own, which read as a gap rather than a rhythm. One gap of 16px, with the
form itself given a little more separation from the description above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-08-07 18:47:31 +02:00
parent bc18ebec16
commit 5cc4cd55df
5 changed files with 169 additions and 40 deletions
@@ -11,12 +11,18 @@
import Button from './common/button/Button.svelte'
import { Loader2 } from 'lucide-svelte'
import { untrack } from 'svelte'
import { base } from '$lib/base'
import GitHubAppIntegration from './GitHubAppIntegration.svelte'
import BedrockCredentialsCheck from './BedrockCredentialsCheck.svelte'
import { isCloudHosted } from '$lib/cloud'
import ResourceGen from './copilot/ResourceGen.svelte'
import SyncResourceTypes from './SyncResourceTypes.svelte'
import Modal2 from './common/modal/Modal2.svelte'
import SupabaseProjectStep, {
type SupabasePick
} from './workspaceSettings/SupabaseProjectStep.svelte'
import { supabaseResourceValue } from './workspaceSettings/supabaseProvisioning'
import { useSupabaseOauth } from './workspaceSettings/supabaseOauth.svelte'
import { sendUserToast } from '$lib/toast'
interface Props {
resourceType: string
@@ -127,6 +133,51 @@
let rawCodeEditor: { setCode: (code: string) => void } | undefined = $state(undefined)
let textFileContent: string | undefined = $state(undefined)
let supabaseOpen = $state(false)
let supaStep: ReturnType<typeof SupabaseProjectStep> | undefined = $state(undefined)
let supaResult: SupabasePick | undefined = $state(undefined)
// Authorizing is not something to present a dialog about first: the button goes straight
// to the popup, and the dialog opens on the way back, already holding the projects.
const supaOauth = useSupabaseOauth()
let awaitingSupabaseAuth = $state(false)
function connectSupabase() {
if (supaOauth.authed) {
supabaseOpen = true
return
}
awaitingSupabaseAuth = true
supaOauth.connect()
}
$effect(() => {
if (awaitingSupabaseAuth && supaOauth.authed) {
awaitingSupabaseAuth = false
supabaseOpen = true
}
})
// The resource is being edited here rather than created for us, so the project's password
// goes straight into the form as a value. The user can link it to a secret variable with
// the same affordance every other password field has.
function applySupabasePick(pick: SupabasePick) {
args = {
...(args ?? {}),
...supabaseResourceValue(pick.project, ''),
password: pick.password
}
rawCode = JSON.stringify(args, null, 2)
rawCodeEditor?.setCode(rawCode)
supabaseOpen = false
supaResult = undefined
sendUserToast(`Filled in the connection for ${pick.project.name}`)
}
$effect(() => {
if (supaResult) applySupabasePick(supaResult)
})
function parseTextFileContent() {
args = {
content: textFileContent
@@ -172,7 +223,7 @@
}}
>
{#snippet trigger()}
<Button spacingSize="sm" size="xs" variant="default" nonCaptureEvent>
<Button spacingSize="sm" size="xs" unifiedSize="md" variant="default" nonCaptureEvent>
From connection string
</Button>
{/snippet}
@@ -206,14 +257,15 @@
</Popover>
{/if}
{#if resourceType == 'postgresql' && supabaseWizard}
<a
target="_blank"
href="{base}/api/oauth/connect/supabase_wizard"
class="border rounded-lg flex flex-row gap-2 items-center text-xs px-3 py-1.5 h-8 bg-[#F1F3F5] hover:bg-[#E6E8EB] dark:bg-[#1C1C1C] dark:hover:bg-black"
<Button
unifiedSize="md"
variant="default"
startIcon={{ icon: SupabaseIcon }}
loading={awaitingSupabaseAuth}
on:click={connectSupabase}
>
<SupabaseIcon height="16px" width="16px" />
<div class="text-[#11181C] dark:text-[#EDEDED] font-semibold">Connect Supabase</div>
</a>
Connect Supabase
</Button>
{/if}
<GitHubAppIntegration
{resourceType}
@@ -286,3 +338,37 @@
bind:args
/>
{/if}
<Modal2
bind:isOpen={supabaseOpen}
target="#content"
title="Connect Supabase"
contentClasses="flex flex-col"
fixedWidth="md"
fixedHeight="md"
>
<div class="flex h-full flex-col gap-3">
<div class="flex-1 flex flex-col gap-3 min-h-0">
<SupabaseProjectStep
bind:this={supaStep}
bind:result={supaResult}
defaultProjectName={`windmill-${$workspaceStore ?? 'workspace'}`}
continueLabel="Use this project"
/>
</div>
{#if supaStep}
{@const action = supaStep.getAction()}
<div class="flex justify-end pt-3">
<Button
size="sm"
variant="accent"
disabled={action.disabled}
loading={action.busy}
onClick={() => action.act?.()}
>
{action.label}
</Button>
</div>
{/if}
</div>
</Modal2>
@@ -990,7 +990,7 @@
/>
</div>
{:else if step == 2 && manual}
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<Label label="Path">
<Path
bind:error={pathError}
@@ -1086,18 +1086,22 @@
Acquire the token automatically via client credentials instead
</button>
{/if}
{#key resourceTypeInfo}
<ApiConnectForm
bind:linkedSecrets
bind:description
{linkedSecretCandidates}
{resourceType}
{resourceTypeInfo}
bind:args
bind:isValid
onSynced={getResourceTypeInfo}
/>
{/key}
<!-- The form is a section of its own, not just the next field: it needs more of a break
from the description than the uniform gap gives. -->
<div class="mt-2">
{#key resourceTypeInfo}
<ApiConnectForm
bind:linkedSecrets
bind:description
{linkedSecretCandidates}
{resourceType}
{resourceTypeInfo}
bind:args
bind:isValid
onSynced={getResourceTypeInfo}
/>
{/key}
</div>
</div>
{:else if step == 2 && !manual}
{#if manual == false && resourceType != ''}
@@ -18,7 +18,6 @@
</script>
{#if ips}
<div class="mt-4"></div>
<Alert size="xs" type="info" title="IPs to whitelist">
<span class="text-primary">If necessary, the workers IPs to whitelist are:</span>
{ips.join(', ')}
@@ -124,6 +124,7 @@
<Button
color={genLoading ? 'red' : 'light'}
size="xs"
unifiedSize="md"
nonCaptureEvent={!genLoading}
startIcon={{ icon: Wand2 }}
iconOnly
@@ -1,24 +1,63 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
/** Accepting `size` is what makes this usable as a Button `startIcon`. */
size?: number
class?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let {
size,
height = size ? `${size}px` : '24px',
width = size ? `${size}px` : '24px',
class: className
}: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 168 168" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M97.4434 164.242C93.2027 169.582 84.6042 166.656 84.502 159.837L83.0078 60.1013H150.07C162.217 60.1013 168.992 74.1309 161.439 83.644L97.4434 164.242Z" fill="url(#paint0_linear_210_201)"/>
<path d="M97.4434 164.242C93.2027 169.582 84.6042 166.656 84.502 159.837L83.0078 60.1013H150.07C162.217 60.1013 168.992 74.1309 161.439 83.644L97.4434 164.242Z" fill="url(#paint1_linear_210_201)" fill-opacity="0.2"/>
<path d="M70.1702 3.75823C74.411 -1.58278 83.0096 1.3437 83.1118 8.1628L83.7666 107.899H17.5434C5.39617 107.899 -1.37853 93.8692 6.17495 84.3561L70.1702 3.75823Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear_210_201" x1="83.0078" y1="82.2153" x2="142.611" y2="107.213" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear_210_201" x1="56.5831" y1="46.0351" x2="83.765" y2="97.2037" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
<svg
{width}
{height}
class={className}
viewBox="0 0 168 168"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M97.4434 164.242C93.2027 169.582 84.6042 166.656 84.502 159.837L83.0078 60.1013H150.07C162.217 60.1013 168.992 74.1309 161.439 83.644L97.4434 164.242Z"
fill="url(#paint0_linear_210_201)"
/>
<path
d="M97.4434 164.242C93.2027 169.582 84.6042 166.656 84.502 159.837L83.0078 60.1013H150.07C162.217 60.1013 168.992 74.1309 161.439 83.644L97.4434 164.242Z"
fill="url(#paint1_linear_210_201)"
fill-opacity="0.2"
/>
<path
d="M70.1702 3.75823C74.411 -1.58278 83.0096 1.3437 83.1118 8.1628L83.7666 107.899H17.5434C5.39617 107.899 -1.37853 93.8692 6.17495 84.3561L70.1702 3.75823Z"
fill="#3ECF8E"
/>
<defs>
<linearGradient
id="paint0_linear_210_201"
x1="83.0078"
y1="82.2153"
x2="142.611"
y2="107.213"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#249361" />
<stop offset="1" stop-color="#3ECF8E" />
</linearGradient>
<linearGradient
id="paint1_linear_210_201"
x1="56.5831"
y1="46.0351"
x2="83.765"
y2="97.2037"
gradientUnits="userSpaceOnUse"
>
<stop />
<stop offset="1" stop-opacity="0" />
</linearGradient>
</defs>
</svg>