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

@@ -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";
}
}