Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
110 changes: 110 additions & 0 deletions packages/plugin-dashboard/src/DashboardDebugOverlay.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={cn(
"col-span-full rounded-lg border text-xs font-mono mb-3",
"bg-yellow-50 border-yellow-300 text-yellow-900",
"dark:bg-yellow-950 dark:border-yellow-700 dark:text-yellow-200"
)}
data-testid="dashboard-debug-overlay"
>
<button
type="button"
className="flex w-full items-center gap-2 px-3 py-2 text-left"
onClick={() => setExpanded(v => !v)}
aria-expanded={expanded}
data-testid="dashboard-debug-toggle"
>
<Bug className="h-4 w-4 shrink-0" />
<span className="font-semibold">Debug</span>
<span className="truncate">
{dashboardTitle ?? 'Dashboard'} — {widgetCount} widget(s) — dataSource: {hasDataSource ? 'present' : '⚠ missing'}
</span>
{expanded ? <ChevronUp className="ml-auto h-4 w-4 shrink-0" /> : <ChevronDown className="ml-auto h-4 w-4 shrink-0" />}
</button>

{expanded && (
<div className="px-3 pb-3 space-y-2" data-testid="dashboard-debug-details">
{/* Context summary */}
<div>
<span className="font-semibold">dataSource keys: </span>
{dataSourceKeys && dataSourceKeys.length > 0 ? dataSourceKeys.join(', ') : <span className="text-red-600 dark:text-red-400">none / empty</span>}
</div>

{/* Per-widget diagnostics */}
{widgets.length === 0 && <div className="italic">No widgets rendered yet.</div>}
{widgets.map((w, i) => (
<div
key={w.id ?? i}
className={cn(
"rounded border px-2 py-1",
w.hasData || w.isObjectProvider
? "border-green-300 bg-green-50 dark:border-green-700 dark:bg-green-950"
: "border-red-300 bg-red-50 dark:border-red-700 dark:bg-red-950"
)}
data-testid={`debug-widget-${w.id}`}
>
<div className="flex flex-wrap gap-x-4 gap-y-0.5">
<span><b>id:</b> {w.id}</span>
{w.title && <span><b>title:</b> {w.title}</span>}
<span><b>type:</b> {w.type ?? '–'}</span>
<span><b>resolved:</b> {w.resolvedType ?? '–'}</span>
<span><b>data:</b> {w.hasData ? '✓' : '✗'}</span>
<span><b>provider:object:</b> {w.isObjectProvider ? '✓' : '✗'}</span>
{w.objectName && <span><b>objectName:</b> {w.objectName}</span>}
<span><b>aggregate:</b> {w.hasAggregate ? '✓' : '✗'}</span>
</div>
</div>
))}
</div>
)}
</div>
);
}
42 changes: 41 additions & 1 deletion packages/plugin-dashboard/src/DashboardGridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -53,6 +55,10 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
const hasDndProvider = useHasDndProvider();
const intervalRef = React.useRef<ReturnType<typeof setInterval> | 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);
Expand Down Expand Up @@ -218,8 +224,42 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
};
}, []);

// --- 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 (
<div ref={containerRef} className={cn("w-full", className)} data-testid="grid-layout">
{debugMode && (
<DashboardDebugOverlay
dashboardTitle={schema.title}
widgetCount={schema.widgets?.length ?? 0}
hasDataSource={false}
widgets={widgetDebugInfoList}
/>
)}
{hasDndProvider && <DndEditModeBridge editMode={editMode} />}
<div className="mb-4 flex items-center justify-between">
<h2 className="text-2xl font-bold">{schema.title || 'Dashboard'}</h2>
Expand Down
51 changes: 49 additions & 2 deletions packages/plugin-dashboard/src/DashboardRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -48,6 +50,10 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
const [isMobile, setIsMobile] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

// --- Debug mode ---
const schemaCtx = useContext(SchemaRendererContext);
const debugMode = schema.debug === true || schemaCtx?.debug === true;

useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
Expand Down Expand Up @@ -310,6 +316,35 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro

const userActionsAttr = userActions ? JSON.stringify(userActions) : undefined;

// --- Debug: reset per-render list & log dashboard-level context ---
// --- Debug: pre-compute per-widget diagnostics ---
const widgetDebugInfoList: WidgetDebugInfo[] = [];
if (debugMode) {
debugLog('dashboard', 'Dashboard render', {
title: schema.title,
widgetCount: schema.widgets?.length ?? 0,
hasDataSource: dataSource != null,
dataSourceType: typeof dataSource,
dataSourceKeys: dataSource && typeof dataSource === 'object' ? Object.keys(dataSource) : undefined,
});
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, // filled after getComponentSchema inside renderWidget
hasData: widgetData != null,
isObjectProvider: isObjectProvider(widgetData),
objectName: widgetData?.object || widget.object,
hasAggregate: !!(widgetData?.aggregate),
dataSnapshot: widgetData,
};
widgetDebugInfoList.push(info);
debugLog('dashboard', `Widget [${info.id}]`, info);
}
}

const refreshButton = onRefresh && (
<div className={cn("flex items-center justify-end gap-3 mb-2", !isMobile && "col-span-full")}>
{recordCountBadge}
Expand All @@ -326,13 +361,24 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
</div>
);

const debugOverlay = debugMode && (
<DashboardDebugOverlay
dashboardTitle={schema.title}
widgetCount={schema.widgets?.length ?? 0}
hasDataSource={dataSource != null}
dataSourceKeys={dataSource && typeof dataSource === 'object' ? Object.keys(dataSource) : undefined}
widgets={widgetDebugInfoList}
/>
);

if (isMobile) {
// Separate metric widgets from other widgets for better mobile layout
const metricWidgets = schema.widgets?.filter((w: DashboardWidgetSchema) => w.type === 'metric') || [];
const otherWidgets = schema.widgets?.filter((w: DashboardWidgetSchema) => w.type !== 'metric') || [];

return (
<div ref={ref} className={cn("flex flex-col gap-4 px-4", className)} data-user-actions={userActionsAttr} onClick={handleBackgroundClick} {...props}>
{debugOverlay}
{headerSection}
{refreshButton}

Expand Down Expand Up @@ -369,6 +415,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
onClick={handleBackgroundClick}
{...props}
>
{debugOverlay}
{headerSection}
{refreshButton}
{schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index))}
Expand Down
Loading