diff --git a/package.json b/package.json index fce3526334599e1f7d9303987f76c21c5e7b592d..183701d29f4ccf5ace7d1b1a5b812b8dcaf67ad5 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.3", "@testing-library/user-event": "^12.6.0", + "chart.js": "^3.4.1", + "chartjs-adapter-moment": "^1.0.0", + "chartjs-plugin-datalabels": "^2.0.0", + "chartjs-plugin-zoom": "^1.1.1", "isomorphic-fetch": "^3.0.0", "moment": "^2.29.1", "msw": "^0.28.2", @@ -21,6 +25,7 @@ "primeicons": "^4.1.0", "primereact": "^6.3.2", "react": "^17.0.1", + "react-chartjs-2": "^3.0.4", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", diff --git a/src/api/SwaggerApi.js b/src/api/SwaggerApi.js index 78d7e47a1e19bb3676a73dd8565344190256f940..a070abe4185280356679a512bf72b698870519de 100644 --- a/src/api/SwaggerApi.js +++ b/src/api/SwaggerApi.js @@ -449,6 +449,16 @@ export function useNamingNames() { return useAsync({ fcn: method, call: false, init: [] }); } +export function unpackDeploymentStatistics(statistics) { + return { ...statistics }; +} + +export function useDeploymentStatistics() { + const api = useContext(apiContext); + const method = callAndUnpack(api.apis.Statistics.deploymentStatistics, unpackDeploymentStatistics) + + return useAsync({ fcn: method, call: false }); +} export function unpackTagAndCommitIdList(tagList) { return tagList.toString().split(","); } @@ -457,4 +467,48 @@ export function useTagsAndCommitIds() { const api = useContext(apiContext); const method = callAndUnpack((repoUrl) => api.apis.Git.listTagsAndCommitIds({gitRepo: repoUrl}), unpackTagAndCommitIdList) return useAsync({ fcn: method, call: false, init: [] }); +} + +export function unpackIOCStatistics(statistics) { + return { ...statistics }; +} + +export function useIOCStatistics() { + const api = useContext(apiContext); + const method = callAndUnpack(api.apis.Statistics.iocStatistics, unpackIOCStatistics) + + return useAsync({ fcn: method, call: false }); +} + +export function unpackCurrentlyActiveIOCS(statistics) { + return { ...statistics }; +} + +export function useCurrentlyActiveIOCs() { + const api = useContext(apiContext); + const method = callAndUnpack(api.apis.Statistics.currentlyActiveIocs, unpackCurrentlyActiveIOCS) + + return useAsync({ fcn: method, call: false }); +} + +export function unpackActiveIOCHistory(statistics) { + return { ...statistics }; +} + +export function useActiveIOCHistory() { + const api = useContext(apiContext); + const method = callAndUnpack(api.apis.Statistics.activeIocHistory, unpackActiveIOCHistory) + + return useAsync({ fcn: method, call: false }); +} + +export function unpackDeployedIOCStat(statistics) { + return { ...statistics }; +} + +export function useDeployedIOCStatistics() { + const api = useContext(apiContext); + const method = callAndUnpack(api.apis.Statistics.iocDeploymentHistory, unpackDeployedIOCStat) + + return useAsync({ fcn: method, call: false }); } \ No newline at end of file diff --git a/src/components/common/Helper.js b/src/components/common/Helper.js index 02f1ca08753876363bdf5504e4e4abe85b5c91d2..5a80fc01c563f6c4827542c5692964e7bf6931e3 100644 --- a/src/components/common/Helper.js +++ b/src/components/common/Helper.js @@ -31,6 +31,17 @@ export const formatDate = (value) => { return null; } +export const formatDateOnly = (value) => { + if (value) { + return moment(value).format(dateFormat); + } + return null; +} + +export const dateFormat = () => { + return 'DD/MM/YYYY'; +} + function getWindowDimensions() { const { innerWidth: width, innerHeight: height } = window; return { @@ -59,4 +70,45 @@ export function parseHostUrl(url) { const network = url?.split('.', 2)[1]; return {hostName: hostName, network: network?.includes('cslab') ? "CSLab" : (network?.includes('tn') ? "TN" : null)}; +} + +export function colorFromIndex(index) { + const COLORS=[ + 'rgba(66, 165, 245, 1)', + 'rgba(245, 132, 66, 1)', + 'rgba(245, 221, 66, 1)', + 'rgba(194, 66, 245, 1)', + 'rgba(100, 242, 75, 1)', + 'rgba(242, 75, 100, 1)', + 'rgba(75, 242, 192, 1)', + 'rgba(161, 168, 167, 1)', + 'rgba(37, 38, 38, 1)', + 'rgba(242, 162, 172, 1)', + 'rgba(172, 242, 162, 1)', + 'rgba(162, 242, 222, 1)' + ]; + + let n = COLORS.length; + + return COLORS[(index % n + n) % n]; +} + +export function backgroundColorFromIndex(index) { + const COLORS=[ + 'rgba(66, 165, 245, 0.2)', + 'rgba(245, 132, 66, 0.2)', + 'rgba(245, 221, 66, 0.2)', + 'rgba(194, 66, 245, 0.2)', + 'rgba(100, 242, 75, 0.2)', + 'rgba(242, 75, 100, 0.2)', + 'rgba(75, 242, 192, 0.2)', + 'rgba(161, 168, 167, 0.2)', + 'rgba(37, 38, 38, 0.2)', + 'rgba(242, 162, 172, 0.2)', + 'rgba(172, 242, 162, 0.2)', + 'rgba(162, 242, 222, 0.2)']; + + let n = COLORS.length; + + return COLORS[(index % n + n) % n]; } \ No newline at end of file diff --git a/src/components/statistics/ActiveIOCChart.js b/src/components/statistics/ActiveIOCChart.js new file mode 100644 index 0000000000000000000000000000000000000000..334f4dd3747c3063c7d920ec0d1dae8a37225a14 --- /dev/null +++ b/src/components/statistics/ActiveIOCChart.js @@ -0,0 +1,142 @@ +import React, { useState } from 'react'; +import { useCurrentlyActiveIOCs } from '../../api/SwaggerApi'; +import { useEffect, useRef } from 'react'; +import { LinearProgress } from '@material-ui/core'; +import { Bar } from 'react-chartjs-2'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; +import { colorFromIndex } from '../common/Helper'; + +export default function ActiveIOCChart() { + + const [activeIocs, getActiveIocs] = useCurrentlyActiveIOCs(); + const activeIocChartRef = useRef(null); + const [chartData, setChartData] = useState({}); + + useEffect(() => { + getActiveIocs(); + }, [getActiveIocs] + ); + + useEffect(() => { + + const chart = () => { + let barData = []; + let brdColor = []; + + Object.values(activeIocs).forEach((ioc, index) => { + barData.push(ioc); + brdColor.push(colorFromIndex(index)); + + });//Object.foreach + + setChartData({ + datasets: [ + { + parsing: { + xAxisKey: 'network', + yAxisKey: 'iocCount' + }, + data: barData, + backgroundColor: brdColor + } + ] + }); + + } + + if ((activeIocs)) { + chart(); + } + }, [activeIocs] + ); + + let basicOptions = { + aspectRatio: 5 / 3, + maintainAspectRatio: true, + layout: { + padding: { + top: 32, + right: 16, + bottom: 16, + left: 8 + } + }, + plugins: { + datalabels: { + backgroundColor: function (context) { + return context.dataset.backgroundColor; + }, + formatter: function (value, context) { + return value.iocCount; + }, + borderRadius: 3, + color: 'white', + font: { + weight: 'bold' + }, + padding: { + top: 4, + bottom: 4, + left: 4, + right: 4 + }, + align: 'top', + anchor: 'end', + offset: 10, + clip: true + }, + legend: { + display: false, + labels: { + color: '#495057' + }, + position: 'right' + }, + title: { + display: true, + font: { + size: 16 + }, + text: 'Statistics about currently active IOCs according to Prometheus' + }, + tooltip: { + position: 'nearest', + mode: 'point' + } + }, + scales: { + x: { + ticks: { + color: '#495057', + display: true + }, + grid: { + color: '#ebedef' + } + }, + y: { + ticks: { + color: '#495057' + }, + grid: { + color: '#ebedef' + } + } + } + }; + + + return ( + <> + { + activeIocs ? + <div> + <Bar data={chartData} options={basicOptions} ref={activeIocChartRef} plugins={[ChartDataLabels]} /> + </div> + : + <div style={{ width: "100%" }}><LinearProgress color="primary" /></div> + } + </> + ); + +} \ No newline at end of file diff --git a/src/components/statistics/DeploymentLineChart.js b/src/components/statistics/DeploymentLineChart.js new file mode 100644 index 0000000000000000000000000000000000000000..3b8e19cc4044c2e1e534042d03c5a5e34ede7a1c --- /dev/null +++ b/src/components/statistics/DeploymentLineChart.js @@ -0,0 +1,138 @@ +import React from 'react'; +import { useEffect, useRef } from 'react'; +import { backgroundColorFromIndex, dateFormat } from '../common/Helper'; +import { LinearProgress, Button } from '@material-ui/core'; +import zoomPlugin from "chartjs-plugin-zoom"; +import { Line, Chart } from 'react-chartjs-2'; +import 'chartjs-adapter-moment'; +import { colorFromIndex } from '../common/Helper'; + +Chart.register(zoomPlugin); // REGISTER PLUGIN + +export default function DeploymentLineChart({title, chartLabel, hook}) { + + const [iocDeployments, getIOCDeployments] = hook(); + + useEffect(() => { + getIOCDeployments(); + }, [getIOCDeployments] + ); + + let basicOptions = { + aspectRatio: 5 / 3, + layout: { + padding: { + top: 32, + right: 16, + bottom: 16, + left: 8 + } + }, + parsing: { + xAxisKey: 'historyDate', + yAxisKey: 'iocCount' + }, + plugins: { + zoom: { + pan: { + enabled: true, + mode: 'xy', + threshold: 0 + }, + zoom: { + wheel: { + modifierKey: 'ctrl', + enabled: true + }, + pinch: { + enabled: true + }, + enabled: true, + mode: 'xy' + } + }, + legend: { + labels: { + color: '#495057' + } + }, + title: { + display: true, + font: { + size: 16 + }, + text: title + } + }, + scales: { + x: { + type: 'time', + time: { + unit: "day", + displayFormats: { + day: dateFormat + }, + tooltipFormat: dateFormat, + }, + ticks: { + color: '#495057' + }, + grid: { + color: '#ebedef' + } + }, + y: { + ticks: { + color: '#495057' + }, + grid: { + color: '#ebedef' + } + } + } + }; + + const iocChartRef = useRef(null); + + const resetIocZoom = () => { + iocChartRef.current.resetZoom() + } + + useEffect(() => { + if ((iocChartRef.current) && (iocDeployments)) { + + let iocColor = colorFromIndex(0); + let fillColor = backgroundColorFromIndex(0); + iocChartRef.current.data.datasets.push({ + label: chartLabel, + fill: true, + backgroundColor: fillColor, + borderColor: iocColor, + tension: 0, + parsing: { + xAxisKey: 'historyDate', + yAxisKey: 'iocCount' + }, + data: Object.values(iocDeployments) + }); + iocChartRef.current.update(); + } + } + ); + + + return ( + <> + { + iocDeployments ? + <div> + <Button onClick={resetIocZoom} variant="contained">RESET VIEW</Button> + <Line options={basicOptions} ref={iocChartRef} /> + </div> + : + <div style={{ width: "100%" }}><LinearProgress color="primary" /></div> + } + </> + + ); +} diff --git a/src/components/statistics/IocCountStat.js b/src/components/statistics/IocCountStat.js new file mode 100644 index 0000000000000000000000000000000000000000..cc853f256a8f1c8336d5f0b85e51459448382631 --- /dev/null +++ b/src/components/statistics/IocCountStat.js @@ -0,0 +1,93 @@ +import React from 'react'; +import {Bar} from 'react-chartjs-2'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; + +export default function IocCountStat({stat}) { + + const basicData = { + labels: [''], + datasets: [ + { + label: 'Number of active IOCs', + backgroundColor: '#42A5F5', + data: [stat.numberOfActiveIocs] + }, + { + label: 'Number of IOCs created with the Deployment tool', + backgroundColor: '#FFA726', + data: [stat.numberOfCreatedIocs] + } + ] + }; + + let basicOptions = { + aspectRatio: 5 / 3, + layout: { + padding: { + top: 32, + right: 16, + bottom: 16, + left: 8 + } + }, + plugins: { + datalabels: { + backgroundColor: function(context) { + return context.dataset.backgroundColor; + }, + borderRadius: 3, + color: 'white', + font: { + weight: 'bold' + }, + padding: { + top: 4, + bottom: 4, + left: 4, + right: 4 + }, + align: 'top', + anchor: 'end', + offset: 10 + }, + legend: { + labels: { + color: '#495057' + } + }, + title: { + display: true, + font: { + size: 16 + }, + text: 'Statistics about IOCs' + }, + tooltip: { + position: 'nearest', + mode: 'point' + } + }, + scales: { + x: { + ticks: { + color: '#495057' + }, + grid: { + color: '#ebedef' + } + }, + y: { + ticks: { + color: '#495057' + }, + grid: { + color: '#ebedef' + } + } + } + }; + + return ( + <Bar type="bar" data={basicData} options={basicOptions} plugins={[ChartDataLabels]}/> + ); +} \ No newline at end of file diff --git a/src/views/statistics/StatisticsView.js b/src/views/statistics/StatisticsView.js index 34c760cd6f49af5f7bfd54bc0e5ab213f341e615..035e748555e97b0173e97fe9672f37a42009e6e5 100644 --- a/src/views/statistics/StatisticsView.js +++ b/src/views/statistics/StatisticsView.js @@ -1,9 +1,12 @@ import React, { useEffect } from "react"; -import { Paper } from "@material-ui/core"; +import { Paper, Box } from "@material-ui/core"; import { KeyValueTable } from '../../components/common/KeyValueTable/KeyValueTable'; import { useStatistics } from "../../api/SwaggerApi"; import { useGlobalAppBar } from "../../components/navigation/GlobalAppBar/GlobalAppBar"; import { RootContainer } from "../../components/common/Container/RootContainer"; +import ActiveIOCChart from "../../components/statistics/ActiveIOCChart"; +import DeploymentLineChart from "../../components/statistics/DeploymentLineChart"; +import { useActiveIOCHistory, useDeployedIOCStatistics } from '../../api/SwaggerApi'; const clone = (obj) => Object.assign({}, obj); @@ -47,7 +50,25 @@ export function StatisticsView() { <Paper> {statistics && <KeyValueTable obj={renderStat} variant="table" />} </Paper> + + <Box m={2} /> + + <Paper> + <DeploymentLineChart title="Number of IOCs deployed by the Deployment tool" chartLabel="Deployed IOCs in time" hook={useDeployedIOCStatistics}/> + </Paper> + + <Box m={2} /> + + <Paper> + <ActiveIOCChart /> + </Paper> + + <Box m={2} /> + + <Paper> + <DeploymentLineChart title="IOC deployment history graph (Propmetheus)" chartLabel="Active IOCs in time" hook={useActiveIOCHistory}/> + </Paper> + </RootContainer> ) - } \ No newline at end of file