refactor(desktop): harden workspace shell foundation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -37,6 +37,13 @@ import {
|
||||
} from "./components/ui/dialog";
|
||||
import { Input } from "./components/ui/input";
|
||||
import { Textarea } from "./components/ui/textarea";
|
||||
import {
|
||||
isLocalSmokeFixtureConnection,
|
||||
localSmokeFixtureCommandPresets,
|
||||
localSmokeFixtureExpectedKeys,
|
||||
localSmokeFixtureProfileId,
|
||||
localSmokeFixtureSearchText,
|
||||
} from "./lib/local-smoke-fixture";
|
||||
import {
|
||||
toBackendError,
|
||||
type BackendErrorContext,
|
||||
@@ -45,10 +52,24 @@ import {
|
||||
toBackendErrorNotice,
|
||||
type OperatorNotice,
|
||||
} from "./lib/operator-notices";
|
||||
import { buildQaSnapshot } from "./lib/qa-snapshot";
|
||||
import {
|
||||
buildStructuredInspectorSections,
|
||||
type StructuredInspectorSection,
|
||||
} from "./lib/structured-inspector";
|
||||
import {
|
||||
appendWorkspaceKeyRecords,
|
||||
filterWorkspaceKeys,
|
||||
replaceWorkspaceKeyRecord,
|
||||
resolveSelectedWorkspaceKey,
|
||||
resolveSelectedWorkspaceKeyId,
|
||||
} from "./lib/key-workspace-state";
|
||||
import {
|
||||
parseDemoTtlToSeconds,
|
||||
persistDemoKeyTtl,
|
||||
updateDemoKeyTtl,
|
||||
updateDemoStringValue,
|
||||
} from "./lib/demo-workspace";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
@@ -293,7 +314,7 @@ const initialConnections: ConnectionProfile[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const keyData: KeyRecord[] = [
|
||||
const initialDemoKeys: KeyRecord[] = [
|
||||
{
|
||||
id: "session:42",
|
||||
name: "session:42",
|
||||
@@ -373,11 +394,12 @@ const initialConsoleHistory: ConsoleEntry[] = [
|
||||
|
||||
function App() {
|
||||
const [connections, setConnections] = useState(initialConnections);
|
||||
const [demoKeys, setDemoKeys] = useState(initialDemoKeys);
|
||||
const [activeConnectionId, setActiveConnectionId] = useState(initialConnections[0].id);
|
||||
const [activeDb, setActiveDb] = useState(databases[0]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(keyData[1].id);
|
||||
const [draftValue, setDraftValue] = useState(keyData[1].value);
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(initialDemoKeys[1].id);
|
||||
const [draftValue, setDraftValue] = useState(initialDemoKeys[1].value);
|
||||
const [consoleInput, setConsoleInput] = useState("GET cache:homepage");
|
||||
const [consoleHistory, setConsoleHistory] = useState(initialConsoleHistory);
|
||||
const [bootstrap, setBootstrap] = useState(fallbackBootstrap);
|
||||
@@ -422,18 +444,12 @@ function App() {
|
||||
connections.find((connection) => connection.id === activeConnectionId) ??
|
||||
connections[0];
|
||||
|
||||
const demoQuery = deferredSearch.trim().toLowerCase();
|
||||
const demoKeys = !demoQuery
|
||||
? keyData
|
||||
: keyData.filter((item) => item.name.toLowerCase().includes(demoQuery));
|
||||
const filteredKeys = tauriAvailable ? liveKeys : demoKeys;
|
||||
const filteredDemoKeys = filterWorkspaceKeys(demoKeys, deferredSearch);
|
||||
const filteredKeys = tauriAvailable ? liveKeys : filteredDemoKeys;
|
||||
const draftErrors = validateConnectionDraft(connectionDraft, connections);
|
||||
const draftErrorList = Object.values(draftErrors);
|
||||
|
||||
const selectedKey =
|
||||
filteredKeys.length === 0
|
||||
? null
|
||||
: filteredKeys.find((item) => item.id === selectedKeyId) ?? filteredKeys[0];
|
||||
const selectedKey = resolveSelectedWorkspaceKey(filteredKeys, selectedKeyId);
|
||||
|
||||
const isEditable = tauriAvailable
|
||||
? selectedValueRecord?.write_capability === "replace_string" &&
|
||||
@@ -459,6 +475,8 @@ function App() {
|
||||
const ttlWriteSupported = tauriAvailable
|
||||
? selectedValueRecord?.ttl_write_supported === true
|
||||
: selectedKey !== null;
|
||||
const localSmokeFixtureActive = isLocalSmokeFixtureConnection(activeConnection);
|
||||
const localSmokeFilterActive = search.trim() === localSmokeFixtureSearchText;
|
||||
const ttlDraftError = ttlDraft.trim() ? validateTtlDraft(ttlDraft) : null;
|
||||
const canSubmitTtlUpdate =
|
||||
selectedKey !== null &&
|
||||
@@ -474,23 +492,25 @@ function App() {
|
||||
? selectedValueRecord?.metadata.ttl.kind !== "persistent"
|
||||
: selectedKey?.ttl !== "persistent");
|
||||
const latestConsoleEntry = consoleHistory[0] ?? null;
|
||||
const qaSnapshot = [
|
||||
`app=${bootstrap.app_name}`,
|
||||
`version=${appVersion}`,
|
||||
`runtime=${runtimeMode}`,
|
||||
`build=${buildMode}`,
|
||||
`connection=${activeConnection.name}`,
|
||||
`endpoint=${formatEndpoint(activeConnection.target)}`,
|
||||
`database=${activeDb}`,
|
||||
`selectedKey=${selectedKey?.name ?? "none"}`,
|
||||
`selectedType=${selectedKey?.type ?? "none"}`,
|
||||
`selectedTtl=${selectedKey?.ttl ?? "none"}`,
|
||||
`browseVisible=${filteredKeys.length}${tauriAvailable && liveHasMore ? "+" : ""}`,
|
||||
`latestCommand=${latestConsoleEntry?.command ?? "none"}`,
|
||||
`latestCommandSource=${latestConsoleEntry?.source ?? "none"}`,
|
||||
`latestCommandStatus=${latestConsoleEntry?.status ?? "none"}`,
|
||||
`banner=${banner.message}`,
|
||||
].join("\n");
|
||||
const qaSnapshot = buildQaSnapshot({
|
||||
appName: bootstrap.app_name,
|
||||
appVersion,
|
||||
runtimeMode,
|
||||
buildMode,
|
||||
connectionName: activeConnection.name,
|
||||
endpoint: formatEndpoint(activeConnection.target),
|
||||
databaseLabel: activeDb,
|
||||
smokeFixtureActive: localSmokeFixtureActive,
|
||||
smokeFilter: localSmokeFilterActive ? localSmokeFixtureSearchText : search,
|
||||
selectedKeyName: selectedKey?.name ?? null,
|
||||
selectedKeyType: selectedKey?.type ?? null,
|
||||
selectedKeyTtl: selectedKey?.ttl ?? null,
|
||||
browseVisibleCount: `${filteredKeys.length}${tauriAvailable && liveHasMore ? "+" : ""}`,
|
||||
latestCommand: latestConsoleEntry?.command ?? null,
|
||||
latestCommandSource: latestConsoleEntry?.source ?? null,
|
||||
latestCommandStatus: latestConsoleEntry?.status ?? null,
|
||||
bannerMessage: banner.message,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!tauriAvailable) {
|
||||
@@ -569,9 +589,7 @@ function App() {
|
||||
setLiveKeys(nextKeys);
|
||||
setLiveNextCursor(result.next_cursor);
|
||||
setLiveHasMore(result.has_more);
|
||||
setSelectedKeyId((current) =>
|
||||
nextKeys.some((item) => item.id === current) ? current : (nextKeys[0]?.id ?? null),
|
||||
);
|
||||
setSelectedKeyId((current) => resolveSelectedWorkspaceKeyId(nextKeys, current));
|
||||
setIsBrowseLoading(false);
|
||||
});
|
||||
})
|
||||
@@ -644,7 +662,7 @@ function App() {
|
||||
const nextRecord = toLiveKeyRecordFromValue(record);
|
||||
startTransition(() => {
|
||||
setSelectedValueRecord(record);
|
||||
setLiveKeys((current) => replaceKeyRecord(current, nextRecord));
|
||||
setLiveKeys((current) => replaceWorkspaceKeyRecord(current, nextRecord));
|
||||
setDraftValue(
|
||||
record.data.kind === "string" && record.write_capability === "replace_string"
|
||||
? formatRedisValueContent(record.data.value)
|
||||
@@ -723,6 +741,60 @@ function App() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleUseLocalSmokeFixture = () => {
|
||||
const existingFixture = connections.find(isLocalSmokeFixtureConnection);
|
||||
const fixtureConnection =
|
||||
existingFixture ??
|
||||
{
|
||||
id: localSmokeFixtureProfileId,
|
||||
name: localSmokeFixtureProfileId,
|
||||
environment: "local" as const,
|
||||
detail: "Fixture profile",
|
||||
status: "idle" as const,
|
||||
password: "",
|
||||
target: {
|
||||
host: "127.0.0.1",
|
||||
port: 6380,
|
||||
database: 0,
|
||||
username: null,
|
||||
tls_mode: "disabled" as const,
|
||||
},
|
||||
};
|
||||
|
||||
startTransition(() => {
|
||||
if (!existingFixture) {
|
||||
setConnections((current) => [fixtureConnection, ...current]);
|
||||
}
|
||||
setActiveConnectionId(fixtureConnection.id);
|
||||
setActiveDb(formatDatabaseLabel(fixtureConnection.target.database));
|
||||
setSelectedKeyId(null);
|
||||
setSearch(localSmokeFixtureSearchText);
|
||||
setConsoleInput(localSmokeFixtureCommandPresets[0].command);
|
||||
showAccentBanner(
|
||||
"Loaded the local smoke fixture profile, narrowed the browser to smoke:* keys, and primed the command tray with PING.",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFocusLocalSmokeKeys = () => {
|
||||
startTransition(() => {
|
||||
setSearch(localSmokeFixtureSearchText);
|
||||
setSelectedKeyId(null);
|
||||
showNeutralBanner(
|
||||
`Focused the browser on ${localSmokeFixtureSearchText} for local smoke verification in ${activeDb}.`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleApplyLocalSmokeCommandPreset = (command: string) => {
|
||||
startTransition(() => {
|
||||
setConsoleInput(command);
|
||||
showNeutralBanner(
|
||||
`Loaded ${command} into the command tray so QA can execute the next safe smoke step without retyping.`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateDraftConnection = () => {
|
||||
if (draftErrorList.length > 0) {
|
||||
showNeutralBanner(`Connection draft is incomplete: ${draftErrorList[0]}`);
|
||||
@@ -766,9 +838,12 @@ function App() {
|
||||
}
|
||||
|
||||
if (!tauriAvailable) {
|
||||
showNeutralBanner(
|
||||
`Saved mock string value for ${selectedKey.name} in ${activeDb}. Backend persistence is not wired yet.`,
|
||||
);
|
||||
startTransition(() => {
|
||||
setDemoKeys((current) => updateDemoStringValue(current, selectedKey.name, draftValue));
|
||||
showNeutralBanner(
|
||||
`Saved session-local mock string value for ${selectedKey.name} in ${activeDb}. Backend persistence is still unavailable in browser demo mode.`,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -791,7 +866,7 @@ function App() {
|
||||
const nextRecord = toLiveKeyRecordFromValue(result);
|
||||
startTransition(() => {
|
||||
setSelectedValueRecord(result);
|
||||
setLiveKeys((current) => replaceKeyRecord(current, nextRecord));
|
||||
setLiveKeys((current) => replaceWorkspaceKeyRecord(current, nextRecord));
|
||||
setDraftValue(
|
||||
result.data.kind === "string"
|
||||
? formatRedisValueContent(result.data.value)
|
||||
@@ -839,9 +914,14 @@ function App() {
|
||||
}
|
||||
|
||||
if (!tauriAvailable) {
|
||||
showNeutralBanner(
|
||||
`Browser dev mode keeps TTL changes in demo fallback for ${selectedKey.name}. Use desktop runtime for a live TTL update.`,
|
||||
);
|
||||
startTransition(() => {
|
||||
setDemoKeys((current) =>
|
||||
updateDemoKeyTtl(current, selectedKey.name, Number.parseInt(ttlDraft, 10)),
|
||||
);
|
||||
showNeutralBanner(
|
||||
`Updated session-local mock TTL for ${selectedKey.name} in ${activeDb}. Use desktop runtime for a live TTL change.`,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -893,9 +973,13 @@ function App() {
|
||||
}
|
||||
|
||||
if (!tauriAvailable) {
|
||||
showNeutralBanner(
|
||||
`Browser dev mode keeps TTL removal in demo fallback for ${selectedKey.name}. Use desktop runtime for a live TTL change.`,
|
||||
);
|
||||
startTransition(() => {
|
||||
setDemoKeys((current) => persistDemoKeyTtl(current, selectedKey.name));
|
||||
setTtlDraft("");
|
||||
showNeutralBanner(
|
||||
`Removed session-local mock TTL for ${selectedKey.name} in ${activeDb}. Use desktop runtime for a live TTL change.`,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -967,7 +1051,7 @@ function App() {
|
||||
const nextRecord = toLiveKeyRecordFromValue(result);
|
||||
startTransition(() => {
|
||||
setSelectedValueRecord(result);
|
||||
setLiveKeys((current) => replaceKeyRecord(current, nextRecord));
|
||||
setLiveKeys((current) => replaceWorkspaceKeyRecord(current, nextRecord));
|
||||
setDraftValue(
|
||||
result.data.kind === "string" && result.write_capability === "replace_string"
|
||||
? formatRedisValueContent(result.data.value)
|
||||
@@ -1020,11 +1104,15 @@ function App() {
|
||||
metadata,
|
||||
};
|
||||
setSelectedValueRecord(nextRecord);
|
||||
setLiveKeys((current) => replaceKeyRecord(current, toLiveKeyRecordFromValue(nextRecord)));
|
||||
setLiveKeys((current) =>
|
||||
replaceWorkspaceKeyRecord(current, toLiveKeyRecordFromValue(nextRecord)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setLiveKeys((current) => replaceKeyRecord(current, toLiveKeyRecordFromMetadata(metadata)));
|
||||
setLiveKeys((current) =>
|
||||
replaceWorkspaceKeyRecord(current, toLiveKeyRecordFromMetadata(metadata)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleLoadMoreKeys = async () => {
|
||||
@@ -1048,7 +1136,7 @@ function App() {
|
||||
|
||||
const nextKeys = result.keys.map(toLiveKeyRecordFromMetadata);
|
||||
startTransition(() => {
|
||||
setLiveKeys((current) => appendKeyRecords(current, nextKeys));
|
||||
setLiveKeys((current) => appendWorkspaceKeyRecords(current, nextKeys));
|
||||
setLiveNextCursor(result.next_cursor);
|
||||
setLiveHasMore(result.has_more);
|
||||
setIsBrowseLoading(false);
|
||||
@@ -1532,6 +1620,67 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<SectionEyebrow icon={<Activity className="h-4 w-4" />} label="Local smoke assist" />
|
||||
<div className="mt-3 rounded-[22px] border border-[var(--line-soft)] bg-[var(--surface-base)] p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--ink-0)]">
|
||||
Seeded fixture path for desktop smoke runs
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-[var(--ink-soft)]">
|
||||
Mirrors `docs/qa/local-desktop-smoke.md` and keeps the safe validation path visible inside the shell.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={localSmokeFixtureActive ? "accent" : "neutral"}>
|
||||
{localSmokeFixtureActive ? "active" : "inactive"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Badge variant="neutral">127.0.0.1:6380</Badge>
|
||||
<Badge variant="neutral">db0</Badge>
|
||||
<Badge variant="neutral">{localSmokeFixtureExpectedKeys.length} seeded keys</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleUseLocalSmokeFixture}>
|
||||
Use fixture
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleFocusLocalSmokeKeys}>
|
||||
Focus smoke:*
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
{localSmokeFixtureCommandPresets.map((preset) => (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => handleApplyLocalSmokeCommandPreset(preset.command)}
|
||||
className="w-full rounded-[18px] border border-[var(--line-soft)] bg-[var(--surface-muted)] px-3 py-2 text-left transition hover:border-[var(--line-strong)]"
|
||||
>
|
||||
<p className="font-mono text-sm text-[var(--ink-0)]">{preset.label}</p>
|
||||
<p className="mt-1 text-xs text-[var(--ink-soft)]">{preset.summary}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-[18px] border border-[var(--line-soft)] bg-[var(--surface-muted)] px-3 py-3">
|
||||
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-[var(--ink-muted)]">
|
||||
Seeded keys
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{localSmokeFixtureExpectedKeys.map((keyName) => (
|
||||
<Badge key={keyName} variant="neutral" className="normal-case tracking-normal">
|
||||
{keyName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<SectionEyebrow icon={<ClipboardList className="h-4 w-4" />} label="QA snapshot" />
|
||||
<div className="mt-3 rounded-[22px] border border-[var(--line-soft)] bg-[var(--surface-base)] p-4">
|
||||
@@ -1571,7 +1720,7 @@ function App() {
|
||||
<Badge variant="neutral">
|
||||
{tauriAvailable
|
||||
? `${filteredKeys.length}${liveHasMore ? "+" : ""} visible in ${activeDb}`
|
||||
: `${filteredKeys.length} of ${keyData.length} keys in ${activeDb}`}
|
||||
: `${filteredKeys.length} of ${demoKeys.length} keys in ${activeDb}`}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -2147,7 +2296,7 @@ function buildMockCommandOutput(command: string, selectedKey: KeyRecord | null)
|
||||
}
|
||||
|
||||
if (upper === "TTL") {
|
||||
return `(integer) ${selectedKey.ttl === "persistent" ? -1 : selectedKey.ttl.replace(/[^0-9]/g, "")}`;
|
||||
return `(integer) ${parseDemoTtlToSeconds(selectedKey.ttl)}`;
|
||||
}
|
||||
|
||||
if (upper === "TYPE") {
|
||||
@@ -2290,39 +2439,6 @@ function toLiveKeyRecordFromValue(record: RedisValueRecord): KeyRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function replaceKeyRecord(current: KeyRecord[], next: KeyRecord) {
|
||||
let replaced = false;
|
||||
const updated = current.map((item) => {
|
||||
if (item.id !== next.id) {
|
||||
return item;
|
||||
}
|
||||
|
||||
replaced = true;
|
||||
return {
|
||||
...item,
|
||||
...next,
|
||||
};
|
||||
});
|
||||
|
||||
return replaced ? updated : [next, ...current];
|
||||
}
|
||||
|
||||
function appendKeyRecords(current: KeyRecord[], next: KeyRecord[]) {
|
||||
const seen = new Set(current.map((item) => item.id));
|
||||
const appended = [...current];
|
||||
|
||||
for (const item of next) {
|
||||
if (seen.has(item.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(item.id);
|
||||
appended.push(item);
|
||||
}
|
||||
|
||||
return appended;
|
||||
}
|
||||
|
||||
function buildMetadataPreview(metadata: RedisKeyMetadata) {
|
||||
return `${formatRedisKeyTypeLabel(metadata.key_type)} key · TTL ${formatRedisKeyTtl(metadata.ttl)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user