From 85b6afd3c1782ed7fb8e274f04d0ca16dc92a4a7 Mon Sep 17 00:00:00 2001 From: onenewcode Date: Fri, 24 Jul 2026 23:50:08 +0800 Subject: [PATCH] fix(explain): fix execution plan node detail overflow --- .../explain/ExplainPlanNodeTree.vue | 104 ++++++++++++----- .../__tests__/ExplainPlanNodeTree.spec.ts | 108 ++++++++++++++++++ 2 files changed, 185 insertions(+), 27 deletions(-) create mode 100644 apps/desktop/src/components/explain/__tests__/ExplainPlanNodeTree.spec.ts diff --git a/apps/desktop/src/components/explain/ExplainPlanNodeTree.vue b/apps/desktop/src/components/explain/ExplainPlanNodeTree.vue index b8cd76503..62e8dfb19 100644 --- a/apps/desktop/src/components/explain/ExplainPlanNodeTree.vue +++ b/apps/desktop/src/components/explain/ExplainPlanNodeTree.vue @@ -1,57 +1,107 @@ diff --git a/apps/desktop/src/components/explain/__tests__/ExplainPlanNodeTree.spec.ts b/apps/desktop/src/components/explain/__tests__/ExplainPlanNodeTree.spec.ts new file mode 100644 index 000000000..6e3b36e35 --- /dev/null +++ b/apps/desktop/src/components/explain/__tests__/ExplainPlanNodeTree.spec.ts @@ -0,0 +1,108 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const nodeTreeSource = readFileSync(new URL("../ExplainPlanNodeTree.vue", import.meta.url), "utf8"); +const globalStyles = readFileSync(new URL("../../../styles/globals.css", import.meta.url), "utf8"); + +type Rgb = [number, number, number]; + +type ThemeColors = { + selector: string; + background: Rgb; + foreground: Rgb; + popover: Rgb; + popoverForeground: Rgb; +}; + +function parseRgb(value: string): Rgb { + const channels = value + .match(/\d+(?:\.\d+)?/g) + ?.slice(0, 3) + .map(Number); + if (!channels || channels.length !== 3) throw new Error(`Expected an opaque RGB color, received ${value}`); + return [channels[0], channels[1], channels[2]]; +} + +function tokenColor(block: string, token: string): Rgb | undefined { + const value = block.match(new RegExp(`--${token}:\\s*rgb\\(([^)]+)\\)`))?.[1]; + return value ? parseRgb(value) : undefined; +} + +const themeColors: ThemeColors[] = [...globalStyles.matchAll(/(?:root|\.dark|html\.theme-[\w-]+(?:\.dark)?)\s*\{(?[^{}]*)\}/g)] + .map((match) => { + const block = match.groups?.block ?? ""; + const background = tokenColor(block, "background"); + const foreground = tokenColor(block, "foreground"); + const popover = tokenColor(block, "popover"); + const popoverForeground = tokenColor(block, "popover-foreground"); + if (!background || !foreground || !popover || !popoverForeground) return undefined; + + return { selector: match.groups?.selector ?? "", background, foreground, popover, popoverForeground }; + }) + .filter((theme): theme is ThemeColors => !!theme); + +function linearRgbChannel(channel: number): number { + const normalized = channel / 255; + return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; +} + +function luminance([red, green, blue]: Rgb): number { + return 0.2126 * linearRgbChannel(red) + 0.7152 * linearRgbChannel(green) + 0.0722 * linearRgbChannel(blue); +} + +function contrastRatio(first: Rgb, second: Rgb): number { + const [lighter, darker] = [luminance(first), luminance(second)].sort((left, right) => right - left); + return (lighter + 0.05) / (darker + 0.05); +} + +function blend(foreground: Rgb, background: Rgb, opacity: number): Rgb { + return foreground.map((channel, index) => Math.round(channel * opacity + background[index] * (1 - opacity))) as Rgb; +} + +describe("ExplainPlanNodeTree interactions", () => { + it("keeps a concise preview of the most useful details in the tree row", () => { + expect(nodeTreeSource).toContain('const detailPreviewEntries = computed(() => detailEntries.value.filter((detail) => detail.label !== "Actual Rows").slice(0, 2))'); + expect(nodeTreeSource).toContain('
{ + expect(nodeTreeSource).toContain('import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"'); + expect(nodeTreeSource).toContain(''); + expect(nodeTreeSource).toContain(""); + expect(nodeTreeSource).toContain(' { + expect(nodeTreeSource).toContain('const separatorIndex = detail.indexOf(":")'); + expect(nodeTreeSource).toContain('v-if="detail.label"'); + expect(nodeTreeSource).toContain('