diff --git a/ROADMAP.md b/ROADMAP.md index 620fc85c..913a06ca 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -401,6 +401,17 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind - [x] Updated SchemaRenderer mock to forward `className` and include interactive child button for more realistic testing - [x] Add 9 new Vitest tests: pointer-events-none presence/absence, overlay presence/absence, relative positioning, click-to-select on Card-based widgets +**Phase 10 — Debug / Diagnostic Mode:** +- [x] Add `debug?: boolean` field to `DashboardSchema` type definition +- [x] Add `'dashboard'` to core `DebugCategory` type for `debugLog()` integration +- [x] Implement `DashboardDebugOverlay` — collapsible diagnostic panel rendered at dashboard top when `schema.debug: true` or `SchemaRendererProvider debug` context is enabled +- [x] Per-widget diagnostics: type, resolved component type, data presence, `provider:object` status, `objectName`, aggregate config +- [x] Color-coded status: green border for widgets with data/provider, red border for missing data +- [x] Console logging via `debugLog('dashboard', ...)` when `OBJECTUI_DEBUG` is globally enabled +- [x] Integrate debug mode into both `DashboardRenderer` and `DashboardGridLayout` +- [x] Export `DashboardDebugOverlay` and `WidgetDebugInfo` from `@object-ui/plugin-dashboard` +- [x] Add 7 Vitest tests (overlay render, toggle expand, per-widget diagnostics, provider:object identification, debugLog emission) + ### P1.11 Console — Schema-Driven View Config Panel Migration > Migrated the Console ViewConfigPanel from imperative implementation (~1655 lines) to Schema-Driven architecture using `ConfigPanelRenderer` + `useConfigDraft` + `ConfigPanelSchema`, reducing to ~170 lines declarative wrapper + schema factory. diff --git a/packages/core/src/utils/debug.ts b/packages/core/src/utils/debug.ts index 38a4eeda..dd22d66e 100644 --- a/packages/core/src/utils/debug.ts +++ b/packages/core/src/utils/debug.ts @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -type DebugCategory = 'schema' | 'registry' | 'expression' | 'action' | 'plugin' | 'render'; +type DebugCategory = 'schema' | 'registry' | 'expression' | 'action' | 'plugin' | 'render' | 'dashboard'; function isDebugEnabled(): boolean { try { diff --git a/packages/plugin-dashboard/src/DashboardDebugOverlay.tsx b/packages/plugin-dashboard/src/DashboardDebugOverlay.tsx new file mode 100644 index 00000000..96abffed --- /dev/null +++ b/packages/plugin-dashboard/src/DashboardDebugOverlay.tsx @@ -0,0 +1,110 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { useState } from 'react'; +import { cn } from '@object-ui/components'; +import { Bug, ChevronDown, ChevronUp } from 'lucide-react'; + +/** Per-widget diagnostic information surfaced by the debug overlay. */ +export interface WidgetDebugInfo { + id: string; + title?: string; + type?: string; + resolvedType?: string; + hasData: boolean; + isObjectProvider: boolean; + objectName?: string; + hasAggregate: boolean; + dataSnapshot?: unknown; +} + +export interface DashboardDebugOverlayProps { + dashboardTitle?: string; + widgetCount: number; + hasDataSource: boolean; + dataSourceKeys?: string[]; + widgets: WidgetDebugInfo[]; +} + +/** + * Visual debug overlay rendered at the top of a Dashboard when `debug: true`. + * Shows data-chain diagnostics for every widget so developers can quickly + * locate broken dataSource/context injection, missing objectName, or + * absent aggregate configs. + */ +export function DashboardDebugOverlay({ + dashboardTitle, + widgetCount, + hasDataSource, + dataSourceKeys, + widgets, +}: DashboardDebugOverlayProps) { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + + {expanded && ( +
+ {/* Context summary */} +
+ dataSource keys: + {dataSourceKeys && dataSourceKeys.length > 0 ? dataSourceKeys.join(', ') : none / empty} +
+ + {/* Per-widget diagnostics */} + {widgets.length === 0 &&
No widgets rendered yet.
} + {widgets.map((w, i) => ( +
+
+ id: {w.id} + {w.title && title: {w.title}} + type: {w.type ?? '–'} + resolved: {w.resolvedType ?? '–'} + data: {w.hasData ? '✓' : '✗'} + provider:object: {w.isObjectProvider ? '✓' : '✗'} + {w.objectName && objectName: {w.objectName}} + aggregate: {w.hasAggregate ? '✓' : '✗'} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/packages/plugin-dashboard/src/DashboardGridLayout.tsx b/packages/plugin-dashboard/src/DashboardGridLayout.tsx index 044aa33f..26a90006 100644 --- a/packages/plugin-dashboard/src/DashboardGridLayout.tsx +++ b/packages/plugin-dashboard/src/DashboardGridLayout.tsx @@ -3,9 +3,11 @@ import { ResponsiveGridLayout, useContainerWidth, type LayoutItem as RGLLayout, import 'react-grid-layout/css/styles.css'; import { cn, Card, CardHeader, CardTitle, CardContent, Button } from '@object-ui/components'; import { Edit, GripVertical, Save, X, RefreshCw } from 'lucide-react'; -import { SchemaRenderer, useHasDndProvider, useDnd } from '@object-ui/react'; +import { SchemaRenderer, useHasDndProvider, useDnd, SchemaRendererContext } from '@object-ui/react'; +import { debugLog } from '@object-ui/core'; import type { DashboardSchema, DashboardWidgetSchema } from '@object-ui/types'; import { isObjectProvider } from './utils'; +import { DashboardDebugOverlay, type WidgetDebugInfo } from './DashboardDebugOverlay'; /** Bridges editMode transitions to the ObjectUI DnD system when a DndProvider is present. */ function DndEditModeBridge({ editMode }: { editMode: boolean }) { @@ -53,6 +55,10 @@ export const DashboardGridLayout: React.FC = ({ const hasDndProvider = useHasDndProvider(); const intervalRef = React.useRef | null>(null); + // --- Debug mode --- + const schemaCtx = React.useContext(SchemaRendererContext); + const debugMode = schema.debug === true || schemaCtx?.debug === true; + const handleRefresh = React.useCallback(() => { if (!onRefresh) return; setRefreshing(true); @@ -218,8 +224,42 @@ export const DashboardGridLayout: React.FC = ({ }; }, []); + // --- Debug: reset per-render list & log dashboard-level context --- + // --- Debug: pre-compute per-widget diagnostics --- + const widgetDebugInfoList: WidgetDebugInfo[] = []; + if (debugMode) { + debugLog('dashboard', 'DashboardGridLayout render', { + title: schema.title, + widgetCount: schema.widgets?.length ?? 0, + }); + for (const widget of schema.widgets ?? []) { + const widgetData = (widget as any).data || (widget.options as any)?.data; + const info: WidgetDebugInfo = { + id: widget.id || widget.title || '–', + title: widget.title, + type: widget.type, + resolvedType: undefined, + hasData: widgetData != null, + isObjectProvider: isObjectProvider(widgetData), + objectName: widgetData?.object || widget.object, + hasAggregate: !!(widgetData?.aggregate), + dataSnapshot: widgetData, + }; + widgetDebugInfoList.push(info); + debugLog('dashboard', `GridLayout Widget [${info.id}]`, info); + } + } + return (
+ {debugMode && ( + + )} {hasDndProvider && }

{schema.title || 'Dashboard'}

diff --git a/packages/plugin-dashboard/src/DashboardRenderer.tsx b/packages/plugin-dashboard/src/DashboardRenderer.tsx index 7098066b..eda7cab7 100644 --- a/packages/plugin-dashboard/src/DashboardRenderer.tsx +++ b/packages/plugin-dashboard/src/DashboardRenderer.tsx @@ -7,11 +7,13 @@ */ import type { DashboardSchema, DashboardWidgetSchema } from '@object-ui/types'; -import { SchemaRenderer } from '@object-ui/react'; +import { SchemaRenderer, SchemaRendererContext } from '@object-ui/react'; +import { debugLog } from '@object-ui/core'; import { cn, Card, CardHeader, CardTitle, CardContent, Button } from '@object-ui/components'; -import { forwardRef, useState, useEffect, useCallback, useRef } from 'react'; +import { forwardRef, useState, useEffect, useCallback, useRef, useContext } from 'react'; import { RefreshCw } from 'lucide-react'; import { isObjectProvider } from './utils'; +import { DashboardDebugOverlay, type WidgetDebugInfo } from './DashboardDebugOverlay'; // Color palette for charts const CHART_COLORS = [ @@ -48,6 +50,10 @@ export const DashboardRenderer = forwardRef | null>(null); + // --- Debug mode --- + const schemaCtx = useContext(SchemaRendererContext); + const debugMode = schema.debug === true || schemaCtx?.debug === true; + useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth < 768); checkMobile(); @@ -310,6 +316,35 @@ export const DashboardRenderer = forwardRef {recordCountBadge} @@ -326,6 +361,16 @@ export const DashboardRenderer = forwardRef ); + const debugOverlay = debugMode && ( + + ); + if (isMobile) { // Separate metric widgets from other widgets for better mobile layout const metricWidgets = schema.widgets?.filter((w: DashboardWidgetSchema) => w.type === 'metric') || []; @@ -333,6 +378,7 @@ export const DashboardRenderer = forwardRef + {debugOverlay} {headerSection} {refreshButton} @@ -369,6 +415,7 @@ export const DashboardRenderer = forwardRef + {debugOverlay} {headerSection} {refreshButton} {schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index))} diff --git a/packages/plugin-dashboard/src/__tests__/DashboardRenderer.debug.test.tsx b/packages/plugin-dashboard/src/__tests__/DashboardRenderer.debug.test.tsx new file mode 100644 index 00000000..81d16620 --- /dev/null +++ b/packages/plugin-dashboard/src/__tests__/DashboardRenderer.debug.test.tsx @@ -0,0 +1,118 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, fireEvent } from '@testing-library/react'; +import { DashboardRenderer } from '../DashboardRenderer'; + +describe('DashboardRenderer debug mode', () => { + let consoleSpy: ReturnType; + + beforeEach(() => { + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + (globalThis as any).OBJECTUI_DEBUG = true; + }); + + afterEach(() => { + consoleSpy.mockRestore(); + (globalThis as any).OBJECTUI_DEBUG = undefined; + }); + + const makeSchema = (debug: boolean) => + ({ + type: 'dashboard' as const, + name: 'test-debug', + title: 'Debug Dashboard', + debug, + widgets: [ + { + id: 'chart-1', + type: 'bar', + title: 'Revenue Chart', + layout: { x: 0, y: 0, w: 2, h: 2 }, + data: { + provider: 'object', + object: 'opportunity', + aggregate: { function: 'sum', field: 'amount', groupBy: 'stage' }, + }, + options: { xField: 'stage', yField: 'amount' }, + }, + { + id: 'table-1', + type: 'table', + title: 'Orders Table', + layout: { x: 2, y: 0, w: 2, h: 2 }, + data: { + provider: 'value', + items: [{ name: 'Item A', qty: 10 }], + }, + }, + ], + } as any); + + it('should render debug overlay when schema.debug is true', () => { + const { getByTestId } = render(); + expect(getByTestId('dashboard-debug-overlay')).toBeTruthy(); + }); + + it('should NOT render debug overlay when schema.debug is false', () => { + const { queryByTestId } = render(); + expect(queryByTestId('dashboard-debug-overlay')).toBeNull(); + }); + + it('should show widget count and dataSource status in overlay summary', () => { + const { getByTestId } = render(); + const overlay = getByTestId('dashboard-debug-overlay'); + expect(overlay.textContent).toContain('2 widget(s)'); + expect(overlay.textContent).toContain('dataSource'); + }); + + it('should expand to show per-widget diagnostics', () => { + const { getByTestId, queryByTestId } = render( + + ); + // Details not visible by default + expect(queryByTestId('dashboard-debug-details')).toBeNull(); + + // Click toggle to expand + fireEvent.click(getByTestId('dashboard-debug-toggle')); + expect(getByTestId('dashboard-debug-details')).toBeTruthy(); + + // Should show per-widget info + expect(getByTestId('debug-widget-chart-1')).toBeTruthy(); + expect(getByTestId('debug-widget-table-1')).toBeTruthy(); + }); + + it('should identify provider:object widgets correctly', () => { + const { getByTestId } = render(); + fireEvent.click(getByTestId('dashboard-debug-toggle')); + + const chartDebug = getByTestId('debug-widget-chart-1'); + expect(chartDebug.textContent).toContain('provider:object: ✓'); + expect(chartDebug.textContent).toContain('objectName: opportunity'); + expect(chartDebug.textContent).toContain('aggregate: ✓'); + }); + + it('should identify static data widgets correctly', () => { + const { getByTestId } = render(); + fireEvent.click(getByTestId('dashboard-debug-toggle')); + + const tableDebug = getByTestId('debug-widget-table-1'); + expect(tableDebug.textContent).toContain('data: ✓'); + expect(tableDebug.textContent).toContain('provider:object: ✗'); + }); + + it('should emit debugLog calls when OBJECTUI_DEBUG is enabled', () => { + render(); + const dashboardLogs = consoleSpy.mock.calls.filter( + args => typeof args[0] === 'string' && args[0].includes('[dashboard]') + ); + // Should have at least: 1 dashboard render + 2 widget logs + expect(dashboardLogs.length).toBeGreaterThanOrEqual(3); + }); +}); diff --git a/packages/plugin-dashboard/src/index.tsx b/packages/plugin-dashboard/src/index.tsx index e0029ac4..915a56c7 100644 --- a/packages/plugin-dashboard/src/index.tsx +++ b/packages/plugin-dashboard/src/index.tsx @@ -17,6 +17,8 @@ import { WidgetConfigPanel } from './WidgetConfigPanel'; import { DashboardWithConfig } from './DashboardWithConfig'; export { DashboardRenderer, DashboardGridLayout, MetricWidget, MetricCard, PivotTable, DashboardConfigPanel, WidgetConfigPanel, DashboardWithConfig }; +export { DashboardDebugOverlay } from './DashboardDebugOverlay'; +export type { WidgetDebugInfo } from './DashboardDebugOverlay'; // Register dashboard component ComponentRegistry.register( diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index bd54c330..a9dc31cd 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -567,6 +567,11 @@ export interface DashboardSchema extends BaseSchema { widgets: DashboardWidgetSchema[]; /** Auto-refresh interval in seconds. When set, the dashboard will periodically trigger onRefresh. */ refreshInterval?: number; + /** + * Enable debug mode. When true, a diagnostic overlay is rendered and + * detailed data-chain information is logged to the console. + */ + debug?: boolean; /** * Dashboard header configuration. * Aligned with @objectstack/spec DashboardHeaderSchema.