diff --git a/application_events.json b/application_events.json index 8f1cf8f..732ed62 100644 --- a/application_events.json +++ b/application_events.json @@ -7561,8 +7561,49 @@ "activated_datasets": 2, "total_datasets": 3 } + }, + { + "timestamp": "2025-08-15T20:06:52.191312", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-15T20:06:52.271192", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 2, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-15T20:06:52.281191", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 1, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-15T20:06:52.289232", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } } ], - "last_updated": "2025-08-15T19:53:52.599789", - "total_entries": 616 + "last_updated": "2025-08-15T20:06:52.289232", + "total_entries": 620 } \ No newline at end of file diff --git a/frontend/src/components/CsvFileBrowser.jsx b/frontend/src/components/CsvFileBrowser.jsx new file mode 100644 index 0000000..9ad74f2 --- /dev/null +++ b/frontend/src/components/CsvFileBrowser.jsx @@ -0,0 +1,737 @@ +import React, { useState, useEffect } from 'react' +import { + Box, + VStack, + HStack, + Heading, + Text, + Input, + Button, + Card, + CardBody, + CardHeader, + Flex, + Spacer, + Badge, + Spinner, + useToast, + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon, + Checkbox, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + IconButton, + Tooltip, + Alert, + AlertIcon, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + ModalCloseButton, + useDisclosure, + FormControl, + FormLabel, + Collapse, + Tag, + TagLabel, + Wrap, + WrapItem, + useColorModeValue +} from '@chakra-ui/react' +import { + FaPlay, + FaFileExcel, + FaFolder, + FaFile, + FaSearch, + FaFilter, + FaCalendar, + FaDatabase, + FaSync, + FaCog +} from 'react-icons/fa' +import * as api from '../services/api' + +// Filter functions +const filterFiles = (files, query, selectedDatasets, selectedDates) => { + if (!files) return [] + + return files.filter(file => { + // Text search in filename + const matchesQuery = !query || + file.name.toLowerCase().includes(query.toLowerCase()) || + file.dataset.toLowerCase().includes(query.toLowerCase()) + + // Dataset filter + const matchesDataset = selectedDatasets.length === 0 || + selectedDatasets.includes(file.dataset) + + // Date filter + const matchesDate = selectedDates.length === 0 || + selectedDates.includes(file.date) + + return matchesQuery && matchesDataset && matchesDate + }) +} + +// Rebuild tree structure from filtered files +const rebuildTreeFromFilteredFiles = (filteredFiles) => { + if (!filteredFiles || filteredFiles.length === 0) return [] + + const treeMap = new Map() + + filteredFiles.forEach(file => { + const dateKey = file.date + const datasetKey = `${file.date}_${file.dataset}` + + // Create date node if it doesn't exist + if (!treeMap.has(dateKey)) { + treeMap.set(dateKey, { + id: `date_${dateKey}`, + name: `๐Ÿ“… ${dateKey}`, + value: dateKey, + type: "date", + children: new Map() + }) + } + + const dateNode = treeMap.get(dateKey) + + // Create dataset node if it doesn't exist under this date + if (!dateNode.children.has(datasetKey)) { + dateNode.children.set(datasetKey, { + id: `dataset_${dateKey}_${file.dataset}`, + name: `๐Ÿ“Š ${file.dataset}`, + value: file.dataset, + type: "dataset", + children: [] + }) + } + + const datasetNode = dateNode.children.get(datasetKey) + datasetNode.children.push({ + id: `file_${file.date}_${file.value}`, + name: `๐Ÿ“Š ${file.value}`, + value: file.value, + type: "file", + path: file.path, + date: file.date, + dataset: file.dataset, + size: file.size, + size_human: file.size_human, + modified: file.modified, + modified_human: file.modified_human, + columns: file.columns, + column_count: file.column_count, + row_count: file.row_count, + preview: file.preview + }) + }) + + // Convert Maps to Arrays and sort + const tree = Array.from(treeMap.values()).map(dateNode => ({ + ...dateNode, + children: Array.from(dateNode.children.values()).sort((a, b) => a.value.localeCompare(b.value)) + })).sort((a, b) => b.value.localeCompare(a.value)) // Sort dates in descending order + + return tree +} + +// PlotJuggler Configuration Modal +function PlotJugglerConfigModal({ isOpen, onClose, onSave, currentPath }) { + const [path, setPath] = useState(currentPath || '') + const [testing, setTesting] = useState(false) + const toast = useToast() + + useEffect(() => { + setPath(currentPath || '') + }, [currentPath]) + + const testPath = async () => { + if (!path) { + toast({ + title: 'โŒ No path provided', + description: 'Please enter a path to test', + status: 'error', + duration: 2000 + }) + return + } + + setTesting(true) + try { + // Simple test by trying to set the path + await api.setPlotJugglerPath(path) + toast({ + title: 'โœ… Path is valid', + description: 'PlotJuggler executable found', + status: 'success', + duration: 2000 + }) + } catch (error) { + toast({ + title: 'โŒ Invalid path', + description: error.message, + status: 'error', + duration: 3000 + }) + } finally { + setTesting(false) + } + } + + const handleSave = () => { + onSave(path) + onClose() + } + + return ( + + + + ๐Ÿ”ง Configure PlotJuggler Path + + + + + PlotJuggler Executable Path + setPath(e.target.value)} + placeholder="C:\Program Files\PlotJuggler\plotjuggler.exe" + /> + + + + + + The system will automatically search common locations for PlotJuggler. + You only need to set this if PlotJuggler is installed in a custom location. + + + + + Common locations: +
โ€ข C:\Program Files\PlotJuggler\plotjuggler.exe +
โ€ข C:\Program Files (x86)\PlotJuggler\plotjuggler.exe +
+
+
+ + + + + + + +
+
+ ) +} + +// File Tree Component (Accordion-based since TreeView might not be available) +function FileTree({ tree, selectedFiles, onFileToggle, expandedItems, onToggleExpand }) { + const cardBg = useColorModeValue('white', 'gray.700') + + if (!tree || tree.length === 0) { + return ( + + No CSV files found + + ) + } + + return ( + + {tree.map((dateNode, dateIndex) => ( + + + + + + {dateNode.name} + + {dateNode.children?.reduce((sum, dataset) => sum + (dataset.children?.length || 0), 0)} files + + + + + + + + {dateNode.children?.map((datasetNode, datasetIndex) => ( + + + + + + {datasetNode.name} + + {datasetNode.children?.length || 0} files + + + + + + + + {datasetNode.children?.map((fileNode) => ( + + + + onFileToggle(fileNode)} + /> + + + + {fileNode.value} + + + + {fileNode.size_human} + + + {fileNode.row_count} rows + + + {fileNode.column_count} cols + + + + ๐Ÿ“Š {fileNode.preview} + + + + + } + colorScheme="blue" + variant="outline" + onClick={() => onFileToggle(fileNode, 'plotjuggler')} + /> + + + } + colorScheme="green" + variant="outline" + onClick={() => onFileToggle(fileNode, 'excel')} + /> + + + + + + ))} + + + + ))} + + + + ))} + + ) +} + +// Main CSV File Browser Component +export default function CsvFileBrowser() { + const [loading, setLoading] = useState(true) + const [files, setFiles] = useState([]) + const [originalTree, setOriginalTree] = useState([]) + const [filteredTree, setFilteredTree] = useState([]) + const [filteredFiles, setFilteredFiles] = useState([]) + const [selectedFiles, setSelectedFiles] = useState(new Set()) + const [searchQuery, setSearchQuery] = useState('') + const [selectedDatasets, setSelectedDatasets] = useState([]) + const [selectedDates, setSelectedDates] = useState([]) + const [expandedItems, setExpandedItems] = useState([]) + const [plotjugglerPath, setPlotjugglerPath] = useState(null) + + const toast = useToast() + const { isOpen: isConfigOpen, onOpen: onConfigOpen, onClose: onConfigClose } = useDisclosure() + + // Load CSV files + const loadFiles = async () => { + try { + setLoading(true) + const response = await api.getCsvFiles() + setFiles(response.files || []) + setOriginalTree(response.tree || []) + setFilteredTree(response.tree || []) + + toast({ + title: 'โœ… Files loaded', + description: `Found ${response.total_files || 0} CSV files`, + status: 'success', + duration: 2000 + }) + } catch (error) { + toast({ + title: 'โŒ Failed to load files', + description: error.message, + status: 'error', + duration: 3000 + }) + } finally { + setLoading(false) + } + } + + // Load PlotJuggler path + const loadPlotJugglerPath = async () => { + try { + const response = await api.getPlotJugglerPath() + setPlotjugglerPath(response.path) + } catch (error) { + console.warn('PlotJuggler path not configured') + } + } + + // Handle file selection + const handleFileToggle = async (fileNode, action = 'select') => { + if (action === 'plotjuggler') { + // Open single file in PlotJuggler + try { + await api.launchPlotJuggler([fileNode.path]) + toast({ + title: '๐Ÿš€ PlotJuggler launched', + description: `Opened ${fileNode.value}`, + status: 'success', + duration: 2000 + }) + } catch (error) { + toast({ + title: 'โŒ Failed to launch PlotJuggler', + description: error.message, + status: 'error', + duration: 3000 + }) + } + } else if (action === 'excel') { + // Open in Excel + try { + await api.openCsvInExcel(fileNode.path) + toast({ + title: '๐Ÿ“Š File opened', + description: `Opened ${fileNode.value} in Excel`, + status: 'success', + duration: 2000 + }) + } catch (error) { + toast({ + title: 'โŒ Failed to open file', + description: error.message, + status: 'error', + duration: 3000 + }) + } + } else { + // Toggle selection + const newSelected = new Set(selectedFiles) + if (newSelected.has(fileNode.path)) { + newSelected.delete(fileNode.path) + } else { + newSelected.add(fileNode.path) + } + setSelectedFiles(newSelected) + } + } + + // Launch PlotJuggler with selected files + const launchSelectedFiles = async () => { + if (selectedFiles.size === 0) { + toast({ + title: 'โš ๏ธ No files selected', + description: 'Please select files to open in PlotJuggler', + status: 'warning', + duration: 2000 + }) + return + } + + try { + const filePaths = Array.from(selectedFiles) + await api.launchPlotJuggler(filePaths) + toast({ + title: '๐Ÿš€ PlotJuggler launched', + description: `Opened ${filePaths.length} file(s)`, + status: 'success', + duration: 2000 + }) + } catch (error) { + toast({ + title: 'โŒ Failed to launch PlotJuggler', + description: error.message, + status: 'error', + duration: 3000 + }) + } + } + + // Save PlotJuggler path + const savePlotJugglerPath = async (path) => { + try { + await api.setPlotJugglerPath(path) + setPlotjugglerPath(path) + toast({ + title: 'โœ… PlotJuggler path saved', + status: 'success', + duration: 2000 + }) + } catch (error) { + toast({ + title: 'โŒ Failed to save path', + description: error.message, + status: 'error', + duration: 3000 + }) + } + } + + // Get unique datasets and dates for filtering + const availableDatasets = [...new Set(files.map(f => f.dataset))].sort() + const availableDates = [...new Set(files.map(f => f.date))].sort().reverse() + + // Filter files and rebuild tree based on search and filters + useEffect(() => { + const filtered = filterFiles(files, searchQuery, selectedDatasets, selectedDates) + setFilteredFiles(filtered) + + // Rebuild tree with filtered files + const newFilteredTree = rebuildTreeFromFilteredFiles(filtered) + setFilteredTree(newFilteredTree) + }, [files, searchQuery, selectedDatasets, selectedDates]) + + // Load data on mount + useEffect(() => { + loadFiles() + loadPlotJugglerPath() + }, []) + + if (loading) { + return ( + + + + + Loading CSV files... + + + + ) + } + + return ( + + {/* Header */} + + ๐Ÿ“ CSV File Browser + + + + } + size="sm" + variant="outline" + onClick={onConfigOpen} + /> + + + + + + {/* PlotJuggler Status */} + {plotjugglerPath && ( + + + + PlotJuggler found: {plotjugglerPath} + + + )} + + {/* Search and Filters */} + + + ๐Ÿ” Search & Filter + + + + {/* Search */} + + + setSearchQuery(e.target.value)} + /> + + + {/* Filters */} + + + ๐Ÿ“Š Datasets + + {availableDatasets.map(dataset => ( + + { + if (selectedDatasets.includes(dataset)) { + setSelectedDatasets(selectedDatasets.filter(d => d !== dataset)) + } else { + setSelectedDatasets([...selectedDatasets, dataset]) + } + }} + > + {dataset} + + + ))} + + + + + ๐Ÿ“… Dates + + {availableDates.map(date => ( + + { + if (selectedDates.includes(date)) { + setSelectedDates(selectedDates.filter(d => d !== date)) + } else { + setSelectedDates([...selectedDates, date]) + } + }} + > + {date} + + + ))} + + + + + {/* Clear filters */} + {(searchQuery || selectedDatasets.length > 0 || selectedDates.length > 0) && ( + + )} + + + + + {/* Selected Files Actions */} + {selectedFiles.size > 0 && ( + + + + + {selectedFiles.size} file(s) selected + + + + + + + )} + + {/* File Tree */} + + + + ๐Ÿ“‚ File Tree + + + {filteredFiles.length} of {files.length} files + + + + + + + + + {/* PlotJuggler Config Modal */} + + + ) +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index bffc438..0daa50a 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -51,6 +51,7 @@ import validator from '@rjsf/validator-ajv8' import PlotManager from '../components/PlotManagerSimple' import allWidgets from '../components/widgets/AllWidgets' import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate' +import CsvFileBrowser from '../components/CsvFileBrowser' import { VariableProvider, useVariableContext } from '../contexts/VariableContext' import * as api from '../services/api' import { useCoordinatedPolling } from '../hooks/useCoordinatedConnection' @@ -1137,7 +1138,8 @@ function DashboardContent() { ๐Ÿ”ง Configuration ๐Ÿ“Š Datasets ๐Ÿ“ˆ Plotting - ๐Ÿ“‹ Events + ๏ฟฝ CSV Files + ๏ฟฝ๐Ÿ“‹ Events @@ -1163,6 +1165,11 @@ function DashboardContent() { + + {/* CSV File Browser Section */} + + + {/* Events Section */} 1: + # If there's a second part, it might be part of dataset name + parts = filename.split("_") + if not parts[-1].replace(".csv", "").isdigit(): + dataset_name = "_".join(parts[:-1]) + + # Get file info + try: + stat = os.stat(file_path) + file_size = stat.st_size + file_mtime = datetime.fromtimestamp(stat.st_mtime) + + # Try to get CSV info (first 3 columns and row count) + csv_info = get_csv_file_info(file_path) + + file_info = { + "id": f"file_{date_dir}_{filename}", + "name": f"๐Ÿ“Š {filename}", + "value": filename, + "type": "file", + "path": file_path, + "date": date_dir, + "dataset": dataset_name, + "size": file_size, + "size_human": format_file_size(file_size), + "modified": file_mtime.isoformat(), + "modified_human": file_mtime.strftime("%H:%M:%S"), + **csv_info, + } + + files_flat.append(file_info) + + if dataset_name not in datasets: + datasets[dataset_name] = { + "id": f"dataset_{date_dir}_{dataset_name}", + "name": f"๐Ÿ“Š {dataset_name}", + "value": dataset_name, + "type": "dataset", + "children": [], + } + + datasets[dataset_name]["children"].append(file_info) + + except Exception as e: + print(f"Error processing file {filename}: {e}") + continue + + # Add datasets to date node + date_node["children"] = list(datasets.values()) + if date_node["children"]: # Only add dates that have files + file_tree.append(date_node) + + # Sort by date (newest first) + file_tree.sort(key=lambda x: x["value"], reverse=True) + + return jsonify( + {"files": files_flat, "tree": file_tree, "total_files": len(files_flat)} + ) + + except Exception as e: + return jsonify({"error": str(e), "files": [], "tree": []}), 500 + + +def get_csv_file_info(file_path): + """Get CSV file information: first 3 columns and row count""" + try: + import csv + + with open(file_path, "r", encoding="utf-8") as f: + reader = csv.reader(f) + header = next(reader, []) + + # Count rows (approximately, for performance) + row_count = sum(1 for _ in reader) + + # Get first 3 column names + first_columns = header[:3] if header else [] + + return { + "columns": first_columns, + "column_count": len(header), + "row_count": row_count, + "preview": ", ".join(first_columns[:3]), + } + except Exception: + return {"columns": [], "column_count": 0, "row_count": 0, "preview": "Unknown"} + + +def format_file_size(size_bytes): + """Format file size in human readable format""" + if size_bytes == 0: + return "0 B" + size_names = ["B", "KB", "MB", "GB"] + import math + + i = int(math.floor(math.log(size_bytes, 1024))) + p = math.pow(1024, i) + s = round(size_bytes / p, 2) + return f"{s} {size_names[i]}" + + +@app.route("/api/plotjuggler/launch", methods=["POST"]) +def launch_plotjuggler(): + """Launch PlotJuggler with selected CSV files""" + try: + data = request.get_json() + file_paths = data.get("files", []) + + if not file_paths: + return jsonify({"error": "No files provided"}), 400 + + # Get PlotJuggler path from system state + plotjuggler_path = get_plotjuggler_path() + if not plotjuggler_path: + return jsonify({"error": "PlotJuggler not found"}), 404 + + # Launch PlotJuggler with files + import subprocess + + if len(file_paths) == 1: + # Single file + cmd = [plotjuggler_path, "--nosplash", "--datafile", file_paths[0]] + else: + # Multiple files - launch separate instances + commands = [] + for file_path in file_paths: + commands.append( + [plotjuggler_path, "--nosplash", "--datafile", file_path] + ) + + # Execute all commands + for cmd in commands: + subprocess.Popen(cmd, shell=True) + + return jsonify( + { + "success": True, + "message": f"Launched {len(file_paths)} PlotJuggler instances", + "commands": [" ".join(cmd) for cmd in commands], + } + ) + + # Execute single command + subprocess.Popen(cmd, shell=True) + + return jsonify( + { + "success": True, + "message": "PlotJuggler launched successfully", + "command": " ".join(cmd), + } + ) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/plotjuggler/path", methods=["GET"]) +def get_plotjuggler_path_endpoint(): + """Get PlotJuggler executable path""" + try: + path = get_plotjuggler_path() + return jsonify({"path": path, "found": path is not None}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/plotjuggler/path", methods=["POST"]) +def set_plotjuggler_path(): + """Set PlotJuggler executable path""" + try: + data = request.get_json() + path = data.get("path", "") + + if not path or not os.path.exists(path): + return jsonify({"error": "Invalid path provided"}), 400 + + # Save to system state + save_plotjuggler_path(path) + + return jsonify( + {"success": True, "message": "PlotJuggler path saved", "path": path} + ) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +def get_plotjuggler_path(): + """Get PlotJuggler executable path, search if not found""" + try: + # Load from system state + state_file = resource_path("system_state.json") + if os.path.exists(state_file): + with open(state_file, "r") as f: + state = json.load(f) + saved_path = state.get("plotjuggler_path") + if saved_path and os.path.exists(saved_path): + return saved_path + + # Search for PlotJuggler + search_paths = [ + r"C:\Program Files\PlotJuggler\plotjuggler.exe", + r"C:\Program Files (x86)\PlotJuggler\plotjuggler.exe", + r"C:\PlotJuggler\plotjuggler.exe", + ] + + for path in search_paths: + if os.path.exists(path): + save_plotjuggler_path(path) + return path + + return None + + except Exception as e: + print(f"Error getting PlotJuggler path: {e}") + return None + + +def save_plotjuggler_path(path): + """Save PlotJuggler path to system state""" + try: + state_file = resource_path("system_state.json") + state = {} + + if os.path.exists(state_file): + with open(state_file, "r") as f: + state = json.load(f) + + state["plotjuggler_path"] = path + state["last_update"] = datetime.now().isoformat() + + with open(state_file, "w") as f: + json.dump(state, f, indent=4) + + except Exception as e: + print(f"Error saving PlotJuggler path: {e}") + + +@app.route("/api/csv/open-excel", methods=["POST"]) +def open_csv_in_excel(): + """Open CSV file in Excel (or default spreadsheet app)""" + try: + data = request.get_json() + file_path = data.get("file_path", "") + + if not file_path or not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + import subprocess + import platform + + system = platform.system() + if system == "Windows": + # Use os.startfile on Windows + os.startfile(file_path) + elif system == "Darwin": # macOS + subprocess.Popen(["open", file_path]) + else: # Linux + subprocess.Popen(["xdg-open", file_path]) + + return jsonify( + { + "success": True, + "message": f"Opened {os.path.basename(file_path)} in default application", + } + ) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + if __name__ == "__main__": try: # Initialize streamer instance diff --git a/system_state.json b/system_state.json index 1caccae..e134174 100644 --- a/system_state.json +++ b/system_state.json @@ -3,11 +3,12 @@ "should_connect": true, "should_stream": false, "active_datasets": [ - "DAR", + "Fast", "Test", - "Fast" + "DAR" ] }, "auto_recovery_enabled": true, - "last_update": "2025-08-15T19:53:52.606916" + "last_update": "2025-08-15T20:15:35.459362", + "plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe" } \ No newline at end of file