implement monitor filter
This commit is contained in:
1 parent
a40277be4a
commit
adbb4e7817
11 files changed
+276
-78
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import MuiBreadcrumbs from "@mui/material/Breadcrumbs";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { ChevronRight, Home } from "lucide-react";
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ interface SelectInputProps<T> extends Omit<SelectProps<T>, "label"> {
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const SelectInput = <T,>({ fieldLabel, required, ...props }: SelectInputProps<T>) => {
|
||||
export const SelectInput = <T,>({
|
||||
fieldLabel,
|
||||
required,
|
||||
...props
|
||||
}: SelectInputProps<T>) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const select = (
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Select, Checkbox } from "@/components/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { SelectChangeEvent } from "@mui/material/Select";
|
||||
import {
|
||||
UptimeMonitorTypes,
|
||||
MonitorStatuses,
|
||||
type UptimeMonitorType,
|
||||
type MonitorStatus,
|
||||
} from "@/types/monitor";
|
||||
|
||||
const formatValue = (value: string[], fallback: string) => {
|
||||
if (value.length === 0) {
|
||||
return fallback;
|
||||
}
|
||||
const capitalized = value.map(
|
||||
(item) => item.charAt(0).toUpperCase() + item.slice(1)
|
||||
);
|
||||
return capitalized.join(" | ");
|
||||
};
|
||||
|
||||
type HeaderFilterProps = {
|
||||
selectedTypes: UptimeMonitorType[];
|
||||
selectedStatuses: MonitorStatus[];
|
||||
onTypesChange: (types: UptimeMonitorType[]) => void;
|
||||
onStatusesChange: (statuses: MonitorStatus[]) => void;
|
||||
};
|
||||
|
||||
const coerceValue = <T extends string>(value: string | string[]): T[] => {
|
||||
if (typeof value === "string") {
|
||||
return value.split(",") as T[];
|
||||
}
|
||||
return value as T[];
|
||||
};
|
||||
|
||||
export const HeaderFilter = ({
|
||||
selectedTypes,
|
||||
selectedStatuses,
|
||||
onTypesChange,
|
||||
onStatusesChange,
|
||||
}: HeaderFilterProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleTypeChange = (event: SelectChangeEvent<UptimeMonitorType[]>) => {
|
||||
onTypesChange(coerceValue<UptimeMonitorType>(event.target.value));
|
||||
};
|
||||
|
||||
const handleStatusChange = (event: SelectChangeEvent<MonitorStatus[]>) => {
|
||||
onStatusesChange(coerceValue<MonitorStatus>(event.target.value));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={theme.spacing(4)} direction={{ xs: "column", sm: "row" }}>
|
||||
<Select
|
||||
fieldLabel="Monitor type"
|
||||
multiple
|
||||
value={selectedTypes}
|
||||
onChange={handleTypeChange}
|
||||
renderValue={(selected) => (
|
||||
<Typography>{formatValue(selected, "All Monitor Types")}</Typography>
|
||||
)}
|
||||
>
|
||||
{UptimeMonitorTypes.map((type) => (
|
||||
<MenuItem key={type} value={type}>
|
||||
<Checkbox checked={selectedTypes.includes(type)} />
|
||||
<Typography textTransform={"capitalize"}>{type}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
fieldLabel="Monitor status"
|
||||
multiple
|
||||
value={selectedStatuses}
|
||||
onChange={handleStatusChange}
|
||||
renderValue={(selected) => (
|
||||
<Typography>
|
||||
{formatValue(selected, "All Monitor Statuses")}
|
||||
</Typography>
|
||||
)}
|
||||
>
|
||||
{MonitorStatuses.map((status) => (
|
||||
<MenuItem key={status} value={status}>
|
||||
<Checkbox checked={selectedStatuses.includes(status)} />
|
||||
<Typography textTransform={"capitalize"}>{status}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderFilter;
|
||||
@@ -7,3 +7,5 @@ export { ChartResponseTime } from "./ChartResponseTime";
|
||||
export { HistogramResponseTime } from "./HistogramResponseTime";
|
||||
export { HistogramPageSpeed } from "./HistogramPageSpeed";
|
||||
export { HistogramInfrastructure } from "./HistogramInfrastructure";
|
||||
|
||||
export { HeaderFilter } from "./HeaderFilter";
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PausedStatusBox,
|
||||
InfoBox,
|
||||
} from "@/components/design-elements";
|
||||
import { HeaderCreate } from "@/components/monitors";
|
||||
import { HeaderCreate, HeaderFilter } from "@/components/monitors";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Dialog } from "@/components/inputs";
|
||||
import { MonitorTable } from "@/pages/uptime/MonitorTable";
|
||||
@@ -14,7 +14,14 @@ import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useGet } from "@/hooks/UseApi";
|
||||
import type { ApiResponse } from "@/hooks/UseApi";
|
||||
import type { IMonitor, IMonitorWithStats } from "@/types/monitor";
|
||||
import {
|
||||
type IMonitor,
|
||||
type IMonitorWithStats,
|
||||
type MonitorStatus,
|
||||
type UptimeMonitorType,
|
||||
UptimeMonitorTypes,
|
||||
MonitorStatuses,
|
||||
} from "@/types/monitor";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useState } from "react";
|
||||
import { useDelete } from "@/hooks/UseApi";
|
||||
@@ -29,12 +36,45 @@ const UptimeMonitors = () => {
|
||||
const isDialogOpen = Boolean(selectedMonitor);
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [selectedTypes, setSelectedTypes] = useState<UptimeMonitorType[]>([
|
||||
"http",
|
||||
"https",
|
||||
"ping",
|
||||
]);
|
||||
const [selectedStatuses, setSelectedStatuses] = useState<MonitorStatus[]>([
|
||||
"up",
|
||||
"down",
|
||||
"paused",
|
||||
"initializing",
|
||||
]);
|
||||
|
||||
const { deleteFn } = useDelete();
|
||||
|
||||
const typeFilter =
|
||||
selectedTypes.length > 0 ? selectedTypes : UptimeMonitorTypes;
|
||||
const typeQuery = typeFilter
|
||||
.map((type: UptimeMonitorType) => `type=${encodeURIComponent(type)}`)
|
||||
.join("&");
|
||||
|
||||
const statusFilter =
|
||||
selectedStatuses.length > 0 ? selectedStatuses : MonitorStatuses;
|
||||
const statusQuery = statusFilter
|
||||
.map((status) => `status=${encodeURIComponent(status)}`)
|
||||
.join("&");
|
||||
|
||||
const requestParams = [
|
||||
"embedChecks=true",
|
||||
typeQuery,
|
||||
statusQuery,
|
||||
`page=${page}`,
|
||||
`rowsPerPage=${rowsPerPage}`,
|
||||
].filter(Boolean);
|
||||
const monitorRequestUrl = `/monitors?${requestParams.join("&")}`;
|
||||
|
||||
const { response, loading, isValidating, error, refetch } = useGet<
|
||||
ApiResponse<IMonitorWithStats>
|
||||
>(
|
||||
`/monitors?embedChecks=true&type=http&type=https&type=ping&page=${page}&rowsPerPage=${rowsPerPage}`,
|
||||
monitorRequestUrl,
|
||||
{},
|
||||
{
|
||||
refreshInterval: GLOBAL_REFRESH,
|
||||
@@ -62,11 +102,21 @@ const UptimeMonitors = () => {
|
||||
setSelectedMonitor(null);
|
||||
};
|
||||
|
||||
const hasActiveFilters =
|
||||
selectedTypes.length > 0 || selectedStatuses.length > 0;
|
||||
const showFallback = monitors.length === 0 && !hasActiveFilters;
|
||||
|
||||
const monitorItems = showFallback
|
||||
? []
|
||||
: monitors.length > 0
|
||||
? monitors
|
||||
: [{}];
|
||||
|
||||
return (
|
||||
<MonitorBasePageWithStates
|
||||
loading={isValidating}
|
||||
error={error}
|
||||
items={monitors}
|
||||
items={monitorItems}
|
||||
page="uptime"
|
||||
actionLink="/uptime/create"
|
||||
>
|
||||
@@ -80,6 +130,12 @@ const UptimeMonitors = () => {
|
||||
<DownStatusBox n={downCount} />
|
||||
<PausedStatusBox n={pausedCount} />
|
||||
</Stack>
|
||||
<HeaderFilter
|
||||
selectedTypes={selectedTypes}
|
||||
selectedStatuses={selectedStatuses}
|
||||
onTypesChange={setSelectedTypes}
|
||||
onStatusesChange={setSelectedStatuses}
|
||||
/>
|
||||
<MonitorTable
|
||||
monitors={monitors}
|
||||
refetch={refetch}
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import type { GroupedCheck, ICheck } from "@/types/check";
|
||||
import type { IMonitorStats } from "./monitorStats";
|
||||
|
||||
export type MonitorStatus = "up" | "down" | "initializing";
|
||||
export const MonitorTypes = [
|
||||
"http",
|
||||
"https",
|
||||
"ping",
|
||||
"infrastructure",
|
||||
"pagespeed",
|
||||
] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const UptimeMonitorTypes = ["http", "https", "ping"] as const;
|
||||
export type UptimeMonitorType = (typeof UptimeMonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = [
|
||||
"up",
|
||||
"down",
|
||||
"paused",
|
||||
"initializing",
|
||||
] as const;
|
||||
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export interface IMonitor {
|
||||
checks: ICheck[];
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T06:49:20.562Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T22:54:43.678Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T22:59:49.455Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:14:29.150Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:15:18.426Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:15:41.182Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:15:49.937Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:16:16.327Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:16:38.020Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:16:55.490Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:17:15.411Z"}
|
||||
{"level":"info","message":"Translations loaded from files successfully","method":"loadFromFiles","service":"TranslationService","timestamp":"2025-11-16T23:17:34.412Z"}
|
||||
Generated
-60
@@ -2924,34 +2924,6 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/transform": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
|
||||
"integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.27.4",
|
||||
"@jest/types": "30.2.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"babel-plugin-istanbul": "^7.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jest-haste-map": "30.2.0",
|
||||
"jest-regex-util": "30.0.1",
|
||||
"jest-util": "30.2.0",
|
||||
"micromatch": "^4.0.8",
|
||||
"pirates": "^4.0.7",
|
||||
"slash": "^3.0.0",
|
||||
"write-file-atomic": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/types": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
|
||||
@@ -4231,20 +4203,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-jest-hoist": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz",
|
||||
"integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/babel__core": "^7.20.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-current-node-syntax": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
|
||||
@@ -4272,24 +4230,6 @@
|
||||
"@babel/core": "^7.0.0 || ^8.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-jest": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz",
|
||||
"integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"babel-plugin-jest-hoist": "30.2.0",
|
||||
"babel-preset-current-node-syntax": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.11.0 || ^8.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
||||
@@ -79,12 +79,14 @@ class MonitorController {
|
||||
const page = req.validatedQuery.page || 0;
|
||||
const rowsPerPage = req.validatedQuery.rowsPerPage || 10;
|
||||
const type = req.validatedQuery.type;
|
||||
const status = req.validatedQuery.status;
|
||||
|
||||
result = await this.monitorService.getAllEmbedChecks(
|
||||
teamId,
|
||||
page,
|
||||
rowsPerPage,
|
||||
type
|
||||
type,
|
||||
status
|
||||
);
|
||||
} else {
|
||||
result = await this.monitorService.getAll(teamId);
|
||||
|
||||
@@ -6,6 +6,58 @@ import { IJobQueue } from "@/services/infrastructure/JobQueue.js";
|
||||
import { MonitorWithChecksResponse } from "@/types/index.js";
|
||||
import { MonitorStatus, MonitorType } from "@/db/models/monitors/Monitor.js";
|
||||
|
||||
const buildStatusCriteria = (
|
||||
statuses: MonitorStatus[]
|
||||
): Record<string, unknown> => {
|
||||
if (!Array.isArray(statuses) || statuses.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const uniqueStatuses = Array.from(new Set(statuses)) as MonitorStatus[];
|
||||
const includesPaused = uniqueStatuses.includes("paused");
|
||||
const activeStatuses = uniqueStatuses.filter((status) => status !== "paused");
|
||||
|
||||
if (includesPaused && activeStatuses.length > 0) {
|
||||
return {
|
||||
$or: [
|
||||
{ status: { $in: activeStatuses }, isActive: true },
|
||||
{ isActive: false },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (includesPaused) {
|
||||
return { isActive: false };
|
||||
}
|
||||
|
||||
return {
|
||||
status: { $in: uniqueStatuses },
|
||||
isActive: true,
|
||||
};
|
||||
};
|
||||
|
||||
const buildMatchConditions = (
|
||||
teamIdValue: mongoose.Types.ObjectId | string,
|
||||
types: MonitorType[],
|
||||
statusCriteria: Record<string, unknown>
|
||||
): Record<string, unknown> => {
|
||||
const conditions: Record<string, unknown> = { teamId: teamIdValue };
|
||||
|
||||
if (Array.isArray(types) && types.length > 0) {
|
||||
conditions.type = { $in: types };
|
||||
}
|
||||
|
||||
if (statusCriteria && Object.keys(statusCriteria).length > 0) {
|
||||
if ("$or" in statusCriteria) {
|
||||
conditions.$or = (statusCriteria as { $or: unknown }).$or;
|
||||
} else {
|
||||
Object.assign(conditions, statusCriteria);
|
||||
}
|
||||
}
|
||||
|
||||
return conditions;
|
||||
};
|
||||
|
||||
const SERVICE_NAME = "MonitorService";
|
||||
|
||||
export interface IMonitorService {
|
||||
@@ -20,7 +72,8 @@ export interface IMonitorService {
|
||||
teamId: string,
|
||||
page: number,
|
||||
limit: number,
|
||||
type: MonitorType[]
|
||||
type: MonitorType[],
|
||||
status: MonitorStatus[]
|
||||
) => Promise<{
|
||||
count: number;
|
||||
upCount: number;
|
||||
@@ -88,14 +141,20 @@ class MonitorService implements IMonitorService {
|
||||
teamId: string,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
type: MonitorType[] = []
|
||||
type: MonitorType[] = [],
|
||||
status: MonitorStatus[] = []
|
||||
) => {
|
||||
const teamObjectId = new mongoose.Types.ObjectId(teamId);
|
||||
const statusCriteria = buildStatusCriteria(status);
|
||||
const matchConditions = buildMatchConditions(
|
||||
teamObjectId,
|
||||
type,
|
||||
statusCriteria
|
||||
);
|
||||
|
||||
const countResult = await Monitor.aggregate([
|
||||
{
|
||||
$match: {
|
||||
teamId: new mongoose.Types.ObjectId(teamId),
|
||||
type: { $in: type },
|
||||
},
|
||||
$match: matchConditions,
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
@@ -118,11 +177,13 @@ class MonitorService implements IMonitorService {
|
||||
};
|
||||
|
||||
const skip = page * rowsPerPage;
|
||||
let find = {};
|
||||
if (type.length > 0) find = { type: { $in: type } };
|
||||
find = { ...find, teamId };
|
||||
const findConditions = buildMatchConditions(
|
||||
teamObjectId,
|
||||
type,
|
||||
statusCriteria
|
||||
);
|
||||
|
||||
const monitors = await Monitor.find(find)
|
||||
const monitors = await Monitor.find(findConditions)
|
||||
.lean()
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage);
|
||||
|
||||
@@ -128,6 +128,13 @@ export const monitorAllEmbedChecksQuerySchema = z.object({
|
||||
if (!val) return [];
|
||||
return Array.isArray(val) ? val : [val];
|
||||
}),
|
||||
status: z
|
||||
.union([z.string().optional(), z.string().optional().array()])
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (!val) return [];
|
||||
return Array.isArray(val) ? val : [val];
|
||||
}),
|
||||
page: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
Reference in new issue
Block a user