diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index aa007e28..7c857d01 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.3 //replace github.com/frikky/schemaless => ../../../schemaless -replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared +//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi @@ -24,7 +24,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.99 + github.com/shuffle/shuffle-shared v0.9.0 github.com/shuffle/singul v0.0.15 golang.org/x/crypto v0.38.0 google.golang.org/api v0.236.0 diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 51d36d6e..163f7931 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -359,8 +359,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shuffle/shuffle-shared v0.8.91 h1:ZcxSKhns96w1GQG/EjEJSLBvuEA3KVjvORAetV3W4bM= -github.com/shuffle/shuffle-shared v0.8.91/go.mod h1:PFLG4eaxz8zQc9Lbc0Ok/nG8rNsaBkZ0TNar+VU+J/g= +github.com/shuffle/shuffle-shared v0.9.0 h1:hUNtW3qHkji2cUVyR1cIXAz6qxpAHHP5hBmSAe3pIhs= +github.com/shuffle/shuffle-shared v0.9.0/go.mod h1:PFLG4eaxz8zQc9Lbc0Ok/nG8rNsaBkZ0TNar+VU+J/g= github.com/shuffle/singul v0.0.15 h1:a5Qro1BKs9a+mWE289oYTkDeVFoKEd/H9qu9XJGsVxo= github.com/shuffle/singul v0.0.15/go.mod h1:z1RRZsymTYxsB8WMQwwswypruy4j5EugGPFEZspTEA4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/frontend/src/components/HealthBarChart.jsx b/frontend/src/components/HealthBarChart.jsx index a4f05273..a5d1dec4 100644 --- a/frontend/src/components/HealthBarChart.jsx +++ b/frontend/src/components/HealthBarChart.jsx @@ -1,22 +1,93 @@ -import React from 'react'; -import { Bar } from 'react-chartjs-2'; +import React, { useState } from 'react'; const HealthBarChart = (props) => { - const { globalUrl, filteredData, options, onBarClick } = props; - - return ( - { - if (elements && elements.length > 0) { - onBarClick(elements); - } + const { filteredData, onBarClick } = props; + const [hoveredBar, setHoveredBar] = useState(null); + + if (!filteredData || !Array.isArray(filteredData)) { + console.error('Invalid chart data format'); + return null; + } + + const formatData = () => { + return filteredData.map((item) => ({ + label: item.date, + value: item.avgRunFinished, + color: item.color, + executionIds: item.executionIds, + })); + }; + + const handleBarClick = (data) => { + if (onBarClick) { + onBarClick(data); + } + }; + + const chartData = formatData(); + + const barWidth = `calc((100% - ${(chartData.length - 1) * 5}px) / ${chartData.length})`; + const barGap = '5px'; + + return ( +
+ {/* Chart Container */} +
+ {chartData.map((item, index) => ( +
- ); + onMouseEnter={() => setHoveredBar(item)} + onMouseLeave={() => setHoveredBar(null)} + onClick={() => handleBarClick(item)} + /> + ))} +
+ + {/* Tooltip */} + {hoveredBar && ( +
+
Date: {hoveredBar.label}
+
Uptime: {hoveredBar.value}%
+
Executions: {hoveredBar.executionIds.length}
+
+ )} +
+ ); }; export default HealthBarChart; diff --git a/frontend/src/components/HealthPage.jsx b/frontend/src/components/HealthPage.jsx index fcadb776..660719e0 100644 --- a/frontend/src/components/HealthPage.jsx +++ b/frontend/src/components/HealthPage.jsx @@ -1,31 +1,35 @@ 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 { toast } from "react-toastify"; +import { + CheckOutlined as CheckOutlinedIcon, +} from '@mui/icons-material'; + import { Button, ButtonGroup, Typography, + LinearProgress, } from "@mui/material"; + import HealthBarChart from '../components/HealthBarChart.jsx'; +import LiveExecutionsChart from '../components/LiveExecutionsGraph.jsx'; const HealthPage = (props) => { - const { globalUrl, userdata } = props; + const { userdata, globalUrl } = props; const [healthData, setHealthData] = useState(null); const [selectedRange, setSelectedRange] = useState('30d'); + const [liveExecutionsData, setLiveExecutionsData] = useState([]); const [filteredData, setFilteredData] = useState([]); const [averageUptime, setAverageUptime] = useState(0); + const [liveExecutionsRange, setLiveExecutionsRange] = useState('1h'); // Default to 1h + const [isHealthLoading, setIsHealthLoading] = useState(false); // Loading state for HealthBarChart + const [isLiveExecutionsLoading, setIsLiveExecutionsLoading] = useState(false); // Loading state for LiveExecutionsChart - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; - //const globalUrl = `https://shuffler.io` - - console.log("HEALTHPAGE 1") + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); const fetchHealthStats = useCallback(async () => { - //const after = new Date().getTime() - 90 * 24 * 60 * 60 + setIsHealthLoading(true); // Start loading for HealthBarChart try { - //const response = await fetch(`${globalUrl}/api/v1/health/stats?after=${after}`, { const response = await fetch(`${globalUrl}/api/v1/health/stats`, { method: "GET", headers: { @@ -42,31 +46,118 @@ const HealthPage = (props) => { } catch (error) { console.error("Error fetching health stats:", error); toast.error("Failed loading health stats"); + } finally { + setIsHealthLoading(false); // Stop loading for HealthBarChart + } + }, [globalUrl]); + + const fetchLiveExecutions = useCallback(async (range = '1h') => { + setIsLiveExecutionsLoading(true); // Start loading for LiveExecutionsChart + try { + const fetchOptions = { + method: "GET", + credentials: "include", + }; + + if (window.location.host !== "localhost:3002") { + fetchOptions.headers = { + "Content-Type": "application/json", + Accept: "application/json", + }; + } + + const now = Math.floor(Date.now() / 1000); + // let after = now - 3600; // Default to 1h + let mode = "" + + switch (range) { + case '1h': + mode = "1h" + break; + case '7h': + mode = "7h" + break; + case '1d': + mode = "1d" + break; + case '7d': + mode = "7d" + break; + case 'month': + mode = "month" + break; + default: + mode = "1h" + } + + if (!userdata.support_access) { + return + } + + const response = await fetch( + `${globalUrl}/api/v1/health/executions/live?mode=${mode}`, + fetchOptions + ); + + if (!response.ok) { + throw new Error("Failed to fetch live executions"); + } + + const data = await response.json(); + console.log("Raw live executions data:", data); + + if (Array.isArray(data)) { + const formattedData = data + .map(item => ({ + ...item, + // failed: Number(item.failed) || 0, + executing: Number(item.executing) || 0, + finished: Number(item.finished) || 0, + aborted: Number(item.aborted) || 0, + created_at: Number(item.created_at) || 0 + })) + .sort((a, b) => a.created_at - b.created_at); + + console.log("Formatted live executions data:", formattedData); + setLiveExecutionsData(formattedData); + } else { + console.error("Received invalid data format:", data); + setLiveExecutionsData([]); + } + } catch (error) { + console.error("Error fetching live executions:", error); + toast.error("Failed loading live executions data"); + } finally { + setIsLiveExecutionsLoading(false); // Stop loading for LiveExecutionsChart } }, [globalUrl]); useEffect(() => { fetchHealthStats(); - }, [fetchHealthStats]); + fetchLiveExecutions(liveExecutionsRange); + + const interval = setInterval(() => fetchLiveExecutions(liveExecutionsRange), 60000); + return () => clearInterval(interval); + }, [fetchHealthStats, fetchLiveExecutions, liveExecutionsRange]); 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() - + }; + 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); @@ -83,7 +174,7 @@ const HealthPage = (props) => { default: key = new Date(timestamp).toLocaleDateString(); } - + // Check if date already exists in the map if (aggregatedData.has(key)) { // Update aggregated values @@ -100,13 +191,13 @@ const HealthPage = (props) => { }); } }); - + // 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) { @@ -114,7 +205,7 @@ const HealthPage = (props) => { } else if (FinalAvg <= 98.49) { color = '#FF354C'; } - + return { date: range === '24hr' ? `${key}:00` : key, avgRunFinished: FinalAvg, @@ -122,11 +213,10 @@ const HealthPage = (props) => { executionIds }; }); - + return result; }; - - + useEffect(() => { if (healthData) { const newData = extractRunFinished(healthData, selectedRange); @@ -160,19 +250,19 @@ const HealthPage = (props) => { const labels = filteredData.map((value, i) => { if (selectedRange === '24hr') { const [datePart, hourPart] = value.date.split(' '); - const [day, month, year] = datePart.split('/'); + const [month, day, 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 [month, day, 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' })}`; @@ -185,12 +275,12 @@ const HealthPage = (props) => { } 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" + const date = new Date(`${dateParts[2]}-${dateParts[0]}-${dateParts[1]}`); // 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; } @@ -200,12 +290,6 @@ const HealthPage = (props) => { 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: "", @@ -213,14 +297,11 @@ const HealthPage = (props) => { backgroundColor: filteredData.map(item => item.color), borderWidth: 1, barThickness: barThickness, - // barPercentage: barPercentage, }]; return { labels, datasets }; }; - console.log("HEALTHPAGE 2") - const options = { legend: { display: false @@ -253,12 +334,11 @@ const HealthPage = (props) => { }, 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 + return `Test-Workflow Health: ${uptime}%`; // Customize the uptime display }, title: function () { - return 'Fully Oprational'; // Hide the tooltip title + return 'Fully Operational'; // Hide the tooltip title } } } @@ -284,32 +364,118 @@ const HealthPage = (props) => { } }; + const healthBarData = updateChartData() + return ( -
- +
+ {/* Health Bar Chart Section */} - - - - - + + + + + -
-
- -
- Workflow Health - Operational + {/* Loading Bar for HealthBarChart */} + {isHealthLoading && ( + + )} + +
+
+
+ +
+ Workflow Health + Operational +
-
- {averageUptime.toFixed(2)}% - Success Rate +
+ {averageUptime.toFixed(2)}% + Success Rate
- +
+ + {userdata.support_access && ( +
+
+ Live Executions + + + + + + {/* */} + +
+ {/* Loading Bar for LiveExecutionsChart */} + {isLiveExecutionsLoading && ( + + )} + +
+ )} + +
); };