This commit is contained in:
Alex Holliday committed 2026-01-23 23:54:58 +00:00
1 parent c2dece862c
commit a0a8ceb0db
27 files changed
+2326 -94

No files matched your search

@@ -0,0 +1,276 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import {
Table,
type Header,
Pagination,
StatusLabel,
} from "@/Components/v2/design-elements";
import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/v2/common";
import { ActionsMenu } from "@/Components/v2/actions-menu";
import { ArrowDown, ArrowUp } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { usePost } from "@/Hooks/UseApi";
import type { Check } from "@/Types/Check";
import type { Monitor } from "@/Types/Monitor";
import type { ActionMenuItem } from "@/Components/v2/actions-menu";
// import type { ChartType } from "@/features/uiSlice";
export const MonitorTable = ({
monitors,
refetch,
// setSelectedMonitor,
// sortField,
// setSortField,
// sortOrder,
// setSortOrder,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
monitors: Monitor[];
refetch: Function;
// setSelectedMonitor: Function;
// sortField: string;
// setSortField: (field: string) => void;
// sortOrder: "asc" | "desc";
// setSortOrder: (order: "asc" | "desc") => void;
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
const chartType = "histogram";
const {
post,
// loading: isPosting,
// error: postError,
} = usePost<any, Monitor>();
// const handlePageChange = (
// _e: React.MouseEvent<HTMLButtonElement> | null,
// newPage: number
// ) => {
// setPage(newPage);
// };
// const handleRowsPerPageChange = (
// e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
// ) => {
// const value = Number(e.target.value);
// setPage(0);
// setRowsPerPage(value);
// };
// const handleSort = (e: any, field: string) => {
// e.preventDefault();
// e.stopPropagation();
// if (sortField === field) {
// const newOrder = sortOrder === "asc" ? "desc" : "asc";
// setSortOrder(newOrder);
// } else {
// setSortField(field);
// setSortOrder("asc");
// }
// refetch();
// };
const getActions = (monitor: Monitor): ActionMenuItem[] => {
return [
{
id: 1,
label: t("pages.common.monitors.actions.openSite"),
action: () => {
window.open(monitor.url, "_blank", "noreferrer");
},
closeMenu: true,
},
{
id: 2,
label: t("pages.common.monitors.actions.details"),
action: () => {
navigate(`${monitor.id}`);
},
},
{
id: 3,
label: t("pages.common.monitors.actions.incidents"),
action: () => {
navigate(`/incidents?monitorId=${monitor.id}`);
},
},
{
id: 4,
label: t("pages.common.monitors.actions.configure"),
action: () => {
navigate(`/uptime/create/${monitor.id}`);
},
},
// {
// id: 5,
// label: "Clone",
// action: () => {
// },
// },
{
id: 6,
label:
monitor.isActive === false
? t("pages.common.monitors.actions.resume")
: t("pages.common.monitors.actions.pause"),
action: async () => {
await post(`/monitors/pause/${monitor.id}`, {});
refetch();
},
closeMenu: true,
},
{
id: 7,
label: (
<Typography color={theme.palette.error.main}>
{t("pages.common.monitors.actions.delete")}
</Typography>
),
action: () => {
// setSelectedMonitor(monitor);
},
closeMenu: true,
},
];
};
const getHeaders = (chartType: string) => {
const renderSortIcon = (isActive: boolean) => (
<Box
width={16}
display="inline-flex"
justifyContent="center"
>
{/* {isActive ? (
sortOrder === "asc" ? (
<ArrowUp size={16} />
) : (
<ArrowDown size={16} />
)
) : null} */}
</Box>
);
const headers: Header<Monitor>[] = [
{
id: "name",
content: (
<Stack
gap={theme.spacing(4)}
direction={"row"}
alignItems={"center"}
// onClick={(e) => handleSort(e, "name")}
sx={{ cursor: "pointer" }}
>
{t("common.table.headers.name")}
{/* {renderSortIcon(sortField === "name")} */}
</Stack>
),
render: (row) => {
return row?.name;
},
},
{
id: "status",
content: (
<Stack
gap={theme.spacing(4)}
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
// onClick={(e) => handleSort(e, "status")}
sx={{ cursor: "pointer" }}
>
{t("common.table.headers.status")}
{/* {renderSortIcon(sortField === "status")} */}
</Stack>
),
render: (row) => {
return (
<StatusLabel
status={row.status}
isActive={row.isActive}
/>
);
},
},
{
id: "histogram",
content: t("pages.uptime.table.headers.responseTime"),
render: (row) => {
if (chartType === "histogram") {
return <HistogramResponseTime checks={row.checks} />;
} else {
return <HeatmapResponseTime checks={row.checks} />;
}
},
},
{
id: "type",
content: (
<Stack
gap={theme.spacing(4)}
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
// onClick={(e) => handleSort(e, "type")}
sx={{ cursor: "pointer" }}
>
{t("common.table.headers.type")}
{/* {renderSortIcon(sortField === "type")} */}
</Stack>
),
render: (row) => {
return row.type;
},
},
{
id: "actions",
content: t("common.table.headers.actions"),
render: (row) => {
return <ActionsMenu items={getActions(row)} />;
},
},
];
return headers;
};
const headers = getHeaders(chartType);
return (
<Box>
<Table
headers={headers}
data={monitors}
onRowClick={(row) => {
navigate(`/uptime/${row.id}`);
}}
/>
<Pagination
component="div"
count={count}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={(_e, newPage) => setPage(newPage)}
onRowsPerPageChange={(e) => setRowsPerPage(Number(e.target.value))}
/>
</Box>
);
};
+125 -27
View File
@@ -1,39 +1,86 @@
import { useGet } from "@/Hooks/UseApi";
import { MonitorBasePageWithStates } from "@/Components/v2/design-elements";
import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor";
import { FilterControls } from "@/Components/v2/monitors";
import { useState, useMemo } from "react";
import { useSelector } from "react-redux";
import {
MonitorBasePageWithStates,
UpStatusBox,
DownStatusBox,
PausedStatusBox,
} from "@/Components/v2/design-elements";
import { TextField } from "@/Components/v2/inputs";
import Stack from "@mui/material/Stack";
import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable";
import { useTranslation } from "react-i18next";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useGet } from "@/Hooks/UseApi";
import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor";
import { useState, useMemo, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { setRowsPerPage } from "@/Features/UI/uiSlice.js";
import type { RootState } from "@/Types/state";
import { useTheme } from "@mui/material";
const UptimeMonitorsPage = () => {
const { t } = useTranslation();
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const dispatch = useDispatch();
// Redux state
const rowsPerPage = useSelector((state: RootState) => state.ui.monitors.rowsPerPage);
const rowsPerPage = useSelector(
(state: RootState) => state.ui?.monitors?.rowsPerPage ?? 10
);
// Local state
const [selectedTypes, setSelectedTypes] = useState<MonitorType[]>([]);
const [selectedStatus, setSelectedStatus] = useState<string>("");
const [selectedState, setSelectedState] = useState<string>("");
const [page, setPage] = useState(0);
const [search, setSearch] = useState("");
const [sort, setSort] = useState<
{ field: string; order: "asc" | "desc" } | undefined
>();
const [page, setPage] = useState<number>(0);
const [search, setSearch] = useState<string>("");
const [sort, setSort] = useState<{ field: string; order: "asc" | "desc" } | undefined>(
undefined
);
// Convert filter selections to API filter values
// Status: "up" -> true, "down" -> false
// State: "active" -> true, "paused" -> false
const toFilterStatus = useMemo(() => {
if (selectedStatus === "up") return "true";
if (selectedStatus === "down") return "false";
return undefined;
}, [selectedStatus]);
const toFilterActive = useMemo(() => {
if (selectedState === "active") return "true";
if (selectedState === "paused") return "false";
return undefined;
}, [selectedState]);
// Determine field and filter for the API request
// Priority: status > isActive > search > sort
const filterLookup = new Map<string | undefined, string>([
[toFilterStatus, "status"],
[toFilterActive, "isActive"],
]);
const activeFilter = [...filterLookup].find(([key]) => key !== undefined);
const field = activeFilter?.[1] || (search ? "name" : sort?.field);
const filter = activeFilter?.[0] || search;
// Default to all types when none selected
const effectiveTypes =
selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port"];
// Build URL for monitors with checks
const monitorsWithChecksUrl = useMemo(() => {
const params = new URLSearchParams();
const types =
selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port"];
types.forEach((type) => params.append("type", type));
effectiveTypes.forEach((type) => params.append("type", type));
params.append("limit", "25");
if (page) params.append("page", String(page));
if (page !== undefined) params.append("page", String(page));
if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage));
if (search) params.append("filter", search);
if (sort?.field) params.append("field", sort.field);
if (filter) params.append("filter", filter);
if (field) params.append("field", field);
if (sort?.order) params.append("order", sort.order);
return `/monitors/team/with-checks?${params.toString()}`;
}, [selectedTypes, page, rowsPerPage, search, sort]);
}, [effectiveTypes, page, rowsPerPage, filter, field, sort?.order]);
// Data fetching
const {
@@ -46,10 +93,23 @@ const UptimeMonitorsPage = () => {
data: monitorsWithChecksData,
isLoading: monitorsWithChecksLoading,
error: monitorsWithChecksError,
} = useGet<MonitorsWithChecksResponse>(monitorsWithChecksUrl);
refetch,
} = useGet<MonitorsWithChecksResponse>(
monitorsWithChecksUrl,
{},
{ refreshInterval: 5000 }
);
const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {};
// Handlers
const handleClearFilters = useCallback(() => {
setSelectedTypes([]);
setSelectedStatus("");
setSelectedState("");
setSearch("");
}, []);
return (
<MonitorBasePageWithStates
loading={isLoading || monitorsWithChecksLoading}
@@ -58,15 +118,53 @@ const UptimeMonitorsPage = () => {
page="uptime"
actionLink="/uptime/create"
>
<FilterControls
selectedTypes={selectedTypes}
setSelectedTypes={setSelectedTypes}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
<Stack
direction={isSmall ? "column" : "row"}
gap={theme.spacing(8)}
>
<UpStatusBox n={summary?.upMonitors || 0} />
<DownStatusBox n={summary?.downMonitors || 0} />
<PausedStatusBox n={summary?.pausedMonitors || 0} />
</Stack>
<Stack
direction="row"
justifyContent={"space-between"}
>
<FilterControls
selectedTypes={selectedTypes}
setSelectedTypes={setSelectedTypes}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
onClearFilters={handleClearFilters}
/>
<TextField
placeholder={t("pages.uptime.filters.search.placeholder")}
value={search}
onChange={(event) => {
setSearch(event.target.value);
}}
/>
</Stack>
<MonitorTable
monitors={monitorsWithChecks || []}
refetch={refetch}
count={count || 0}
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
setRowsPerPage={(e: any) => {
dispatch(
setRowsPerPage({
value: parseInt(e.target.value, 10),
table: "monitors",
})
);
setPage(0);
}}
/>
<pre>{JSON.stringify(monitors, null, 2)}</pre>
</MonitorBasePageWithStates>
);
};