|
|
@@ -0,0 +1,257 @@
|
|
|
+<script lang="ts">
|
|
|
+ import { Plus, X } from '@lucide/svelte';
|
|
|
+
|
|
|
+ import * as secretsApi from '$lib/api/secrets';
|
|
|
+ import { Button } from '$lib/components/controls/button';
|
|
|
+ import * as Field from '$lib/components/controls/field';
|
|
|
+ import { Input } from '$lib/components/controls/input';
|
|
|
+ import * as Select from '$lib/components/controls/select';
|
|
|
+ import { Textarea } from '$lib/components/controls/textarea';
|
|
|
+ import { type Secret } from '$lib/types/entities';
|
|
|
+
|
|
|
+ import type { DeploymentFormValue } from './types';
|
|
|
+
|
|
|
+ interface Runtime {
|
|
|
+ value: string;
|
|
|
+ label: string;
|
|
|
+ versions: {
|
|
|
+ value: string;
|
|
|
+ label: string;
|
|
|
+ }[];
|
|
|
+ }
|
|
|
+
|
|
|
+ let {
|
|
|
+ workspaceId,
|
|
|
+ disabled = false,
|
|
|
+ form = $bindable(),
|
|
|
+ runtimes = [],
|
|
|
+ imageRequired = false,
|
|
|
+ }: {
|
|
|
+ workspaceId: string;
|
|
|
+ disabled?: boolean;
|
|
|
+ form: DeploymentFormValue;
|
|
|
+ runtimes?: Runtime[];
|
|
|
+ imageRequired?: boolean;
|
|
|
+ } = $props();
|
|
|
+
|
|
|
+ let secrets = $state<Secret[]>([]);
|
|
|
+ let secretItems = $derived.by<Record<string, string[]>>(() => {
|
|
|
+ const items: Record<string, string[]> = {};
|
|
|
+ for (const secret of secrets) {
|
|
|
+ items[secret.id] = secret.items.map((item) => item.key);
|
|
|
+ }
|
|
|
+ return items;
|
|
|
+ });
|
|
|
+ const selectedRuntime = $derived(
|
|
|
+ form.runtime === ''
|
|
|
+ ? { value: '', label: '<Custom>', versions: [{ value: '', label: '<Custom>' }] }
|
|
|
+ : runtimes.find((r) => r.value === form.runtime),
|
|
|
+ );
|
|
|
+ const selectedRuntimeVersion = $derived(
|
|
|
+ selectedRuntime?.versions.find((v) => v.value === form.runtimeVersion) ?? {
|
|
|
+ value: '',
|
|
|
+ label: '<Default>',
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ const loadSecrets = async () => {
|
|
|
+ secrets = await secretsApi.list(workspaceId, fetch);
|
|
|
+ };
|
|
|
+
|
|
|
+ $effect(() => {
|
|
|
+ loadSecrets();
|
|
|
+ });
|
|
|
+
|
|
|
+ const onSelectSecret = (idx: number, secretId: string) => {
|
|
|
+ if (!form.env) return;
|
|
|
+ const currentSecret = form.env[idx].value.split('/', 2)[0] || '';
|
|
|
+ if (currentSecret === secretId) return;
|
|
|
+ form.env[idx].value = `${secretId}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const onSelectSecretKey = (idx: number, secretKey: string) => {
|
|
|
+ if (!form.env) return;
|
|
|
+ const currentSecret = form.env[idx].value.split('/', 2)[0] || '';
|
|
|
+ form.env[idx].value = `${currentSecret}/${secretKey}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const onRemoveEnvVar = (idx: number) => {
|
|
|
+ if (!form.env) return;
|
|
|
+ form.env.splice(idx, 1);
|
|
|
+ form.env = [...form.env];
|
|
|
+ };
|
|
|
+
|
|
|
+ const onAddEnvVar = () => {
|
|
|
+ if (!form.env) {
|
|
|
+ form.env = [];
|
|
|
+ }
|
|
|
+ form.env.push({ name: '', type: 'text', value: '' });
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="runtime">Runtime</Field.Label>
|
|
|
+ <Select.Root {disabled} type="single" bind:value={form.runtime}>
|
|
|
+ <Select.Trigger id="runtime" class="w-full"
|
|
|
+ >{selectedRuntime?.label ?? form.runtime}</Select.Trigger
|
|
|
+ >
|
|
|
+ <Select.Content>
|
|
|
+ <Select.Item label="<Custom>" value="" />
|
|
|
+ {#each runtimes as r (r.value)}
|
|
|
+ <Select.Item label={r.label} value={r.value} />
|
|
|
+ {/each}
|
|
|
+ </Select.Content>
|
|
|
+ </Select.Root>
|
|
|
+</Field.Field>
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="runtimeVersion">Runtime Version</Field.Label>
|
|
|
+ <Select.Root {disabled} type="single" bind:value={form.runtimeVersion}>
|
|
|
+ <Select.Trigger id="runtimeVersion" class="w-full">
|
|
|
+ {selectedRuntimeVersion?.label ?? form.runtimeVersion}
|
|
|
+ </Select.Trigger>
|
|
|
+ <Select.Content>
|
|
|
+ <Select.Item label="<Default>" value="" />
|
|
|
+ {#each selectedRuntime?.versions ?? [] as v (v.value)}
|
|
|
+ <Select.Item label={v.label} value={v.value} />
|
|
|
+ {/each}
|
|
|
+ </Select.Content>
|
|
|
+ </Select.Root>
|
|
|
+</Field.Field>
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="replicas">Replicas</Field.Label>
|
|
|
+ <Input
|
|
|
+ id="replicas"
|
|
|
+ {disabled}
|
|
|
+ min={1}
|
|
|
+ placeholder="1"
|
|
|
+ type="number"
|
|
|
+ bind:value={form.replicas}
|
|
|
+ />
|
|
|
+</Field.Field>
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="image">Container Image</Field.Label>
|
|
|
+ <Input
|
|
|
+ id="image"
|
|
|
+ {disabled}
|
|
|
+ placeholder={imageRequired ? undefined : 'Default'}
|
|
|
+ required={imageRequired}
|
|
|
+ type="text"
|
|
|
+ bind:value={form.image}
|
|
|
+ />
|
|
|
+</Field.Field>
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="command">Container Command</Field.Label>
|
|
|
+ <Textarea
|
|
|
+ id="command"
|
|
|
+ class="break-all"
|
|
|
+ {disabled}
|
|
|
+ placeholder="Runtime default"
|
|
|
+ rows={4}
|
|
|
+ bind:value={() => form.command?.join('\n') || '', (v) => (form.command = v.split('\n'))}
|
|
|
+ />
|
|
|
+</Field.Field>
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="args">Container Args</Field.Label>
|
|
|
+ <Textarea
|
|
|
+ id="args"
|
|
|
+ class="break-all"
|
|
|
+ {disabled}
|
|
|
+ placeholder="Runtime default"
|
|
|
+ rows={4}
|
|
|
+ bind:value={() => form.args?.join('\n') || '', (v) => (form.args = v.split('\n'))}
|
|
|
+ />
|
|
|
+</Field.Field>
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="args">Container Extra Args</Field.Label>
|
|
|
+ <Textarea
|
|
|
+ id="extraArgs"
|
|
|
+ class="break-all"
|
|
|
+ {disabled}
|
|
|
+ rows={4}
|
|
|
+ bind:value={() => form.extraArgs?.join('\n') || '', (v) => (form.extraArgs = v.split('\n'))}
|
|
|
+ />
|
|
|
+</Field.Field>
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="env">Environment Variables</Field.Label>
|
|
|
+ {#each form.env || [] as entry, idx (idx)}
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <Select.Root {disabled} type="single" bind:value={entry.type}>
|
|
|
+ <Select.Trigger class="w-24 flex-none capitalize">{entry.type}</Select.Trigger>
|
|
|
+ <Select.Content>
|
|
|
+ <Select.Item label="Text" value="text" />
|
|
|
+ <Select.Item label="Secret" value="secret" />
|
|
|
+ </Select.Content>
|
|
|
+ </Select.Root>
|
|
|
+ <Input class="flex-1" {disabled} placeholder="KEY" bind:value={entry.name} />
|
|
|
+ {#if entry.type === 'text'}
|
|
|
+ <Input class="flex-1" {disabled} placeholder="VALUE" bind:value={entry.value} />
|
|
|
+ {:else}
|
|
|
+ {@const secretPath = entry.value.split('/', 2)}
|
|
|
+ <Select.Root
|
|
|
+ onValueChange={(v) => onSelectSecret(idx, v)}
|
|
|
+ type="single"
|
|
|
+ value={secretPath[0]}
|
|
|
+ >
|
|
|
+ <Select.Trigger class="flex-1">
|
|
|
+ {secretPath[0] || 'Select secret...'}
|
|
|
+ </Select.Trigger>
|
|
|
+ <Select.Content>
|
|
|
+ {#each secrets as secret (secret.id)}
|
|
|
+ <Select.Item value={secret.id}>{secret.name}</Select.Item>
|
|
|
+ {/each}
|
|
|
+ </Select.Content>
|
|
|
+ </Select.Root>
|
|
|
+ <Select.Root
|
|
|
+ onValueChange={(v) => onSelectSecretKey(idx, v)}
|
|
|
+ type="single"
|
|
|
+ value={secretPath[1]}
|
|
|
+ >
|
|
|
+ <Select.Trigger class="flex-1">
|
|
|
+ {secretPath[1] || 'Select key...'}
|
|
|
+ </Select.Trigger>
|
|
|
+ <Select.Content>
|
|
|
+ {#each secretItems[secretPath[0]] as secretKey (secretKey)}
|
|
|
+ <Select.Item value={secretKey}>{secretKey}</Select.Item>
|
|
|
+ {/each}
|
|
|
+ </Select.Content>
|
|
|
+ </Select.Root>
|
|
|
+ {/if}
|
|
|
+ <button class="opacity-25 hover:opacity-75" type="button" onclick={() => onRemoveEnvVar(idx)}>
|
|
|
+ <X size={12} strokeWidth={2} />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ {/each}
|
|
|
+ <Button variant="outline" onclick={() => onAddEnvVar()}>
|
|
|
+ <Plus size={14} strokeWidth={2} />
|
|
|
+ Add Environment Variable
|
|
|
+ </Button>
|
|
|
+</Field.Field>
|
|
|
+<Field.Field>
|
|
|
+ <Field.Label for="port">Port</Field.Label>
|
|
|
+ <Input
|
|
|
+ id="port"
|
|
|
+ {disabled}
|
|
|
+ min={0}
|
|
|
+ placeholder="Runtime default"
|
|
|
+ type="number"
|
|
|
+ bind:value={form.port}
|
|
|
+ />
|
|
|
+</Field.Field>
|
|
|
+<div class="grid grid-cols-2 gap-4">
|
|
|
+ <Field.Field>
|
|
|
+ <Field.Label for="cpu">CPU</Field.Label>
|
|
|
+ <Input id="cpu" {disabled} placeholder="Unbound" bind:value={form.cpu} />
|
|
|
+ </Field.Field>
|
|
|
+ <Field.Field>
|
|
|
+ <Field.Label for="memory">Memory</Field.Label>
|
|
|
+ <Input id="memory" {disabled} placeholder="Unbound" bind:value={form.memory} />
|
|
|
+ </Field.Field>
|
|
|
+ <Field.Field>
|
|
|
+ <Field.Label for="gpuModel">GPU Model</Field.Label>
|
|
|
+ <Input id="gpuModel" {disabled} placeholder="Any" bind:value={form.gpuModel} />
|
|
|
+ </Field.Field>
|
|
|
+ <Field.Field>
|
|
|
+ <Field.Label for="gpu">GPU</Field.Label>
|
|
|
+ <Input id="gpu" {disabled} placeholder="0" bind:value={form.gpu} />
|
|
|
+ </Field.Field>
|
|
|
+</div>
|