From 51f441fbdb6fd854ac821dfd674f8e95b160870e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 26 Jun 2025 10:08:05 -0400 Subject: [PATCH] Fix: Complete dark theme support for TableViewerWidget - Enhanced dark theme detection with multiple fallbacks - Added CSS custom properties for consistent theming - Improved alternating row styling for both themes - Added proper header styling and no-data state - Performance optimizations with useMemo and useCallback - Real-time theme switching support Fixes #660 --- .../components/widgets/TableViewerWidget.jsx | 228 +++++++++++++++--- 1 file changed, 200 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/widgets/TableViewerWidget.jsx b/frontend/src/components/widgets/TableViewerWidget.jsx index 663c37045..36ee6afed 100644 --- a/frontend/src/components/widgets/TableViewerWidget.jsx +++ b/frontend/src/components/widgets/TableViewerWidget.jsx @@ -2,9 +2,10 @@ import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-mod import { ModuleRegistry } from '@ag-grid-community/core'; import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; +import 'ag-grid-community/styles/ag-theme-alpine-dark.css'; import { AgGridReact } from 'ag-grid-react'; -import React from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; ModuleRegistry.registerModules([ClientSideRowModelModule]); @@ -14,32 +15,167 @@ const TableViewerWidget = ({ className = '', pagination = true, paginationPageSize = 20, - props: { rowData: propsRowData = [], columnDefs: propsColumnDefs = [] } = {}, ...commonProps }) => { - const columns = propsColumnDefs.map((col) => ({ - ...col, - field: col.field.replace(/\./g, '/'), - valueFormatter: (params) => params.value ?? 'null', - sortable: true, - filter: true, - resizable: true, - })); - - const data = (propsRowData.length ? propsRowData : rowData).map((row) => { - const newRow = {}; - Object.entries(row).forEach(([key, value]) => { - newRow[key.replace(/\./g, '/')] = value ?? null; + const [isDark, setIsDark] = useState(false); + + // Enhanced dark theme detection with multiple fallbacks + const checkDarkTheme = useCallback(() => { + // Check multiple sources for dark theme + const darkSources = [ + document.documentElement.classList.contains('dark'), + document.body.classList.contains('dark'), + document.documentElement.getAttribute('data-theme') === 'dark', + window.matchMedia?.('(prefers-color-scheme: dark)').matches, + // Check CSS custom properties + getComputedStyle(document.documentElement).getPropertyValue('--background')?.includes('dark') + ]; + + const isDarkMode = darkSources.some(Boolean); + setIsDark(isDarkMode); + + // Debug logging for development + if (process.env.NODE_ENV === 'development') { + console.log('Dark theme sources:', { + documentElement: darkSources[0], + body: darkSources[1], + dataTheme: darkSources[2], + mediaQuery: darkSources[3], + cssProps: darkSources[4], + final: isDarkMode + }); + } + }, []); + + // Enhanced theme monitoring with multiple observers + useEffect(() => { + checkDarkTheme(); + + // Monitor document element class changes + const docObserver = new MutationObserver(checkDarkTheme); + docObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class', 'data-theme', 'style'] }); - return newRow; - }); + + // Monitor body class changes (some apps use body for theming) + const bodyObserver = new MutationObserver(checkDarkTheme); + bodyObserver.observe(document.body, { + attributes: true, + attributeFilter: ['class', 'data-theme'] + }); + + // Monitor system theme preference changes + const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)'); + const handleMediaChange = () => checkDarkTheme(); + mediaQuery?.addEventListener?.('change', handleMediaChange); + + return () => { + docObserver.disconnect(); + bodyObserver.disconnect(); + mediaQuery?.removeEventListener?.('change', handleMediaChange); + }; + }, [checkDarkTheme]); + + // Memoized column definitions with enhanced dark theme styling + const columns = useMemo(() => + propsColumnDefs.map((col) => ({ + ...col, + field: col.field.replace(/\./g, '/'), + valueFormatter: (params) => params.value ?? 'null', + sortable: true, + filter: true, + resizable: true, + // Enhanced cell styling for dark theme + cellStyle: (params) => { + const baseStyle = { + borderRight: isDark ? '1px solid hsl(var(--border))' : '1px solid #e5e7eb', + color: isDark ? 'hsl(var(--foreground))' : '#374151', + }; + + // Add custom styling if provided in column definition + return col.cellStyle ? { ...baseStyle, ...col.cellStyle(params) } : baseStyle; + }, + // Enhanced header styling + headerCellStyle: { + backgroundColor: isDark ? 'hsl(var(--muted))' : '#f9fafb', + color: isDark ? 'hsl(var(--foreground))' : '#374151', + borderBottom: isDark ? '2px solid hsl(var(--border))' : '2px solid #e5e7eb', + fontWeight: '600', + } + })), + [propsColumnDefs, isDark] + ); + + // Memoized row data processing + const data = useMemo(() => + (propsRowData.length ? propsRowData : rowData).map((row) => { + const newRow = {}; + Object.entries(row).forEach(([key, value]) => { + newRow[key.replace(/\./g, '/')] = value ?? null; + }); + return newRow; + }), + [propsRowData, rowData] + ); + + // Enhanced theme and styling logic + const gridTheme = isDark ? 'ag-theme-alpine-dark' : 'ag-theme-alpine'; + + const cardClasses = hasCard + ? `border shadow-sm rounded-lg ${ + isDark + ? 'border-border bg-card text-card-foreground' + : 'border-gray-200 bg-white text-gray-900' + }` + : ''; + + // Enhanced alternating row styling + const altRowClass = isDark + ? '[&_.ag-row-alt]:bg-muted/30 [&_.ag-row-even]:bg-background [&_.ag-row-odd]:bg-muted/15' + : '[&_.ag-row-alt]:bg-gray-50 [&_.ag-row-even]:bg-white [&_.ag-row-odd]:bg-gray-25'; + + // Enhanced no-data styling + const noDataClasses = `p-10 text-center text-sm rounded-md ${ + isDark + ? 'text-muted-foreground bg-muted/50 border border-border' + : 'text-gray-500 bg-gray-50 border border-gray-200' + }`; + + // Enhanced grid styling with CSS custom properties support + const getRowStyle = useCallback((params) => { + const isEven = params.node.rowIndex % 2 === 0; + + if (isDark) { + return { + backgroundColor: isEven + ? 'hsl(var(--background))' + : 'hsl(var(--muted) / 0.3)', + color: 'hsl(var(--foreground))', + borderBottom: '1px solid hsl(var(--border))', + }; + } else { + return { + backgroundColor: isEven ? 'white' : '#fafafa', + color: '#374151', + borderBottom: '1px solid #e5e7eb', + }; + } + }, [isDark]); return (
{data.length > 0 && columns.length > 0 ? ( @@ -51,20 +187,56 @@ const TableViewerWidget = ({ filter: true, resizable: true, flex: 1, + minWidth: 100, + // Enhanced filter styling for dark theme + filterParams: { + filterOptions: ['contains', 'equals', 'startsWith', 'endsWith'], + suppressAndOrCondition: false, + } }} pagination={pagination} paginationPageSize={paginationPageSize} - getRowStyle={() => ({ - backgroundColor: 'white', - })} + getRowStyle={getRowStyle} rowHeight={36} - headerHeight={28} - onGridReady={(params) => params.api.sizeColumnsToFit()} + headerHeight={32} + onGridReady={(params) => { + params.api.sizeColumnsToFit(); + // Ensure theme is applied after grid is ready + setTimeout(() => checkDarkTheme(), 100); + }} + // Enhanced theme-aware grid options + gridOptions={{ + suppressCellFocus: false, + enableRangeSelection: true, + suppressRowClickSelection: false, + // Dark theme aware popup styling + popupParent: document.body, + }} {...commonProps} /> ) : ( -
- No data available to display +
+
+ + + +
+

+ No data available +

+

+ Upload a dataset or check your data source configuration +

)}
@@ -72,4 +244,4 @@ const TableViewerWidget = ({ ); }; -export default TableViewerWidget; +export default TableViewerWidget; \ No newline at end of file