|
@@ -0,0 +1,177 @@
|
|
|
|
|
+<script lang="ts">
|
|
|
|
|
+ import { Send, SquarePen } from '@lucide/svelte';
|
|
|
|
|
+
|
|
|
|
|
+ import { goto } from '$app/navigation';
|
|
|
|
|
+ import { resolve } from '$app/paths';
|
|
|
|
|
+
|
|
|
|
|
+ import * as toolsApi from '$lib/api/tools';
|
|
|
|
|
+ import { Card } from '$lib/components/common/card';
|
|
|
|
|
+ import { ConfirmDialog } from '$lib/components/common/confirmation';
|
|
|
|
|
+ import { Icon } from '$lib/components/common/icon';
|
|
|
|
|
+ import { MoreMenu } from '$lib/components/common/more-menu';
|
|
|
|
|
+ import {
|
|
|
|
|
+ PropertyTable,
|
|
|
|
|
+ toAuthProps,
|
|
|
|
|
+ toDeploymentProps,
|
|
|
|
|
+ } from '$lib/components/common/property-table';
|
|
|
|
|
+ import { toInfoProps } from '$lib/components/common/property-table/types';
|
|
|
|
|
+ import { StatusBadge } from '$lib/components/common/status-badge';
|
|
|
|
|
+ import { Button } from '$lib/components/controls/button';
|
|
|
|
|
+ import * as DropdownMenu from '$lib/components/controls/dropdown-menu';
|
|
|
|
|
+ import { ManagementTabs } from '$lib/components/management';
|
|
|
|
|
+ import { getBreadcrumbContext } from '$lib/stores/breadcrumb.svelte';
|
|
|
|
|
+ import { toolsStore } from '$lib/stores/tools.svelte';
|
|
|
|
|
+ import { toExternalToolCR, toManagedToolCR } from '$lib/types/custom-resources';
|
|
|
|
|
+ import type { ExternalTool, ManagedTool } from '$lib/types/entities';
|
|
|
|
|
+ import type { Changes, CoreResources } from '$lib/types/workspace';
|
|
|
|
|
+ import * as yaml from '$lib/yaml';
|
|
|
|
|
+
|
|
|
|
|
+ import type { PageData } from './$types';
|
|
|
|
|
+
|
|
|
|
|
+ let { data }: { data: PageData } = $props();
|
|
|
|
|
+
|
|
|
|
|
+ $effect.root(() => {
|
|
|
|
|
+ toolsStore.upsert(data.tool);
|
|
|
|
|
+ toolsStore.refreshStatus(data.tool.id);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const tool = $derived(toolsStore.get(data.tool.id) ?? data.tool);
|
|
|
|
|
+ const status = $derived(toolsStore.getStatus(tool.id));
|
|
|
|
|
+
|
|
|
|
|
+ const infoProps = $derived(toInfoProps(tool));
|
|
|
|
|
+ const configProps = $derived.by(() => {
|
|
|
|
|
+ const props: Record<string, string | number | string[]> = {};
|
|
|
|
|
+ if (tool.mode === 'external') {
|
|
|
|
|
+ const ext = tool as ExternalTool;
|
|
|
|
|
+ props['URL'] = ext.endpoint;
|
|
|
|
|
+ Object.assign(props, toAuthProps(ext.auth));
|
|
|
|
|
+ }
|
|
|
|
|
+ props['Transport'] = { sse: 'Server-Side Events', http: 'Streamable HTTP' }[tool.transport];
|
|
|
|
|
+ return props;
|
|
|
|
|
+ });
|
|
|
|
|
+ const deploymentProps = $derived.by(() => {
|
|
|
|
|
+ if (tool.mode !== 'managed') return {};
|
|
|
|
|
+ const man = tool as ManagedTool;
|
|
|
|
|
+ return toDeploymentProps(man.runtime);
|
|
|
|
|
+ });
|
|
|
|
|
+ const cr = $derived.by(() => {
|
|
|
|
|
+ if (tool.mode === 'managed') return yaml.stringify(toManagedToolCR(tool as ManagedTool));
|
|
|
|
|
+ else if (tool.mode === 'external')
|
|
|
|
|
+ return yaml.stringify(toExternalToolCR(tool as ExternalTool));
|
|
|
|
|
+ else return undefined;
|
|
|
|
|
+ });
|
|
|
|
|
+ let deleteOpen = $state(false);
|
|
|
|
|
+
|
|
|
|
|
+ const breadcrumb = getBreadcrumbContext();
|
|
|
|
|
+ $effect(() => {
|
|
|
|
|
+ breadcrumb.set({
|
|
|
|
|
+ module: 'Tools',
|
|
|
|
|
+ pages: [{ name: tool.name, href: resolve('/workloads/tools/[id]', { id: tool.id }) }],
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const onTest = () => {
|
|
|
|
|
+ goto(resolve(`/utilities/testing/tool?tool=${tool.id}`));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const onDeploy = () => {
|
|
|
|
|
+ toolsStore.update(tool);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const onDelete = async () => {
|
|
|
|
|
+ await toolsStore.remove(tool.id);
|
|
|
|
|
+ goto(resolve('/workloads/tools'));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchResources = async (): Promise<CoreResources> => {
|
|
|
|
|
+ return toolsApi.getResources(data.workspaceId!, tool.id);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchChanges = async (): Promise<Changes> => {
|
|
|
|
|
+ return toolsApi.getChanges(data.workspaceId!, tool.id);
|
|
|
|
|
+ };
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<svelte:head>
|
|
|
|
|
+ <title>{tool.name} - LocoStack</title>
|
|
|
|
|
+</svelte:head>
|
|
|
|
|
+
|
|
|
|
|
+<div class="mx-auto max-w-2xl p-6">
|
|
|
|
|
+ <!-- Page header -->
|
|
|
|
|
+ <div class="mb-4 flex items-center justify-between gap-4">
|
|
|
|
|
+ <div class="flex items-center gap-4">
|
|
|
|
|
+ <Icon icon={tool.icon} />
|
|
|
|
|
+ <div class="flex flex-1 flex-col overflow-hidden">
|
|
|
|
|
+ <div class="truncate text-lg font-medium text-white/75">{tool.name}</div>
|
|
|
|
|
+ <div class="text-xs text-white/50">
|
|
|
|
|
+ {tool.mode === 'external' ? 'External' : 'Managed'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
|
|
+ {#if tool.status}
|
|
|
|
|
+ <StatusBadge status={tool.status || 'not-deployed'} />
|
|
|
|
|
+ {/if}
|
|
|
|
|
+ <Button
|
|
|
|
|
+ href={resolve('/workloads/tools/[id]/edit', { id: tool.id })}
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ >
|
|
|
|
|
+ <SquarePen size={14} strokeWidth={2} />
|
|
|
|
|
+ Edit
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <MoreMenu ondelete={() => (deleteOpen = true)}>
|
|
|
|
|
+ {#snippet menu()}
|
|
|
|
|
+ {#if tool.status || tool.mode === 'external'}
|
|
|
|
|
+ <DropdownMenu.Item onclick={() => onTest()}>
|
|
|
|
|
+ <Send size={14} strokeWidth={2} />
|
|
|
|
|
+ Test
|
|
|
|
|
+ </DropdownMenu.Item>
|
|
|
|
|
+ {:else}
|
|
|
|
|
+ <DropdownMenu.Item onclick={() => onDeploy()}>
|
|
|
|
|
+ <Send size={14} strokeWidth={2} />
|
|
|
|
|
+ Deploy
|
|
|
|
|
+ </DropdownMenu.Item>
|
|
|
|
|
+ {/if}
|
|
|
|
|
+ {/snippet}
|
|
|
|
|
+ </MoreMenu>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Card class="mb-4" title="Basic Information">
|
|
|
|
|
+ <PropertyTable properties={infoProps} />
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {#if tool.mode === 'external'}
|
|
|
|
|
+ <Card class="mb-8" title="Configuration">
|
|
|
|
|
+ <PropertyTable properties={configProps} />
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ {/if}
|
|
|
|
|
+
|
|
|
|
|
+ {#if tool.mode === 'managed'}
|
|
|
|
|
+ <Card class="mb-8" title="Deployment">
|
|
|
|
|
+ <PropertyTable properties={deploymentProps} />
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ {/if}
|
|
|
|
|
+
|
|
|
|
|
+ <ManagementTabs
|
|
|
|
|
+ conditions={status?.conditions}
|
|
|
|
|
+ {cr}
|
|
|
|
|
+ onloadresources={tool.mode === 'managed' ? fetchResources : undefined}
|
|
|
|
|
+ onloadchanges={fetchChanges}
|
|
|
|
|
+ />
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Delete confirmation dialog -->
|
|
|
|
|
+<ConfirmDialog
|
|
|
|
|
+ confirmLabel="Delete"
|
|
|
|
|
+ confirmingLabel="Deleting…"
|
|
|
|
|
+ title="Delete tool?"
|
|
|
|
|
+ variant="destructive"
|
|
|
|
|
+ onconfirm={() => onDelete()}
|
|
|
|
|
+ bind:open={deleteOpen}
|
|
|
|
|
+>
|
|
|
|
|
+ {#snippet description()}
|
|
|
|
|
+ <strong class="text-white/90">"{tool.name}"</strong> will be permanently deleted.
|
|
|
|
|
+ {/snippet}
|
|
|
|
|
+</ConfirmDialog>
|