@burdenoff/microfe-bigconsole 2026.805.3 → 2026.812.1
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.
- package/dist/bigconsole/components/DrilldownDashboardRenderer.js +118 -106
- package/dist/bigconsole/components/DrilldownDashboardRenderer.js.map +1 -1
- package/dist/bigconsole/components/DrilldownModal.js +54 -50
- package/dist/bigconsole/components/DrilldownModal.js.map +1 -1
- package/dist/bigconsole/components/builder/BuilderCanvas.js +2 -2
- package/dist/bigconsole/components/builder/BuilderCanvas.js.map +1 -1
- package/dist/bigconsole/components/builder/BuilderTopbar.js +3 -3
- package/dist/bigconsole/components/builder/BuilderTopbar.js.map +1 -1
- package/dist/bigconsole/components/builder/inspector/AdvancedSection.js +24 -25
- package/dist/bigconsole/components/builder/inspector/AdvancedSection.js.map +1 -1
- package/dist/bigconsole/components/builder/inspector/AppearanceSection.js +40 -29
- package/dist/bigconsole/components/builder/inspector/AppearanceSection.js.map +1 -1
- package/dist/bigconsole/components/builder/inspector/DataBindingSection.js +135 -97
- package/dist/bigconsole/components/builder/inspector/DataBindingSection.js.map +1 -1
- package/dist/bigconsole/components/builder/inspector/GeneralSection.js +58 -44
- package/dist/bigconsole/components/builder/inspector/GeneralSection.js.map +1 -1
- package/dist/bigconsole/components/common/Section.js +70 -0
- package/dist/bigconsole/components/common/Section.js.map +1 -0
- package/dist/bigconsole/components/common/index.js +2 -0
- package/dist/bigconsole/components/common/sectionStyles.js +42 -0
- package/dist/bigconsole/components/common/sectionStyles.js.map +1 -0
- package/dist/bigconsole/components/dashboard/CreateDashboardDialog.js +209 -193
- package/dist/bigconsole/components/dashboard/CreateDashboardDialog.js.map +1 -1
- package/dist/bigconsole/components/dashboard/DashboardCanvas.js +213 -231
- package/dist/bigconsole/components/dashboard/DashboardCanvas.js.map +1 -1
- package/dist/bigconsole/components/dashboard/DashboardGrid.js +12 -18
- package/dist/bigconsole/components/dashboard/DashboardGrid.js.map +1 -1
- package/dist/bigconsole/components/dashboard/DashboardToolbar.js +109 -102
- package/dist/bigconsole/components/dashboard/DashboardToolbar.js.map +1 -1
- package/dist/bigconsole/components/dashboard/DrilldownBreadcrumb.js +81 -70
- package/dist/bigconsole/components/dashboard/DrilldownBreadcrumb.js.map +1 -1
- package/dist/bigconsole/components/dashboard/ExportDashboardDialog.js +125 -159
- package/dist/bigconsole/components/dashboard/ExportDashboardDialog.js.map +1 -1
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterChip.js +25 -23
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterChip.js.map +1 -1
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterEditor.js +3 -3
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterEditor.js.map +1 -1
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterPresetSelector.js +5 -3
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterPresetSelector.js.map +1 -1
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/GlobalFiltersBar.js +170 -284
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/GlobalFiltersBar.js.map +1 -1
- package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js +147 -151
- package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js.map +1 -1
- package/dist/bigconsole/components/dashboard/LiveBoardBanner.js +45 -53
- package/dist/bigconsole/components/dashboard/LiveBoardBanner.js.map +1 -1
- package/dist/bigconsole/components/dashboard/PageTabsManager/PageTabsManager.js +157 -148
- package/dist/bigconsole/components/dashboard/PageTabsManager/PageTabsManager.js.map +1 -1
- package/dist/bigconsole/components/dashboard/SavedViewsDropdown/SavedViewsDropdown.js +17 -8
- package/dist/bigconsole/components/dashboard/SavedViewsDropdown/SavedViewsDropdown.js.map +1 -1
- package/dist/bigconsole/components/dashboard/ShareDialog/ShareDialog.js +101 -95
- package/dist/bigconsole/components/dashboard/ShareDialog/ShareDialog.js.map +1 -1
- package/dist/bigconsole/components/dashboard/StoreTemplatePicker.js +20 -18
- package/dist/bigconsole/components/dashboard/StoreTemplatePicker.js.map +1 -1
- package/dist/bigconsole/components/dashboard/TemplateSetupBanner.js +26 -58
- package/dist/bigconsole/components/dashboard/TemplateSetupBanner.js.map +1 -1
- package/dist/bigconsole/components/dashboard/WidgetPalette.js +307 -231
- package/dist/bigconsole/components/dashboard/WidgetPalette.js.map +1 -1
- package/dist/bigconsole/components/datasink/CacheTTLSelector.js +29 -29
- package/dist/bigconsole/components/datasink/CacheTTLSelector.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkCard.js +78 -79
- package/dist/bigconsole/components/datasink/DataSinkCard.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkDataViewer.js +33 -34
- package/dist/bigconsole/components/datasink/DataSinkDataViewer.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js +79 -56
- package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkRefreshDialog.js +75 -65
- package/dist/bigconsole/components/datasink/DataSinkRefreshDialog.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkStatusBadge.js +1 -1
- package/dist/bigconsole/components/datasink/DataSinkStatusBadge.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkTableViewer.js +20 -19
- package/dist/bigconsole/components/datasink/DataSinkTableViewer.js.map +1 -1
- package/dist/bigconsole/components/datasink/ExportDataSinkDialog.js +127 -171
- package/dist/bigconsole/components/datasink/ExportDataSinkDialog.js.map +1 -1
- package/dist/bigconsole/components/datasink/ImportDataSinkDialog.js +263 -243
- package/dist/bigconsole/components/datasink/ImportDataSinkDialog.js.map +1 -1
- package/dist/bigconsole/components/datasink/LinkedParsersPanel.js +65 -66
- package/dist/bigconsole/components/datasink/LinkedParsersPanel.js.map +1 -1
- package/dist/bigconsole/components/datasink/LinkedWorkflowsPanel.js +121 -127
- package/dist/bigconsole/components/datasink/LinkedWorkflowsPanel.js.map +1 -1
- package/dist/bigconsole/components/datasink/RefreshTypeSelector.js +33 -26
- package/dist/bigconsole/components/datasink/RefreshTypeSelector.js.map +1 -1
- package/dist/bigconsole/components/datasink/SetupStateBadge.js +3 -3
- package/dist/bigconsole/components/datasink/SetupStateBadge.js.map +1 -1
- package/dist/bigconsole/components/datasink/WorkflowStatusPanel.js +95 -95
- package/dist/bigconsole/components/datasink/WorkflowStatusPanel.js.map +1 -1
- package/dist/bigconsole/components/parser/DataSinkSelector.js +154 -123
- package/dist/bigconsole/components/parser/DataSinkSelector.js.map +1 -1
- package/dist/bigconsole/components/parser/ExpressionEditor.js +3 -3
- package/dist/bigconsole/components/parser/ExpressionEditor.js.map +1 -1
- package/dist/bigconsole/components/parser/ImportParserDialog.js +235 -275
- package/dist/bigconsole/components/parser/ImportParserDialog.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserCard.js +133 -114
- package/dist/bigconsole/components/parser/ParserCard.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserSelector.js +191 -174
- package/dist/bigconsole/components/parser/ParserSelector.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserStatusBadge.js +2 -2
- package/dist/bigconsole/components/parser/ParserStatusBadge.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserTypeBadge.js +4 -4
- package/dist/bigconsole/components/parser/ParserTypeBadge.js.map +1 -1
- package/dist/bigconsole/components/pipeline/PipelineCard.js +134 -89
- package/dist/bigconsole/components/pipeline/PipelineCard.js.map +1 -1
- package/dist/bigconsole/components/pipeline/PipelineDeleteDialog.js +77 -64
- package/dist/bigconsole/components/pipeline/PipelineDeleteDialog.js.map +1 -1
- package/dist/bigconsole/components/pipeline/PipelineStatusBadge.js +7 -10
- package/dist/bigconsole/components/pipeline/PipelineStatusBadge.js.map +1 -1
- package/dist/bigconsole/components/preview/AiPreviewContent.js +31 -32
- package/dist/bigconsole/components/preview/AiPreviewContent.js.map +1 -1
- package/dist/bigconsole/components/preview/AiPreviewMiniPanel.js +152 -106
- package/dist/bigconsole/components/preview/AiPreviewMiniPanel.js.map +1 -1
- package/dist/bigconsole/components/preview/AiPreviewPanel.js +193 -152
- package/dist/bigconsole/components/preview/AiPreviewPanel.js.map +1 -1
- package/dist/bigconsole/components/preview/previewShared.js +41 -23
- package/dist/bigconsole/components/preview/previewShared.js.map +1 -1
- package/dist/bigconsole/components/widgets/ActionConfigDialog.js +342 -308
- package/dist/bigconsole/components/widgets/ActionConfigDialog.js.map +1 -1
- package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js +262 -238
- package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js.map +1 -1
- package/dist/bigconsole/components/widgets/PropertiesPanel.js +712 -803
- package/dist/bigconsole/components/widgets/PropertiesPanel.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetErrorBoundary.js +7 -5
- package/dist/bigconsole/components/widgets/WidgetErrorBoundary.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetHeader.js +7 -8
- package/dist/bigconsole/components/widgets/WidgetHeader.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetWrapper.js +8 -7
- package/dist/bigconsole/components/widgets/WidgetWrapper.js.map +1 -1
- package/dist/bigconsole/components/widgets/adaptive-card/AdaptiveCardWidget.js +1 -1
- package/dist/bigconsole/components/widgets/adaptive-card/AdaptiveCardWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js +2 -2
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/chart/ChartConfig.js +11 -5
- package/dist/bigconsole/components/widgets/chart/ChartConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js +19 -15
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js.map +1 -1
- package/dist/bigconsole/components/widgets/event-logs/EventLogViewer.js +154 -102
- package/dist/bigconsole/components/widgets/event-logs/EventLogViewer.js.map +1 -1
- package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js +28 -13
- package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js +1 -1
- package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/heatmap/HeatmapConfig.js +35 -24
- package/dist/bigconsole/components/widgets/heatmap/HeatmapConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/iframe-widget/IframeWidget.js +2 -2
- package/dist/bigconsole/components/widgets/iframe-widget/IframeWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +4 -4
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js +2 -2
- package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/list/ListConfig.js +8 -2
- package/dist/bigconsole/components/widgets/list/ListConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/list/ListItem.js +1 -1
- package/dist/bigconsole/components/widgets/list/ListItem.js.map +1 -1
- package/dist/bigconsole/components/widgets/map-widget/MapWidget.js +1 -1
- package/dist/bigconsole/components/widgets/map-widget/MapWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/MetricCardConfig.js +9 -3
- package/dist/bigconsole/components/widgets/metric-card/MetricCardConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js +3 -3
- package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/progress/ProgressConfig.js +34 -25
- package/dist/bigconsole/components/widgets/progress/ProgressConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/retention/RetentionWidget.js +3 -3
- package/dist/bigconsole/components/widgets/retention/RetentionWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/states/EmptyState.js +6 -4
- package/dist/bigconsole/components/widgets/states/EmptyState.js.map +1 -1
- package/dist/bigconsole/components/widgets/states/ErrorState.js +7 -5
- package/dist/bigconsole/components/widgets/states/ErrorState.js.map +1 -1
- package/dist/bigconsole/components/widgets/states/LoadingState.js +5 -4
- package/dist/bigconsole/components/widgets/states/LoadingState.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableConfig.js +10 -4
- package/dist/bigconsole/components/widgets/table/TableConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableHeader.js +1 -1
- package/dist/bigconsole/components/widgets/table/TableHeader.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableWidget.js +1 -1
- package/dist/bigconsole/components/widgets/table/TableWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/text-widget/TextWidget.js +1 -1
- package/dist/bigconsole/components/widgets/text-widget/TextWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js +3 -3
- package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/utils/conditionalFormat.js +2 -2
- package/dist/bigconsole/components/widgets/utils/conditionalFormat.js.map +1 -1
- package/dist/bigconsole/components/widgets/webhook-config/WebhookConfigPanel.js +112 -85
- package/dist/bigconsole/components/widgets/webhook-config/WebhookConfigPanel.js.map +1 -1
- package/dist/bigconsole/components/widgets/widget-actions/WidgetActions.js +28 -20
- package/dist/bigconsole/components/widgets/widget-actions/WidgetActions.js.map +1 -1
- package/dist/bigconsole/components/workflow/WorkflowCard.js +92 -77
- package/dist/bigconsole/components/workflow/WorkflowCard.js.map +1 -1
- package/dist/bigconsole/components/workflow/WorkflowDeleteDialog.js +70 -51
- package/dist/bigconsole/components/workflow/WorkflowDeleteDialog.js.map +1 -1
- package/dist/bigconsole/components/workflow/WorkflowStatusBadge.js +16 -17
- package/dist/bigconsole/components/workflow/WorkflowStatusBadge.js.map +1 -1
- package/dist/bigconsole/customWidget/CustomWidgetBuilderSheet.js +6 -4
- package/dist/bigconsole/customWidget/CustomWidgetBuilderSheet.js.map +1 -1
- package/dist/bigconsole/customWidget/CustomWidgetProposalPanel.js +360 -338
- package/dist/bigconsole/customWidget/CustomWidgetProposalPanel.js.map +1 -1
- package/dist/bigconsole/hooks/useModalFocusTrap.js +53 -0
- package/dist/bigconsole/hooks/useModalFocusTrap.js.map +1 -0
- package/dist/bigconsole/pages/DashboardBuilderPage.js +209 -203
- package/dist/bigconsole/pages/DashboardBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/DashboardViewPage.js +126 -120
- package/dist/bigconsole/pages/DashboardViewPage.js.map +1 -1
- package/dist/bigconsole/pages/DashboardsPage.js +838 -800
- package/dist/bigconsole/pages/DashboardsPage.js.map +1 -1
- package/dist/bigconsole/pages/DataParsersPage.js +263 -315
- package/dist/bigconsole/pages/DataParsersPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSinkBuilderPage.js +301 -341
- package/dist/bigconsole/pages/DataSinkBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSinkViewPage.js +346 -339
- package/dist/bigconsole/pages/DataSinkViewPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSinksPage.js +356 -458
- package/dist/bigconsole/pages/DataSinksPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSourcesPage.js +7 -2
- package/dist/bigconsole/pages/DataSourcesPage.js.map +1 -1
- package/dist/bigconsole/pages/HomePage.js +186 -193
- package/dist/bigconsole/pages/HomePage.js.map +1 -1
- package/dist/bigconsole/pages/InsightsPage.js +887 -740
- package/dist/bigconsole/pages/InsightsPage.js.map +1 -1
- package/dist/bigconsole/pages/ParserBuilderPage.js +679 -637
- package/dist/bigconsole/pages/ParserBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/ParserViewPage.js +368 -342
- package/dist/bigconsole/pages/ParserViewPage.js.map +1 -1
- package/dist/bigconsole/pages/PipelineBuilderPage.js +537 -444
- package/dist/bigconsole/pages/PipelineBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/PipelineViewPage.js +412 -377
- package/dist/bigconsole/pages/PipelineViewPage.js.map +1 -1
- package/dist/bigconsole/pages/PipelinesPage.js +248 -298
- package/dist/bigconsole/pages/PipelinesPage.js.map +1 -1
- package/dist/bigconsole/pages/WidgetDemoPage.js +251 -153
- package/dist/bigconsole/pages/WidgetDemoPage.js.map +1 -1
- package/dist/bigconsole/pages/WorkflowViewPage.js +381 -371
- package/dist/bigconsole/pages/WorkflowViewPage.js.map +1 -1
- package/dist/bigconsole/pages/WorkflowsPage.js +171 -286
- package/dist/bigconsole/pages/WorkflowsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GaugeConfig.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/gauge/GaugeConfig.tsx"],"sourcesContent":["/**\n * GaugeConfig Component\n *\n * Configuration panel for Gauge widgets.\n */\n\nimport { type FC, memo, useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { GaugeConfig as ConfigType } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface GaugeConfigProps {\n /** Current configuration */\n config: ConfigType;\n /** Callback when config changes */\n onChange: (config: ConfigType) => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const GaugeConfig: FC<GaugeConfigProps> = memo(function GaugeConfig({ config, onChange }) {\n const { t } = useI18n();\n const handleChange = useCallback(\n <K extends keyof ConfigType>(key: K, value: ConfigType[K]) => {\n onChange({ ...config, [key]: value });\n },\n [config, onChange]\n );\n\n return (\n <div className=\"space-y-4\">\n {/* Variant */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.gaugeType', 'Gauge Type')}\n </label>\n <select\n value={config.variant || 'circular'}\n onChange={(e) => handleChange('variant', e.target.value as ConfigType['variant'])}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n >\n <option value=\"circular\">{t('bigconsole.widget.config.gauge.variant.circular', 'Circular')}</option>\n <option value=\"linear\">{t('bigconsole.widget.config.gauge.variant.linear', 'Linear')}</option>\n </select>\n </div>\n\n {/* Min/Max Range */}\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.minimum', 'Minimum')}\n </label>\n <input\n type=\"number\"\n value={config.min ?? 0}\n onChange={(e) => handleChange('min', Number(e.target.value))}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.maximum', 'Maximum')}\n </label>\n <input\n type=\"number\"\n value={config.max ?? 100}\n onChange={(e) => handleChange('max', Number(e.target.value))}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n </div>\n\n {/* Unit */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.unitLabel', 'Unit Label')}\n </label>\n <input\n type=\"text\"\n value={config.unit || ''}\n onChange={(e) => handleChange('unit', e.target.value)}\n placeholder={t('bigconsole.widget.config.gauge.unitPlaceholder', 'e.g., %, ms, users')}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n\n {/* Format */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.valueFormat', 'Value Format')}\n </label>\n <select\n value={config.format || 'number'}\n onChange={(e) => handleChange('format', e.target.value as ConfigType['format'])}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n >\n <option value=\"number\">{t('bigconsole.widget.config.gauge.format.number', 'Number')}</option>\n <option value=\"percentage\">{t('bigconsole.widget.config.gauge.format.percentage', 'Percentage')}</option>\n <option value=\"currency\">{t('bigconsole.widget.config.gauge.format.currency', 'Currency')}</option>\n </select>\n </div>\n\n {/* Thresholds Section */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-2\">\n {t('bigconsole.widget.config.gauge.colorThresholds', 'Color Thresholds')}\n </label>\n <p className=\"text-xs text-text-secondary mb-3\">\n {t(\n 'bigconsole.widget.config.gauge.colorThresholdsHelp',\n 'Define color zones based on value ranges. Values up to each threshold will use that color.'\n )}\n </p>\n\n <div className=\"space-y-2\">\n {/* Red zone */}\n <div className=\"flex items-center gap-2\">\n <div className=\"w-4 h-4 rounded bg-status-error-bg\" />\n <span className=\"text-sm text-text-primary flex-1\">\n {t('bigconsole.widget.config.gauge.threshold.danger', 'Danger (Red)')}\n </span>\n <input\n type=\"number\"\n value={config.thresholds?.[0]?.value ?? 33}\n onChange={(e) => {\n const thresholds = [...(config.thresholds || [])];\n thresholds[0] = {\n value: Number(e.target.value),\n color: 'error',\n };\n handleChange('thresholds', thresholds);\n }}\n className=\"\n w-20 px-2 py-1\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n\n {/* Yellow zone */}\n <div className=\"flex items-center gap-2\">\n <div className=\"w-4 h-4 rounded bg-status-warning-bg\" />\n <span className=\"text-sm text-text-primary flex-1\">\n {t('bigconsole.widget.config.gauge.threshold.warning', 'Warning (Yellow)')}\n </span>\n <input\n type=\"number\"\n value={config.thresholds?.[1]?.value ?? 66}\n onChange={(e) => {\n const thresholds = [...(config.thresholds || [])];\n thresholds[1] = {\n value: Number(e.target.value),\n color: 'warning',\n };\n handleChange('thresholds', thresholds);\n }}\n className=\"\n w-20 px-2 py-1\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n\n {/* Green zone */}\n <div className=\"flex items-center gap-2\">\n <div className=\"w-4 h-4 rounded bg-status-success-bg\" />\n <span className=\"text-sm text-text-primary flex-1\">\n {t('bigconsole.widget.config.gauge.threshold.good', 'Good (Green)')}\n </span>\n <input\n type=\"number\"\n value={config.thresholds?.[2]?.value ?? 100}\n onChange={(e) => {\n const thresholds = [...(config.thresholds || [])];\n thresholds[2] = {\n value: Number(e.target.value),\n color: 'success',\n };\n handleChange('thresholds', thresholds);\n }}\n className=\"\n w-20 px-2 py-1\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n </div>\n </div>\n\n {/* Show zones (for linear gauge) */}\n {config.variant === 'linear' && (\n <label className=\"flex items-center gap-3\">\n <input\n type=\"checkbox\"\n checked={config.showZones === true}\n onChange={(e) => handleChange('showZones', e.target.checked)}\n className=\"\n w-4 h-4\n text-action-primary-bg\n bg-bg-surface\n border-border-default\n rounded\n focus:ring-action-primary-bg\n \"\n />\n <span className=\"text-sm text-text-primary\">\n {t('bigconsole.widget.config.gauge.showZones', 'Show color zones in background')}\n </span>\n </label>\n )}\n </div>\n );\n});\n\nexport default GaugeConfig;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GaugeConfig.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/gauge/GaugeConfig.tsx"],"sourcesContent":["/**\n * GaugeConfig Component\n *\n * Configuration panel for Gauge widgets.\n */\n\nimport { type FC, memo, useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { GaugeConfig as ConfigType } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface GaugeConfigProps {\n /** Current configuration */\n config: ConfigType;\n /** Callback when config changes */\n onChange: (config: ConfigType) => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const GaugeConfig: FC<GaugeConfigProps> = memo(function GaugeConfig({ config, onChange }) {\n const { t } = useI18n();\n const handleChange = useCallback(\n <K extends keyof ConfigType>(key: K, value: ConfigType[K]) => {\n onChange({ ...config, [key]: value });\n },\n [config, onChange]\n );\n\n return (\n <div className=\"min-w-0 space-y-form-gap\">\n {/* Variant */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.gaugeType', 'Gauge Type')}\n </label>\n <select\n value={config.variant || 'circular'}\n onChange={(e) => handleChange('variant', e.target.value as ConfigType['variant'])}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n >\n <option value=\"circular\">{t('bigconsole.widget.config.gauge.variant.circular', 'Circular')}</option>\n <option value=\"linear\">{t('bigconsole.widget.config.gauge.variant.linear', 'Linear')}</option>\n </select>\n </div>\n\n {/* Min/Max Range */}\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.minimum', 'Minimum')}\n </label>\n <input\n type=\"number\"\n value={config.min ?? 0}\n onChange={(e) => handleChange('min', Number(e.target.value))}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.maximum', 'Maximum')}\n </label>\n <input\n type=\"number\"\n value={config.max ?? 100}\n onChange={(e) => handleChange('max', Number(e.target.value))}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n </div>\n\n {/* Unit */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.unitLabel', 'Unit Label')}\n </label>\n <input\n type=\"text\"\n value={config.unit || ''}\n onChange={(e) => handleChange('unit', e.target.value)}\n placeholder={t('bigconsole.widget.config.gauge.unitPlaceholder', 'e.g., %, ms, users')}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n\n {/* Format */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.gauge.valueFormat', 'Value Format')}\n </label>\n <select\n value={config.format || 'number'}\n onChange={(e) => handleChange('format', e.target.value as ConfigType['format'])}\n className=\"\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n >\n <option value=\"number\">{t('bigconsole.widget.config.gauge.format.number', 'Number')}</option>\n <option value=\"percentage\">{t('bigconsole.widget.config.gauge.format.percentage', 'Percentage')}</option>\n <option value=\"currency\">{t('bigconsole.widget.config.gauge.format.currency', 'Currency')}</option>\n </select>\n </div>\n\n {/* Thresholds Section */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-2\">\n {t('bigconsole.widget.config.gauge.colorThresholds', 'Color Thresholds')}\n </label>\n <p className=\"text-xs text-text-secondary mb-3\">\n {t(\n 'bigconsole.widget.config.gauge.colorThresholdsHelp',\n 'Define color zones based on value ranges. Values up to each threshold will use that color.'\n )}\n </p>\n\n <div className=\"space-y-2\">\n {/* Red zone */}\n <div className=\"flex items-center gap-2\">\n <div className=\"w-4 h-4 rounded bg-status-error-bg\" />\n <span className=\"text-sm text-text-primary flex-1\">\n {t('bigconsole.widget.config.gauge.threshold.danger', 'Danger (Red)')}\n </span>\n <input\n type=\"number\"\n value={config.thresholds?.[0]?.value ?? 33}\n onChange={(e) => {\n const thresholds = [...(config.thresholds || [])];\n thresholds[0] = {\n value: Number(e.target.value),\n color: 'error',\n };\n handleChange('thresholds', thresholds);\n }}\n className=\"\n w-20 px-2 py-1\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n\n {/* Yellow zone */}\n <div className=\"flex items-center gap-2\">\n <div className=\"w-4 h-4 rounded bg-status-warning-bg\" />\n <span className=\"text-sm text-text-primary flex-1\">\n {t('bigconsole.widget.config.gauge.threshold.warning', 'Warning (Yellow)')}\n </span>\n <input\n type=\"number\"\n value={config.thresholds?.[1]?.value ?? 66}\n onChange={(e) => {\n const thresholds = [...(config.thresholds || [])];\n thresholds[1] = {\n value: Number(e.target.value),\n color: 'warning',\n };\n handleChange('thresholds', thresholds);\n }}\n className=\"\n w-20 px-2 py-1\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n\n {/* Green zone */}\n <div className=\"flex items-center gap-2\">\n <div className=\"w-4 h-4 rounded bg-status-success-bg\" />\n <span className=\"text-sm text-text-primary flex-1\">\n {t('bigconsole.widget.config.gauge.threshold.good', 'Good (Green)')}\n </span>\n <input\n type=\"number\"\n value={config.thresholds?.[2]?.value ?? 100}\n onChange={(e) => {\n const thresholds = [...(config.thresholds || [])];\n thresholds[2] = {\n value: Number(e.target.value),\n color: 'success',\n };\n handleChange('thresholds', thresholds);\n }}\n className=\"\n w-20 px-2 py-1\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n \"\n />\n </div>\n </div>\n </div>\n\n {/* Show zones (for linear gauge) */}\n <div\n role=\"group\"\n aria-label=\"Display options\"\n className=\"space-y-3 border-t border-[var(--color-border-subtle)] pt-form-gap\"\n >\n {/* Toggles are a different KIND of control from the field stack above, so they\n get their own labelled group instead of being visually identical to it\n (BOFF-5636). */}\n <p className=\"text-xs font-bold uppercase tracking-wider text-text-secondary\">Display options</p>\n {config.variant === 'linear' && (\n <label className=\"flex items-center gap-3\">\n <input\n type=\"checkbox\"\n checked={config.showZones === true}\n onChange={(e) => handleChange('showZones', e.target.checked)}\n className=\"\n w-4 h-4\n text-action-primary-bg\n bg-bg-surface\n border-border-default\n rounded\n focus:ring-action-primary-bg\n \"\n />\n <span className=\"text-sm text-text-primary\">\n {t('bigconsole.widget.config.gauge.showZones', 'Show color zones in background')}\n </span>\n </label>\n )}\n {config.variant !== 'linear' && (\n <p className=\"text-xs text-text-secondary\">\n {t('bigconsole.widget.config.gauge.noCircularOptions', 'No display options for the circular gauge.')}\n </p>\n )}\n </div>\n </div>\n );\n});\n\nexport default GaugeConfig;\n"],"mappings":"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"}
|
|
@@ -197,7 +197,7 @@ var d = r(function({ widget: r, data: c, onClick: d, onDrilldown: f }) {
|
|
|
197
197
|
children: /* @__PURE__ */ s("div", {
|
|
198
198
|
className: "text-center",
|
|
199
199
|
children: [/* @__PURE__ */ o("svg", {
|
|
200
|
-
className: "w-12 h-12 mx-auto mb-2 text-text-
|
|
200
|
+
className: "w-12 h-12 mx-auto mb-2 text-text-muted",
|
|
201
201
|
fill: "none",
|
|
202
202
|
stroke: "currentColor",
|
|
203
203
|
viewBox: "0 0 24 24",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GaugeWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/gauge/GaugeWidget.tsx"],"sourcesContent":["/**\n * GaugeWidget Component\n *\n * Gauge widget with circular and linear variants.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, GaugeConfig } from '../../../types';\nimport { CircularGauge, type ThresholdConfig } from './CircularGauge';\nimport { LinearGauge } from './LinearGauge';\nimport { unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface GaugeWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Gauge data */\n data?: GaugeData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives gauge data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface GaugeData {\n /** Current value */\n value: number;\n /** Optional label */\n label?: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst DEFAULT_THRESHOLDS: ThresholdConfig[] = [\n { value: 33, color: 'var(--color-state-error)' },\n { value: 66, color: 'var(--color-state-warning)' },\n { value: 100, color: 'var(--color-state-success)' },\n];\n\nfunction buildThresholds(config?: GaugeConfig): ThresholdConfig[] {\n if (!config?.thresholds?.length) {\n return DEFAULT_THRESHOLDS;\n }\n\n return config.thresholds.map((t) => ({\n value: t.value,\n color:\n t.color === 'success'\n ? 'var(--color-state-success)'\n : t.color === 'warning'\n ? 'var(--color-state-warning)'\n : t.color === 'error'\n ? 'var(--color-state-error)'\n : t.color,\n }));\n}\n\nfunction formatGaugeValue(value: number, config?: GaugeConfig): string {\n const format = config?.format || 'number';\n\n switch (format) {\n case 'percentage':\n return `${value.toFixed(0)}%`;\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'number':\n default:\n return new Intl.NumberFormat('en-US', {\n maximumFractionDigits: config?.decimals || 0,\n }).format(value);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const GaugeWidget: FC<GaugeWidgetProps> = memo(function GaugeWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as GaugeConfig | undefined;\n\n const thresholds = useMemo(() => buildThresholds(config), [config]);\n\n const formatValue = useCallback((value: number) => formatGaugeValue(value, config), [config]);\n\n const hasDrilldown = !!widget.drilldown?.enabled;\n const isLinear = config?.variant === 'linear';\n\n // Normalize data from different formats (DataSink array, single object, etc.)\n const normalizedData = useMemo((): GaugeData | null => {\n if (!rawData) return null;\n\n // Explicit top-level `value` always wins — even if a sibling envelope\n // key exists (`{value: 42, data: [...]}` is a common summary+detail\n // shape) we prefer the summary value rather than coercing to array mode.\n if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {\n const asRecord = rawData as unknown as Record<string, unknown>;\n if ('value' in asRecord && typeof asRecord.value === 'number') {\n return rawData as GaugeData;\n }\n }\n\n // BOFF-2246: unwrap common envelopes so the existing array handler below\n // (with its rich common-field heuristics) runs for parsers emitting\n // `{data: [...]}` / `{items: [...]}` — previously those payloads hit\n // \"No gauge data\" because no handler matched.\n //\n // Caveat: summary+detail payloads like `{ totalSales: 120, data: [...] }`\n // should keep the top-level numeric (the existing single-object handler\n // below has a generic \"any numeric field\" last-resort scan that would\n // otherwise surface it). Skip the unwrap whenever ANY numeric sibling\n // exists alongside the envelope key — matches the scope of the generic\n // scan so we don't regress previously-working payloads.\n let data: unknown = rawData;\n if (data && typeof data === 'object' && !Array.isArray(data)) {\n const obj = data as Record<string, unknown>;\n const hasTopLevelNumericSibling = Object.entries(obj).some(([k, v]) => {\n if (k === 'data' || k === 'items' || k === 'rows') return false;\n return typeof v === 'number';\n });\n\n if (!hasTopLevelNumericSibling) {\n // Array-aware unwrap — picks the first envelope key that's actually\n // an array, so payloads like `{data: {page: 1}, items: [...]}` still\n // reach the array handler below.\n const maybe = unwrapRows(obj);\n if (maybe && maybe.length > 0) data = maybe;\n }\n }\n\n // Get configured value field from widget config\n const valueField =\n ((config as Record<string, unknown> | undefined)?.valueField as string) ||\n ((config as Record<string, unknown> | undefined)?.value_field as string) ||\n 'value';\n\n // Handle array from DataSink - extract value from first item\n if (Array.isArray(data) && data.length > 0) {\n const firstItem = data[0] as Record<string, unknown>;\n\n // Try configured field first\n if (typeof firstItem[valueField] === 'number') {\n return {\n value: firstItem[valueField] as number,\n label: typeof firstItem.label === 'string' ? firstItem.label : undefined,\n };\n }\n\n // Try common numeric field names\n const numericFields = [\n 'value',\n 'growth',\n 'percentage',\n 'percent',\n 'progress',\n 'score',\n 'rate',\n 'amount',\n 'total',\n ];\n for (const field of numericFields) {\n if (typeof firstItem[field] === 'number') {\n return {\n value: firstItem[field] as number,\n label:\n typeof firstItem.label === 'string'\n ? firstItem.label\n : typeof firstItem.name === 'string'\n ? firstItem.name\n : undefined,\n };\n }\n }\n\n // Find any numeric field\n const numericEntry = Object.entries(firstItem).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n return {\n value: numericEntry[1] as number,\n label: `${numericEntry[0].charAt(0).toUpperCase()}${numericEntry[0].slice(1)}`,\n };\n }\n\n // Try to aggregate (average) across all rows if single item didn't work\n if (data.length > 1) {\n const allNumericFields = Object.entries(firstItem).filter(([, v]) => typeof v === 'number');\n if (allNumericFields.length > 0) {\n const [fieldName] = allNumericFields[0];\n const avg =\n data.reduce((sum, row) => {\n const val = (row as Record<string, unknown>)[fieldName];\n return sum + (typeof val === 'number' ? val : 0);\n }, 0) / data.length;\n\n return {\n value: Math.round(avg * 100) / 100,\n label: `Avg ${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`,\n };\n }\n }\n }\n\n // Handle single object without 'value' property\n if (typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as unknown as Record<string, unknown>;\n\n // Try configured field\n if (typeof dataObj[valueField] === 'number') {\n return {\n value: dataObj[valueField] as number,\n label: typeof dataObj.label === 'string' ? dataObj.label : undefined,\n };\n }\n\n // Try common field names\n const numericFields = [\n 'value',\n 'growth',\n 'percentage',\n 'percent',\n 'progress',\n 'score',\n 'rate',\n 'amount',\n 'total',\n ];\n for (const field of numericFields) {\n if (typeof dataObj[field] === 'number') {\n return {\n value: dataObj[field] as number,\n label: typeof dataObj.label === 'string' ? dataObj.label : undefined,\n };\n }\n }\n\n // Find any numeric field\n const numericEntry = Object.entries(dataObj).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n return {\n value: numericEntry[1] as number,\n label: `${numericEntry[0].charAt(0).toUpperCase()}${numericEntry[0].slice(1)}`,\n };\n }\n }\n\n // Handle direct number\n if (typeof data === 'number') {\n return { value: data };\n }\n\n // Handle string that can be parsed as number\n if (typeof data === 'string') {\n const parsed = parseFloat(data);\n if (!isNaN(parsed)) {\n return { value: parsed };\n }\n }\n\n return null;\n }, [rawData, config]);\n\n // Handle drilldown click - must be after normalizedData is computed\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown && normalizedData) {\n onDrilldown({\n value: normalizedData.value,\n label: normalizedData.label,\n min: config?.min,\n max: config?.max,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, normalizedData, config?.min, config?.max]);\n\n const isClickable = Boolean(hasDrilldown || (onClick && widget.drilldowns?.length));\n\n if (!normalizedData) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No gauge data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={`\n flex flex-col items-center justify-center 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 {/* Label */}\n {normalizedData.label && <p className=\"text-sm text-text-secondary mb-3 text-center\">{normalizedData.label}</p>}\n\n {/* Gauge */}\n <div className={`${isLinear ? 'w-full max-w-xs' : ''}`}>\n {isLinear ? (\n <LinearGauge\n value={normalizedData.value}\n min={config?.min}\n max={config?.max}\n thresholds={thresholds}\n unit={config?.unit}\n formatValue={formatValue}\n showZones={config?.showZones}\n />\n ) : (\n <CircularGauge\n value={normalizedData.value}\n min={config?.min}\n max={config?.max}\n thresholds={thresholds}\n unit={config?.unit}\n formatValue={formatValue}\n size={config?.size === 'sm' ? 100 : config?.size === 'md' ? 140 : config?.size === 'lg' ? 180 : 140}\n />\n )}\n </div>\n\n {/* Drilldown indicator */}\n {isClickable && (\n <div className=\"mt-3 flex items-center justify-center\">\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 GaugeWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GaugeWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/gauge/GaugeWidget.tsx"],"sourcesContent":["/**\n * GaugeWidget Component\n *\n * Gauge widget with circular and linear variants.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, GaugeConfig } from '../../../types';\nimport { CircularGauge, type ThresholdConfig } from './CircularGauge';\nimport { LinearGauge } from './LinearGauge';\nimport { unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface GaugeWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Gauge data */\n data?: GaugeData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives gauge data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface GaugeData {\n /** Current value */\n value: number;\n /** Optional label */\n label?: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst DEFAULT_THRESHOLDS: ThresholdConfig[] = [\n { value: 33, color: 'var(--color-state-error)' },\n { value: 66, color: 'var(--color-state-warning)' },\n { value: 100, color: 'var(--color-state-success)' },\n];\n\nfunction buildThresholds(config?: GaugeConfig): ThresholdConfig[] {\n if (!config?.thresholds?.length) {\n return DEFAULT_THRESHOLDS;\n }\n\n return config.thresholds.map((t) => ({\n value: t.value,\n color:\n t.color === 'success'\n ? 'var(--color-state-success)'\n : t.color === 'warning'\n ? 'var(--color-state-warning)'\n : t.color === 'error'\n ? 'var(--color-state-error)'\n : t.color,\n }));\n}\n\nfunction formatGaugeValue(value: number, config?: GaugeConfig): string {\n const format = config?.format || 'number';\n\n switch (format) {\n case 'percentage':\n return `${value.toFixed(0)}%`;\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'number':\n default:\n return new Intl.NumberFormat('en-US', {\n maximumFractionDigits: config?.decimals || 0,\n }).format(value);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const GaugeWidget: FC<GaugeWidgetProps> = memo(function GaugeWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as GaugeConfig | undefined;\n\n const thresholds = useMemo(() => buildThresholds(config), [config]);\n\n const formatValue = useCallback((value: number) => formatGaugeValue(value, config), [config]);\n\n const hasDrilldown = !!widget.drilldown?.enabled;\n const isLinear = config?.variant === 'linear';\n\n // Normalize data from different formats (DataSink array, single object, etc.)\n const normalizedData = useMemo((): GaugeData | null => {\n if (!rawData) return null;\n\n // Explicit top-level `value` always wins — even if a sibling envelope\n // key exists (`{value: 42, data: [...]}` is a common summary+detail\n // shape) we prefer the summary value rather than coercing to array mode.\n if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {\n const asRecord = rawData as unknown as Record<string, unknown>;\n if ('value' in asRecord && typeof asRecord.value === 'number') {\n return rawData as GaugeData;\n }\n }\n\n // BOFF-2246: unwrap common envelopes so the existing array handler below\n // (with its rich common-field heuristics) runs for parsers emitting\n // `{data: [...]}` / `{items: [...]}` — previously those payloads hit\n // \"No gauge data\" because no handler matched.\n //\n // Caveat: summary+detail payloads like `{ totalSales: 120, data: [...] }`\n // should keep the top-level numeric (the existing single-object handler\n // below has a generic \"any numeric field\" last-resort scan that would\n // otherwise surface it). Skip the unwrap whenever ANY numeric sibling\n // exists alongside the envelope key — matches the scope of the generic\n // scan so we don't regress previously-working payloads.\n let data: unknown = rawData;\n if (data && typeof data === 'object' && !Array.isArray(data)) {\n const obj = data as Record<string, unknown>;\n const hasTopLevelNumericSibling = Object.entries(obj).some(([k, v]) => {\n if (k === 'data' || k === 'items' || k === 'rows') return false;\n return typeof v === 'number';\n });\n\n if (!hasTopLevelNumericSibling) {\n // Array-aware unwrap — picks the first envelope key that's actually\n // an array, so payloads like `{data: {page: 1}, items: [...]}` still\n // reach the array handler below.\n const maybe = unwrapRows(obj);\n if (maybe && maybe.length > 0) data = maybe;\n }\n }\n\n // Get configured value field from widget config\n const valueField =\n ((config as Record<string, unknown> | undefined)?.valueField as string) ||\n ((config as Record<string, unknown> | undefined)?.value_field as string) ||\n 'value';\n\n // Handle array from DataSink - extract value from first item\n if (Array.isArray(data) && data.length > 0) {\n const firstItem = data[0] as Record<string, unknown>;\n\n // Try configured field first\n if (typeof firstItem[valueField] === 'number') {\n return {\n value: firstItem[valueField] as number,\n label: typeof firstItem.label === 'string' ? firstItem.label : undefined,\n };\n }\n\n // Try common numeric field names\n const numericFields = [\n 'value',\n 'growth',\n 'percentage',\n 'percent',\n 'progress',\n 'score',\n 'rate',\n 'amount',\n 'total',\n ];\n for (const field of numericFields) {\n if (typeof firstItem[field] === 'number') {\n return {\n value: firstItem[field] as number,\n label:\n typeof firstItem.label === 'string'\n ? firstItem.label\n : typeof firstItem.name === 'string'\n ? firstItem.name\n : undefined,\n };\n }\n }\n\n // Find any numeric field\n const numericEntry = Object.entries(firstItem).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n return {\n value: numericEntry[1] as number,\n label: `${numericEntry[0].charAt(0).toUpperCase()}${numericEntry[0].slice(1)}`,\n };\n }\n\n // Try to aggregate (average) across all rows if single item didn't work\n if (data.length > 1) {\n const allNumericFields = Object.entries(firstItem).filter(([, v]) => typeof v === 'number');\n if (allNumericFields.length > 0) {\n const [fieldName] = allNumericFields[0];\n const avg =\n data.reduce((sum, row) => {\n const val = (row as Record<string, unknown>)[fieldName];\n return sum + (typeof val === 'number' ? val : 0);\n }, 0) / data.length;\n\n return {\n value: Math.round(avg * 100) / 100,\n label: `Avg ${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`,\n };\n }\n }\n }\n\n // Handle single object without 'value' property\n if (typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as unknown as Record<string, unknown>;\n\n // Try configured field\n if (typeof dataObj[valueField] === 'number') {\n return {\n value: dataObj[valueField] as number,\n label: typeof dataObj.label === 'string' ? dataObj.label : undefined,\n };\n }\n\n // Try common field names\n const numericFields = [\n 'value',\n 'growth',\n 'percentage',\n 'percent',\n 'progress',\n 'score',\n 'rate',\n 'amount',\n 'total',\n ];\n for (const field of numericFields) {\n if (typeof dataObj[field] === 'number') {\n return {\n value: dataObj[field] as number,\n label: typeof dataObj.label === 'string' ? dataObj.label : undefined,\n };\n }\n }\n\n // Find any numeric field\n const numericEntry = Object.entries(dataObj).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n return {\n value: numericEntry[1] as number,\n label: `${numericEntry[0].charAt(0).toUpperCase()}${numericEntry[0].slice(1)}`,\n };\n }\n }\n\n // Handle direct number\n if (typeof data === 'number') {\n return { value: data };\n }\n\n // Handle string that can be parsed as number\n if (typeof data === 'string') {\n const parsed = parseFloat(data);\n if (!isNaN(parsed)) {\n return { value: parsed };\n }\n }\n\n return null;\n }, [rawData, config]);\n\n // Handle drilldown click - must be after normalizedData is computed\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown && normalizedData) {\n onDrilldown({\n value: normalizedData.value,\n label: normalizedData.label,\n min: config?.min,\n max: config?.max,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, normalizedData, config?.min, config?.max]);\n\n const isClickable = Boolean(hasDrilldown || (onClick && widget.drilldowns?.length));\n\n if (!normalizedData) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg className=\"w-12 h-12 mx-auto mb-2 text-text-muted\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No gauge data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={`\n flex flex-col items-center justify-center 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 {/* Label */}\n {normalizedData.label && <p className=\"text-sm text-text-secondary mb-3 text-center\">{normalizedData.label}</p>}\n\n {/* Gauge */}\n <div className={`${isLinear ? 'w-full max-w-xs' : ''}`}>\n {isLinear ? (\n <LinearGauge\n value={normalizedData.value}\n min={config?.min}\n max={config?.max}\n thresholds={thresholds}\n unit={config?.unit}\n formatValue={formatValue}\n showZones={config?.showZones}\n />\n ) : (\n <CircularGauge\n value={normalizedData.value}\n min={config?.min}\n max={config?.max}\n thresholds={thresholds}\n unit={config?.unit}\n formatValue={formatValue}\n size={config?.size === 'sm' ? 100 : config?.size === 'md' ? 140 : config?.size === 'lg' ? 180 : 140}\n />\n )}\n </div>\n\n {/* Drilldown indicator */}\n {isClickable && (\n <div className=\"mt-3 flex items-center justify-center\">\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 GaugeWidget;\n"],"mappings":"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"}
|
|
@@ -23,7 +23,7 @@ var o = "\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n b
|
|
|
23
23
|
});
|
|
24
24
|
}, [e, s]);
|
|
25
25
|
return /* @__PURE__ */ a("div", {
|
|
26
|
-
className: "space-y-
|
|
26
|
+
className: "min-w-0 space-y-form-gap",
|
|
27
27
|
children: [
|
|
28
28
|
/* @__PURE__ */ a("div", { children: [/* @__PURE__ */ i("label", {
|
|
29
29
|
className: "block text-sm font-medium text-text-primary mb-1",
|
|
@@ -85,29 +85,40 @@ var o = "\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n b
|
|
|
85
85
|
onChange: (e) => u("decimals", Number(e.target.value)),
|
|
86
86
|
className: o
|
|
87
87
|
})] }),
|
|
88
|
-
/* @__PURE__ */ a("
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
88
|
+
/* @__PURE__ */ a("div", {
|
|
89
|
+
role: "group",
|
|
90
|
+
"aria-label": "Display options",
|
|
91
|
+
className: "space-y-3 border-t border-[var(--color-border-subtle)] pt-form-gap",
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ i("p", {
|
|
94
|
+
className: "text-xs font-bold uppercase tracking-wider text-text-secondary",
|
|
95
|
+
children: "Display options"
|
|
96
|
+
}),
|
|
97
|
+
/* @__PURE__ */ a("label", {
|
|
98
|
+
className: "flex items-center gap-3",
|
|
99
|
+
children: [/* @__PURE__ */ i("input", {
|
|
100
|
+
type: "checkbox",
|
|
101
|
+
checked: e.showValues !== !1,
|
|
102
|
+
onChange: (e) => u("showValues", e.target.checked),
|
|
103
|
+
className: "\n w-4 h-4\n text-action-primary-bg\n bg-bg-surface\n border-border-default\n rounded\n focus:ring-action-primary-bg\n "
|
|
104
|
+
}), /* @__PURE__ */ i("span", {
|
|
105
|
+
className: "text-sm text-text-primary",
|
|
106
|
+
children: c("bigconsole.widget.config.heatmap.showValues", "Show cell values")
|
|
107
|
+
})]
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ a("label", {
|
|
110
|
+
className: "flex items-center gap-3",
|
|
111
|
+
children: [/* @__PURE__ */ i("input", {
|
|
112
|
+
type: "checkbox",
|
|
113
|
+
checked: e.showLegend !== !1,
|
|
114
|
+
onChange: (e) => u("showLegend", e.target.checked),
|
|
115
|
+
className: "\n w-4 h-4\n text-action-primary-bg\n bg-bg-surface\n border-border-default\n rounded\n focus:ring-action-primary-bg\n "
|
|
116
|
+
}), /* @__PURE__ */ i("span", {
|
|
117
|
+
className: "text-sm text-text-primary",
|
|
118
|
+
children: c("bigconsole.widget.config.heatmap.showLegend", "Show color legend")
|
|
119
|
+
})]
|
|
120
|
+
})
|
|
121
|
+
]
|
|
111
122
|
})
|
|
112
123
|
]
|
|
113
124
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HeatmapConfig.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/heatmap/HeatmapConfig.tsx"],"sourcesContent":["/**\n * HeatmapConfig Component\n *\n * Configuration panel for Heatmap widgets.\n */\n\nimport { type FC, memo, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { HeatmapConfig as ConfigType } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface HeatmapConfigProps {\n /** Current configuration */\n config: ConfigType;\n /** Callback when config changes */\n onChange: (config: ConfigType) => void;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst FIELD_CLASSES = `\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n`;\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const HeatmapConfig: FC<HeatmapConfigProps> = memo(function HeatmapConfig({ config, onChange }) {\n const { t } = useI18n();\n\n const COLOR_SCHEMES = useMemo(\n () => [\n { value: 'blue', label: t('bigconsole.widget.config.heatmap.colorScheme.blue', 'Blue') },\n { value: 'green', label: t('bigconsole.widget.config.heatmap.colorScheme.green', 'Green') },\n { value: 'red', label: t('bigconsole.widget.config.heatmap.colorScheme.red', 'Red') },\n ],\n [t]\n );\n\n const handleChange = useCallback(\n <K extends keyof ConfigType>(key: K, value: ConfigType[K]) => {\n onChange({ ...config, [key]: value });\n },\n [config, onChange]\n );\n\n return (\n <div className=\"space-y-
|
|
1
|
+
{"version":3,"file":"HeatmapConfig.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/heatmap/HeatmapConfig.tsx"],"sourcesContent":["/**\n * HeatmapConfig Component\n *\n * Configuration panel for Heatmap widgets.\n */\n\nimport { type FC, memo, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { HeatmapConfig as ConfigType } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface HeatmapConfigProps {\n /** Current configuration */\n config: ConfigType;\n /** Callback when config changes */\n onChange: (config: ConfigType) => void;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst FIELD_CLASSES = `\n w-full px-3 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg\n`;\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const HeatmapConfig: FC<HeatmapConfigProps> = memo(function HeatmapConfig({ config, onChange }) {\n const { t } = useI18n();\n\n const COLOR_SCHEMES = useMemo(\n () => [\n { value: 'blue', label: t('bigconsole.widget.config.heatmap.colorScheme.blue', 'Blue') },\n { value: 'green', label: t('bigconsole.widget.config.heatmap.colorScheme.green', 'Green') },\n { value: 'red', label: t('bigconsole.widget.config.heatmap.colorScheme.red', 'Red') },\n ],\n [t]\n );\n\n const handleChange = useCallback(\n <K extends keyof ConfigType>(key: K, value: ConfigType[K]) => {\n onChange({ ...config, [key]: value });\n },\n [config, onChange]\n );\n\n return (\n <div className=\"min-w-0 space-y-form-gap\">\n {/* Axis + value fields */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.heatmap.xAxisFieldLabel', 'X-Axis Field')}\n </label>\n <input\n type=\"text\"\n value={config.xAxisField || ''}\n onChange={(e) => handleChange('xAxisField', e.target.value)}\n placeholder={t('bigconsole.widget.config.heatmap.xAxisFieldPlaceholder', 'e.g., hour, column')}\n className={FIELD_CLASSES}\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.heatmap.yAxisFieldLabel', 'Y-Axis Field')}\n </label>\n <input\n type=\"text\"\n value={config.yAxisField || ''}\n onChange={(e) => handleChange('yAxisField', e.target.value)}\n placeholder={t('bigconsole.widget.config.heatmap.yAxisFieldPlaceholder', 'e.g., day, row')}\n className={FIELD_CLASSES}\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.heatmap.valueFieldLabel', 'Value Field')}\n </label>\n <input\n type=\"text\"\n value={config.valueField || ''}\n onChange={(e) => handleChange('valueField', e.target.value)}\n placeholder={t('bigconsole.widget.config.heatmap.valueFieldPlaceholder', 'e.g., tickets, count')}\n className={FIELD_CLASSES}\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t(\n 'bigconsole.widget.config.heatmap.valueFieldHelp',\n \"Left empty, the widget reads the parser's rows / columns / cells output directly.\"\n )}\n </p>\n </div>\n\n {/* Color scheme */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.heatmap.colorSchemeLabel', 'Color Scheme')}\n </label>\n <select\n value={config.colorScheme || 'blue'}\n onChange={(e) => handleChange('colorScheme', e.target.value)}\n className={FIELD_CLASSES}\n >\n {COLOR_SCHEMES.map((scheme) => (\n <option key={scheme.value} value={scheme.value}>\n {scheme.label}\n </option>\n ))}\n </select>\n </div>\n\n {/* Decimals */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {t('bigconsole.widget.config.heatmap.decimalPlacesLabel', 'Decimal Places')}\n </label>\n <input\n type=\"number\"\n min={0}\n max={6}\n value={config.decimals ?? 0}\n onChange={(e) => handleChange('decimals', Number(e.target.value))}\n className={FIELD_CLASSES}\n />\n </div>\n\n {/* Toggles */}\n <div\n role=\"group\"\n aria-label=\"Display options\"\n className=\"space-y-3 border-t border-[var(--color-border-subtle)] pt-form-gap\"\n >\n {/* Toggles are a different KIND of control from the field stack above, so they\n get their own labelled group instead of being visually identical to it\n (BOFF-5636). */}\n <p className=\"text-xs font-bold uppercase tracking-wider text-text-secondary\">Display options</p>\n <label className=\"flex items-center gap-3\">\n <input\n type=\"checkbox\"\n checked={config.showValues !== false}\n onChange={(e) => handleChange('showValues', e.target.checked)}\n className=\"\n w-4 h-4\n text-action-primary-bg\n bg-bg-surface\n border-border-default\n rounded\n focus:ring-action-primary-bg\n \"\n />\n <span className=\"text-sm text-text-primary\">\n {t('bigconsole.widget.config.heatmap.showValues', 'Show cell values')}\n </span>\n </label>\n\n <label className=\"flex items-center gap-3\">\n <input\n type=\"checkbox\"\n checked={config.showLegend !== false}\n onChange={(e) => handleChange('showLegend', e.target.checked)}\n className=\"\n w-4 h-4\n text-action-primary-bg\n bg-bg-surface\n border-border-default\n rounded\n focus:ring-action-primary-bg\n \"\n />\n <span className=\"text-sm text-text-primary\">\n {t('bigconsole.widget.config.heatmap.showLegend', 'Show color legend')}\n </span>\n </label>\n </div>\n </div>\n );\n});\n\nexport default HeatmapConfig;\n"],"mappings":"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"}
|
|
@@ -33,7 +33,7 @@ var a = e(function({ widget: e, data: a }) {
|
|
|
33
33
|
className: "text-center",
|
|
34
34
|
children: [
|
|
35
35
|
/* @__PURE__ */ n("svg", {
|
|
36
|
-
className: "w-12 h-12 mx-auto mb-2 text-text-
|
|
36
|
+
className: "w-12 h-12 mx-auto mb-2 text-text-muted",
|
|
37
37
|
fill: "none",
|
|
38
38
|
stroke: "currentColor",
|
|
39
39
|
viewBox: "0 0 24 24",
|
|
@@ -46,7 +46,7 @@ var a = e(function({ widget: e, data: a }) {
|
|
|
46
46
|
}),
|
|
47
47
|
/* @__PURE__ */ n("p", { children: "No URL configured" }),
|
|
48
48
|
/* @__PURE__ */ n("p", {
|
|
49
|
-
className: "text-xs text-text-
|
|
49
|
+
className: "text-xs text-text-muted mt-1",
|
|
50
50
|
children: "Configure a URL to embed content"
|
|
51
51
|
})
|
|
52
52
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IframeWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/iframe-widget/IframeWidget.tsx"],"sourcesContent":["/**\n * IframeWidget Component\n *\n * Displays embedded iframe content.\n *\n * Data flow: DataSink → Parser → Widget\n * - Static configuration lives in `widget.config` (url, sandbox, allowFullscreen, title).\n * - Dynamic parser output is passed in `data` and overrides matching config keys.\n * - This lets the iframe render from manual configuration, parser-driven data, or both.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface IframeWidgetProps {\n widget: Widget;\n data?: IframeData;\n}\n\nexport interface IframeData {\n url?: string;\n title?: string;\n allowFullscreen?: boolean;\n sandbox?: string;\n loading?: 'lazy' | 'eager';\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Resolve the effective iframe configuration.\n *\n * Priority (highest first):\n * 1. Parser-provided `data` fields\n * 2. User-configured `widget.config` fields\n * 3. Safe defaults (empty URL, restricted sandbox, no fullscreen)\n *\n * This is the function the widget tests exercise directly; keeping the merge\n * logic pure makes the DataSink → Parser → Widget contract explicit.\n */\nexport function resolveIframeConfig(\n widgetConfig: Record<string, unknown> | undefined,\n data: IframeData | undefined\n): Required<IframeData> & { hasUrl: boolean } {\n const cfg = widgetConfig ?? {};\n\n const coercedBoolean = (value: unknown): boolean | undefined => {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') return value.toLowerCase() === 'true';\n return undefined;\n };\n\n const coercedLoading = (value: unknown): 'lazy' | 'eager' => {\n if (value === 'eager' || value === 'lazy') return value;\n return 'lazy';\n };\n\n const url = (data?.url ?? cfg.url ?? '') as string;\n const title = (data?.title ?? cfg.title ?? 'Embedded Content') as string;\n const allowFullscreen = coercedBoolean(data?.allowFullscreen ?? cfg.allowFullscreen) ?? false;\n const sandbox = ((data?.sandbox ?? cfg.sandbox) as string | undefined) ?? 'allow-scripts allow-same-origin';\n const loading = coercedLoading(data?.loading ?? cfg.loading);\n\n return {\n url,\n title,\n allowFullscreen,\n sandbox,\n loading,\n hasUrl: typeof url === 'string' && url.length > 0,\n };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const IframeWidget: FC<IframeWidgetProps> = memo(function IframeWidget({ widget, data }) {\n const config = useMemo(() => resolveIframeConfig(widget.config, data), [widget.config, data]);\n\n if (!config.hasUrl) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg
|
|
1
|
+
{"version":3,"file":"IframeWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/iframe-widget/IframeWidget.tsx"],"sourcesContent":["/**\n * IframeWidget Component\n *\n * Displays embedded iframe content.\n *\n * Data flow: DataSink → Parser → Widget\n * - Static configuration lives in `widget.config` (url, sandbox, allowFullscreen, title).\n * - Dynamic parser output is passed in `data` and overrides matching config keys.\n * - This lets the iframe render from manual configuration, parser-driven data, or both.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface IframeWidgetProps {\n widget: Widget;\n data?: IframeData;\n}\n\nexport interface IframeData {\n url?: string;\n title?: string;\n allowFullscreen?: boolean;\n sandbox?: string;\n loading?: 'lazy' | 'eager';\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Resolve the effective iframe configuration.\n *\n * Priority (highest first):\n * 1. Parser-provided `data` fields\n * 2. User-configured `widget.config` fields\n * 3. Safe defaults (empty URL, restricted sandbox, no fullscreen)\n *\n * This is the function the widget tests exercise directly; keeping the merge\n * logic pure makes the DataSink → Parser → Widget contract explicit.\n */\nexport function resolveIframeConfig(\n widgetConfig: Record<string, unknown> | undefined,\n data: IframeData | undefined\n): Required<IframeData> & { hasUrl: boolean } {\n const cfg = widgetConfig ?? {};\n\n const coercedBoolean = (value: unknown): boolean | undefined => {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') return value.toLowerCase() === 'true';\n return undefined;\n };\n\n const coercedLoading = (value: unknown): 'lazy' | 'eager' => {\n if (value === 'eager' || value === 'lazy') return value;\n return 'lazy';\n };\n\n const url = (data?.url ?? cfg.url ?? '') as string;\n const title = (data?.title ?? cfg.title ?? 'Embedded Content') as string;\n const allowFullscreen = coercedBoolean(data?.allowFullscreen ?? cfg.allowFullscreen) ?? false;\n const sandbox = ((data?.sandbox ?? cfg.sandbox) as string | undefined) ?? 'allow-scripts allow-same-origin';\n const loading = coercedLoading(data?.loading ?? cfg.loading);\n\n return {\n url,\n title,\n allowFullscreen,\n sandbox,\n loading,\n hasUrl: typeof url === 'string' && url.length > 0,\n };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const IframeWidget: FC<IframeWidgetProps> = memo(function IframeWidget({ widget, data }) {\n const config = useMemo(() => resolveIframeConfig(widget.config, data), [widget.config, data]);\n\n if (!config.hasUrl) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg className=\"w-12 h-12 mx-auto mb-2 text-text-muted\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n />\n </svg>\n <p>No URL configured</p>\n <p className=\"text-xs text-text-muted mt-1\">Configure a URL to embed content</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full w-full\">\n <iframe\n src={config.url}\n title={config.title}\n className=\"w-full h-full border-0 rounded\"\n allowFullScreen={config.allowFullscreen}\n sandbox={config.sandbox}\n loading={config.loading}\n />\n </div>\n );\n});\n\nexport default IframeWidget;\n"],"mappings":";;;AA8CA,SAAgB,EACd,GACA,GAC4C;CAC5C,IAAM,IAAM,KAAgB,EAAE,EAExB,KAAkB,MAAwC;AAC9D,MAAI,OAAO,KAAU,UAAW,QAAO;AACvC,MAAI,OAAO,KAAU,SAAU,QAAO,EAAM,aAAa,KAAK;IAI1D,KAAkB,MAClB,MAAU,WAAW,MAAU,SAAe,IAC3C,QAGH,IAAO,GAAM,OAAO,EAAI,OAAO;AAMrC,QAAO;EACL;EACA,OAPa,GAAM,SAAS,EAAI,SAAS;EAQzC,iBAPsB,EAAe,GAAM,mBAAmB,EAAI,gBAAgB,IAAI;EAQtF,SAPgB,GAAM,WAAW,EAAI,WAAmC;EAQxE,SAPc,EAAe,GAAM,WAAW,EAAI,QAAQ;EAQ1D,QAAQ,OAAO,KAAQ,YAAY,EAAI,SAAS;EACjD;;AAOH,IAAa,IAAsC,EAAK,SAAsB,EAAE,WAAQ,WAAQ;CAC9F,IAAM,IAAS,QAAc,EAAoB,EAAO,QAAQ,EAAK,EAAE,CAAC,EAAO,QAAQ,EAAK,CAAC;AAqB7F,QAnBK,EAAO,SAoBV,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,UAAD;GACE,KAAK,EAAO;GACZ,OAAO,EAAO;GACd,WAAU;GACV,iBAAiB,EAAO;GACxB,SAAS,EAAO;GAChB,SAAS,EAAO;GAChB,CAAA;EACE,CAAA,GA3BJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;KAAyC,MAAK;KAAO,QAAO;KAAe,SAAQ;eAChG,kBAAC,QAAD;MACE,eAAc;MACd,gBAAe;MACf,aAAa;MACb,GAAE;MACF,CAAA;KACE,CAAA;IACN,kBAAC,KAAD,EAAA,UAAG,qBAAqB,CAAA;IACxB,kBAAC,KAAD;KAAG,WAAU;eAA+B;KAAoC,CAAA;IAC5E;;EACF,CAAA;EAgBV"}
|
|
@@ -5,7 +5,7 @@ import { jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
|
5
5
|
function a(e) {
|
|
6
6
|
switch (e) {
|
|
7
7
|
case "high": return "bg-status-error-bg/20 text-status-error-text";
|
|
8
|
-
case "medium": return "bg-status-warning-bg/20 text-status-warning-text";
|
|
8
|
+
case "medium": return "bg-status-warning-bg/20 text-[var(--color-status-warning-text)]";
|
|
9
9
|
case "low": return "bg-status-success-bg/20 text-status-success-text";
|
|
10
10
|
default: return "bg-bg-sunken text-text-secondary";
|
|
11
11
|
}
|
|
@@ -135,7 +135,7 @@ var p = e(function({ widget: e, data: o, onClick: s }) {
|
|
|
135
135
|
children: e.title
|
|
136
136
|
}),
|
|
137
137
|
/* @__PURE__ */ r("span", {
|
|
138
|
-
className: "ml-auto text-xs text-text-
|
|
138
|
+
className: "ml-auto text-xs text-text-muted",
|
|
139
139
|
children: t.length
|
|
140
140
|
})
|
|
141
141
|
]
|
|
@@ -162,7 +162,7 @@ var p = e(function({ widget: e, data: o, onClick: s }) {
|
|
|
162
162
|
})]
|
|
163
163
|
})]
|
|
164
164
|
}, e.id)), t.length === 0 && /* @__PURE__ */ r("p", {
|
|
165
|
-
className: "text-[10px] text-text-
|
|
165
|
+
className: "text-[10px] text-text-muted text-center py-4",
|
|
166
166
|
children: c("bigconsole.widget.kanban.noTasks", "No tasks")
|
|
167
167
|
})]
|
|
168
168
|
})]
|
|
@@ -173,7 +173,7 @@ var p = e(function({ widget: e, data: o, onClick: s }) {
|
|
|
173
173
|
children: /* @__PURE__ */ i("div", {
|
|
174
174
|
className: "text-center",
|
|
175
175
|
children: [/* @__PURE__ */ r("svg", {
|
|
176
|
-
className: "w-12 h-12 mx-auto mb-2 text-text-
|
|
176
|
+
className: "w-12 h-12 mx-auto mb-2 text-text-muted",
|
|
177
177
|
fill: "none",
|
|
178
178
|
stroke: "currentColor",
|
|
179
179
|
viewBox: "0 0 24 24",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-status-error-bg/20 text-status-error-text';\n case 'medium':\n return 'bg-status-warning-bg/20 text-status-warning-text';\n case 'low':\n return 'bg-status-success-bg/20 text-status-success-text';\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// Field names we try, in order, when the widget config doesn't name one\n// explicitly. A DataSink/parser typically emits a flat array of rows (e.g. the\n// Linear \"Kanban board\" parser -> [{ id, title, status, assignee, ... }]), so\n// the board is derived from a status/column field on those rows.\nconst COLUMN_FIELD_CANDIDATES = ['status', 'state', 'column', 'stage', 'category', 'phase', 'group'];\nconst TITLE_FIELD_CANDIDATES = ['title', 'name', 'summary', 'label', 'identifier', 'id'];\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/** Coerce parser output into an array of object rows (handles common envelopes). */\nfunction asRows(data: unknown): Record<string, unknown>[] {\n const onlyObjects = (arr: unknown[]): Record<string, unknown>[] =>\n arr.filter((r): r is Record<string, unknown> => !!r && typeof r === 'object' && !Array.isArray(r));\n\n if (Array.isArray(data)) return onlyObjects(data);\n\n if (data && typeof data === 'object') {\n const obj = data as Record<string, unknown>;\n for (const key of ['tasks', 'rows', 'items', 'data', 'nodes', 'results', 'records']) {\n if (Array.isArray(obj[key])) return onlyObjects(obj[key] as unknown[]);\n }\n const firstArray = Object.values(obj).find((v) => Array.isArray(v));\n if (Array.isArray(firstArray)) return onlyObjects(firstArray);\n }\n return [];\n}\n\n/** First key that actually exists on the sample row. */\nfunction firstPresent(row: Record<string, unknown>, keys: (string | undefined)[]): string | undefined {\n for (const key of keys) {\n if (key && key in row) return key;\n }\n return undefined;\n}\n\nfunction normalizePriority(value: unknown): KanbanTask['priority'] {\n const v = asString(value)?.toLowerCase();\n if (!v) return undefined;\n if (v === 'urgent' || v === 'high') return 'high';\n if (v === 'medium' || v === 'normal') return 'medium';\n if (v === 'low') return 'low';\n return undefined;\n}\n\n/**\n * Normalize widget data into the board's { columns, tasks } shape.\n *\n * KanbanWidget previously required the exact canonical shape and rendered\n * \"No board data\" for any DataSink/parser output that was a plain array of\n * rows (which is what parsers actually emit) — BOFF-4110. This adapts a flat\n * array into columns (distinct values of a status/column field, in first-seen\n * order) plus one card per row, honoring config field mappings with fallbacks.\n */\nfunction normalizeKanban(\n data: unknown,\n config?: Record<string, unknown>\n): { columns: KanbanColumn[]; tasks: KanbanTask[] } {\n // Canonical shape passes through untouched (back-compat).\n if (data && typeof data === 'object' && !Array.isArray(data) && Array.isArray((data as KanbanData).columns)) {\n const d = data as KanbanData;\n return { columns: d.columns ?? [], tasks: d.tasks ?? [] };\n }\n\n const rows = asRows(data);\n if (rows.length === 0) return { columns: [], tasks: [] };\n\n const sample = rows[0];\n const columnField = firstPresent(sample, [\n asString(config?.columnField),\n asString(config?.groupField),\n asString(config?.statusField),\n ...COLUMN_FIELD_CANDIDATES,\n asString(config?.xAxisField),\n asString(config?.labelField),\n ]);\n if (!columnField) return { columns: [], tasks: [] };\n\n const titleField = firstPresent(sample, [\n asString(config?.cardTitleField),\n asString(config?.titleField),\n ...TITLE_FIELD_CANDIDATES,\n asString(config?.labelField),\n ]);\n\n const columnTitles: Record<string, string> = {};\n const columnOrder: string[] = [];\n const tasks: KanbanTask[] = rows.map((row, i) => {\n const columnRaw = row[columnField];\n const column = columnRaw == null || columnRaw === '' ? 'Uncategorized' : String(columnRaw);\n if (!(column in columnTitles)) {\n columnTitles[column] = column;\n columnOrder.push(column);\n }\n const idRaw = row.id ?? row.identifier ?? row.key;\n const labels = Array.isArray(row.labels) ? row.labels.filter((l): l is string => typeof l === 'string') : undefined;\n return {\n id: idRaw != null ? String(idRaw) : `task-${i}`,\n title: titleField && row[titleField] != null ? String(row[titleField]) : `Item ${i + 1}`,\n column,\n assignee: asString(row.assignee),\n priority: normalizePriority(row.priority ?? row.priorityLabel),\n labels: labels && labels.length > 0 ? labels : undefined,\n };\n });\n\n const columns: KanbanColumn[] = columnOrder.map((id) => ({ id, title: columnTitles[id] ?? id }));\n return { columns, tasks };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget, data, onClick }) {\n const { t } = useI18n();\n const config = widget.config as Record<string, unknown> | undefined;\n const { columns, tasks } = useMemo(() => normalizeKanban(data, config), [data, config]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>{t('bigconsole.widget.kanban.emptyState', 'No board data')}</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-tertiary\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && (\n <p className=\"text-[10px] text-text-tertiary text-center py-4\">\n {t('bigconsole.widget.kanban.noTasks', 'No tasks')}\n </p>\n )}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":";;;;AA6CA,SAAS,EAAiB,GAA2B;AACnD,SAAQ,GAAR;EACE,KAAK,OACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,MACH,QAAO;EACT,QACE,QAAO;;;AAQb,IAAM,IAA0B;CAAC;CAAU;CAAS;CAAU;CAAS;CAAY;CAAS;CAAQ,EAC9F,IAAyB;CAAC;CAAS;CAAQ;CAAW;CAAS;CAAc;CAAK;AAExF,SAAS,EAAS,GAAoC;AACpD,QAAO,OAAO,KAAU,YAAY,EAAM,SAAS,IAAI,IAAQ,KAAA;;AAIjE,SAAS,EAAO,GAA0C;CACxD,IAAM,KAAe,MACnB,EAAI,QAAQ,MAAoC,CAAC,CAAC,KAAK,OAAO,KAAM,YAAY,CAAC,MAAM,QAAQ,EAAE,CAAC;AAEpG,KAAI,MAAM,QAAQ,EAAK,CAAE,QAAO,EAAY,EAAK;AAEjD,KAAI,KAAQ,OAAO,KAAS,UAAU;EACpC,IAAM,IAAM;AACZ,OAAK,IAAM,KAAO;GAAC;GAAS;GAAQ;GAAS;GAAQ;GAAS;GAAW;GAAU,CACjF,KAAI,MAAM,QAAQ,EAAI,GAAK,CAAE,QAAO,EAAY,EAAI,GAAkB;EAExE,IAAM,IAAa,OAAO,OAAO,EAAI,CAAC,MAAM,MAAM,MAAM,QAAQ,EAAE,CAAC;AACnE,MAAI,MAAM,QAAQ,EAAW,CAAE,QAAO,EAAY,EAAW;;AAE/D,QAAO,EAAE;;AAIX,SAAS,EAAa,GAA8B,GAAkD;AACpG,MAAK,IAAM,KAAO,EAChB,KAAI,KAAO,KAAO,EAAK,QAAO;;AAKlC,SAAS,EAAkB,GAAwC;CACjE,IAAM,IAAI,EAAS,EAAM,EAAE,aAAa;AACnC,QACL;MAAI,MAAM,YAAY,MAAM,OAAQ,QAAO;AAC3C,MAAI,MAAM,YAAY,MAAM,SAAU,QAAO;AAC7C,MAAI,MAAM,MAAO,QAAO;;;AAa1B,SAAS,EACP,GACA,GACkD;AAElD,KAAI,KAAQ,OAAO,KAAS,YAAY,CAAC,MAAM,QAAQ,EAAK,IAAI,MAAM,QAAS,EAAoB,QAAQ,EAAE;EAC3G,IAAM,IAAI;AACV,SAAO;GAAE,SAAS,EAAE,WAAW,EAAE;GAAE,OAAO,EAAE,SAAS,EAAE;GAAE;;CAG3D,IAAM,IAAO,EAAO,EAAK;AACzB,KAAI,EAAK,WAAW,EAAG,QAAO;EAAE,SAAS,EAAE;EAAE,OAAO,EAAE;EAAE;CAExD,IAAM,IAAS,EAAK,IACd,IAAc,EAAa,GAAQ;EACvC,EAAS,GAAQ,YAAY;EAC7B,EAAS,GAAQ,WAAW;EAC5B,EAAS,GAAQ,YAAY;EAC7B,GAAG;EACH,EAAS,GAAQ,WAAW;EAC5B,EAAS,GAAQ,WAAW;EAC7B,CAAC;AACF,KAAI,CAAC,EAAa,QAAO;EAAE,SAAS,EAAE;EAAE,OAAO,EAAE;EAAE;CAEnD,IAAM,IAAa,EAAa,GAAQ;EACtC,EAAS,GAAQ,eAAe;EAChC,EAAS,GAAQ,WAAW;EAC5B,GAAG;EACH,EAAS,GAAQ,WAAW;EAC7B,CAAC,EAEI,IAAuC,EAAE,EACzC,IAAwB,EAAE,EAC1B,IAAsB,EAAK,KAAK,GAAK,MAAM;EAC/C,IAAM,IAAY,EAAI,IAChB,IAAS,KAAa,QAAQ,MAAc,KAAK,kBAAkB,OAAO,EAAU;AAC1F,EAAM,KAAU,MACd,EAAa,KAAU,GACvB,EAAY,KAAK,EAAO;EAE1B,IAAM,IAAQ,EAAI,MAAM,EAAI,cAAc,EAAI,KACxC,IAAS,MAAM,QAAQ,EAAI,OAAO,GAAG,EAAI,OAAO,QAAQ,MAAmB,OAAO,KAAM,SAAS,GAAG,KAAA;AAC1G,SAAO;GACL,IAAI,KAAS,OAAuB,QAAQ,MAAxB,OAAO,EAAM;GACjC,OAAO,KAAc,EAAI,MAAe,OAAO,OAAO,EAAI,GAAY,GAAG,QAAQ,IAAI;GACrF;GACA,UAAU,EAAS,EAAI,SAAS;GAChC,UAAU,EAAkB,EAAI,YAAY,EAAI,cAAc;GAC9D,QAAQ,KAAU,EAAO,SAAS,IAAI,IAAS,KAAA;GAChD;GACD;AAGF,QAAO;EAAE,SADuB,EAAY,KAAK,OAAQ;GAAE;GAAI,OAAO,EAAa,MAAO;GAAI,EAAE;EAC9E;EAAO;;AAO3B,IAAa,IAAsC,EAAK,SAAsB,EAAE,WAAQ,SAAM,cAAW;CACvG,IAAM,EAAE,SAAM,GAAS,EACjB,IAAS,EAAO,QAChB,EAAE,YAAS,aAAU,QAAc,EAAgB,GAAM,EAAO,EAAE,CAAC,GAAM,EAAO,CAAC,EAGjF,IAAgB,QAAc;EAClC,IAAM,IAAwC,EAAE;AAIhD,SAHA,EAAQ,SAAS,MAAQ;AACvB,KAAQ,EAAI,MAAM,EAAM,QAAQ,MAAM,EAAE,WAAW,EAAI,GAAG;IAC1D,EACK;IACN,CAAC,GAAS,EAAM,CAAC;AAyBpB,QAvBK,EAAQ,SAwBX,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAQ,KAAK,MAAW;GACvB,IAAM,IAAc,EAAc,EAAO,OAAO,EAAE;AAElD,UACE,kBAAC,OAAD;IAAqB,WAAU;cAA/B,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,iBAAiB,EAAO,SAAS,2BAA2B;OACrE,CAAA;MACF,kBAAC,QAAD;OAAM,WAAU;iBAAkD,EAAO;OAAa,CAAA;MACtF,kBAAC,QAAD;OAAM,WAAU;iBAAsC,EAAY;OAAc,CAAA;MAC5E;QAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAY,KAAK,MAChB,kBAAC,OAAD;MAEE,WAAW;;wBAEL,IAAU,kDAAkD,GAAG;;;MAGrE,eAAe,IAAU,EAAK;gBAPhC,CASE,kBAAC,KAAD;OAAG,WAAU;iBAAsD,EAAK;OAAU,CAAA,EAElF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAW,qCAAqC,EAAiB,EAAK,SAAS;kBAClF,EAAK;QACD,CAAA,EAER,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAK;QACD,CAAA,CAEL;SACF;QAtBC,EAAK,GAsBN,CACN,EAED,EAAY,WAAW,KACtB,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,oCAAoC,WAAW;MAChD,CAAA,CAEF;OACF;MA9CI,EAAO,GA8CX;IAER;EACE,CAAA,GA5EJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,QAAO;IACP,SAAQ;cAER,kBAAC,QAAD;KACE,eAAc;KACd,gBAAe;KACf,aAAa;KACb,GAAE;KACF,CAAA;IACE,CAAA,EACN,kBAAC,KAAD,EAAA,UAAI,EAAE,uCAAuC,gBAAgB,EAAK,CAAA,CAC9D;;EACF,CAAA;EA6DV"}
|
|
1
|
+
{"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-status-error-bg/20 text-status-error-text';\n case 'medium':\n return 'bg-status-warning-bg/20 text-[var(--color-status-warning-text)]';\n case 'low':\n return 'bg-status-success-bg/20 text-status-success-text';\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// Field names we try, in order, when the widget config doesn't name one\n// explicitly. A DataSink/parser typically emits a flat array of rows (e.g. the\n// Linear \"Kanban board\" parser -> [{ id, title, status, assignee, ... }]), so\n// the board is derived from a status/column field on those rows.\nconst COLUMN_FIELD_CANDIDATES = ['status', 'state', 'column', 'stage', 'category', 'phase', 'group'];\nconst TITLE_FIELD_CANDIDATES = ['title', 'name', 'summary', 'label', 'identifier', 'id'];\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/** Coerce parser output into an array of object rows (handles common envelopes). */\nfunction asRows(data: unknown): Record<string, unknown>[] {\n const onlyObjects = (arr: unknown[]): Record<string, unknown>[] =>\n arr.filter((r): r is Record<string, unknown> => !!r && typeof r === 'object' && !Array.isArray(r));\n\n if (Array.isArray(data)) return onlyObjects(data);\n\n if (data && typeof data === 'object') {\n const obj = data as Record<string, unknown>;\n for (const key of ['tasks', 'rows', 'items', 'data', 'nodes', 'results', 'records']) {\n if (Array.isArray(obj[key])) return onlyObjects(obj[key] as unknown[]);\n }\n const firstArray = Object.values(obj).find((v) => Array.isArray(v));\n if (Array.isArray(firstArray)) return onlyObjects(firstArray);\n }\n return [];\n}\n\n/** First key that actually exists on the sample row. */\nfunction firstPresent(row: Record<string, unknown>, keys: (string | undefined)[]): string | undefined {\n for (const key of keys) {\n if (key && key in row) return key;\n }\n return undefined;\n}\n\nfunction normalizePriority(value: unknown): KanbanTask['priority'] {\n const v = asString(value)?.toLowerCase();\n if (!v) return undefined;\n if (v === 'urgent' || v === 'high') return 'high';\n if (v === 'medium' || v === 'normal') return 'medium';\n if (v === 'low') return 'low';\n return undefined;\n}\n\n/**\n * Normalize widget data into the board's { columns, tasks } shape.\n *\n * KanbanWidget previously required the exact canonical shape and rendered\n * \"No board data\" for any DataSink/parser output that was a plain array of\n * rows (which is what parsers actually emit) — BOFF-4110. This adapts a flat\n * array into columns (distinct values of a status/column field, in first-seen\n * order) plus one card per row, honoring config field mappings with fallbacks.\n */\nfunction normalizeKanban(\n data: unknown,\n config?: Record<string, unknown>\n): { columns: KanbanColumn[]; tasks: KanbanTask[] } {\n // Canonical shape passes through untouched (back-compat).\n if (data && typeof data === 'object' && !Array.isArray(data) && Array.isArray((data as KanbanData).columns)) {\n const d = data as KanbanData;\n return { columns: d.columns ?? [], tasks: d.tasks ?? [] };\n }\n\n const rows = asRows(data);\n if (rows.length === 0) return { columns: [], tasks: [] };\n\n const sample = rows[0];\n const columnField = firstPresent(sample, [\n asString(config?.columnField),\n asString(config?.groupField),\n asString(config?.statusField),\n ...COLUMN_FIELD_CANDIDATES,\n asString(config?.xAxisField),\n asString(config?.labelField),\n ]);\n if (!columnField) return { columns: [], tasks: [] };\n\n const titleField = firstPresent(sample, [\n asString(config?.cardTitleField),\n asString(config?.titleField),\n ...TITLE_FIELD_CANDIDATES,\n asString(config?.labelField),\n ]);\n\n const columnTitles: Record<string, string> = {};\n const columnOrder: string[] = [];\n const tasks: KanbanTask[] = rows.map((row, i) => {\n const columnRaw = row[columnField];\n const column = columnRaw == null || columnRaw === '' ? 'Uncategorized' : String(columnRaw);\n if (!(column in columnTitles)) {\n columnTitles[column] = column;\n columnOrder.push(column);\n }\n const idRaw = row.id ?? row.identifier ?? row.key;\n const labels = Array.isArray(row.labels) ? row.labels.filter((l): l is string => typeof l === 'string') : undefined;\n return {\n id: idRaw != null ? String(idRaw) : `task-${i}`,\n title: titleField && row[titleField] != null ? String(row[titleField]) : `Item ${i + 1}`,\n column,\n assignee: asString(row.assignee),\n priority: normalizePriority(row.priority ?? row.priorityLabel),\n labels: labels && labels.length > 0 ? labels : undefined,\n };\n });\n\n const columns: KanbanColumn[] = columnOrder.map((id) => ({ id, title: columnTitles[id] ?? id }));\n return { columns, tasks };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget, data, onClick }) {\n const { t } = useI18n();\n const config = widget.config as Record<string, unknown> | undefined;\n const { columns, tasks } = useMemo(() => normalizeKanban(data, config), [data, config]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg className=\"w-12 h-12 mx-auto mb-2 text-text-muted\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>{t('bigconsole.widget.kanban.emptyState', 'No board data')}</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-muted\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && (\n <p className=\"text-[10px] text-text-muted text-center py-4\">\n {t('bigconsole.widget.kanban.noTasks', 'No tasks')}\n </p>\n )}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":"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"}
|
|
@@ -154,7 +154,7 @@ var c = n(function({ widget: n, data: c, onClick: l, onDrilldown: u }) {
|
|
|
154
154
|
n.target !== void 0 && /* @__PURE__ */ i("div", {
|
|
155
155
|
className: "mt-2",
|
|
156
156
|
children: [/* @__PURE__ */ i("div", {
|
|
157
|
-
className: "flex justify-between text-[10px] text-text-
|
|
157
|
+
className: "flex justify-between text-[10px] text-text-muted mb-0.5",
|
|
158
158
|
children: [/* @__PURE__ */ r("span", { children: "vs target" }), /* @__PURE__ */ i("span", { children: [(n.value / n.target * 100).toFixed(0), "%"] })]
|
|
159
159
|
}), /* @__PURE__ */ r("div", {
|
|
160
160
|
className: "h-1 bg-bg-sunken rounded-full overflow-hidden",
|
|
@@ -172,7 +172,7 @@ var c = n(function({ widget: n, data: c, onClick: l, onDrilldown: u }) {
|
|
|
172
172
|
children: /* @__PURE__ */ i("div", {
|
|
173
173
|
className: "text-center",
|
|
174
174
|
children: [/* @__PURE__ */ r("svg", {
|
|
175
|
-
className: "w-12 h-12 mx-auto mb-2 text-text-
|
|
175
|
+
className: "w-12 h-12 mx-auto mb-2 text-text-muted",
|
|
176
176
|
fill: "none",
|
|
177
177
|
stroke: "currentColor",
|
|
178
178
|
viewBox: "0 0 24 24",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KPIComparisonWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx"],"sourcesContent":["/**\n * KPIComparisonWidget Component\n *\n * Displays multiple KPIs in a grid for comparison.\n */\n\nimport { type FC, memo } from 'react';\nimport type { Widget } from '../../../types';\nimport { type ConditionalRule, resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIComparisonWidgetProps {\n widget: Widget;\n data?: KPIComparisonData;\n onClick?: (kpi: KPIItem) => void;\n onDrilldown?: (data: Record<string, unknown>) => void;\n}\n\nexport interface KPIItem {\n id: string;\n label: string;\n value: number;\n previousValue?: number;\n target?: number;\n unit?: string;\n trend?: 'up' | 'down' | 'stable';\n trendValue?: number;\n}\n\nexport interface KPIComparisonData {\n kpis?: KPIItem[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number, unit?: string): string {\n if (unit === '$') {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n }\n if (unit === '%') {\n return `${value.toFixed(1)}%`;\n }\n return new Intl.NumberFormat('en-US', {\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction getTrendColor(trend?: string, trendValue?: number): string {\n if (!trend || trend === 'stable') return 'text-text-secondary';\n if (trend === 'up' && (trendValue ?? 0) >= 0) return 'text-status-success-text';\n if (trend === 'down' && (trendValue ?? 0) < 0) return 'text-status-error-text';\n return 'text-text-secondary';\n}\n\nfunction getTrendIcon(trend?: string): string {\n if (trend === 'up') return '↑';\n if (trend === 'down') return '↓';\n return '→';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIComparisonWidget: FC<KPIComparisonWidgetProps> = memo(function KPIComparisonWidget({\n widget,\n data,\n onClick,\n onDrilldown,\n}) {\n const hasDrilldown = !!widget.drilldown?.enabled;\n const config = widget.config as Record<string, unknown> | undefined;\n const conditionalRules = config?.conditionalRules as ConditionalRule[] | undefined;\n\n // Build KPIs array from data\n // Support multiple formats:\n // 1. data.kpis array (standard format)\n // 2. Single value from data.processed (e.g., 125000 from DataSink)\n // 3. data is a number/string directly\n // 4. Nested summary object (e.g., {sales: [...], summary: {totalSales: 123}})\n // 5. Array of objects from DataSink (e.g., [{month: \"Jan\", revenue: 45000}, ...])\n let kpis: KPIItem[] = [];\n\n // Get configured value field from widget config (for DataSink data)\n const valueField = (config?.valueField as string) || (config?.value_field as string) || 'value';\n const labelField = (config?.labelField as string) || (config?.label_field as string) || 'label';\n\n // Helper to convert object with numeric values to KPIs\n const objectToKpis = (obj: Record<string, unknown>): KPIItem[] => {\n const entries = Object.entries(obj);\n return entries\n .filter(([, v]) => typeof v === 'number')\n .map(([key, value]) => ({\n id: key,\n label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: value as number,\n unit:\n key.toLowerCase().includes('sales') ||\n key.toLowerCase().includes('revenue') ||\n key.toLowerCase().includes('value')\n ? '$'\n : undefined,\n }));\n };\n\n // Helper to extract KPI from a single data row using config fields\n const extractKpiFromRow = (row: Record<string, unknown>, index: number): KPIItem | null => {\n // Try to get value from configured field or common field names\n const valueCandidates = [valueField, 'value', 'revenue', 'amount', 'total', 'count', 'sales', 'growth'];\n let value: number | null = null;\n\n for (const field of valueCandidates) {\n if (typeof row[field] === 'number') {\n value = row[field] as number;\n break;\n }\n if (typeof row[field] === 'string' && !isNaN(parseFloat(row[field] as string))) {\n value = parseFloat(row[field] as string);\n break;\n }\n }\n\n if (value === null) {\n // Try to find any numeric field\n const numericEntry = Object.entries(row).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n value = numericEntry[1] as number;\n }\n }\n\n if (value === null) return null;\n\n // Try to get label from configured field or common field names\n const labelCandidates = [labelField, 'label', 'name', 'title', 'month', 'region', 'category'];\n let label = `Item ${index + 1}`;\n\n for (const field of labelCandidates) {\n if (row[field] && typeof row[field] === 'string') {\n label = row[field] as string;\n break;\n }\n }\n\n // Check for growth/trend fields\n const growthValue = row.growth ?? row.change ?? row.trend ?? row.percentChange;\n const trend =\n typeof growthValue === 'number' ? (growthValue > 0 ? 'up' : growthValue < 0 ? 'down' : 'stable') : undefined;\n\n return {\n id: `kpi-${index}`,\n label,\n value,\n unit:\n valueField.toLowerCase().includes('revenue') || valueField.toLowerCase().includes('sales') ? '$' : undefined,\n trend: trend as 'up' | 'down' | 'stable' | undefined,\n trendValue: typeof growthValue === 'number' ? growthValue : undefined,\n };\n };\n\n if (data?.kpis && Array.isArray(data.kpis)) {\n // Standard KPIs array\n kpis = data.kpis;\n } else if (Array.isArray(data) && data.length > 0) {\n // NEW: Handle array of objects from DataSink\n // Extract KPIs from each row, up to 4 items for display\n const maxKpis = 4;\n for (let i = 0; i < Math.min(data.length, maxKpis); i++) {\n const row = data[i] as Record<string, unknown>;\n if (row && typeof row === 'object') {\n const kpi = extractKpiFromRow(row, i);\n if (kpi) kpis.push(kpi);\n }\n }\n\n // If we couldn't extract KPIs from rows, try to aggregate numeric fields\n if (kpis.length === 0 && data.length > 0) {\n const firstRow = data[0] as Record<string, unknown>;\n const numericFields = Object.entries(firstRow).filter(([, v]) => typeof v === 'number');\n\n // Create KPIs by aggregating (summing) each numeric field across all rows\n kpis = numericFields.slice(0, maxKpis).map(([field]) => {\n const sum = data.reduce((acc, row) => {\n const val = (row as Record<string, unknown>)[field];\n return acc + (typeof val === 'number' ? val : 0);\n }, 0);\n\n return {\n id: field,\n label: field.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: sum,\n unit: field.toLowerCase().includes('revenue') || field.toLowerCase().includes('sales') ? '$' : undefined,\n };\n });\n }\n } else if (typeof data === 'number' || typeof data === 'string') {\n // Single value - create a KPI from it using widget config\n const value = typeof data === 'string' ? parseFloat(data) : data;\n if (!isNaN(value)) {\n kpis = [\n {\n id: 'primary',\n label: ((config?.subtitle as string) || widget.title || 'Value').trim(),\n value,\n unit: config?.format === 'currency' ? '$' : config?.format === 'percent' ? '%' : undefined,\n },\n ];\n }\n } else if (data && typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as Record<string, unknown>;\n\n // First, check if there's a nested 'summary' object with numeric values\n if ('summary' in dataObj && dataObj.summary && typeof dataObj.summary === 'object') {\n kpis = objectToKpis(dataObj.summary as Record<string, unknown>);\n }\n\n // If no KPIs from summary, try top-level numeric values\n if (kpis.length === 0) {\n kpis = objectToKpis(dataObj);\n }\n }\n\n if (!kpis.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No KPI data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full p-4 overflow-auto\">\n <div className=\"grid grid-cols-2 gap-3\">\n {kpis.map((kpi) => (\n <div\n key={kpi.id}\n className={`\n p-3 rounded-lg border border-border-default bg-bg-sunken/30\n ${onClick || hasDrilldown ? 'cursor-pointer hover:border-action-primary-bg transition-colors' : ''}\n `}\n onClick={() => {\n // Trigger drilldown with KPI data if enabled\n if (hasDrilldown && onDrilldown) {\n onDrilldown({\n kpiId: kpi.id,\n kpiLabel: kpi.label,\n kpiValue: kpi.value,\n kpiUnit: kpi.unit,\n kpiTrend: kpi.trend,\n kpiTrendValue: kpi.trendValue,\n kpiTarget: kpi.target,\n kpiPreviousValue: kpi.previousValue,\n });\n }\n onClick?.(kpi);\n }}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary truncate mb-1\">{kpi.label}</p>\n\n {/* Value (with optional threshold-based conditional color) */}\n <p\n className={`text-xl font-semibold ${\n conditionalStyleToClassName(\n resolveConditionalStyle(kpi.value, conditionalRules, { [kpi.label]: kpi.value, value: kpi.value })\n ) || 'text-text-primary'\n }`}\n >\n {formatValue(kpi.value, kpi.unit)}\n </p>\n\n {/* Trend */}\n {kpi.trendValue !== undefined && (\n <div className={`flex items-center gap-1 mt-1 text-xs ${getTrendColor(kpi.trend, kpi.trendValue)}`}>\n <span>{getTrendIcon(kpi.trend)}</span>\n <span>\n {kpi.trendValue >= 0 ? '+' : ''}\n {kpi.trendValue.toFixed(1)}%\n </span>\n </div>\n )}\n\n {/* Target progress */}\n {kpi.target !== undefined && (\n <div className=\"mt-2\">\n <div className=\"flex justify-between text-[10px] text-text-tertiary mb-0.5\">\n <span>vs target</span>\n <span>{((kpi.value / kpi.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${kpi.value >= kpi.target ? 'bg-status-success-bg' : 'bg-action-primary-bg'}`}\n style={{ width: `${Math.min((kpi.value / kpi.target) * 100, 100)}%` }}\n />\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIComparisonWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"KPIComparisonWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx"],"sourcesContent":["/**\n * KPIComparisonWidget Component\n *\n * Displays multiple KPIs in a grid for comparison.\n */\n\nimport { type FC, memo } from 'react';\nimport type { Widget } from '../../../types';\nimport { type ConditionalRule, resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIComparisonWidgetProps {\n widget: Widget;\n data?: KPIComparisonData;\n onClick?: (kpi: KPIItem) => void;\n onDrilldown?: (data: Record<string, unknown>) => void;\n}\n\nexport interface KPIItem {\n id: string;\n label: string;\n value: number;\n previousValue?: number;\n target?: number;\n unit?: string;\n trend?: 'up' | 'down' | 'stable';\n trendValue?: number;\n}\n\nexport interface KPIComparisonData {\n kpis?: KPIItem[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number, unit?: string): string {\n if (unit === '$') {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n }\n if (unit === '%') {\n return `${value.toFixed(1)}%`;\n }\n return new Intl.NumberFormat('en-US', {\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction getTrendColor(trend?: string, trendValue?: number): string {\n if (!trend || trend === 'stable') return 'text-text-secondary';\n if (trend === 'up' && (trendValue ?? 0) >= 0) return 'text-status-success-text';\n if (trend === 'down' && (trendValue ?? 0) < 0) return 'text-status-error-text';\n return 'text-text-secondary';\n}\n\nfunction getTrendIcon(trend?: string): string {\n if (trend === 'up') return '↑';\n if (trend === 'down') return '↓';\n return '→';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIComparisonWidget: FC<KPIComparisonWidgetProps> = memo(function KPIComparisonWidget({\n widget,\n data,\n onClick,\n onDrilldown,\n}) {\n const hasDrilldown = !!widget.drilldown?.enabled;\n const config = widget.config as Record<string, unknown> | undefined;\n const conditionalRules = config?.conditionalRules as ConditionalRule[] | undefined;\n\n // Build KPIs array from data\n // Support multiple formats:\n // 1. data.kpis array (standard format)\n // 2. Single value from data.processed (e.g., 125000 from DataSink)\n // 3. data is a number/string directly\n // 4. Nested summary object (e.g., {sales: [...], summary: {totalSales: 123}})\n // 5. Array of objects from DataSink (e.g., [{month: \"Jan\", revenue: 45000}, ...])\n let kpis: KPIItem[] = [];\n\n // Get configured value field from widget config (for DataSink data)\n const valueField = (config?.valueField as string) || (config?.value_field as string) || 'value';\n const labelField = (config?.labelField as string) || (config?.label_field as string) || 'label';\n\n // Helper to convert object with numeric values to KPIs\n const objectToKpis = (obj: Record<string, unknown>): KPIItem[] => {\n const entries = Object.entries(obj);\n return entries\n .filter(([, v]) => typeof v === 'number')\n .map(([key, value]) => ({\n id: key,\n label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: value as number,\n unit:\n key.toLowerCase().includes('sales') ||\n key.toLowerCase().includes('revenue') ||\n key.toLowerCase().includes('value')\n ? '$'\n : undefined,\n }));\n };\n\n // Helper to extract KPI from a single data row using config fields\n const extractKpiFromRow = (row: Record<string, unknown>, index: number): KPIItem | null => {\n // Try to get value from configured field or common field names\n const valueCandidates = [valueField, 'value', 'revenue', 'amount', 'total', 'count', 'sales', 'growth'];\n let value: number | null = null;\n\n for (const field of valueCandidates) {\n if (typeof row[field] === 'number') {\n value = row[field] as number;\n break;\n }\n if (typeof row[field] === 'string' && !isNaN(parseFloat(row[field] as string))) {\n value = parseFloat(row[field] as string);\n break;\n }\n }\n\n if (value === null) {\n // Try to find any numeric field\n const numericEntry = Object.entries(row).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n value = numericEntry[1] as number;\n }\n }\n\n if (value === null) return null;\n\n // Try to get label from configured field or common field names\n const labelCandidates = [labelField, 'label', 'name', 'title', 'month', 'region', 'category'];\n let label = `Item ${index + 1}`;\n\n for (const field of labelCandidates) {\n if (row[field] && typeof row[field] === 'string') {\n label = row[field] as string;\n break;\n }\n }\n\n // Check for growth/trend fields\n const growthValue = row.growth ?? row.change ?? row.trend ?? row.percentChange;\n const trend =\n typeof growthValue === 'number' ? (growthValue > 0 ? 'up' : growthValue < 0 ? 'down' : 'stable') : undefined;\n\n return {\n id: `kpi-${index}`,\n label,\n value,\n unit:\n valueField.toLowerCase().includes('revenue') || valueField.toLowerCase().includes('sales') ? '$' : undefined,\n trend: trend as 'up' | 'down' | 'stable' | undefined,\n trendValue: typeof growthValue === 'number' ? growthValue : undefined,\n };\n };\n\n if (data?.kpis && Array.isArray(data.kpis)) {\n // Standard KPIs array\n kpis = data.kpis;\n } else if (Array.isArray(data) && data.length > 0) {\n // NEW: Handle array of objects from DataSink\n // Extract KPIs from each row, up to 4 items for display\n const maxKpis = 4;\n for (let i = 0; i < Math.min(data.length, maxKpis); i++) {\n const row = data[i] as Record<string, unknown>;\n if (row && typeof row === 'object') {\n const kpi = extractKpiFromRow(row, i);\n if (kpi) kpis.push(kpi);\n }\n }\n\n // If we couldn't extract KPIs from rows, try to aggregate numeric fields\n if (kpis.length === 0 && data.length > 0) {\n const firstRow = data[0] as Record<string, unknown>;\n const numericFields = Object.entries(firstRow).filter(([, v]) => typeof v === 'number');\n\n // Create KPIs by aggregating (summing) each numeric field across all rows\n kpis = numericFields.slice(0, maxKpis).map(([field]) => {\n const sum = data.reduce((acc, row) => {\n const val = (row as Record<string, unknown>)[field];\n return acc + (typeof val === 'number' ? val : 0);\n }, 0);\n\n return {\n id: field,\n label: field.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: sum,\n unit: field.toLowerCase().includes('revenue') || field.toLowerCase().includes('sales') ? '$' : undefined,\n };\n });\n }\n } else if (typeof data === 'number' || typeof data === 'string') {\n // Single value - create a KPI from it using widget config\n const value = typeof data === 'string' ? parseFloat(data) : data;\n if (!isNaN(value)) {\n kpis = [\n {\n id: 'primary',\n label: ((config?.subtitle as string) || widget.title || 'Value').trim(),\n value,\n unit: config?.format === 'currency' ? '$' : config?.format === 'percent' ? '%' : undefined,\n },\n ];\n }\n } else if (data && typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as Record<string, unknown>;\n\n // First, check if there's a nested 'summary' object with numeric values\n if ('summary' in dataObj && dataObj.summary && typeof dataObj.summary === 'object') {\n kpis = objectToKpis(dataObj.summary as Record<string, unknown>);\n }\n\n // If no KPIs from summary, try top-level numeric values\n if (kpis.length === 0) {\n kpis = objectToKpis(dataObj);\n }\n }\n\n if (!kpis.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg className=\"w-12 h-12 mx-auto mb-2 text-text-muted\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No KPI data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full p-4 overflow-auto\">\n <div className=\"grid grid-cols-2 gap-3\">\n {kpis.map((kpi) => (\n <div\n key={kpi.id}\n className={`\n p-3 rounded-lg border border-border-default bg-bg-sunken/30\n ${onClick || hasDrilldown ? 'cursor-pointer hover:border-action-primary-bg transition-colors' : ''}\n `}\n onClick={() => {\n // Trigger drilldown with KPI data if enabled\n if (hasDrilldown && onDrilldown) {\n onDrilldown({\n kpiId: kpi.id,\n kpiLabel: kpi.label,\n kpiValue: kpi.value,\n kpiUnit: kpi.unit,\n kpiTrend: kpi.trend,\n kpiTrendValue: kpi.trendValue,\n kpiTarget: kpi.target,\n kpiPreviousValue: kpi.previousValue,\n });\n }\n onClick?.(kpi);\n }}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary truncate mb-1\">{kpi.label}</p>\n\n {/* Value (with optional threshold-based conditional color) */}\n <p\n className={`text-xl font-semibold ${\n conditionalStyleToClassName(\n resolveConditionalStyle(kpi.value, conditionalRules, { [kpi.label]: kpi.value, value: kpi.value })\n ) || 'text-text-primary'\n }`}\n >\n {formatValue(kpi.value, kpi.unit)}\n </p>\n\n {/* Trend */}\n {kpi.trendValue !== undefined && (\n <div className={`flex items-center gap-1 mt-1 text-xs ${getTrendColor(kpi.trend, kpi.trendValue)}`}>\n <span>{getTrendIcon(kpi.trend)}</span>\n <span>\n {kpi.trendValue >= 0 ? '+' : ''}\n {kpi.trendValue.toFixed(1)}%\n </span>\n </div>\n )}\n\n {/* Target progress */}\n {kpi.target !== undefined && (\n <div className=\"mt-2\">\n <div className=\"flex justify-between text-[10px] text-text-muted mb-0.5\">\n <span>vs target</span>\n <span>{((kpi.value / kpi.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${kpi.value >= kpi.target ? 'bg-status-success-bg' : 'bg-action-primary-bg'}`}\n style={{ width: `${Math.min((kpi.value / kpi.target) * 100, 100)}%` }}\n />\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIComparisonWidget;\n"],"mappings":"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"}
|