mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-24 19:01:01 -06:00
Merge branch 'develop' into feat/be/hardware-details
This commit is contained in:
@@ -17,9 +17,11 @@ import "./index.css";
|
||||
|
||||
const CustomToolTip = ({ active, payload, label }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const theme = useTheme();
|
||||
if (active && payload && payload.length) {
|
||||
const responseTime = payload[0]?.payload?.originalAvgResponseTime
|
||||
? payload[0]?.payload?.originalAvgResponseTime
|
||||
: (payload[0]?.payload?.avgResponseTime ?? 0);
|
||||
return (
|
||||
<Box
|
||||
className="area-tooltip"
|
||||
@@ -69,7 +71,7 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
Response Time
|
||||
</Typography>{" "}
|
||||
<Typography component="span">
|
||||
{payload[0].payload.originalResponseTime}
|
||||
{Math.floor(responseTime)}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
@@ -87,11 +89,24 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.number,
|
||||
payload: PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
avgResponseTime: PropTypes.number,
|
||||
originalAvgResponseTime: PropTypes.number,
|
||||
}),
|
||||
})
|
||||
),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
const CustomTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
// Render nothing for the first tick
|
||||
if (index === 0) return null;
|
||||
return (
|
||||
@@ -168,7 +183,7 @@ const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
</defs>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
dataKey="createdAt"
|
||||
dataKey="_id"
|
||||
tick={<CustomTick />}
|
||||
minTickGap={0}
|
||||
axisLine={false}
|
||||
@@ -183,7 +198,7 @@ const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="responseTime"
|
||||
dataKey="avgResponseTime"
|
||||
stroke={theme.palette.primary.main}
|
||||
fill="url(#colorUv)"
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
@@ -198,15 +213,4 @@ MonitorDetailsAreaChart.propTypes = {
|
||||
checks: PropTypes.array,
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
payload: PropTypes.shape({
|
||||
originalResponseTime: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
})
|
||||
),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
export default MonitorDetailsAreaChart;
|
||||
|
||||
42
Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx
Normal file
42
Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
|
||||
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
|
||||
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
x={x}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
<text
|
||||
x={width}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CustomLabels.propTypes = {
|
||||
x: PropTypes.number.isRequired,
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
firstDataPoint: PropTypes.object.isRequired,
|
||||
lastDataPoint: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CustomLabels;
|
||||
89
Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
Normal file
89
Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { memo, useState } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
|
||||
import PropTypes from "prop-types";
|
||||
import CustomLabels from "./CustomLabels";
|
||||
|
||||
const DownBarChart = memo(({ stats, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={250}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={stats.downChecks}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={stats.downChecks?.[0] ?? {}}
|
||||
lastDataPoint={stats.downChecks?.[stats.downChecks.length - 1] ?? {}}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="avgResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{stats.downChecks.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette.error.main
|
||||
: chartHovered
|
||||
? theme.palette.error.light
|
||||
: theme.palette.error.main
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({ time: null, totalChecks: 0 });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
DownBarChart.displayName = "DownBarChart";
|
||||
DownBarChart.propTypes = {
|
||||
stats: PropTypes.shape({
|
||||
downChecks: PropTypes.arrayOf(PropTypes.object),
|
||||
downChecksAggregate: PropTypes.object,
|
||||
}),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export default DownBarChart;
|
||||
117
Client/src/Pages/Uptime/Details/Charts/ResponseGaugeChart.jsx
Normal file
117
Client/src/Pages/Uptime/Details/Charts/ResponseGaugeChart.jsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts";
|
||||
|
||||
const ResponseGaugeChart = ({ avgResponseTime }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let max = 1000; // max ms
|
||||
|
||||
const data = [
|
||||
{ response: max, fill: "transparent", background: false },
|
||||
{ response: avgResponseTime, background: true },
|
||||
];
|
||||
let responseTime = Math.floor(avgResponseTime);
|
||||
let responseProps =
|
||||
responseTime <= 200
|
||||
? {
|
||||
category: "Excellent",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.contrastText,
|
||||
}
|
||||
: responseTime <= 500
|
||||
? {
|
||||
category: "Fair",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.contrastText,
|
||||
}
|
||||
: responseTime <= 600
|
||||
? {
|
||||
category: "Acceptable",
|
||||
main: theme.palette.warning.main,
|
||||
bg: theme.palette.warning.dark,
|
||||
}
|
||||
: {
|
||||
category: "Poor",
|
||||
main: theme.palette.error.main,
|
||||
bg: theme.palette.error.light,
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<RadialBarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
cy="89%"
|
||||
data={data}
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={100}
|
||||
outerRadius={150}
|
||||
>
|
||||
<text
|
||||
x={0}
|
||||
y="100%"
|
||||
dx="5%"
|
||||
dy={-2}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
low
|
||||
</text>
|
||||
<text
|
||||
x="100%"
|
||||
y="100%"
|
||||
dx="-3%"
|
||||
dy={-2}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
high
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="45%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize={18}
|
||||
fontWeight={400}
|
||||
>
|
||||
{responseProps.category}
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="55%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="hanging"
|
||||
fontSize={25}
|
||||
>
|
||||
<tspan fontWeight={600}>{responseTime}</tspan> <tspan opacity={0.8}>ms</tspan>
|
||||
</text>
|
||||
<RadialBar
|
||||
background={{ fill: responseProps.bg }}
|
||||
clockWise
|
||||
dataKey="response"
|
||||
stroke="none"
|
||||
>
|
||||
<Cell
|
||||
fill="transparent"
|
||||
background={false}
|
||||
barSize={0}
|
||||
/>
|
||||
<Cell fill={responseProps.main} />
|
||||
</RadialBar>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseGaugeChart.propTypes = {
|
||||
avgResponseTime: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default ResponseGaugeChart;
|
||||
100
Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
Normal file
100
Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { memo, useState } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
|
||||
import PropTypes from "prop-types";
|
||||
import CustomLabels from "./CustomLabels";
|
||||
|
||||
const UpBarChart = memo(({ stats, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
const getColorRange = (responseTime) => {
|
||||
return responseTime < 200
|
||||
? { main: theme.palette.success.main, light: theme.palette.success.light }
|
||||
: responseTime < 300
|
||||
? { main: theme.palette.warning.main, light: theme.palette.warning.light }
|
||||
: { main: theme.palette.error.main, light: theme.palette.error.light };
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={stats.upChecks}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={stats.upChecks[0]}
|
||||
lastDataPoint={stats.upChecks[stats.upChecks.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="avgResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{stats.upChecks.map((entry, index) => {
|
||||
let { main, light } = getColorRange(entry.avgResponseTime);
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={hoveredBarIndex === index ? main : chartHovered ? light : main}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({
|
||||
time: null,
|
||||
totalChecks: 0,
|
||||
groupUptimePercentage: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
// Add display name for the component
|
||||
UpBarChart.displayName = "UpBarChart";
|
||||
|
||||
// Validate props using PropTypes
|
||||
UpBarChart.propTypes = {
|
||||
stats: PropTypes.shape({
|
||||
upChecks: PropTypes.array,
|
||||
upChecksAggregate: PropTypes.object,
|
||||
}),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export default UpBarChart;
|
||||
@@ -1,346 +0,0 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
RadialBarChart,
|
||||
RadialBar,
|
||||
} from "recharts";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
|
||||
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
|
||||
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
x={x}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(new Date(firstDataPoint.time), dateFormat, uiTimezone)}
|
||||
</text>
|
||||
<text
|
||||
x={width}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(new Date(lastDataPoint.time), dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CustomLabels.propTypes = {
|
||||
x: PropTypes.number.isRequired,
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
firstDataPoint: PropTypes.object.isRequired,
|
||||
lastDataPoint: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const UpBarChart = memo(({ data, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
const getColorRange = (uptime) => {
|
||||
return uptime > 80
|
||||
? { main: theme.palette.success.main, light: theme.palette.success.light }
|
||||
: uptime > 50
|
||||
? { main: theme.palette.warning.main, light: theme.palette.warning.light }
|
||||
: { main: theme.palette.error.contrastText, light: theme.palette.error.light };
|
||||
};
|
||||
|
||||
// TODO - REMOVE THIS LATER
|
||||
const reversedData = useMemo(() => [...data].reverse(), [data]);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={reversedData}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0, uptimePercentage: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={reversedData[0]}
|
||||
lastDataPoint={reversedData[reversedData.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="totalChecks"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{reversedData.map((entry, index) => {
|
||||
let { main, light } = getColorRange(entry.uptimePercentage);
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={hoveredBarIndex === index ? main : chartHovered ? light : main}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({
|
||||
time: null,
|
||||
totalChecks: 0,
|
||||
uptimePercentage: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
// Add display name for the component
|
||||
UpBarChart.displayName = "UpBarChart";
|
||||
|
||||
// Validate props using PropTypes
|
||||
UpBarChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export { UpBarChart };
|
||||
|
||||
const DownBarChart = memo(({ data, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
// TODO - REMOVE THIS LATER
|
||||
const reversedData = useMemo(() => [...data].reverse(), [data]);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={250}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={reversedData}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalIncidents: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={reversedData[0]}
|
||||
lastDataPoint={reversedData[reversedData.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="totalIncidents"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{reversedData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette.error.contrastText
|
||||
: chartHovered
|
||||
? theme.palette.error.light
|
||||
: theme.palette.error.contrastText
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({ time: null, totalIncidents: 0 });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
DownBarChart.displayName = "DownBarChart";
|
||||
DownBarChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export { DownBarChart };
|
||||
|
||||
const ResponseGaugeChart = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let max = 1000; // max ms
|
||||
|
||||
const memoizedData = useMemo(
|
||||
() => [{ response: max, fill: "transparent", background: false }, ...data],
|
||||
[data[0].response]
|
||||
);
|
||||
|
||||
let responseTime = Math.floor(memoizedData[1].response);
|
||||
let responseProps =
|
||||
responseTime <= 200
|
||||
? {
|
||||
category: "Excellent",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.contrastText,
|
||||
}
|
||||
: responseTime <= 500
|
||||
? {
|
||||
category: "Fair",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.contrastText,
|
||||
}
|
||||
: responseTime <= 600
|
||||
? {
|
||||
category: "Acceptable",
|
||||
main: theme.palette.warning.main,
|
||||
bg: theme.palette.warning.dark,
|
||||
}
|
||||
: {
|
||||
category: "Poor",
|
||||
main: theme.palette.error.light,
|
||||
bg: theme.palette.error.dark,
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<RadialBarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
cy="89%"
|
||||
data={memoizedData}
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={100}
|
||||
outerRadius={150}
|
||||
>
|
||||
<text
|
||||
x={0}
|
||||
y="100%"
|
||||
dx="5%"
|
||||
dy={-2}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
low
|
||||
</text>
|
||||
<text
|
||||
x="100%"
|
||||
y="100%"
|
||||
dx="-3%"
|
||||
dy={-2}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
high
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="45%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize={18}
|
||||
fontWeight={400}
|
||||
>
|
||||
{responseProps.category}
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="55%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="hanging"
|
||||
fontSize={25}
|
||||
>
|
||||
<tspan fontWeight={600}>{responseTime}</tspan> <tspan opacity={0.8}>ms</tspan>
|
||||
</text>
|
||||
<RadialBar
|
||||
background={{ fill: responseProps.bg }}
|
||||
clockWise
|
||||
dataKey="response"
|
||||
stroke="none"
|
||||
>
|
||||
<Cell
|
||||
fill="transparent"
|
||||
background={false}
|
||||
barSize={0}
|
||||
/>
|
||||
<Cell fill={responseProps.main} />
|
||||
</RadialBar>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseGaugeChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export { ResponseGaugeChart };
|
||||
@@ -5,7 +5,6 @@ import { useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { networkService } from "../../../main";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
|
||||
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
|
||||
@@ -18,14 +17,16 @@ import PaginationTable from "./PaginationTable";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import { ChartBox } from "./styled";
|
||||
import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
import useUtils from "../utils";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import IconBox from "../../../Components/IconBox";
|
||||
import StatBox from "../../../Components/StatBox";
|
||||
import UpBarChart from "./Charts/UpBarChart";
|
||||
import DownBarChart from "./Charts/DownBarChart";
|
||||
import ResponseGaugeChart from "./Charts/ResponseGaugeChart";
|
||||
/**
|
||||
* Details page component displaying monitor details and related information.
|
||||
* @component
|
||||
@@ -47,16 +48,12 @@ const DetailsPage = () => {
|
||||
|
||||
const fetchMonitor = useCallback(async () => {
|
||||
try {
|
||||
const res = await networkService.getStatsByMonitorId({
|
||||
const res = await networkService.getUptimeDetailsById({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
sortOrder: null,
|
||||
limit: null,
|
||||
dateRange: dateRange,
|
||||
numToDisplay: 50,
|
||||
normalize: true,
|
||||
});
|
||||
console.log(res?.data?.data);
|
||||
setMonitor(res?.data?.data ?? {});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
@@ -185,7 +182,7 @@ const DetailsPage = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
{/* Checking every {formatDurationRounded(monitor?.interval)}. */}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -224,17 +221,17 @@ const DetailsPage = () => {
|
||||
<StatBox
|
||||
sx={statusStyles[determineState(monitor)]}
|
||||
heading={"active for"}
|
||||
subHeading={splitDuration(monitor?.uptimeDuration)}
|
||||
subHeading={splitDuration(monitor?.stats?.timeSinceLastFalseCheck)}
|
||||
/>
|
||||
<StatBox
|
||||
heading="last check"
|
||||
subHeading={splitDuration(monitor?.lastChecked)}
|
||||
subHeading={splitDuration(monitor?.stats?.timeSinceLastCheck)}
|
||||
/>
|
||||
<StatBox
|
||||
heading="last response time"
|
||||
subHeading={
|
||||
<>
|
||||
{monitor?.latestResponseTime}
|
||||
{monitor?.stats?.latestResponseTime}
|
||||
<Typography component="span">{"ms"}</Typography>
|
||||
</>
|
||||
}
|
||||
@@ -311,7 +308,7 @@ const DetailsPage = () => {
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? hoveredUptimeData.totalChecks
|
||||
: monitor?.periodTotalChecks}
|
||||
: (monitor.stats?.upChecksAggregate?.totalChecks ?? 0)}
|
||||
</Typography>
|
||||
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
|
||||
<Typography
|
||||
@@ -322,7 +319,7 @@ const DetailsPage = () => {
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
{formatDateWithTz(
|
||||
hoveredUptimeData.time,
|
||||
hoveredUptimeData._id,
|
||||
dateFormat,
|
||||
uiTimezone
|
||||
)}
|
||||
@@ -330,17 +327,27 @@ const DetailsPage = () => {
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>Uptime Percentage</Typography>
|
||||
<Typography>
|
||||
{hoveredUptimeData !== null
|
||||
? "Avg Response Time"
|
||||
: "Uptime Percentage"}
|
||||
</Typography>
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? Math.floor(hoveredUptimeData.uptimePercentage * 10) / 10
|
||||
: Math.floor(monitor?.periodUptime * 10) / 10}
|
||||
<Typography component="span">%</Typography>
|
||||
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
|
||||
: Math.floor(
|
||||
((monitor?.stats?.upChecksAggregate?.totalChecks ?? 0) /
|
||||
(monitor?.stats?.totalChecks ?? 1)) *
|
||||
100
|
||||
)}
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null ? " ms" : " %"}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<UpBarChart
|
||||
data={monitor?.aggregateData}
|
||||
stats={monitor?.stats}
|
||||
type={dateRange}
|
||||
onBarHover={setHoveredUptimeData}
|
||||
/>
|
||||
@@ -356,8 +363,8 @@ const DetailsPage = () => {
|
||||
<Typography>Total Incidents</Typography>
|
||||
<Typography component="span">
|
||||
{hoveredIncidentsData !== null
|
||||
? hoveredIncidentsData.totalIncidents
|
||||
: monitor?.periodIncidents}
|
||||
? hoveredIncidentsData.totalChecks
|
||||
: (monitor.stats?.downChecksAggregate?.totalChecks ?? 0)}
|
||||
</Typography>
|
||||
{hoveredIncidentsData !== null &&
|
||||
hoveredIncidentsData.time !== null && (
|
||||
@@ -369,7 +376,7 @@ const DetailsPage = () => {
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
{formatDateWithTz(
|
||||
hoveredIncidentsData.time,
|
||||
hoveredIncidentsData._id,
|
||||
dateFormat,
|
||||
uiTimezone
|
||||
)}
|
||||
@@ -377,7 +384,7 @@ const DetailsPage = () => {
|
||||
)}
|
||||
</Box>
|
||||
<DownBarChart
|
||||
data={monitor?.aggregateData}
|
||||
stats={monitor?.stats}
|
||||
type={dateRange}
|
||||
onBarHover={setHoveredIncidentsData}
|
||||
/>
|
||||
@@ -390,7 +397,7 @@ const DetailsPage = () => {
|
||||
<Typography component="h2">Average Response Time</Typography>
|
||||
</Stack>
|
||||
<ResponseGaugeChart
|
||||
data={[{ response: monitor?.periodAvgResponseTime }]}
|
||||
avgResponseTime={monitor?.stats?.groupAggregate?.avgResponseTime ?? 0}
|
||||
/>
|
||||
</ChartBox>
|
||||
<ChartBox sx={{ padding: 0 }}>
|
||||
@@ -403,7 +410,7 @@ const DetailsPage = () => {
|
||||
</IconBox>
|
||||
<Typography component="h2">Response Times</Typography>
|
||||
</Stack>
|
||||
<MonitorDetailsAreaChart checks={[...monitor.checks].reverse()} />
|
||||
<MonitorDetailsAreaChart checks={monitor?.stats?.groupChecks ?? []} />
|
||||
</ChartBox>
|
||||
<ChartBox
|
||||
gap={theme.spacing(8)}
|
||||
|
||||
@@ -248,6 +248,13 @@ class NetworkService {
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/hardware/details/${config.monitorId}?${params.toString()}`,
|
||||
async getUptimeDetailsById(config) {
|
||||
const params = new URLSearchParams();
|
||||
if (config.dateRange) params.append("dateRange", config.dateRange);
|
||||
if (config.normalize) params.append("normalize", config.normalize);
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/uptime/details/${config.monitorId}?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.authToken}`,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
export const MS_PER_SECOND = 1000;
|
||||
export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
||||
@@ -90,6 +93,6 @@ export const formatDate = (date, customOptions) => {
|
||||
};
|
||||
|
||||
export const formatDateWithTz = (timestamp, format, timezone) => {
|
||||
const formattedDate = dayjs(timestamp, timezone).tz(timezone).format(format);
|
||||
const formattedDate = dayjs(timestamp).tz(timezone).format(format);
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
20
README.md
20
README.md
@@ -1,10 +1,9 @@
|
||||
**Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.**
|
||||
|
||||
**Checkmate is on [GitHub trending](https://github.com/trending) and #1 trending for JavaScript apps on Fri Dec 13th & 15th**
|
||||
# **We're opening our $5000 grant funding announcement soon, powered by Checkmate and [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for preliminary details.**
|
||||
|
||||
**If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback.**
|
||||
**If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.**
|
||||
|
||||

|
||||
<img width="1259" alt="Frame 34" src="https://github.com/user-attachments/assets/d491a734-fd7a-4841-9f84-fb5bef5ad586" />
|
||||
|
||||
|
||||

|
||||
@@ -53,20 +52,21 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
|
||||
- Docker monitoring
|
||||
- Ping monitoring
|
||||
- SSL monitoring
|
||||
- Incidents at a glance
|
||||
- E-mail notifications
|
||||
- Scheduled maintenance
|
||||
- Can monitor 1000+ servers at the same time on a moderate server
|
||||
|
||||
**Short term roadmap:**
|
||||
|
||||
- Global (distributed) uptime checking on Solana network
|
||||
- Status pages
|
||||
- Better notification options (think Discord, Telegram, Slack)
|
||||
- Port monitoring (**complete**, waiting to be deployed to stable version)
|
||||
- Global (distributed) uptime checking on Solana network (**in progress**)
|
||||
- Status pages (**in progress**)
|
||||
- Better notification options (Webhooks, Discord, Telegram, Slack)
|
||||
- More configuration options
|
||||
- Translations
|
||||
- Tagging/grouping monitors
|
||||
- DNS monitoring
|
||||
- SSL monitoring
|
||||
- Port monitoring
|
||||
|
||||
## 🏗️ Screenshots
|
||||
|
||||
|
||||
@@ -76,6 +76,19 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
getUptimeDetailsById = async (req, res, next) => {
|
||||
try {
|
||||
const monitor = await this.db.getUptimeDetailsById(req);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_GET_BY_ID,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorDetailsById"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns monitor stats for monitor with matching ID
|
||||
* @async
|
||||
|
||||
@@ -4,7 +4,7 @@ import PageSpeedCheck from "../../models/PageSpeedCheck.js";
|
||||
import HardwareCheck from "../../models/HardwareCheck.js";
|
||||
import { errorMessages } from "../../../utils/messages.js";
|
||||
import Notification from "../../models/Notification.js";
|
||||
import { NormalizeData } from "../../../utils/dataUtils.js";
|
||||
import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
@@ -311,6 +311,329 @@ const calculateGroupStats = (group) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get uptime details by monitor ID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Monitor>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getUptimeDetailsById = async (req) => {
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
|
||||
}
|
||||
|
||||
const { dateRange, normalize } = req.query;
|
||||
const dates = getDateRange(dateRange);
|
||||
const formatLookup = {
|
||||
day: "%Y-%m-%dT%H:00:00Z",
|
||||
week: "%Y-%m-%dT%H:00:00Z",
|
||||
month: "%Y-%m-%dT00:00:00Z",
|
||||
};
|
||||
|
||||
const dateString = formatLookup[dateRange];
|
||||
const monitorData = await Check.aggregate([
|
||||
{
|
||||
$match: {
|
||||
monitorId: monitor._id,
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
aggregateData: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
firstCheck: {
|
||||
$first: "$$ROOT",
|
||||
},
|
||||
lastCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
uptimeDuration: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
lastFalseCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
groupChecks: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
_id: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
groupAggregate: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
upChecksAggregate: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
upChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
],
|
||||
downChecksAggregate: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
downChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
avgResponseTime: {
|
||||
$arrayElemAt: ["$aggregateData.avgResponseTime", 0],
|
||||
},
|
||||
totalChecks: {
|
||||
$arrayElemAt: ["$aggregateData.totalChecks", 0],
|
||||
},
|
||||
latestResponseTime: {
|
||||
$arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0],
|
||||
},
|
||||
timeSinceLastCheck: {
|
||||
$let: {
|
||||
vars: {
|
||||
lastCheck: {
|
||||
$arrayElemAt: ["$aggregateData.lastCheck", 0],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$lastCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$lastCheck.createdAt"],
|
||||
},
|
||||
0,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
timeSinceLastFalseCheck: {
|
||||
$let: {
|
||||
vars: {
|
||||
lastFalseCheck: {
|
||||
$arrayElemAt: ["$uptimeDuration.lastFalseCheck", 0],
|
||||
},
|
||||
firstCheck: {
|
||||
$arrayElemAt: ["$aggregateData.firstCheck", 0],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$lastFalseCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$lastFalseCheck.createdAt"],
|
||||
},
|
||||
{
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$firstCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$firstCheck.createdAt"],
|
||||
},
|
||||
0,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
groupChecks: "$groupChecks",
|
||||
groupAggregate: {
|
||||
$arrayElemAt: ["$groupAggregate", 0],
|
||||
},
|
||||
upChecksAggregate: {
|
||||
$arrayElemAt: ["$upChecksAggregate", 0],
|
||||
},
|
||||
upChecks: "$upChecks",
|
||||
downChecksAggregate: {
|
||||
$arrayElemAt: ["$downChecksAggregate", 0],
|
||||
},
|
||||
downChecks: "$downChecks",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const normalizedGroupChecks = NormalizeDataUptimeDetails(
|
||||
monitorData[0].groupChecks,
|
||||
10,
|
||||
100
|
||||
);
|
||||
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
stats: {
|
||||
avgResponseTime: monitorData[0].avgResponseTime,
|
||||
totalChecks: monitorData[0].totalChecks,
|
||||
timeSinceLastCheck: monitorData[0].timeSinceLastCheck,
|
||||
timeSinceLastFalseCheck: monitorData[0].timeSinceLastFalseCheck,
|
||||
latestResponseTime: monitorData[0].latestResponseTime,
|
||||
groupChecks: normalizedGroupChecks,
|
||||
groupAggregate: monitorData[0].groupAggregate,
|
||||
upChecksAggregate: monitorData[0].upChecksAggregate,
|
||||
upChecks: monitorData[0].upChecks,
|
||||
downChecksAggregate: monitorData[0].downChecksAggregate,
|
||||
downChecks: monitorData[0].downChecks,
|
||||
},
|
||||
};
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getUptimeDetailsById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get stats by monitor ID
|
||||
* @async
|
||||
@@ -939,6 +1262,7 @@ export {
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getUptimeDetailsById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
|
||||
8
Server/package-lock.json
generated
8
Server/package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bullmq": "5.34.5",
|
||||
"bullmq": "5.34.6",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -1541,9 +1541,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.34.5",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.5.tgz",
|
||||
"integrity": "sha512-MHho9EOhLCTY3ZF+dd0wHv0VlY2FtpBcopMRsvj0kPra4TAwBFh2pik/s4WbX56cIfCE+VzfHIHy4xvqp3g1+Q==",
|
||||
"version": "5.34.6",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.6.tgz",
|
||||
"integrity": "sha512-pRCYyO9RlkQWxdmKlrNnUthyFwurYXRYLVXD1YIx+nCCdhAOiHatD8FDHbsT/w2I31c0NWoMcfZiIGuipiF7Lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.9.0",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bullmq": "5.34.5",
|
||||
"bullmq": "5.34.6",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
@@ -17,6 +17,10 @@ class MonitorRoutes {
|
||||
"/hardware/details/:monitorId",
|
||||
this.monitorController.getHardwareDetailsById
|
||||
);
|
||||
this.router.get(
|
||||
"/uptime/details/:monitorId",
|
||||
this.monitorController.getUptimeDetailsById
|
||||
);
|
||||
this.router.get("/certificate/:monitorId", (req, res, next) => {
|
||||
this.monitorController.getMonitorCertificate(
|
||||
req,
|
||||
|
||||
@@ -8,12 +8,23 @@ const calculatePercentile = (arr, percentile) => {
|
||||
return sorted[lower].responseTime * (1 - weight) + sorted[upper].responseTime * weight;
|
||||
};
|
||||
|
||||
const calculatePercentileUptimeDetails = (arr, percentile) => {
|
||||
const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime);
|
||||
const index = (percentile / 100) * (sorted.length - 1);
|
||||
const lower = Math.floor(index);
|
||||
const upper = lower + 1;
|
||||
const weight = index % 1;
|
||||
if (upper >= sorted.length) return sorted[lower].avgResponseTime;
|
||||
return (
|
||||
sorted[lower].avgResponseTime * (1 - weight) + sorted[upper].avgResponseTime * weight
|
||||
);
|
||||
};
|
||||
|
||||
const NormalizeData = (checks, rangeMin, rangeMax) => {
|
||||
if (checks.length > 1) {
|
||||
// Get the 5th and 95th percentile
|
||||
const min = calculatePercentile(checks, 0);
|
||||
const max = calculatePercentile(checks, 95);
|
||||
|
||||
const normalizedChecks = checks.map((check) => {
|
||||
const originalResponseTime = check.responseTime;
|
||||
// Normalize the response time between 1 and 100
|
||||
@@ -40,5 +51,42 @@ const NormalizeData = (checks, rangeMin, rangeMax) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => {
|
||||
if (checks.length > 1) {
|
||||
// Get the 5th and 95th percentile
|
||||
const min = calculatePercentileUptimeDetails(checks, 0);
|
||||
const max = calculatePercentileUptimeDetails(checks, 95);
|
||||
|
||||
export { calculatePercentile, NormalizeData };
|
||||
const normalizedChecks = checks.map((check) => {
|
||||
const originalResponseTime = check.avgResponseTime;
|
||||
// Normalize the response time between 1 and 100
|
||||
let normalizedResponseTime =
|
||||
rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min);
|
||||
|
||||
// Put a floor on the response times so we don't have extreme outliers
|
||||
// Better visuals
|
||||
normalizedResponseTime = Math.max(
|
||||
rangeMin,
|
||||
Math.min(rangeMax, normalizedResponseTime)
|
||||
);
|
||||
return {
|
||||
...check,
|
||||
avgResponseTime: normalizedResponseTime,
|
||||
originalAvgResponseTime: originalResponseTime,
|
||||
};
|
||||
});
|
||||
|
||||
return normalizedChecks;
|
||||
} else {
|
||||
return checks.map((check) => {
|
||||
return { ...check, originalResponseTime: check.responseTime };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
calculatePercentile,
|
||||
NormalizeData,
|
||||
calculatePercentileUptimeDetails,
|
||||
NormalizeDataUptimeDetails,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user