feat(desktop): add typed inspector panels

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Senior Frontend Engineer
2026-03-28 11:16:39 +00:00
parent a9524dfbf6
commit 7c1abf2fd1
7 changed files with 397 additions and 11 deletions

View File

@@ -45,6 +45,10 @@ import {
toBackendErrorNotice,
type OperatorNotice,
} from "./lib/operator-notices";
import {
buildStructuredInspectorSections,
type StructuredInspectorSection,
} from "./lib/structured-inspector";
import { cn } from "./lib/utils";
declare const __APP_VERSION__: string;
@@ -439,6 +443,14 @@ function App() {
const inspectorText = tauriAvailable
? formatInspectorValue(selectedValueRecord, selectedKey, isInspectorLoading)
: (selectedKey?.value ?? "");
const structuredInspectorSections = tauriAvailable
? buildStructuredInspectorSections(selectedValueRecord)
: [];
const showStructuredInspector =
selectedKey !== null &&
!isInspectorLoading &&
structuredInspectorSections.length > 0 &&
!isEditable;
const supportNote = tauriAvailable
? buildLiveSupportNote(selectedValueRecord, selectedKey)
@@ -1662,15 +1674,20 @@ function App() {
</div>
{selectedKey ? (
<Textarea
value={isEditable ? draftValue : inspectorText}
onChange={(event) => setDraftValue(event.target.value)}
readOnly={!isEditable}
className={cn(
"mt-3 h-64 resize-none font-mono",
!isEditable && "border-[var(--line-soft)] bg-[var(--surface-muted)] text-[var(--ink-soft)]",
)}
/>
showStructuredInspector ? (
<StructuredInspectorPanels sections={structuredInspectorSections} />
) : (
<Textarea
value={isEditable ? draftValue : inspectorText}
onChange={(event) => setDraftValue(event.target.value)}
readOnly={!isEditable}
className={cn(
"mt-3 h-64 resize-none font-mono",
!isEditable &&
"border-[var(--line-soft)] bg-[var(--surface-muted)] text-[var(--ink-soft)]",
)}
/>
)
) : (
<div className="mt-3 flex h-64 items-center justify-center rounded-[22px] border border-dashed border-[var(--line-strong)] bg-[var(--surface-muted)] px-6 text-center text-sm text-[var(--ink-soft)]">
No key matches the current pattern in {activeDb}. Clear or adjust the search to restore an inspector target.
@@ -1944,6 +1961,85 @@ function InfoCard({
);
}
function StructuredInspectorPanels({
sections,
}: {
sections: StructuredInspectorSection[];
}) {
return (
<div className="mt-3 h-64 space-y-3 overflow-auto pr-1">
{sections.map((section) => (
<section
key={section.id}
className="rounded-[22px] border border-[var(--line-soft)] bg-[var(--surface-muted)] p-4"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--ink-0)]">{section.title}</p>
<p className="mt-1 text-sm text-[var(--ink-soft)]">{section.summary}</p>
</div>
<Badge variant="neutral">{section.rows.length} rows</Badge>
</div>
<div className="mt-4 hidden grid-cols-[minmax(0,0.38fr)_minmax(0,0.62fr)] gap-3 px-1 sm:grid">
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-[var(--ink-muted)]">
{section.primaryLabel}
</p>
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-[var(--ink-muted)]">
{section.secondaryLabel}
</p>
</div>
<div className="mt-3 space-y-2">
{section.rows.map((row) => (
<div
key={row.id}
className="grid gap-3 rounded-[18px] border border-[var(--line-soft)] bg-[var(--surface-base)] p-3 sm:grid-cols-[minmax(0,0.38fr)_minmax(0,0.62fr)]"
>
<div>
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-[var(--ink-muted)] sm:hidden">
{section.primaryLabel}
</p>
<p className="mt-1 break-all font-mono text-sm text-[var(--ink-0)] sm:mt-0">
{row.primary}
</p>
</div>
<div>
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-[var(--ink-muted)] sm:hidden">
{section.secondaryLabel}
</p>
<p className="mt-1 break-all font-mono text-sm text-[var(--ink-0)] sm:mt-0">
{row.secondary}
</p>
</div>
{row.detailLines && row.detailLines.length > 0 ? (
<div className="sm:col-span-2">
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-[var(--ink-muted)]">
Payload
</p>
<div className="mt-2 space-y-2">
{row.detailLines.map((detail) => (
<div
key={`${row.id}-${detail}`}
className="rounded-2xl border border-[var(--line-soft)] bg-[var(--surface-muted)] px-3 py-2 font-mono text-xs text-[var(--ink-soft)]"
>
{detail}
</div>
))}
</div>
</div>
) : null}
</div>
))}
</div>
</section>
))}
</div>
);
}
function isTauriRuntimeAvailable() {
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
}
@@ -2281,7 +2377,7 @@ function buildLiveSupportNote(record: RedisValueRecord | null, selectedKey: KeyR
case "set":
case "sorted_set":
case "stream":
return `${formatRedisKeyTypeLabel(record.metadata.key_type)} values are loaded live and remain explicitly read only in the current desktop contract.`;
return `${formatRedisKeyTypeLabel(record.metadata.key_type)} values are loaded live into structured read-only panels in the current desktop contract.`;
default:
return "The current key is available for live inspection through the desktop backend contract.";
}

View File

@@ -0,0 +1,99 @@
import { describe, expect, it } from "vitest";
import { buildStructuredInspectorSections } from "./structured-inspector";
describe("structured inspector sections", () => {
it("renders hash entries as field/value rows", () => {
expect(
buildStructuredInspectorSections({
metadata: { key_type: "hash" },
data: {
kind: "hash",
entries: [
{
field: { kind: "string", value: "region" },
value: { kind: "string", value: "eu-west-1" },
},
],
},
}),
).toEqual([
{
id: "hash",
title: "1 field",
summary: "Live HGETALL output rendered as field/value rows.",
primaryLabel: "Field",
secondaryLabel: "Value",
rows: [
{
id: "hash-0",
primary: "region",
secondary: "eu-west-1",
},
],
},
]);
});
it("renders sorted sets and stream entries with readable secondary detail", () => {
expect(
buildStructuredInspectorSections({
metadata: { key_type: "stream" },
data: {
kind: "stream",
entries: [
{
id: "1711642800000-0",
fields: [
{
field: { kind: "string", value: "event" },
value: { kind: "string", value: "ttl-updated" },
},
{
field: { kind: "string", value: "payload" },
value: { kind: "binary", bytes: [1, 2, 3, 4] },
},
],
},
],
},
}),
).toEqual([
{
id: "stream",
title: "1 entry",
summary: "Live XRANGE output rendered as stream entries with inline field payloads.",
primaryLabel: "Entry",
secondaryLabel: "Fields",
rows: [
{
id: "1711642800000-0",
primary: "1711642800000-0",
secondary: "2 fields",
detailLines: ["event = ttl-updated", "payload = [binary 4 bytes]"],
},
],
},
]);
});
it("keeps string and missing payloads on the existing textarea path", () => {
expect(
buildStructuredInspectorSections({
metadata: { key_type: "string" },
data: {
kind: "string",
value: { kind: "string", value: "hello" },
},
}),
).toEqual([]);
expect(
buildStructuredInspectorSections({
metadata: { key_type: "string" },
data: {
kind: "missing",
},
}),
).toEqual([]);
});
});

View File

@@ -0,0 +1,155 @@
export type RedisValueContent =
| { kind: "string"; value: string }
| { kind: "binary"; bytes: number[] };
export type RedisFieldValueEntry = {
field: RedisValueContent;
value: RedisValueContent;
};
export type RedisSortedSetEntry = {
member: RedisValueContent;
score: string;
};
export type RedisStreamEntry = {
id: string;
fields: RedisFieldValueEntry[];
};
export type RedisValueData =
| { kind: "missing" }
| { kind: "string"; value: RedisValueContent }
| { kind: "hash"; entries: RedisFieldValueEntry[] }
| { kind: "list"; items: RedisValueContent[] }
| { kind: "set"; members: RedisValueContent[] }
| { kind: "sorted_set"; entries: RedisSortedSetEntry[] }
| { kind: "stream"; entries: RedisStreamEntry[] };
export type StructuredValueRecord = {
metadata: {
key_type: string;
};
data: RedisValueData;
};
export type StructuredInspectorRow = {
id: string;
primary: string;
secondary: string;
detailLines?: string[];
};
export type StructuredInspectorSection = {
id: string;
title: string;
summary: string;
primaryLabel: string;
secondaryLabel: string;
rows: StructuredInspectorRow[];
};
export function buildStructuredInspectorSections(
record: StructuredValueRecord | null,
): StructuredInspectorSection[] {
if (!record) {
return [];
}
switch (record.data.kind) {
case "hash":
return [
{
id: "hash",
title: `${record.data.entries.length} field${record.data.entries.length === 1 ? "" : "s"}`,
summary: "Live HGETALL output rendered as field/value rows.",
primaryLabel: "Field",
secondaryLabel: "Value",
rows: record.data.entries.map((entry, index) => ({
id: `hash-${index}`,
primary: formatStructuredContent(entry.field),
secondary: formatStructuredContent(entry.value),
})),
},
];
case "list":
return [
{
id: "list",
title: `${record.data.items.length} item${record.data.items.length === 1 ? "" : "s"}`,
summary: "Live LRANGE output rendered in the preserved list order.",
primaryLabel: "Index",
secondaryLabel: "Value",
rows: record.data.items.map((item, index) => ({
id: `list-${index}`,
primary: `[${index}]`,
secondary: formatStructuredContent(item),
})),
},
];
case "set":
return [
{
id: "set",
title: `${record.data.members.length} member${record.data.members.length === 1 ? "" : "s"}`,
summary: "Live SMEMBERS output rendered as unique set members.",
primaryLabel: "Member",
secondaryLabel: "Value",
rows: record.data.members.map((member, index) => ({
id: `set-${index}`,
primary: `member ${index + 1}`,
secondary: formatStructuredContent(member),
})),
},
];
case "sorted_set":
return [
{
id: "sorted-set",
title: `${record.data.entries.length} scored member${record.data.entries.length === 1 ? "" : "s"}`,
summary: "Live ZRANGE ... WITHSCORES output rendered as member/score pairs.",
primaryLabel: "Member",
secondaryLabel: "Score",
rows: record.data.entries.map((entry, index) => ({
id: `sorted-set-${index}`,
primary: formatStructuredContent(entry.member),
secondary: entry.score,
})),
},
];
case "stream":
return [
{
id: "stream",
title: `${record.data.entries.length} entr${record.data.entries.length === 1 ? "y" : "ies"}`,
summary: "Live XRANGE output rendered as stream entries with inline field payloads.",
primaryLabel: "Entry",
secondaryLabel: "Fields",
rows: record.data.entries.map((entry) => ({
id: entry.id,
primary: entry.id,
secondary: `${entry.fields.length} field${entry.fields.length === 1 ? "" : "s"}`,
detailLines: entry.fields.map(
(field) =>
`${formatStructuredContent(field.field)} = ${formatStructuredContent(field.value)}`,
),
})),
},
];
case "missing":
case "string":
default:
return [];
}
}
function formatStructuredContent(content: RedisValueContent) {
switch (content.kind) {
case "string":
return content.value;
case "binary":
return `[binary ${content.bytes.length} bytes]`;
default:
return "unsupported";
}
}