mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-23 22:50:09 +03:00
Revert "F OpenNebula/one#6121: Added new multichart (#2703)"
This reverts commit 87a009f165c55b953cbee961e84debec8ed41c4f.
This commit is contained in:
parent
cde8501eab
commit
addb5edc33
1104
src/fireedge/package-lock.json
generated
1104
src/fireedge/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,7 +36,6 @@
|
||||
"eslint-config-opennebula": "2.0.2",
|
||||
"eslint-import-resolver-alias": "1.1.2",
|
||||
"eslint-import-resolver-webpack": "0.13.0",
|
||||
"file-loader": "5.0.0",
|
||||
"opennebula-generatepotfile": "1.0.0",
|
||||
"opennebula-potojson": "1.0.0",
|
||||
"react-refresh": "0.10.0",
|
||||
@ -65,10 +64,9 @@
|
||||
"@loadable/server": "5.15.1",
|
||||
"@loadable/webpack-plugin": "5.15.1",
|
||||
"@mui/lab": "5.0.0-alpha.59",
|
||||
"@mui/material": "5.4.1",
|
||||
"@mui/material": "5.1.0",
|
||||
"@mui/styles": "5.1.0",
|
||||
"@mui/system": "5.4.1",
|
||||
"@mui/x-data-grid": "5.0.1",
|
||||
"@mui/system": "5.1.0",
|
||||
"@reduxjs/toolkit": "1.7.1",
|
||||
"atob": "2.1.2",
|
||||
"axios": "0.25.0",
|
||||
@ -111,8 +109,8 @@
|
||||
"notistack": "2.0.3",
|
||||
"opennebula-guacamole": "1.0.0",
|
||||
"opennebula-wmks": "2.1.4",
|
||||
"papaparse": "5.4.1",
|
||||
"path": "0.12.7",
|
||||
"process": "0.11.10",
|
||||
"prop-types": "15.7.2",
|
||||
"qrcode": "1.4.4",
|
||||
"react": "17.0.2",
|
||||
@ -129,7 +127,6 @@
|
||||
"react-table": "7.7.0",
|
||||
"react-transition-group": "4.4.1",
|
||||
"react-virtual": "2.7.1",
|
||||
"recharts": "2.7.3",
|
||||
"redux": "4.1.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"rimraf": "3.0.2",
|
||||
|
@ -1,83 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import { CartesianGrid } from 'recharts'
|
||||
import { Component, Fragment } from 'react'
|
||||
|
||||
/**
|
||||
* Generates a color based on the metric, datasetId, and theme type.
|
||||
*
|
||||
* @function
|
||||
* @param {string} metric - The metric for which the color is generated.
|
||||
* @param {object} metricHues - Object containing hue values for different metrics.
|
||||
* @param {number} datasetId - The ID of the dataset.
|
||||
* @returns {string} The generated color in HSL format.
|
||||
*/
|
||||
export const generateColorByMetric = (metric, metricHues, datasetId) => {
|
||||
const baseHue = metricHues[metric] || 0
|
||||
|
||||
// Using datasetId as seed to generate a unique hue offset
|
||||
const hueOffset = (datasetId * 1327) % 360 // 1327 is just a random prime number
|
||||
|
||||
const baseSaturation = 90
|
||||
const baseLightness = 60
|
||||
|
||||
const hue = (baseHue + hueOffset) % 360
|
||||
const saturation = `${baseSaturation}%`
|
||||
const lightness = `${baseLightness}%`
|
||||
|
||||
return `hsl(${hue}, ${saturation}, ${lightness})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a React component containing SVG definitions and a CartesianGrid for a chart.
|
||||
*
|
||||
* @function
|
||||
* @param {Array<string>} metrics - List of metrics.
|
||||
* @param {number} datasetId - The ID of the dataset.
|
||||
* @param {object} metricHues - Object containing hue values for different metrics.
|
||||
* @returns {Component} A React component with SVG definitions and a CartesianGrid.
|
||||
*/
|
||||
export const GetChartDefs = (metrics, datasetId, metricHues) => (
|
||||
<Fragment key={`defs-${datasetId}`}>
|
||||
<defs>
|
||||
{metrics.map((metric) => {
|
||||
const color = generateColorByMetric(metric, metricHues, datasetId)
|
||||
|
||||
return (
|
||||
<linearGradient
|
||||
key={metric}
|
||||
id={`color${metric}-${datasetId}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="5%" stopColor={color} stopOpacity={0.9} />
|
||||
<stop offset="95%" stopColor={color} stopOpacity={0.2} />
|
||||
</linearGradient>
|
||||
)
|
||||
})}
|
||||
</defs>
|
||||
<CartesianGrid stroke={'#ccc'} strokeDasharray="4 4" strokeOpacity={0.1} />
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
GetChartDefs.propTypes = {
|
||||
metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
metricHues: PropTypes.objectOf(PropTypes.number).isRequired,
|
||||
datasetId: PropTypes.number.isRequired,
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/**
|
||||
* Processes data for rendering on a chart.
|
||||
*
|
||||
* This function restructures the input data. For each unique grouping attribute,
|
||||
* it creates an object that sums metric values across all datasets. If a dataset doesn't have
|
||||
* a metric value for a particular group, it defaults to 0.
|
||||
*
|
||||
* @function
|
||||
* @param {Array<string>} uniqueGroups - An array of unique groups representing different data points or entities.
|
||||
* @param {Array<object>} datasets - An array of datasets.
|
||||
* @param {Array<number>} visibleDatasetIDs - An array of dataset ID's to display.
|
||||
* @param {string} groupBy - The attribute by which data should be grouped (e.g., 'NAME', 'OID').
|
||||
* @returns {Array<object>} An array of processed data items, each structured with properties for every metric from every dataset.
|
||||
*/
|
||||
export const processDataForChart = (
|
||||
uniqueGroups,
|
||||
datasets,
|
||||
visibleDatasetIDs,
|
||||
groupBy
|
||||
) => {
|
||||
const visibleDatasets = datasets.filter((dataset) =>
|
||||
visibleDatasetIDs.includes(dataset.id)
|
||||
)
|
||||
|
||||
return uniqueGroups.map((group) => {
|
||||
const item = { [groupBy]: group }
|
||||
visibleDatasets.forEach((dataset) => {
|
||||
const matchingItem = dataset.data.find((d) => d[groupBy] === group)
|
||||
dataset.metrics.forEach((metric) => {
|
||||
item[`${metric.key}-${dataset.id}`] = matchingItem
|
||||
? matchingItem[metric.key]
|
||||
: 0
|
||||
})
|
||||
})
|
||||
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively searches for the first array of objects in the given object.
|
||||
* Used with all pool-like API requests to find the data array dynamically.
|
||||
*
|
||||
* @param {object} obj - The object to search within.
|
||||
* @returns {Array|null} - The found array or null if not found.
|
||||
*/
|
||||
const findFirstArray = (obj) => {
|
||||
for (const [, value] of Object.entries(obj)) {
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
typeof value[0] === 'object'
|
||||
) {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const result = findFirstArray(value)
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the API response into the desired dataset format.
|
||||
*
|
||||
* @param {object} apiResponse - The API response to process.
|
||||
* @param {object} keyMap - An object that maps the keys in the API response to the desired output keys.
|
||||
* @param {Array} metricKeys - An array of keys to aggregate for the metrics.
|
||||
* @param {Function} labelingFunction - A function to generate the label for the dataset.
|
||||
* @returns {object} - The transformed dataset.
|
||||
*/
|
||||
export const transformApiResponseToDataset = (
|
||||
apiResponse,
|
||||
keyMap,
|
||||
metricKeys,
|
||||
labelingFunction
|
||||
) => {
|
||||
const dataArray = findFirstArray(apiResponse)
|
||||
|
||||
const transformedRecords = dataArray.map((record) => {
|
||||
const transformedRecord = {}
|
||||
Object.keys(keyMap).forEach((key) => {
|
||||
transformedRecord[keyMap[key]] = record[key]
|
||||
})
|
||||
|
||||
return transformedRecord
|
||||
})
|
||||
|
||||
const metrics = metricKeys.map((key) => {
|
||||
const total = transformedRecords.reduce(
|
||||
(acc, record) => acc + parseFloat(record[key] || 0),
|
||||
0
|
||||
)
|
||||
|
||||
return { key: key, value: total }
|
||||
})
|
||||
|
||||
let label = 'N/A'
|
||||
if (labelingFunction) {
|
||||
try {
|
||||
label = labelingFunction(transformedRecords[0])
|
||||
} catch (error) {
|
||||
// Handle this sometime
|
||||
}
|
||||
|
||||
return {
|
||||
id: generateDatasetId({
|
||||
data: transformedRecords,
|
||||
metrics: metrics,
|
||||
label: label,
|
||||
}),
|
||||
data: transformedRecords,
|
||||
metrics: metrics,
|
||||
label: label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a processed dataset based on a custom filter function and recalculates the label.
|
||||
*
|
||||
* @param {object} dataset - The processed dataset.
|
||||
* @param {Function} filterFn - A custom function that determines which records to include.
|
||||
* @param {Function} labelingFunction - A function to generate the label for the subset.
|
||||
* @returns {object} - A subset of the dataset with a recalculated label.
|
||||
*/
|
||||
export const filterDataset = (dataset, filterFn, labelingFunction) => {
|
||||
const { data, metrics } = dataset
|
||||
|
||||
const filteredData = data.filter(filterFn)
|
||||
|
||||
const filteredMetrics = metrics.map((metric) => {
|
||||
const total = filteredData.reduce(
|
||||
(acc, record) => acc + parseFloat(record[metric.key] || 0),
|
||||
0
|
||||
)
|
||||
|
||||
return { key: metric.key, value: total }
|
||||
})
|
||||
|
||||
let label = 'N/A'
|
||||
if (labelingFunction && filteredData.length > 0) {
|
||||
try {
|
||||
label = labelingFunction(filteredData[0])
|
||||
} catch (error) {
|
||||
// Handle this sometime
|
||||
}
|
||||
|
||||
return {
|
||||
id: generateDatasetId({
|
||||
data: filteredData,
|
||||
metrics: metrics,
|
||||
label: label,
|
||||
}),
|
||||
data: filteredData,
|
||||
metrics: filteredMetrics,
|
||||
label: label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateDatasetId = (dataset) => {
|
||||
const dataLength = dataset.data.length
|
||||
if (dataLength === 0) return generateChecksum('empty')
|
||||
|
||||
const firstRecord = JSON.stringify(dataset.data[0])
|
||||
const middleRecord = JSON.stringify(dataset.data[Math.floor(dataLength / 2)])
|
||||
const lastRecord = JSON.stringify(dataset.data[dataLength - 1])
|
||||
|
||||
const combinedString = firstRecord + middleRecord + lastRecord + dataLength // This will not collide
|
||||
|
||||
return generateChecksum(combinedString)
|
||||
}
|
||||
|
||||
const generateChecksum = (input) => {
|
||||
let sum = 0
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
sum += input.charCodeAt(i)
|
||||
}
|
||||
|
||||
return sum * 1327 // Random prime number
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import Papa from 'papaparse'
|
||||
|
||||
/**
|
||||
* Exports the provided data as a CSV file.
|
||||
*
|
||||
* @function
|
||||
* @param {Array<object>} data - An array of datasets containing data to be exported.
|
||||
* @returns {Error} - Returns the error to the Exporter component to enqueue it.
|
||||
*/
|
||||
export const exportDataToCSV = (data) => {
|
||||
try {
|
||||
const csvData = data.flatMap((item) => item.data)
|
||||
|
||||
const csv = Papa.unparse(csvData)
|
||||
const blob = new Blob([csv], { type: 'text/csv' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('hidden', '')
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute(
|
||||
'download',
|
||||
data[0]?.label
|
||||
? `${data[0].label.replace(/ /g, '')}_report.csv`
|
||||
: 'one_report.csv'
|
||||
)
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Box,
|
||||
} from '@mui/material'
|
||||
|
||||
import logo from 'client/assets/images/logo.png'
|
||||
|
||||
/**
|
||||
* Generates a printable PDF report from the provided data.
|
||||
*
|
||||
* @function
|
||||
* @param {Array<object>} data - The data to be exported to PDF.
|
||||
* @returns {Error} - Returns the error to the Exporter component to enqueue it.
|
||||
*/
|
||||
export const exportDataToPDF = (data) => {
|
||||
try {
|
||||
const rows = data.flatMap((item) => item.data)
|
||||
|
||||
const tableComponent = (
|
||||
<Box
|
||||
style={{
|
||||
padding: '20px',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
backgroundColor: '#f9f9f9',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
style={{
|
||||
maxWidth: '150px',
|
||||
maxHeight: '50px',
|
||||
marginBottom: '10px',
|
||||
marginLeft: '0px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<h2 style={{ textAlign: 'center', margin: '20px 0', color: '#333' }}>
|
||||
Data Report
|
||||
</h2>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{Object.keys(rows[0]).map((header) => (
|
||||
<TableCell
|
||||
key={header}
|
||||
style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
fontWeight: 'bold',
|
||||
color: '#555',
|
||||
border: '1px solid #333',
|
||||
}}
|
||||
>
|
||||
{header}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<TableRow key={rowIndex}>
|
||||
{Object.values(row).map((value, valueIndex) => (
|
||||
<TableCell
|
||||
key={valueIndex}
|
||||
style={{ color: '#666', border: '1px solid #333' }}
|
||||
>
|
||||
{String(value)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)
|
||||
|
||||
const tableHTML = ReactDOMServer.renderToString(tableComponent)
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.style.display = 'none'
|
||||
document.body.appendChild(iframe)
|
||||
iframe.contentDocument.write(
|
||||
'<html><head><title>Accounting Report</title></head><body>'
|
||||
)
|
||||
iframe.contentDocument.write(tableHTML)
|
||||
iframe.contentDocument.write('</body></html>')
|
||||
iframe.contentDocument.close()
|
||||
|
||||
iframe.contentWindow.print()
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframe)
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import {
|
||||
processDataForChart,
|
||||
transformApiResponseToDataset,
|
||||
filterDataset,
|
||||
} from 'client/components/Charts/MultiChart/helpers/scripts/dataProcessing'
|
||||
import {
|
||||
generateColorByMetric,
|
||||
GetChartDefs,
|
||||
} from 'client/components/Charts/MultiChart/helpers/scripts/chartDefs'
|
||||
import { exportDataToPDF } from 'client/components/Charts/MultiChart/helpers/scripts/exportPDF'
|
||||
import { exportDataToCSV } from 'client/components/Charts/MultiChart/helpers/scripts/exportCSV'
|
||||
|
||||
export {
|
||||
processDataForChart,
|
||||
transformApiResponseToDataset,
|
||||
filterDataset,
|
||||
generateColorByMetric,
|
||||
GetChartDefs,
|
||||
exportDataToPDF,
|
||||
exportDataToCSV,
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
AreaChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
Bar,
|
||||
Legend,
|
||||
Line,
|
||||
Area,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
|
||||
import { DataGridTable } from 'client/components/Tables'
|
||||
import { CustomTooltip } from 'client/components/Tooltip'
|
||||
|
||||
import { generateColorByMetric } from 'client/components/Charts/MultiChart/helpers/scripts'
|
||||
|
||||
const CHART_TYPES = {
|
||||
BAR: 'bar',
|
||||
LINE: 'line',
|
||||
AREA: 'area',
|
||||
TABLE: 'table',
|
||||
}
|
||||
|
||||
const ChartComponents = {
|
||||
[CHART_TYPES.BAR]: BarChart,
|
||||
[CHART_TYPES.LINE]: LineChart,
|
||||
[CHART_TYPES.AREA]: AreaChart,
|
||||
[CHART_TYPES.TABLE]: DataGridTable,
|
||||
}
|
||||
|
||||
const ChartElements = {
|
||||
[CHART_TYPES.BAR]: Bar,
|
||||
[CHART_TYPES.LINE]: Line,
|
||||
[CHART_TYPES.AREA]: Area,
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a chart based on the provided type and data.
|
||||
*
|
||||
* @param {object} props - The properties for the component.
|
||||
* @param {'bar' | 'line' | 'area' | 'table'} props.chartType - The type of chart to render.
|
||||
* @param {Array} props.datasets - The datasets to be used for the chart.
|
||||
* @param {object} props.selectedMetrics - The metrics selected for display.
|
||||
* @param {Function} props.customChartDefs - Custom definitions for the chart.
|
||||
* @param {Array} props.paginatedData - The paginated data for the chart.
|
||||
* @param {Array} props.tableColumns - The columns for the table chart type.
|
||||
* @param {Function} props.humanReadableMetric - Function to convert metric keys to human-readable format.
|
||||
* @param {string} props.groupBy - The variable to group data under.
|
||||
* @param {object} props.metricHues - Object containing hue values for different metrics.
|
||||
* @returns {React.Component} The rendered chart component.
|
||||
*/
|
||||
export const ChartRenderer = ({
|
||||
chartType,
|
||||
datasets,
|
||||
selectedMetrics,
|
||||
customChartDefs,
|
||||
paginatedData,
|
||||
tableColumns,
|
||||
humanReadableMetric,
|
||||
groupBy,
|
||||
metricHues,
|
||||
}) => {
|
||||
const ChartComponent = ChartComponents[chartType]
|
||||
const ChartElement = ChartElements[chartType]
|
||||
|
||||
return (
|
||||
<ResponsiveContainer height="100%" width="100%">
|
||||
{chartType === CHART_TYPES.TABLE ? (
|
||||
<DataGridTable
|
||||
columns={tableColumns}
|
||||
data={datasets}
|
||||
selectedItems={selectedMetrics}
|
||||
/>
|
||||
) : (
|
||||
<ChartComponent
|
||||
data={paginatedData}
|
||||
barCategoryGap={20}
|
||||
style={!datasets.length ? { pointerEvents: 'none' } : {}}
|
||||
padding={{ top: 0, right: 60, bottom: 0, left: 60 }}
|
||||
>
|
||||
{datasets.map((dataset) =>
|
||||
customChartDefs(
|
||||
dataset.metrics.map((m) => m.key),
|
||||
dataset.id,
|
||||
metricHues
|
||||
)
|
||||
)}
|
||||
<XAxis interval={0} dataKey={groupBy} />
|
||||
<YAxis />
|
||||
<Tooltip
|
||||
content={
|
||||
<CustomTooltip
|
||||
labels={datasets.map((ds) => ds.label)}
|
||||
generateColor={generateColorByMetric}
|
||||
formatMetric={humanReadableMetric}
|
||||
metricHues={metricHues}
|
||||
/>
|
||||
}
|
||||
cursor="pointer"
|
||||
/>
|
||||
<Legend
|
||||
formatter={(value) => {
|
||||
const [metric, datasetId] = value.split('-')
|
||||
const currentDataset = datasets.find(
|
||||
(ds) => ds.id === parseInt(datasetId, 10)
|
||||
)
|
||||
|
||||
const datasetLabel = currentDataset.label
|
||||
|
||||
const lastSelectedMetric = [...currentDataset.metrics]
|
||||
.reverse()
|
||||
.find((m) => selectedMetrics[m.key])
|
||||
|
||||
if (lastSelectedMetric && metric === lastSelectedMetric.key) {
|
||||
return `${humanReadableMetric(metric)} (${datasetLabel})`
|
||||
}
|
||||
|
||||
return humanReadableMetric(metric)
|
||||
}}
|
||||
wrapperStyle={{
|
||||
wordWrap: 'break-word',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
/>
|
||||
|
||||
{datasets.map((dataset) =>
|
||||
dataset.metrics.map((metric) =>
|
||||
selectedMetrics[metric.key] ? (
|
||||
<ChartElement
|
||||
key={`${metric.key}-${dataset.id}`}
|
||||
type="monotone"
|
||||
dataKey={`${metric.key}-${dataset.id}`}
|
||||
fill={`url(#color${metric.key}-${dataset.id})`}
|
||||
name={metric.name}
|
||||
animationDuration={500}
|
||||
{...(chartType === 'area' && {
|
||||
fillOpacity: 0.5,
|
||||
stroke: 'transparent',
|
||||
})}
|
||||
{...(chartType === 'line' && {
|
||||
strokeWidth: 3,
|
||||
activeDot: {
|
||||
r: 8,
|
||||
fill: `url(#color${metric.key}-${dataset.id})`,
|
||||
stroke: 'white',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
stroke: `url(#color${metric.key}-${dataset.id})`,
|
||||
})}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
)}
|
||||
</ChartComponent>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
|
||||
ChartRenderer.propTypes = {
|
||||
chartType: PropTypes.oneOf(['bar', 'line', 'area', 'table']).isRequired,
|
||||
datasets: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedMetrics: PropTypes.object.isRequired,
|
||||
customChartDefs: PropTypes.func.isRequired,
|
||||
paginatedData: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tableColumns: PropTypes.arrayOf(PropTypes.object),
|
||||
humanReadableMetric: PropTypes.func.isRequired,
|
||||
groupBy: PropTypes.string.isRequired,
|
||||
metricHues: PropTypes.objectOf(PropTypes.number).isRequired,
|
||||
}
|
||||
|
||||
ChartRenderer.defaultProps = {
|
||||
groupBy: 'NAME',
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useState } from 'react'
|
||||
import { Button, Menu, MenuItem, Box } from '@mui/material'
|
||||
import { Download } from 'iconoir-react'
|
||||
import {
|
||||
exportDataToCSV,
|
||||
exportDataToPDF,
|
||||
} from 'client/components/Charts/MultiChart/helpers/scripts'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
/**
|
||||
* Renders a button that provides export options for data.
|
||||
*
|
||||
* @param {object} props - The properties for the component.
|
||||
* @param {Array} props.data - The data to be exported.
|
||||
* @param {Array} props.exportOptions - The available export options.
|
||||
* @param {object} props.exportHandlers - The handlers for each export type.
|
||||
* @returns {React.Component} The rendered export button component.
|
||||
*/
|
||||
export const ExportButton = ({ data, exportOptions, exportHandlers }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const { enqueueError } = useGeneralApi()
|
||||
|
||||
const handleMenuOpen = (event) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleExport = (type) => {
|
||||
if (exportHandlers[type]) {
|
||||
const error = exportHandlers[type](data)
|
||||
if (error) {
|
||||
enqueueError(
|
||||
'Error exporting data to ' + type.toUpperCase() + ': ' + error.message
|
||||
)
|
||||
}
|
||||
}
|
||||
handleMenuClose()
|
||||
}
|
||||
|
||||
const noData =
|
||||
!data ||
|
||||
data.length === 0 ||
|
||||
data.every((item) => !item.data || item.data.length === 0)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Button
|
||||
endIcon={<Download />}
|
||||
onClick={handleMenuOpen}
|
||||
disabled={noData}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
padding: '3px 17px',
|
||||
transition: 'all 0.1s ease',
|
||||
'&:hover': {
|
||||
borderColor: '#2a2a2a',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
{exportOptions.map((option) => (
|
||||
<MenuItem key={option.type} onClick={() => handleExport(option.type)}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
ExportButton.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
exportOptions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
type: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
exportHandlers: PropTypes.objectOf(PropTypes.func),
|
||||
}
|
||||
|
||||
ExportButton.defaultProps = {
|
||||
exportOptions: [
|
||||
{ type: 'csv', label: 'Export as CSV' },
|
||||
{ type: 'pdf', label: 'Export as PDF' },
|
||||
],
|
||||
exportHandlers: {
|
||||
csv: (data) => exportDataToCSV(data),
|
||||
pdf: (data) => exportDataToPDF(data),
|
||||
},
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { NavArrowRight, NavArrowLeft, Filter } from 'iconoir-react'
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
FormControl,
|
||||
Select,
|
||||
MenuItem,
|
||||
Checkbox,
|
||||
ListItemText,
|
||||
} from '@mui/material'
|
||||
|
||||
/**
|
||||
* NavigationController component for navigating through items and filtering them.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {Function} props.onPrev - Callback function when the previous button is clicked.
|
||||
* @param {Function} props.onNext - Callback function when the next button is clicked.
|
||||
* @param {boolean} props.isPrevDisabled - Determines if the previous button is disabled.
|
||||
* @param {boolean} props.isNextDisabled - Determines if the next button is disabled.
|
||||
* @param {Array} props.selectedItems - List of currently selected items.
|
||||
* @param {Array} props.items - List of all available items.
|
||||
* @param {Function} props.setSelectedItems - Callback function to set the selected items.
|
||||
* @param {boolean} props.isFilterDisabled - Determines if the filter is disabled.
|
||||
* @param {boolean} props.isPaginationDisabled - Determines if the pagination is disabled.
|
||||
* @param {object} props.styles - Custom styles for the component.
|
||||
* @returns {React.Component} NavigationController component.
|
||||
*/
|
||||
export const NavigationController = ({
|
||||
onPrev,
|
||||
onNext,
|
||||
isPrevDisabled,
|
||||
isNextDisabled,
|
||||
selectedItems,
|
||||
items,
|
||||
setSelectedItems,
|
||||
isFilterDisabled,
|
||||
isPaginationDisabled,
|
||||
styles,
|
||||
}) => (
|
||||
<Box sx={{ ...styles.container, display: 'inline-flex' }}>
|
||||
<BoxedIcon disabled={isFilterDisabled}>
|
||||
<FormControl size="small" sx={styles.formControl}>
|
||||
<Select
|
||||
multiple
|
||||
value={selectedItems}
|
||||
onChange={(event) => setSelectedItems(event.target.value)}
|
||||
IconComponent={Filter}
|
||||
sx={styles.select}
|
||||
renderValue={(selected) => ''}
|
||||
disabled={isFilterDisabled}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
<Checkbox checked={selectedItems.includes(item)} />
|
||||
<ListItemText primary={item} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</BoxedIcon>
|
||||
|
||||
<Box sx={styles.divider} />
|
||||
|
||||
<BoxedIcon
|
||||
onClick={onPrev}
|
||||
disabled={isPrevDisabled || isPaginationDisabled}
|
||||
>
|
||||
<NavArrowLeft />
|
||||
</BoxedIcon>
|
||||
|
||||
<Box sx={styles.divider} />
|
||||
|
||||
<BoxedIcon
|
||||
onClick={onNext}
|
||||
disabled={isNextDisabled || isPaginationDisabled}
|
||||
>
|
||||
<NavArrowRight />
|
||||
</BoxedIcon>
|
||||
</Box>
|
||||
)
|
||||
|
||||
const BoxedIcon = ({ children, ...props }) => (
|
||||
<Box
|
||||
component={IconButton}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
justifyContent: 'center',
|
||||
'&.Mui-focusVisible': {
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
|
||||
NavigationController.propTypes = {
|
||||
onPrev: PropTypes.func.isRequired,
|
||||
onNext: PropTypes.func.isRequired,
|
||||
isPrevDisabled: PropTypes.bool.isRequired,
|
||||
isNextDisabled: PropTypes.bool.isRequired,
|
||||
selectedItems: PropTypes.array.isRequired,
|
||||
items: PropTypes.array,
|
||||
setSelectedItems: PropTypes.func.isRequired,
|
||||
isFilterDisabled: PropTypes.bool.isRequired,
|
||||
isPaginationDisabled: PropTypes.bool.isRequired,
|
||||
styles: PropTypes.object,
|
||||
}
|
||||
|
||||
BoxedIcon.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
NavigationController.defaultProps = {
|
||||
styles: {
|
||||
container: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: 'none',
|
||||
borderBottom: 'none',
|
||||
borderRadius: '2px 2px 0 0',
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 40,
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
border: 'none',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
border: 'none',
|
||||
},
|
||||
},
|
||||
select: {
|
||||
borderRadius: 0,
|
||||
paddingRight: 0,
|
||||
border: 'none',
|
||||
},
|
||||
divider: {
|
||||
width: 1,
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
items: [],
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ChartRenderer } from 'client/components/Charts/MultiChart/helpers/subComponents/ChartRenderer'
|
||||
import { NavigationController } from 'client/components/Charts/MultiChart/helpers/subComponents/NavigationController'
|
||||
import { ExportButton } from 'client/components/Charts/MultiChart/helpers/subComponents/Exporter'
|
||||
|
||||
export { ChartRenderer, NavigationController, ExportButton }
|
@ -1,252 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useState, useMemo, useEffect } from 'react'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
import {
|
||||
ChartRenderer,
|
||||
NavigationController,
|
||||
ExportButton,
|
||||
} from 'client/components/Charts/MultiChart/helpers/subComponents'
|
||||
import { LoadingDisplay } from 'client/components/LoadingState'
|
||||
import {
|
||||
processDataForChart,
|
||||
GetChartDefs,
|
||||
} from 'client/components/Charts/MultiChart/helpers/scripts'
|
||||
|
||||
/**
|
||||
* @param {object} props - Component properties.
|
||||
* @param {Array} props.datasets - Array of datasets to visualize.
|
||||
* @param {Array} props.visibleDatasets - Array of visible dataset IDs (subset of datasets).
|
||||
* @param {Array} props.xAxisLabels - Array of unique names for the X-axis.
|
||||
* @param {string} props.chartType - Type of chart to render ('bar', 'line', etc.).
|
||||
* @param {object} props.selectedMetrics - Object containing selected metrics (e.g., "cpuHours").
|
||||
* @param {string} [props.error] - Error message, if any.
|
||||
* @param {string} props.groupBy - Key to group X values by.
|
||||
* @param {number} props.ItemsPerPage - Number of items to display per page.
|
||||
* @param {Array} props.tableColumns - Array of table column configurations.
|
||||
* @param {Function} props.customChartDefs - Function to generate custom chart definitions.
|
||||
* @param {object} [props.metricNames={}] - Object mapping metric keys to human-readable names.
|
||||
* @param {object} props.metricHues - Object containing hue values for different metrics.
|
||||
* @returns {React.Component} MultiChart component.
|
||||
*/
|
||||
const MultiChart = ({
|
||||
datasets,
|
||||
visibleDatasets,
|
||||
xAxisLabels: passedxAxisLabels,
|
||||
chartType,
|
||||
selectedMetrics: passedSelectedMetrics,
|
||||
error,
|
||||
ItemsPerPage,
|
||||
tableColumns,
|
||||
customChartDefs,
|
||||
metricNames,
|
||||
groupBy,
|
||||
metricHues: passedMetricHues,
|
||||
}) => {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const [selectedXValues, setSelectedXValues] = useState([])
|
||||
const [isFilterDisabled, setIsFilterDisabled] = useState(true)
|
||||
const [isPaginationDisabled, setIsPaginationDisabled] = useState(true)
|
||||
|
||||
const xAxisLabels = useMemo(() => {
|
||||
if (passedxAxisLabels && passedxAxisLabels.length > 0) {
|
||||
return passedxAxisLabels
|
||||
}
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
datasets.flatMap((dataset) => dataset.data.map((item) => item[groupBy]))
|
||||
),
|
||||
]
|
||||
}, [datasets, groupBy, passedxAxisLabels])
|
||||
|
||||
const selectedMetrics = useMemo(() => {
|
||||
if (
|
||||
passedSelectedMetrics &&
|
||||
Object.keys(passedSelectedMetrics).length > 0
|
||||
) {
|
||||
return passedSelectedMetrics
|
||||
}
|
||||
|
||||
return datasets.reduce((acc, dataset) => {
|
||||
dataset.metrics.forEach((metric) => {
|
||||
acc[metric.key] = true
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}, [datasets, passedSelectedMetrics])
|
||||
|
||||
const metricHues = useMemo(() => {
|
||||
if (passedMetricHues && Object.keys(passedMetricHues).length > 0) {
|
||||
return passedMetricHues
|
||||
}
|
||||
|
||||
const allMetrics = [
|
||||
...new Set(
|
||||
datasets.flatMap((dataset) =>
|
||||
dataset.metrics.map((metric) => metric.key)
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
const hueStep = 360 / allMetrics.length
|
||||
|
||||
return allMetrics.reduce((acc, metric, index) => {
|
||||
acc[metric] = index * hueStep
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}, [datasets, passedMetricHues])
|
||||
|
||||
const visibleDatasetIDs = useMemo(() => {
|
||||
if (visibleDatasets && visibleDatasets.length > 0) {
|
||||
return visibleDatasets
|
||||
}
|
||||
|
||||
return datasets.map((dataset) => dataset.id)
|
||||
}, [datasets, visibleDatasets])
|
||||
|
||||
const noDataAvailable = visibleDatasetIDs.length === 0
|
||||
|
||||
const mergedDataForXAxis = processDataForChart(
|
||||
xAxisLabels,
|
||||
datasets,
|
||||
visibleDatasetIDs,
|
||||
groupBy
|
||||
)
|
||||
|
||||
const humanReadableMetric = (key) => metricNames[key] || key
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedXValues(xAxisLabels)
|
||||
}, [xAxisLabels])
|
||||
|
||||
useEffect(() => {
|
||||
setIsFilterDisabled(visibleDatasetIDs.length === 0)
|
||||
}, [visibleDatasetIDs])
|
||||
|
||||
useEffect(() => {
|
||||
setIsPaginationDisabled(
|
||||
selectedXValues.length <= ItemsPerPage || isFilterDisabled
|
||||
)
|
||||
}, [selectedXValues, isFilterDisabled])
|
||||
|
||||
const paginatedData = useMemo(() => {
|
||||
const filteredData = mergedDataForXAxis.filter((item) =>
|
||||
selectedXValues.includes(item[groupBy])
|
||||
)
|
||||
const start = currentPage * ItemsPerPage
|
||||
const end = start + ItemsPerPage
|
||||
|
||||
return filteredData.slice(start, end)
|
||||
}, [mergedDataForXAxis, currentPage, selectedXValues])
|
||||
|
||||
return (
|
||||
<Box width="100%" height="100%" display="flex" flexDirection="column">
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{chartType !== 'table' && (
|
||||
<Box flex={1}>
|
||||
<NavigationController
|
||||
onPrev={() => setCurrentPage((prev) => Math.max(prev - 1, 0))}
|
||||
onNext={() =>
|
||||
setCurrentPage((prev) =>
|
||||
Math.min(
|
||||
prev + 1,
|
||||
Math.ceil(selectedXValues.length / ItemsPerPage) - 1
|
||||
)
|
||||
)
|
||||
}
|
||||
isPrevDisabled={currentPage === 0}
|
||||
isNextDisabled={
|
||||
currentPage ===
|
||||
Math.ceil(selectedXValues.length / ItemsPerPage) - 1
|
||||
}
|
||||
selectedItems={selectedXValues}
|
||||
items={xAxisLabels}
|
||||
setSelectedItems={setSelectedXValues}
|
||||
isFilterDisabled={isFilterDisabled}
|
||||
isPaginationDisabled={isPaginationDisabled}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box flex={1} display="flex" justifyContent="flex-end">
|
||||
<ExportButton data={datasets} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flex={1} mt={-1}>
|
||||
{error || noDataAvailable ? (
|
||||
<LoadingDisplay error={error} />
|
||||
) : (
|
||||
<ChartRenderer
|
||||
chartType={chartType}
|
||||
datasets={datasets}
|
||||
selectedMetrics={selectedMetrics}
|
||||
customChartDefs={customChartDefs}
|
||||
tableColumns={tableColumns}
|
||||
paginatedData={paginatedData}
|
||||
humanReadableMetric={humanReadableMetric}
|
||||
groupBy={groupBy}
|
||||
metricHues={metricHues}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
MultiChart.propTypes = {
|
||||
datasets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired,
|
||||
metrics: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
visibleDatasets: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
xAxisLabels: PropTypes.arrayOf(PropTypes.string),
|
||||
chartType: PropTypes.oneOf(['bar', 'line', 'area', 'table']).isRequired,
|
||||
selectedMetrics: PropTypes.object,
|
||||
error: PropTypes.string,
|
||||
groupBy: PropTypes.string,
|
||||
ItemsPerPage: PropTypes.number.isRequired,
|
||||
tableColumns: PropTypes.arrayOf(PropTypes.object),
|
||||
customChartDefs: PropTypes.func.isRequired,
|
||||
metricNames: PropTypes.object,
|
||||
metricHues: PropTypes.objectOf(PropTypes.number).isRequired,
|
||||
}
|
||||
|
||||
MultiChart.defaultProps = {
|
||||
visibleDatasets: [],
|
||||
chartType: 'bar',
|
||||
ItemsPerPage: 10,
|
||||
groupBy: 'ID',
|
||||
customChartDefs: GetChartDefs,
|
||||
metricNames: {},
|
||||
metricHues: {},
|
||||
}
|
||||
|
||||
export default MultiChart
|
@ -16,6 +16,5 @@
|
||||
import CircleChart from 'client/components/Charts/CircleChart'
|
||||
import SingleBar from 'client/components/Charts/SingleBar'
|
||||
import Chartist from 'client/components/Charts/Chartist'
|
||||
import MultiChart from 'client/components/Charts/MultiChart/index'
|
||||
|
||||
export { CircleChart, SingleBar, Chartist, MultiChart }
|
||||
export { CircleChart, SingleBar, Chartist }
|
||||
|
@ -1,53 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { InfoEmpty, CloudError } from 'iconoir-react'
|
||||
|
||||
/**
|
||||
* Renders a display message based on the presence of an error.
|
||||
*
|
||||
* @param {object} props - The properties for the component.
|
||||
* @param {boolean} props.error - Indicates if there was an error fetching data.
|
||||
* @returns {React.Component} The rendered loading display component.
|
||||
*/
|
||||
export const LoadingDisplay = ({ error }) => {
|
||||
const displayMessage = error ? 'Error fetching data' : 'No data available'
|
||||
const DisplayIcon = error ? CloudError : InfoEmpty
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="500px"
|
||||
borderRadius={4}
|
||||
boxShadow={2}
|
||||
padding={3}
|
||||
>
|
||||
<DisplayIcon style={{ fontSize: 40 }} />
|
||||
<Typography variant="h6" color="textSecondary" marginTop={2}>
|
||||
{displayMessage}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
LoadingDisplay.propTypes = {
|
||||
error: PropTypes.bool,
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export { LoadingDisplay } from './LoadingDisplay'
|
@ -1,66 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { DataGrid } from '@mui/x-data-grid'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
/**
|
||||
* Renders a data grid table using the provided data and columns.
|
||||
*
|
||||
* @param {object} props - The properties for the component.
|
||||
* @param {Array} props.data - The data to be displayed in the table.
|
||||
* @param {Array} props.columns - The columns configuration for the table.
|
||||
* @returns {React.Component} The rendered data grid table component.
|
||||
*/
|
||||
const DataGridTable = ({ data, columns }) => {
|
||||
const flattenedData = data.flatMap((dataset) => dataset.data)
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
paddingBottom: '54px',
|
||||
paddingTop: '18px',
|
||||
}}
|
||||
>
|
||||
<DataGrid
|
||||
rows={flattenedData.map((row, index) => ({ ...row, id: index }))}
|
||||
columns={columns}
|
||||
rowsPerPageOptions={[25, 50, 100]}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
DataGridTable.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
columns: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
field: PropTypes.string.isRequired,
|
||||
headerName: PropTypes.string.isRequired,
|
||||
width: PropTypes.number,
|
||||
})
|
||||
).isRequired,
|
||||
}
|
||||
|
||||
export default DataGridTable
|
@ -17,7 +17,6 @@ import AllImagesTable from 'client/components/Tables/AllImages'
|
||||
import BackupsTable from 'client/components/Tables/Backups'
|
||||
import ClustersTable from 'client/components/Tables/Clusters'
|
||||
import DatastoresTable from 'client/components/Tables/Datastores'
|
||||
import DataGridTable from 'client/components/Tables/DataGrid'
|
||||
import DockerHubTagsTable from 'client/components/Tables/DockerHubTags'
|
||||
import EnhancedTable from 'client/components/Tables/Enhanced'
|
||||
import GroupsTable from 'client/components/Tables/Groups'
|
||||
@ -53,7 +52,6 @@ export {
|
||||
ClustersTable,
|
||||
DatastoresTable,
|
||||
DockerHubTagsTable,
|
||||
DataGridTable,
|
||||
GroupsTable,
|
||||
HostsTable,
|
||||
ImagesTable,
|
||||
|
@ -1,121 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Paper, Typography, Box } from '@mui/material'
|
||||
|
||||
export const CustomTooltip = React.memo(
|
||||
({ active, payload, labels, generateColor, formatMetric, metricHues }) => {
|
||||
if (active && payload && payload.length) {
|
||||
const groupedMetrics = useMemo(
|
||||
() =>
|
||||
payload.reduce((acc, entry) => {
|
||||
const [metric, datasetId] = entry.name.split('-')
|
||||
if (!acc[datasetId]) {
|
||||
acc[datasetId] = []
|
||||
}
|
||||
acc[datasetId].push({ metric, value: entry.value })
|
||||
|
||||
return acc
|
||||
}, {}),
|
||||
[payload]
|
||||
)
|
||||
|
||||
const keys = Object.keys(groupedMetrics)
|
||||
const itemWidth = keys.length === 1 ? '100%' : '50%'
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={3}
|
||||
style={{
|
||||
padding: '2px',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
maxWidth: '370px',
|
||||
}}
|
||||
>
|
||||
{keys.map((datasetId, index) => (
|
||||
<Box
|
||||
key={`dataset-${datasetId}`}
|
||||
style={{
|
||||
padding: '0 1px',
|
||||
width: itemWidth,
|
||||
boxSizing: 'border-box',
|
||||
maxWidth: '175px',
|
||||
flex: 'none',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
style={{ fontWeight: 'bold', margin: '0 0 2px 0' }}
|
||||
>
|
||||
{labels[index]}
|
||||
</Typography>
|
||||
{/* eslint-disable-next-line no-shadow */}
|
||||
{groupedMetrics[datasetId].map((entry, index) => {
|
||||
const metricColor = generateColor(
|
||||
entry.metric,
|
||||
metricHues,
|
||||
parseInt(datasetId, 10)
|
||||
)
|
||||
|
||||
const formattedValue =
|
||||
typeof entry.value === 'number'
|
||||
? entry.value.toFixed(2)
|
||||
: String(entry.value).slice(0, 12)
|
||||
|
||||
return (
|
||||
<Typography
|
||||
key={`metric-${index}`}
|
||||
variant="body2"
|
||||
style={{ margin: '0.5px 0' }}
|
||||
>
|
||||
<span style={{ color: metricColor }}>
|
||||
{formatMetric(entry.metric)}:
|
||||
</span>
|
||||
{formattedValue}
|
||||
</Typography>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
)
|
||||
|
||||
CustomTooltip.displayName = 'CustomTooltip'
|
||||
|
||||
CustomTooltip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.arrayOf(PropTypes.object),
|
||||
labels: PropTypes.arrayOf(PropTypes.string),
|
||||
generateColor: PropTypes.func,
|
||||
formatMetric: PropTypes.func,
|
||||
metricHues: PropTypes.objectOf(PropTypes.number).isRequired,
|
||||
}
|
||||
|
||||
CustomTooltip.defaultProps = {
|
||||
active: false,
|
||||
payload: [],
|
||||
labels: [],
|
||||
generateColor: (metric, hues, id) => hues[metric] || '#000',
|
||||
formatMetric: (input) => `${input}`,
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { CustomTooltip } from './MultiChart/AccountingTooltip'
|
||||
|
||||
export { CustomTooltip }
|
@ -52,10 +52,6 @@ const getDevConfiguration = () => {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: `${appName}/client`,
|
||||
},
|
||||
watchOptions: {
|
||||
poll: 1000,
|
||||
ignored: /node_modules/,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@ -75,18 +71,6 @@ const getDevConfiguration = () => {
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[path][name].[ext]',
|
||||
outputPath: 'assets/images/',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
|
@ -36,19 +36,6 @@ const css = {
|
||||
use: ['style-loader', 'css-loader'],
|
||||
}
|
||||
|
||||
const images = {
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[path][name].[ext]',
|
||||
outputPath: 'assets/images/',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle app.
|
||||
*
|
||||
@ -103,7 +90,7 @@ const bundle = ({ assets = false, name = 'sunstone' }) => {
|
||||
minimizer: [new TerserPlugin({ extractComments: false })],
|
||||
},
|
||||
module: {
|
||||
rules: [js, css, images],
|
||||
rules: [js, css],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -41,19 +41,6 @@ const css = {
|
||||
],
|
||||
}
|
||||
|
||||
const images = {
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[path][name].[ext]',
|
||||
outputPath: 'assets/images/',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const worker = {
|
||||
test: /\.worker\.js$/,
|
||||
loader: 'worker-loader',
|
||||
@ -92,6 +79,6 @@ module.exports = {
|
||||
minimizer: [new TerserPlugin({ extractComments: false })],
|
||||
},
|
||||
module: {
|
||||
rules: [js, worker, css, images],
|
||||
rules: [js, worker, css],
|
||||
},
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user