Merge branch 'develop' into fix/fe/text-input-integration

This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-11-26 18:05:52 -08:00
commit c62d62f58e
14 files changed
+1006 -847

No files matched your search

+415 -611
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -14,8 +14,8 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
"@mui/icons-material": "^5.15.17",
"@mui/lab": "^5.0.0-alpha.170",
"@mui/icons-material": "6.1.8",
"@mui/lab": "6.0.0-beta.16",
"@mui/material": "6.1.8",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.22.3",
+133 -76
View File
@@ -1,3 +1,57 @@
/**
* CustomAreaChart component for rendering an area chart with optional gradient and custom ticks.
*
* @param {Object} props - The properties object.
* @param {Array} props.data - The data array for the chart.
* @param {Array} props.dataKeys - An array of data keys to be plotted as separate areas.
* @param {string} props.xKey - The key for the x-axis data.
* @param {string} [props.yKey] - The key for the y-axis data (optional).
* @param {Object} [props.xTick] - Custom tick component for the x-axis.
* @param {Object} [props.yTick] - Custom tick component for the y-axis.
* @param {string} [props.strokeColor] - The base stroke color for the areas.
* If not provided, uses a predefined color palette.
* @param {string} [props.fillColor] - The base fill color for the areas.
* @param {boolean} [props.gradient=false] - Whether to apply a gradient fill to the areas.
* @param {string} [props.gradientDirection="vertical"] - The direction of the gradient.
* @param {string} [props.gradientStartColor] - The start color of the gradient.
* Defaults to the area's stroke color if not provided.
* @param {string} [props.gradientEndColor] - The end color of the gradient.
* @param {Object} [props.customTooltip] - Custom tooltip component for the chart.
* @param {string|number} [props.height="100%"] - Height of the chart container.
*
* @returns {JSX.Element} The rendered area chart component.
*
* @example
* // Single series chart
* <CustomAreaChart
* data={temperatureData}
* dataKeys={["temperature"]}
* xKey="date"
* yKey="temperature"
* gradient={true}
* gradientStartColor="#ff6b6b"
* gradientEndColor="#4ecdc4"
* />
*
* @example
* // Multi-series chart with custom tooltip
* <CustomAreaChart
* data={performanceData}
* dataKeys={["cpu.usage", "memory.usage"]}
* xKey="timestamp"
* xTick={<CustomTimeTick />}
* yTick={<PercentageTick />}
* gradient={true}
* customTooltip={({ active, payload, label }) => (
* <CustomTooltip
* label={label}
* payload={payload}
* active={active}
* />
* )}
* />
*/
import {
AreaChart,
Area,
@@ -11,69 +65,15 @@ import { createGradient } from "../Utils/gradientUtils";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
import { useId } from "react";
/**
* CustomAreaChart component for rendering an area chart with optional gradient and custom ticks.
*
* @param {Object} props - The properties object.
* @param {Array} props.data - The data array for the chart.
* @param {string} props.xKey - The key for the x-axis data.
* @param {string} props.yKey - The key for the y-axis data.
* @param {Object} [props.xTick] - Custom tick component for the x-axis.
* @param {Object} [props.yTick] - Custom tick component for the y-axis.
* @param {string} [props.strokeColor] - The stroke color for the area.
* @param {string} [props.fillColor] - The fill color for the area.
* @param {boolean} [props.gradient=false] - Whether to apply a gradient fill.
* @param {string} [props.gradientDirection="vertical"] - The direction of the gradient.
* @param {string} [props.gradientStartColor] - The start color of the gradient.
* @param {string} [props.gradientEndColor] - The end color of the gradient.
* @param {Object} [props.customTooltip] - Custom tooltip component.
* @returns {JSX.Element} The rendered area chart component.
*
* @example
* // Example usage of CustomAreaChart
* import React from 'react';
* import CustomAreaChart from './CustomAreaChart';
* import { TzTick, PercentTick, InfrastructureTooltip } from './chartUtils';
*
* const data = [
* { createdAt: '2023-01-01T00:00:00Z', cpu: { usage_percent: 0.5 } },
* { createdAt: '2023-01-01T01:00:00Z', cpu: { usage_percent: 0.6 } },
* // more data points...
* ];
*
* const MyChartComponent = () => {
* return (
* <CustomAreaChart
* data={data}
* xKey="createdAt"
* yKey="cpu.usage_percent"
* xTick={<TzTick />}
* yTick={<PercentTick />}
* strokeColor="#8884d8"
* fillColor="#8884d8"
* gradient={true}
* gradientStartColor="#8884d8"
* gradientEndColor="#82ca9d"
* customTooltip={({ active, payload, label }) => (
* <InfrastructureTooltip
* label={label?.toString() ?? ""}
* yKey="cpu.usage_percent"
* yLabel="CPU Usage"
* active={active}
* payload={payload}
* />
* )}
* />
* );
* };
*
* export default MyChartComponent;
*/
import { Fragment } from "react";
const CustomAreaChart = ({
data,
dataKey,
dataKeys,
xKey,
xDomain,
yKey,
yDomain,
xTick,
yTick,
strokeColor,
@@ -87,7 +87,49 @@ const CustomAreaChart = ({
}) => {
const theme = useTheme();
const uniqueId = useId();
const gradientId = `gradient-${uniqueId}`;
const AREA_COLORS = [
// Blues
"#3182bd", // Deep blue
"#6baed6", // Medium blue
"#9ecae1", // Light blue
// Greens
"#74c476", // Soft green
"#a1d99b", // Light green
"#c7e9c0", // Pale green
// Oranges
"#fdae6b", // Warm orange
"#fdd0a2", // Light orange
"#feedde", // Pale orange
// Purples
"#9467bd", // Lavender
"#a55194", // Deep magenta
"#c994c7", // Soft magenta
// Reds
"#ff9896", // Soft red
"#de2d26", // Deep red
"#fc9272", // Medium red
// Cyans/Teals
"#17becf", // Cyan
"#7fcdbb", // Teal
"#a1dab4", // Light teal
// Yellows
"#fec44f", // Mustard
"#fee391", // Light yellow
"#ffffd4", // Pale yellow
// Additional colors
"#e377c2", // Soft pink
"#bcbd22", // Olive
"#2ca02c", // Vibrant green
];
return (
<ResponsiveContainer
width="100%"
@@ -97,19 +139,15 @@ const CustomAreaChart = ({
<AreaChart data={data}>
<XAxis
dataKey={xKey}
{...(xDomain && { domain: xDomain })}
{...(xTick && { tick: xTick })}
/>
<YAxis
dataKey={yKey}
{...(yDomain && { domain: yDomain })}
{...(yTick && { tick: yTick })}
/>
{gradient === true &&
createGradient({
id: gradientId,
startColor: gradientStartColor,
endColor: gradientEndColor,
direction: gradientDirection,
})}
<CartesianGrid
stroke={theme.palette.border.light}
strokeWidth={1}
@@ -117,12 +155,29 @@ const CustomAreaChart = ({
fill="transparent"
vertical={false}
/>
<Area
type="monotone"
dataKey={dataKey}
stroke={strokeColor}
fill={gradient === true ? `url(#${gradientId})` : fillColor}
/>
{dataKeys.map((dataKey, index) => {
const gradientId = `gradient-${uniqueId}-${index}`;
return (
<Fragment key={dataKey}>
{gradient === true &&
createGradient({
id: gradientId,
startColor: gradientStartColor || AREA_COLORS[index],
endColor: gradientEndColor,
direction: gradientDirection,
})}
<Area
yKey={dataKey}
key={dataKey}
type="monotone"
dataKey={dataKey}
stroke={strokeColor || AREA_COLORS[index]}
fill={gradient === true ? `url(#${gradientId})` : fillColor}
/>
</Fragment>
);
})}
{customTooltip ? (
<Tooltip
cursor={{ stroke: theme.palette.border.light }}
@@ -139,18 +194,20 @@ const CustomAreaChart = ({
CustomAreaChart.propTypes = {
data: PropTypes.array.isRequired,
dataKey: PropTypes.string.isRequired,
dataKeys: PropTypes.array.isRequired,
xTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
yTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
xKey: PropTypes.string.isRequired,
yKey: PropTypes.string.isRequired,
xDomain: PropTypes.array,
yKey: PropTypes.string,
yDomain: PropTypes.array,
fillColor: PropTypes.string,
strokeColor: PropTypes.string,
gradient: PropTypes.bool,
gradientDirection: PropTypes.string,
gradientStartColor: PropTypes.string,
gradientEndColor: PropTypes.string,
customTooltip: PropTypes.func,
customTooltip: PropTypes.object,
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
@@ -62,7 +62,7 @@ export const PercentTick = ({ x, y, payload, index }) => {
fontSize={11}
fontWeight={400}
>
{`${payload?.value * 100}%`}
{`${(payload?.value * 100).toFixed()}%`}
</Text>
);
};
@@ -165,7 +165,6 @@ export const InfrastructureTooltip = ({
? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}`
: `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][metric])}`}
</Typography>
<Typography component="span"></Typography>
</Stack>
</Box>
{/* Display original value */}
@@ -188,3 +187,91 @@ InfrastructureTooltip.propTypes = {
yLabel: PropTypes.string,
dotColor: PropTypes.string,
};
export const TemperatureTooltip = ({ active, payload, label, keys, dotColor }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
const formatCoreKey = (key) => {
return key.replace(/^core(\d+)$/, "Core $1");
};
if (active && payload && payload.length) {
return (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
</Typography>
<Stack direction="column">
{keys.map((key) => {
return (
<Stack
key={key}
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{`${formatCoreKey(key)}: ${payload[0].payload[key]} °C`}
</Typography>
</Stack>
<Typography component="span"></Typography>
</Stack>
);
})}
</Stack>
</Box>
);
}
return null;
};
TemperatureTooltip.propTypes = {
active: PropTypes.bool,
keys: PropTypes.array,
payload: PropTypes.array,
label: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.string,
PropTypes.number,
]),
};
+3 -3
View File
@@ -83,7 +83,7 @@ const menu = [
const URL_MAP = {
support: "https://github.com/bluewave-labs/bluewave-uptime/issues",
docs: "https://bluewavelabs.gitbook.io/uptime-manager",
docs: "https://bluewavelabs.gitbook.io/checkmate",
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
};
@@ -207,14 +207,14 @@ function Sidebar() {
userSelect: "none",
}}
>
BU
C
</Stack>
<Typography
component="span"
mt={theme.spacing(2)}
sx={{ opacity: 0.8, fontWeight: 500 }}
>
BlueWave Uptime
Checkmate
</Typography>
</Stack>
<IconButton
@@ -10,6 +10,7 @@ import {
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { createToast } from "../../../Utils/toastUtils";
import Link from "../../../Components/Link";
import { ConfigBox } from "../../Monitors/styled";
import TextInput from "../../../Components/Inputs/TextInput";
import Select from "../../../Components/Inputs/Select";
@@ -31,12 +32,14 @@ const CreateInfrastructureMonitor = () => {
usage_memory: "",
disk: false,
usage_disk: "",
temperature: false,
usage_temperature: "",
secret: "",
});
const MS_PER_MINUTE = 60000;
const THRESHOLD_FIELD_PREFIX = "usage_";
const HARDWARE_MONITOR_TYPES = ["cpu", "memory", "disk"];
const HARDWARE_MONITOR_TYPES = ["cpu", "memory", "disk", "temperature"];
const { user, authToken } = useSelector((state) => state.auth);
const monitorState = useSelector((state) => state.infrastructureMonitor);
const dispatch = useDispatch();
@@ -73,18 +76,18 @@ const CreateInfrastructureMonitor = () => {
});
};
const handleBlur = (event, appenedID) => {
const handleBlur = (event, appendID) => {
event.preventDefault();
const { value, id } = event.target;
if (id?.startsWith("notify-email-")) return;
const { error } = infrastructureMonitorValidation.validate(
{ [id ?? appenedID]: value },
{ [id ?? appendID]: value },
{
abortEarly: false,
}
);
setErrors((prev) => {
return buildErrors(prev, id ?? appenedID, error);
return buildErrors(prev, id ?? appendID, error);
});
};
@@ -231,11 +234,21 @@ const CreateInfrastructureMonitor = () => {
</Typography>
<ConfigBox>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the friendly name and
authorization secret to connect to the server agent.
</Typography>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the friendly name
and authorization secret to connect to the server agent.
</Typography>
<Typography component="p">
The server you are monitoring must be running the{" "}
<Link
level="primary"
url="https://github.com/bluewave-labs/checkmate-agent"
label="Checkmate Monitoring Agent"
/>
</Typography>
</Stack>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
+270 -119
View File
@@ -17,6 +17,7 @@ import {
TzTick,
PercentTick,
InfrastructureTooltip,
TemperatureTooltip,
} from "../../../Components/Charts/Utils/chartUtils";
import PropTypes from "prop-types";
@@ -204,6 +205,238 @@ const InfrastructureDetails = () => {
(chartContainerHeight - totalChartContainerPadding - totalTypographyPadding) * 0.95;
// end height calculations
const buildStatBoxes = (checks) => {
let latestCheck = checks[0] ?? null;
if (latestCheck === null) return [];
// Extract values from latest check
const physicalCores = latestCheck?.cpu?.physical_core ?? 0;
const logicalCores = latestCheck?.cpu?.logical_core ?? 0;
const cpuFrequency = latestCheck?.cpu?.frequency ?? 0;
const cpuTemperature =
latestCheck?.cpu?.temperature?.length > 0
? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) /
latestCheck.cpu.temperature.length
: 0;
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0;
const os = latestCheck?.host?.os ?? null;
const platform = latestCheck?.host?.platform ?? null;
const osPlatform = os === null && platform === null ? null : `${os} ${platform}`;
return [
{
id: 0,
heading: "CPU (Physical)",
subHeading: `${physicalCores} cores`,
},
{
id: 1,
heading: "CPU (Logical)",
subHeading: `${logicalCores} cores`,
},
{
id: 2,
heading: "CPU Frequency",
subHeading: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
},
{
id: 3,
heading: "Average CPU Temperature",
subHeading: `${cpuTemperature.toFixed(2)} C`,
},
{
id: 4,
heading: "Memory",
subHeading: formatBytes(memoryTotalBytes),
},
{
id: 5,
heading: "Disk",
subHeading: formatBytes(diskTotalBytes),
},
{ id: 6, heading: "Uptime", subHeading: "100%" },
{
id: 7,
heading: "Status",
subHeading: monitor?.status === true ? "Active" : "Inactive",
},
{
id: 8,
heading: "OS",
subHeading: osPlatform,
},
];
};
const buildGaugeBoxConfigs = (checks) => {
let latestCheck = checks[0] ?? null;
if (latestCheck === null) return [];
// Extract values from latest check
const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0;
const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0;
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
const cpuUsagePercent = latestCheck?.cpu?.usage_percent ?? 0;
const cpuPhysicalCores = latestCheck?.cpu?.physical_core ?? 0;
const cpuFrequency = latestCheck?.cpu?.frequency ?? 0;
return [
{
type: "memory",
value: decimalToPercentage(memoryUsagePercent),
heading: "Memory Usage",
metricOne: "Used",
valueOne: formatBytes(memoryUsedBytes),
metricTwo: "Total",
valueTwo: formatBytes(memoryTotalBytes),
},
{
type: "cpu",
value: decimalToPercentage(cpuUsagePercent),
heading: "CPU Usage",
metricOne: "Cores",
valueOne: cpuPhysicalCores ?? 0,
metricTwo: "Frequency",
valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
},
...(latestCheck?.disk ?? []).map((disk, idx) => ({
type: "disk",
diskIndex: idx,
value: decimalToPercentage(disk.usage_percent),
heading: `Disk${idx} usage`,
metricOne: "Used",
valueOne: formatBytes(disk.total_bytes - disk.free_bytes),
metricTwo: "Total",
valueTwo: formatBytes(disk.total_bytes),
})),
];
};
const buildTemps = (checks) => {
let numCores = 0;
if (checks === null) return { temps: [], tempKeys: [] };
for (const check of checks) {
if (check?.cpu?.temperature?.length > numCores) {
numCores = check.cpu.temperature.length;
break;
}
}
if (numCores === 0) return { temps: [], tempKeys: [] };
const temps = checks.map((check) => {
if (check.cpu.temperature.length > numCores) {
numCores = check.cpu.temperature.length;
}
// If there's no data, set the temperature to 0
if (check.cpu.temperature.length === 0) {
check.cpu.temperature = Array(numCores).fill(0);
}
return check.cpu.temperature.reduce(
(acc, cur, idx) => {
acc[`core${idx + 1}`] = cur;
return acc;
},
{
createdAt: check.createdAt,
}
);
});
// Slice to remove `createdAt` key
return { tempKeys: Object.keys(temps[0]).slice(1), temps };
};
const buildAreaChartConfigs = (checks) => {
let latestCheck = checks[0] ?? null;
if (latestCheck === null) return [];
const reversedChecks = checks.toReversed();
const tempData = buildTemps(reversedChecks);
return [
{
type: "memory",
data: reversedChecks,
dataKeys: ["memory.usage_percent"],
heading: "Memory usage",
strokeColor: theme.palette.primary.main,
gradientStartColor: theme.palette.primary.main,
yLabel: "Memory Usage",
yDomain: [0, 1],
yTick: <PercentTick />,
xTick: <TzTick />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.primary.main}
yKey={"memory.usage_percent"}
yLabel={"Memory Usage"}
/>
),
},
{
type: "cpu",
data: reversedChecks,
dataKeys: ["cpu.usage_percent"],
heading: "CPU usage",
strokeColor: theme.palette.success.main,
gradientStartColor: theme.palette.success.main,
yLabel: "CPU Usage",
yDomain: [0, 1],
yTick: <PercentTick />,
xTick: <TzTick />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.success.main}
yKey={"cpu.usage_percent"}
yLabel={"CPU Usage"}
/>
),
},
{
type: "temperature",
data: tempData.temps,
dataKeys: tempData.tempKeys,
strokeColor: theme.palette.error.main,
gradientStartColor: theme.palette.error.main,
heading: "CPU Temperature",
yLabel: "Temperature",
xTick: <TzTick />,
yDomain: [
Math.min(...tempData.temps.flatMap((t) => tempData.tempKeys.map((k) => t[k]))) *
0.9,
Math.max(...tempData.temps.flatMap((t) => tempData.tempKeys.map((k) => t[k]))) *
1.1,
],
toolTip: (
<TemperatureTooltip
keys={tempData.tempKeys}
dotColor={theme.palette.error.main}
/>
),
},
...(latestCheck?.disk?.map((disk, idx) => ({
type: "disk",
data: reversedChecks,
diskIndex: idx,
dataKeys: [`disk[${idx}].usage_percent`],
heading: `Disk${idx} usage`,
strokeColor: theme.palette.warning.main,
gradientStartColor: theme.palette.warning.main,
yLabel: "Disk Usage",
yDomain: [0, 1],
yTick: <PercentTick />,
xTick: <TzTick />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.warning.main}
yKey={`disk.usage_percent`}
yLabel={"Disc usage"}
yIdx={idx}
/>
),
})) || []),
];
};
// Fetch data
useEffect(() => {
const fetchData = async () => {
@@ -211,102 +444,24 @@ const InfrastructureDetails = () => {
const response = await networkService.getStatsByMonitorId({
authToken: authToken,
monitorId: monitorId,
sortOrder: "asc",
sortOrder: null,
limit: null,
dateRange: dateRange,
numToDisplay: 50,
normalize: false,
});
setMonitor(response.data.data);
} catch (error) {
navigate("/not-found", { replace: true });
logger.error(error);
logger.error(error);
}
};
fetchData();
}, [authToken, monitorId, dateRange]);
}, [authToken, monitorId, dateRange, navigate]);
const statBoxConfigs = [
{
id: 0,
heading: "CPU",
subHeading: `${monitor?.checks[0]?.cpu?.physical_core ?? 0} cores`,
},
{
id: 1,
heading: "Memory",
subHeading: formatBytes(monitor?.checks[0]?.memory?.total_bytes),
},
{
id: 2,
heading: "Disk",
subHeading: formatBytes(monitor?.checks[0]?.disk[0]?.total_bytes),
},
{ id: 3, heading: "Uptime", subHeading: "100%" },
{
id: 4,
heading: "Status",
subHeading: monitor?.status === true ? "Active" : "Inactive",
},
];
const gaugeBoxConfigs = [
{
type: "memory",
value: decimalToPercentage(monitor?.checks[0]?.memory?.usage_percent),
heading: "Memory Usage",
metricOne: "Used",
valueOne: formatBytes(monitor?.checks[0]?.memory?.used_bytes),
metricTwo: "Total",
valueTwo: formatBytes(monitor?.checks[0]?.memory?.total_bytes),
},
{
type: "cpu",
value: decimalToPercentage(monitor?.checks[0]?.cpu?.usage_percent),
heading: "CPU Usage",
metricOne: "Cores",
valueOne: monitor?.checks[0]?.cpu?.physical_core ?? 0,
metricTwo: "Frequency",
valueTwo: `${(monitor?.checks[0]?.cpu?.frequency ?? 0 / 1000).toFixed(2)} Ghz`,
},
...(monitor?.checks?.[0]?.disk ?? []).map((disk, idx) => ({
type: "disk",
diskIndex: idx,
value: decimalToPercentage(disk.usage_percent),
heading: `Disk${idx} usage`,
metricOne: "Used",
valueOne: formatBytes(disk.total_bytes - disk.free_bytes),
metricTwo: "Total",
valueTwo: formatBytes(disk.total_bytes),
})),
];
const areaChartConfigs = [
{
type: "memory",
dataKey: "memory.usage_percent",
heading: "Memory usage",
strokeColor: theme.palette.primary.main,
yLabel: "Memory Usage",
},
{
type: "cpu",
dataKey: "cpu.usage_percent",
heading: "CPU usage",
strokeColor: theme.palette.success.main,
yLabel: "CPU Usage",
},
...(monitor?.checks?.[0]?.disk?.map((disk, idx) => ({
type: "disk",
diskIndex: idx,
dataKey: `disk[${idx}].usage_percent`,
heading: `Disk${idx} usage`,
strokeColor: theme.palette.warning.main,
yLabel: "Disk Usage",
})) || []),
];
const statBoxConfigs = buildStatBoxes(monitor?.checks ?? []);
const gaugeBoxConfigs = buildGaugeBoxConfigs(monitor?.checks ?? []);
const areaChartConfigs = buildAreaChartConfigs(monitor?.checks ?? []);
return (
<Box>
@@ -343,6 +498,7 @@ const InfrastructureDetails = () => {
</Stack>
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
{statBoxConfigs.map((statBox) => (
@@ -380,41 +536,36 @@ const InfrastructureDetails = () => {
},
}}
>
{areaChartConfigs.map((config) => (
<BaseBox key={`${config.type}-${config.diskIndex ?? ""}`}>
<Typography
component="h2"
padding={theme.spacing(8)}
>
{config.heading}
</Typography>
<AreaChart
height={areaChartHeight}
data={monitor?.checks ?? []}
dataKey={config.dataKey}
xKey="createdAt"
yKey={config.dataKey}
customTooltip={({ active, payload, label }) => (
<InfrastructureTooltip
label={label}
yKey={
config.type === "disk" ? "disk.usage_percent" : config.dataKey
}
yLabel={config.yLabel}
yIdx={config.diskIndex}
active={active}
payload={payload}
/>
)}
xTick={<TzTick />}
yTick={<PercentTick />}
strokeColor={config.strokeColor}
gradient={true}
gradientStartColor={config.strokeColor}
gradientEndColor="#ffffff"
/>
</BaseBox>
))}
{areaChartConfigs.map((config) => {
if (config?.data?.length === 0) {
return;
}
return (
<BaseBox key={`${config.type}-${config.diskIndex ?? ""}`}>
<Typography
component="h2"
padding={theme.spacing(8)}
>
{config.heading}
</Typography>
<AreaChart
height={areaChartHeight}
data={config.data}
dataKeys={config.dataKeys}
xKey="createdAt"
yDomain={config.yDomain}
customTooltip={config.toolTip}
xTick={config.xTick}
yTick={config.yTick}
strokeColor={config.strokeColor}
gradient={true}
gradientStartColor={config.gradientStartColor}
gradientEndColor="#ffffff"
/>
</BaseBox>
);
})}
</Stack>
</Stack>
) : (
+7 -2
View File
@@ -37,8 +37,13 @@ const hasValidationErrors = (form, validation, setErrors) => {
if (!form.disk || form.usage_disk) {
newErrors["usage_disk"] = null;
}
if (!form.temperature || form.usage_temperature) {
newErrors["usage_temperature"] = null;
}
});
if (Object.values(newErrors).some(v=> v)) {
console.log("newErrors", newErrors);
if (Object.values(newErrors).some((v) => v)) {
setErrors(newErrors);
return true;
} else {
@@ -48,4 +53,4 @@ const hasValidationErrors = (form, validation, setErrors) => {
}
return false;
};
export { buildErrors, hasValidationErrors };
export { buildErrors, hasValidationErrors };
+4 -3
View File
@@ -190,15 +190,16 @@ const infrastructureMonitorValidation = joi.object({
cpu: joi.boolean(),
memory: joi.boolean(),
disk: joi.boolean(),
temperature: joi.boolean(),
usage_memory: joi.number().messages({
"number.base": THRESHOLD_COMMON_BASE_MSG,
}),
usage_disk: joi.number().messages({
"number.base": THRESHOLD_COMMON_BASE_MSG,
}),
// usage_temperature: joi.number().messages({
// "number.base": "Temperature must be a number.",
// }),
usage_temperature: joi.number().messages({
"number.base": "Temperature must be a number.",
}),
// usage_system: joi.number().messages({
// "number.base": "System load must be a number.",
// }),
+3 -3
View File
@@ -13,7 +13,7 @@
![Dashboard-dark](https://github.com/user-attachments/assets/db875138-164f-453c-a75e-889f88747578)
(yes, we have a light theme as well, but this looks better on readme.md)
Checkmate is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
Checkmate is an open source uptime manager, server & Docker monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
## Demo
@@ -21,11 +21,11 @@ See [Checkmate](https://uptime-demo.bluewavelabs.ca/) in action. The username is
## User's guide
Usage instructions can be found [here](https://bluewavelabs.gitbook.io/uptime-manager).
Usage instructions can be found [here](https://bluewavelabs.gitbook.io/checkmate).
## Installation
See installation instructions in [Checkmate documentation portal](https://bluewavelabs.gitbook.io/uptime-manager/quickstart).
See installation instructions in [Checkmate documentation portal](https://bluewavelabs.gitbook.io/checkmate/quickstart).
## Questions & ideas
+2 -1
View File
@@ -4,7 +4,7 @@ const cpuSchema = mongoose.Schema({
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
frequency: { type: Number, default: 0 },
temperature: { type: Number, default: 0 },
temperature: { type: [Number], default: [] },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
@@ -54,6 +54,7 @@ const HardwareCheckSchema = mongoose.Schema(
type: hostSchema,
default: () => ({}),
},
errors: {
type: [errorSchema],
default: () => [],
+33
View File
@@ -38,9 +38,42 @@ const NotificationSchema = mongoose.Schema(
return this.alertThreshold;
},
},
tempAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
},
},
{
timestamps: true,
}
);
NotificationSchema.pre("save", function (next) {
if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) {
this.cpuAlertThreshold = this.alertThreshold;
}
if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) {
this.memoryAlertThreshold = this.alertThreshold;
}
if (!this.diskAlertThreshold || this.isModified("alertThreshold")) {
this.diskAlertThreshold = this.alertThreshold;
}
if (!this.tempAlertThreshold || this.isModified("alertThreshold")) {
this.tempAlertThreshold = this.alertThreshold;
}
next();
});
NotificationSchema.pre("findOneAndUpdate", function (next) {
const update = this.getUpdate();
if (update.alertThreshold) {
update.cpuAlertThreshold = update.alertThreshold;
update.memoryAlertThreshold = update.alertThreshold;
update.diskAlertThreshold = update.alertThreshold;
update.tempAlertThreshold = update.alertThreshold;
}
next();
});
export default mongoose.model("Notification", NotificationSchema);
+14 -12
View File
@@ -79,9 +79,10 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz",
"integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==",
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -1787,9 +1788,10 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -4522,9 +4524,9 @@
}
},
"node_modules/mongoose": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.2.tgz",
"integrity": "sha512-jCTSqDANfRzk909v4YoZQi7jlGRB2MTvgG+spVBc/BA4tOs1oWJr//V6yYujqNq9UybpOtsSfBqxI0dSOEFJHQ==",
"version": "8.8.3",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.3.tgz",
"integrity": "sha512-/I4n/DcXqXyIiLRfAmUIiTjj3vXfeISke8dt4U4Y8Wfm074Wa6sXnQrXN49NFOFf2mM1kUdOXryoBvkuCnr+Qw==",
"license": "MIT",
"dependencies": {
"bson": "^6.7.0",
@@ -5669,9 +5671,9 @@
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz",
"integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
+9 -4
View File
@@ -144,8 +144,10 @@ const getMonitorsAndSummaryByTeamIdQueryValidation = joi.object({
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker"),
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker"))
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware"),
joi
.array()
.items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware"))
),
});
@@ -161,8 +163,10 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker"),
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker"))
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware"),
joi
.array()
.items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware"))
),
page: joi.number(),
rowsPerPage: joi.number(),
@@ -201,6 +205,7 @@ const createMonitorBodyValidation = joi.object({
usage_cpu: joi.number(),
usage_memory: joi.number(),
usage_disk: joi.number(),
usage_temperature: joi.number(),
}),
notifications: joi.array().items(joi.object()),
secret: joi.string(),