Merge branch 'develop' into v2-select-component

This commit is contained in:
karenvicent
2025-10-14 15:58:52 -04:00
23 changed files with 371 additions and 14 deletions

View File

@@ -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 = ({

View File

@@ -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 }) => {

View File

@@ -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";

View File

@@ -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");

View File

@@ -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";

View File

@@ -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,

View File

@@ -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";

View File

@@ -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;

View 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 };
};

View File

@@ -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>[] = [

View 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}
/>
);
};

View File

@@ -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 = () => {

View 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>
);
};

View File

@@ -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",

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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({