From 41bf24420b15121004e8ff1b374430f3dac2d2a5 Mon Sep 17 00:00:00 2001 From: Amin Khorramii Date: Sat, 21 Jun 2025 01:27:51 +0200 Subject: [PATCH 1/3] phase1 completed --- .../src/components/tabs/VisualizationTab.tsx | 313 +++++++----------- .../tabs/visualization/ChartConfigPanel.tsx | 41 ++- .../visualization/DuckDBTableSelector.tsx | 213 ++++++++++++ .../panels/ChartGeneratorPanel.tsx | 290 ++++++++++------ frontend/src/hooks/chart/useMosaicQuery.ts | 285 ++++++++++++++++ 5 files changed, 849 insertions(+), 293 deletions(-) create mode 100644 frontend/src/components/tabs/visualization/DuckDBTableSelector.tsx create mode 100644 frontend/src/hooks/chart/useMosaicQuery.ts diff --git a/frontend/src/components/tabs/VisualizationTab.tsx b/frontend/src/components/tabs/VisualizationTab.tsx index 4b7f765..f0b2797 100644 --- a/frontend/src/components/tabs/VisualizationTab.tsx +++ b/frontend/src/components/tabs/VisualizationTab.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect } from "react"; import { BarChart4, LineChart, @@ -7,99 +7,34 @@ import { TrendingUp, ChevronLeft, ChevronRight, - ChevronDown, - Database, - FileText, Settings, Copy, ArrowRight, + Clock, + Rows, } from "lucide-react"; import { useAppStore } from "@/store/appStore"; +import { useDuckDBStore } from "@/store/duckDBStore"; import { selectHasFiles } from "@/store/selectors/appSelectors"; import { useChartsStore, ChartType } from "@/store/chartsStore"; import { Button } from "@/components/ui/Button"; +import DuckDBTableSelector from "./visualization/DuckDBTableSelector"; import ChartCanvas from "./visualization/ChartCanvas"; import ChartConfigPanel from "./visualization/ChartConfigPanel"; import ExportModal from "./visualization/ExportModal"; -import ChartGallery from "./visualization/ChartGallery"; - -interface DataSource { - type: "file"; - fileId: string; - fileName: string; - data: any[][]; - columns: string[]; - rowCount: number; +import { useMosaicQuery } from "@/hooks/chart/useMosaicQuery"; + +interface DuckDBTable { + name: string; + rowCount?: number; + isView: boolean; + source: "local" | "motherduck"; + database?: string; + schema?: { name: string; type: string }[]; } -/** - * Minimal compact dropdown for data source selection - */ -const DataSourceDropdown: React.FC<{ - selectedSource: DataSource | null; - onSourceChange: (source: DataSource) => void; -}> = ({ selectedSource, onSourceChange }) => { - const { files } = useAppStore(); - const [isOpen, setIsOpen] = useState(false); - - const dataSources = useMemo(() => { - return files - .filter((file) => file.data && file.data.length > 1) - .map((file) => ({ - type: "file" as const, - fileId: file.id, - fileName: file.fileName, - data: file.data, - columns: file.data[0], - rowCount: file.data.length - 1, - })); - }, [files]); - - return ( -
- - - {isOpen && ( -
- {dataSources.map((source) => ( - - ))} -
- )} -
- ); -}; - /** * Compact chart type selector */ @@ -141,86 +76,99 @@ const ChartTypeRow: React.FC = () => { }; /** - * Main visualization component - clean and minimal + * Performance indicators for query execution + */ +const PerformanceIndicators: React.FC<{ + executionTime?: number; + rowCount?: number; + isExecuting?: boolean; +}> = ({ executionTime, rowCount, isExecuting }) => { + if (!executionTime && !rowCount && !isExecuting) return null; + + return ( +
+ {isExecuting && ( +
+
+ Querying... +
+ )} + {executionTime && !isExecuting && ( +
+ + {executionTime}ms +
+ )} + {rowCount && !isExecuting && ( +
+ + {rowCount.toLocaleString()} rows +
+ )} +
+ ); +}; + +/** + * Main visualization component - updated to use DuckDB directly */ const VisualizationTab: React.FC = () => { const hasFiles = useAppStore(selectHasFiles); - const { setActiveTab, activeFileId, setActiveFile, files } = useAppStore(); + const { setActiveTab } = useAppStore(); + const { registeredTables } = useDuckDBStore(); const { currentChart, createNewChart, loadChartsFromStorage, toggleExportModal, + updateCurrentChart, } = useChartsStore(); - const [selectedDataSource, setSelectedDataSource] = - useState(null); - const [selectedColumns, setSelectedColumns] = useState([]); - const [filteredData, setFilteredData] = useState(null); + const [selectedTable, setSelectedTable] = useState(null); const [showLeftPanel, setShowLeftPanel] = useState(true); + const [queryResult, setQueryResult] = useState(null); + + const { isExecuting, progress, lastResult } = useMosaicQuery(); // Load saved charts on mount useEffect(() => { loadChartsFromStorage(); }, [loadChartsFromStorage]); - // Auto-select first file + // Auto-select first table if available useEffect(() => { - if (!selectedDataSource && files.length > 0) { - const firstFileWithData = files.find( - (file) => file.id === activeFileId - ); - if (firstFileWithData) { - const source: DataSource = { - type: "file", - fileId: firstFileWithData.id, - fileName: firstFileWithData.fileName, - data: firstFileWithData.data, - columns: firstFileWithData.data[0], - rowCount: firstFileWithData.data.length - 1, - }; - setSelectedDataSource(source); - setSelectedColumns(source.columns.slice(0, 2)); - } + if (!selectedTable && registeredTables.size > 0) { + const firstTable = Array.from(registeredTables.keys())[0]; + setSelectedTable({ + name: firstTable, + isView: false, + source: "local", + }); } - }, [files, selectedDataSource]); + }, [registeredTables, selectedTable]); - // Process data when source/columns change + // Update chart data when query result changes useEffect(() => { - if (selectedDataSource?.data && selectedColumns.length > 0) { - const headers = selectedDataSource.data[0]; - const rows = selectedDataSource.data.slice(1); - - const columnIndices = selectedColumns - .map((col) => headers.indexOf(col)) - .filter((idx) => idx !== -1); - - const formattedData = rows.map((row) => { - const obj: Record = {}; - selectedColumns.forEach((column, index) => { - const colIndex = columnIndices[index]; - if (colIndex !== -1) { - const value = row[colIndex]; - obj[column] = isNaN(Number(value)) ? value : Number(value); - } + if (lastResult && lastResult.data.length > 0) { + if (!currentChart) { + createNewChart("bar", lastResult.data, lastResult.query); + } else { + updateCurrentChart({ + data: lastResult.data, + originalData: [...lastResult.data], }); - return obj; - }); - - setFilteredData(formattedData); - - if (formattedData.length > 0 && !currentChart) { - createNewChart("bar", formattedData); } - } else { - setFilteredData(null); + setQueryResult(lastResult); } - }, [selectedDataSource, selectedColumns, createNewChart, currentChart]); + }, [lastResult, currentChart, createNewChart, updateCurrentChart]); + + const hasVisualizationData = currentChart?.data && currentChart.data.length > 0; - const hasVisualizationData = filteredData && filteredData.length > 0; + // Show progress bar for long queries + const showProgress = isExecuting && progress > 0; - // No files state - if (!hasFiles) { + // No tables state + if (registeredTables.size === 0) { return (
@@ -242,37 +190,15 @@ const VisualizationTab: React.FC = () => { return (
- {/* CSS for chart styling */} -