diff --git a/apps/ai-dial-admin/src/components/Runs/Compare/CompareView.tsx b/apps/ai-dial-admin/src/components/Runs/Compare/CompareView.tsx index 2d67b7c089..12bb410241 100644 --- a/apps/ai-dial-admin/src/components/Runs/Compare/CompareView.tsx +++ b/apps/ai-dial-admin/src/components/Runs/Compare/CompareView.tsx @@ -1,6 +1,7 @@ 'use client'; import { IconAdjustmentsHorizontal } from '@tabler/icons-react'; +import { ColDef } from 'ag-grid-community'; import { useRouter } from 'next/navigation'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -76,6 +77,9 @@ const CompareView: FC = ({ runId, comparedRunId: comparedRunIdProp }) => const [availableMetricGroups, setAvailableMetricGroups] = useState([]); const [selectedMetricGroups, setSelectedMetricGroups] = useState>(new Set()); const areMetricGroupsInitializedRef = useRef(false); + const displayTreeRef = useRef([]); + const viewDifferencesOnlyRef = useRef(false); + const hideHighlightsRef = useRef(false); const { state: tabState, @@ -215,6 +219,16 @@ const CompareView: FC = ({ runId, comparedRunId: comparedRunIdProp }) => setSelectedMetricGroups(groups); }, []); + const persistDisplayTree = useCallback((tree: ColDef[]) => { + displayTreeRef.current = tree; + }, []); + const persistViewDifferencesOnly = useCallback((value: boolean) => { + viewDifferencesOnlyRef.current = value; + }, []); + const persistHideHighlights = useCallback((value: boolean) => { + hideHighlightsRef.current = value; + }, []); + const closeRowDetail = useCallback(() => { setSelectedRow(null); setFocusFieldKey(null); @@ -250,6 +264,12 @@ const CompareView: FC = ({ runId, comparedRunId: comparedRunIdProp }) => focusFieldKey={fieldKey} fieldSchema={fieldSchema} metricGroupOrder={metricGroupOrder} + initialDisplayTree={displayTreeRef.current} + onDisplayTreeChange={persistDisplayTree} + initialViewDifferencesOnly={viewDifferencesOnlyRef.current} + onViewDifferencesOnlyChange={persistViewDifferencesOnly} + initialHideHighlights={hideHighlightsRef.current} + onHideHighlightsChange={persistHideHighlights} /> ) : ( = ({ runId, comparedRunId: comparedRunIdProp }) => focusFieldKey={fieldKey} fieldSchema={fieldSchema} metricGroupOrder={metricGroupOrder} + initialDisplayTree={displayTreeRef.current} + onDisplayTreeChange={persistDisplayTree} + initialViewDifferencesOnly={viewDifferencesOnlyRef.current} + onViewDifferencesOnlyChange={persistViewDifferencesOnly} + initialHideHighlights={hideHighlightsRef.current} + onHideHighlightsChange={persistHideHighlights} /> ); const className = isBottom ? ROW_DETAIL_BOTTOM_CLASS : ROW_DETAIL_SIDEBAR_CLASS; sidebarRef.current.showSidebar(content, className, position); }, - [primaryRunName, comparedRunName, closeRowDetail, switchToBottom, switchToSidebar, fieldSchema, metricGroupOrder], + [ + primaryRunName, + comparedRunName, + closeRowDetail, + switchToBottom, + switchToSidebar, + fieldSchema, + metricGroupOrder, + persistDisplayTree, + persistViewDifferencesOnly, + persistHideHighlights, + ], ); showDetailPanelRef.current = showDetailPanel; @@ -292,6 +329,9 @@ const CompareView: FC = ({ runId, comparedRunId: comparedRunIdProp }) => useEffect(() => { closeRowDetail(); + displayTreeRef.current = []; + viewDifferencesOnlyRef.current = false; + hideHighlightsRef.current = false; setAvailableMetricGroups([]); setSelectedMetricGroups(new Set()); areMetricGroupsInitializedRef.current = false; diff --git a/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailBottomPanel.tsx b/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailBottomPanel.tsx index 317eba2837..e38698471e 100644 --- a/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailBottomPanel.tsx +++ b/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailBottomPanel.tsx @@ -1,5 +1,6 @@ 'use client'; +import { ColDef } from 'ag-grid-community'; import { FC } from 'react'; import CompareRowDetailPanel from '@/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPanel'; @@ -16,6 +17,12 @@ interface Props { focusFieldKey?: string | null; fieldSchema?: RowDetailFieldSchema; metricGroupOrder?: readonly string[]; + initialDisplayTree?: ColDef[]; + onDisplayTreeChange?: (tree: ColDef[]) => void; + initialViewDifferencesOnly?: boolean; + onViewDifferencesOnlyChange?: (value: boolean) => void; + initialHideHighlights?: boolean; + onHideHighlightsChange?: (value: boolean) => void; } const CompareRowDetailBottomPanel: FC = ({ @@ -27,6 +34,12 @@ const CompareRowDetailBottomPanel: FC = ({ focusFieldKey, fieldSchema, metricGroupOrder, + initialDisplayTree, + onDisplayTreeChange, + initialViewDifferencesOnly, + onViewDifferencesOnlyChange, + initialHideHighlights, + onHideHighlightsChange, }) => { return (
@@ -40,6 +53,12 @@ const CompareRowDetailBottomPanel: FC = ({ focusFieldKey={focusFieldKey} fieldSchema={fieldSchema} metricGroupOrder={metricGroupOrder} + initialDisplayTree={initialDisplayTree} + onDisplayTreeChange={onDisplayTreeChange} + initialViewDifferencesOnly={initialViewDifferencesOnly} + onViewDifferencesOnlyChange={onViewDifferencesOnlyChange} + initialHideHighlights={initialHideHighlights} + onHideHighlightsChange={onHideHighlightsChange} />
); diff --git a/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPanel.tsx b/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPanel.tsx index 07c94355c2..d1d3327d48 100644 --- a/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPanel.tsx +++ b/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPanel.tsx @@ -42,6 +42,12 @@ interface Props { focusFieldKey?: string | null; fieldSchema?: RowDetailFieldSchema; metricGroupOrder?: readonly string[]; + initialDisplayTree?: ColDef[]; + onDisplayTreeChange?: (tree: ColDef[]) => void; + initialViewDifferencesOnly?: boolean; + onViewDifferencesOnlyChange?: (value: boolean) => void; + initialHideHighlights?: boolean; + onHideHighlightsChange?: (value: boolean) => void; } const CompareRowDetailPanel: FC = ({ @@ -55,6 +61,12 @@ const CompareRowDetailPanel: FC = ({ focusFieldKey, fieldSchema, metricGroupOrder = [], + initialDisplayTree, + onDisplayTreeChange, + initialViewDifferencesOnly, + onViewDifferencesOnlyChange, + initialHideHighlights, + onHideHighlightsChange, }) => { const t = useI18n(); const [primaryDetail, setPrimaryDetail] = useState(null); @@ -63,9 +75,9 @@ const CompareRowDetailPanel: FC = ({ const [hasError, setHasError] = useState(false); const [showDisplayPanel, setShowDisplayPanel] = useState(false); - const [viewDifferencesOnly, setViewDifferencesOnly] = useState(false); - const [hideHighlights, setHideHighlights] = useState(false); - const [displayTree, setDisplayTree] = useState([]); + const [viewDifferencesOnly, setViewDifferencesOnly] = useState(initialViewDifferencesOnly ?? false); + const [hideHighlights, setHideHighlights] = useState(initialHideHighlights ?? false); + const [displayTree, setDisplayTree] = useState(() => initialDisplayTree ?? []); const isPivotView = position === SidebarPosition.Bottom; const comparedId = row._compared?.id ?? null; @@ -140,10 +152,31 @@ const CompareRowDetailPanel: FC = ({ setDisplayTree((prev) => buildRowDetailDisplayTree(sections, prev, DEFAULT_HIDDEN_ROW_DETAIL_FIELDS)); }, [sections]); + useEffect(() => { + if (displayTree.length === 0) { + return; + } + onDisplayTreeChange?.(displayTree); + }, [displayTree, onDisplayTreeChange]); + const displaySections = useMemo(() => applyRowDetailDisplayTree(sections, displayTree), [sections, displayTree]); const onToggleDisplayPanel = useCallback(() => setShowDisplayPanel((prev) => !prev), []); const onCloseDisplayPanel = useCallback(() => setShowDisplayPanel(false), []); + const onViewDifferencesOnlyUpdate = useCallback( + (value: boolean) => { + setViewDifferencesOnly(value); + onViewDifferencesOnlyChange?.(value); + }, + [onViewDifferencesOnlyChange], + ); + const onHideHighlightsUpdate = useCallback( + (value: boolean) => { + setHideHighlights(value); + onHideHighlightsChange?.(value); + }, + [onHideHighlightsChange], + ); return (
@@ -200,9 +233,9 @@ const CompareRowDetailPanel: FC = ({ onClose={onCloseDisplayPanel} panelClassName={ROW_DETAIL_DISPLAY_PANEL_CLASS} viewDifferencesOnly={viewDifferencesOnly} - onViewDifferencesOnlyChange={setViewDifferencesOnly} + onViewDifferencesOnlyChange={onViewDifferencesOnlyUpdate} hideHighlights={hideHighlights} - onHideHighlightsChange={setHideHighlights} + onHideHighlightsChange={onHideHighlightsUpdate} />
)} diff --git a/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPivotTable.tsx b/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPivotTable.tsx index 717572dbf3..0a65649fb3 100644 --- a/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPivotTable.tsx +++ b/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPivotTable.tsx @@ -43,7 +43,8 @@ interface Props { const HEADER_CELL_BASE = 'h-10 px-3 flex items-center bg-layer-1 border-b border-secondary dial-small-semi-text'; const LEFT_CELL_STICKY = 'sticky left-0'; -const VALUE_CELL_BASE = 'p-3 border-b border-r border-tertiary min-w-0 overflow-hidden h-10 flex items-center'; +const VALUE_CELL_BASE = + 'p-3 border-b border-r border-tertiary min-w-0 overflow-hidden h-full min-h-10 flex items-start self-stretch'; const CompareRowDetailPivotTable: FC = ({ sections, @@ -73,6 +74,7 @@ const CompareRowDetailPivotTable: FC = ({ const gridTemplateColumns = useMemo(() => getPivotGridTemplateColumns(columns), [columns]); const gridMinWidth = useMemo(() => getPivotGridMinWidth(columns), [columns]); + const gridTemplateRows = hasComparedMatch ? 'auto auto 1fr 1fr auto' : 'auto auto 1fr 1fr'; useEffect(() => { scrollPivotToField(scrollContainerRef.current, focusFieldKey); @@ -95,7 +97,7 @@ const CompareRowDetailPivotTable: FC = ({ field={field} raw={raw} isFailed={isFailed} - className={mergeClasses(rowBg, 'h-10 min-h-10 items-center self-auto', props.className)} + className={mergeClasses(rowBg, props.className)} data-compare-diff={props['data-compare-diff']} onOpenFullscreen={() => onOpenDiff(field)} /> @@ -108,7 +110,10 @@ const CompareRowDetailPivotTable: FC = ({ ref={scrollContainerRef} className="flex-1 min-h-0 overflow-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" > -
+
{/* Section header row */}
{columns.map((column) => { diff --git a/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/tests/CompareRowDetailPanel.spec.tsx b/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/tests/CompareRowDetailPanel.spec.tsx new file mode 100644 index 0000000000..7615634bd7 --- /dev/null +++ b/apps/ai-dial-admin/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/tests/CompareRowDetailPanel.spec.tsx @@ -0,0 +1,87 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { describe, expect, test, vi } from 'vitest'; + +import { SidebarPosition } from '@/src/components/Common/Sidebar/models'; +import CompareRowDetailPanel from '@/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPanel'; +import { CompareAnalyticsRow } from '@/src/components/Runs/View/models'; + +vi.mock('@/src/app/[lang]/runs/actions', () => ({ + getTestCaseRunResultDetails: vi.fn((id: string) => + Promise.resolve({ + id, + executionStatus: 'SUCCESS', + execDurationMs: 100, + testCaseName: 'Case 1', + }), + ), +})); + +vi.mock('@/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailPivotTable', () => ({ + default: () =>
pivot
, +})); + +vi.mock('@/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailTable', () => ({ + default: () =>
table
, +})); + +vi.mock('@/src/components/Runs/Compare/ExecutionResults/DiffLegend', () => ({ + default: () => null, +})); + +vi.mock('@/src/components/Runs/Compare/ExecutionResults/RowCompareDetails/CompareRowDetailDisplayPanel', () => ({ + default: ({ hideHighlights, viewDifferencesOnly }: { hideHighlights: boolean; viewDifferencesOnly: boolean }) => ( +
+ {hideHighlights ? 'highlights-hidden' : 'highlights-shown'} + {viewDifferencesOnly ? 'diffs-only' : 'all-rows'} +
+ ), +})); + +const row = { + id: 'r1', + testCaseName: 'Case 1', + _compared: { id: 'r2', testCaseName: 'Case 1' }, +} as CompareAnalyticsRow; + +describe('CompareRowDetailPanel', () => { + test('persists Display tree after sections load', async () => { + const onDisplayTreeChange = vi.fn(); + + render( + , + ); + + await waitFor(() => { + expect(onDisplayTreeChange).toHaveBeenCalled(); + }); + expect(onDisplayTreeChange.mock.calls.at(-1)?.[0]).toEqual(expect.arrayContaining([expect.any(Object)])); + }); + + test('restores Display diff toggles from initial values', () => { + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: 'Runs.RunCompareDisplay' })); + + expect(screen.getByText('highlights-hidden')).toBeInTheDocument(); + expect(screen.getByText('diffs-only')).toBeInTheDocument(); + }); +}); diff --git a/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/constants.ts b/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/constants.ts index d5f800e6c7..e011ac7024 100644 --- a/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/constants.ts +++ b/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/constants.ts @@ -9,9 +9,10 @@ export const ROW_DETAIL_PIVOT_LEFT_COL_WIDTH = 212; export const ROW_DETAIL_PIVOT_STATUS_COL_WIDTH = 135; export const ROW_DETAIL_PIVOT_RUN_NUMBER_COL_WIDTH = 124; export const ROW_DETAIL_PIVOT_HTTP_COL_WIDTH = 66; -export const ROW_DETAIL_PIVOT_DURATION_COL_WIDTH = 109; +export const ROW_DETAIL_PIVOT_DURATION_COL_WIDTH = 140; export const ROW_DETAIL_PIVOT_SCORE_COL_WIDTH = 124; export const ROW_DETAIL_PIVOT_DEFAULT_COL_WIDTH = 200; +export const ROW_DETAIL_PIVOT_TEXT_COL_WIDTH = 320; export const ROW_DETAIL_EXECUTION_SECTION_KEY = 'execution'; export const ROW_DETAIL_RUN_NUMBER_FIELD_KEY = 'runNumber'; diff --git a/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/models.ts b/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/models.ts index 6b2197f872..ae3ef681b2 100644 --- a/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/models.ts +++ b/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/models.ts @@ -33,4 +33,5 @@ export enum PivotColumnWidthTier { Duration = 'duration', Score = 'score', Default = 'default', + Text = 'text', } diff --git a/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/utils/pivot-column-width.ts b/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/utils/pivot-column-width.ts index 8cbf5dbe7d..31c0808ac4 100644 --- a/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/utils/pivot-column-width.ts +++ b/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/utils/pivot-column-width.ts @@ -9,6 +9,7 @@ import { ROW_DETAIL_RUN_NUMBER_FIELD_KEY, ROW_DETAIL_PIVOT_SCORE_COL_WIDTH, ROW_DETAIL_PIVOT_STATUS_COL_WIDTH, + ROW_DETAIL_PIVOT_TEXT_COL_WIDTH, } from '@/src/components/Runs/Details/RowDetails/constants'; import { RowDetailField, PivotColumnWidthTier } from '@/src/components/Runs/Details/RowDetails/models'; import { PivotColumn } from '@/src/components/Runs/Details/RowDetails/utils/flatten-pivot-fields'; @@ -21,6 +22,7 @@ const PIVOT_COLUMN_WIDTH_BY_TIER: Record = { [PivotColumnWidthTier.Duration]: ROW_DETAIL_PIVOT_DURATION_COL_WIDTH, [PivotColumnWidthTier.Score]: ROW_DETAIL_PIVOT_SCORE_COL_WIDTH, [PivotColumnWidthTier.Default]: ROW_DETAIL_PIVOT_DEFAULT_COL_WIDTH, + [PivotColumnWidthTier.Text]: ROW_DETAIL_PIVOT_TEXT_COL_WIDTH, }; export const resolvePivotFieldWidthTier = (field: RowDetailField): PivotColumnWidthTier => { @@ -41,6 +43,10 @@ export const resolvePivotFieldWidthTier = (field: RowDetailField): PivotColumnWi return PivotColumnWidthTier.Score; } + if (!field.isNumeric) { + return PivotColumnWidthTier.Text; + } + return PivotColumnWidthTier.Default; }; diff --git a/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/utils/tests/pivot-column-width.spec.ts b/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/utils/tests/pivot-column-width.spec.ts index 2ea5640c12..a0ef568fac 100644 --- a/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/utils/tests/pivot-column-width.spec.ts +++ b/apps/ai-dial-admin/src/components/Runs/Details/RowDetails/utils/tests/pivot-column-width.spec.ts @@ -8,6 +8,7 @@ import { ROW_DETAIL_PIVOT_RUN_NUMBER_COL_WIDTH, ROW_DETAIL_PIVOT_SCORE_COL_WIDTH, ROW_DETAIL_PIVOT_STATUS_COL_WIDTH, + ROW_DETAIL_PIVOT_TEXT_COL_WIDTH, } from '@/src/components/Runs/Details/RowDetails/constants'; import { RowDetailField, PivotColumnWidthTier } from '@/src/components/Runs/Details/RowDetails/models'; import { PivotColumn } from '@/src/components/Runs/Details/RowDetails/utils/flatten-pivot-fields'; @@ -51,8 +52,12 @@ describe('resolvePivotFieldWidthTier', () => { expect(resolvePivotFieldWidthTier(field('equality', { isScoreIndicator: true }))).toBe(PivotColumnWidthTier.Score); }); - test('falls back to default tier for text fields', () => { - expect(resolvePivotFieldWidthTier(field('answer'))).toBe(PivotColumnWidthTier.Default); + test('maps non-numeric fields to the text tier', () => { + expect(resolvePivotFieldWidthTier(field('answer'))).toBe(PivotColumnWidthTier.Text); + }); + + test('falls back to default tier for numeric non-score fields', () => { + expect(resolvePivotFieldWidthTier(field('exact_match', { isNumeric: true }))).toBe(PivotColumnWidthTier.Default); }); }); @@ -65,7 +70,10 @@ describe('resolvePivotFieldColumnWidth', () => { expect(resolvePivotFieldColumnWidth(field('equality', { isScoreIndicator: true }))).toBe( ROW_DETAIL_PIVOT_SCORE_COL_WIDTH, ); - expect(resolvePivotFieldColumnWidth(field('answer'))).toBe(ROW_DETAIL_PIVOT_DEFAULT_COL_WIDTH); + expect(resolvePivotFieldColumnWidth(field('answer'))).toBe(ROW_DETAIL_PIVOT_TEXT_COL_WIDTH); + expect(resolvePivotFieldColumnWidth(field('exact_match', { isNumeric: true }))).toBe( + ROW_DETAIL_PIVOT_DEFAULT_COL_WIDTH, + ); }); }); diff --git a/apps/ai-dial-admin/src/components/Runs/View/RowDetails/ExecutionRowDetailBottomPanel.tsx b/apps/ai-dial-admin/src/components/Runs/View/RowDetails/ExecutionRowDetailBottomPanel.tsx index 6078f62b39..bcedd81545 100644 --- a/apps/ai-dial-admin/src/components/Runs/View/RowDetails/ExecutionRowDetailBottomPanel.tsx +++ b/apps/ai-dial-admin/src/components/Runs/View/RowDetails/ExecutionRowDetailBottomPanel.tsx @@ -36,6 +36,8 @@ interface Props { metricGroupOrder?: readonly string[]; fieldSchema?: RowDetailFieldSchema; className?: string; + initialDisplayTree?: ColDef[]; + onDisplayTreeChange?: (tree: ColDef[]) => void; } const ExecutionRowDetailBottomPanel: FC = ({ @@ -46,13 +48,15 @@ const ExecutionRowDetailBottomPanel: FC = ({ metricGroupOrder = [], fieldSchema, className, + initialDisplayTree, + onDisplayTreeChange, }) => { const t = useI18n(); const [detail, setDetail] = useState(null); const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); const [showDisplayPanel, setShowDisplayPanel] = useState(false); - const [displayTree, setDisplayTree] = useState([]); + const [displayTree, setDisplayTree] = useState(() => initialDisplayTree ?? []); useEffect(() => { let isCancelled = false; @@ -98,6 +102,13 @@ const ExecutionRowDetailBottomPanel: FC = ({ setDisplayTree((prev) => buildRowDetailDisplayTree(sections, prev, EXECUTION_RESULT_DEFAULT_HIDDEN_FIELDS)); }, [sections]); + useEffect(() => { + if (displayTree.length === 0) { + return; + } + onDisplayTreeChange?.(displayTree); + }, [displayTree, onDisplayTreeChange]); + const displaySections = useMemo(() => applyRowDetailDisplayTree(sections, displayTree), [sections, displayTree]); const title = useMemo(() => (detail ? getRowDetailTitle(detail) : null), [detail]); diff --git a/apps/ai-dial-admin/src/components/Runs/View/tests/use-detail-mode.spec.tsx b/apps/ai-dial-admin/src/components/Runs/View/tests/use-detail-mode.spec.tsx index f8e81c426d..e83235c1e2 100644 --- a/apps/ai-dial-admin/src/components/Runs/View/tests/use-detail-mode.spec.tsx +++ b/apps/ai-dial-admin/src/components/Runs/View/tests/use-detail-mode.spec.tsx @@ -91,4 +91,29 @@ describe('useDetailMode', () => { expect(mockCloseSidebar).toHaveBeenCalled(); }); + + it('reopens the bottom panel with the persisted Display tree', () => { + const { result } = renderHook(() => useDetailMode()); + const persistedTree = [ + { + headerName: 'Execution', + hide: false, + children: [{ headerName: 'Duration', hide: false, context: { panelName: 'execDurationMs' } }], + }, + ]; + + act(() => result.current.openDetail('r1')); + + const firstPanel = mockShowSidebar.mock.calls[0][0] as { props: { onDisplayTreeChange?: (tree: unknown) => void } }; + act(() => { + firstPanel.props.onDisplayTreeChange?.(persistedTree); + }); + + act(() => result.current.closeDetail()); + mockShowSidebar.mockClear(); + act(() => result.current.openDetail('r1')); + + const reopenedPanel = mockShowSidebar.mock.calls[0][0] as { props: { initialDisplayTree?: unknown } }; + expect(reopenedPanel.props.initialDisplayTree).toBe(persistedTree); + }); }); diff --git a/apps/ai-dial-admin/src/components/Runs/View/use-detail-mode.tsx b/apps/ai-dial-admin/src/components/Runs/View/use-detail-mode.tsx index 60093f43ba..fdbb051f28 100644 --- a/apps/ai-dial-admin/src/components/Runs/View/use-detail-mode.tsx +++ b/apps/ai-dial-admin/src/components/Runs/View/use-detail-mode.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +import type { ColDef } from 'ag-grid-community'; + import { SidebarPosition } from '@/src/components/Common/Sidebar/models'; import { ROW_DETAIL_BOTTOM_CLASS } from '@/src/components/Runs/Details/RowDetails/constants'; import RunMetricDetailPanel from '@/src/components/Runs/Details/RunMetricDetailPanel'; @@ -47,6 +49,11 @@ export function useDetailMode( detailModeRef.current = detailMode; const showDetailPanelRef = useRef<(resultId: string, mode: DetailMode, fieldKey: string | null) => void>(() => {}); + const displayTreeRef = useRef([]); + + const persistDisplayTree = useCallback((tree: ColDef[]) => { + displayTreeRef.current = tree; + }, []); const closeDetail = useCallback(() => { setSelectedResultId(null); @@ -79,6 +86,8 @@ export function useDetailMode( focusFieldKey={fieldKey} metricGroupOrder={metricGroupOrder} fieldSchema={fieldSchema} + initialDisplayTree={displayTreeRef.current} + onDisplayTreeChange={persistDisplayTree} onClose={closeDetail} onSwitchToSidebar={switchToSidebar} />, @@ -99,7 +108,7 @@ export function useDetailMode( SidebarPosition.Right, ); }, - [closeDetail, switchToSidebar, switchToDrawer, metricBindings, metricGroupOrder, fieldSchema], + [closeDetail, switchToSidebar, switchToDrawer, persistDisplayTree, metricBindings, metricGroupOrder, fieldSchema], ); showDetailPanelRef.current = showDetailPanel; diff --git a/openspec/changes/redesign-compare-execution-row-details/specs/run-compare-row-details/spec.md b/openspec/changes/redesign-compare-execution-row-details/specs/run-compare-row-details/spec.md index 51eabeda4b..85ffacf168 100644 --- a/openspec/changes/redesign-compare-execution-row-details/specs/run-compare-row-details/spec.md +++ b/openspec/changes/redesign-compare-execution-row-details/specs/run-compare-row-details/spec.md @@ -65,6 +65,18 @@ Row-detail Display SHALL control field visibility/order and diff toggles only. V - **WHEN** the panel is on the right - **THEN** the body shows the table view +### Requirement: Display settings survive close and reopen + +Closing and reopening the compare row-detail panel within the same comparison session SHALL preserve Display field visibility, order, and diff toggles. Changing either compared run SHALL reset Display to defaults. + +#### Scenario: Display settings survive close and reopen +- **WHEN** the user changes Display visibility or diff toggles, closes the panel, and opens it again on the same comparison +- **THEN** the previously chosen field visibility, order, and diff toggles are still applied + +#### Scenario: Changing compared runs resets Display +- **WHEN** the user changes the primary or compared run +- **THEN** Display field visibility and diff toggles reset to defaults + ### Requirement: Drawer displays two-run comparison for the clicked row The compare row-detail panel SHALL display a side-by-side comparison between the primary and compared run results for the selected test case, including diff highlighting and counts. diff --git a/openspec/changes/redesign-execution-result-bottom-panel/specs/analytics-bottom-drawer/spec.md b/openspec/changes/redesign-execution-result-bottom-panel/specs/analytics-bottom-drawer/spec.md index 613ef212a1..2f4d4937c9 100644 --- a/openspec/changes/redesign-execution-result-bottom-panel/specs/analytics-bottom-drawer/spec.md +++ b/openspec/changes/redesign-execution-result-bottom-panel/specs/analytics-bottom-drawer/spec.md @@ -33,19 +33,23 @@ The bottom panel body SHALL render a pivot with a section-header row, a field-la ### Requirement: Display overlay controls field visibility and order -The panel header SHALL provide a Display control that opens an overlay `TreeColumnsPanel` for reordering and hiding fields/sections. By default, Execution Status, `# Run number`, HTTP, and Request / Response body fields SHALL be visible; Duration SHALL be hidden. Closing the panel SHALL discard Display session state. +The panel header SHALL provide a Display control that opens an overlay `TreeColumnsPanel` for reordering and hiding fields/sections. By default, Execution Status, `# Run number`, HTTP, and Request / Response body fields SHALL be visible; Duration SHALL be hidden. Closing and reopening the panel within the same run session SHALL preserve Display field visibility and order. Navigating away from the run SHALL reset Display to those defaults. #### Scenario: Display opens field tree - **WHEN** the user clicks Display - **THEN** an overlay tree of sections and fields is shown for visibility and order changes #### Scenario: Default field visibility -- **WHEN** the bottom panel opens +- **WHEN** the bottom panel opens for the first time in a run session - **THEN** Duration is hidden until shown via Display, and Request / Response body fields are visible #### Scenario: Hide a field - **WHEN** the user hides a field in Display -- **THEN** that field’s column is removed from the pivot until shown again or the panel is reopened +- **THEN** that field’s column is removed from the pivot until shown again + +#### Scenario: Display settings survive close and reopen +- **WHEN** the user changes Display visibility, closes the bottom panel, and opens it again on the same run +- **THEN** the previously chosen field visibility and order are still applied ### Requirement: Grid cell click scrolls to the related pivot column diff --git a/openspec/specs/analytics-bottom-drawer/spec.md b/openspec/specs/analytics-bottom-drawer/spec.md index f1b95ba85c..2fc6f08c12 100644 --- a/openspec/specs/analytics-bottom-drawer/spec.md +++ b/openspec/specs/analytics-bottom-drawer/spec.md @@ -57,19 +57,23 @@ The bottom pivot SHALL include every Test Case Data, Extracted, and metric field ### Requirement: Display overlay controls field visibility and order -The panel header SHALL provide a Display control that opens an overlay `TreeColumnsPanel` for reordering and hiding fields/sections. By default, Execution Status, `# Run number`, HTTP, and Request / Response body fields SHALL be visible; Duration SHALL be hidden. Closing the panel SHALL discard Display session state. +The panel header SHALL provide a Display control that opens an overlay `TreeColumnsPanel` for reordering and hiding fields/sections. By default, Execution Status, `# Run number`, HTTP, and Request / Response body fields SHALL be visible; Duration SHALL be hidden. Closing and reopening the panel within the same run session SHALL preserve Display field visibility and order. Navigating away from the run SHALL reset Display to those defaults. #### Scenario: Display opens field tree - **WHEN** the user clicks Display - **THEN** an overlay tree of sections and fields is shown for visibility and order changes #### Scenario: Default field visibility -- **WHEN** the bottom panel opens +- **WHEN** the bottom panel opens for the first time in a run session - **THEN** Duration is hidden until shown via Display, and Request / Response body fields are visible #### Scenario: Hide a field - **WHEN** the user hides a field in Display -- **THEN** that field’s column is removed from the pivot until shown again or the panel is reopened +- **THEN** that field’s column is removed from the pivot until shown again + +#### Scenario: Display settings survive close and reopen +- **WHEN** the user changes Display visibility, closes the bottom panel, and opens it again on the same run +- **THEN** the previously chosen field visibility and order are still applied ### Requirement: Grid cell click scrolls to the related pivot column diff --git a/openspec/specs/run-compare-row-details/spec.md b/openspec/specs/run-compare-row-details/spec.md index 3cafe42d15..a13376f911 100644 --- a/openspec/specs/run-compare-row-details/spec.md +++ b/openspec/specs/run-compare-row-details/spec.md @@ -63,6 +63,18 @@ Row-detail Display SHALL control field visibility/order and diff toggles only. V - **WHEN** the panel is on the right - **THEN** the body shows the table view +### Requirement: Display settings survive close and reopen + +Closing and reopening the compare row-detail panel within the same comparison session SHALL preserve Display field visibility, order, and diff toggles. Changing either compared run SHALL reset Display to defaults. + +#### Scenario: Display settings survive close and reopen +- **WHEN** the user changes Display visibility or diff toggles, closes the panel, and opens it again on the same comparison +- **THEN** the previously chosen field visibility, order, and diff toggles are still applied + +#### Scenario: Changing compared runs resets Display +- **WHEN** the user changes the primary or compared run +- **THEN** Display field visibility and diff toggles reset to defaults + ### Requirement: Drawer displays two-run comparison for the clicked row The compare row-detail panel SHALL display a side-by-side comparison between the primary and compared run results for the selected test case, including diff highlighting and counts.