diff --git a/.changeset/sunny-houses-eat.md b/.changeset/sunny-houses-eat.md new file mode 100644 index 0000000..8969a5b --- /dev/null +++ b/.changeset/sunny-houses-eat.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/devtools': patch +--- + +refactor: Improve UI aesthetics with updated tab and card styles, and refactor RenderTree data formatting for hooks. diff --git a/.gitignore b/.gitignore index 45d3309..c67ca9f 100644 --- a/.gitignore +++ b/.gitignore @@ -64,4 +64,6 @@ testem.log .DS_Store Thumbs.db .vite-inspect -.pnpm-store/* \ No newline at end of file +.pnpm-store/* +qwik/* +.cursor/skills/* \ No newline at end of file diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 42770ec..24b43ea 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -29,7 +29,7 @@ "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/vite": "^4.2.1", "tailwindcss": "^4.2.1", - "vite": "8.0.0" + "vite": ">=7.0.0 <9.0.0" }, "dependencies": { "birpc": "^4.0.0", diff --git a/packages/kit/package.json b/packages/kit/package.json index aaa3529..f19f32b 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -22,7 +22,7 @@ "superjson": "^2.2.6" }, "peerDependencies": { - "vite": "8.0.0" + "vite": ">=7.0.0 <9.0.0" }, "devDependencies": { "@types/eslint": "9.6.1", diff --git a/packages/ui/src/components/DevtoolsButton/DevtoolsButton.tsx b/packages/ui/src/components/DevtoolsButton/DevtoolsButton.tsx index 6783eee..7903141 100644 --- a/packages/ui/src/components/DevtoolsButton/DevtoolsButton.tsx +++ b/packages/ui/src/components/DevtoolsButton/DevtoolsButton.tsx @@ -7,7 +7,7 @@ interface DevtoolsButtonProps { export const DevtoolsButton = component$((props: DevtoolsButtonProps) => { // Signal for the button's position (distance from bottom-right corner) - const position = useSignal({ x: 16, y: 16 }); + const position = useSignal({ x: 24, y: 24 }); // Signal to track if the element is currently being dragged const isDragging = useSignal(false); // Ref for the draggable element @@ -94,15 +94,13 @@ export const DevtoolsButton = component$((props: DevtoolsButtonProps) => { return (
{ onClick$={handleClick} > Qwik Logo
); diff --git a/packages/ui/src/components/DevtoolsPanel/DevtoolsPanel.tsx b/packages/ui/src/components/DevtoolsPanel/DevtoolsPanel.tsx index 87885b1..573b0c1 100644 --- a/packages/ui/src/components/DevtoolsPanel/DevtoolsPanel.tsx +++ b/packages/ui/src/components/DevtoolsPanel/DevtoolsPanel.tsx @@ -30,18 +30,18 @@ export const DevtoolsPanel = component$(({ state }: DevtoolsPanelProps) => { return ( <>
{ state.isOpen = false; }} />
diff --git a/packages/ui/src/components/TabContent/TabContent.tsx b/packages/ui/src/components/TabContent/TabContent.tsx index 33e10c3..bd7b767 100644 --- a/packages/ui/src/components/TabContent/TabContent.tsx +++ b/packages/ui/src/components/TabContent/TabContent.tsx @@ -2,12 +2,12 @@ import { component$, Slot } from '@qwik.dev/core'; export const TabContent = component$(() => { return ( -
-
+
+
-
+
diff --git a/packages/ui/src/components/TabTitle/TabTitle.tsx b/packages/ui/src/components/TabTitle/TabTitle.tsx index 5bef3b5..f6cbec5 100644 --- a/packages/ui/src/components/TabTitle/TabTitle.tsx +++ b/packages/ui/src/components/TabTitle/TabTitle.tsx @@ -5,5 +5,9 @@ interface TabTitleProps { } export const TabTitle = component$(({ title }: TabTitleProps) => { - return

{title}

; + return ( +

+ {title} +

+ ); }); diff --git a/packages/ui/src/components/Tree/Tree.tsx b/packages/ui/src/components/Tree/Tree.tsx index 37fc9cf..70326c3 100644 --- a/packages/ui/src/components/Tree/Tree.tsx +++ b/packages/ui/src/components/Tree/Tree.tsx @@ -1,171 +1,193 @@ -import { $, component$, QRL, useSignal } from '@qwik.dev/core'; +import { $, component$, type QRL, useSignal } from '@qwik.dev/core'; import type { JSXOutput, Signal } from '@qwik.dev/core'; import { IconChevronUpMini } from '../Icons/Icons'; -export interface TreeNode { - name?: string | 'text'; - props?: Record; - children?: TreeNode[]; - elementType?: string; - label?: string; +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +import type { TreeNode, TreeNodePropValue } from './type'; + +export interface TreeProps { + data: Signal; + onNodeClick?: QRL<(node: TreeNode) => void>; + renderNode?: QRL<(node: TreeNode) => JSXOutput>; + gap?: number; isHover?: boolean; - id: string; + animate?: boolean; + animationDuration?: number; + expandLevel?: number; +} + +interface TreeNodeComponentProps { + node: TreeNode; + level: number; + gap: number; + isHover: boolean; + activeNodeId: string; + expandLevel: number; + onNodeClick: QRL<(node: TreeNode) => void>; + renderNode?: QRL<(node: TreeNode) => JSXOutput>; + animate?: boolean; + animationDuration?: number; } -const TreeNodeComponent = component$( - (props: { - node: TreeNode; - level: number; - gap: number; - isHover: boolean; - activeNodeId: string; - expandLevel: number; - onNodeClick: QRL<(node: TreeNode) => void>; - renderNode?: QRL<(node: TreeNode) => JSXOutput>; - animate?: boolean; - animationDuration?: number; - }) => { - const isExpanded = useSignal(props.expandLevel <= props.level); // Default to expanded - const hasChildren = props.node.children && props.node.children.length > 0; - - const handleNodeClick = $(() => { - // Set the current node as active - props.onNodeClick(props.node); - // Toggle expansion if it has children - if (hasChildren) { - isExpanded.value = !isExpanded.value; - } - }); - - const iterateProps = (porps: Record) => { - const displayProp = ['q:id', 'q:key']; - return displayProp.reduce((totalStr, prop) => { - if (porps[prop]) { - totalStr += `${prop}="${porps[prop]}" `; +// --------------------------------------------------------------------------- +// Helpers (pure, framework-agnostic) +// --------------------------------------------------------------------------- + +/** Prop keys that should be rendered next to the element name in the tree. */ +const DISPLAY_PROPS = ['q:id', 'q:key'] as const; + +/** + * Build a compact attribute string from a node's props. + * Only displays the keys listed in `DISPLAY_PROPS`. + */ +function formatDisplayProps(props: Record): string { + let result = ''; + for (const key of DISPLAY_PROPS) { + const value = props[key]; + if (value != null) { + result += `${key}="${String(value)}" `; + } + } + return result; +} + +// --------------------------------------------------------------------------- +// TreeNodeComponent +// --------------------------------------------------------------------------- + +const TreeNodeComponent = component$((props) => { + const isExpanded = useSignal(props.expandLevel <= props.level); + const hasChildren = (props.node.children?.length ?? 0) > 0; + + const handleNodeClick = $(() => { + props.onNodeClick(props.node); + if (hasChildren) { + isExpanded.value = !isExpanded.value; + } + }); + + const isActive = props.isHover && props.node.id === props.activeNodeId; + const duration = props.animationDuration ?? 200; + const shouldShowChildren = hasChildren && !isExpanded.value; + + const renderChildren = props.node.children?.map((child) => ( + + )); + + return ( +
+
( - - )); - return ( -
-
-
- {hasChildren ? ( - + onClick$={handleNodeClick} + > +
+ {hasChildren ? ( + + ) : ( +
+ )} +
+ {props.renderNode ? ( + <>{props.renderNode(props.node)} ) : ( -
+ <> + < + + {props.node.label || props.node.name} + + + {` ${formatDisplayProps(props.node.props ?? {})}`}> + + )} -
- {props.renderNode ? ( - <>{props.renderNode(props.node)} - ) : ( - <> - < - - {props.node.label || props.node.name} - - - {` ${iterateProps(props.node.props! || {})}`}> - - - )} -
- {hasChildren ? ( - props.animate ? ( -
- {renderChildren} -
- ) : ( - shouldShowChildren && <>{renderChildren} - ) - ) : null}
- ); - }, -); - -export const Tree = component$( - (props: { - data: Signal; - onNodeClick?: QRL<(node: TreeNode) => void>; - renderNode?: QRL<(node: TreeNode) => JSXOutput>; - gap?: number; - isHover?: boolean; - animate?: boolean; - animationDuration?: number; - expandLevel?: number; - }) => { - const ref = useSignal(); - const store = props.data; - const activeNodeId = useSignal(''); - // QRL to update the active node ID - const setActiveNode = $((node: TreeNode) => { - ref.value!.scrollLeft = ref.value!.scrollWidth; - activeNodeId.value = node.id; - props.onNodeClick && props.onNodeClick(node); - }); - - return ( -
- {store.value.map((rootNode) => ( - + + {hasChildren && + (props.animate ? ( +
+ {renderChildren} +
+ ) : ( + shouldShowChildren && <>{renderChildren} ))} -
- ); - }, -); +
+ ); +}); + +// --------------------------------------------------------------------------- +// Tree (public root component) +// --------------------------------------------------------------------------- + +export const Tree = component$((props) => { + const ref = useSignal(); + const activeNodeId = useSignal(''); + + const setActiveNode = $((node: TreeNode) => { + if (ref.value) { + ref.value.scrollLeft = ref.value.scrollWidth; + } + activeNodeId.value = node.id; + props.onNodeClick?.(node); + }); + + return ( +
+ {props.data.value.map((rootNode) => ( + + ))} +
+ ); +}); diff --git a/packages/ui/src/components/Tree/filterVnode.ts b/packages/ui/src/components/Tree/filterVnode.ts index 3ce7aa5..6dcc4e1 100644 --- a/packages/ui/src/components/Tree/filterVnode.ts +++ b/packages/ui/src/components/Tree/filterVnode.ts @@ -9,7 +9,7 @@ import { } from '@qwik.dev/core/internal'; import { normalizeName } from './vnode'; import { htmlContainer } from '../../utils/location'; -import { TreeNode } from './Tree'; +import { TreeNode, TreeNodePropValue } from './type'; import { QPROPS, QRENDERFN, QSEQ, QTYPE } from '@devtools/kit'; import { QRLInternal } from '../../features/RenderTree/types'; @@ -26,7 +26,7 @@ function initVnode({ name = 'text', props = {}, children = [], -}): TreeNode { +}: Partial> = {}): TreeNode { return { name, props, @@ -68,24 +68,23 @@ function buildTreeRecursive( _vnode_getAttrKeys( container, currentVNode as _ElementVNode | _VirtualVNode, - ).forEach( - (key) => { - // We skip the QTYPE prop as it's for internal use. - if (key === QTYPE) return; - // Keep only the fields consumed by Devtools to avoid - // leaking non-serializable runtime VNode references. - if (!ALLOWED_PROP_KEYS.has(key)) return; + ).forEach((key) => { + // We skip the QTYPE prop as it's for internal use. + if (key === QTYPE) return; + // Keep only the fields consumed by Devtools to avoid + // leaking non-serializable runtime VNode references. + if (!ALLOWED_PROP_KEYS.has(key)) return; - const value = container.getHostProp(currentVNode!, key) as QRLInternal; - vnodeObject.props![key] = value; + const value: unknown = container.getHostProp(currentVNode!, key); + vnodeObject.props![key] = value as TreeNodePropValue; - // Special handling to set the label from the render function's symbol. - if (key === QRENDERFN) { - vnodeObject.label = normalizeName(value!.getSymbol()); - vnodeObject.name = normalizeName(value!.getSymbol()); - } - }, - ); + // Special handling to set the label from the render function's symbol. + if (key === QRENDERFN && value != null) { + const qrl = value as QRLInternal; + vnodeObject.label = normalizeName(qrl.getSymbol()); + vnodeObject.name = normalizeName(qrl.getSymbol()); + } + }); // Recursively build the tree for child nodes. const firstChild = _vnode_getFirstChild(currentVNode); diff --git a/packages/ui/src/components/Tree/type.ts b/packages/ui/src/components/Tree/type.ts index aaa1504..f1d30cf 100644 --- a/packages/ui/src/components/Tree/type.ts +++ b/packages/ui/src/components/Tree/type.ts @@ -2,4 +2,33 @@ export const ISDEVTOOL = 'Qwikdevtools'; export const QContainerAttr = 'q:container'; -export type StoreTarget = Record; +/** + * A value stored inside a Qwik reactive store proxy. + * We intentionally keep a narrow union so that `Record` + * is assignable without falling back to `any`. + */ +export type StoreValue = string | number | boolean | null | undefined | object; + +export type StoreTarget = Record; + +/** Value types that a Qwik VNode prop can hold. */ +export type TreeNodePropValue = string | number | boolean | null | undefined | object; + +export type ElementType = + | 'null' + | 'boolean' + | 'number' + | 'string' + | 'function' + | 'array' + | 'object'; + +export interface TreeNode { + name?: string; + props?: Record; + children?: TreeNode[]; + elementType?: ElementType; + label?: string; + isHover?: boolean; + id: string; +} diff --git a/packages/ui/src/components/Tree/vnode.ts b/packages/ui/src/components/Tree/vnode.ts index 774d6b6..0b89a34 100644 --- a/packages/ui/src/components/Tree/vnode.ts +++ b/packages/ui/src/components/Tree/vnode.ts @@ -1,5 +1,4 @@ -import { TreeNode } from './Tree'; - +import { TreeNode } from './type'; export function normalizeName(str: string) { const array = str.split('_'); if (array.length > 0) { diff --git a/packages/ui/src/devtools.tsx b/packages/ui/src/devtools.tsx index e3c8be4..b671a25 100644 --- a/packages/ui/src/devtools.tsx +++ b/packages/ui/src/devtools.tsx @@ -150,32 +150,32 @@ export const QwikDevtools = component$(() => { {state.isOpen && ( -
+
- + - + - + - + - + - + - + - + -
+
diff --git a/packages/ui/src/features/Assets/Assets.tsx b/packages/ui/src/features/Assets/Assets.tsx index 80a4bbb..3bc6155 100644 --- a/packages/ui/src/features/Assets/Assets.tsx +++ b/packages/ui/src/features/Assets/Assets.tsx @@ -15,10 +15,10 @@ export const Assets = component$(({ state }: AssetsProps) => { return (
{isImage ? ( -
+
{ />
) : ( -
+
{fileExt} diff --git a/packages/ui/src/features/CodeBreack/CodeBreack.tsx b/packages/ui/src/features/CodeBreack/CodeBreack.tsx index 7dbbd99..f7ad9fa 100644 --- a/packages/ui/src/features/CodeBreack/CodeBreack.tsx +++ b/packages/ui/src/features/CodeBreack/CodeBreack.tsx @@ -26,7 +26,7 @@ export const CodeBreack = component$(() => {
{/* Segmented Navigation */}
-
+
-
-
+
+
VNode Tree
{parsingTime.value !== null && ( - + {parsingTime.value}ms )} diff --git a/packages/ui/src/features/CodeBreack/StateParser.tsx b/packages/ui/src/features/CodeBreack/StateParser.tsx index 40b096a..949ff31 100644 --- a/packages/ui/src/features/CodeBreack/StateParser.tsx +++ b/packages/ui/src/features/CodeBreack/StateParser.tsx @@ -116,11 +116,11 @@ export const StateParser = component$(() => { return (
-
-
+
+
Input State
{parsingTime.value !== null && ( - + {parsingTime.value}ms )} @@ -132,7 +132,7 @@ export const StateParser = component$(() => { (inputState.value = (t as HTMLTextAreaElement).value) } placeholder="Paste Qwik state and click to parse/format." - class="border-border bg-background h-full min-h-0 w-full flex-1 resize-none rounded-md border p-3 font-mono text-sm" + class="border-glass-border bg-card-item-bg text-foreground h-full min-h-0 w-full flex-1 resize-none rounded-md border p-3 font-mono text-sm placeholder:text-muted-foreground" />
-
-
+
+
Parsed State
{parsingTime.value !== null && ( - + {parsingTime.value}ms )} diff --git a/packages/ui/src/features/Packages/components/DependencyCard/DependencyCard.tsx b/packages/ui/src/features/Packages/components/DependencyCard/DependencyCard.tsx index d0fa8fd..1a73241 100644 --- a/packages/ui/src/features/Packages/components/DependencyCard/DependencyCard.tsx +++ b/packages/ui/src/features/Packages/components/DependencyCard/DependencyCard.tsx @@ -45,7 +45,7 @@ export const DependencyCard = component$(({ pkg }: { pkg: Package }) => { }); return ( -
+
{/* Subtle gradient background on hover */}
diff --git a/packages/ui/src/features/Performance/Performance.tsx b/packages/ui/src/features/Performance/Performance.tsx index 5590671..4c83b97 100644 --- a/packages/ui/src/features/Performance/Performance.tsx +++ b/packages/ui/src/features/Performance/Performance.tsx @@ -1,6 +1,16 @@ import { component$, useComputed$, useSignal, useTask$, $, isBrowser } from '@qwik.dev/core'; import type { QwikPerfStoreRemembered } from '@devtools/kit'; -import { computeEventRows, computePerfViewModel } from './computePerfViewModel'; +import { + computeEventRows, + computePerfViewModel, + type PerfComponentVm, + type PerfOverviewVm, + type PerfEventVm, +} from './computePerfViewModel'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- function formatMs(ms: number): string { if (!Number.isFinite(ms)) return '-'; @@ -9,6 +19,137 @@ function formatMs(ms: number): string { return `${ms.toFixed(2)}ms`; } +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +/** Single stat card shown in the overview row. */ +const StatCard = component$<{ label: string; value: string; subtitle?: string }>((props) => ( +
+
+
{props.label}
+
{props.value}
+ {props.subtitle && ( +
{props.subtitle}
+ )} +
+
+)); + +/** Top overview row of four stat cards. */ +const PerformanceOverview = component$<{ overview: PerfOverviewVm }>((props) => { + const { overview } = props; + const slowest = overview.slowestComponent; + return ( +
+ + + + +
+ ); +}); + +/** A single component card in the left-hand list. */ +const ComponentCard = component$<{ + component: PerfComponentVm; + selected: boolean; + onClick$: () => void; +}>((props) => { + const { component: c, selected } = props; + return ( + + ); +}); + +/** Single row inside the hook-details table. */ +const HookRow = component$<{ row: PerfEventVm }>((props) => ( +
+
{props.row.eventName}
+
{formatMs(props.row.time)}
+
{props.row.calls}
+
+)); + +/** Right-hand panel showing hook-level details for a selected component. */ +const HookDetailsPanel = component$<{ + component: PerfComponentVm; + onClose$: () => void; +}>((props) => { + const rows = computeEventRows(props.component.csrItems); + return ( + <> +
+
+
+ {props.component.componentName} Hook Details +
+
+ Total: {formatMs(props.component.totalTime)} • {props.component.calls} calls +
+
+ +
+ +
+
+
+
HOOK NAME
+
TIME
+
CALLS
+
+
+ {rows.map((row) => ( + + ))} + {rows.length === 0 && ( +
+ No CSR records for this component. +
+ )} +
+
+
+ + ); +}); + +// --------------------------------------------------------------------------- +// Main Performance page +// --------------------------------------------------------------------------- + export const Performance = component$(() => { const perf = useSignal(null); const selectedComponent = useSignal(null); @@ -23,7 +164,7 @@ export const Performance = component$(() => { const selectedVm = useComputed$(() => { const name = selectedComponent.value; if (!name) return null; - return vm.value.components.find((c) => c.componentName === name) || null; + return vm.value.components.find((c) => c.componentName === name) ?? null; }); const onSelect = $((name: string) => { @@ -34,9 +175,12 @@ export const Performance = component$(() => { selectedComponent.value = null; }); - return ( -
- {!perf.value?.ssr?.length && !perf.value?.csr?.length ? ( + // ── Empty-state ── + const hasData = perf.value?.ssr?.length || perf.value?.csr?.length; + + if (!hasData) { + return ( +
No performance data found. @@ -45,148 +189,58 @@ export const Performance = component$(() => {
- ) : ( -
- {/* Overview cards */} -
-
-
-
TOTAL RENDER TIME
-
{formatMs(vm.value.overview.totalRenderTime)}
-
-
+
+ ); + } -
-
-
SLOWEST COMPONENT
-
- {vm.value.overview.slowestComponent?.componentName || '-'} -
-
- {vm.value.overview.slowestComponent - ? `${formatMs(vm.value.overview.slowestComponent.avgTime)} avg` - : '-'} + // ── Main layout ── + return ( +
+
+ {/* Overview cards */} + + + {/* Main split */} +
+ {/* Left: component list */} +
+
+
+
Components
+
+ CSR only • {vm.value.components.length} total
-
-
-
AVG TIME
-
{formatMs(vm.value.overview.avgTime)}
-
-
- -
-
-
TOTAL CALLS
-
{vm.value.overview.totalCalls}
-
+
+ {vm.value.components.map((c) => ( + onSelect(c.componentName)} + /> + ))}
- {/* Main split */} -
-
-
-
-
Components
-
- CSR only • {vm.value.components.length} total -
-
-
- -
- {vm.value.components.map((c) => { - const selected = selectedComponent.value === c.componentName; - return ( - - ); - })} -
-
+
-
- -
- {selectedVm.value ? ( - <> -
-
-
- {selectedVm.value.componentName} Hook Details -
-
- Total: {formatMs(selectedVm.value.totalTime)} • {selectedVm.value.calls} calls -
-
- -
- -
-
-
-
HOOK NAME
-
TIME
-
CALLS
-
-
- {computeEventRows(selectedVm.value.csrItems).map((row) => ( -
-
{row.eventName}
-
{formatMs(row.time)}
-
{row.calls}
-
- ))} - {!selectedVm.value.csrItems.length && ( -
- No CSR records for this component. -
- )} -
-
-
- - ) : ( -
-
- Select a component to view hook details. -
+ {/* Right: hook details */} +
+ {selectedVm.value ? ( + + ) : ( +
+
+ Select a component to view hook details.
- )} -
+
+ )}
- )} +
); }); diff --git a/packages/ui/src/features/Performance/computePerfViewModel.test.ts b/packages/ui/src/features/Performance/computePerfViewModel.test.ts index 57f0b4a..aeba290 100644 --- a/packages/ui/src/features/Performance/computePerfViewModel.test.ts +++ b/packages/ui/src/features/Performance/computePerfViewModel.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { computeEventRows, computePerfViewModel } from './computePerfViewModel'; import type { QwikPerfStoreRemembered } from '@devtools/kit'; +import type { PerfGroupedCsrItem } from './transformPerformanceData'; describe('computePerfViewModel', () => { it('prefers ssr list when present and groups csr via _component_ prefix', () => { @@ -86,7 +87,7 @@ describe('computePerfViewModel', () => { describe('computeEventRows', () => { it('uses render when eventName is missing and uses renderCount as calls fallback', () => { - const rows = computeEventRows([ + const items: PerfGroupedCsrItem[] = [ { id: 1, component: 'A_component_x', @@ -95,8 +96,7 @@ describe('computeEventRows', () => { duration: 2, start: 0, end: 2, - // no eventName -> render - } as any, + }, { id: 2, component: 'A_component_x_useEffect_1', @@ -107,9 +107,10 @@ describe('computeEventRows', () => { start: 0, end: 1, renderCount: 3, - } as any, - ]); + }, + ]; + const rows = computeEventRows(items); const byName = new Map(rows.map((r) => [r.eventName, r])); expect(byName.get('render')?.calls).toBe(1); expect(byName.get('render')?.time).toBe(2); @@ -119,3 +120,4 @@ describe('computeEventRows', () => { }); + diff --git a/packages/ui/src/features/Performance/computePerfViewModel.ts b/packages/ui/src/features/Performance/computePerfViewModel.ts index 6e1ceff..457304e 100644 --- a/packages/ui/src/features/Performance/computePerfViewModel.ts +++ b/packages/ui/src/features/Performance/computePerfViewModel.ts @@ -1,5 +1,11 @@ import type { QwikPerfStoreRemembered } from '@devtools/kit'; -import { groupCsrBySsr, parseComponentAndEventName, type PerfGroupedCsrItem, type PerfSsrItem } from './transformPerformanceData'; +import { + groupCsrBySsr, + parseComponentAndEventName, + type PerfCsrItem, + type PerfGroupedCsrItem, + type PerfSsrItem, +} from './transformPerformanceData'; export type PerfPayload = QwikPerfStoreRemembered; @@ -64,34 +70,45 @@ function computeComponentVmFromCsr(componentName: string, csrItems: PerfGroupedC return { componentName, totalTime, calls, avgTime, ssr, csrItems }; } -export function computePerfViewModel(perf: PerfPayload | undefined | null): PerfViewModel { - const safe: PerfPayload = perf || { ssr: [], csr: [] }; +// --------------------------------------------------------------------------- +// Component grouping helpers +// --------------------------------------------------------------------------- + +/** Build component view-models when SSR entries are available. */ +function groupComponentsBySsr(safe: PerfPayload): PerfComponentVm[] { + const grouped = groupCsrBySsr(safe); + const result: PerfComponentVm[] = []; + for (const raw of safe.ssr) { + const ssrItem: PerfSsrItem = { ...raw, phase: 'ssr' as const }; + const componentName = parseComponentAndEventName(ssrItem.component).componentName; + const csrItems = grouped.get(raw as PerfSsrItem) || []; + result.push(computeComponentVmFromCsr(componentName, csrItems, ssrItem)); + } + return result; +} - const components: PerfComponentVm[] = []; - - if (safe.ssr?.length) { - const grouped = groupCsrBySsr(safe); - for (const ssrItem of safe.ssr as PerfSsrItem[]) { - const componentName = parseComponentAndEventName(ssrItem.component).componentName; - const csrItems = grouped.get(ssrItem as PerfSsrItem) || []; - components.push(computeComponentVmFromCsr(componentName, csrItems, ssrItem as PerfSsrItem)); - } - } else if (safe.csr?.length) { - const byName = new Map(); - for (const csrItem of safe.csr as any[]) { - const parsed = parseComponentAndEventName(csrItem.component); - const list = byName.get(parsed.componentName) || []; - list.push({ ...csrItem, ...parsed }); - byName.set(parsed.componentName, list); - } - for (const [componentName, csrItems] of byName.entries()) { - components.push(computeComponentVmFromCsr(componentName, csrItems)); - } +/** Build component view-models from CSR-only data, grouped by component name. */ +function groupComponentsByName(csrRaw: PerfPayload['csr']): PerfComponentVm[] { + const byName = new Map(); + for (const entry of csrRaw) { + const csrItem: PerfCsrItem = { ...entry, phase: 'csr' as const }; + const parsed = parseComponentAndEventName(csrItem.component); + const list = byName.get(parsed.componentName) || []; + list.push({ ...csrItem, ...parsed }); + byName.set(parsed.componentName, list); + } + const result: PerfComponentVm[] = []; + for (const [componentName, csrItems] of byName.entries()) { + result.push(computeComponentVmFromCsr(componentName, csrItems)); } + return result; +} - // Sort components by total time (desc) to make the list useful. - components.sort((a, b) => b.totalTime - a.totalTime); +// --------------------------------------------------------------------------- +// Overview helpers +// --------------------------------------------------------------------------- +function computeOverview(components: PerfComponentVm[]): PerfOverviewVm { let totalRenderTime = 0; let totalCalls = 0; for (const c of components) { @@ -107,7 +124,7 @@ export function computePerfViewModel(perf: PerfPayload | undefined | null): Perf return cur.avgTime > acc.avgTime ? cur : acc; }, undefined); - const overview: PerfOverviewVm = { + return { totalRenderTime, totalCalls, avgTime, @@ -120,8 +137,25 @@ export function computePerfViewModel(perf: PerfPayload | undefined | null): Perf } : undefined, }; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export function computePerfViewModel(perf: PerfPayload | undefined | null): PerfViewModel { + const safe: PerfPayload = perf || { ssr: [], csr: [] }; + + const components = safe.ssr?.length + ? groupComponentsBySsr(safe) + : safe.csr?.length + ? groupComponentsByName(safe.csr) + : []; + + // Sort components by total time (desc) to make the list useful. + components.sort((a, b) => b.totalTime - a.totalTime); - return { overview, components }; + return { overview: computeOverview(components), components }; } diff --git a/packages/ui/src/features/RenderTree/RenderTree.tsx b/packages/ui/src/features/RenderTree/RenderTree.tsx index 2931629..44da5ef 100644 --- a/packages/ui/src/features/RenderTree/RenderTree.tsx +++ b/packages/ui/src/features/RenderTree/RenderTree.tsx @@ -8,7 +8,8 @@ import { useResource$, Resource, } from '@qwik.dev/core'; -import { Tree, TreeNode } from '../../components/Tree/Tree'; +import { Tree } from '../../components/Tree/Tree'; +import type { TreeNode } from '../../components/Tree/type'; import { vnode_toObject } from '../../components/Tree/filterVnode'; import { htmlContainer } from '../../utils/location'; import { ISDEVTOOL } from '../../components/Tree/type'; @@ -16,6 +17,7 @@ import { removeNodeFromTree } from '../../components/Tree/vnode'; import { isListen } from '../../utils/type'; import debug from 'debug'; import { getHookStore, QrlUtils, type HookType } from './formatTreeData'; +import type { QRLInternal } from './types'; import { unwrapStore } from '@qwik.dev/core/internal'; import { getViteClientRpc, @@ -27,9 +29,16 @@ import { import { getHighlighter } from '../../utils/shiki'; import { getQwikState, returnQrlData } from './data'; import { IconChevronUpMini } from '../../components/Icons/Icons'; +import type { ParsedHookEntry } from './types'; const log = debug('qwik:devtools:renderTree'); +interface CodeModule { + pathId: string; + modules: { code: string } | null; + error?: string; +} + function getValueColorClass(node: TreeNode, valueText: string): string { switch (node.elementType) { case 'string': @@ -59,9 +68,7 @@ export const RenderTree = component$(() => { padding: 10px; } `); - const codes = useSignal<{ pathId: string; modules: any; error?: string }[]>( - [], - ); + const codes = useSignal([]); const data = useSignal([]); const stateTree = useSignal([]); @@ -114,14 +121,14 @@ export const RenderTree = component$(() => { if (node.props?.[QRENDERFN]) { hookStore.value.add('render', { data: { render: node.props[QRENDERFN] } }); - const qrl = QrlUtils.getChunkName(node.props[QRENDERFN]); + const qrl = QrlUtils.getChunkName(node.props[QRENDERFN] as QRLInternal); parsed = getQwikState(qrl); } if (Array.isArray(node.props?.[QSEQ]) && parsed.length > 0) { const normalizedData = [...parsed, ...returnQrlData(node.props?.[QSEQ])]; normalizedData.forEach((item) => { - hookStore.value.add(item.hookType as HookType, item); + hookStore.value.add(item.hookType as HookType, item as ParsedHookEntry); }); } @@ -140,43 +147,45 @@ export const RenderTree = component$(() => { (await rpc?.getModulesByPathIds(hookStore.value.findAllQrlPaths())) ?? []; log('getModulesByPathIds return: %O', res); codes.value = res.filter( - (item: { pathId: string; modules: unknown; error?: string }) => item.modules + (item: CodeModule) => item.modules ); - stateTree.value = hookStore.value.buildTree() as TreeNode[]; + stateTree.value = hookStore.value.buildTree(); hookFilters.value = hookStore.value.getFilterList(); }); const currentTab = useSignal<'state' | 'code'>('state'); return ( -
+
-
+
-
+
-
-
+
+
@@ -185,8 +194,8 @@ export const RenderTree = component$(() => { {currentTab.value === 'state' && (
-
-
+
+
Hooks @@ -202,7 +211,7 @@ export const RenderTree = component$(() => { hookFilters.value.some( (hook) => hook.key === item?.label && hook.display, ), - ) as TreeNode[]; + ); })} > Select all @@ -218,7 +227,7 @@ export const RenderTree = component$(() => { hookFilters.value.some( (hook) => hook.key === item?.label && hook.display, ), - ) as TreeNode[]; + ); })} > Clear @@ -245,9 +254,9 @@ export const RenderTree = component$(() => { }} > {hookFilters.value.map((item, idx) => ( -