Files
windmill/frontend/src/lib/components/ObjectStoreConfigSettings.svelte
Guilhem dd419ade94 fix(frontend): redesign instance settings (#7916)
* handle dirty config

* Move instance update in drawer actions

* Put windmill version in drawer header

* Use sidebar instead of tabs

* Rework user section

* improve user table

* Handle EE

* Add settings section header

* test-1

* option 2

* create new settings group

* harmonize all settings inputs

* improve members/users table styling

* refactor instance setup

* show user count

* nit

* Create setting card component

* harmonize instance settings and workspace settings

* fix python version loader

* nit

* clean code

* nit

* add email validation

* fix reactivity issue on default value

* fix dirty config check

* fix object storage dirty config check

* Fix object storage settings reactivity

* fix indexer dirty reactivity

* Add validation for indexer

* fix sso dirty issues

* clean

* nit
2026-02-12 09:50:01 +00:00

407 lines
12 KiB
Svelte

<script lang="ts">
import { Database, Loader2 } from 'lucide-svelte'
import Toggle from './Toggle.svelte'
import { Button, Tab, Tabs } from './common'
import { SettingService } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import TestConnection from './TestConnection.svelte'
import { enterpriseLicense } from '$lib/stores'
import SimpleEditor from './SimpleEditor.svelte'
import Label from './Label.svelte'
import TextInput from './text_input/TextInput.svelte'
type S3Config = {
type: 'S3'
bucket: string
region: string
access_key: string
secret_key: string
endpoint: string
allow_http?: boolean
}
type AzureConfig = {
type: 'Azure'
accountName: string
containerName: string
useSSL?: boolean
tenantId: string
clientId: string
accessKey: string
endpoint?: string
}
type AwsOidcConfig = {
type: 'AwsOidc'
bucket: string
region: string
roleArn: string
}
type GcsConfig = {
type: 'Gcs'
bucket: string
serviceAccountKey: Record<string, string> | undefined
}
interface Props {
bucket_config?: S3Config | AzureConfig | AwsOidcConfig | GcsConfig | undefined
}
let {
bucket_config = $bindable<S3Config | AzureConfig | AwsOidcConfig | GcsConfig | undefined>(
undefined
)
}: Props = $props()
let effectiveAllowHttp = $derived(
bucket_config?.type === 'S3' ? (bucket_config.allow_http ?? true) : false
)
let loading = $state(false)
async function testConnection() {
loading = true
try {
if (bucket_config) {
await SettingService.testObjectStorageConfig({
requestBody: bucket_config
})
sendUserToast('Connection successful', false)
}
} catch (e) {
sendUserToast(e.body, true)
} finally {
loading = false
}
}
let simpleEditor: SimpleEditor | undefined = $state(undefined)
let serviceAccountKeyCode = $state(
bucket_config?.type === 'Gcs'
? JSON.stringify(bucket_config.serviceAccountKey, null, '\t')
: '{}'
)
let lastEditorSyncedJson =
bucket_config?.type === 'Gcs' ? JSON.stringify(bucket_config.serviceAccountKey) : '{}'
$effect(() => {
if (bucket_config?.type === 'Gcs') {
const configJson = JSON.stringify(bucket_config.serviceAccountKey)
if (configJson !== lastEditorSyncedJson) {
lastEditorSyncedJson = configJson
const formatted = JSON.stringify(bucket_config.serviceAccountKey, null, '\t')
serviceAccountKeyCode = formatted
simpleEditor?.setCode(formatted)
}
}
})
</script>
<div class="my-0.5">
<Toggle
disabled={!$enterpriseLicense}
options={{ right: bucket_config ? '' : 'set object store' }}
checked={Boolean(bucket_config)}
on:change={(e) => {
if (e.detail) {
bucket_config = {
type: 'S3',
bucket: '',
region: '',
access_key: '',
secret_key: '',
endpoint: '',
allow_http: true
}
} else {
bucket_config = undefined
}
}}
/>
</div>
{#if bucket_config}
<div class="">
<div class="flex gap-2 py-1">
<Button
spacingSize="sm"
size="xs"
btnClasses="h-8"
variant="default"
on:click={testConnection}
>
{#if loading}
<Loader2 class="animate-spin mr-2 !h-4 !w-4" />
{:else}
<Database class="mr-2 !h-4 !w-4" />
{/if}
Test from a server
</Button>
<TestConnection
args={bucket_config}
resourceType="s3_bucket"
workspaceOverride="admins"
buttonTextOverride="Test from a worker"
/>
</div>
<Tabs
selected={bucket_config?.type ?? 'S3'}
on:selected={(e) => {
if (e.detail === 'S3' && bucket_config?.type !== 'S3') {
bucket_config = {
type: 'S3',
bucket: '',
region: '',
access_key: '',
secret_key: '',
endpoint: '',
allow_http: true
}
} else if (e.detail === 'Azure' && bucket_config?.type !== 'Azure') {
bucket_config = {
type: 'Azure',
accountName: '',
containerName: '',
useSSL: false,
tenantId: '',
clientId: '',
accessKey: ''
}
} else if (e.detail === 'Gcs' && bucket_config?.type !== 'Gcs') {
bucket_config = {
type: 'Gcs',
bucket: '',
serviceAccountKey: {}
}
} else if (e.detail === 'AwsOidc' && bucket_config?.type !== 'AwsOidc') {
bucket_config = {
type: 'AwsOidc',
bucket: '',
region: '',
roleArn: ''
}
}
}}
>
<Tab value="S3" label="S3" />
<Tab value="Azure" label="Azure Blob" />
<Tab value="AwsOidc" label="AWS OIDC" />
<Tab value="Gcs" label="Google Cloud Storage" />
</Tabs>
<div class="flex flex-col gap-6 mt-2 p-4 border rounded-md">
{#if bucket_config.type === 'S3'}
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Bucket</span>
<TextInput
inputProps={{ placeholder: 'bucket-name' }}
bind:value={
() => (bucket_config as S3Config).bucket,
(v) => (bucket_config = { ...(bucket_config as S3Config), bucket: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Region</span>
<span class="text-primary text-2xs"
>If left empty, will be derived automatically from $AWS_REGION</span
>
<TextInput
bind:value={
() => (bucket_config as S3Config).region,
(v) => (bucket_config = { ...(bucket_config as S3Config), region: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Access key ID</span>
<span class="text-primary text-2xs"
>If left empty, will be derived automatically from $AWS_ACCESS_KEY_ID, pod or ec2
profile</span
>
<TextInput
bind:value={
() => (bucket_config as S3Config).access_key,
(v) => (bucket_config = { ...(bucket_config as S3Config), access_key: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Secret key</span>
<span class="text-primary text-2xs"
>If left empty, will be derived automatically from $AWS_SECRET_KEY, pod or ec2 profile</span
>
<TextInput
inputProps={{ type: 'password', autocomplete: 'new-password' }}
bind:value={
() => (bucket_config as S3Config).secret_key,
(v) => (bucket_config = { ...(bucket_config as S3Config), secret_key: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Endpoint</span>
<span class="text-primary text-2xs"
>Only needed for non AWS S3 providers like R2 or MinIo</span
>
<TextInput
bind:value={
() => (bucket_config as S3Config).endpoint,
(v) => (bucket_config = { ...(bucket_config as S3Config), endpoint: v })
}
/>
</label>
<div class="block pb-2">
<span class="text-primary text-2xs">Disable if using https only policy</span>
<div>
<Toggle
checked={effectiveAllowHttp}
on:change={(e) => {
if (bucket_config?.type === 'S3') {
bucket_config = { ...bucket_config, allow_http: e.detail }
}
}}
options={{ right: 'Allow http' }}
/>
</div>
</div>
{:else if bucket_config.type === 'Azure'}
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Account name</span>
<TextInput
inputProps={{ placeholder: 'account-name' }}
bind:value={
() => (bucket_config as AzureConfig).accountName,
(v) => (bucket_config = { ...(bucket_config as AzureConfig), accountName: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Container name</span>
<TextInput
inputProps={{ placeholder: 'container-name' }}
bind:value={
() => (bucket_config as AzureConfig).containerName,
(v) => (bucket_config = { ...(bucket_config as AzureConfig), containerName: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Access key</span>
<TextInput
inputProps={{ type: 'password', autocomplete: 'new-password' }}
bind:value={
() => (bucket_config as AzureConfig).accessKey,
(v) => (bucket_config = { ...(bucket_config as AzureConfig), accessKey: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis"
>Tenant ID <span class="text-2xs text-primary">(optional)</span></span
>
<TextInput
bind:value={
() => (bucket_config as AzureConfig).tenantId,
(v) => (bucket_config = { ...(bucket_config as AzureConfig), tenantId: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis"
>Client ID <span class="text-2xs text-primary">(optional)</span></span
>
<TextInput
bind:value={
() => (bucket_config as AzureConfig).clientId,
(v) => (bucket_config = { ...(bucket_config as AzureConfig), clientId: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis"
>Endpoint <span class="text-2xs text-primary">(optional)</span></span
>
<span class="text-primary text-2xs"
>Only needed for non Azure Blob providers like Azurite</span
>
<TextInput
bind:value={
() => (bucket_config as AzureConfig).endpoint,
(v) => (bucket_config = { ...(bucket_config as AzureConfig), endpoint: v })
}
/>
</label>
{:else if bucket_config.type === 'AwsOidc'}
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Bucket</span>
<TextInput
inputProps={{ placeholder: 'bucket-name' }}
bind:value={
() => (bucket_config as AwsOidcConfig).bucket,
(v) => (bucket_config = { ...(bucket_config as AwsOidcConfig), bucket: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Region</span>
<TextInput
inputProps={{ placeholder: 'region' }}
bind:value={
() => (bucket_config as AwsOidcConfig).region,
(v) => (bucket_config = { ...(bucket_config as AwsOidcConfig), region: v })
}
/>
</label>
<label class="block pb-2">
<span class="text-xs font-semibold text-emphasis">Role ARN</span>
<TextInput
inputProps={{ placeholder: 'arn:aws:iam::123456789012:role/test' }}
bind:value={
() => (bucket_config as AwsOidcConfig).roleArn,
(v) => (bucket_config = { ...(bucket_config as AwsOidcConfig), roleArn: v })
}
/>
</label>
{:else if bucket_config.type === 'Gcs'}
<Label label="Bucket">
<TextInput
inputProps={{ placeholder: 'bucket-name' }}
bind:value={
() => (bucket_config as GcsConfig).bucket,
(v) => (bucket_config = { ...(bucket_config as GcsConfig), bucket: v })
}
/>
</Label>
<Label label="Service Account Key">
<span class="text-primary text-2xs">JSON content of the service account key file</span>
<SimpleEditor
bind:this={simpleEditor}
lang="json"
bind:code={serviceAccountKeyCode}
on:change={(e) => {
if (bucket_config?.type === 'Gcs') {
if (e.detail.code === undefined || e.detail.code === '') {
bucket_config = { ...bucket_config, serviceAccountKey: undefined }
return
}
try {
const parsed = JSON.parse(e.detail.code ?? '{}')
lastEditorSyncedJson = JSON.stringify(parsed)
bucket_config = { ...bucket_config, serviceAccountKey: parsed }
} catch (_) {
bucket_config = { ...bucket_config, serviceAccountKey: undefined }
}
}
}}
class="h-80"
/>
</Label>
{:else}
<div>Unknown bucket type {bucket_config['type']}</div>
{/if}
</div>
</div>
{/if}