@burdenoff/microfe-bigconsole 2026.710.2 → 2026.710.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"WidgetWrapper.js","names":[],"sources":["../../../../src/bigconsole/components/widgets/WidgetWrapper.tsx"],"sourcesContent":["/**\n * WidgetWrapper Component (v2.1)\n *\n * Common wrapper for all widget types. Handles:\n * - Header with title and menu\n * - Loading, error, and empty states\n * - Selection highlighting\n * - Edit mode controls\n * - Real data only — no mock fallback; widgets show empty state when data is missing\n * - v2.0: Multi-renderer support via WidgetRendererFactory\n * - v2.0: Widget actions support\n */\n\nimport {\n type FC,\n type ReactNode,\n type RefObject,\n useCallback,\n memo,\n useMemo,\n useRef,\n useState,\n useEffect,\n lazy,\n Suspense,\n} from 'react';\nimport type { Widget } from '../../types';\nimport {\n useWidgetStore,\n useDashboardStore,\n useIsWidgetSelected,\n useWidgetUIState,\n useGlobalFilterValues,\n} from '../../store';\nimport {\n useWidgetAutoRefresh,\n formatTimeUntilRefresh,\n useWidgetExport,\n useDrilldown,\n useWidgetData,\n useWidgetComments,\n type DrilldownContext,\n} from '../../hooks';\nimport { useApolloClient } from '@apollo/client/react';\nimport { GetDashboardDocument } from '../../../generated/wspace-operations';\nimport { getWidgetDefinition } from './WidgetRegistry';\nimport { WidgetHeader } from './WidgetHeader';\nimport { WidgetLoadingState } from './states/LoadingState';\nimport { WidgetErrorState } from './states/ErrorState';\nimport { WidgetEmptyState } from './states/EmptyState';\nimport { WidgetErrorBoundary } from './WidgetErrorBoundary';\nimport { hasRenderableCustomConfig, hasCustomIframeConfig } from './custom/config';\n\n// v2.0: Renderer factory and actions\nimport { WidgetRendererFactory } from '../../renderers/factory/WidgetRendererFactory';\nimport { WidgetActions } from './widget-actions';\nimport type { WidgetAction, ActionContext, ActionResult, RendererType } from '../../renderers/types/renderer';\n// Widget components - lazy loaded for better code splitting\n// Each widget loads only when its type is rendered\nconst MetricCardWidget = lazy(() => import('./metric-card').then((m) => ({ default: m.MetricCardWidget })));\nconst ChartWidget = lazy(() => import('./chart').then((m) => ({ default: m.ChartWidget })));\nconst TableWidget = lazy(() => import('./table').then((m) => ({ default: m.TableWidget })));\nconst PivotTableWidget = lazy(() => import('./pivot-table').then((m) => ({ default: m.PivotTableWidget })));\nconst GaugeWidget = lazy(() => import('./gauge').then((m) => ({ default: m.GaugeWidget })));\nconst ProgressWidget = lazy(() => import('./progress').then((m) => ({ default: m.ProgressWidget })));\nconst ListWidget = lazy(() => import('./list').then((m) => ({ default: m.ListWidget })));\nconst HeatmapWidget = lazy(() => import('./heatmap').then((m) => ({ default: m.HeatmapWidget })));\nconst RetentionWidget = lazy(() => import('./retention').then((m) => ({ default: m.RetentionWidget })));\nconst CalendarWidget = lazy(() => import('./calendar').then((m) => ({ default: m.CalendarWidget })));\nconst KanbanWidget = lazy(() => import('./kanban').then((m) => ({ default: m.KanbanWidget })));\nconst TimelineWidget = lazy(() => import('./timeline').then((m) => ({ default: m.TimelineWidget })));\nconst KPIComparisonWidget = lazy(() => import('./kpi-comparison').then((m) => ({ default: m.KPIComparisonWidget })));\nconst TextWidget = lazy(() => import('./text-widget').then((m) => ({ default: m.TextWidget })));\nconst MapWidget = lazy(() => import('./map-widget').then((m) => ({ default: m.MapWidget })));\nconst IframeWidget = lazy(() => import('./iframe-widget').then((m) => ({ default: m.IframeWidget })));\nconst CustomWidget = lazy(() => import('./custom').then((m) => ({ default: m.CustomWidget })));\n// AdaptiveCardWidget is HUGE (984KB) - lazy load only when 'form' type is used\nconst AdaptiveCardWidget = lazy(() => import('./adaptive-card').then((m) => ({ default: m.AdaptiveCardWidget })));\n// Comments panel - lazy loaded; only mounts when the user opens comments\nconst WidgetComments = lazy(() => import('./comments/WidgetComments').then((m) => ({ default: m.WidgetComments })));\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WidgetWrapperProps {\n /** The widget data */\n widget: Widget;\n /** Widget content to render */\n children?: ReactNode;\n /** Data for the widget */\n data?: Record<string, unknown> | null;\n /** Custom class name */\n className?: string;\n /** Whether the widget is loading */\n isLoading?: boolean;\n /** Error message */\n error?: string | null;\n /** Callback to refresh widget data */\n onRefresh?: () => Promise<void>;\n /** Callback for drilldown navigation */\n onDrilldown?: (params: Record<string, unknown>) => void;\n /** v2.0: Widget actions */\n actions?: WidgetAction[];\n /** v2.0: Callback for action events */\n onAction?: (action: WidgetAction, context: ActionContext) => Promise<ActionResult | void>;\n /** Phase 2: Global filter values to apply to widget data */\n filterValues?: Record<string, unknown>;\n}\n\n// ============================================================================\n// Custom comparison for memo - ensures actions changes trigger re-render\n// ============================================================================\n\nfunction arePropsEqual(prevProps: WidgetWrapperProps, nextProps: WidgetWrapperProps): boolean {\n // Always re-render if widget ID changes\n if (prevProps.widget.id !== nextProps.widget.id) return false;\n // Re-render if widget was updated\n if (prevProps.widget.updatedAt !== nextProps.widget.updatedAt) return false;\n // Re-render if loading state changes\n if (prevProps.isLoading !== nextProps.isLoading) return false;\n // Re-render if error changes\n if (prevProps.error !== nextProps.error) return false;\n // Re-render if actions count changes\n if ((prevProps.actions?.length ?? 0) !== (nextProps.actions?.length ?? 0)) return false;\n // Check if any action ID changed\n if (prevProps.actions && nextProps.actions) {\n for (let i = 0; i < prevProps.actions.length; i++) {\n if (prevProps.actions[i]?.id !== nextProps.actions[i]?.id) return false;\n }\n }\n // Also check data changes\n if (prevProps.data !== nextProps.data) return false;\n // Phase 2: Re-render if filter values changed\n if (JSON.stringify(prevProps.filterValues) !== JSON.stringify(nextProps.filterValues)) return false;\n return true;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const WidgetWrapper: FC<WidgetWrapperProps> = memo(function WidgetWrapper({\n widget,\n children,\n data,\n className = '',\n isLoading: isLoadingProp,\n error: errorProp,\n onRefresh,\n onDrilldown,\n actions,\n onAction,\n filterValues,\n}) {\n // Store state - use Dashboard store for edit mode (synced with toolbar)\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const isSelected = useIsWidgetSelected(widget.id);\n const uiState = useWidgetUIState(widget.id);\n const globalFilterValues = useGlobalFilterValues();\n const apolloClient = useApolloClient();\n\n // Map a global-filter's id -> the data field it targets (`config.field`).\n // Global CATEGORY/TEXT filter values are keyed in the store by filter id (a\n // UUID), not by the data column they filter. Without this map, applyClientFilters\n // can only fall back to value/heuristic matching, which fails to narrow rows\n // whose targeted column (e.g. `country`, `cohort`) isn't in the hardcoded\n // heuristic list — so a selected Country/Cohort would not actually filter the\n // widget. We resolve the targeted field from the dashboard's globalFilters\n // (already in the Apollo cache from the page load) so a UUID-keyed filter\n // matches the row's configured field exactly.\n const widgetDashboardId = (widget as unknown as Record<string, unknown>).dashboardId as string | undefined;\n const filterFieldMap = useMemo<Record<string, string>>(() => {\n if (!widgetDashboardId) return {};\n try {\n const cached = apolloClient.readQuery<{\n getDashboard?: {\n globalFilters?: Array<{ id: string; config?: Record<string, unknown> | null }>;\n };\n }>({ query: GetDashboardDocument, variables: { id: widgetDashboardId } });\n const map: Record<string, string> = {};\n for (const f of cached?.getDashboard?.globalFilters ?? []) {\n const field = f.config?.field;\n if (typeof field === 'string' && field) map[f.id] = field;\n }\n return map;\n } catch {\n return {};\n }\n }, [apolloClient, widgetDashboardId]);\n\n // Fetch per-widget data from backend (parser-processed data)\n // This is needed because listWidgetsByDashboard uses WidgetListFields (without data)\n // for performance, so each widget fetches its own data individually\n const hasDataSource =\n !!(widget as unknown as Record<string, unknown>).dataSinkId ||\n !!(widget as unknown as Record<string, unknown>).datasetId ||\n !!(widget as unknown as Record<string, unknown>).parserId;\n const { data: fetchedWidgetData, loading: widgetDataLoading } = useWidgetData(widget.id, {\n skip: !hasDataSource,\n pollInterval: (widget.refreshInterval ?? 0) * 1000 || 0,\n });\n\n // WORKAROUND: If Zustand store is empty, directly read filter_* params from URL\n // This handles race conditions where the store hasn't been populated yet\n const urlFilterValues = useMemo(() => {\n if (typeof window === 'undefined') return {};\n\n const params = new URLSearchParams(window.location.search);\n const filters: Record<string, unknown> = {};\n\n params.forEach((value, key) => {\n if (key.startsWith('filter_')) {\n const filterId = key.slice('filter_'.length);\n // Skip internal metadata keys\n if (!filterId.startsWith('__')) {\n filters[filterId] = value;\n }\n }\n });\n\n return filters;\n }, []);\n\n // Phase 2: Merge global filter values (from URL) with explicit filterValues prop\n // This ensures both URL params (filter_*) and drilldown params work together\n const mergedFilterValues = useMemo(() => {\n const merged: Record<string, unknown> = {};\n\n // First, add URL filter values as fallback (in case Zustand store hasn't loaded yet)\n if (urlFilterValues && Object.keys(urlFilterValues).length > 0) {\n Object.assign(merged, urlFilterValues);\n }\n\n // Then, add global filter values from Zustand store (if populated)\n if (globalFilterValues && Object.keys(globalFilterValues).length > 0) {\n Object.assign(merged, globalFilterValues);\n }\n\n // Finally, add/override with explicit filterValues prop (e.g., from drilldown modal)\n if (filterValues && Object.keys(filterValues).length > 0) {\n Object.assign(merged, filterValues);\n }\n\n return merged;\n }, [urlFilterValues, globalFilterValues, filterValues]);\n\n // Track active filters (client-side filtering until backend supports it)\n const hasFilters = Object.keys(mergedFilterValues).length > 0;\n\n // Merge loading/error from props and store\n // Show loading if widget data is being fetched for the first time (no data yet)\n const isLoading = isLoadingProp ?? (widgetDataLoading && !fetchedWidgetData) ?? uiState.isLoading;\n const error = errorProp ?? uiState.errorMessage;\n\n // Get widget definition\n const definition = getWidgetDefinition(widget.type);\n\n // Stable refresh callback - use ref to avoid creating new function on each render\n const onRefreshRef = useRef(onRefresh);\n useEffect(() => {\n onRefreshRef.current = onRefresh;\n }, [onRefresh]);\n\n const stableOnRefresh = useCallback(async () => {\n if (onRefreshRef.current) {\n await onRefreshRef.current();\n }\n }, []);\n\n // Auto-refresh\n const { timeUntilRefresh, isPaused } = useWidgetAutoRefresh({\n widgetId: widget.id,\n refreshInterval: widget.refreshInterval,\n onRefresh: stableOnRefresh,\n enabled: !isEditMode && !!widget.refreshInterval,\n });\n\n // Container ref for PNG export\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Comments panel. The count query stays disabled until the panel is first\n // opened, so a dashboard of N widgets doesn't fire N comment queries on load.\n const [showComments, setShowComments] = useState(false);\n const [commentsEverOpened, setCommentsEverOpened] = useState(false);\n const { count: commentCount } = useWidgetComments(widget.id, commentsEverOpened);\n const toggleComments = useCallback(() => {\n setShowComments((open) => {\n const next = !open;\n if (next) setCommentsEverOpened(true);\n return next;\n });\n }, []);\n\n // Get widget data for export (computed early for hook)\n const exportData = useMemo(() => {\n if (data && Array.isArray(data)) return data;\n const rawData = (widget as unknown as Record<string, unknown>).data as Record<string, unknown> | undefined;\n if (rawData?.processed && Array.isArray(rawData.processed)) {\n return rawData.processed;\n }\n if (rawData?.raw) {\n const parsed = typeof rawData.raw === 'string' ? JSON.parse(rawData.raw) : rawData.raw;\n if (Array.isArray(parsed)) return parsed;\n }\n return null;\n }, [data, widget]);\n\n // Export functionality\n const { exportCSV, exportPNG } = useWidgetExport(widget, {\n data: exportData,\n containerRef: containerRef as unknown as RefObject<HTMLElement>,\n });\n\n // Helper to check if data is actually usable (has real content)\n const isUsableData = useCallback((d: unknown): boolean => {\n if (d === null || d === undefined) return false;\n if (Array.isArray(d)) return d.length > 0;\n if (typeof d === 'object') {\n const obj = d as Record<string, unknown>;\n const keys = Object.keys(obj);\n if (keys.length === 0) return false;\n // Check recursively for actual data content\n return keys.some((key) => {\n const val = obj[key];\n if (val === null || val === undefined || val === '') return false;\n if (Array.isArray(val)) return val.length > 0;\n if (typeof val === 'object') return isUsableData(val);\n return true;\n });\n }\n return true;\n }, []);\n\n // Phase 2: Client-side filter function for widget data\n const applyClientFilters = useCallback(\n (\n widgetData: Record<string, unknown> | Record<string, unknown>[]\n ): Record<string, unknown> | Record<string, unknown>[] => {\n if (!hasFilters || !widgetData) return widgetData;\n\n // Separate drilldown context filters (ctx_*) from regular filters\n const contextFilters: Record<string, unknown> = {};\n const regularFilters: Record<string, unknown> = {};\n\n Object.entries(mergedFilterValues || {}).forEach(([key, value]) => {\n if (key.startsWith('ctx_')) {\n // Strip ctx_ prefix to get the target field name\n const fieldName = key.slice(4); // Remove 'ctx_'\n contextFilters[fieldName] = value;\n } else if (!key.startsWith('__')) {\n // Skip internal metadata keys (__drilldown_depth, etc.)\n regularFilters[key] = value;\n }\n });\n\n // Helper to match a value against a filter (case-insensitive)\n const matchesFilter = (itemValue: unknown, filterValue: unknown): boolean => {\n if (filterValue === undefined || filterValue === null || filterValue === '') return true;\n if (itemValue === undefined || itemValue === null) return false;\n\n const itemStr = String(itemValue).toLowerCase();\n const filterStr = String(filterValue).toLowerCase();\n\n // Exact match or contains\n return itemStr === filterStr || itemStr.includes(filterStr);\n };\n\n // Helper to find a matching field in the item (case-insensitive key matching)\n const findFieldValue = (item: Record<string, unknown>, targetField: string): unknown => {\n const targetLower = targetField.toLowerCase();\n\n // Try exact match first\n if (item[targetField] !== undefined) return item[targetField];\n\n // Try case-insensitive match\n const matchingKey = Object.keys(item).find((k) => k.toLowerCase() === targetLower);\n if (matchingKey) return item[matchingKey];\n\n // Try partial match (e.g., 'region' matches 'regionName' or 'user_region')\n const partialMatch = Object.keys(item).find(\n (k) => k.toLowerCase().includes(targetLower) || targetLower.includes(k.toLowerCase())\n );\n if (partialMatch) return item[partialMatch];\n\n // Try stripping common prefixes from target field (e.g., 'kpiId' -> 'id', 'rowName' -> 'name')\n // This handles drilldown context where widget passes 'kpiId' but data has 'id'\n const commonPrefixes = ['kpi', 'row', 'item', 'data', 'cell', 'record', 'entry'];\n for (const prefix of commonPrefixes) {\n if (targetLower.startsWith(prefix) && targetLower.length > prefix.length) {\n const strippedField = targetLower.slice(prefix.length);\n // Try exact match with stripped field\n const strippedMatch = Object.keys(item).find((k) => k.toLowerCase() === strippedField);\n if (strippedMatch) return item[strippedMatch];\n }\n }\n\n return undefined;\n };\n\n // Helper function to filter an array of items\n const filterItems = (items: Record<string, unknown>[]): Record<string, unknown>[] => {\n return items.filter((item: Record<string, unknown>) => {\n // Only object rows are field-filterable. An array of primitives (e.g.\n // a heatmap's `rows: [\"5W-MedSurg\", …]`) carries no named fields — keep\n // every element. Without this, `findFieldValue` indexes into the string\n // by character offset and spuriously \"matches\" a filter, dropping rows.\n if (item === null || typeof item !== 'object' || Array.isArray(item)) {\n return true;\n }\n // First, apply drilldown context filters (ctx_*) - these have explicit field names\n for (const [fieldName, filterValue] of Object.entries(contextFilters)) {\n if (filterValue === undefined || filterValue === null || filterValue === '') continue;\n\n const itemValue = findFieldValue(item, fieldName);\n // If the field doesn't exist in this data item, skip this context filter.\n // Drilldown context params (e.g., source=support_status) are metadata about\n // the drilldown origin and should NOT filter out data when the target\n // dashboard's widgets don't have a matching field.\n if (itemValue === undefined) continue;\n if (!matchesFilter(itemValue, filterValue)) {\n return false;\n }\n }\n\n // Then, apply regular filters\n for (const [filterKey, filterValue] of Object.entries(regularFilters)) {\n if (filterValue === undefined || filterValue === null || filterValue === '') continue;\n\n // For date range filters\n if (typeof filterValue === 'object') {\n const fv = filterValue as Record<string, unknown>;\n if (fv.start || fv.end) {\n const dateFields = Object.entries(item).filter(\n ([k, _v]) => k.toLowerCase().includes('date') || k.toLowerCase().includes('time')\n );\n if (dateFields.length > 0) {\n const itemDate = new Date(dateFields[0][1] as string);\n if (fv.start && itemDate < new Date(fv.start as string)) return false;\n if (fv.end && itemDate > new Date(fv.end as string)) return false;\n }\n continue;\n }\n }\n\n // For numeric range filters\n if (typeof filterValue === 'object') {\n const nv = filterValue as Record<string, unknown>;\n if (nv.min !== undefined || nv.max !== undefined) {\n const numericFields = Object.entries(item).filter(([_k, v]) => typeof v === 'number');\n if (numericFields.length > 0) {\n const itemNum = numericFields[0][1] as number;\n if (nv.min !== undefined && itemNum < (nv.min as number)) return false;\n if (nv.max !== undefined && itemNum > (nv.max as number)) return false;\n }\n continue;\n }\n }\n\n // Any other object-shaped filter value (e.g. a DATE_RANGE filter set\n // to a preset like { preset: \"last_30_days\" } with no resolved\n // start/end, or a structured value we don't understand) cannot be\n // compared with the scalar matchers below. Passing it through would\n // stringify it to \"[object Object]\" and, via fuzzy field matching\n // (e.g. an `enrolledAt` filter partial-matching an `enrolled` number\n // column), spuriously exclude EVERY row. Treat such values as a no-op\n // for client-side filtering — the row set is unchanged until the\n // filter resolves to concrete start/end or scalar values.\n if (filterValue !== null && typeof filterValue === 'object' && !Array.isArray(filterValue)) {\n continue;\n }\n\n // Global CATEGORY/TEXT filters are keyed by filter id (UUID). Resolve\n // the data column they target from the filter's config.field. When the\n // widget's rows carry that column, the filter is authoritative — a\n // mismatch excludes the row; a missing column is a no-op (the filter\n // simply doesn't apply to this widget's shape). This is what makes a\n // selected Country/Cohort actually narrow the visible data.\n const targetField = filterFieldMap[filterKey];\n if (targetField) {\n const mappedValue = findFieldValue(item, targetField);\n if (mappedValue !== undefined) {\n if (!matchesFilter(mappedValue, filterValue)) {\n return false;\n }\n continue;\n }\n // Configured field absent from this widget's rows → filter is a\n // no-op for this widget (do not fall through to value heuristics,\n // which could spuriously match an unrelated column).\n continue;\n }\n\n // Try to match by filter key name first (for named filters). When\n // the widget's data actually carries the filter's field, the filter\n // is authoritative — a mismatch excludes the row.\n const fieldValue = findFieldValue(item, filterKey);\n if (fieldValue !== undefined) {\n if (!matchesFilter(fieldValue, filterValue)) {\n return false;\n }\n continue;\n }\n\n // For text/category filters - try multiple matching strategies\n const filterStr = String(filterValue).toLowerCase();\n\n // Strategy 1: Check all field values for exact or partial match\n const itemValues = Object.values(item).map((v) => String(v).toLowerCase());\n const hasMatch = itemValues.some((v) => v === filterStr || v.includes(filterStr));\n\n if (!hasMatch) {\n // Strategy 2: Try to find a field name that matches the filter value context\n const fieldNames = Object.keys(item).map((k) => k.toLowerCase());\n const possibleFields = fieldNames.filter(\n (f) =>\n f.includes('category') ||\n f.includes('type') ||\n f.includes('status') ||\n f.includes('name') ||\n f.includes('region') ||\n f.includes('group')\n );\n\n let fieldMatch = false;\n for (const fieldName of possibleFields) {\n const originalKey = Object.keys(item).find((k) => k.toLowerCase() === fieldName);\n if (originalKey) {\n const itemFieldValue = String(item[originalKey]).toLowerCase();\n if (itemFieldValue === filterStr || itemFieldValue.includes(filterStr)) {\n fieldMatch = true;\n break;\n }\n }\n }\n\n // The filter's field is NOT present in this dataset (named-field\n // lookup failed AND no value/heuristic match). A global filter that\n // doesn't apply to a widget's shape must be a no-op for that widget\n // — not a silent \"exclude every row\". This mirrors the ctx_* rule\n // above (skip when the targeted field is absent). Without this, an\n // always-active required filter (e.g. campus=main) blanks every\n // parser-derived widget whose output has no campus column.\n if (!fieldMatch) {\n continue;\n }\n }\n }\n return true;\n });\n };\n\n // If data is an array, filter the items directly\n if (Array.isArray(widgetData)) {\n const filtered = filterItems(widgetData);\n return filtered;\n }\n\n // Handle object with array properties (rows, items, members, questions, data, etc.)\n if (widgetData && typeof widgetData === 'object') {\n // Common array property names in widget data\n const arrayPropertyNames = [\n 'kpis', // KPI Comparison widget\n 'rows', // Table widget\n 'items', // List widget\n 'members', // Team/member widgets\n 'questions', // Form widget\n 'data', // Generic data\n 'records', // Database-style data\n 'results', // Search/query results\n 'entries', // Log/feed entries\n 'list', // Generic list\n 'values', // Value arrays\n 'series', // Chart series\n 'datasets', // Chart datasets\n ];\n\n for (const propName of arrayPropertyNames) {\n if (widgetData[propName] && Array.isArray(widgetData[propName])) {\n const filteredArray = filterItems(widgetData[propName]);\n return {\n ...widgetData,\n [propName]: filteredArray,\n ...(propName === 'rows' ? { totalRows: filteredArray.length } : {}),\n };\n }\n }\n\n // If no known array property found, check for any array property\n const arrayProps = Object.entries(widgetData).filter(([_key, v]) => Array.isArray(v));\n if (arrayProps.length > 0) {\n const [propName, propValue] = arrayProps[0];\n const filteredArray = filterItems(propValue as Record<string, unknown>[]);\n return {\n ...widgetData,\n [propName]: filteredArray,\n };\n }\n }\n\n // For non-filterable data, return as-is\n return widgetData;\n },\n [hasFilters, mergedFilterValues, widget.id, filterFieldMap]\n );\n\n // Get widget data from the widget itself (fetched from backend)\n const realWidgetData = useMemo(() => {\n let rawWidgetData = null;\n\n // Priority 1: Explicitly passed data prop\n if (data && isUsableData(data)) {\n rawWidgetData = data;\n }\n // Priority 2: Per-widget fetched data (from useWidgetData hook)\n // fetchedWidgetData IS a WidgetData object: { raw, processed, fetchedAt, cached }\n else if (fetchedWidgetData) {\n const fetched = fetchedWidgetData as unknown as Record<string, unknown>;\n if (fetched?.processed && isUsableData(fetched.processed)) {\n rawWidgetData = fetched.processed;\n } else if (fetched?.raw) {\n try {\n const parsed = typeof fetched.raw === 'string' ? JSON.parse(fetched.raw as string) : fetched.raw;\n if (isUsableData(parsed)) rawWidgetData = parsed;\n } catch {\n // Invalid JSON, ignore\n }\n }\n }\n // Priority 3: widget.data from GraphQL response (if already on widget object)\n else {\n const rawData = (widget as unknown as Record<string, unknown>).data as Record<string, unknown> | undefined;\n if (rawData?.processed && isUsableData(rawData.processed)) {\n rawWidgetData = rawData.processed;\n } else if (rawData?.raw) {\n try {\n const parsed = typeof rawData.raw === 'string' ? JSON.parse(rawData.raw) : rawData.raw;\n if (isUsableData(parsed)) rawWidgetData = parsed;\n } catch {\n // Invalid JSON, ignore\n }\n }\n }\n\n // Priority 4: config.mockData — user-configured sample JSON from Widget Properties Data tab\n // This is NOT auto-generated mock data; it's explicit JSON the user pasted in the UI\n if (!rawWidgetData) {\n const configMockData = (widget.config as Record<string, unknown> | undefined)?.mockData;\n if (configMockData) {\n try {\n const parsed = typeof configMockData === 'string' ? JSON.parse(configMockData) : configMockData;\n if (isUsableData(parsed)) {\n rawWidgetData = parsed;\n }\n } catch (e) {}\n }\n }\n\n // Generic parser-output resolution: a widget may bind to a parser whose\n // output is an envelope ({ byUnit:[...], house:{...} }) rather than the\n // exact array/object the renderer wants. config.dataKey / dataPath /\n // seriesPath is a dot-path INTO the resolved data that points at the\n // renderable subtree (e.g. \"byUnit\" for a table, \"rows\" for a chart).\n // Resolving it here keeps every renderer generic — the use-case-specific\n // shape knowledge lives in the widget config (set by the seed/UI), not in\n // the renderer. valueField (single-value metric path) is intentionally NOT\n // applied here: MetricCardWidget resolves it itself against the envelope.\n if (rawWidgetData && typeof rawWidgetData === 'object' && !Array.isArray(rawWidgetData)) {\n const cfg = widget.config as Record<string, unknown> | undefined;\n const pathKey = (cfg?.dataKey ?? cfg?.dataPath ?? cfg?.seriesPath) as string | undefined;\n if (pathKey && typeof pathKey === 'string') {\n const resolved = pathKey.split('.').reduce<unknown>((cur, seg) => {\n return cur && typeof cur === 'object' ? (cur as Record<string, unknown>)[seg] : undefined;\n }, rawWidgetData);\n if (resolved !== undefined && resolved !== null && isUsableData(resolved)) {\n rawWidgetData = resolved as Record<string, unknown>;\n }\n }\n }\n\n // Phase 2: Apply client-side filters if active\n if (hasFilters && rawWidgetData) {\n const filtered = applyClientFilters(rawWidgetData);\n return filtered;\n }\n\n return rawWidgetData;\n }, [data, widget, fetchedWidgetData, hasFilters, applyClientFilters, mergedFilterValues, isUsableData]);\n\n // Final widget data - real data only, no mock fallback\n const widgetData = useMemo(() => {\n return realWidgetData ?? null;\n }, [realWidgetData]);\n\n // Drilldown navigation (must be after widgetData is defined)\n const { executeDrilldown, canDrilldown } = useDrilldown({\n onDrilldown: (params, _mode) => {\n onDrilldown?.(params);\n },\n });\n\n // Check if widget has drilldown configured\n const hasDrilldown = useMemo(() => canDrilldown(widget), [widget, canDrilldown]);\n\n // Handle drilldown execution with context\n const handleDrilldown = useCallback(\n (selectedData?: Record<string, unknown>) => {\n if (!widget.drilldown?.enabled) return;\n\n const context: DrilldownContext = {\n selectedData: selectedData || {},\n globalFilters: globalFilterValues || {},\n widgetData: widgetData || {},\n metadata: {\n widgetId: widget.id,\n widgetType: widget.type,\n widgetTitle: widget.title,\n },\n sourceDashboardId: widget.dashboardId,\n };\n\n executeDrilldown(widget.drilldown, context);\n },\n [widget, widgetData, globalFilterValues, executeDrilldown]\n );\n\n // Track mouse position to distinguish click from drag\n const mouseDownPos = useRef<{ x: number; y: number } | null>(null);\n const DRAG_THRESHOLD = 5; // pixels - if mouse moves more than this, it's a drag\n\n // Handle mouse down - record position\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n mouseDownPos.current = { x: e.clientX, y: e.clientY };\n }, []);\n\n // Handle widget click - Edit mode only (View mode drilldown is triggered by data clicks)\n // Only trigger if mouse hasn't moved much (not a drag)\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation(); // Prevent DashboardGrid from also handling\n\n // Check if this was a drag (mouse moved significantly)\n if (mouseDownPos.current) {\n const dx = Math.abs(e.clientX - mouseDownPos.current.x);\n const dy = Math.abs(e.clientY - mouseDownPos.current.y);\n if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {\n // This was a drag, not a click - don't open config panel\n mouseDownPos.current = null;\n return;\n }\n }\n mouseDownPos.current = null;\n\n if (isEditMode) {\n // Edit mode: select widget for editing\n selectWidget(widget.id);\n }\n // View mode: drilldown is triggered by clicking specific data elements\n // (table rows, chart points, etc.) - not by clicking the widget itself\n },\n [isEditMode, selectWidget, widget.id]\n );\n\n // Handle keyboard navigation (Edit mode only)\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n if (isEditMode) {\n selectWidget(widget.id);\n }\n }\n },\n [isEditMode, selectWidget, widget.id]\n );\n\n const hasRenderableCustomWidgetConfig = useMemo(\n () => widget.type === 'custom' && hasRenderableCustomConfig(widget.config),\n [widget.type, widget.config]\n );\n\n const customWidgetHasIframeConfig = useMemo(\n () => widget.type === 'custom' && hasCustomIframeConfig(widget.config),\n [widget.type, widget.config]\n );\n\n const showContainerDrilldownHints = hasDrilldown && !customWidgetHasIframeConfig;\n\n // Determine if data is empty\n const isEmpty = !isLoading && !error && !widgetData && !hasRenderableCustomWidgetConfig;\n\n // v2.0: Check if widget has a renderer specified (non-BIGCONSOLE)\n const widgetRenderer = useMemo(() => {\n const widgetWithRenderer = widget as Widget & { renderer?: RendererType };\n return widgetWithRenderer.renderer;\n }, [widget]);\n\n // v2.0: Determine if we should use the factory (non-BIGCONSOLE renderer)\n const useRendererFactory = useMemo(() => {\n const shouldUse = widgetRenderer && widgetRenderer !== 'BIGCONSOLE';\n return shouldUse;\n }, [widgetRenderer]);\n\n // v2.0: Create action context\n const actionContext = useMemo<ActionContext>(\n () => ({\n widgetId: widget.id,\n data: widgetData || {},\n }),\n [widget.id, widgetData]\n );\n\n // v2.0: Handle action callback from WidgetRendererFactory\n // Receives (action: WidgetAction, context: ActionContext) and forwards to parent\n // Checks if a more detailed backend action exists with same name\n const handleActionFromFactory = useCallback(\n (action: WidgetAction, context: ActionContext) => {\n if (!onAction) return;\n\n // Try to find a matching backend action by name (has more details like confirmation)\n const existingAction = actions?.find((a) => a.name === action.name);\n\n if (existingAction) {\n // Use existing backend action (has confirmation, style, icon, etc.)\n onAction(existingAction, context);\n } else {\n // Use the action from WidgetRendererFactory as-is\n onAction(action, context);\n }\n },\n [onAction, actions]\n );\n\n // Render the appropriate widget component based on type (v1.0 snake_case)\n const renderWidgetComponent = useCallback(() => {\n // Extract data based on widget type expectations\n // Different widgets expect different data formats\n const getDataForWidget = () => {\n if (!widgetData) return widgetData;\n\n // If data has 'rows' property (table data), extract rows\n if (widget.type === 'table' || widget.type === 'pivot_table') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'rows' in widgetData) {\n return (widgetData as { rows: unknown[] }).rows;\n }\n }\n\n // If data has 'items' property (list data), extract items\n if (widget.type === 'list') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'items' in widgetData) {\n return (widgetData as { items: unknown[] }).items;\n }\n }\n\n // If data has 'stages' property (old funnel format), extract stages\n if (widget.type === 'funnel_chart') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'stages' in widgetData) {\n return (widgetData as { stages: unknown[] }).stages;\n }\n }\n\n // If data has 'datasets' property (old chart format), convert to array format\n if (widget.type === 'chart') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'datasets' in widgetData) {\n // Convert old format to new array format\n const oldData = widgetData as { labels?: string[]; datasets?: { name: string; data: number[] }[] };\n if (oldData.labels && oldData.datasets) {\n return oldData.labels.map((label, i) => {\n const point: Record<string, unknown> = { name: label };\n oldData.datasets?.forEach((ds) => {\n point[ds.name.toLowerCase()] = ds.data[i];\n });\n return point;\n });\n }\n }\n }\n\n // For text widgets: convert parsed object data to {content, format} shape\n if (widget.type === 'text') {\n if (widgetData && typeof widgetData === 'object' && !('content' in (widgetData as Record<string, unknown>))) {\n // Auto-format parsed data as markdown content\n const obj = widgetData as Record<string, unknown>;\n const lines: string[] = [];\n for (const [key, val] of Object.entries(obj)) {\n if (val && typeof val === 'object' && !Array.isArray(val)) {\n lines.push(`### ${key}`);\n for (const [k2, v2] of Object.entries(val as Record<string, unknown>)) {\n lines.push(`- **${k2}:** ${v2}`);\n }\n } else if (Array.isArray(val)) {\n lines.push(`### ${key}`);\n val.slice(0, 10).forEach((item: unknown) => {\n if (item && typeof item === 'object') {\n const entries = Object.entries(item as Record<string, unknown>);\n lines.push(`- ${entries.map(([k, v]) => `**${k}:** ${v}`).join(' | ')}`);\n } else {\n lines.push(`- ${item}`);\n }\n });\n } else {\n lines.push(`- **${key}:** ${val}`);\n }\n }\n return { content: lines.join('\\n'), format: 'markdown' };\n }\n }\n\n // For custom widgets: pass data through with extracted value if configured\n if (widget.type === 'custom') {\n const config = widget.config as Record<string, unknown> | undefined;\n const valueField = config?.valueField as string | undefined;\n if (valueField && widgetData && typeof widgetData === 'object' && !Array.isArray(widgetData)) {\n const obj = widgetData as Record<string, unknown>;\n if (valueField in obj) {\n return { value: obj[valueField], label: widget.title, data: obj };\n }\n }\n }\n\n return widgetData;\n };\n\n const normalizedData = getDataForWidget();\n const props = {\n widget,\n data: normalizedData,\n onDrilldown: hasDrilldown ? handleDrilldown : undefined,\n };\n\n // v2.0: Use WidgetRendererFactory for non-BIGCONSOLE renderers\n if (useRendererFactory) {\n // For factory renderers (Adaptive Card, BlockKit, Custom), pass the RAW\n // processed data — NOT the type-normalized data. Normalization (e.g., text→markdown)\n // is only for BIGCONSOLE renderer widgets. Factory renderers handle their own\n // template interpolation against the raw data object.\n let factoryData: Record<string, unknown> = {};\n\n if (\n realWidgetData &&\n typeof realWidgetData === 'object' &&\n !Array.isArray(realWidgetData) &&\n Object.keys(realWidgetData as Record<string, unknown>).length > 0\n ) {\n // Use real data from backend (raw processed, not type-normalized)\n factoryData = { ...(realWidgetData as Record<string, unknown>) };\n }\n\n // For CUSTOM renderer: enrich data with standard fields expected by built-in components.\n // Built-in components (Counter, Card, SimpleText) read specific fields from data:\n // Counter: data.value, Card: data.title/data.description, SimpleText: data.text\n // Parsed data from datasinks has domain-specific fields (totalFiles, totalMessages, etc.)\n // Use config.valueField to extract the primary value and map it to standard fields.\n const internalType = (widget.config as Record<string, unknown> | undefined)?._internalWidgetType as\n | string\n | undefined;\n if (internalType === 'custom' && Object.keys(factoryData).length > 0) {\n const config = widget.config as Record<string, unknown> | undefined;\n const valueField = config?.valueField as string | undefined;\n if (valueField && valueField in factoryData) {\n const val = factoryData[valueField];\n const formatted = typeof val === 'number' ? val.toLocaleString() : String(val);\n // Map to standard fields for all built-in custom components\n factoryData = {\n ...factoryData,\n value: val, // Counter component\n title: factoryData.title || formatted, // Card component\n description: factoryData.description || widget.title || '', // Card component\n text: factoryData.text || formatted, // SimpleText component\n };\n }\n }\n\n return (\n <WidgetRendererFactory\n widget={widget}\n data={factoryData}\n isEditMode={isEditMode}\n isSelected={isSelected}\n isLoading={isLoading}\n error={error}\n onDrilldown={hasDrilldown ? handleDrilldown : undefined}\n onAction={handleActionFromFactory}\n />\n );\n }\n\n // v1.0: Use switch-case for BIGCONSOLE renderer (default)\n // Normalize widget type to lowercase for case-insensitive matching\n const normalizedType = widget.type?.toLowerCase() || '';\n switch (normalizedType) {\n case 'metric_card':\n return <MetricCardWidget {...props} />;\n case 'kpi_card_comparison':\n return <KPIComparisonWidget {...props} />;\n case 'chart':\n case 'funnel_chart':\n return <ChartWidget {...props} />;\n case 'table':\n return <TableWidget {...props} />;\n case 'pivot_table':\n return <PivotTableWidget {...props} />;\n case 'gauge':\n return <GaugeWidget {...props} />;\n case 'progress':\n return <ProgressWidget {...props} />;\n case 'list':\n return <ListWidget {...props} />;\n case 'heatmap':\n return <HeatmapWidget {...props} />;\n case 'retention':\n return <RetentionWidget {...props} />;\n case 'calendar':\n return <CalendarWidget {...props} />;\n case 'timeline':\n return <TimelineWidget {...props} />;\n case 'kanban':\n return <KanbanWidget {...props} />;\n case 'form':\n return <AdaptiveCardWidget {...props} />;\n case 'text':\n return <TextWidget {...props} />;\n case 'iframe':\n return <IframeWidget {...props} />;\n case 'map':\n return <MapWidget {...props} />;\n case 'custom':\n return <CustomWidget {...props} />;\n default:\n return children || <WidgetEmptyState widgetType={widget.type} />;\n }\n }, [\n widget,\n widgetData,\n children,\n hasDrilldown,\n handleDrilldown,\n useRendererFactory,\n isEditMode,\n isSelected,\n isLoading,\n error,\n handleActionFromFactory,\n realWidgetData,\n ]);\n\n // Determine content to render\n const renderContent = () => {\n if (isLoading) {\n return <WidgetLoadingState />;\n }\n\n if (error) {\n return <WidgetErrorState error={error} onRetry={onRefresh} />;\n }\n\n if (isEmpty && !children) {\n // v2.0: Check parserId (primary) or dataSinkId (for context/drilldowns)\n return (\n <WidgetEmptyState\n widgetType={widget.type}\n hasDataSource={\n !!(widget as unknown as Record<string, unknown>).parserId ||\n !!(widget as unknown as Record<string, unknown>).dataSinkId\n }\n />\n );\n }\n\n // resetKey auto-resets the boundary on parser/sink re-bind or config edit without widget.id changing (BOFF-2242).\n const resetKey = `${widget.parserId ?? ''}|${widget.dataSinkId ?? ''}|${widget.updatedAt ?? ''}`;\n\n return (\n <WidgetErrorBoundary widgetId={widget.id} widgetType={widget.type} widgetTitle={widget.title} resetKey={resetKey}>\n {children ?? <Suspense fallback={<WidgetLoadingState />}>{renderWidgetComponent()}</Suspense>}\n </WidgetErrorBoundary>\n );\n };\n\n return (\n <div\n ref={containerRef}\n className={`\n group relative flex flex-col\n h-full w-full\n rounded-lg border\n bg-bg-surface\n transition-all duration-200\n ${\n isSelected && isEditMode\n ? 'border-action-primary-border ring-2 ring-action-primary-border/30'\n : 'border-border-default hover:border-action-primary-bg'\n }\n ${isEditMode ? 'cursor-pointer' : ''}\n ${className}\n `}\n onMouseDown={handleMouseDown}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n tabIndex={isEditMode ? 0 : -1}\n role={isEditMode ? 'button' : undefined}\n aria-selected={isSelected}\n aria-label={`Widget: ${widget.title}`}\n data-widget-id={widget.id}\n data-widget-type={widget.type}\n >\n {/* Header */}\n <WidgetHeader\n widget={widget}\n definition={definition}\n isSelected={isSelected}\n isEditMode={isEditMode}\n isLoading={isLoading}\n timeUntilRefresh={widget.refreshInterval && !isPaused ? formatTimeUntilRefresh(timeUntilRefresh) : undefined}\n hasDrilldown={hasDrilldown}\n onRefresh={onRefresh}\n onExportCSV={exportData ? exportCSV : undefined}\n onExportPNG={exportPNG}\n onDrilldown={hasDrilldown ? () => handleDrilldown() : undefined}\n onToggleComments={toggleComments}\n commentsOpen={showComments}\n commentCount={commentsEverOpened ? commentCount : undefined}\n />\n\n {/* Content Area */}\n <div\n className={`\n flex-1 overflow-hidden p-4 min-h-0\n ${showContainerDrilldownHints && !isEditMode ? 'cursor-pointer' : ''}\n `}\n title={showContainerDrilldownHints && !isEditMode ? 'Click data to drill down' : undefined}\n >\n <div className=\"h-full overflow-auto\">{renderContent()}</div>\n </div>\n\n {/* v2.0: Widget Actions */}\n {actions && actions.length > 0 && !isLoading && !error && (\n <div className=\"px-4 py-3 border-t border-border-default bg-bg-sunken\" style={{ flexShrink: 0 }}>\n <WidgetActions\n actions={actions}\n context={actionContext}\n onAction={onAction}\n disabled={isEditMode}\n layout=\"horizontal\"\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Edit mode: react-grid-layout provides resize handles */}\n\n {/* Drilldown Indicator */}\n {showContainerDrilldownHints && !isEditMode && (\n <div\n className=\"\n absolute bottom-2 right-2\n opacity-0 group-hover:opacity-100\n transition-opacity duration-200\n \"\n title=\"Click to drill down\"\n >\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 7l5 5m0 0l-5 5m5-5H6\" />\n </svg>\n </div>\n )}\n\n {/* Comments panel (overlay) */}\n {showComments && (\n <Suspense fallback={null}>\n <WidgetComments widgetId={widget.id} onClose={() => setShowComments(false)} />\n </Suspense>\n )}\n </div>\n );\n}, arePropsEqual);\n\nexport default WidgetWrapper;\n"],"mappings":"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"}
1
+ {"version":3,"file":"WidgetWrapper.js","names":[],"sources":["../../../../src/bigconsole/components/widgets/WidgetWrapper.tsx"],"sourcesContent":["/**\n * WidgetWrapper Component (v2.1)\n *\n * Common wrapper for all widget types. Handles:\n * - Header with title and menu\n * - Loading, error, and empty states\n * - Selection highlighting\n * - Edit mode controls\n * - Real data only — no mock fallback; widgets show empty state when data is missing\n * - v2.0: Multi-renderer support via WidgetRendererFactory\n * - v2.0: Widget actions support\n */\n\nimport {\n type FC,\n type ReactNode,\n type RefObject,\n useCallback,\n memo,\n useMemo,\n useRef,\n useState,\n useEffect,\n lazy,\n Suspense,\n} from 'react';\nimport type { Widget } from '../../types';\nimport {\n useWidgetStore,\n useDashboardStore,\n useIsWidgetSelected,\n useWidgetUIState,\n useGlobalFilterValues,\n} from '../../store';\nimport {\n useWidgetAutoRefresh,\n formatTimeUntilRefresh,\n useWidgetExport,\n useDrilldown,\n useWidgetData,\n useWidgetComments,\n type DrilldownContext,\n} from '../../hooks';\nimport { useApolloClient } from '@apollo/client/react';\nimport { GetDashboardDocument } from '../../../generated/wspace-operations';\nimport { getWidgetDefinition } from './WidgetRegistry';\nimport { WidgetHeader } from './WidgetHeader';\nimport { WidgetLoadingState } from './states/LoadingState';\nimport { WidgetErrorState } from './states/ErrorState';\nimport { WidgetEmptyState } from './states/EmptyState';\nimport { WidgetErrorBoundary } from './WidgetErrorBoundary';\nimport { hasRenderableCustomConfig, hasCustomIframeConfig } from './custom/config';\n\n// v2.0: Renderer factory and actions\nimport { WidgetRendererFactory } from '../../renderers/factory/WidgetRendererFactory';\nimport { WidgetActions } from './widget-actions';\nimport type { WidgetAction, ActionContext, ActionResult, RendererType } from '../../renderers/types/renderer';\n// Widget components - lazy loaded for better code splitting\n// Each widget loads only when its type is rendered\nconst MetricCardWidget = lazy(() => import('./metric-card').then((m) => ({ default: m.MetricCardWidget })));\nconst ChartWidget = lazy(() => import('./chart').then((m) => ({ default: m.ChartWidget })));\nconst TableWidget = lazy(() => import('./table').then((m) => ({ default: m.TableWidget })));\nconst PivotTableWidget = lazy(() => import('./pivot-table').then((m) => ({ default: m.PivotTableWidget })));\nconst GaugeWidget = lazy(() => import('./gauge').then((m) => ({ default: m.GaugeWidget })));\nconst ProgressWidget = lazy(() => import('./progress').then((m) => ({ default: m.ProgressWidget })));\nconst ListWidget = lazy(() => import('./list').then((m) => ({ default: m.ListWidget })));\nconst HeatmapWidget = lazy(() => import('./heatmap').then((m) => ({ default: m.HeatmapWidget })));\nconst RetentionWidget = lazy(() => import('./retention').then((m) => ({ default: m.RetentionWidget })));\nconst CalendarWidget = lazy(() => import('./calendar').then((m) => ({ default: m.CalendarWidget })));\nconst KanbanWidget = lazy(() => import('./kanban').then((m) => ({ default: m.KanbanWidget })));\nconst TimelineWidget = lazy(() => import('./timeline').then((m) => ({ default: m.TimelineWidget })));\nconst KPIComparisonWidget = lazy(() => import('./kpi-comparison').then((m) => ({ default: m.KPIComparisonWidget })));\nconst TextWidget = lazy(() => import('./text-widget').then((m) => ({ default: m.TextWidget })));\nconst MapWidget = lazy(() => import('./map-widget').then((m) => ({ default: m.MapWidget })));\nconst IframeWidget = lazy(() => import('./iframe-widget').then((m) => ({ default: m.IframeWidget })));\nconst CustomWidget = lazy(() => import('./custom').then((m) => ({ default: m.CustomWidget })));\n// AdaptiveCardWidget is HUGE (984KB) - lazy load only when 'form' type is used\nconst AdaptiveCardWidget = lazy(() => import('./adaptive-card').then((m) => ({ default: m.AdaptiveCardWidget })));\n// Comments panel - lazy loaded; only mounts when the user opens comments\nconst WidgetComments = lazy(() => import('./comments/WidgetComments').then((m) => ({ default: m.WidgetComments })));\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WidgetWrapperProps {\n /** The widget data */\n widget: Widget;\n /** Widget content to render */\n children?: ReactNode;\n /** Data for the widget */\n data?: Record<string, unknown> | null;\n /** Custom class name */\n className?: string;\n /** Whether the widget is loading */\n isLoading?: boolean;\n /** Error message */\n error?: string | null;\n /** Callback to refresh widget data */\n onRefresh?: () => Promise<void>;\n /** Callback for drilldown navigation */\n onDrilldown?: (params: Record<string, unknown>) => void;\n /** v2.0: Widget actions */\n actions?: WidgetAction[];\n /** v2.0: Callback for action events */\n onAction?: (action: WidgetAction, context: ActionContext) => Promise<ActionResult | void>;\n /** Phase 2: Global filter values to apply to widget data */\n filterValues?: Record<string, unknown>;\n}\n\n// ============================================================================\n// Custom comparison for memo - ensures actions changes trigger re-render\n// ============================================================================\n\nfunction arePropsEqual(prevProps: WidgetWrapperProps, nextProps: WidgetWrapperProps): boolean {\n // Always re-render if widget ID changes\n if (prevProps.widget.id !== nextProps.widget.id) return false;\n // Re-render if widget was updated\n if (prevProps.widget.updatedAt !== nextProps.widget.updatedAt) return false;\n // Re-render if loading state changes\n if (prevProps.isLoading !== nextProps.isLoading) return false;\n // Re-render if error changes\n if (prevProps.error !== nextProps.error) return false;\n // Re-render if actions count changes\n if ((prevProps.actions?.length ?? 0) !== (nextProps.actions?.length ?? 0)) return false;\n // Check if any action ID changed\n if (prevProps.actions && nextProps.actions) {\n for (let i = 0; i < prevProps.actions.length; i++) {\n if (prevProps.actions[i]?.id !== nextProps.actions[i]?.id) return false;\n }\n }\n // Also check data changes\n if (prevProps.data !== nextProps.data) return false;\n // Phase 2: Re-render if filter values changed\n if (JSON.stringify(prevProps.filterValues) !== JSON.stringify(nextProps.filterValues)) return false;\n return true;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const WidgetWrapper: FC<WidgetWrapperProps> = memo(function WidgetWrapper({\n widget,\n children,\n data,\n className = '',\n isLoading: isLoadingProp,\n error: errorProp,\n onRefresh,\n onDrilldown,\n actions,\n onAction,\n filterValues,\n}) {\n // Store state - use Dashboard store for edit mode (synced with toolbar)\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const isSelected = useIsWidgetSelected(widget.id);\n const uiState = useWidgetUIState(widget.id);\n const globalFilterValues = useGlobalFilterValues();\n const apolloClient = useApolloClient();\n\n // Map a global-filter's id -> the data field it targets (`config.field`).\n // Global CATEGORY/TEXT filter values are keyed in the store by filter id (a\n // UUID), not by the data column they filter. Without this map, applyClientFilters\n // can only fall back to value/heuristic matching, which fails to narrow rows\n // whose targeted column (e.g. `country`, `cohort`) isn't in the hardcoded\n // heuristic list — so a selected Country/Cohort would not actually filter the\n // widget. We resolve the targeted field from the dashboard's globalFilters\n // (already in the Apollo cache from the page load) so a UUID-keyed filter\n // matches the row's configured field exactly.\n const widgetDashboardId = (widget as unknown as Record<string, unknown>).dashboardId as string | undefined;\n const filterFieldMap = useMemo<Record<string, string>>(() => {\n if (!widgetDashboardId) return {};\n try {\n const cached = apolloClient.readQuery<{\n getDashboard?: {\n globalFilters?: Array<{ id: string; config?: Record<string, unknown> | null }>;\n };\n }>({ query: GetDashboardDocument, variables: { id: widgetDashboardId } });\n const map: Record<string, string> = {};\n for (const f of cached?.getDashboard?.globalFilters ?? []) {\n const field = f.config?.field;\n if (typeof field === 'string' && field) map[f.id] = field;\n }\n return map;\n } catch {\n return {};\n }\n }, [apolloClient, widgetDashboardId]);\n\n // Fetch per-widget data from backend (parser-processed data)\n // This is needed because listWidgetsByDashboard uses WidgetListFields (without data)\n // for performance, so each widget fetches its own data individually\n const hasDataSource =\n !!(widget as unknown as Record<string, unknown>).dataSinkId ||\n !!(widget as unknown as Record<string, unknown>).datasetId ||\n !!(widget as unknown as Record<string, unknown>).parserId;\n const { data: fetchedWidgetData, loading: widgetDataLoading } = useWidgetData(widget.id, {\n skip: !hasDataSource,\n pollInterval: (widget.refreshInterval ?? 0) * 1000 || 0,\n });\n\n // WORKAROUND: If Zustand store is empty, directly read filter_* params from URL\n // This handles race conditions where the store hasn't been populated yet\n const urlFilterValues = useMemo(() => {\n if (typeof window === 'undefined') return {};\n\n const params = new URLSearchParams(window.location.search);\n const filters: Record<string, unknown> = {};\n\n params.forEach((value, key) => {\n if (key.startsWith('filter_')) {\n const filterId = key.slice('filter_'.length);\n // Skip internal metadata keys\n if (!filterId.startsWith('__')) {\n filters[filterId] = value;\n }\n }\n });\n\n return filters;\n }, []);\n\n // Phase 2: Merge global filter values (from URL) with explicit filterValues prop\n // This ensures both URL params (filter_*) and drilldown params work together\n const mergedFilterValues = useMemo(() => {\n const merged: Record<string, unknown> = {};\n\n // First, add URL filter values as fallback (in case Zustand store hasn't loaded yet)\n if (urlFilterValues && Object.keys(urlFilterValues).length > 0) {\n Object.assign(merged, urlFilterValues);\n }\n\n // Then, add global filter values from Zustand store (if populated)\n if (globalFilterValues && Object.keys(globalFilterValues).length > 0) {\n Object.assign(merged, globalFilterValues);\n }\n\n // Finally, add/override with explicit filterValues prop (e.g., from drilldown modal)\n if (filterValues && Object.keys(filterValues).length > 0) {\n Object.assign(merged, filterValues);\n }\n\n return merged;\n }, [urlFilterValues, globalFilterValues, filterValues]);\n\n // Track active filters (client-side filtering until backend supports it)\n const hasFilters = Object.keys(mergedFilterValues).length > 0;\n\n // Merge loading/error from props and store\n // Show loading if widget data is being fetched for the first time (no data yet)\n const isLoading = isLoadingProp ?? (widgetDataLoading && !fetchedWidgetData) ?? uiState.isLoading;\n const error = errorProp ?? uiState.errorMessage;\n\n // Get widget definition\n const definition = getWidgetDefinition(widget.type);\n\n // Stable refresh callback - use ref to avoid creating new function on each render\n const onRefreshRef = useRef(onRefresh);\n useEffect(() => {\n onRefreshRef.current = onRefresh;\n }, [onRefresh]);\n\n const stableOnRefresh = useCallback(async () => {\n if (onRefreshRef.current) {\n await onRefreshRef.current();\n }\n }, []);\n\n // Auto-refresh\n const { timeUntilRefresh, isPaused } = useWidgetAutoRefresh({\n widgetId: widget.id,\n refreshInterval: widget.refreshInterval,\n onRefresh: stableOnRefresh,\n enabled: !isEditMode && !!widget.refreshInterval,\n });\n\n // Container ref for PNG export\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Comments panel. The count query stays disabled until the panel is first\n // opened, so a dashboard of N widgets doesn't fire N comment queries on load.\n const [showComments, setShowComments] = useState(false);\n const [commentsEverOpened, setCommentsEverOpened] = useState(false);\n const { count: commentCount } = useWidgetComments(widget.id, commentsEverOpened);\n const toggleComments = useCallback(() => {\n setShowComments((open) => {\n const next = !open;\n if (next) setCommentsEverOpened(true);\n return next;\n });\n }, []);\n\n // Get widget data for export (computed early for hook)\n const exportData = useMemo(() => {\n if (data && Array.isArray(data)) return data;\n const rawData = (widget as unknown as Record<string, unknown>).data as Record<string, unknown> | undefined;\n if (rawData?.processed && Array.isArray(rawData.processed)) {\n return rawData.processed;\n }\n if (rawData?.raw) {\n const parsed = typeof rawData.raw === 'string' ? JSON.parse(rawData.raw) : rawData.raw;\n if (Array.isArray(parsed)) return parsed;\n }\n return null;\n }, [data, widget]);\n\n // Export functionality\n const { exportCSV, exportPNG } = useWidgetExport(widget, {\n data: exportData,\n containerRef: containerRef as unknown as RefObject<HTMLElement>,\n });\n\n // Helper to check if data is actually usable (has real content)\n const isUsableData = useCallback((d: unknown): boolean => {\n if (d === null || d === undefined) return false;\n if (Array.isArray(d)) return d.length > 0;\n if (typeof d === 'object') {\n const obj = d as Record<string, unknown>;\n const keys = Object.keys(obj);\n if (keys.length === 0) return false;\n // Check recursively for actual data content\n return keys.some((key) => {\n const val = obj[key];\n if (val === null || val === undefined || val === '') return false;\n if (Array.isArray(val)) return val.length > 0;\n if (typeof val === 'object') return isUsableData(val);\n return true;\n });\n }\n return true;\n }, []);\n\n // Phase 2: Client-side filter function for widget data\n const applyClientFilters = useCallback(\n (\n widgetData: Record<string, unknown> | Record<string, unknown>[]\n ): Record<string, unknown> | Record<string, unknown>[] => {\n if (!hasFilters || !widgetData) return widgetData;\n\n // Separate drilldown context filters (ctx_*) from regular filters\n const contextFilters: Record<string, unknown> = {};\n const regularFilters: Record<string, unknown> = {};\n\n Object.entries(mergedFilterValues || {}).forEach(([key, value]) => {\n if (key.startsWith('ctx_')) {\n // Strip ctx_ prefix to get the target field name\n const fieldName = key.slice(4); // Remove 'ctx_'\n contextFilters[fieldName] = value;\n } else if (!key.startsWith('__')) {\n // Skip internal metadata keys (__drilldown_depth, etc.)\n regularFilters[key] = value;\n }\n });\n\n // Helper to match a value against a filter (case-insensitive)\n const matchesFilter = (itemValue: unknown, filterValue: unknown): boolean => {\n if (filterValue === undefined || filterValue === null || filterValue === '') return true;\n if (itemValue === undefined || itemValue === null) return false;\n\n const itemStr = String(itemValue).toLowerCase();\n const filterStr = String(filterValue).toLowerCase();\n\n // Exact match or contains\n return itemStr === filterStr || itemStr.includes(filterStr);\n };\n\n // Helper to find a matching field in the item (case-insensitive key matching)\n const findFieldValue = (item: Record<string, unknown>, targetField: string): unknown => {\n const targetLower = targetField.toLowerCase();\n\n // Try exact match first\n if (item[targetField] !== undefined) return item[targetField];\n\n // Try case-insensitive match\n const matchingKey = Object.keys(item).find((k) => k.toLowerCase() === targetLower);\n if (matchingKey) return item[matchingKey];\n\n // Try partial match (e.g., 'region' matches 'regionName' or 'user_region')\n const partialMatch = Object.keys(item).find(\n (k) => k.toLowerCase().includes(targetLower) || targetLower.includes(k.toLowerCase())\n );\n if (partialMatch) return item[partialMatch];\n\n // Try stripping common prefixes from target field (e.g., 'kpiId' -> 'id', 'rowName' -> 'name')\n // This handles drilldown context where widget passes 'kpiId' but data has 'id'\n const commonPrefixes = ['kpi', 'row', 'item', 'data', 'cell', 'record', 'entry'];\n for (const prefix of commonPrefixes) {\n if (targetLower.startsWith(prefix) && targetLower.length > prefix.length) {\n const strippedField = targetLower.slice(prefix.length);\n // Try exact match with stripped field\n const strippedMatch = Object.keys(item).find((k) => k.toLowerCase() === strippedField);\n if (strippedMatch) return item[strippedMatch];\n }\n }\n\n return undefined;\n };\n\n // Helper function to filter an array of items\n const filterItems = (items: Record<string, unknown>[]): Record<string, unknown>[] => {\n return items.filter((item: Record<string, unknown>) => {\n // Only object rows are field-filterable. An array of primitives (e.g.\n // a heatmap's `rows: [\"5W-MedSurg\", …]`) carries no named fields — keep\n // every element. Without this, `findFieldValue` indexes into the string\n // by character offset and spuriously \"matches\" a filter, dropping rows.\n if (item === null || typeof item !== 'object' || Array.isArray(item)) {\n return true;\n }\n // First, apply drilldown context filters (ctx_*) - these have explicit field names\n for (const [fieldName, filterValue] of Object.entries(contextFilters)) {\n if (filterValue === undefined || filterValue === null || filterValue === '') continue;\n\n const itemValue = findFieldValue(item, fieldName);\n // If the field doesn't exist in this data item, skip this context filter.\n // Drilldown context params (e.g., source=support_status) are metadata about\n // the drilldown origin and should NOT filter out data when the target\n // dashboard's widgets don't have a matching field.\n if (itemValue === undefined) continue;\n if (!matchesFilter(itemValue, filterValue)) {\n return false;\n }\n }\n\n // Then, apply regular filters\n for (const [filterKey, filterValue] of Object.entries(regularFilters)) {\n if (filterValue === undefined || filterValue === null || filterValue === '') continue;\n\n // For date range filters\n if (typeof filterValue === 'object') {\n const fv = filterValue as Record<string, unknown>;\n if (fv.start || fv.end) {\n const dateFields = Object.entries(item).filter(\n ([k, _v]) => k.toLowerCase().includes('date') || k.toLowerCase().includes('time')\n );\n if (dateFields.length > 0) {\n const itemDate = new Date(dateFields[0][1] as string);\n if (fv.start && itemDate < new Date(fv.start as string)) return false;\n if (fv.end && itemDate > new Date(fv.end as string)) return false;\n }\n continue;\n }\n }\n\n // For numeric range filters\n if (typeof filterValue === 'object') {\n const nv = filterValue as Record<string, unknown>;\n if (nv.min !== undefined || nv.max !== undefined) {\n const numericFields = Object.entries(item).filter(([_k, v]) => typeof v === 'number');\n if (numericFields.length > 0) {\n const itemNum = numericFields[0][1] as number;\n if (nv.min !== undefined && itemNum < (nv.min as number)) return false;\n if (nv.max !== undefined && itemNum > (nv.max as number)) return false;\n }\n continue;\n }\n }\n\n // Any other object-shaped filter value (e.g. a DATE_RANGE filter set\n // to a preset like { preset: \"last_30_days\" } with no resolved\n // start/end, or a structured value we don't understand) cannot be\n // compared with the scalar matchers below. Passing it through would\n // stringify it to \"[object Object]\" and, via fuzzy field matching\n // (e.g. an `enrolledAt` filter partial-matching an `enrolled` number\n // column), spuriously exclude EVERY row. Treat such values as a no-op\n // for client-side filtering — the row set is unchanged until the\n // filter resolves to concrete start/end or scalar values.\n if (filterValue !== null && typeof filterValue === 'object' && !Array.isArray(filterValue)) {\n continue;\n }\n\n // Global CATEGORY/TEXT filters are keyed by filter id (UUID). Resolve\n // the data column they target from the filter's config.field. When the\n // widget's rows carry that column, the filter is authoritative — a\n // mismatch excludes the row; a missing column is a no-op (the filter\n // simply doesn't apply to this widget's shape). This is what makes a\n // selected Country/Cohort actually narrow the visible data.\n const targetField = filterFieldMap[filterKey];\n if (targetField) {\n const mappedValue = findFieldValue(item, targetField);\n if (mappedValue !== undefined) {\n if (!matchesFilter(mappedValue, filterValue)) {\n return false;\n }\n continue;\n }\n // Configured field absent from this widget's rows → filter is a\n // no-op for this widget (do not fall through to value heuristics,\n // which could spuriously match an unrelated column).\n continue;\n }\n\n // Try to match by filter key name first (for named filters). When\n // the widget's data actually carries the filter's field, the filter\n // is authoritative — a mismatch excludes the row.\n const fieldValue = findFieldValue(item, filterKey);\n if (fieldValue !== undefined) {\n if (!matchesFilter(fieldValue, filterValue)) {\n return false;\n }\n continue;\n }\n\n // For text/category filters - try multiple matching strategies\n const filterStr = String(filterValue).toLowerCase();\n\n // Strategy 1: Check all field values for exact or partial match\n const itemValues = Object.values(item).map((v) => String(v).toLowerCase());\n const hasMatch = itemValues.some((v) => v === filterStr || v.includes(filterStr));\n\n if (!hasMatch) {\n // Strategy 2: Try to find a field name that matches the filter value context\n const fieldNames = Object.keys(item).map((k) => k.toLowerCase());\n const possibleFields = fieldNames.filter(\n (f) =>\n f.includes('category') ||\n f.includes('type') ||\n f.includes('status') ||\n f.includes('name') ||\n f.includes('region') ||\n f.includes('group')\n );\n\n let fieldMatch = false;\n for (const fieldName of possibleFields) {\n const originalKey = Object.keys(item).find((k) => k.toLowerCase() === fieldName);\n if (originalKey) {\n const itemFieldValue = String(item[originalKey]).toLowerCase();\n if (itemFieldValue === filterStr || itemFieldValue.includes(filterStr)) {\n fieldMatch = true;\n break;\n }\n }\n }\n\n // The filter's field is NOT present in this dataset (named-field\n // lookup failed AND no value/heuristic match). A global filter that\n // doesn't apply to a widget's shape must be a no-op for that widget\n // — not a silent \"exclude every row\". This mirrors the ctx_* rule\n // above (skip when the targeted field is absent). Without this, an\n // always-active required filter (e.g. campus=main) blanks every\n // parser-derived widget whose output has no campus column.\n if (!fieldMatch) {\n continue;\n }\n }\n }\n return true;\n });\n };\n\n // If data is an array, filter the items directly\n if (Array.isArray(widgetData)) {\n const filtered = filterItems(widgetData);\n return filtered;\n }\n\n // Handle object with array properties (rows, items, members, questions, data, etc.)\n if (widgetData && typeof widgetData === 'object') {\n // Common array property names in widget data\n const arrayPropertyNames = [\n 'kpis', // KPI Comparison widget\n 'rows', // Table widget\n 'items', // List widget\n 'members', // Team/member widgets\n 'questions', // Form widget\n 'data', // Generic data\n 'records', // Database-style data\n 'results', // Search/query results\n 'entries', // Log/feed entries\n 'list', // Generic list\n 'values', // Value arrays\n 'series', // Chart series\n 'datasets', // Chart datasets\n ];\n\n for (const propName of arrayPropertyNames) {\n if (widgetData[propName] && Array.isArray(widgetData[propName])) {\n const filteredArray = filterItems(widgetData[propName]);\n return {\n ...widgetData,\n [propName]: filteredArray,\n ...(propName === 'rows' ? { totalRows: filteredArray.length } : {}),\n };\n }\n }\n\n // If no known array property found, check for any array property\n const arrayProps = Object.entries(widgetData).filter(([_key, v]) => Array.isArray(v));\n if (arrayProps.length > 0) {\n const [propName, propValue] = arrayProps[0];\n const filteredArray = filterItems(propValue as Record<string, unknown>[]);\n return {\n ...widgetData,\n [propName]: filteredArray,\n };\n }\n }\n\n // For non-filterable data, return as-is\n return widgetData;\n },\n [hasFilters, mergedFilterValues, widget.id, filterFieldMap]\n );\n\n // Get widget data from the widget itself (fetched from backend)\n const realWidgetData = useMemo(() => {\n let rawWidgetData = null;\n\n // Priority 1: Explicitly passed data prop\n if (data && isUsableData(data)) {\n rawWidgetData = data;\n }\n // Priority 2: Per-widget fetched data (from useWidgetData hook)\n // fetchedWidgetData IS a WidgetData object: { raw, processed, fetchedAt, cached }\n else if (fetchedWidgetData) {\n const fetched = fetchedWidgetData as unknown as Record<string, unknown>;\n if (fetched?.processed && isUsableData(fetched.processed)) {\n rawWidgetData = fetched.processed;\n } else if (fetched?.raw) {\n try {\n const parsed = typeof fetched.raw === 'string' ? JSON.parse(fetched.raw as string) : fetched.raw;\n if (isUsableData(parsed)) rawWidgetData = parsed;\n } catch {\n // Invalid JSON, ignore\n }\n }\n }\n // Priority 3: widget.data from GraphQL response (if already on widget object)\n else {\n const rawData = (widget as unknown as Record<string, unknown>).data as Record<string, unknown> | undefined;\n if (rawData?.processed && isUsableData(rawData.processed)) {\n rawWidgetData = rawData.processed;\n } else if (rawData?.raw) {\n try {\n const parsed = typeof rawData.raw === 'string' ? JSON.parse(rawData.raw) : rawData.raw;\n if (isUsableData(parsed)) rawWidgetData = parsed;\n } catch {\n // Invalid JSON, ignore\n }\n }\n }\n\n // Priority 4: config.mockData — user-configured sample JSON from Widget Properties Data tab\n // This is NOT auto-generated mock data; it's explicit JSON the user pasted in the UI\n if (!rawWidgetData) {\n const configMockData = (widget.config as Record<string, unknown> | undefined)?.mockData;\n if (configMockData) {\n try {\n const parsed = typeof configMockData === 'string' ? JSON.parse(configMockData) : configMockData;\n if (isUsableData(parsed)) {\n rawWidgetData = parsed;\n }\n } catch (e) {}\n }\n }\n\n // Generic parser-output resolution: a widget may bind to a parser whose\n // output is an envelope ({ byUnit:[...], house:{...} }) rather than the\n // exact array/object the renderer wants. config.dataKey / dataPath /\n // seriesPath is a dot-path INTO the resolved data that points at the\n // renderable subtree (e.g. \"byUnit\" for a table, \"rows\" for a chart).\n // Resolving it here keeps every renderer generic — the use-case-specific\n // shape knowledge lives in the widget config (set by the seed/UI), not in\n // the renderer. valueField (single-value metric path) is intentionally NOT\n // applied here: MetricCardWidget resolves it itself against the envelope.\n if (rawWidgetData && typeof rawWidgetData === 'object' && !Array.isArray(rawWidgetData)) {\n const cfg = widget.config as Record<string, unknown> | undefined;\n const pathKey = (cfg?.dataKey ?? cfg?.dataPath ?? cfg?.seriesPath) as string | undefined;\n if (pathKey && typeof pathKey === 'string') {\n const resolved = pathKey.split('.').reduce<unknown>((cur, seg) => {\n return cur && typeof cur === 'object' ? (cur as Record<string, unknown>)[seg] : undefined;\n }, rawWidgetData);\n if (resolved !== undefined && resolved !== null && isUsableData(resolved)) {\n rawWidgetData = resolved as Record<string, unknown>;\n }\n }\n }\n\n // Phase 2: Apply client-side filters if active\n if (hasFilters && rawWidgetData) {\n const filtered = applyClientFilters(rawWidgetData);\n return filtered;\n }\n\n return rawWidgetData;\n }, [data, widget, fetchedWidgetData, hasFilters, applyClientFilters, mergedFilterValues, isUsableData]);\n\n // Final widget data - real data only, no mock fallback\n const widgetData = useMemo(() => {\n return realWidgetData ?? null;\n }, [realWidgetData]);\n\n // Drilldown navigation (must be after widgetData is defined)\n const { executeDrilldown, canDrilldown } = useDrilldown({\n onDrilldown: (params, _mode) => {\n onDrilldown?.(params);\n },\n });\n\n // Check if widget has drilldown configured\n const hasDrilldown = useMemo(() => canDrilldown(widget), [widget, canDrilldown]);\n\n // Handle drilldown execution with context\n const handleDrilldown = useCallback(\n (selectedData?: Record<string, unknown>) => {\n if (!widget.drilldown?.enabled) return;\n\n const context: DrilldownContext = {\n selectedData: selectedData || {},\n globalFilters: globalFilterValues || {},\n widgetData: widgetData || {},\n metadata: {\n widgetId: widget.id,\n widgetType: widget.type,\n widgetTitle: widget.title,\n },\n sourceDashboardId: widget.dashboardId,\n };\n\n executeDrilldown(widget.drilldown, context);\n },\n [widget, widgetData, globalFilterValues, executeDrilldown]\n );\n\n // Track mouse position to distinguish click from drag\n const mouseDownPos = useRef<{ x: number; y: number } | null>(null);\n const DRAG_THRESHOLD = 5; // pixels - if mouse moves more than this, it's a drag\n\n // Handle mouse down - record position\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n mouseDownPos.current = { x: e.clientX, y: e.clientY };\n }, []);\n\n // Handle widget click - Edit mode only (View mode drilldown is triggered by data clicks)\n // Only trigger if mouse hasn't moved much (not a drag)\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation(); // Prevent DashboardGrid from also handling\n\n // Check if this was a drag (mouse moved significantly)\n if (mouseDownPos.current) {\n const dx = Math.abs(e.clientX - mouseDownPos.current.x);\n const dy = Math.abs(e.clientY - mouseDownPos.current.y);\n if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {\n // This was a drag, not a click - don't open config panel\n mouseDownPos.current = null;\n return;\n }\n }\n mouseDownPos.current = null;\n\n if (isEditMode) {\n // Edit mode: select widget for editing\n selectWidget(widget.id);\n }\n // View mode: drilldown is triggered by clicking specific data elements\n // (table rows, chart points, etc.) - not by clicking the widget itself\n },\n [isEditMode, selectWidget, widget.id]\n );\n\n // Handle keyboard navigation (Edit mode only)\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n if (isEditMode) {\n selectWidget(widget.id);\n }\n }\n },\n [isEditMode, selectWidget, widget.id]\n );\n\n const hasRenderableCustomWidgetConfig = useMemo(\n () => widget.type === 'custom' && hasRenderableCustomConfig(widget.config),\n [widget.type, widget.config]\n );\n\n const customWidgetHasIframeConfig = useMemo(\n () => widget.type === 'custom' && hasCustomIframeConfig(widget.config),\n [widget.type, widget.config]\n );\n\n const showContainerDrilldownHints = hasDrilldown && !customWidgetHasIframeConfig;\n\n // Determine if data is empty\n const isEmpty = !isLoading && !error && !widgetData && !hasRenderableCustomWidgetConfig;\n\n // v2.0: Check if widget has a renderer specified (non-BIGCONSOLE)\n const widgetRenderer = useMemo(() => {\n const widgetWithRenderer = widget as Widget & { renderer?: RendererType };\n return widgetWithRenderer.renderer;\n }, [widget]);\n\n // v2.0: Determine if we should use the factory (non-BIGCONSOLE renderer)\n const useRendererFactory = useMemo(() => {\n const shouldUse = widgetRenderer && widgetRenderer !== 'BIGCONSOLE';\n return shouldUse;\n }, [widgetRenderer]);\n\n // v2.0: Create action context\n const actionContext = useMemo<ActionContext>(\n () => ({\n widgetId: widget.id,\n data: widgetData || {},\n }),\n [widget.id, widgetData]\n );\n\n // v2.0: Handle action callback from WidgetRendererFactory\n // Receives (action: WidgetAction, context: ActionContext) and forwards to parent\n // Checks if a more detailed backend action exists with same name\n const handleActionFromFactory = useCallback(\n (action: WidgetAction, context: ActionContext) => {\n if (!onAction) return;\n\n // Try to find a matching backend action by name (has more details like confirmation)\n const existingAction = actions?.find((a) => a.name === action.name);\n\n if (existingAction) {\n // Use existing backend action (has confirmation, style, icon, etc.)\n onAction(existingAction, context);\n } else {\n // Use the action from WidgetRendererFactory as-is\n onAction(action, context);\n }\n },\n [onAction, actions]\n );\n\n // Render the appropriate widget component based on type (v1.0 snake_case)\n const renderWidgetComponent = useCallback(() => {\n // Extract data based on widget type expectations\n // Different widgets expect different data formats\n const getDataForWidget = () => {\n if (!widgetData) return widgetData;\n\n // If data has 'rows' property (table data), extract rows\n if (widget.type === 'table' || widget.type === 'pivot_table') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'rows' in widgetData) {\n return (widgetData as { rows: unknown[] }).rows;\n }\n }\n\n // If data has 'items' property (list data), extract items\n if (widget.type === 'list') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'items' in widgetData) {\n return (widgetData as { items: unknown[] }).items;\n }\n }\n\n // If data has 'stages' property (old funnel format), extract stages\n if (widget.type === 'funnel_chart') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'stages' in widgetData) {\n return (widgetData as { stages: unknown[] }).stages;\n }\n }\n\n // If data has 'datasets' property (old chart format), convert to array format\n if (widget.type === 'chart') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'datasets' in widgetData) {\n // Convert old format to new array format\n const oldData = widgetData as { labels?: string[]; datasets?: { name: string; data: number[] }[] };\n if (oldData.labels && oldData.datasets) {\n return oldData.labels.map((label, i) => {\n const point: Record<string, unknown> = { name: label };\n oldData.datasets?.forEach((ds) => {\n point[ds.name.toLowerCase()] = ds.data[i];\n });\n return point;\n });\n }\n }\n }\n\n // For text widgets: convert parsed object data to {content, format} shape\n if (widget.type === 'text') {\n if (widgetData && typeof widgetData === 'object' && !('content' in (widgetData as Record<string, unknown>))) {\n // Auto-format parsed data as markdown content\n const obj = widgetData as Record<string, unknown>;\n const lines: string[] = [];\n for (const [key, val] of Object.entries(obj)) {\n if (val && typeof val === 'object' && !Array.isArray(val)) {\n lines.push(`### ${key}`);\n for (const [k2, v2] of Object.entries(val as Record<string, unknown>)) {\n lines.push(`- **${k2}:** ${v2}`);\n }\n } else if (Array.isArray(val)) {\n lines.push(`### ${key}`);\n val.slice(0, 10).forEach((item: unknown) => {\n if (item && typeof item === 'object') {\n const entries = Object.entries(item as Record<string, unknown>);\n lines.push(`- ${entries.map(([k, v]) => `**${k}:** ${v}`).join(' | ')}`);\n } else {\n lines.push(`- ${item}`);\n }\n });\n } else {\n lines.push(`- **${key}:** ${val}`);\n }\n }\n return { content: lines.join('\\n'), format: 'markdown' };\n }\n }\n\n // For custom widgets: pass data through with extracted value if configured\n if (widget.type === 'custom') {\n const config = widget.config as Record<string, unknown> | undefined;\n const valueField = config?.valueField as string | undefined;\n if (valueField && widgetData && typeof widgetData === 'object' && !Array.isArray(widgetData)) {\n const obj = widgetData as Record<string, unknown>;\n if (valueField in obj) {\n return { value: obj[valueField], label: widget.title, data: obj };\n }\n }\n }\n\n return widgetData;\n };\n\n const normalizedData = getDataForWidget();\n const props = {\n widget,\n data: normalizedData,\n onDrilldown: hasDrilldown ? handleDrilldown : undefined,\n };\n\n // v2.0: Use WidgetRendererFactory for non-BIGCONSOLE renderers\n if (useRendererFactory) {\n // For factory renderers (Adaptive Card, BlockKit, Custom), pass the RAW\n // processed data — NOT the type-normalized data. Normalization (e.g., text→markdown)\n // is only for BIGCONSOLE renderer widgets. Factory renderers handle their own\n // template interpolation against the raw data object.\n let factoryData: Record<string, unknown> = {};\n\n if (\n realWidgetData &&\n typeof realWidgetData === 'object' &&\n !Array.isArray(realWidgetData) &&\n Object.keys(realWidgetData as Record<string, unknown>).length > 0\n ) {\n // Use real data from backend (raw processed, not type-normalized)\n factoryData = { ...(realWidgetData as Record<string, unknown>) };\n }\n\n // For CUSTOM renderer: enrich data with standard fields expected by built-in components.\n // Built-in components (Counter, Card, SimpleText) read specific fields from data:\n // Counter: data.value, Card: data.title/data.description, SimpleText: data.text\n // Parsed data from datasinks has domain-specific fields (totalFiles, totalMessages, etc.)\n // Use config.valueField to extract the primary value and map it to standard fields.\n const internalType = (widget.config as Record<string, unknown> | undefined)?._internalWidgetType as\n string | undefined;\n if (internalType === 'custom' && Object.keys(factoryData).length > 0) {\n const config = widget.config as Record<string, unknown> | undefined;\n const valueField = config?.valueField as string | undefined;\n if (valueField && valueField in factoryData) {\n const val = factoryData[valueField];\n const formatted = typeof val === 'number' ? val.toLocaleString() : String(val);\n // Map to standard fields for all built-in custom components\n factoryData = {\n ...factoryData,\n value: val, // Counter component\n title: factoryData.title || formatted, // Card component\n description: factoryData.description || widget.title || '', // Card component\n text: factoryData.text || formatted, // SimpleText component\n };\n }\n }\n\n return (\n <WidgetRendererFactory\n widget={widget}\n data={factoryData}\n isEditMode={isEditMode}\n isSelected={isSelected}\n isLoading={isLoading}\n error={error}\n onDrilldown={hasDrilldown ? handleDrilldown : undefined}\n onAction={handleActionFromFactory}\n />\n );\n }\n\n // v1.0: Use switch-case for BIGCONSOLE renderer (default)\n // Normalize widget type to lowercase for case-insensitive matching\n const normalizedType = widget.type?.toLowerCase() || '';\n switch (normalizedType) {\n case 'metric_card':\n return <MetricCardWidget {...props} />;\n case 'kpi_card_comparison':\n return <KPIComparisonWidget {...props} />;\n case 'chart':\n case 'funnel_chart':\n return <ChartWidget {...props} />;\n case 'table':\n return <TableWidget {...props} />;\n case 'pivot_table':\n return <PivotTableWidget {...props} />;\n case 'gauge':\n return <GaugeWidget {...props} />;\n case 'progress':\n return <ProgressWidget {...props} />;\n case 'list':\n return <ListWidget {...props} />;\n case 'heatmap':\n return <HeatmapWidget {...props} />;\n case 'retention':\n return <RetentionWidget {...props} />;\n case 'calendar':\n return <CalendarWidget {...props} />;\n case 'timeline':\n return <TimelineWidget {...props} />;\n case 'kanban':\n return <KanbanWidget {...props} />;\n case 'form':\n return <AdaptiveCardWidget {...props} />;\n case 'text':\n return <TextWidget {...props} />;\n case 'iframe':\n return <IframeWidget {...props} />;\n case 'map':\n return <MapWidget {...props} />;\n case 'custom':\n return <CustomWidget {...props} />;\n default:\n return children || <WidgetEmptyState widgetType={widget.type} />;\n }\n }, [\n widget,\n widgetData,\n children,\n hasDrilldown,\n handleDrilldown,\n useRendererFactory,\n isEditMode,\n isSelected,\n isLoading,\n error,\n handleActionFromFactory,\n realWidgetData,\n ]);\n\n // Determine content to render\n const renderContent = () => {\n if (isLoading) {\n return <WidgetLoadingState />;\n }\n\n if (error) {\n return <WidgetErrorState error={error} onRetry={onRefresh} />;\n }\n\n if (isEmpty && !children) {\n // v2.0: Check parserId (primary) or dataSinkId (for context/drilldowns)\n return (\n <WidgetEmptyState\n widgetType={widget.type}\n hasDataSource={\n !!(widget as unknown as Record<string, unknown>).parserId ||\n !!(widget as unknown as Record<string, unknown>).dataSinkId\n }\n />\n );\n }\n\n // resetKey auto-resets the boundary on parser/sink re-bind or config edit without widget.id changing (BOFF-2242).\n const resetKey = `${widget.parserId ?? ''}|${widget.dataSinkId ?? ''}|${widget.updatedAt ?? ''}`;\n\n return (\n <WidgetErrorBoundary widgetId={widget.id} widgetType={widget.type} widgetTitle={widget.title} resetKey={resetKey}>\n {children ?? <Suspense fallback={<WidgetLoadingState />}>{renderWidgetComponent()}</Suspense>}\n </WidgetErrorBoundary>\n );\n };\n\n return (\n <div\n ref={containerRef}\n className={`\n group relative flex flex-col\n h-full w-full\n rounded-lg border\n bg-bg-surface\n transition-all duration-200\n ${\n isSelected && isEditMode\n ? 'border-action-primary-border ring-2 ring-action-primary-border/30'\n : 'border-border-default hover:border-action-primary-bg'\n }\n ${isEditMode ? 'cursor-pointer' : ''}\n ${className}\n `}\n onMouseDown={handleMouseDown}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n tabIndex={isEditMode ? 0 : -1}\n role={isEditMode ? 'button' : undefined}\n aria-selected={isSelected}\n aria-label={`Widget: ${widget.title}`}\n data-widget-id={widget.id}\n data-widget-type={widget.type}\n >\n {/* Header */}\n <WidgetHeader\n widget={widget}\n definition={definition}\n isSelected={isSelected}\n isEditMode={isEditMode}\n isLoading={isLoading}\n timeUntilRefresh={widget.refreshInterval && !isPaused ? formatTimeUntilRefresh(timeUntilRefresh) : undefined}\n hasDrilldown={hasDrilldown}\n onRefresh={onRefresh}\n onExportCSV={exportData ? exportCSV : undefined}\n onExportPNG={exportPNG}\n onDrilldown={hasDrilldown ? () => handleDrilldown() : undefined}\n onToggleComments={toggleComments}\n commentsOpen={showComments}\n commentCount={commentsEverOpened ? commentCount : undefined}\n />\n\n {/* Content Area */}\n <div\n className={`\n flex-1 overflow-hidden p-4 min-h-0\n ${showContainerDrilldownHints && !isEditMode ? 'cursor-pointer' : ''}\n `}\n title={showContainerDrilldownHints && !isEditMode ? 'Click data to drill down' : undefined}\n >\n <div className=\"h-full overflow-auto\">{renderContent()}</div>\n </div>\n\n {/* v2.0: Widget Actions */}\n {actions && actions.length > 0 && !isLoading && !error && (\n <div className=\"px-4 py-3 border-t border-border-default bg-bg-sunken\" style={{ flexShrink: 0 }}>\n <WidgetActions\n actions={actions}\n context={actionContext}\n onAction={onAction}\n disabled={isEditMode}\n layout=\"horizontal\"\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Edit mode: react-grid-layout provides resize handles */}\n\n {/* Drilldown Indicator */}\n {showContainerDrilldownHints && !isEditMode && (\n <div\n className=\"\n absolute bottom-2 right-2\n opacity-0 group-hover:opacity-100\n transition-opacity duration-200\n \"\n title=\"Click to drill down\"\n >\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 7l5 5m0 0l-5 5m5-5H6\" />\n </svg>\n </div>\n )}\n\n {/* Comments panel (overlay) */}\n {showComments && (\n <Suspense fallback={null}>\n <WidgetComments widgetId={widget.id} onClose={() => setShowComments(false)} />\n </Suspense>\n )}\n </div>\n );\n}, arePropsEqual);\n\nexport default WidgetWrapper;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"MetricCardWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx"],"sourcesContent":["/**\n * MetricCardWidget Component\n *\n * Displays a single KPI/metric with trend, comparison, and optional sparkline.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, MetricCardConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\nimport { ComparisonBadge } from './ComparisonBadge';\nimport { SparklineChart, type SparklineDataPoint } from './SparklineChart';\nimport { firstObjectRow, inferValueField, unwrapRows } from '../utils/inferFields';\nimport { resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MetricCardWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Fetched data */\n data?: MetricCardData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives metric data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MetricCardData {\n /** Primary value to display */\n value: number | string;\n /** Previous value for comparison */\n previousValue?: number;\n /** Percentage change from previous */\n changePercent?: number;\n /** Trend direction override */\n trend?: TrendDirection;\n /** Sparkline data points */\n sparklineData?: SparklineDataPoint[];\n /** Target value for comparison */\n target?: number;\n /** Unit label (e.g., \"users\", \"$\") */\n unit?: string;\n /** Whether the metric is a currency */\n isCurrency?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Format a number based on configuration\n */\nfunction formatValue(value: number | string, config?: MetricCardConfig): string {\n if (typeof value === 'string') return value;\n\n const { format = 'number', prefix, suffix } = config || {};\n\n let formatted: string;\n\n switch (format) {\n case 'currency':\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(value);\n break;\n\n case 'percentage':\n formatted = `${(value * 100).toFixed(1)}%`;\n break;\n\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n break;\n\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n\n // Apply prefix/suffix if provided\n if (prefix) formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n\n return formatted;\n}\n\n/**\n * Calculate trend direction from change percent\n */\nfunction calculateTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n/**\n * Get sparkline color based on trend\n */\nfunction getSparklineColor(trend: TrendDirection, positiveIsGood: boolean): 'success' | 'error' | 'muted' {\n if (trend === 'flat') return 'muted';\n const isGood = positiveIsGood ? trend === 'up' : trend === 'down';\n return isGood ? 'success' : 'error';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MetricCardWidget: FC<MetricCardWidgetProps> = memo(function MetricCardWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as MetricCardConfig | undefined;\n\n // Normalize data - extract value from object if valueField is configured\n const data = useMemo((): MetricCardData | undefined => {\n if (!rawData) return undefined;\n\n // Helper to get nested value using dot notation (e.g., \"summary.totalSales\")\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {\n return path.split('.').reduce((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj as unknown);\n };\n\n // If data already has 'value' property, use it directly\n if ('value' in rawData && rawData.value !== undefined) {\n return rawData as MetricCardData;\n }\n\n // If valueField is configured, extract the value from that field (supports dot notation)\n const valueField = (config as unknown as Record<string, unknown> | undefined)?.valueField as string | undefined;\n if (valueField && typeof rawData === 'object') {\n const extractedValue = getNestedValue(rawData as unknown as Record<string, unknown>, valueField);\n if (extractedValue !== undefined) {\n // Also try to extract previousValue if previousValueField is configured\n const previousValueField = (config as unknown as Record<string, unknown> | undefined)?.previousValueField as\n | string\n | undefined;\n const previousValue = previousValueField\n ? (getNestedValue(rawData as unknown as Record<string, unknown>, previousValueField) as number | undefined)\n : undefined;\n\n return {\n value: extractedValue as number | string,\n previousValue,\n changePercent:\n previousValue !== undefined && typeof extractedValue === 'number'\n ? ((extractedValue - previousValue) / previousValue) * 100\n : undefined,\n };\n }\n }\n\n // Try common field names at top level\n const commonFields = ['value', 'total', 'amount', 'count', 'sum', 'totalSales', 'totalRevenue', 'totalOrders'];\n for (const field of commonFields) {\n if (typeof rawData === 'object' && field in rawData) {\n const val = (rawData as unknown as Record<string, unknown>)[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n // Try looking in nested 'summary' object (common pattern)\n if (typeof rawData === 'object' && 'summary' in rawData) {\n const summary = (rawData as Record<string, unknown>).summary as Record<string, unknown> | undefined;\n if (summary && typeof summary === 'object') {\n for (const field of commonFields) {\n if (field in summary) {\n const val = summary[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n }\n }\n\n // If rawData is a number or string directly, use it\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // BOFF-2246 smart default: if rawData is an array (or envelope around an\n // array) with object rows, use the configured valueField if it resolves\n // on any row (supports dotted paths like \"metrics.revenue\") — otherwise\n // pick the first numeric field from row[0]. This path only fires when\n // the earlier top-level extraction didn't find anything, but an explicit\n // `config.valueField` (flat or dotted) must always win over inference.\n const firstRow = firstObjectRow(rawData);\n if (firstRow) {\n const configuredField = (config as unknown as Record<string, unknown> | undefined)?.valueField as\n | string\n | undefined;\n if (configuredField) {\n // Configured field is authoritative. Scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column\n // from row[0] (that would silently misrepresent the user's mapping).\n const rows = unwrapRows(rawData) ?? [firstRow];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolved = getNestedValue(r as Record<string, unknown>, configuredField);\n if (typeof resolved === 'number' || typeof resolved === 'string') {\n return { value: resolved };\n }\n }\n }\n return undefined;\n }\n const inferredField = inferValueField(firstRow);\n if (inferredField) {\n const val = firstRow[inferredField];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n return undefined;\n }, [rawData, config]);\n\n // Calculate derived values\n const trend = useMemo(() => {\n return data?.trend || calculateTrend(data?.changePercent);\n }, [data?.trend, data?.changePercent]);\n\n const positiveIsGood = config?.positiveIsGood ?? true;\n\n const formattedValue = useMemo(() => {\n // Treat 0 / \"\" as real values — only a missing value renders the em dash.\n // (`if (!data?.value)` would wrongly blank out a legitimate 0, e.g. an ED\n // boarding-compliance of 0% or an available-bed count of 0.)\n if (data?.value === undefined || data?.value === null) return '—';\n return formatValue(data.value, config);\n }, [data?.value, config]);\n\n const sparklineColor = useMemo(() => {\n return getSparklineColor(trend, positiveIsGood);\n }, [trend, positiveIsGood]);\n\n // Conditional formatting on the metric value (threshold-driven color, not\n // just trend direction). Falls back to the default primary text color.\n const valueConditionalClass = useMemo(() => {\n const style = resolveConditionalStyle(data?.value, config?.conditionalRules);\n return conditionalStyleToClassName(style);\n }, [data?.value, config?.conditionalRules]);\n\n // Handle click\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown) {\n onDrilldown({\n value: data?.value,\n changePercent: data?.changePercent,\n trend,\n target: data?.target,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, trend]);\n\n // Determine if clickable\n const isClickable = Boolean(onDrilldown || (onClick && widget.drilldowns?.length));\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Subtitle/Label */}\n {config?.subtitle && <p className=\"text-xs text-text-secondary mb-1 truncate\">{config.subtitle}</p>}\n\n {/* Main Value */}\n <div className=\"flex items-baseline gap-2 mb-2\">\n <span\n className={`text-3xl font-semibold tracking-tight ${valueConditionalClass || 'text-text-primary'}`}\n title={String(data?.value)}\n >\n {formattedValue}\n </span>\n\n {/* Trend Indicator (inline) */}\n {config?.showTrend !== false && data?.changePercent !== undefined && (\n <TrendIndicator direction={trend} size=\"md\" positiveIsGood={positiveIsGood} />\n )}\n </div>\n\n {/* Comparison Badge */}\n {config?.showComparison !== false && data?.changePercent !== undefined && (\n <div className=\"mb-3\">\n <ComparisonBadge\n value={data.changePercent}\n label={config?.comparisonLabel}\n positiveIsGood={positiveIsGood}\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Target Progress */}\n {data?.target !== undefined && typeof data.value === 'number' && (\n <div className=\"mb-3\">\n <div className=\"flex justify-between text-xs text-text-secondary mb-1\">\n <span>Target: {formatValue(data.target, config)}</span>\n <span>{((data.value / data.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1.5 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${\n data.value >= data.target ? 'bg-state-success' : 'bg-action-primary-bg'\n }`}\n style={{\n width: `${Math.min((data.value / data.target) * 100, 100)}%`,\n }}\n />\n </div>\n </div>\n )}\n\n {/* Sparkline */}\n {config?.showSparkline !== false && data?.sparklineData?.length ? (\n <div className=\"mt-auto pt-2\">\n <SparklineChart data={data.sparklineData} width={120} height={32} color={sparklineColor} showFill />\n </div>\n ) : null}\n\n {/* Drilldown Indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-2 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default MetricCardWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"MetricCardWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx"],"sourcesContent":["/**\n * MetricCardWidget Component\n *\n * Displays a single KPI/metric with trend, comparison, and optional sparkline.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, MetricCardConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\nimport { ComparisonBadge } from './ComparisonBadge';\nimport { SparklineChart, type SparklineDataPoint } from './SparklineChart';\nimport { firstObjectRow, inferValueField, unwrapRows } from '../utils/inferFields';\nimport { resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MetricCardWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Fetched data */\n data?: MetricCardData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives metric data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MetricCardData {\n /** Primary value to display */\n value: number | string;\n /** Previous value for comparison */\n previousValue?: number;\n /** Percentage change from previous */\n changePercent?: number;\n /** Trend direction override */\n trend?: TrendDirection;\n /** Sparkline data points */\n sparklineData?: SparklineDataPoint[];\n /** Target value for comparison */\n target?: number;\n /** Unit label (e.g., \"users\", \"$\") */\n unit?: string;\n /** Whether the metric is a currency */\n isCurrency?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Format a number based on configuration\n */\nfunction formatValue(value: number | string, config?: MetricCardConfig): string {\n if (typeof value === 'string') return value;\n\n const { format = 'number', prefix, suffix } = config || {};\n\n let formatted: string;\n\n switch (format) {\n case 'currency':\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(value);\n break;\n\n case 'percentage':\n formatted = `${(value * 100).toFixed(1)}%`;\n break;\n\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n break;\n\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n\n // Apply prefix/suffix if provided\n if (prefix) formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n\n return formatted;\n}\n\n/**\n * Calculate trend direction from change percent\n */\nfunction calculateTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n/**\n * Get sparkline color based on trend\n */\nfunction getSparklineColor(trend: TrendDirection, positiveIsGood: boolean): 'success' | 'error' | 'muted' {\n if (trend === 'flat') return 'muted';\n const isGood = positiveIsGood ? trend === 'up' : trend === 'down';\n return isGood ? 'success' : 'error';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MetricCardWidget: FC<MetricCardWidgetProps> = memo(function MetricCardWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as MetricCardConfig | undefined;\n\n // Normalize data - extract value from object if valueField is configured\n const data = useMemo((): MetricCardData | undefined => {\n if (!rawData) return undefined;\n\n // Helper to get nested value using dot notation (e.g., \"summary.totalSales\")\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {\n return path.split('.').reduce((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj as unknown);\n };\n\n // If data already has 'value' property, use it directly\n if ('value' in rawData && rawData.value !== undefined) {\n return rawData as MetricCardData;\n }\n\n // If valueField is configured, extract the value from that field (supports dot notation)\n const valueField = (config as unknown as Record<string, unknown> | undefined)?.valueField as string | undefined;\n if (valueField && typeof rawData === 'object') {\n const extractedValue = getNestedValue(rawData as unknown as Record<string, unknown>, valueField);\n if (extractedValue !== undefined) {\n // Also try to extract previousValue if previousValueField is configured\n const previousValueField = (config as unknown as Record<string, unknown> | undefined)?.previousValueField as\n string | undefined;\n const previousValue = previousValueField\n ? (getNestedValue(rawData as unknown as Record<string, unknown>, previousValueField) as number | undefined)\n : undefined;\n\n return {\n value: extractedValue as number | string,\n previousValue,\n changePercent:\n previousValue !== undefined && typeof extractedValue === 'number'\n ? ((extractedValue - previousValue) / previousValue) * 100\n : undefined,\n };\n }\n }\n\n // Try common field names at top level\n const commonFields = ['value', 'total', 'amount', 'count', 'sum', 'totalSales', 'totalRevenue', 'totalOrders'];\n for (const field of commonFields) {\n if (typeof rawData === 'object' && field in rawData) {\n const val = (rawData as unknown as Record<string, unknown>)[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n // Try looking in nested 'summary' object (common pattern)\n if (typeof rawData === 'object' && 'summary' in rawData) {\n const summary = (rawData as Record<string, unknown>).summary as Record<string, unknown> | undefined;\n if (summary && typeof summary === 'object') {\n for (const field of commonFields) {\n if (field in summary) {\n const val = summary[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n }\n }\n\n // If rawData is a number or string directly, use it\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // BOFF-2246 smart default: if rawData is an array (or envelope around an\n // array) with object rows, use the configured valueField if it resolves\n // on any row (supports dotted paths like \"metrics.revenue\") — otherwise\n // pick the first numeric field from row[0]. This path only fires when\n // the earlier top-level extraction didn't find anything, but an explicit\n // `config.valueField` (flat or dotted) must always win over inference.\n const firstRow = firstObjectRow(rawData);\n if (firstRow) {\n const configuredField = (config as unknown as Record<string, unknown> | undefined)?.valueField as\n string | undefined;\n if (configuredField) {\n // Configured field is authoritative. Scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column\n // from row[0] (that would silently misrepresent the user's mapping).\n const rows = unwrapRows(rawData) ?? [firstRow];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolved = getNestedValue(r as Record<string, unknown>, configuredField);\n if (typeof resolved === 'number' || typeof resolved === 'string') {\n return { value: resolved };\n }\n }\n }\n return undefined;\n }\n const inferredField = inferValueField(firstRow);\n if (inferredField) {\n const val = firstRow[inferredField];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n return undefined;\n }, [rawData, config]);\n\n // Calculate derived values\n const trend = useMemo(() => {\n return data?.trend || calculateTrend(data?.changePercent);\n }, [data?.trend, data?.changePercent]);\n\n const positiveIsGood = config?.positiveIsGood ?? true;\n\n const formattedValue = useMemo(() => {\n // Treat 0 / \"\" as real values — only a missing value renders the em dash.\n // (`if (!data?.value)` would wrongly blank out a legitimate 0, e.g. an ED\n // boarding-compliance of 0% or an available-bed count of 0.)\n if (data?.value === undefined || data?.value === null) return '—';\n return formatValue(data.value, config);\n }, [data?.value, config]);\n\n const sparklineColor = useMemo(() => {\n return getSparklineColor(trend, positiveIsGood);\n }, [trend, positiveIsGood]);\n\n // Conditional formatting on the metric value (threshold-driven color, not\n // just trend direction). Falls back to the default primary text color.\n const valueConditionalClass = useMemo(() => {\n const style = resolveConditionalStyle(data?.value, config?.conditionalRules);\n return conditionalStyleToClassName(style);\n }, [data?.value, config?.conditionalRules]);\n\n // Handle click\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown) {\n onDrilldown({\n value: data?.value,\n changePercent: data?.changePercent,\n trend,\n target: data?.target,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, trend]);\n\n // Determine if clickable\n const isClickable = Boolean(onDrilldown || (onClick && widget.drilldowns?.length));\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Subtitle/Label */}\n {config?.subtitle && <p className=\"text-xs text-text-secondary mb-1 truncate\">{config.subtitle}</p>}\n\n {/* Main Value */}\n <div className=\"flex items-baseline gap-2 mb-2\">\n <span\n className={`text-3xl font-semibold tracking-tight ${valueConditionalClass || 'text-text-primary'}`}\n title={String(data?.value)}\n >\n {formattedValue}\n </span>\n\n {/* Trend Indicator (inline) */}\n {config?.showTrend !== false && data?.changePercent !== undefined && (\n <TrendIndicator direction={trend} size=\"md\" positiveIsGood={positiveIsGood} />\n )}\n </div>\n\n {/* Comparison Badge */}\n {config?.showComparison !== false && data?.changePercent !== undefined && (\n <div className=\"mb-3\">\n <ComparisonBadge\n value={data.changePercent}\n label={config?.comparisonLabel}\n positiveIsGood={positiveIsGood}\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Target Progress */}\n {data?.target !== undefined && typeof data.value === 'number' && (\n <div className=\"mb-3\">\n <div className=\"flex justify-between text-xs text-text-secondary mb-1\">\n <span>Target: {formatValue(data.target, config)}</span>\n <span>{((data.value / data.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1.5 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${\n data.value >= data.target ? 'bg-state-success' : 'bg-action-primary-bg'\n }`}\n style={{\n width: `${Math.min((data.value / data.target) * 100, 100)}%`,\n }}\n />\n </div>\n </div>\n )}\n\n {/* Sparkline */}\n {config?.showSparkline !== false && data?.sparklineData?.length ? (\n <div className=\"mt-auto pt-2\">\n <SparklineChart data={data.sparklineData} width={120} height={32} color={sparklineColor} showFill />\n </div>\n ) : null}\n\n {/* Drilldown Indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-2 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default MetricCardWidget;\n"],"mappings":";;;;;;;;AAuDA,SAAS,EAAY,GAAwB,GAAmC;AAC9E,KAAI,OAAO,KAAU,SAAU,QAAO;CAEtC,IAAM,EAAE,YAAS,UAAU,WAAQ,cAAW,KAAU,EAAE,EAEtD;AAEJ,SAAQ,GAAR;EACE,KAAK;AACH,OAAY,IAAI,KAAK,aAAa,SAAS;IACzC,OAAO;IACP,UAAU;IACV,uBAAuB;IACvB,uBAAuB;IACxB,CAAC,CAAC,OAAO,EAAM;AAChB;EAEF,KAAK;AACH,OAAY,IAAI,IAAQ,KAAK,QAAQ,EAAE,CAAC;AACxC;EAEF,KAAK;AACH,OAAY,IAAI,KAAK,aAAa,SAAS;IACzC,UAAU;IACV,uBAAuB;IACxB,CAAC,CAAC,OAAO,EAAM;AAChB;EAGF,QACE,KAAY,IAAI,KAAK,aAAa,SAAS,EACzC,uBAAuB,GACxB,CAAC,CAAC,OAAO,EAAM;;AAOpB,QAHI,MAAQ,IAAY,GAAG,IAAS,MAChC,MAAQ,IAAY,GAAG,IAAY,MAEhC;;AAMT,SAAS,EAAe,GAAwC;AAE9D,QADI,MAAkB,KAAA,KAAa,MAAkB,IAAU,SACxD,IAAgB,IAAI,OAAO;;AAMpC,SAAS,EAAkB,GAAuB,GAAwD;AAGxG,QAFI,MAAU,SAAe,WACd,IAAiB,MAAU,OAAO,MAAU,UAC3C,YAAY;;AAO9B,IAAa,IAA8C,EAAK,SAA0B,EACxF,WACA,MAAM,GACN,YACA,kBACC;CACD,IAAM,IAAS,EAAO,QAGhB,IAAO,QAA0C;AACrD,MAAI,CAAC,EAAS;EAGd,IAAM,KAAkB,GAA8B,MAC7C,EAAK,MAAM,IAAI,CAAC,QAAQ,GAAS,MAC/B,KAAW,OAAO,KAAY,WAAY,EAAoC,KAAO,KAAA,GAC3F,EAAe;AAIpB,MAAI,WAAW,KAAW,EAAQ,UAAU,KAAA,EAC1C,QAAO;EAIT,IAAM,IAAc,GAA2D;AAC/E,MAAI,KAAc,OAAO,KAAY,UAAU;GAC7C,IAAM,IAAiB,EAAe,GAA+C,EAAW;AAChG,OAAI,MAAmB,KAAA,GAAW;IAEhC,IAAM,IAAsB,GAA2D,oBAEjF,IAAgB,IACjB,EAAe,GAA+C,EAAmB,GAClF,KAAA;AAEJ,WAAO;KACL,OAAO;KACP;KACA,eACE,MAAkB,KAAA,KAAa,OAAO,KAAmB,YACnD,IAAiB,KAAiB,IAAiB,MACrD,KAAA;KACP;;;EAKL,IAAM,IAAe;GAAC;GAAS;GAAS;GAAU;GAAS;GAAO;GAAc;GAAgB;GAAc;AAC9G,OAAK,IAAM,KAAS,EAClB,KAAI,OAAO,KAAY,YAAY,KAAS,GAAS;GACnD,IAAM,IAAO,EAA+C;AAC5D,OAAI,OAAO,KAAQ,YAAY,OAAO,KAAQ,SAC5C,QAAO,EAAE,OAAO,GAAK;;AAM3B,MAAI,OAAO,KAAY,YAAY,aAAa,GAAS;GACvD,IAAM,IAAW,EAAoC;AACrD,OAAI,KAAW,OAAO,KAAY;SAC3B,IAAM,KAAS,EAClB,KAAI,KAAS,GAAS;KACpB,IAAM,IAAM,EAAQ;AACpB,SAAI,OAAO,KAAQ,YAAY,OAAO,KAAQ,SAC5C,QAAO,EAAE,OAAO,GAAK;;;;AAQ/B,MAAI,OAAO,KAAY,YAAY,OAAO,KAAY,SACpD,QAAO,EAAE,OAAO,GAAS;EAS3B,IAAM,IAAW,EAAe,EAAQ;AACxC,MAAI,GAAU;GACZ,IAAM,IAAmB,GAA2D;AAEpF,OAAI,GAAiB;IAMnB,IAAM,IAAO,EAAW,EAAQ,IAAI,CAAC,EAAS;AAC9C,SAAK,IAAM,KAAK,EACd,KAAI,KAAK,OAAO,KAAM,UAAU;KAC9B,IAAM,IAAW,EAAe,GAA8B,EAAgB;AAC9E,SAAI,OAAO,KAAa,YAAY,OAAO,KAAa,SACtD,QAAO,EAAE,OAAO,GAAU;;AAIhC;;GAEF,IAAM,IAAgB,EAAgB,EAAS;AAC/C,OAAI,GAAe;IACjB,IAAM,IAAM,EAAS;AACrB,QAAI,OAAO,KAAQ,YAAY,OAAO,KAAQ,SAC5C,QAAO,EAAE,OAAO,GAAK;;;IAM1B,CAAC,GAAS,EAAO,CAAC,EAGf,IAAQ,QACL,GAAM,SAAS,EAAe,GAAM,cAAc,EACxD,CAAC,GAAM,OAAO,GAAM,cAAc,CAAC,EAEhC,IAAiB,GAAQ,kBAAkB,IAE3C,IAAiB,QAIjB,GAAM,UAAU,KAAA,KAAa,GAAM,UAAU,OAAa,MACvD,EAAY,EAAK,OAAO,EAAO,EACrC,CAAC,GAAM,OAAO,EAAO,CAAC,EAEnB,IAAiB,QACd,EAAkB,GAAO,EAAe,EAC9C,CAAC,GAAO,EAAe,CAAC,EAIrB,IAAwB,QAErB,EADO,EAAwB,GAAM,OAAO,GAAQ,iBAAiB,CACnC,EACxC,CAAC,GAAM,OAAO,GAAQ,iBAAiB,CAAC,EAGrC,IAAc,QAAkB;AAEpC,EAAI,IACF,EAAY;GACV,OAAO,GAAM;GACb,eAAe,GAAM;GACrB;GACA,QAAQ,GAAM;GACf,CAAC,GACO,KAAW,EAAO,YAAY,UACvC,GAAS;IAEV;EAAC;EAAS;EAAa,EAAO;EAAY;EAAM;EAAM,CAAC,EAGpD,IAAc,GAAQ,KAAgB,KAAW,EAAO,YAAY;AAE1E,QACE,kBAAC,OAAD;EACE,WAAW;;YAEL,IAAc,2DAA2D,GAAG;;EAElF,SAAS;EACT,MAAM,IAAc,WAAW,KAAA;EAC/B,UAAU,IAAc,IAAI,KAAA;EAC5B,YAAY,MAAM;AAChB,GAAI,MAAgB,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjD,EAAE,gBAAgB,EAClB,GAAa;;YAXnB;GAgBG,GAAQ,YAAY,kBAAC,KAAD;IAAG,WAAU;cAA6C,EAAO;IAAa,CAAA;GAGnG,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KACE,WAAW,yCAAyC,KAAyB;KAC7E,OAAO,OAAO,GAAM,MAAM;eAEzB;KACI,CAAA,EAGN,GAAQ,cAAc,MAAS,GAAM,kBAAkB,KAAA,KACtD,kBAAC,GAAD;KAAgB,WAAW;KAAO,MAAK;KAAqB;KAAkB,CAAA,CAE5E;;GAGL,GAAQ,mBAAmB,MAAS,GAAM,kBAAkB,KAAA,KAC3D,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,OAAO,EAAK;KACZ,OAAO,GAAQ;KACC;KAChB,MAAK;KACL,CAAA;IACE,CAAA;GAIP,GAAM,WAAW,KAAA,KAAa,OAAO,EAAK,SAAU,YACnD,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD,EAAA,UAAA,CAAM,YAAS,EAAY,EAAK,QAAQ,EAAO,CAAQ,EAAA,CAAA,EACvD,kBAAC,QAAD,EAAA,UAAA,EAAS,EAAK,QAAQ,EAAK,SAAU,KAAK,QAAQ,EAAE,EAAC,IAAQ,EAAA,CAAA,CACzD;QACN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MACE,WAAW,sCACT,EAAK,SAAS,EAAK,SAAS,qBAAqB;MAEnD,OAAO,EACL,OAAO,GAAG,KAAK,IAAK,EAAK,QAAQ,EAAK,SAAU,KAAK,IAAI,CAAC,IAC3D;MACD,CAAA;KACE,CAAA,CACF;;GAIP,GAAQ,kBAAkB,MAAS,GAAM,eAAe,SACvD,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAgB,MAAM,EAAK;KAAe,OAAO;KAAK,QAAQ;KAAI,OAAO;KAAgB,UAAA;KAAW,CAAA;IAChG,CAAA,GACJ;GAGH,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAAsE,gBAEpE,kBAAC,OAAD;MAAK,WAAU;MAAU,MAAK;MAAO,QAAO;MAAe,SAAQ;gBACjE,kBAAC,QAAD;OAAM,eAAc;OAAQ,gBAAe;OAAQ,aAAa;OAAG,GAAE;OAAiB,CAAA;MAClF,CAAA,CACD;;IACH,CAAA;GAEJ;;EAER"}
@@ -56,7 +56,7 @@ function l({ dataSinkId: l, workspaceId: u, enabled: d = !0, onUpdate: f, onWork
56
56
  type: e,
57
57
  dataSink: i,
58
58
  timestamp: n,
59
- userId: r
59
+ actorId: r
60
60
  };
61
61
  switch (h(new Date(n)), e) {
62
62
  case "CREATED":
@@ -96,7 +96,7 @@ function l({ dataSinkId: l, workspaceId: u, enabled: d = !0, onUpdate: f, onWork
96
96
  onData: ({ data: e }) => {
97
97
  if (e.data?.dataSinkUpdated) {
98
98
  let t = e.data.dataSinkUpdated;
99
- S(t.type, t.dataSink, t.timestamp, t.userId);
99
+ S(t.type, t.dataSink, t.timestamp, t.actorId);
100
100
  }
101
101
  },
102
102
  onError: (e) => {
@@ -1 +1 @@
1
- {"version":3,"file":"useDataSinkSubscription.js","names":[],"sources":["../../../src/bigconsole/hooks/useDataSinkSubscription.ts"],"sourcesContent":["/**\n * DataSink Subscription Hook\n *\n * Custom hook for subscribing to DataSink updates via GraphQL subscriptions.\n * Supports both sink-level (individual) and workspace-level (all sinks) subscriptions.\n *\n * Note: Subscriptions require WebSocket transport in Apollo Client.\n * In standalone dev mode (HTTP-only), subscriptions are gracefully skipped.\n */\n\nimport { useState, useCallback, useEffect } from 'react';\nimport { useSubscription } from '@apollo/client/react';\nimport { useDataSinkStore } from '../store/dataSinkStore';\nimport { DataSinkDataUpdatedDocument, DataSinkUpdatedDocument } from '../../generated/wspace-operations';\nimport type { DataSink } from '../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface UseDataSinkSubscriptionOptions {\n /** Specific DataSink ID to subscribe to (sink-level updates) */\n dataSinkId?: string;\n /** Workspace ID for workspace-level events */\n workspaceId?: string;\n /** Whether subscriptions are enabled */\n enabled?: boolean;\n /** Callback when sink-level update received */\n onUpdate?: (dataSink: DataSink) => void;\n /** Callback when workspace-level event received */\n onWorkspaceEvent?: (event: DataSinkWorkspaceEvent) => void;\n}\n\ninterface DataSinkWorkspaceEvent {\n type: 'CREATED' | 'UPDATED' | 'REFRESHED' | 'DELETED';\n dataSink: DataSink;\n timestamp: string;\n userId?: string;\n}\n\ninterface UseDataSinkSubscriptionResult {\n /** Whether subscriptions are active */\n isSubscribed: boolean;\n /** Last update timestamp */\n lastUpdate: Date | null;\n /** Subscription error */\n error: Error | null;\n}\n\n// Subscription Result Types\ninterface DataSinkDataUpdatedResult {\n dataSinkDataUpdated?: Record<string, unknown>;\n}\n\ninterface DataSinkUpdatedResult {\n dataSinkUpdated?: {\n type: string;\n dataSink: Record<string, unknown>;\n timestamp: string;\n userId?: string;\n };\n}\n\n// ============================================================================\n// Helper: Check if error is a known subscription transport issue\n// ============================================================================\n\n/**\n * Detects errors caused by missing WebSocket transport (e.g., standalone dev mode).\n * These are expected and should not be treated as application errors.\n */\nfunction isSubscriptionTransportError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return (\n message.includes('Async Iterable') ||\n message.includes('Observable') ||\n message.includes('WebSocket') ||\n message.includes('Subscription field must return')\n );\n}\n\n// ============================================================================\n// Helper: Map API response to DataSink type\n// ============================================================================\n\nfunction mapToDataSink(data: Record<string, unknown>): DataSink {\n return {\n id: data.id as string,\n workspaceId: data.workspaceId as string,\n tenantId: data.tenantId as string,\n name: (data.name as string) || null,\n description: (data.description as string) || null,\n key: data.key as string,\n fluidGridsWorkflowId: (data.fluidGridsWorkflowId as string) || null,\n fluidGridsWorkflowExecutionId: (data.fluidGridsWorkflowExecutionId as string) || null,\n config: (data.config as Record<string, unknown>) || null,\n data: (data.data as Record<string, unknown>) || {},\n dataStats: (data.dataStats as DataSink['dataStats']) || null,\n lastRefreshedAt: (data.lastRefreshedAt as string) || new Date().toISOString(),\n lastRefreshedBy: (data.lastRefreshedBy as string) || null,\n expiresAt: (data.expiresAt as string) || null,\n nextRefreshAt: (data.nextRefreshAt as string) || null,\n ttl: (data.ttl as number) ?? 0,\n version: (data.version as number) ?? 1,\n sizeBytes: (data.sizeBytes as number) ?? 0,\n isActive: (data.isActive as boolean) ?? true,\n fieldMaskingEnabled: (data.fieldMaskingEnabled as boolean) ?? false,\n fieldMaskingRules: (data.fieldMaskingRules as Record<string, unknown>) || null,\n status: (data.status as DataSink['status']) || 'ready',\n statusChangedAt: (data.statusChangedAt as string) || null,\n statusChangedBy: (data.statusChangedBy as string) || null,\n setupState: (data.setupState as DataSink['setupState']) || 'not_started',\n refresh: (data.refresh as DataSink['refresh']) || null,\n exports: (data.exports as DataSink['exports']) || null,\n alerts: (data.alerts as DataSink['alerts']) || null,\n quota: (data.quota as DataSink['quota']) || null,\n metadata: (data.metadata as Record<string, unknown>) || null,\n createdAt: data.createdAt as string,\n createdBy: (data.createdBy as string) || '',\n updatedAt: data.updatedAt as string,\n updatedBy: (data.updatedBy as string) || '',\n deletedAt: (data.deletedAt as string) || null,\n deletedBy: (data.deletedBy as string) || null,\n };\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useDataSinkSubscription({\n dataSinkId,\n workspaceId,\n enabled = true,\n onUpdate,\n onWorkspaceEvent,\n}: UseDataSinkSubscriptionOptions): UseDataSinkSubscriptionResult {\n const [lastUpdate, setLastUpdate] = useState<Date | null>(null);\n const [subscriptionError, setSubscriptionError] = useState<Error | null>(null);\n\n // Store actions\n const updateDataSink = useDataSinkStore((state) => state.updateDataSink);\n const addDataSink = useDataSinkStore((state) => state.addDataSink);\n const removeDataSink = useDataSinkStore((state) => state.removeDataSink);\n\n // Handle sink-level data update\n const handleSinkDataUpdate = useCallback(\n (data: Record<string, unknown>) => {\n const dataSink = mapToDataSink(data);\n updateDataSink(dataSink.id, dataSink);\n setLastUpdate(new Date());\n onUpdate?.(dataSink);\n },\n [updateDataSink, onUpdate]\n );\n\n // Handle workspace-level event\n const handleWorkspaceEvent = useCallback(\n (eventType: string, data: Record<string, unknown>, timestamp: string, userId?: string) => {\n const dataSink = mapToDataSink(data);\n const event: DataSinkWorkspaceEvent = {\n type: eventType as DataSinkWorkspaceEvent['type'],\n dataSink,\n timestamp,\n userId,\n };\n\n setLastUpdate(new Date(timestamp));\n\n switch (eventType) {\n case 'CREATED':\n addDataSink(dataSink);\n break;\n case 'UPDATED':\n case 'REFRESHED':\n updateDataSink(dataSink.id, dataSink);\n break;\n case 'DELETED':\n removeDataSink(dataSink.id);\n break;\n default:\n }\n\n onWorkspaceEvent?.(event);\n },\n [addDataSink, updateDataSink, removeDataSink, onWorkspaceEvent]\n );\n\n // Sink-level subscription (for specific DataSink updates)\n const { loading: sinkLoading, error: sinkError } = useSubscription<DataSinkDataUpdatedResult>(\n DataSinkDataUpdatedDocument,\n {\n variables: { id: dataSinkId },\n skip: !enabled || !dataSinkId,\n onData: ({ data }) => {\n if (data.data?.dataSinkDataUpdated) {\n handleSinkDataUpdate(data.data.dataSinkDataUpdated);\n }\n },\n onError: (error) => {\n if (isSubscriptionTransportError(error)) {\n console.debug('[useDataSinkSubscription] Sink-level subscription not available (no WebSocket transport)');\n return;\n }\n setSubscriptionError(error instanceof Error ? error : new Error('Subscription error'));\n },\n }\n );\n\n // Workspace-level subscription (for all DataSink events in workspace)\n const { loading: workspaceLoading, error: workspaceError } = useSubscription<DataSinkUpdatedResult>(\n DataSinkUpdatedDocument,\n {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !enabled || !workspaceId,\n onData: ({ data }) => {\n if (data.data?.dataSinkUpdated) {\n const event = data.data.dataSinkUpdated;\n handleWorkspaceEvent(event.type, event.dataSink, event.timestamp, event.userId);\n }\n },\n onError: (error) => {\n if (isSubscriptionTransportError(error)) {\n console.debug(\n '[useDataSinkSubscription] Workspace-level subscription not available (no WebSocket transport)'\n );\n return;\n }\n setSubscriptionError(error instanceof Error ? error : new Error('Subscription error'));\n },\n }\n );\n\n // Log subscription status\n useEffect(() => {\n if (enabled && dataSinkId) {\n console.debug('[useDataSinkSubscription] Sink-level subscription active:', dataSinkId);\n }\n if (enabled && workspaceId) {\n console.debug('[useDataSinkSubscription] Workspace-level subscription active:', workspaceId);\n }\n }, [enabled, dataSinkId, workspaceId]);\n\n // Combined error (filter out known transport errors)\n const rawError = sinkError || workspaceError || subscriptionError;\n const error = rawError && !isSubscriptionTransportError(rawError) ? rawError : null;\n\n useEffect(() => {\n if (error) {\n }\n }, [error]);\n\n // Determine if subscriptions are active\n const isSubscribed = !!(enabled && ((dataSinkId && !sinkLoading) || (workspaceId && !workspaceLoading)));\n\n return {\n isSubscribed,\n lastUpdate,\n error,\n };\n}\n\nexport default useDataSinkSubscription;\n"],"mappings":"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"}
1
+ {"version":3,"file":"useDataSinkSubscription.js","names":[],"sources":["../../../src/bigconsole/hooks/useDataSinkSubscription.ts"],"sourcesContent":["/**\n * DataSink Subscription Hook\n *\n * Custom hook for subscribing to DataSink updates via GraphQL subscriptions.\n * Supports both sink-level (individual) and workspace-level (all sinks) subscriptions.\n *\n * Note: Subscriptions require WebSocket transport in Apollo Client.\n * In standalone dev mode (HTTP-only), subscriptions are gracefully skipped.\n */\n\nimport { useState, useCallback, useEffect } from 'react';\nimport { useSubscription } from '@apollo/client/react';\nimport { useDataSinkStore } from '../store/dataSinkStore';\nimport { DataSinkDataUpdatedDocument, DataSinkUpdatedDocument } from '../../generated/wspace-operations';\nimport type { DataSink } from '../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface UseDataSinkSubscriptionOptions {\n /** Specific DataSink ID to subscribe to (sink-level updates) */\n dataSinkId?: string;\n /** Workspace ID for workspace-level events */\n workspaceId?: string;\n /** Whether subscriptions are enabled */\n enabled?: boolean;\n /** Callback when sink-level update received */\n onUpdate?: (dataSink: DataSink) => void;\n /** Callback when workspace-level event received */\n onWorkspaceEvent?: (event: DataSinkWorkspaceEvent) => void;\n}\n\ninterface DataSinkWorkspaceEvent {\n type: 'CREATED' | 'UPDATED' | 'REFRESHED' | 'DELETED';\n dataSink: DataSink;\n timestamp: string;\n actorId?: string;\n}\n\ninterface UseDataSinkSubscriptionResult {\n /** Whether subscriptions are active */\n isSubscribed: boolean;\n /** Last update timestamp */\n lastUpdate: Date | null;\n /** Subscription error */\n error: Error | null;\n}\n\n// Subscription Result Types\ninterface DataSinkDataUpdatedResult {\n dataSinkDataUpdated?: Record<string, unknown>;\n}\n\ninterface DataSinkUpdatedResult {\n dataSinkUpdated?: {\n type: string;\n dataSink: Record<string, unknown>;\n timestamp: string;\n actorId?: string;\n };\n}\n\n// ============================================================================\n// Helper: Check if error is a known subscription transport issue\n// ============================================================================\n\n/**\n * Detects errors caused by missing WebSocket transport (e.g., standalone dev mode).\n * These are expected and should not be treated as application errors.\n */\nfunction isSubscriptionTransportError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return (\n message.includes('Async Iterable') ||\n message.includes('Observable') ||\n message.includes('WebSocket') ||\n message.includes('Subscription field must return')\n );\n}\n\n// ============================================================================\n// Helper: Map API response to DataSink type\n// ============================================================================\n\nfunction mapToDataSink(data: Record<string, unknown>): DataSink {\n return {\n id: data.id as string,\n workspaceId: data.workspaceId as string,\n tenantId: data.tenantId as string,\n name: (data.name as string) || null,\n description: (data.description as string) || null,\n key: data.key as string,\n fluidGridsWorkflowId: (data.fluidGridsWorkflowId as string) || null,\n fluidGridsWorkflowExecutionId: (data.fluidGridsWorkflowExecutionId as string) || null,\n config: (data.config as Record<string, unknown>) || null,\n data: (data.data as Record<string, unknown>) || {},\n dataStats: (data.dataStats as DataSink['dataStats']) || null,\n lastRefreshedAt: (data.lastRefreshedAt as string) || new Date().toISOString(),\n lastRefreshedBy: (data.lastRefreshedBy as string) || null,\n expiresAt: (data.expiresAt as string) || null,\n nextRefreshAt: (data.nextRefreshAt as string) || null,\n ttl: (data.ttl as number) ?? 0,\n version: (data.version as number) ?? 1,\n sizeBytes: (data.sizeBytes as number) ?? 0,\n isActive: (data.isActive as boolean) ?? true,\n fieldMaskingEnabled: (data.fieldMaskingEnabled as boolean) ?? false,\n fieldMaskingRules: (data.fieldMaskingRules as Record<string, unknown>) || null,\n status: (data.status as DataSink['status']) || 'ready',\n statusChangedAt: (data.statusChangedAt as string) || null,\n statusChangedBy: (data.statusChangedBy as string) || null,\n setupState: (data.setupState as DataSink['setupState']) || 'not_started',\n refresh: (data.refresh as DataSink['refresh']) || null,\n exports: (data.exports as DataSink['exports']) || null,\n alerts: (data.alerts as DataSink['alerts']) || null,\n quota: (data.quota as DataSink['quota']) || null,\n metadata: (data.metadata as Record<string, unknown>) || null,\n createdAt: data.createdAt as string,\n createdBy: (data.createdBy as string) || '',\n updatedAt: data.updatedAt as string,\n updatedBy: (data.updatedBy as string) || '',\n deletedAt: (data.deletedAt as string) || null,\n deletedBy: (data.deletedBy as string) || null,\n };\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useDataSinkSubscription({\n dataSinkId,\n workspaceId,\n enabled = true,\n onUpdate,\n onWorkspaceEvent,\n}: UseDataSinkSubscriptionOptions): UseDataSinkSubscriptionResult {\n const [lastUpdate, setLastUpdate] = useState<Date | null>(null);\n const [subscriptionError, setSubscriptionError] = useState<Error | null>(null);\n\n // Store actions\n const updateDataSink = useDataSinkStore((state) => state.updateDataSink);\n const addDataSink = useDataSinkStore((state) => state.addDataSink);\n const removeDataSink = useDataSinkStore((state) => state.removeDataSink);\n\n // Handle sink-level data update\n const handleSinkDataUpdate = useCallback(\n (data: Record<string, unknown>) => {\n const dataSink = mapToDataSink(data);\n updateDataSink(dataSink.id, dataSink);\n setLastUpdate(new Date());\n onUpdate?.(dataSink);\n },\n [updateDataSink, onUpdate]\n );\n\n // Handle workspace-level event\n const handleWorkspaceEvent = useCallback(\n (eventType: string, data: Record<string, unknown>, timestamp: string, actorId?: string) => {\n const dataSink = mapToDataSink(data);\n const event: DataSinkWorkspaceEvent = {\n type: eventType as DataSinkWorkspaceEvent['type'],\n dataSink,\n timestamp,\n actorId,\n };\n\n setLastUpdate(new Date(timestamp));\n\n switch (eventType) {\n case 'CREATED':\n addDataSink(dataSink);\n break;\n case 'UPDATED':\n case 'REFRESHED':\n updateDataSink(dataSink.id, dataSink);\n break;\n case 'DELETED':\n removeDataSink(dataSink.id);\n break;\n default:\n }\n\n onWorkspaceEvent?.(event);\n },\n [addDataSink, updateDataSink, removeDataSink, onWorkspaceEvent]\n );\n\n // Sink-level subscription (for specific DataSink updates)\n const { loading: sinkLoading, error: sinkError } = useSubscription<DataSinkDataUpdatedResult>(\n DataSinkDataUpdatedDocument,\n {\n variables: { id: dataSinkId },\n skip: !enabled || !dataSinkId,\n onData: ({ data }) => {\n if (data.data?.dataSinkDataUpdated) {\n handleSinkDataUpdate(data.data.dataSinkDataUpdated);\n }\n },\n onError: (error) => {\n if (isSubscriptionTransportError(error)) {\n console.debug('[useDataSinkSubscription] Sink-level subscription not available (no WebSocket transport)');\n return;\n }\n setSubscriptionError(error instanceof Error ? error : new Error('Subscription error'));\n },\n }\n );\n\n // Workspace-level subscription (for all DataSink events in workspace)\n const { loading: workspaceLoading, error: workspaceError } = useSubscription<DataSinkUpdatedResult>(\n DataSinkUpdatedDocument,\n {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !enabled || !workspaceId,\n onData: ({ data }) => {\n if (data.data?.dataSinkUpdated) {\n const event = data.data.dataSinkUpdated;\n handleWorkspaceEvent(event.type, event.dataSink, event.timestamp, event.actorId);\n }\n },\n onError: (error) => {\n if (isSubscriptionTransportError(error)) {\n console.debug(\n '[useDataSinkSubscription] Workspace-level subscription not available (no WebSocket transport)'\n );\n return;\n }\n setSubscriptionError(error instanceof Error ? error : new Error('Subscription error'));\n },\n }\n );\n\n // Log subscription status\n useEffect(() => {\n if (enabled && dataSinkId) {\n console.debug('[useDataSinkSubscription] Sink-level subscription active:', dataSinkId);\n }\n if (enabled && workspaceId) {\n console.debug('[useDataSinkSubscription] Workspace-level subscription active:', workspaceId);\n }\n }, [enabled, dataSinkId, workspaceId]);\n\n // Combined error (filter out known transport errors)\n const rawError = sinkError || workspaceError || subscriptionError;\n const error = rawError && !isSubscriptionTransportError(rawError) ? rawError : null;\n\n useEffect(() => {\n if (error) {\n }\n }, [error]);\n\n // Determine if subscriptions are active\n const isSubscribed = !!(enabled && ((dataSinkId && !sinkLoading) || (workspaceId && !workspaceLoading)));\n\n return {\n isSubscribed,\n lastUpdate,\n error,\n };\n}\n\nexport default useDataSinkSubscription;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"WidgetDemoPage.js","names":[],"sources":["../../../src/bigconsole/pages/WidgetDemoPage.tsx"],"sourcesContent":["/**\n * WidgetDemoPage\n *\n * Demonstrates all 18 widget types.\n * Widgets show empty states when no real data is available.\n * Connect a DataSink + Parser pipeline for real data processing.\n */\n\nimport { type FC, useState, useMemo } from 'react';\nimport { IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport type { Widget } from '../types';\nimport { WidgetWrapper } from '../components/widgets';\n\n// Widget type string literals\ntype WidgetType =\n | 'metric_card'\n | 'kpi_card_comparison'\n | 'chart'\n | 'funnel_chart'\n | 'table'\n | 'pivot_table'\n | 'gauge'\n | 'progress'\n | 'list'\n | 'form'\n | 'text'\n | 'iframe'\n | 'map'\n | 'heatmap'\n | 'calendar'\n | 'kanban'\n | 'timeline'\n | 'custom';\n\n// ============================================================================\n// Widget Types for Demo\n// ============================================================================\n\nconst WIDGET_TYPES: { type: WidgetType; title: string; description: string }[] = [\n { type: 'metric_card', title: 'Metric Card', description: 'Single metric with trend and sparkline' },\n { type: 'kpi_card_comparison', title: 'KPI Card Comparison', description: 'Multiple KPIs with comparisons' },\n { type: 'chart', title: 'Chart (Line/Bar/Area)', description: 'Time-series data visualization' },\n { type: 'funnel_chart', title: 'Funnel Chart', description: 'Conversion funnel with stages' },\n { type: 'table', title: 'Table', description: 'Data table with sort/filter/pagination' },\n { type: 'pivot_table', title: 'Pivot Table', description: 'Multi-dimensional data analysis' },\n { type: 'gauge', title: 'Gauge', description: 'Circular/linear gauge with thresholds' },\n { type: 'progress', title: 'Progress', description: 'Progress bar with milestones' },\n { type: 'list', title: 'List', description: 'Scrollable list with items' },\n { type: 'form', title: 'Form (Adaptive Card)', description: 'Interactive form with inputs' },\n { type: 'text', title: 'Text (Markdown)', description: 'Rich text with markdown support' },\n { type: 'iframe', title: 'iFrame', description: 'Embedded external content' },\n { type: 'map', title: 'Map', description: 'Geographic data with markers' },\n { type: 'heatmap', title: 'Heatmap', description: 'Grid visualization with color intensity' },\n { type: 'calendar', title: 'Calendar', description: 'Event calendar view' },\n { type: 'kanban', title: 'Kanban', description: 'Task board with columns' },\n { type: 'timeline', title: 'Timeline (Gantt)', description: 'Project timeline with tasks' },\n { type: 'custom', title: 'Custom', description: 'User-defined widget rendering' },\n];\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const WidgetDemoPage: FC = () => {\n const [filter, setFilter] = useState('');\n const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');\n\n // Generate demo widgets from all types\n const demoWidgets = useMemo(() => {\n const filtered = filter\n ? WIDGET_TYPES.filter(\n (w) => w.type.includes(filter.toLowerCase()) || w.title.toLowerCase().includes(filter.toLowerCase())\n )\n : WIDGET_TYPES;\n\n return filtered.map(\n (widgetDef, index): Widget => ({\n id: `demo-${widgetDef.type}`,\n pageId: 'demo-page',\n dashboardId: 'demo-dashboard',\n type: widgetDef.type,\n title: widgetDef.title,\n // No data source - widgets show empty state until connected to DataSink\n config: {},\n position: {\n x: (index % 3) * 4,\n y: Math.floor(index / 3) * 4,\n width: 4,\n height: 4,\n },\n positionX: (index % 3) * 4,\n positionY: Math.floor(index / 3) * 4,\n positionWidth: 4,\n positionHeight: 4,\n refreshInterval: undefined,\n lastRefreshedAt: undefined,\n metadata: { description: widgetDef.description },\n context: {},\n translations: [],\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n })\n );\n }, [filter]);\n\n return (\n <div className=\"h-full flex flex-col bg-bg-canvas\">\n {/* Header */}\n <div className=\"flex-shrink-0 border-b border-border-default bg-bg-surface px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-xl font-semibold text-text-primary\">Widget Demo</h1>\n <p className=\"text-sm text-text-secondary mt-1\">All 18 widget types - connect a DataSink for real data</p>\n </div>\n <div className=\"flex items-center gap-4\">\n {/* Search Filter */}\n <div className=\"relative\">\n <input\n type=\"text\"\n placeholder=\"Filter widgets...\"\n value={filter}\n onChange={(e) => setFilter(e.target.value)}\n className=\"\n w-64 px-3 py-2 pl-[3.25rem]\n text-sm\n bg-bg-sunken\n border border-border-default\n rounded-lg\n text-text-primary\n placeholder:text-text-tertiary\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n <svg\n className=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-secondary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n />\n </svg>\n </div>\n\n {/* View Toggle */}\n <div className=\"flex items-center bg-bg-sunken rounded-lg p-1\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`\n px-3 py-1.5 text-sm font-medium rounded\n transition-colors\n ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }\n `}\n >\n Grid\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`\n px-3 py-1.5 text-sm font-medium rounded\n transition-colors\n ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }\n `}\n >\n List\n </button>\n </div>\n </div>\n </div>\n </div>\n\n {/* Widget Grid */}\n <div className=\"flex-1 overflow-auto p-6\">\n <PagePurpose className=\"mb-[var(--space-sectionGap)]\">\n This gallery previews every widget type BigConsole can render on a dashboard — metrics, charts, tables, maps,\n kanban boards and more. Use it to see what each widget looks like and pick the right one before wiring it to a\n real DataSink in the dashboard builder.\n </PagePurpose>\n\n <div\n className={\n viewMode === 'grid' ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6' : 'flex flex-col gap-4'\n }\n >\n {demoWidgets.map((widget) => {\n const widgetDef = WIDGET_TYPES.find((w) => w.type === widget.type);\n return (\n <div key={widget.id} className={viewMode === 'grid' ? 'h-80' : 'h-64'}>\n <div className=\"h-full flex flex-col\">\n {/* Widget Description */}\n <div className=\"flex-shrink-0 mb-2\">\n <span className=\"text-xs text-text-tertiary\">\n Type: <code className=\"font-mono text-action-primary-text\">{widget.type}</code>\n </span>\n {widgetDef && <span className=\"text-xs text-text-tertiary ml-2\">- {widgetDef.description}</span>}\n </div>\n {/* Widget */}\n <div className=\"flex-1 min-h-0\">\n <WidgetWrapper widget={widget} />\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Empty State */}\n {demoWidgets.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No widgets match your filter\"\n description=\"Try a different search term to preview a widget type.\"\n />\n )}\n </div>\n\n {/* Footer Stats */}\n <div className=\"flex-shrink-0 border-t border-border-default bg-bg-surface px-6 py-3\">\n <div className=\"flex items-center justify-between text-sm text-text-secondary\">\n <span>\n Showing {demoWidgets.length} of {WIDGET_TYPES.length} widget types\n </span>\n <span>Widgets show empty state until connected to DataSink + Parser</span>\n </div>\n </div>\n </div>\n );\n};\n\nexport default WidgetDemoPage;\n"],"mappings":"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"}
1
+ {"version":3,"file":"WidgetDemoPage.js","names":[],"sources":["../../../src/bigconsole/pages/WidgetDemoPage.tsx"],"sourcesContent":["/**\n * WidgetDemoPage\n *\n * Demonstrates all 18 widget types.\n * Widgets show empty states when no real data is available.\n * Connect a DataSink + Parser pipeline for real data processing.\n */\n\nimport { type FC, useState, useMemo } from 'react';\nimport { IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport type { Widget } from '../types';\nimport { WidgetWrapper } from '../components/widgets';\n\n// Widget type string literals\ntype WidgetType =\n | 'metric_card'\n | 'kpi_card_comparison'\n | 'chart'\n | 'funnel_chart'\n | 'table'\n | 'pivot_table'\n | 'gauge'\n | 'progress'\n | 'list'\n | 'form'\n | 'text'\n | 'iframe'\n | 'map'\n | 'heatmap'\n | 'calendar'\n | 'kanban'\n | 'timeline'\n | 'custom';\n\n// ============================================================================\n// Widget Types for Demo\n// ============================================================================\n\nconst WIDGET_TYPES: { type: WidgetType; title: string; description: string }[] = [\n { type: 'metric_card', title: 'Metric Card', description: 'Single metric with trend and sparkline' },\n { type: 'kpi_card_comparison', title: 'KPI Card Comparison', description: 'Multiple KPIs with comparisons' },\n { type: 'chart', title: 'Chart (Line/Bar/Area)', description: 'Time-series data visualization' },\n { type: 'funnel_chart', title: 'Funnel Chart', description: 'Conversion funnel with stages' },\n { type: 'table', title: 'Table', description: 'Data table with sort/filter/pagination' },\n { type: 'pivot_table', title: 'Pivot Table', description: 'Multi-dimensional data analysis' },\n { type: 'gauge', title: 'Gauge', description: 'Circular/linear gauge with thresholds' },\n { type: 'progress', title: 'Progress', description: 'Progress bar with milestones' },\n { type: 'list', title: 'List', description: 'Scrollable list with items' },\n { type: 'form', title: 'Form (Adaptive Card)', description: 'Interactive form with inputs' },\n { type: 'text', title: 'Text (Markdown)', description: 'Rich text with markdown support' },\n { type: 'iframe', title: 'iFrame', description: 'Embedded external content' },\n { type: 'map', title: 'Map', description: 'Geographic data with markers' },\n { type: 'heatmap', title: 'Heatmap', description: 'Grid visualization with color intensity' },\n { type: 'calendar', title: 'Calendar', description: 'Event calendar view' },\n { type: 'kanban', title: 'Kanban', description: 'Task board with columns' },\n { type: 'timeline', title: 'Timeline (Gantt)', description: 'Project timeline with tasks' },\n { type: 'custom', title: 'Custom', description: 'User-defined widget rendering' },\n];\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const WidgetDemoPage: FC = () => {\n const [filter, setFilter] = useState('');\n const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');\n\n // Generate demo widgets from all types\n const demoWidgets = useMemo(() => {\n const filtered = filter\n ? WIDGET_TYPES.filter(\n (w) => w.type.includes(filter.toLowerCase()) || w.title.toLowerCase().includes(filter.toLowerCase())\n )\n : WIDGET_TYPES;\n\n return filtered.map((widgetDef, index): Widget => ({\n id: `demo-${widgetDef.type}`,\n pageId: 'demo-page',\n dashboardId: 'demo-dashboard',\n type: widgetDef.type,\n title: widgetDef.title,\n // No data source - widgets show empty state until connected to DataSink\n config: {},\n position: {\n x: (index % 3) * 4,\n y: Math.floor(index / 3) * 4,\n width: 4,\n height: 4,\n },\n positionX: (index % 3) * 4,\n positionY: Math.floor(index / 3) * 4,\n positionWidth: 4,\n positionHeight: 4,\n refreshInterval: undefined,\n lastRefreshedAt: undefined,\n metadata: { description: widgetDef.description },\n context: {},\n translations: [],\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n }));\n }, [filter]);\n\n return (\n <div className=\"h-full flex flex-col bg-bg-canvas\">\n {/* Header */}\n <div className=\"flex-shrink-0 border-b border-border-default bg-bg-surface px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-xl font-semibold text-text-primary\">Widget Demo</h1>\n <p className=\"text-sm text-text-secondary mt-1\">All 18 widget types - connect a DataSink for real data</p>\n </div>\n <div className=\"flex items-center gap-4\">\n {/* Search Filter */}\n <div className=\"relative\">\n <input\n type=\"text\"\n placeholder=\"Filter widgets...\"\n value={filter}\n onChange={(e) => setFilter(e.target.value)}\n className=\"\n w-64 px-3 py-2 pl-[3.25rem]\n text-sm\n bg-bg-sunken\n border border-border-default\n rounded-lg\n text-text-primary\n placeholder:text-text-tertiary\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n <svg\n className=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-secondary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n />\n </svg>\n </div>\n\n {/* View Toggle */}\n <div className=\"flex items-center bg-bg-sunken rounded-lg p-1\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`\n px-3 py-1.5 text-sm font-medium rounded\n transition-colors\n ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }\n `}\n >\n Grid\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`\n px-3 py-1.5 text-sm font-medium rounded\n transition-colors\n ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }\n `}\n >\n List\n </button>\n </div>\n </div>\n </div>\n </div>\n\n {/* Widget Grid */}\n <div className=\"flex-1 overflow-auto p-6\">\n <PagePurpose className=\"mb-[var(--space-sectionGap)]\">\n This gallery previews every widget type BigConsole can render on a dashboard — metrics, charts, tables, maps,\n kanban boards and more. Use it to see what each widget looks like and pick the right one before wiring it to a\n real DataSink in the dashboard builder.\n </PagePurpose>\n\n <div\n className={\n viewMode === 'grid' ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6' : 'flex flex-col gap-4'\n }\n >\n {demoWidgets.map((widget) => {\n const widgetDef = WIDGET_TYPES.find((w) => w.type === widget.type);\n return (\n <div key={widget.id} className={viewMode === 'grid' ? 'h-80' : 'h-64'}>\n <div className=\"h-full flex flex-col\">\n {/* Widget Description */}\n <div className=\"flex-shrink-0 mb-2\">\n <span className=\"text-xs text-text-tertiary\">\n Type: <code className=\"font-mono text-action-primary-text\">{widget.type}</code>\n </span>\n {widgetDef && <span className=\"text-xs text-text-tertiary ml-2\">- {widgetDef.description}</span>}\n </div>\n {/* Widget */}\n <div className=\"flex-1 min-h-0\">\n <WidgetWrapper widget={widget} />\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Empty State */}\n {demoWidgets.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No widgets match your filter\"\n description=\"Try a different search term to preview a widget type.\"\n />\n )}\n </div>\n\n {/* Footer Stats */}\n <div className=\"flex-shrink-0 border-t border-border-default bg-bg-surface px-6 py-3\">\n <div className=\"flex items-center justify-between text-sm text-text-secondary\">\n <span>\n Showing {demoWidgets.length} of {WIDGET_TYPES.length} widget types\n </span>\n <span>Widgets show empty state until connected to DataSink + Parser</span>\n </div>\n </div>\n </div>\n );\n};\n\nexport default WidgetDemoPage;\n"],"mappings":"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"}