mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-26 11:54:11 -06:00
Merge branch 'develop' into v2-select-component
This commit is contained in:
@@ -2,7 +2,7 @@ import Box from "@mui/material/Box";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
import { getStatusPalette } from "@/Utils/MonitorUtils";
|
||||
import { getStatusPalette } from "@/Utils/v2/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const StatusLabel = ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import Typography from "@mui/material/Typography";
|
||||
import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react";
|
||||
import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
|
||||
|
||||
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
|
||||
import { getResponseTimeColor } from "@/Utils/v2/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const ChartAvgResponse = ({ avg, max }: { avg: number; max: number }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseChart } from "./HistogramStatus";
|
||||
import { BaseBox } from "../DesignElements";
|
||||
import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react";
|
||||
import { normalizeResponseTimes } from "@/Utils/DataUtils";
|
||||
import { normalizeResponseTimes } from "@/Utils/v2/DataUtils";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
formatDateWithTz,
|
||||
tickDateFormatLookup,
|
||||
tooltipDateFormatLookup,
|
||||
} from "@/Utils/TimeUtils";
|
||||
} from "@/Utils/v2/TimeUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { GroupedCheck } from "@/Types/Check";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
@@ -3,7 +3,7 @@ import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { Check } from "@/Types/Check";
|
||||
import { HistogramResponseTimeTooltip } from "@/Components/v2/Monitors/HistogramResponseTimeTooltip";
|
||||
import { normalizeResponseTimes } from "@/Utils/DataUtils";
|
||||
import { normalizeResponseTimes } from "@/Utils/v2/DataUtils";
|
||||
|
||||
export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => {
|
||||
const normalChecks = normalizeResponseTimes(checks, "responseTime");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import { formatDateWithTz } from "@/Utils/v2/TimeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { LatestCheck } from "@/Types/Check";
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import IncidentsIcon from "@/assets/icons/incidents.svg?react";
|
||||
import type { GroupedCheck } from "@/Types/Check";
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
import { normalizeResponseTimes } from "@/Utils/DataUtils";
|
||||
import { normalizeResponseTimes } from "@/Utils/v2/DataUtils";
|
||||
import { useState } from "react";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import { formatDateWithTz } from "@/Utils/v2/TimeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
|
||||
import { getResponseTimeColor } from "@/Utils/v2/MonitorUtils";
|
||||
|
||||
const XLabel = ({
|
||||
p1,
|
||||
|
||||
@@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { PulseDot } from "@/Components/v2/DesignElements/PulseDot";
|
||||
import { Dot } from "@/Components/v2/DesignElements/Dot";
|
||||
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
|
||||
import { getStatusColor, formatUrl } from "@/Utils/v2/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { typographyLevels } from "@/Utils/Theme/v2/palette";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import type { SWRConfiguration } from "swr";
|
||||
import type { AxiosRequestConfig } from "axios";
|
||||
import { get, post, patch } from "@/Utils/ApiClient"; // your axios wrapper
|
||||
import { get, post, patch } from "@/Utils/v2/ApiClient"; // your axios wrapper
|
||||
|
||||
export type ApiResponse = {
|
||||
message: string;
|
||||
|
||||
17
client/src/Hooks/v2/useInitMonitorForm.tsx
Normal file
17
client/src/Hooks/v2/useInitMonitorForm.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { monitorSchema } from "@/Validation/v2/zod";
|
||||
import { z } from "zod";
|
||||
export const useInitForm = ({
|
||||
initialData,
|
||||
}: {
|
||||
initialData: Partial<z.infer<typeof monitorSchema>> | undefined;
|
||||
}) => {
|
||||
const defaults: z.infer<typeof monitorSchema> = {
|
||||
type: initialData?.type || "http",
|
||||
url: initialData?.url || "",
|
||||
n: initialData?.n || 3,
|
||||
notificationChannels: initialData?.notificationChannels || [],
|
||||
name: initialData?.name || "",
|
||||
interval: initialData?.interval || "1 minute",
|
||||
};
|
||||
return { defaults };
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import type { MonitorStatus } from "@/Types/Monitor";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGet } from "@/Hooks/v2/UseApi";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import { formatDateWithTz } from "@/Utils/v2/TimeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
const headers: Header<Check>[] = [
|
||||
|
||||
43
client/src/Pages/v2/Uptime/Configure.tsx
Normal file
43
client/src/Pages/v2/Uptime/Configure.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { monitorSchema } from "@/Validation/v2/zod";
|
||||
import { useGet, usePost } from "@/Hooks/v2/UseApi";
|
||||
import { UptimeForm } from "@/Pages/v2/Uptime/UptimeForm";
|
||||
|
||||
import { useParams } from "react-router";
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
import humanInterval from "human-interval";
|
||||
import { z } from "zod";
|
||||
|
||||
export const UptimeConfigurePage = () => {
|
||||
type FormValues = z.infer<typeof monitorSchema>;
|
||||
type SubmitValues = Omit<FormValues, "interval"> & { interval: number | undefined };
|
||||
|
||||
const { id } = useParams();
|
||||
const { response } = useGet<ApiResponse>("/notification-channels");
|
||||
const { response: monitorResponse } = useGet<ApiResponse>(`/monitors/${id}`);
|
||||
const monitor = monitorResponse?.data || null;
|
||||
const notificationOptions = response?.data ?? [];
|
||||
|
||||
const { post, loading, error } = usePost<SubmitValues, ApiResponse>();
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
let interval = humanInterval(data.interval);
|
||||
if (!interval) interval = 60000;
|
||||
const submitData = { ...data, interval };
|
||||
const result = await post("/monitors", submitData);
|
||||
if (result) {
|
||||
console.log(result);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<UptimeForm
|
||||
initialData={{
|
||||
...monitor,
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
notificationOptions={notificationOptions}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -14,7 +14,7 @@ import { useTheme } from "@mui/material/styles";
|
||||
import { useParams } from "react-router";
|
||||
import { useGet, usePatch, type ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
import { useState } from "react";
|
||||
import { getStatusPalette } from "@/Utils/MonitorUtils";
|
||||
import { getStatusPalette } from "@/Utils/v2/MonitorUtils";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
|
||||
const UptimeDetailsPage = () => {
|
||||
|
||||
247
client/src/Pages/v2/Uptime/UptimeForm.tsx
Normal file
247
client/src/Pages/v2/Uptime/UptimeForm.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { TextInput } from "@/Components/v2/Inputs/TextInput";
|
||||
import { AutoCompleteInput } from "@/Components/v2/Inputs/AutoComplete";
|
||||
import { ConfigBox, BasePage } from "@/Components/v2/DesignElements";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import { RadioWithDescription } from "@/Components/v2/Inputs/RadioInput";
|
||||
import { Button } from "@/Components/v2/Inputs";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { monitorSchema } from "@/Validation/v2/zod";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm, Controller, useWatch, type SubmitHandler } from "react-hook-form";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useInitForm } from "@/Hooks/v2/useInitMonitorForm";
|
||||
|
||||
type FormValues = z.infer<typeof monitorSchema>;
|
||||
|
||||
export const UptimeForm = ({
|
||||
initialData,
|
||||
onSubmit,
|
||||
notificationOptions,
|
||||
loading,
|
||||
}: {
|
||||
initialData?: Partial<FormValues>;
|
||||
onSubmit: SubmitHandler<FormValues>;
|
||||
notificationOptions: any[];
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { defaults } = useInitForm({ initialData: initialData });
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(monitorSchema) as any,
|
||||
defaultValues: defaults,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const selectedType = useWatch({
|
||||
control,
|
||||
name: "type",
|
||||
});
|
||||
const notificationChannels = useWatch({
|
||||
control,
|
||||
name: "notificationChannels",
|
||||
});
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
component={"form"}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<ConfigBox
|
||||
title={t("distributedUptimeCreateChecks")}
|
||||
subtitle={t("distributedUptimeCreateChecksDescription")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl error={!!errors.type}>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
sx={{ gap: theme.spacing(6) }}
|
||||
>
|
||||
<RadioWithDescription
|
||||
value="http"
|
||||
label={"HTTP"}
|
||||
description={"Use HTTP to monitor your website or API endpoint."}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="https"
|
||||
label="HTTPS"
|
||||
description="Use HTTPS to monitor your website or API endpoint.
|
||||
"
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="ping"
|
||||
label={t("pingMonitoring")}
|
||||
description={t("pingMonitoringDescription")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("settingsGeneralSettings")}
|
||||
subtitle={t(`uptimeGeneralInstructions.${selectedType}`)}
|
||||
rightContent={
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("url")}
|
||||
fullWidth
|
||||
error={!!errors.url}
|
||||
helperText={errors.url ? errors.url.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("displayName")}
|
||||
fullWidth
|
||||
error={!!errors.name}
|
||||
helperText={errors.name ? errors.name.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("createMonitorPage.incidentConfigTitle")}
|
||||
subtitle={t("createMonitorPage.incidentConfigDescriptionV2")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="n"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
label={t("createMonitorPage.incidentConfigStatusCheckNumber")}
|
||||
fullWidth
|
||||
error={!!errors.n}
|
||||
helperText={errors.n ? errors.n.message : ""}
|
||||
onChange={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
field.onChange(target.valueAsNumber);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("notificationConfig.title")}
|
||||
subtitle={t("notificationConfig.description")}
|
||||
rightContent={
|
||||
<Stack>
|
||||
<Controller
|
||||
name="notificationChannels"
|
||||
control={control}
|
||||
defaultValue={[]} // important!
|
||||
render={({ field }) => (
|
||||
<AutoCompleteInput
|
||||
multiple
|
||||
options={notificationOptions}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={notificationOptions.filter((o: any) =>
|
||||
(field.value || []).includes(o._id)
|
||||
)}
|
||||
onChange={(_, newValue) => {
|
||||
field.onChange(newValue.map((o: any) => o._id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
mt={theme.spacing(2)}
|
||||
>
|
||||
{notificationChannels.map((notificationId) => {
|
||||
const option = notificationOptions.find(
|
||||
(o: any) => o._id === notificationId
|
||||
);
|
||||
if (!option) return null;
|
||||
return (
|
||||
<Stack
|
||||
width={"100%"}
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
key={notificationId}
|
||||
>
|
||||
<Typography>{option.name}</Typography>
|
||||
<DeleteOutlineRoundedIcon
|
||||
onClick={() => {
|
||||
const updated = notificationChannels.filter(
|
||||
(id) => id !== notificationId
|
||||
);
|
||||
setValue("notificationChannels", updated);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("createMonitorPage.intervalTitle")}
|
||||
subtitle="How often to check the URL"
|
||||
rightContent={
|
||||
<Controller
|
||||
name="interval"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("createMonitorPage.intervalDescription")}
|
||||
fullWidth
|
||||
error={!!errors.interval}
|
||||
helperText={errors.interval ? errors.interval.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
>
|
||||
{t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
import type { PaletteKey } from "./Theme/v2/theme";
|
||||
import type { PaletteKey } from "@/Utils/Theme/v2/theme";
|
||||
export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
|
||||
const paletteMap: Record<MonitorStatus, PaletteKey> = {
|
||||
up: "success",
|
||||
@@ -452,6 +452,24 @@ class MonitorController extends BaseController {
|
||||
SERVICE_NAME,
|
||||
"getAllGames"
|
||||
);
|
||||
|
||||
getGroupsByTeamId = this.asyncHandler(
|
||||
async (req, res) => {
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("Team ID is required");
|
||||
}
|
||||
|
||||
const groups = await this.monitorService.getGroupsByTeamId({ teamId });
|
||||
|
||||
return res.success({
|
||||
msg: "OK",
|
||||
data: groups,
|
||||
});
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getGroupsByTeamId"
|
||||
);
|
||||
}
|
||||
|
||||
export default MonitorController;
|
||||
|
||||
@@ -128,6 +128,15 @@ const MonitorSchema = mongoose.Schema(
|
||||
gameId: {
|
||||
type: String,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxLength: 50,
|
||||
default: null,
|
||||
set: function (value) {
|
||||
return value && value.trim() ? value.trim() : null;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -573,6 +573,21 @@ class MonitorModule {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getGroupsByTeamId = async ({ teamId }) => {
|
||||
try {
|
||||
const groups = await this.Monitor.distinct("group", {
|
||||
teamId: new this.ObjectId(teamId),
|
||||
group: { $ne: null, $ne: "" },
|
||||
});
|
||||
|
||||
return groups.filter(Boolean).sort();
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getGroupsByTeamId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorModule;
|
||||
|
||||
@@ -18,6 +18,7 @@ class MonitorRoutes {
|
||||
this.router.get("/team", this.monitorController.getMonitorsByTeamId);
|
||||
this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId);
|
||||
this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId);
|
||||
this.router.get("/team/groups", this.monitorController.getGroupsByTeamId);
|
||||
|
||||
// Uptime routes
|
||||
this.router.get("/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById);
|
||||
|
||||
@@ -267,6 +267,11 @@ class MonitorService {
|
||||
getAllGames = () => {
|
||||
return this.games;
|
||||
};
|
||||
|
||||
getGroupsByTeamId = async ({ teamId }) => {
|
||||
const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId });
|
||||
return groups;
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorService;
|
||||
|
||||
@@ -174,6 +174,7 @@ const createMonitorBodyValidation = joi.object({
|
||||
expectedValue: joi.string().allow(""),
|
||||
matchMethod: joi.string(),
|
||||
gameId: joi.string().allow(""),
|
||||
group: joi.string().max(50).trim().allow(null, "").optional(),
|
||||
});
|
||||
|
||||
const createMonitorsBodyValidation = joi.array().items(
|
||||
@@ -203,6 +204,7 @@ const editMonitorBodyValidation = joi.object({
|
||||
usage_temperature: joi.number(),
|
||||
}),
|
||||
gameId: joi.string(),
|
||||
group: joi.string().max(50).trim().allow(null, "").optional(),
|
||||
});
|
||||
|
||||
const pauseMonitorParamValidation = joi.object({
|
||||
|
||||
Reference in New Issue
Block a user