From 2de1f1355a621a282dccd16a13f6e891f9a5280d Mon Sep 17 00:00:00 2001 From: Aditya <60684641+0x0elliot@users.noreply.github.com> Date: Wed, 22 May 2024 16:34:25 +0530 Subject: [PATCH] feat[health-page]: adding health page to onprem --- frontend/src/App.jsx | 34 +++ frontend/src/components/HealthBarChart.jsx | 22 ++ frontend/src/components/HealthPage.jsx | 317 +++++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 frontend/src/components/HealthBarChart.jsx create mode 100644 frontend/src/components/HealthPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index dba2d5db..639b80bd 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -9,6 +9,8 @@ import GettingStarted from "./views/GettingStarted"; import AngularWorkflow from "./views/AngularWorkflow.jsx"; import Header from "./components/NewHeader.jsx"; +import HealthPage from "./components/HealthPage.jsx"; + //import Header from "./components/Header.jsx"; import theme from "./theme"; import Apps from "./views/Apps"; @@ -272,6 +274,38 @@ const App = (message, props) => { /> } /> + + } + /> + + } + /> {userdata.id !== undefined ? ( { + const { globalUrl, filteredData, options, onBarClick } = props; + + return ( + { + if (elements && elements.length > 0) { + onBarClick(elements); + } + }} + /> + ); +}; + +export default HealthBarChart; diff --git a/frontend/src/components/HealthPage.jsx b/frontend/src/components/HealthPage.jsx new file mode 100644 index 00000000..3ad6ad8b --- /dev/null +++ b/frontend/src/components/HealthPage.jsx @@ -0,0 +1,317 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { Bar } from 'react-chartjs-2'; +//import healthData1 from '../healthstats1.json'; +import { toast } from "react-toastify" +import CheckOutlinedIcon from '@mui/icons-material/CheckOutlined'; +import { + Button, + ButtonGroup, + Typography, +} from "@mui/material"; +import HealthBarChart from '../components/HealthBarChart.jsx'; + +const HealthPage = (props) => { + const { userdata } = props; + const [healthData, setHealthData] = useState(null); + const [selectedRange, setSelectedRange] = useState('30d'); + const [filteredData, setFilteredData] = useState([]); + const [averageUptime, setAverageUptime] = useState(0); + + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const globalUrl = `https://shuffler.io` + + console.log("HEALTHPAGE 1") + + const fetchHealthStats = useCallback(async () => { + //const after = new Date().getTime() - 90 * 24 * 60 * 60 + try { + //const response = await fetch(`${globalUrl}/api/v1/health/stats?after=${after}`, { + const response = await fetch(`${globalUrl}/api/v1/health/stats`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }); + if (!response.ok) { + throw new Error("Failed to fetch health stats"); + } + const data = await response.json(); + setHealthData(data); + } catch (error) { + console.error("Error fetching health stats:", error); + toast.error("Failed loading health stats"); + } + }, [globalUrl]); + + useEffect(() => { + fetchHealthStats(); + }, [fetchHealthStats]); + + const extractRunFinished = (data, range) => { + if (!data || !Array.isArray(data)) return []; + + const currentDate = new Date().getTime(); + const rangeInMillis = { + '24hr': 24 * 60 * 60 * 1000, + '7day': 7 * 24 * 60 * 60 * 1000, + '30d': 30 * 24 * 60 * 60 * 1000, + '90d': 90 * 24 * 60 * 60 * 1000 + } + const filteredData = data.filter(item => currentDate - item.updated * 1000 <= rangeInMillis[range]) + + const aggregatedData = new Map() + + filteredData.forEach(item => { + const timestamp = item.updated * 1000; // Convert Unix timestamp to milliseconds + let key; + + switch (range) { + case '24hr': + const date = new Date(timestamp); + const hour = date.getHours(); + const formattedDate = `${date.toLocaleDateString()} ${hour}:00`; + key = formattedDate; + break; + case '7day': + const date1 = new Date(timestamp); + const hour1 = date1.getHours(); + const formattedDate1 = `${date1.toLocaleDateString()} ${hour1}:00`; + key = formattedDate1; + break; + default: + key = new Date(timestamp).toLocaleDateString(); + } + + // Check if date already exists in the map + if (aggregatedData.has(key)) { + // Update aggregated values + const existingData = aggregatedData.get(key); + existingData.totalEntries++; + existingData.totalRunFinished += item.workflows.run_finished ? 1 : 0; + existingData.executionIds.push(item.workflows.execution_id); + } else { + // Add new entry to the map + aggregatedData.set(key, { + totalEntries: 1, + totalRunFinished: item.workflows.run_finished ? 1 : 0, + executionIds: [item.workflows.execution_id] + }); + } + }); + + // Calculate averages and assign colors + const result = Array.from(aggregatedData.entries()).map(([key, { totalEntries, totalRunFinished, executionIds }]) => { + const avg = totalEntries > 0 ? totalRunFinished / totalEntries : 0; + const FinalAvg = avg * 100; + let color; + + if (FinalAvg >= 100) { + color = '#00F670'; + } else if (FinalAvg >= 98.50 && FinalAvg <= 99.99) { + color = '#FFD700'; + } else if (FinalAvg <= 98.49) { + color = '#FF354C'; + } + + return { + date: range === '24hr' ? `${key}:00` : key, + avgRunFinished: FinalAvg, + color, + executionIds + }; + }); + + return result; + }; + + + useEffect(() => { + if (healthData) { + const newData = extractRunFinished(healthData, selectedRange); + setFilteredData(newData); + + const totalUptime = newData.reduce((acc, curr) => acc + curr.avgRunFinished, 0); + const avgUptime = totalUptime / newData.length; + setAverageUptime(avgUptime); + } + }, [selectedRange, healthData]); + + const filterDataByRange = (range) => { + setSelectedRange(range); + }; + + const updateChartData = () => { + if (!filteredData) { + return { + labels: [], + datasets: [{ + label: "", + data: [], + backgroundColor: [], + borderWidth: 1, + barThickness: 7, // Default bar thickness + }], + }; + } + let barThickness = 7; + + const labels = filteredData.map((value, i) => { + if (selectedRange === '24hr') { + const [datePart, hourPart] = value.date.split(' '); + const [day, month, year] = datePart.split('/'); + const monthIndex = parseInt(month, 10) - 1; + const date = new Date(year, monthIndex, day); + let formattedDate = `${date.toLocaleString('en-US', { month: 'short', day: '2-digit' })}`; + + // Add hour part if available + if (hourPart) { + formattedDate += `, ${hourPart.split(':').slice(0, 2).join(':')}`; + } + return `${formattedDate} \nUptime: ${value.avgRunFinished.toFixed(2)}%`; + } else if (selectedRange === '7day') { + const [datePart, hourPart] = value.date.split(' '); + const [day, month, year] = datePart.split('/'); + const monthIndex = parseInt(month, 10) - 1; + const date = new Date(year, monthIndex, day); + let formattedDate = `${date.toLocaleString('en-US', { month: 'short', day: '2-digit' })}`; + + // Add hour part if available + if (hourPart) { + formattedDate += `, ${hourPart.split(':').slice(0, 2).join(':')}`; + } + return `${formattedDate} \nUptime: ${value.avgRunFinished.toFixed(2)}%`; + } + else { + const dateParts = value.date.split('/'); // Assuming the date format is "DD/MM/YYYY" + const date = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`); // Reformat the date string to "YYYY-MM-DD" + return `${date.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })} \nUptime: ${value.avgRunFinished.toFixed(2)}%`; + } + }); + + + if (selectedRange === '24hr') { + barThickness = 35; + } + else if (selectedRange === '7day') { + barThickness = 5; + } + else if (selectedRange === '30d') { + barThickness = 25; + } + // let width = 800; // Default chart width + // if (selectedRange === '7day') { + // width = 400; // Adjust chart width for 7 days + // } else if (selectedRange === '30d') { + // width = 600; // Adjust chart width for 30 days + // } + + const datasets = [{ + label: "", + data: filteredData.map(item => 1), + backgroundColor: filteredData.map(item => item.color), + borderWidth: 1, + barThickness: barThickness, + // barPercentage: barPercentage, + }]; + + return { labels, datasets }; + }; + + console.log("HEALTHPAGE 2") + + const options = { + legend: { + display: false + }, + layout: { + padding: { + top: 0, // Adjust the top padding as needed + bottom: 20, // Adjust the bottom padding as needed + left: 20, // Adjust the left padding as needed + right: 20 // Adjust the right padding as needed + } + }, + scales: { + yAxes: [{ + ticks: { + display: false + } + }], + xAxes: [{ + ticks: { + display: false + } + }] + }, + tooltips: { + callbacks: { + label: function (tooltipItem, data) { + const label = data.labels[tooltipItem.index]; + return label.split('\n')[0]; // Return only the date part + }, + afterLabel: function (tooltipItem, data) { + const label = data.labels[tooltipItem.index]; + // console.log(label) + const uptime = label.match(/Uptime:\s*(\d+(?:\.\d+)?)/)[1]; // Extract uptime value using regex + return `Success Rate: ${uptime}%`; // Customize the uptime display + }, + title: function () { + return 'Fully Oprational'; // Hide the tooltip title + } + } + } + }; + + const handleBarClick = (event, elements) => { + if (event && event.length > 0) { + const clickedIndex = event[0]._index + const clickedData = filteredData[clickedIndex] + const executionIds = clickedData.executionIds + .filter(executionId => { + const item = healthData.find(dataItem => dataItem.workflows.execution_id === executionId); + return item && item.workflows.run_finished === false; + }); + + // console.log("Filtered Execution IDs:", executionIds); + if (executionIds.length > 0) { + const url = `${globalUrl}/api/v1/health/stats?execution_id=${executionIds.join(',')}`; + window.open(url, '_blank'); + } else { + toast.success("All executions in selected period succeeded"); + } + } + }; + + + return ( +
+ + + + + + + + + +
+
+ +
+ Workflow Health + Operational +
+
+ {averageUptime.toFixed(2)}% + Success Rate +
+
+ +
+
+ ); +}; + +export default HealthPage;