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);
}
}