mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
perf(ui): virtualize large JsonTree value views
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick } from "vue";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import JsonTree from "../JsonTree.vue";
|
||||
|
||||
// vue-virtual-scroller observes item sizes; happy-dom has no layout engine.
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function mountTree(props: Record<string, unknown>) {
|
||||
const host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
const app = createApp(defineComponent({ render: () => h(JsonTree, props) }));
|
||||
app.mount(host);
|
||||
return host;
|
||||
}
|
||||
|
||||
describe("JsonTree", () => {
|
||||
it("renders fully expanded nested children by default", async () => {
|
||||
const host = mountTree({ value: { outer: { inner: 1 } } });
|
||||
await nextTick();
|
||||
|
||||
const root = host.querySelector(".json-tree");
|
||||
expect(root).not.toBeNull();
|
||||
expect(root?.classList.contains("is-virtualized")).toBe(false);
|
||||
// The recursive branch nests expanded container children.
|
||||
expect(root?.querySelector(".json-tree-children")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders flat virtualized rows and keeps node toggles working", async () => {
|
||||
const host = mountTree({ value: { outer: { inner: 1 } }, virtualized: true });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
const root = host.querySelector(".json-tree");
|
||||
expect(root?.classList.contains("is-virtualized")).toBe(true);
|
||||
expect(root?.querySelector(".json-tree-scroller")).not.toBeNull();
|
||||
// The virtual branch flattens containers; closing brackets become rows.
|
||||
expect(root?.querySelector(".json-tree-children")).toBeNull();
|
||||
expect(root?.querySelector(".json-tree-closing")).not.toBeNull();
|
||||
|
||||
const toggle = root?.querySelector<HTMLButtonElement>(".json-tree-toggle");
|
||||
expect(toggle).not.toBeNull();
|
||||
expect(toggle?.getAttribute("aria-expanded")).toBe("true");
|
||||
toggle?.click();
|
||||
await nextTick();
|
||||
expect(toggle?.getAttribute("aria-expanded")).toBe("false");
|
||||
});
|
||||
});
|
||||
@@ -780,7 +780,7 @@ onBeforeUnmount(() => {
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="min-h-0 flex-1 overflow-auto rounded-md border bg-muted/20">
|
||||
<JsonTree v-if="cellDetailJson !== undefined" :value="cellDetailJson" :highlight-json="renderMarkHtml" class="p-3 font-mono text-xs" />
|
||||
<JsonTree v-if="cellDetailJson !== undefined" :value="cellDetailJson" :highlight-json="renderMarkHtml" class="p-3 font-mono text-xs" virtualized />
|
||||
<div v-else class="whitespace-pre-wrap break-words p-3 font-mono text-xs">{{ cellDetailText }}</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -2820,11 +2820,11 @@ defineExpose({ focusSearch });
|
||||
@save="saveString"
|
||||
/>
|
||||
<div v-else-if="activeStructuredStringDetail && stringValueView !== 'hex' && stringValueView !== 'base64'" class="dbx-editor-font-family min-h-0 flex-1 overflow-auto bg-background p-4 text-sm leading-6">
|
||||
<JsonTree v-if="stringValueView === 'json'" :value="activeStructuredStringDetail.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" />
|
||||
<JsonTree v-if="stringValueView === 'json'" :value="activeStructuredStringDetail.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" virtualized />
|
||||
<pre v-else class="w-full min-w-0 max-w-full" :class="redisJsonWordWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'">{{ stringDecodedText }}</pre>
|
||||
</div>
|
||||
<div v-else-if="stringValueCodec === 'base64' && stringBase64Detail" class="dbx-editor-font-family min-h-0 flex-1 overflow-auto bg-background p-4 text-sm leading-6">
|
||||
<JsonTree v-if="stringValueView === 'json' && stringBase64Detail.json" :value="stringBase64Detail.json.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" />
|
||||
<JsonTree v-if="stringValueView === 'json' && stringBase64Detail.json" :value="stringBase64Detail.json.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" virtualized />
|
||||
<pre v-else class="w-full min-w-0 max-w-full" :class="redisJsonWordWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'">{{ stringDecodedText }}</pre>
|
||||
</div>
|
||||
<div v-else-if="stringCodecMismatch" class="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||
@@ -2848,8 +2848,8 @@ defineExpose({ focusSearch });
|
||||
{{ t("redis.decompressedLoading") }}
|
||||
</div>
|
||||
<div v-else-if="decompressedState.status === 'success'" class="dbx-editor-font-family min-h-0 flex-1 overflow-auto bg-background">
|
||||
<div v-if="stringValueView === 'json' && decompressedJsonDetail" class="p-4">
|
||||
<JsonTree :value="decompressedJsonDetail.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" />
|
||||
<div v-if="stringValueView === 'json' && decompressedJsonDetail" class="h-full p-4">
|
||||
<JsonTree :value="decompressedJsonDetail.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" virtualized />
|
||||
</div>
|
||||
<pre v-else class="w-full min-w-0 max-w-full p-4 text-sm leading-6" :class="detailTextClass('utf8')">{{ stringDecodedText }}</pre>
|
||||
</div>
|
||||
@@ -3563,14 +3563,14 @@ defineExpose({ focusSearch });
|
||||
</div>
|
||||
<RedisJsonEditor v-if="isEditingHashJson" ref="memberJsonEditorRef" v-model="memberEditValue" class="min-h-0 flex-1" :save-disabled="savingMember || !memberValueChanged" :read-only="savingMember" :word-wrap="redisJsonWordWrap" :enable-builtin-find="false" @save="saveMemberEdit" />
|
||||
<div v-else-if="memberValueView === 'json' && selectedMemberDetail.json && memberValueCodec === 'none'" class="dbx-editor-font-family min-h-0 flex-1 overflow-auto bg-background p-5 text-[13px] leading-6">
|
||||
<JsonTree :value="selectedMemberDetail.json.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" />
|
||||
<JsonTree :value="selectedMemberDetail.json.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" virtualized />
|
||||
</div>
|
||||
<div v-else-if="activeStructuredMemberDetail && memberValueView !== 'hex' && memberValueView !== 'base64'" class="dbx-editor-font-family min-h-0 flex-1 overflow-auto bg-background p-5 text-[13px] leading-6">
|
||||
<JsonTree v-if="memberValueView === 'json'" :value="activeStructuredMemberDetail.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" />
|
||||
<JsonTree v-if="memberValueView === 'json'" :value="activeStructuredMemberDetail.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" virtualized />
|
||||
<pre v-else class="w-full min-w-0 max-w-full" :class="redisJsonWordWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'">{{ memberDecodedText }}</pre>
|
||||
</div>
|
||||
<div v-else-if="memberValueCodec === 'base64' && memberBase64Detail" class="dbx-editor-font-family min-h-0 flex-1 overflow-auto bg-background p-5 text-[13px] leading-6">
|
||||
<JsonTree v-if="memberValueView === 'json' && memberBase64Detail.json" :value="memberBase64Detail.json.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" />
|
||||
<JsonTree v-if="memberValueView === 'json' && memberBase64Detail.json" :value="memberBase64Detail.json.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" virtualized />
|
||||
<pre v-else class="w-full min-w-0 max-w-full" :class="redisJsonWordWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'">{{ memberDecodedText }}</pre>
|
||||
</div>
|
||||
<div v-else-if="memberCodecMismatch" class="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||
@@ -3594,8 +3594,8 @@ defineExpose({ focusSearch });
|
||||
{{ t("redis.decompressedLoading") }}
|
||||
</div>
|
||||
<div v-else-if="decompressedState.status === 'success'" class="dbx-editor-font-family min-h-0 flex-1 overflow-auto bg-background">
|
||||
<div v-if="memberValueView === 'json' && decompressedJsonDetail" class="p-5">
|
||||
<JsonTree :value="decompressedJsonDetail.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" />
|
||||
<div v-if="memberValueView === 'json' && decompressedJsonDetail" class="h-full p-5">
|
||||
<JsonTree :value="decompressedJsonDetail.value" :word-wrap="redisJsonWordWrap" :highlight-json="highlightRedisJson" virtualized />
|
||||
</div>
|
||||
<pre v-else class="w-full min-w-0 max-w-full p-5 text-[13px] leading-6" :class="detailTextClass('utf8')">{{ memberDecodedText }}</pre>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user