ui: migrate Activity, Logs views to shadcn

- Activity table wrapped in Card with restyled column menu and Button
- LogPanel toolbar uses Button/Input with lucide icons
- LogViewer source switch uses a ToggleGroup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmuGqwNBJNEAMaWsdCDqUC
This commit is contained in:
Claude
2026-06-27 11:52:11 +00:00
parent 136dcdc25f
commit 8dd91e99e8
3 changed files with 78 additions and 100 deletions
+22 -44
View File
@@ -1,5 +1,8 @@
<script lang="ts">
import { persistentStore } from "../stores/persistent";
import { Type, WrapText, Search, SearchX, CircleX } from "@lucide/svelte";
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
interface Props {
id: string;
@@ -81,59 +84,34 @@
});
</script>
<div class="rounded-lg overflow-hidden flex flex-col bg-gray-950/5 dark:bg-white/10 h-full w-full p-1">
<div class="p-4">
<div class="bg-muted/50 flex h-full w-full flex-col overflow-hidden rounded-xl border p-1">
<div class="p-3">
<div class="flex items-center justify-between">
<h3 class="m-0 text-lg p-0">{title}</h3>
<h3 class="m-0 p-0 text-lg">{title}</h3>
<div class="flex gap-2 items-center">
<button class="btn border-0" onclick={toggleFontSize} title="Change font size">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
<path d="M2 4v3h5v12h3V7h5V4H2zm19 5h-9v3h3v7h3v-7h3V9z"/>
</svg>
</button>
<button class="btn border-0" onclick={toggleWrapText} title="Toggle text wrap">
{#if $wrapTextStore}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
<path fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
<path fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
</svg>
{/if}
</button>
<button class="btn border-0" onclick={toggleFilter} title="Toggle filter">
{#if $showFilterStore}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
<path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z" clip-rule="evenodd" />
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
{/if}
</button>
<div class="flex items-center gap-1">
<Button variant="ghost" size="icon-sm" onclick={toggleFontSize} title="Change font size">
<Type />
</Button>
<Button variant="ghost" size="icon-sm" onclick={toggleWrapText} title="Toggle text wrap">
<WrapText class={$wrapTextStore ? "text-primary" : ""} />
</Button>
<Button variant="ghost" size="icon-sm" onclick={toggleFilter} title="Toggle filter">
{#if $showFilterStore}<SearchX />{:else}<Search />{/if}
</Button>
</div>
</div>
{#if $showFilterStore}
<div class="mt-2 flex gap-2 items-center w-full">
<input
type="text"
class="w-full text-sm border border-gray-950/10 dark:border-white/5 p-2 rounded outline-none"
placeholder="Filter logs (regex)..."
bind:value={filterRegex}
/>
<button class="pl-2" onclick={() => (filterRegex = "")} aria-label="Clear filter">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z" clip-rule="evenodd" />
</svg>
</button>
<div class="mt-2 flex w-full items-center gap-2">
<Input type="text" class="h-8" placeholder="Filter logs (regex)..." bind:value={filterRegex} />
<Button variant="ghost" size="icon-sm" onclick={() => (filterRegex = "")} aria-label="Clear filter">
<CircleX />
</Button>
</div>
{/if}
</div>
<div class="rounded-lg bg-background font-mono text-sm flex-1 overflow-hidden">
<div class="bg-background flex-1 overflow-hidden rounded-lg font-mono text-sm">
<pre bind:this={preElement} onscroll={handleScroll} class="{textWrapClass} {fontSizeClass} h-full overflow-auto p-4">{filteredLogs}</pre>
</div>
</div>
+44 -30
View File
@@ -7,6 +7,9 @@
import { persistentStore } from "../stores/persistent";
import { onMount } from "svelte";
import type { ReqRespCapture } from "../lib/types";
import { Columns3, GripVertical } from "@lucide/svelte";
import * as Card from "$lib/components/ui/card/index.js";
import { Button } from "$lib/components/ui/button/index.js";
type ColumnKey = string;
@@ -219,46 +222,56 @@
<ActivityStats />
</div>
<div class="card overflow-auto relative min-h-[30rem]">
<div class="flex justify-end px-4" bind:this={dropdownContainer}>
<Card.Root class="relative min-h-[30rem] gap-0 overflow-auto py-2">
<div class="flex justify-end px-2" bind:this={dropdownContainer}>
<div class="relative">
<button
class="w-8 h-8 flex items-center justify-center rounded hover:bg-secondary-hover transition-colors"
<Button
variant="ghost"
size="icon-sm"
onclick={() => (columnsMenuOpen = !columnsMenuOpen)}
title="Select columns"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
</svg>
</button>
<Columns3 />
</Button>
{#if columnsMenuOpen}
<div class="absolute right-0 top-full mt-1 bg-surface border border-gray-200 dark:border-white/10 rounded shadow-lg z-10 py-1 min-w-[16rem]" role="list">
<div class="px-3 py-2 text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-white/10" role="presentation">
<div
class="bg-popover text-popover-foreground absolute right-0 top-full z-20 mt-1 min-w-[16rem] rounded-md border py-1 shadow-md"
role="list"
>
<div
class="text-muted-foreground border-b px-3 py-2 text-xs font-medium uppercase tracking-wider"
role="presentation"
>
Columns
</div>
{#each orderedColumns as col (col.key)}
{@const key = col.key}
<div
class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-secondary-hover transition-colors {dragOverKey === key && dragKey !== key ? 'bg-primary/10 ring-1 ring-primary/40' : ''} {dragKey === key ? 'opacity-40' : ''}"
class="hover:bg-accent flex items-center gap-2 px-3 py-1.5 text-sm transition-colors {dragOverKey ===
key && dragKey !== key
? 'bg-primary/10 ring-primary/40 ring-1'
: ''} {dragKey === key ? 'opacity-40' : ''}"
role="listitem"
ondragover={(e) => handleDragOver(e, key)}
ondrop={(e) => handleDrop(e, key)}
>
<span
class="text-txtsecondary select-none cursor-grab"
class="text-muted-foreground flex cursor-grab select-none"
draggable={true}
role="button"
tabindex="-1"
aria-label="Drag to reorder {col.label}"
ondragstart={(e) => handleDragStart(e, key)}
ondragend={handleDragEnd}
>⋮⋮</span>
<label class="flex items-center gap-2 flex-1 cursor-pointer">
>
<GripVertical class="size-4" />
</span>
<label class="flex flex-1 cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={isColumnVisible(key)}
onchange={() => toggleColumn(key)}
class="rounded"
class="accent-primary rounded"
/>
{col.label}
</label>
@@ -269,11 +282,11 @@
</div>
</div>
<table class="min-w-full divide-y">
<thead class="border-gray-200 dark:border-white/10">
<tr class="text-left text-xs uppercase tracking-wider">
<table class="min-w-full">
<thead>
<tr class="text-muted-foreground border-b text-left text-xs uppercase tracking-wider">
{#each activeVisibleColumns as key (key)}
<th class="px-6 py-3">
<th class="px-6 py-3 font-medium">
{#if key === "cached"}
Cached <Tooltip content="prompt tokens from cache" />
{:else if key === "prompt"}
@@ -287,16 +300,16 @@
{/each}
</tr>
</thead>
<tbody class="divide-y">
<tbody>
{#if sortedMetrics.length === 0}
<tr>
<td colspan={activeVisibleColumns.length} class="px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<td colspan={activeVisibleColumns.length} class="text-muted-foreground px-6 py-8 text-center text-sm">
No activity recorded
</td>
</tr>
{:else}
{#each sortedMetrics as metric (metric.id)}
<tr class="whitespace-nowrap text-sm border-gray-200 dark:border-white/10">
<tr class="hover:bg-muted/50 whitespace-nowrap border-b text-sm transition-colors">
{#each activeVisibleColumns as key (key)}
<td class="px-6 py-4">
{#if key === "id"}
@@ -309,7 +322,7 @@
{metric.req_path || "-"}
{:else if key === "resp_status_code"}
{#if metric.error_msg}
<span class="text-red-500 dark:text-red-400 cursor-help" title={metric.error_msg}>
<span class="text-destructive cursor-help" title={metric.error_msg}>
{metric.resp_status_code || "-"}
</span>
{:else}
@@ -333,23 +346,24 @@
{formatDuration(metric.duration_ms)}
{:else if key === "capture"}
{#if metric.has_capture}
<button
<Button
variant="outline"
size="xs"
onclick={() => viewCapture(metric.id)}
disabled={loadingCaptureId === metric.id}
class="btn btn--sm"
>
{loadingCaptureId === metric.id ? "..." : "View"}
</button>
</Button>
{:else}
<span class="text-txtsecondary">-</span>
<span class="text-muted-foreground">-</span>
{/if}
{:else if key === "meta"}
{#if Object.keys(metric.metadata || {}).length > 0}
<MetadataTooltip metadata={metric.metadata}>
<span class="cursor-help text-txtsecondary hover:text-txtmain">...</span>
<span class="text-muted-foreground hover:text-foreground cursor-help">...</span>
</MetadataTooltip>
{:else}
<span class="text-txtsecondary">-</span>
<span class="text-muted-foreground">-</span>
{/if}
{:else}
-
@@ -361,7 +375,7 @@
{/if}
</tbody>
</table>
</div>
</Card.Root>
</div>
<CaptureDialog capture={selectedCapture} open={dialogOpen} onclose={closeDialog} />
+12 -26
View File
@@ -4,6 +4,7 @@
import { persistentStore } from "../stores/persistent";
import LogPanel from "../components/LogPanel.svelte";
import ResizablePanels from "../components/ResizablePanels.svelte";
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js";
type ViewMode = "proxy" | "upstream" | "panels";
@@ -15,32 +16,17 @@
</script>
<div class="flex flex-col h-full w-full gap-2">
<div class="flex items-center gap-1">
<button
onclick={() => viewModeStore.set("panels")}
class:btn={true}
class:bg-primary={$viewModeStore === "panels"}
class:text-btn-primary-text={$viewModeStore === "panels"}
>
Both
</button>
<button
onclick={() => viewModeStore.set("proxy")}
class:btn={true}
class:bg-primary={$viewModeStore === "proxy"}
class:text-btn-primary-text={$viewModeStore === "proxy"}
>
Proxy
</button>
<button
onclick={() => viewModeStore.set("upstream")}
class:btn={true}
class:bg-primary={$viewModeStore === "upstream"}
class:text-btn-primary-text={$viewModeStore === "upstream"}
>
Upstream
</button>
</div>
<ToggleGroup.Root
type="single"
variant="outline"
value={$viewModeStore}
onValueChange={(v) => v && viewModeStore.set(v as ViewMode)}
class="justify-start"
>
<ToggleGroup.Item value="panels">Both</ToggleGroup.Item>
<ToggleGroup.Item value="proxy">Proxy</ToggleGroup.Item>
<ToggleGroup.Item value="upstream">Upstream</ToggleGroup.Item>
</ToggleGroup.Root>
<div class="flex-1 w-full overflow-hidden">
{#if $viewModeStore === "panels"}