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:
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user