From b10fbc6b06e4241728a682623685c5f7e00803aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:32:55 +0000 Subject: [PATCH 1/4] Initial plan From e87e5ac44d2a4658ea803f64b1c8450561299a17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:45:56 +0000 Subject: [PATCH 2/4] Add chart visualization dependencies and core components Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- package.json | 3 + .../ChartVisualization.react.js | 651 ++++++++++++++++++ .../ChartVisualization.scss | 209 ++++++ .../DraggableResizablePanel.react.js | 147 ++++ .../DraggableResizablePanel.scss | 188 +++++ src/components/Toolbar/Toolbar.react.js | 26 +- src/components/Toolbar/Toolbar.scss | 103 +++ .../Data/Browser/BrowserToolbar.react.js | 4 + .../Data/Browser/DataBrowser.react.js | 87 ++- 9 files changed, 1407 insertions(+), 11 deletions(-) create mode 100644 src/components/ChartVisualization/ChartVisualization.react.js create mode 100644 src/components/ChartVisualization/ChartVisualization.scss create mode 100644 src/components/DraggableResizablePanel/DraggableResizablePanel.react.js create mode 100644 src/components/DraggableResizablePanel/DraggableResizablePanel.scss diff --git a/package.json b/package.json index d830c0b53d..bb084791fe 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "@babel/runtime-corejs3": "7.28.3", "bcryptjs": "3.0.3", "body-parser": "2.2.1", + "chart.js": "^4.5.0", + "chartjs-adapter-date-fns": "^3.0.0", "commander": "13.1.0", "connect-flash": "0.1.1", "copy-to-clipboard": "3.3.3", @@ -65,6 +67,7 @@ "qrcode": "1.5.4", "react": "16.14.0", "react-ace": "14.0.1", + "react-chartjs-2": "^5.3.0", "react-dnd": "10.0.2", "react-dnd-html5-backend": "16.0.1", "react-dom": "16.14.0", diff --git a/src/components/ChartVisualization/ChartVisualization.react.js b/src/components/ChartVisualization/ChartVisualization.react.js new file mode 100644 index 0000000000..acdfda2f98 --- /dev/null +++ b/src/components/ChartVisualization/ChartVisualization.react.js @@ -0,0 +1,651 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +import PropTypes from 'lib/PropTypes'; +import React, { useMemo, useState } from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + LineElement, + PointElement, + ArcElement, + Title, + Tooltip, + Legend, + TimeScale, +} from 'chart.js'; +import { Bar, Line, Pie } from 'react-chartjs-2'; +import 'chartjs-adapter-date-fns'; +import styles from './ChartVisualization.scss'; + +// Register necessary Chart.js components +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + LineElement, + PointElement, + ArcElement, + Title, + Tooltip, + Legend, + TimeScale +); + +// Utility functions for chart data processing +const validateInputData = (selectedData, selectedCells, data) => { + if (!selectedData || selectedData.length === 0 || !selectedCells || !data || !Array.isArray(data)) { + return false; + } + + const { rowStart, rowEnd, colStart } = selectedCells; + + // Check if we have valid data and if indices are valid + if (rowStart === -1 || colStart === -1 || rowEnd >= data.length || rowStart < 0) { + return false; + } + + // Check if all row indices are valid + for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) { + if (!data[rowIndex] || !data[rowIndex].attributes) { + return false; // Inconsistent data, abort + } + } + + return true; +}; + +const detectTimeSeriesData = (selectedCells, data, order, columns) => { + const { rowStart, rowEnd, colStart, colEnd } = selectedCells; + let isTimeSeries = false; + let dateColumnName = null; + let dateColumnIndex = -1; + + // Look for any date column in the selection (not just the first) + if (colEnd > colStart && columns) { + for (let colIndex = colStart; colIndex <= colEnd; colIndex++) { + const columnName = order[colIndex]?.name; + if (!columnName) { + continue; + } + + // Check the column type in the schema + const columnType = columns[columnName]?.type; + const isDateColumn = columnType === 'Date' || + /^(date|time|created|updated|when|at)$/i.test(columnName) || + columnName.toLowerCase().includes('date') || + columnName.toLowerCase().includes('time'); + + if (isDateColumn) { + // Check if the column actually contains valid dates + let dateCount = 0; + const totalRows = Math.min(3, rowEnd - rowStart + 1); // Check up to 3 rows + + for (let rowIndex = rowStart; rowIndex < rowStart + totalRows; rowIndex++) { + // Check if the index is valid before accessing + if (rowIndex >= data.length || !data[rowIndex] || !data[rowIndex].attributes) { + continue; + } + const value = data[rowIndex].attributes[columnName]; + if (value instanceof Date || + (typeof value === 'string' && !isNaN(Date.parse(value)) && new Date(value).getFullYear() > 1900)) { + dateCount++; + } + } + + if (dateCount >= totalRows * 0.6) { // 60% must be valid dates + isTimeSeries = true; + dateColumnName = columnName; + dateColumnIndex = colIndex; + break; // Found a valid date column + } + } + } + } + + return { isTimeSeries, dateColumnName, dateColumnIndex }; +}; + +const processTimeSeriesData = (selectedCells, data, order, dateColumnName, dateColumnIndex) => { + const { rowStart, rowEnd, colStart, colEnd } = selectedCells; + const datasets = []; + let datasetIndex = 0; + + // Create a dataset for each numeric column (except the date column) + for (let colIndex = colStart; colIndex <= colEnd; colIndex++) { + // Skip the date column + if (colIndex === dateColumnIndex) { + continue; + } + + const columnName = order[colIndex]?.name; + if (!columnName) { + continue; + } + + const dataPoints = []; + + for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) { + // Check if the index is valid + if (rowIndex >= data.length || !data[rowIndex] || !data[rowIndex].attributes) { + continue; + } + const timeValue = data[rowIndex].attributes[dateColumnName]; + const numericValue = data[rowIndex].attributes[columnName]; + + if (timeValue && typeof numericValue === 'number' && !isNaN(numericValue)) { + dataPoints.push({ + x: new Date(timeValue), + y: numericValue + }); + } + } + + if (dataPoints.length > 0) { + datasets.push({ + label: columnName, + data: dataPoints, + borderColor: `hsl(${datasetIndex * 60}, 70%, 50%)`, + backgroundColor: `hsla(${datasetIndex * 60}, 70%, 50%, 0.1)`, + tension: 0.1 + }); + datasetIndex++; + } + } + + return { + type: 'timeSeries', + datasets, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + type: 'time', + time: { + displayFormats: { + day: 'MMM dd', + hour: 'HH:mm' + } + }, + title: { + display: true, + text: dateColumnName + } + }, + y: { + title: { + display: true, + text: 'Value' + } + } + }, + plugins: { + title: { + display: true, + text: 'Time Series Visualization' + }, + legend: { + display: datasets.length > 1 + } + } + } + }; +}; + +const processNumericSeriesData = (selectedCells, data, order, columns, chartType) => { + const { rowStart, rowEnd, colStart, colEnd } = selectedCells; + const labels = []; + const dataPoints = []; + + // If multiple columns, create separate datasets for each column + if (colEnd > colStart) { + const datasets = []; + + for (let colIndex = colStart; colIndex <= colEnd; colIndex++) { + const columnName = order[colIndex]?.name; + if (!columnName) { + continue; + } + + // Collect all values from this column + const columnValues = []; + + for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) { + // Check if the index is valid + if (rowIndex >= data.length || !data[rowIndex] || !data[rowIndex].attributes) { + continue; + } + const value = data[rowIndex].attributes[columnName]; + if (typeof value === 'number' && !isNaN(value)) { + columnValues.push(value); + } + } + + if (columnValues.length > 0) { + datasets.push({ + label: columnName, + data: columnValues, + backgroundColor: `hsla(${(colIndex - colStart) * 60}, 70%, 60%, 0.8)`, + borderColor: `hsl(${(colIndex - colStart) * 60}, 70%, 50%)`, + borderWidth: 2, + borderRadius: chartType === 'bar' ? 4 : 0, + tension: chartType === 'line' ? 0.4 : 0 + }); + } + } + + // Use labels from the first column (all should have the same number of rows) + for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) { + labels.push(`Row ${rowIndex + 1}`); + } + + return { + type: 'numberSeries', + data: { + labels, + datasets + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + }, + plugins: { + title: { + display: true, + text: 'Selected Data Visualization', + font: { size: 16, weight: 'bold' }, + color: '#333' + }, + legend: { + display: datasets.length > 1 // Show legend if multiple columns + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + titleColor: '#fff', + bodyColor: '#fff', + borderColor: '#169cee', + borderWidth: 1 + } + }, + scales: { + y: { + beginAtZero: true, + title: { display: true, text: 'Value', font: { size: 14, weight: 'bold' }, color: '#555' }, + grid: { color: 'rgba(0, 0, 0, 0.1)' }, + ticks: { color: '#666' } + }, + x: { + title: { display: true, text: 'Categories', font: { size: 14, weight: 'bold' }, color: '#555' }, + grid: { color: 'rgba(0, 0, 0, 0.1)' }, + ticks: { color: '#666' } + } + } + } + }; + } else { + // Single column: use row indices as labels + const columnName = order[colStart]?.name; + if (columnName) { + for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) { + // Check if the index is valid + if (rowIndex >= data.length || !data[rowIndex] || !data[rowIndex].attributes) { + continue; + } + labels.push(`Row ${rowIndex + 1}`); + const value = data[rowIndex].attributes[columnName]; + dataPoints.push(typeof value === 'number' && !isNaN(value) ? value : 0); + } + } + + if (labels.length === 0 || dataPoints.length === 0) { + return null; + } + + return { + type: 'numberSeries', + data: { + labels, + datasets: [{ + label: 'Selected Values', + data: dataPoints, + backgroundColor: chartType === 'bar' + ? dataPoints.map((_, index) => `hsla(${index * 360 / dataPoints.length}, 70%, 60%, 0.8)`) + : 'rgba(22, 156, 238, 0.7)', + borderColor: chartType === 'bar' + ? dataPoints.map((_, index) => `hsl(${index * 360 / dataPoints.length}, 70%, 50%)`) + : 'rgba(22, 156, 238, 1)', + borderWidth: 2, + borderRadius: chartType === 'bar' ? 4 : 0, + tension: chartType === 'line' ? 0.4 : 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + }, + plugins: { + title: { + display: true, + text: 'Selected Data Visualization', + font: { size: 16, weight: 'bold' }, + color: '#333' + }, + legend: { + display: false // Single column doesn't need legend + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + titleColor: '#fff', + bodyColor: '#fff', + borderColor: '#169cee', + borderWidth: 1 + } + }, + scales: { + y: { + beginAtZero: true, + title: { display: true, text: 'Value', font: { size: 14, weight: 'bold' }, color: '#555' }, + grid: { color: 'rgba(0, 0, 0, 0.1)' }, + ticks: { color: '#666' } + }, + x: { + title: { display: true, text: 'Categories', font: { size: 14, weight: 'bold' }, color: '#555' }, + grid: { color: 'rgba(0, 0, 0, 0.1)' }, + ticks: { color: '#666' } + } + } + } + }; + } +}; + +const ChartVisualization = ({ + selectedData, + selectedCells, + data, + order, + columns +}) => { + const [chartType, setChartType] = useState('bar'); + + // Process selected data to determine the type of visualization + const chartData = useMemo(() => { + if (!validateInputData(selectedData, selectedCells, data)) { + return null; + } + + const timeSeriesInfo = detectTimeSeriesData(selectedCells, data, order, columns); + + if (timeSeriesInfo.isTimeSeries) { + return processTimeSeriesData(selectedCells, data, order, timeSeriesInfo.dateColumnName, timeSeriesInfo.dateColumnIndex); + } else { + return processNumericSeriesData(selectedCells, data, order, columns, chartType); + } + }, [selectedData, selectedCells, data, order, columns, chartType]); + + const renderChart = () => { + // Safety check to prevent crashes + if (!chartData) { + return null; + } + + if (chartData.type === 'timeSeries') { + return ( + + ); + } else { + // For number series, support bar, line and pie charts + if (chartType === 'pie') { + // For pie chart, verify if we have valid data + const values = chartData.data.datasets[0].data; + const labels = chartData.data.labels; + + // Filter valid values (> 0) for pie chart + const validData = []; + const validLabels = []; + const validColors = []; + + values.forEach((value, index) => { + if (value && value > 0) { + validData.push(value); + validLabels.push(labels[index]); + validColors.push(`hsl(${index * 360 / values.length}, 75%, 65%)`); + } + }); + + if (validData.length === 0) { + return

No positive values for pie chart

; + } + + const pieData = { + labels: validLabels, + datasets: [{ + label: 'Values', + data: validData, + backgroundColor: validColors, + borderColor: validColors.map(color => color.replace('60%', '40%')), + borderWidth: 1 + }] + }; + + const pieOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: 'Data Distribution', + font: { + size: 16, + weight: 'bold' + }, + color: '#333' + }, + legend: { + display: true, + position: 'right', + labels: { + padding: 20, + usePointStyle: true, + font: { + size: 12 + } + } + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + titleColor: '#fff', + bodyColor: '#fff', + borderColor: '#169cee', + borderWidth: 1, + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.parsed; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = Math.round((value / total) * 100); + return `${label}: ${value} (${percentage}%)`; + } + } + } + } + }; + + return ( + + ); + } else { + // Bar ou Line Chart + const ChartComponent = chartType === 'bar' ? Bar : Line; + + // Improve options for correct sizing + const enhancedOptions = { + ...chartData.options, + responsive: true, + maintainAspectRatio: false, + aspectRatio: 1.6, + layout: { + padding: { + top: 20, + right: 20, + bottom: 20, + left: 20 + } + }, + elements: { + bar: { + borderRadius: 4, + borderWidth: 0 + }, + line: { + borderWidth: 3, + tension: 0.4 + }, + point: { + radius: 5, + borderWidth: 2, + hoverRadius: 7 + } + }, + plugins: { + ...chartData.options.plugins, + title: { + display: true, + text: 'Selected Data Visualization', + position: 'top', + align: 'center', + font: { + size: 16, + weight: 'bold' + }, + color: '#333', + padding: { + top: 10, + bottom: 20 + } + }, + tooltip: { + enabled: true, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + titleColor: '#fff', + bodyColor: '#fff', + borderColor: '#169cee', + borderWidth: 1, + cornerRadius: 6, + displayColors: true + } + }, + scales: { + ...chartData.options.scales, + x: { + ...chartData.options.scales.x, + grid: { + display: true, + color: 'rgba(0, 0, 0, 0.1)' + }, + ticks: { + maxRotation: 45, + minRotation: 0, + font: { + size: 12 + } + } + }, + y: { + ...chartData.options.scales.y, + grid: { + display: true, + color: 'rgba(0, 0, 0, 0.1)' + }, + ticks: { + font: { + size: 12 + } + } + } + } + }; + + return ( + + ); + } + } + }; + + // Add null check to prevent runtime errors + if (!chartData) { + return ( +
+
+

No valid data selected for charting.

+

Please select numeric or date columns to visualize.

+
+
+ ); + } + + return ( +
+
+ {chartData.type === 'numberSeries' && ( +
+ + +
+ )} +
+ {chartData.type === 'timeSeries' ? 'Time Series' : 'Number Series'} | + {selectedData.length} values selected +
+
+
+ {renderChart()} +
+
+ ); +}; + +ChartVisualization.propTypes = { + selectedData: PropTypes.array.isRequired, + selectedCells: PropTypes.shape({ + list: PropTypes.instanceOf(Set), + rowStart: PropTypes.number.isRequired, + rowEnd: PropTypes.number.isRequired, + colStart: PropTypes.number.isRequired, + colEnd: PropTypes.number.isRequired, + }).isRequired, + data: PropTypes.array.isRequired, + order: PropTypes.array.isRequired, + columns: PropTypes.object.isRequired +}; + +export default ChartVisualization; diff --git a/src/components/ChartVisualization/ChartVisualization.scss b/src/components/ChartVisualization/ChartVisualization.scss new file mode 100644 index 0000000000..32b35f64b4 --- /dev/null +++ b/src/components/ChartVisualization/ChartVisualization.scss @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +@import 'stylesheets/globals.scss'; + +.chartVisualization { + display: flex; + flex-direction: column; + height: 100%; + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border-radius: 0px 0px 8px 8px; + overflow: hidden; + min-width: 600px; + min-height: 500px; + position: relative; + z-index: 1000; +} + +.chartHeader { + background: linear-gradient(135deg, #169cee, #1976d2); + color: white; + padding: 20px 24px; + border-bottom: none; + box-shadow: 0 2px 8px rgba(22, 156, 238, 0.2); + + .chartTitle { + margin: 0; + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + + svg { + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)); + } + } +} + +.chartControls { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background: #f8f9fa; + border-bottom: 1px solid #e3e3ea; +} + +.chartTypeSelector { + display: flex; + align-items: center; + gap: 12px; + + label { + font-weight: 500; + color: #555; + font-size: 14px; + } +} + +.select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 6px; + background: white; + font-size: 14px; + color: #333; + cursor: pointer; + transition: border-color 0.2s ease; + + &:focus { + outline: none; + border-color: #169cee; + box-shadow: 0 0 0 2px rgba(22, 156, 238, 0.1); + } + + &:hover { + border-color: #169cee; + } +} + +.chartInfo { + color: #666; + font-size: 13px; + font-weight: 500; + background: white; + padding: 6px 12px; + border-radius: 4px; + border: 1px solid #e0e0e0; +} + +.chartContainer { + flex: 1; + position: relative; + min-height: 450px; + height: calc(100vh - 280px); + width: 100%; + padding: 20px; + display: flex; + align-items: center; + justify-content: center; + background: white; + + > div { + width: 100% !important; + height: 100% !important; + margin: 0 0; + display: flex !important; + align-items: center !important; + justify-content: center !important; + position: relative !important; + } + + canvas { + width: 100% !important; + height: 100% !important; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + margin: 0 0; + position: relative !important; + } +} + +// Responsive design +@media (max-width: 1200px) { + .chartContainer { + max-width: 100%; + padding: 20px; + + > div { + max-width: 100% !important; + min-width: 300px !important; + } + } +} + +@media (max-width: 768px) { + .chartContainer { + min-height: 350px; + padding: 15px; + height: calc(100vh - 250px); + + > div { + min-height: 300px !important; + min-width: 280px !important; + } + } + + .chartHeader { + padding: 16px 20px; + + .chartTitle { + font-size: 16px; + gap: 8px; + } + } + + .chartControls { + padding: 12px 16px; + flex-direction: column; + gap: 12px; + align-items: flex-start; + } +} + +@media (max-width: 480px) { + .chartContainer { + padding: 10px; + min-height: 300px; + + > div { + min-height: 250px !important; + min-width: 250px !important; + } + } +} + +.noData { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + min-height: 300px; + padding: 40px 20px; + text-align: center; + color: #666; + background: #f8f9fa; + border-radius: 8px; + margin: 20px; + + p { + margin: 8px 0; + font-size: 16px; + line-height: 1.5; + + &:first-child { + font-weight: 600; + color: #333; + font-size: 18px; + } + } +} diff --git a/src/components/DraggableResizablePanel/DraggableResizablePanel.react.js b/src/components/DraggableResizablePanel/DraggableResizablePanel.react.js new file mode 100644 index 0000000000..d423c26bd5 --- /dev/null +++ b/src/components/DraggableResizablePanel/DraggableResizablePanel.react.js @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +import React, { useState, useRef, useEffect } from 'react'; +import { ResizableBox } from 'react-resizable'; +import Icon from 'components/Icon/Icon.react'; +import styles from './DraggableResizablePanel.scss'; + +const DraggableResizablePanel = ({ + children, + width = 400, + height = 400, + minWidth = 300, + maxWidth = 800, + minHeight = 300, + maxHeight = 600, + title = 'Panel', + onClose, + initialPosition = { x: 100, y: 100 } +}) => { + const [position, setPosition] = useState(initialPosition); + const [size, setSize] = useState({ width, height }); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const panelRef = useRef(null); + const titleBarRef = useRef(null); const handleMouseDown = (e) => { + // Check if the click was on the title bar or its children + if (titleBarRef.current && ( + e.target === titleBarRef.current || + titleBarRef.current.contains(e.target) + )) { + + // Check that the close button was not clicked + if (!e.target.closest('[data-close-button="true"]')) { + setIsDragging(true); + setDragStart({ + x: e.clientX - position.x, + y: e.clientY - position.y + }); + e.preventDefault(); + e.stopPropagation(); + } + } + }; const handleMouseMove = (e) => { + if (isDragging) { + const newX = e.clientX - dragStart.x; + const newY = e.clientY - dragStart.y; + + // Limit position to not go off screen + const maxX = window.innerWidth - size.width; + const maxY = window.innerHeight - size.height; + + const newPosition = { + x: Math.max(0, Math.min(newX, maxX)), + y: Math.max(0, Math.min(newY, maxY)) + }; + + setPosition(newPosition); + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + const handleResize = (event, { size: newSize }) => { + setSize(newSize); + }; + + useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.cursor = 'move'; + document.body.style.userSelect = 'none'; + } else { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + }, [isDragging, dragStart, position, size]); + + return ( +
+ +
+ {/* Title bar */} +
+
+ + {title} +
+
+ +
+
+ + {/* Panel content */} +
+ {children} +
+
+
+
+ ); +}; + +export default DraggableResizablePanel; diff --git a/src/components/DraggableResizablePanel/DraggableResizablePanel.scss b/src/components/DraggableResizablePanel/DraggableResizablePanel.scss new file mode 100644 index 0000000000..b71b5d229e --- /dev/null +++ b/src/components/DraggableResizablePanel/DraggableResizablePanel.scss @@ -0,0 +1,188 @@ +.draggablePanel { + position: fixed; + z-index: 1000; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + background: white; + border: 1px solid #e1e5e9; +} + +.resizableContainer { + border-radius: 8px; + overflow: visible; // Change: allow handles to be visible outside the border + + :global(.react-resizable-handle) { + position: absolute; + z-index: 1001; + opacity: 0; + transition: opacity 0.2s; + } + + // Show handles when hovering over the window + &:hover :global(.react-resizable-handle) { + opacity: 1; + } + + :global(.react-resizable-handle-se) { + bottom: -5px; + right: -5px; + width: 16px; + height: 16px; + cursor: se-resize; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); + //background: #1f8ce6; + //border: 2px solid white; + } + + :global(.react-resizable-handle-e) { + right: -4px; + top: 20%; + bottom: 20%; + width: 8px; + cursor: e-resize; + border-radius: 4px; + //background: #1f8ce6; + //border: 1px solid white; + } + + :global(.react-resizable-handle-s) { + bottom: -4px; + left: 20%; + right: 20%; + height: 8px; + cursor: s-resize; + border-radius: 4px; + //background: #1f8ce6; + //border: 1px solid white; + } + + :global(.react-resizable-handle-w) { + left: -4px; + top: 20%; + bottom: 20%; + width: 8px; + cursor: w-resize; + border-radius: 4px; + //background: #1f8ce6; + //border: 1px solid white; + } + + :global(.react-resizable-handle-n) { + top: -4px; + left: 20%; + right: 20%; + height: 8px; + cursor: n-resize; + border-radius: 4px; + //background: #1f8ce6; + //border: 1px solid white; + } + + :global(.react-resizable-handle-ne) { + top: -5px; + right: -5px; + width: 12px; + height: 12px; + cursor: ne-resize; + border-radius: 50%; + //background: #1f8ce6; + //border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); + } + + :global(.react-resizable-handle-nw) { + top: -5px; + left: -5px; + width: 12px; + height: 12px; + cursor: nw-resize; + border-radius: 50%; + //background: #1f8ce6; + //border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); + } + + :global(.react-resizable-handle-sw) { + bottom: -5px; + left: -5px; + width: 12px; + height: 12px; + cursor: sw-resize; + border-radius: 50%; + //background: #1f8ce6; + //border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); + } +} + +.panelContainer { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + background: white; + border-radius: 8px; +} + +.titleBar { + background: linear-gradient(135deg, #1f8ce6 0%, #1470c7 100%); + color: white; + padding: 8px 12px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + user-select: none; + border-radius: 8px 8px 0 0; + min-height: 32px; + + &:hover { + background: linear-gradient(135deg, #1a7dd6 0%, #1260b7 100%); + } +} + +.title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 14px; + + span { + margin: 0; + } +} + +.controls { + display: flex; + align-items: center; + gap: 4px; +} + +.closeButton { + background: transparent; + border: none; + color: white; + cursor: pointer; + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } + + &:active { + background: rgba(255, 255, 255, 0.3); + } +} + +.content { + flex: 1; + overflow: auto; + border-radius: 0 0 8px 8px; +} diff --git a/src/components/Toolbar/Toolbar.react.js b/src/components/Toolbar/Toolbar.react.js index 17940e7ac7..2ce7b34719 100644 --- a/src/components/Toolbar/Toolbar.react.js +++ b/src/components/Toolbar/Toolbar.react.js @@ -146,13 +146,25 @@ const Toolbar = props => { {props?.selectedData?.length ? ( - +
+ + {props?.selectedData?.length > 1 && ( + + )} +
) : null}
{props.children}
{props.classwiseCloudFunctions && diff --git a/src/components/Toolbar/Toolbar.scss b/src/components/Toolbar/Toolbar.scss index 5453305442..9367de004c 100644 --- a/src/components/Toolbar/Toolbar.scss +++ b/src/components/Toolbar/Toolbar.scss @@ -170,3 +170,106 @@ body:global(.expanded) { } } } + +.dataControls { + position: absolute; + right: 20px; + bottom: 10px; + display: flex; + align-items: center; + gap: 16px; + z-index: 10; +} + +.chartButton { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: linear-gradient(135deg, #169cee, #1976d2); + border: none; + border-radius: 6px; + color: white; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(22, 156, 238, 0.3); + + &:hover { + background: linear-gradient(135deg, #1976d2, #1565c0); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(22, 156, 238, 0.4); + } + + &:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(22, 156, 238, 0.3); + } + + svg { + flex-shrink: 0; + } + + span { + white-space: nowrap; + } +} + +@media (max-width: 1024px) { + .dataControls { + right: 120px; + } +} + +@media (max-width: 768px) { + .dataControls { + right: 15px; + bottom: 10px; + gap: 12px; + } + + .chartButton { + padding: 6px 12px; + font-size: 12px; + gap: 6px; + + svg { + width: 14px; + height: 14px; + } + } + + .stats { + right: 110px; + bottom: 10px; + padding: 6px 10px; + font-size: 13px; + } +} + +@media (max-width: 480px) { + .dataControls { + right: 10px; + bottom: 10px; + flex-direction: row; + gap: 8px; + align-items: center; + } + + .chartButton { + padding: 5px 8px; + font-size: 11px; + + span { + display: none; + } + } + + .stats { + right: 80px; + bottom: 10px; + font-size: 12px; + padding: 5px 8px; + } +} diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js index f57018c7fb..1411e28287 100644 --- a/src/dashboard/Data/Browser/BrowserToolbar.react.js +++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js @@ -80,6 +80,8 @@ const BrowserToolbar = ({ togglePanel, isPanelVisible, + toggleChartPanel, + isChartPanelVisible, addPanel, removePanel, panelCount, @@ -291,6 +293,8 @@ const BrowserToolbar = ({ selectedData={selectedData} togglePanel={togglePanel} isPanelVisible={isPanelVisible} + toggleChartPanel={toggleChartPanel} + isChartPanelVisible={isChartPanelVisible} addPanel={addPanel} removePanel={removePanel} panelCount={panelCount} diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js index 5694d4b8c1..21af7538c4 100644 --- a/src/dashboard/Data/Browser/DataBrowser.react.js +++ b/src/dashboard/Data/Browser/DataBrowser.react.js @@ -22,6 +22,8 @@ import styles from './Databrowser.scss'; import KeyboardShortcutsManager, { matchesShortcut } from 'lib/KeyboardShortcutsPreferences'; import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel'; +import ChartVisualization from '../../../components/ChartVisualization/ChartVisualization.react'; +import DraggableResizablePanel from '../../../components/DraggableResizablePanel/DraggableResizablePanel.react'; const BROWSER_SHOW_ROW_NUMBER = 'browserShowRowNumber'; const AGGREGATION_PANEL_VISIBLE = 'aggregationPanelVisible'; @@ -128,11 +130,15 @@ export default class DataBrowser extends React.Component { selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 }, firstSelectedCell: null, selectedData: [], + numericSelectedData: [], // Numeric data only for Sum operations + hasDateInSelection: false, // Flag to detect if there are dates in selection prevClassName: props.className, panelWidth: parsedPanelWidth, isResizing: false, maxWidth: window.innerWidth - 300, showAggregatedData: true, + isChartPanelVisible: false, + chartPanelWidth: 400, frozenColumnIndex: -1, showRowNumber: storedRowNumber, scrollToTop: storedScrollToTop, @@ -183,6 +189,7 @@ export default class DataBrowser extends React.Component { this.toggleBatchNavigate = this.toggleBatchNavigate.bind(this); this.toggleShowPanelCheckbox = this.toggleShowPanelCheckbox.bind(this); this.handleCellClick = this.handleCellClick.bind(this); + this.toggleChartPanelVisibility = this.toggleChartPanelVisibility.bind(this); this.addPanel = this.addPanel.bind(this); this.removePanel = this.removePanel.bind(this); this.handlePanelScroll = this.handlePanelScroll.bind(this); @@ -229,6 +236,8 @@ export default class DataBrowser extends React.Component { selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 }, firstSelectedCell: null, selectedData: [], + numericSelectedData: [], + hasDateInSelection: false, frozenColumnIndex: -1, prefetchCache: {}, selectionHistory: [], @@ -315,6 +324,17 @@ export default class DataBrowser extends React.Component { }); } + // Close chart panel if data changed (like when sorting the table) + if (prevProps.data !== this.props.data && this.state.isChartPanelVisible) { + this.setState({ + isChartPanelVisible: false, + selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 }, + selectedData: [], + numericSelectedData: [], + hasDateInSelection: false, + }); + } + if ( this.state.current === null && this.state.selectedObjectId !== undefined && @@ -545,6 +565,12 @@ export default class DataBrowser extends React.Component { } } + toggleChartPanelVisibility() { + this.setState(prevState => ({ + isChartPanelVisible: !prevState.isChartPanelVisible + })); + } + getAllClassesSchema(schema) { const allClasses = Object.keys(schema.data.get('classes').toObject()); const schemaSimplifiedData = {}; @@ -1649,7 +1675,9 @@ export default class DataBrowser extends React.Component { let validColumns = true; for (let i = colStart; i <= colEnd; i++) { const name = this.state.order[i].name; - if (this.props.columns[name].type !== 'Number') { + const columnType = this.props.columns[name].type; + // Allow Number, Date, String (which can contain numbers) for visualization + if (columnType !== 'Number' && columnType !== 'Date' && columnType !== 'String') { validColumns = false; break; } @@ -1657,6 +1685,8 @@ export default class DataBrowser extends React.Component { const newSelection = new Set(); const selectedData = []; + let hasDateColumns = false; // Flag to detect if there are date columns + for (let x = rowStart; x <= rowEnd; x++) { let rowData = null; if (validColumns) { @@ -1665,14 +1695,36 @@ export default class DataBrowser extends React.Component { for (let y = colStart; y <= colEnd; y++) { if (rowData) { const value = rowData.attributes[this.state.order[y].name]; - if (typeof value === 'number' && !isNaN(value)) { - selectedData.push(rowData.attributes[this.state.order[y].name]); + const columnType = this.props.columns[this.state.order[y].name].type; + + // Include different data types for visualization + if (columnType === 'Number' && typeof value === 'number' && !isNaN(value)) { + selectedData.push(value); + } else if (columnType === 'Date' && value instanceof Date) { + selectedData.push(value); + hasDateColumns = true; // Mark that there are dates + } else if (columnType === 'Date' && typeof value === 'string' && !isNaN(Date.parse(value))) { + selectedData.push(new Date(value)); + hasDateColumns = true; // Mark that there are dates + } else if (columnType === 'String' && typeof value === 'string') { + // For strings, include only if they can be interpreted as numbers + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + selectedData.push(numValue); + } else { + selectedData.push(value); // Include strings for labels in time series + } } } newSelection.add(`${x}-${y}`); } } + // Create array with only numbers for sum operations (excluding dates) + const numericData = selectedData.filter(value => + typeof value === 'number' && !isNaN(value) + ); + if (newSelection.size > 1) { this.setCurrent(null); this.props.setLoadingInfoPanel(false); @@ -1686,6 +1738,8 @@ export default class DataBrowser extends React.Component { }, selectedObjectId: undefined, selectedData, + numericSelectedData: numericData, // Numeric data only for Sum + hasDateInSelection: hasDateColumns, // Flag to know if there are dates }); } else { this.setCurrent({ row, col }); @@ -1694,6 +1748,8 @@ export default class DataBrowser extends React.Component { this.setState({ selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 }, selectedData: [], + numericSelectedData: [], // Clear numeric data + hasDateInSelection: false, // Clear dates flag current: { row, col }, firstSelectedCell: clickedCellKey, }); @@ -1887,6 +1943,27 @@ export default class DataBrowser extends React.Component { )} + {this.state.isChartPanelVisible && this.state.selectedData.length > 1 && ( + this.setState({ isChartPanelVisible: false })} + initialPosition={{ x: 320, y: 320 }} + > + + + )} Date: Tue, 6 Jan 2026 14:52:25 +0000 Subject: [PATCH 3/4] Fix webpack config to support react-chartjs-2 Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- package-lock.json | 106 +++++++++++++++++++++++++---------------- webpack/base.config.js | 7 +++ 2 files changed, 73 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2b1957427..942ea2d032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@babel/runtime-corejs3": "7.28.3", "bcryptjs": "3.0.3", "body-parser": "2.2.1", + "chart.js": "^4.5.0", + "chartjs-adapter-date-fns": "^3.0.0", "commander": "13.1.0", "connect-flash": "0.1.1", "copy-to-clipboard": "3.3.3", @@ -38,6 +40,7 @@ "qrcode": "1.5.4", "react": "16.14.0", "react-ace": "14.0.1", + "react-chartjs-2": "^5.3.0", "react-dnd": "10.0.2", "react-dnd-html5-backend": "16.0.1", "react-dom": "16.14.0", @@ -187,7 +190,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2171,12 +2173,14 @@ "node_modules/@codemirror/state": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz", - "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==" + "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==", + "peer": true }, "node_modules/@codemirror/view": { "version": "0.20.7", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz", "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==", + "peer": true, "dependencies": { "@codemirror/state": "^0.20.0", "style-mod": "^4.0.0", @@ -2281,7 +2285,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2305,7 +2308,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3938,15 +3940,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@lezer/common": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz", - "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==" + "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==", + "peer": true }, "node_modules/@lezer/highlight": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz", "integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==", + "peer": true, "dependencies": { "@lezer/common": "^0.16.0" } @@ -3955,6 +3965,7 @@ "version": "0.16.3", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz", "integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==", + "peer": true, "dependencies": { "@lezer/common": "^0.16.0" } @@ -4039,7 +4050,6 @@ "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -4696,7 +4706,6 @@ "version": "2.11.5", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -4998,7 +5007,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "dev": true, - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -5652,7 +5660,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", "dev": true, - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -5802,7 +5809,6 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", "integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==", "dev": true, - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^11.0.0", "@semantic-release/error": "^4.0.0", @@ -6840,7 +6846,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -6860,7 +6865,6 @@ "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.24.tgz", "integrity": "sha512-eIpyco99gTH+FTI3J7Oi/OH8MZoFMJuztNRimDOJwH4iGIsKV2qkGnk4M9VzlaVWeEEWLWSQRy0FEA0Kz218cg==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -7725,7 +7729,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7784,7 +7787,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -8802,7 +8804,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -9041,6 +9042,28 @@ "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "license": "MIT" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-adapter-date-fns": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz", + "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=2.8.0", + "date-fns": ">=2.0.0" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -9306,8 +9329,7 @@ "node_modules/codemirror": { "version": "5.65.9", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz", - "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw==", - "peer": true + "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw==" }, "node_modules/codemirror-graphql": { "version": "2.0.0", @@ -9905,6 +9927,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -10301,8 +10334,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff-match-patch": { "version": "1.0.5", @@ -10931,7 +10963,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -13044,7 +13075,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -14404,7 +14434,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.4.tgz", "integrity": "sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "30.0.4", "@jest/types": "30.0.1", @@ -15655,7 +15684,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -16306,7 +16334,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -19302,7 +19329,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -20431,7 +20457,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -21121,7 +21146,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -21159,6 +21183,16 @@ "pure-color": "^1.2.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-clientside-effect": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", @@ -21221,7 +21255,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -21290,8 +21323,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "peer": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-json-view": { "version": "1.21.3", @@ -22121,7 +22153,6 @@ "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -22256,7 +22287,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -22300,7 +22330,6 @@ "integrity": "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -24691,7 +24720,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -26024,7 +26052,8 @@ "node_modules/style-mod": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "peer": true }, "node_modules/stylus-lookup": { "version": "6.1.0", @@ -26499,7 +26528,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -26821,7 +26849,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -27275,7 +27302,8 @@ "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "peer": true }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", @@ -27358,7 +27386,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -27408,7 +27435,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", diff --git a/webpack/base.config.js b/webpack/base.config.js index c8e7168eab..1e3c0de962 100644 --- a/webpack/base.config.js +++ b/webpack/base.config.js @@ -25,6 +25,7 @@ module.exports = { }, resolve: { modules: [__dirname, path.join(__dirname, '../src'), path.join(__dirname, '../node_modules')], + fullySpecified: false, }, resolveLoader: { modules: [path.join(__dirname, '../node_modules')], @@ -36,6 +37,12 @@ module.exports = { exclude: /node_modules/, use: ['babel-loader'], }, + { + test: /\.m?js/, + resolve: { + fullySpecified: false, + }, + }, { test: /\.scss$/, use: [ From 49e1e1969a4ec2ef436d4ac1c08cfed034730274 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:56:31 +0000 Subject: [PATCH 4/4] Address code review feedback: fix formatting and remove commented code Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- .../ChartVisualization.react.js | 2 +- .../DraggableResizablePanel.react.js | 8 ++++++-- .../DraggableResizablePanel.scss | 16 ---------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/components/ChartVisualization/ChartVisualization.react.js b/src/components/ChartVisualization/ChartVisualization.react.js index acdfda2f98..cfd205c411 100644 --- a/src/components/ChartVisualization/ChartVisualization.react.js +++ b/src/components/ChartVisualization/ChartVisualization.react.js @@ -492,7 +492,7 @@ const ChartVisualization = ({ /> ); } else { - // Bar ou Line Chart + // Bar or Line Chart const ChartComponent = chartType === 'bar' ? Bar : Line; // Improve options for correct sizing diff --git a/src/components/DraggableResizablePanel/DraggableResizablePanel.react.js b/src/components/DraggableResizablePanel/DraggableResizablePanel.react.js index d423c26bd5..2007b9e9c6 100644 --- a/src/components/DraggableResizablePanel/DraggableResizablePanel.react.js +++ b/src/components/DraggableResizablePanel/DraggableResizablePanel.react.js @@ -27,7 +27,9 @@ const DraggableResizablePanel = ({ const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const panelRef = useRef(null); - const titleBarRef = useRef(null); const handleMouseDown = (e) => { + const titleBarRef = useRef(null); + + const handleMouseDown = (e) => { // Check if the click was on the title bar or its children if (titleBarRef.current && ( e.target === titleBarRef.current || @@ -45,7 +47,9 @@ const DraggableResizablePanel = ({ e.stopPropagation(); } } - }; const handleMouseMove = (e) => { + }; + + const handleMouseMove = (e) => { if (isDragging) { const newX = e.clientX - dragStart.x; const newY = e.clientY - dragStart.y; diff --git a/src/components/DraggableResizablePanel/DraggableResizablePanel.scss b/src/components/DraggableResizablePanel/DraggableResizablePanel.scss index b71b5d229e..cecc2681c9 100644 --- a/src/components/DraggableResizablePanel/DraggableResizablePanel.scss +++ b/src/components/DraggableResizablePanel/DraggableResizablePanel.scss @@ -31,8 +31,6 @@ cursor: se-resize; border-radius: 50%; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); - //background: #1f8ce6; - //border: 2px solid white; } :global(.react-resizable-handle-e) { @@ -42,8 +40,6 @@ width: 8px; cursor: e-resize; border-radius: 4px; - //background: #1f8ce6; - //border: 1px solid white; } :global(.react-resizable-handle-s) { @@ -53,8 +49,6 @@ height: 8px; cursor: s-resize; border-radius: 4px; - //background: #1f8ce6; - //border: 1px solid white; } :global(.react-resizable-handle-w) { @@ -64,8 +58,6 @@ width: 8px; cursor: w-resize; border-radius: 4px; - //background: #1f8ce6; - //border: 1px solid white; } :global(.react-resizable-handle-n) { @@ -75,8 +67,6 @@ height: 8px; cursor: n-resize; border-radius: 4px; - //background: #1f8ce6; - //border: 1px solid white; } :global(.react-resizable-handle-ne) { @@ -86,8 +76,6 @@ height: 12px; cursor: ne-resize; border-radius: 50%; - //background: #1f8ce6; - //border: 2px solid white; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); } @@ -98,8 +86,6 @@ height: 12px; cursor: nw-resize; border-radius: 50%; - //background: #1f8ce6; - //border: 2px solid white; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); } @@ -110,8 +96,6 @@ height: 12px; cursor: sw-resize; border-radius: 50%; - //background: #1f8ce6; - //border: 2px solid white; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); } }