|
|
@@ -0,0 +1,141 @@
|
|
|
+<script lang="ts">
|
|
|
+ import { ChevronDown, ChevronUp, StickyNote } from '@lucide/svelte';
|
|
|
+ import type { ClassValue } from 'svelte/elements';
|
|
|
+ import { slide } from 'svelte/transition';
|
|
|
+
|
|
|
+ import Empty from '$lib/components/common/empty/empty.svelte';
|
|
|
+ import { Button } from '$lib/components/controls/button';
|
|
|
+ import { Switch } from '$lib/components/controls/switch';
|
|
|
+ import * as Table from '$lib/components/controls/table';
|
|
|
+ import type { StackCR } from '$lib/types/custom-resources';
|
|
|
+ import type { RuntimeTemplate, Stack } from '$lib/types/entities';
|
|
|
+
|
|
|
+ interface StackComponent {
|
|
|
+ name: string;
|
|
|
+ label: string;
|
|
|
+ description: string;
|
|
|
+ getSpec: (stack: Stack) => { enabled: boolean; runtime?: RuntimeTemplate };
|
|
|
+ getStatus: (status: StackCR['status']) => string;
|
|
|
+ }
|
|
|
+
|
|
|
+ let {
|
|
|
+ class: className = '',
|
|
|
+ stack,
|
|
|
+ status,
|
|
|
+ active = $bindable(false),
|
|
|
+ onchange,
|
|
|
+ }: {
|
|
|
+ class?: ClassValue;
|
|
|
+ stack: Stack;
|
|
|
+ status?: StackCR['status'];
|
|
|
+ active?: boolean;
|
|
|
+ onchange?: (stack: Stack) => void;
|
|
|
+ } = $props();
|
|
|
+
|
|
|
+ const STACK_COMPONENTS: StackComponent[] = [
|
|
|
+ {
|
|
|
+ name: 'gateway',
|
|
|
+ label: 'Gateway',
|
|
|
+ description:
|
|
|
+ 'Routing model calls, tool calls, and agent calls. Responsible for observability and cost management.',
|
|
|
+ getSpec: (stack: Stack) => stack.gateway || { enabled: false },
|
|
|
+ getStatus: (status: StackCR['status']) => status?.componentStatus?.gateway || 'disabled',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'vectorStore',
|
|
|
+ label: 'Vector Store',
|
|
|
+ description:
|
|
|
+ 'Storing embeddings and performing vector searches. Required by knowledge bases.',
|
|
|
+ getSpec: (stack: Stack) => stack.vectorStore || { enabled: false },
|
|
|
+ getStatus: (status: StackCR['status']) => status?.componentStatus?.vectorStore || 'disabled',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'graphStore',
|
|
|
+ label: 'Graph Store',
|
|
|
+ description: 'Storing and querying graph data. Optionally required by knowledge bases.',
|
|
|
+ getSpec: (stack: Stack) => stack.graphStore || { enabled: false },
|
|
|
+ getStatus: (status: StackCR['status']) => status?.componentStatus?.graphStore || 'disabled',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'database',
|
|
|
+ label: 'Database',
|
|
|
+ description: 'Storing application data. Required by some gateway and observability features.',
|
|
|
+ getSpec: (stack: Stack) => stack.database || { enabled: false },
|
|
|
+ getStatus: (status: StackCR['status']) => status?.componentStatus?.database || 'disabled',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'observability',
|
|
|
+ label: 'Observability',
|
|
|
+ description: 'Monitoring and tracing agent calls.',
|
|
|
+ getSpec: (stack: Stack) => stack.observability || { enabled: false },
|
|
|
+ getStatus: (status: StackCR['status']) =>
|
|
|
+ status?.componentStatus?.observability || 'disabled',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const onToggleComponent = (component: StackComponent) => {
|
|
|
+ console.log(component.name);
|
|
|
+ const enabled = component.getStatus(status) !== 'disabled';
|
|
|
+ stack = {
|
|
|
+ ...stack,
|
|
|
+ [component.name]: {
|
|
|
+ ...component.getSpec(stack),
|
|
|
+ enabled: !enabled,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ onchange?.(stack);
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<div class={['overflow-hidden rounded-md border border-white/10', className]}>
|
|
|
+ <div class="bg-white/5">
|
|
|
+ <div class="flex items-center justify-between p-2">
|
|
|
+ <div class="p-2 font-thin">{stack.name}</div>
|
|
|
+ <div class="flex items-center gap-1">
|
|
|
+ <Button size="icon-sm" variant="ghost" onclick={() => (active = !active)}>
|
|
|
+ {#if active}
|
|
|
+ <ChevronUp size={14} />
|
|
|
+ {:else}
|
|
|
+ <ChevronDown size={14} />
|
|
|
+ {/if}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {#if active}
|
|
|
+ <div class="overflow-hidden" transition:slide={{ duration: 200 }}>
|
|
|
+ <Table.Root>
|
|
|
+ <Table.Body>
|
|
|
+ {#each STACK_COMPONENTS as component (component.name)}
|
|
|
+ <Table.Row>
|
|
|
+ <Table.Cell class="w-12">
|
|
|
+ <Switch
|
|
|
+ checked={component.getStatus(status) !== 'disabled'}
|
|
|
+ onCheckedChange={() => onToggleComponent(component)}
|
|
|
+ />
|
|
|
+ </Table.Cell>
|
|
|
+ <Table.Cell class="w-32 text-right text-white/75">
|
|
|
+ {component.label}
|
|
|
+ </Table.Cell>
|
|
|
+ <Table.Cell>
|
|
|
+ {@const componentStatus = component.getStatus(status)}
|
|
|
+ {#if componentStatus === 'disabled'}
|
|
|
+ <span class="text-white/50">Disabled</span>
|
|
|
+ {:else}
|
|
|
+ <span>{component.getStatus(status)}</span>
|
|
|
+ {/if}
|
|
|
+ </Table.Cell>
|
|
|
+ <Table.Cell class="text-xs text-white/25">{component.description}</Table.Cell>
|
|
|
+ </Table.Row>
|
|
|
+ {:else}
|
|
|
+ <Empty icon={StickyNote}>
|
|
|
+ {#snippet caption()}
|
|
|
+ <div>No components</div>
|
|
|
+ {/snippet}
|
|
|
+ </Empty>
|
|
|
+ {/each}
|
|
|
+ </Table.Body>
|
|
|
+ </Table.Root>
|
|
|
+ </div>
|
|
|
+ {/if}
|
|
|
+</div>
|