mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-21 00:48:45 -05:00
v2 infra migration
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { getInfraGaugeColor } from "@/Utils/MonitorUtils";
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
|
||||
export const Gauge = ({
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
progress?: number;
|
||||
radius?: number;
|
||||
strokeWidth?: number;
|
||||
precision?: number;
|
||||
unit?: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
|
||||
|
||||
// Calculate the length of the stroke for the circle
|
||||
const { circumference, totalSize, strokeLength } = useMemo(
|
||||
() => ({
|
||||
circumference: 2 * Math.PI * radius,
|
||||
totalSize: radius * 2 + strokeWidth * 2,
|
||||
strokeLength: (progress / 100) * (2 * Math.PI * radius),
|
||||
}),
|
||||
[radius, strokeWidth, progress]
|
||||
);
|
||||
|
||||
const [offset, setOffset] = useState(circumference);
|
||||
useEffect(() => {
|
||||
setOffset(circumference);
|
||||
const timer = setTimeout(() => {
|
||||
setOffset(circumference - strokeLength);
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [progress, circumference, strokeLength]);
|
||||
|
||||
const fillColor = getInfraGaugeColor(progressWithinRange, theme);
|
||||
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={"inline-block"}
|
||||
position={"relative"}
|
||||
width={radius}
|
||||
height={radius}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
borderRadius={"50%"}
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${totalSize} ${totalSize}`}
|
||||
width={radius}
|
||||
height={radius}
|
||||
>
|
||||
<circle
|
||||
stroke={theme.palette.secondary.main}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
cx={totalSize / 2}
|
||||
cy={totalSize / 2}
|
||||
r={radius}
|
||||
/>
|
||||
<circle
|
||||
stroke={fillColor}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={offset}
|
||||
fill="none"
|
||||
cx={totalSize / 2}
|
||||
cy={totalSize / 2}
|
||||
r={radius}
|
||||
style={{
|
||||
transform: "rotate(-90deg)",
|
||||
transformOrigin: "center",
|
||||
transition: "stroke-dashoffset 1.5s ease-in-out",
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<Typography
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
{`${progressWithinRange.toFixed(precision)}${unit}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -15,3 +15,4 @@ export { default as Icon } from "./Icon";
|
||||
export * from "./Tooltip";
|
||||
export * from "./StatBox";
|
||||
export * from "./BaseChart";
|
||||
export * from "./Gauge";
|
||||
|
||||
@@ -11,6 +11,7 @@ const statuses = ["up", "down"];
|
||||
const states = ["active", "paused"];
|
||||
|
||||
export const ControlsFilter = ({
|
||||
showTypes = true,
|
||||
selectedTypes,
|
||||
setSelectedTypes,
|
||||
selectedStatus,
|
||||
@@ -19,8 +20,9 @@ export const ControlsFilter = ({
|
||||
setSelectedState,
|
||||
onClearFilters,
|
||||
}: {
|
||||
selectedTypes: MonitorType[];
|
||||
setSelectedTypes: React.Dispatch<React.SetStateAction<MonitorType[]>>;
|
||||
showTypes?: boolean;
|
||||
selectedTypes?: MonitorType[];
|
||||
setSelectedTypes?: React.Dispatch<React.SetStateAction<MonitorType[]>>;
|
||||
selectedStatus: string;
|
||||
setSelectedStatus: React.Dispatch<React.SetStateAction<string>>;
|
||||
selectedState: string;
|
||||
@@ -30,27 +32,29 @@ export const ControlsFilter = ({
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isFilterActive =
|
||||
selectedTypes.length > 0 || selectedStatus !== "" || selectedState !== "";
|
||||
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
|
||||
return (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Select
|
||||
multiple
|
||||
placeholder="Type"
|
||||
value={selectedTypes}
|
||||
onChange={(e) => setSelectedTypes(e.target.value as MonitorType[])}
|
||||
>
|
||||
{types.map((type) => (
|
||||
<MenuItem
|
||||
key={type}
|
||||
value={type}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{type}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{showTypes && setSelectedTypes && (
|
||||
<Select
|
||||
multiple
|
||||
placeholder="Type"
|
||||
value={selectedTypes ?? []}
|
||||
onChange={(e) => setSelectedTypes(e.target.value as MonitorType[])}
|
||||
>
|
||||
{types.map((type) => (
|
||||
<MenuItem
|
||||
key={type}
|
||||
value={type}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{type}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<Select
|
||||
placeholder="Status"
|
||||
value={selectedStatus}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
|
||||
import { useMemo } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Filter Component
|
||||
*
|
||||
* A high-level component that provides filtering options for status in Infrastructure Page.
|
||||
* It allows users to select multiple options for each filter and reset the filters.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string[]} props.selectedStatus - An array of selected status values.
|
||||
* @param {function} props.setSelectedStatus - A function to set the selected status values.
|
||||
* @param {function} props.setToFilterStatus - A function to set the filter status based on selected status values.
|
||||
* @param {function} props.handleReset - A function to reset all filters.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered Filter component.
|
||||
*/
|
||||
|
||||
const statusOptions = [
|
||||
{ value: "Up", label: "Up" },
|
||||
{ value: "Down", label: "Down" },
|
||||
];
|
||||
|
||||
const Filter = ({
|
||||
selectedStatus,
|
||||
setSelectedStatus,
|
||||
setToFilterStatus,
|
||||
handleReset,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleStatusChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined);
|
||||
|
||||
if (selectedValues.length === 0 || selectedValues.length === 2) {
|
||||
setToFilterStatus(undefined);
|
||||
} else {
|
||||
setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false");
|
||||
}
|
||||
};
|
||||
|
||||
const isFilterActive = useMemo(() => {
|
||||
return (selectedStatus?.length ?? 0) > 0;
|
||||
}, [selectedStatus]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
m: theme.spacing(2),
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<FilterHeader
|
||||
header={t("status")}
|
||||
options={statusOptions}
|
||||
value={selectedStatus}
|
||||
onChange={handleStatusChange}
|
||||
/>
|
||||
<Button
|
||||
color={theme.palette.primary.contrastText}
|
||||
onClick={handleReset}
|
||||
variant="contained"
|
||||
endIcon={
|
||||
<Icon
|
||||
name="X"
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
visibility: isFilterActive ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Filter.propTypes = {
|
||||
selectedStatus: PropTypes.arrayOf(PropTypes.string),
|
||||
setSelectedStatus: PropTypes.func.isRequired,
|
||||
setToFilterStatus: PropTypes.func.isRequired,
|
||||
handleReset: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
@@ -0,0 +1,275 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table, Pagination, StatusLabel, Gauge } from "@/Components/v2/design-elements";
|
||||
import type { Header } from "@/Components/v2/design-elements/Table";
|
||||
import { ActionsMenu, type ActionMenuItem } from "@/Components/v2/actions-menu";
|
||||
import { ArrowUp, ArrowDown } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
export const InfraMonitorsTable = ({
|
||||
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 isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
post,
|
||||
// loading: isPatching,
|
||||
// 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(`/infrastructure/configure/${monitor.id}`);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: 5,
|
||||
// label: "Clone",
|
||||
// action: () => {
|
||||
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 6,
|
||||
label:
|
||||
monitor.isActive === false
|
||||
? t("common.buttons.resume")
|
||||
: t("common.buttons.pause"),
|
||||
action: async () => {
|
||||
await post(`/monitors/pause/${monitor.id}`, {});
|
||||
refetch();
|
||||
},
|
||||
closeMenu: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: (
|
||||
<Typography color={theme.palette.error.main}>
|
||||
{t("common.buttons.delete")}
|
||||
</Typography>
|
||||
),
|
||||
action: () => {
|
||||
setSelectedMonitor(monitor);
|
||||
},
|
||||
closeMenu: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getHeaders = () => {
|
||||
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: (
|
||||
<Typography
|
||||
component="div"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={(e) => handleSort(e, "name")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("common.table.headers.name")}
|
||||
{renderSortIcon(sortField === "name")}
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return row.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: (
|
||||
<Typography
|
||||
component="div"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={(e) => handleSort(e, "status")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("common.table.headers.status")}
|
||||
{renderSortIcon(sortField === "status")}
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
isActive={row.isActive}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "cpu",
|
||||
content: t("pages.infrastructure.table.headers.cpu"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
const cpuUsage = (check?.cpu?.usage_percent || 0) * 100;
|
||||
return <Gauge progress={cpuUsage} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "memory",
|
||||
content: t("pages.infrastructure.table.headers.memory"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
const memoryUsage = (check?.memory?.usage_percent || 0) * 100;
|
||||
return <Gauge progress={memoryUsage} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "disk",
|
||||
content: t("pages.infrastructure.table.headers.disk"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
|
||||
const totalDiskUsage = check?.disk?.reduce(
|
||||
(acc, disk) => acc + (disk?.usage_percent || 0),
|
||||
0
|
||||
);
|
||||
const diskCount = check?.disk?.length || 1;
|
||||
const diskUsage = ((totalDiskUsage || 0) / diskCount) * 100;
|
||||
return <Gauge progress={diskUsage} />;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
content: t("common.table.headers.actions"),
|
||||
render: (row) => {
|
||||
return <ActionsMenu items={getActions(row)} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
};
|
||||
|
||||
let headers = getHeaders();
|
||||
|
||||
if (isSmall) {
|
||||
headers = headers.filter((h) => h.id !== "histogram");
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Table
|
||||
headers={headers}
|
||||
data={monitors}
|
||||
onRowClick={(row) => {
|
||||
navigate(`/infrastructure/${row.id}`);
|
||||
}}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
count={count}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
onRowsPerPageChange={handleRowsPerPageChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,162 +0,0 @@
|
||||
// Components
|
||||
import { Box } from "@mui/material";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Host from "@/Components/v1/Host/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import { Stack } from "@mui/material";
|
||||
import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx";
|
||||
import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx";
|
||||
// Assets
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const MonitorsTable = ({
|
||||
isLoading,
|
||||
monitors,
|
||||
isAdmin,
|
||||
handleActionMenuDelete,
|
||||
isSearching,
|
||||
}) => {
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { determineState } = useMonitorUtils();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Handlers
|
||||
const openDetails = (id) => {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
};
|
||||
const headers = [
|
||||
{
|
||||
id: "host",
|
||||
content: t("host"),
|
||||
render: (row) => (
|
||||
<Host
|
||||
title={row.name}
|
||||
url={row.url}
|
||||
percentage={row.uptimePercentage}
|
||||
percentageColor={row.percentageColor}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("incidentsTableStatus"),
|
||||
render: (row) => (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
text={row.status}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "frequency",
|
||||
content: t("frequency"),
|
||||
render: (row) => (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
gap=".25rem"
|
||||
>
|
||||
<Icon
|
||||
name="Cpu"
|
||||
size={20}
|
||||
/>
|
||||
{row.processor}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: "cpu", content: t("cpu"), render: (row) => <CustomGauge progress={row.cpu} /> },
|
||||
{
|
||||
id: "memory",
|
||||
content: t("memory"),
|
||||
render: (row) => <CustomGauge progress={row.mem} />,
|
||||
},
|
||||
{
|
||||
id: "disk",
|
||||
content: t("disk"),
|
||||
render: (row) => <CustomGauge progress={row.disk} />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: t("actions"),
|
||||
render: (row) => (
|
||||
<InfrastructureMenu
|
||||
monitor={row}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const data = monitors?.map((monitor) => {
|
||||
const processor =
|
||||
((monitor.recentChecks?.[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz";
|
||||
const cpu = (monitor?.recentChecks?.[0]?.cpu?.usage_percent ?? 0) * 100;
|
||||
const mem = (monitor?.recentChecks?.[0]?.memory?.usage_percent ?? 0) * 100;
|
||||
const disk = (monitor?.recentChecks?.[0]?.disk?.[0]?.usage_percent ?? 0) * 100;
|
||||
const status = determineState(monitor);
|
||||
const percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.error.main
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.warning.main
|
||||
: theme.palette.success.main;
|
||||
|
||||
return {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
processor,
|
||||
cpu,
|
||||
mem,
|
||||
disk,
|
||||
status,
|
||||
percentageColor,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<LoadingSpinner shouldRender={isSearching} />
|
||||
<DataTable
|
||||
shouldRender={!isLoading}
|
||||
headers={headers}
|
||||
data={data}
|
||||
config={{
|
||||
/* TODO this behavior seems to be repeated. Put it on the root table? */
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover td": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => openDetails(row.id),
|
||||
emptyView: "No monitors found",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorsTable.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitors: PropTypes.array,
|
||||
isAdmin: PropTypes.bool,
|
||||
handleActionMenuDelete: PropTypes.func,
|
||||
isSearching: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MonitorsTable;
|
||||
@@ -1,196 +0,0 @@
|
||||
/* TODO I basically copied and pasted this component from the actionsMenu. Check how we can make it reusable */
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "@/Utils/toastUtils.jsx";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import PropTypes from "prop-types";
|
||||
import Dialog from "@/Components/v1/Dialog/index.jsx";
|
||||
import { networkService } from "@/Utils/NetworkService.js";
|
||||
import { usePauseMonitor } from "@/Hooks/monitorHooks.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* InfrastructureMenu Component
|
||||
* Provides a dropdown menu for managing infrastructure monitors.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {Object} props.monitor - The monitor object containing details about the infrastructure monitor.
|
||||
* @param {string} props.monitor.id - Unique ID of the monitor.
|
||||
* @param {string} [props.monitor.url] - URL associated with the monitor.
|
||||
* @param {string} props.monitor.type - Type of monitor (e.g., uptime, infrastructure).
|
||||
* @param {boolean} props.monitor.isActive - Indicates if the monitor is currently active (true) or paused (false).
|
||||
* @param {boolean} props.isAdmin - Whether the user has admin privileges.
|
||||
* @param {Function} props.updateCallback - Callback to trigger when the monitor data is updated.
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
const anchor = useRef(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
const [pauseMonitor] = usePauseMonitor();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const openMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openRemove = (e) => {
|
||||
closeMenu(e);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
const cancelRemove = () => {
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
function openDetails(id) {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
}
|
||||
|
||||
const openConfigure = (id) => {
|
||||
navigate(`/infrastructure/configure/${id}`);
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
// Pass updateCallback as triggerUpdate to the hook
|
||||
await pauseMonitor({ monitorId: monitor.id, triggerUpdate: updateCallback });
|
||||
// Toast is already displayed in the hook, no need to display it again
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
await networkService.deleteMonitorById({
|
||||
monitorId: monitor.id,
|
||||
});
|
||||
createToast({
|
||||
body: t("monitorActions.deleteSuccess"),
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({ body: t("monitorActions.deleteFailed") });
|
||||
} finally {
|
||||
setIsDialogOpen(false);
|
||||
updateCallback();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={openMenu}
|
||||
disabled={!isAdmin}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
ref={anchor}
|
||||
>
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchor.current}
|
||||
open={isOpen}
|
||||
onClose={closeMenu}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDetails(monitor.id);
|
||||
closeMenu(e);
|
||||
}}
|
||||
>
|
||||
{t("monitorActions.details")}
|
||||
</MenuItem>
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openConfigure(monitor.id);
|
||||
closeMenu(e);
|
||||
}}
|
||||
>
|
||||
{t("configure")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await handlePause();
|
||||
closeMenu(e);
|
||||
}}
|
||||
>
|
||||
{!monitor.isActive ? t("resume") : t("pause")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={openRemove}
|
||||
sx={{ color: theme.palette.error.main }}
|
||||
>
|
||||
{t("remove")}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
theme={theme}
|
||||
title={t("deleteDialogTitle")}
|
||||
description={t("deleteDialogDescription")}
|
||||
onCancel={cancelRemove}
|
||||
confirmationButtonLabel={t("delete")}
|
||||
onConfirm={handleRemove}
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InfrastructureMenu.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
// Note: type must remain optional. Making it required (type: PropTypes.string.isRequired)
|
||||
// causes runtime errors as some monitors don't have a defined type property
|
||||
type: PropTypes.string,
|
||||
isActive: PropTypes.bool, // Determines whether the monitor is paused (false) or active (true)
|
||||
status: PropTypes.string, // Represents the monitor's operational status (e.g., 'up', 'down', etc.)
|
||||
}).isRequired,
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
updateCallback: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { InfrastructureMenu };
|
||||
@@ -1,5 +1,200 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import {
|
||||
MonitorBasePageWithStates,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { HeaderCreate } from "@/Components/v2/common";
|
||||
import { ControlsFilter } from "@/Components/v2/monitors";
|
||||
import { TextField, Dialog } from "@/Components/v2/inputs";
|
||||
|
||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import type { Monitor, MonitorsWithChecksResponse } from "@/Types/Monitor";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { setRowsPerPage } from "@/Features/UI/uiSlice.js";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { InfraMonitorsTable } from "./Components/MonitorsTable";
|
||||
|
||||
const InfrastructureMonitors = () => {
|
||||
return null;
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isAdmin = useIsAdmin();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Redux state
|
||||
const rowsPerPage = useSelector(
|
||||
(state: RootState) => state.ui?.infrastructure?.rowsPerPage ?? 5
|
||||
);
|
||||
|
||||
// Local state
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>("");
|
||||
const [selectedState, setSelectedState] = useState<string>("");
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [sortField, setSortField] = useState<string>("");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
|
||||
const isDialogOpen = Boolean(selectedMonitor);
|
||||
|
||||
const handleClearFilters = useCallback(() => {
|
||||
setSelectedStatus("");
|
||||
setSelectedState("");
|
||||
setSearch("");
|
||||
}, []);
|
||||
|
||||
// Convert filter selections to API filter values
|
||||
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
|
||||
const filterLookup = new Map<string | undefined, string>([
|
||||
[toFilterStatus, "status"],
|
||||
[toFilterActive, "isActive"],
|
||||
]);
|
||||
const activeFilter = [...filterLookup].find(([key]) => key !== undefined);
|
||||
const field = activeFilter?.[1] || (search ? "name" : sortField || undefined);
|
||||
const filter = activeFilter?.[0] || search || undefined;
|
||||
|
||||
// Build URL for monitors with checks
|
||||
const monitorsWithChecksUrl = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("type", "hardware");
|
||||
params.append("limit", "1");
|
||||
if (page !== undefined) params.append("page", String(page));
|
||||
if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage));
|
||||
if (filter) params.append("filter", filter);
|
||||
if (field) params.append("field", field);
|
||||
if (sortOrder) params.append("order", sortOrder);
|
||||
return `/monitors/team/with-checks?${params.toString()}`;
|
||||
}, [page, rowsPerPage, filter, field, sortOrder]);
|
||||
|
||||
const {
|
||||
data: monitors,
|
||||
isLoading: monitorsLoading,
|
||||
error,
|
||||
refetch: refetchMonitors,
|
||||
} = useGet<Monitor[]>("/monitors/team?type=hardware", {}, { keepPreviousData: true });
|
||||
|
||||
const {
|
||||
data: monitorsWithChecksData,
|
||||
isLoading: monitorsWithChecksLoading,
|
||||
error: monitorsWithChecksError,
|
||||
refetch,
|
||||
} = useGet<MonitorsWithChecksResponse>(
|
||||
monitorsWithChecksUrl,
|
||||
{},
|
||||
{ refreshInterval: 5000, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { summary, count } = monitorsWithChecksData ?? {};
|
||||
const isLoading = monitorsLoading || monitorsWithChecksLoading;
|
||||
|
||||
// Delete hook
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedMonitor) return;
|
||||
await deleteFn(`/monitors/${selectedMonitor.id}`);
|
||||
setSelectedMonitor(null);
|
||||
refetch();
|
||||
refetchMonitors();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectedMonitor(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<MonitorBasePageWithStates
|
||||
loading={isLoading}
|
||||
error={error ?? monitorsWithChecksError}
|
||||
items={monitors || []}
|
||||
page="infrastructure"
|
||||
actionLink="/infrastructure/create"
|
||||
>
|
||||
<HeaderCreate
|
||||
path="/infrastructure/create"
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<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={isSmall ? "column" : "row"}
|
||||
justifyContent={isSmall ? "flex-start" : "space-between"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<ControlsFilter
|
||||
showTypes={false}
|
||||
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>
|
||||
<InfraMonitorsTable
|
||||
monitors={monitorsWithChecksData?.monitors || []}
|
||||
refetch={refetch}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
sortField={sortField}
|
||||
setSortField={setSortField}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
count={count || 0}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setRowsPerPage={(value: number) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value,
|
||||
table: "infrastructure",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
}}
|
||||
/>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
title={t("common.dialogs.delete.title")}
|
||||
content={t("common.dialogs.delete.description")}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</MonitorBasePageWithStates>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureMonitors;
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
|
||||
import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
|
||||
import MonitorsTable from "./Components/MonitorsTable/index.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
|
||||
import Filter from "./Components/Filters/index.jsx";
|
||||
import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/index.jsx";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setRowsPerPage } from "../../../Features/UI/uiSlice.js";
|
||||
// Constants
|
||||
const TYPES = ["hardware"];
|
||||
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
|
||||
|
||||
const InfrastructureMonitors = () => {
|
||||
// Redux state
|
||||
const rowsPerPage = useSelector((state) => state.ui?.infrastructure?.rowsPerPage ?? 5);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Local state
|
||||
const [page, setPage] = useState(0);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [selectedStatus, setSelectedStatus] = useState(undefined);
|
||||
const [toFilterStatus, setToFilterStatus] = useState(undefined);
|
||||
const [search, setSearch] = useState(undefined);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Handlers
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger(!updateTrigger);
|
||||
};
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "infrastructure",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearching) {
|
||||
setPage(0);
|
||||
}
|
||||
}, [isSearching]);
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedStatus(undefined);
|
||||
setToFilterStatus(undefined);
|
||||
};
|
||||
|
||||
const field = toFilterStatus !== undefined ? "status" : undefined;
|
||||
|
||||
const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({
|
||||
types: TYPES,
|
||||
limit: 1,
|
||||
page: page,
|
||||
field: field,
|
||||
filter: toFilterStatus ?? search,
|
||||
rowsPerPage: rowsPerPage,
|
||||
monitorUpdateTrigger: updateTrigger,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageStateWrapper
|
||||
networkError={networkError}
|
||||
isLoading={isLoading}
|
||||
items={monitors}
|
||||
type="infrastructureMonitor"
|
||||
fallbackLink="/infrastructure/create"
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
isAdmin={isAdmin}
|
||||
isLoading={isLoading}
|
||||
path="/infrastructure/create"
|
||||
/>
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
isLoading={isLoading}
|
||||
monitorCount={count || 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<MonitorsTable
|
||||
isLoading={isLoading}
|
||||
monitors={monitors}
|
||||
isAdmin={isAdmin}
|
||||
handleActionMenuDelete={handleActionMenuDelete}
|
||||
isSearching={isSearching}
|
||||
/>
|
||||
<Pagination
|
||||
itemCount={count || 0}
|
||||
paginationLabel={t("monitors")}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureMonitors;
|
||||
@@ -1,46 +0,0 @@
|
||||
import { CircularProgress, Box } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
const LoadingSpinner = ({ shouldRender }) => {
|
||||
const theme = useTheme();
|
||||
if (shouldRender === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
opacity: 0.8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height="100%"
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
sx={{
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.accent.main,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LoadingSpinner.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default LoadingSpinner;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import Search from "@/Components/v1/Inputs/Search/index.jsx";
|
||||
import { Box } from "@mui/material";
|
||||
import useDebounce from "../../Hooks/useDebounce.jsx";
|
||||
import { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SearchComponent = ({ monitors = [], onSearchChange, setIsSearching }) => {
|
||||
const isFirstRender = useRef(true);
|
||||
const [localSearch, setLocalSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(localSearch, 500);
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current === true) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
onSearchChange(debouncedSearch);
|
||||
setIsSearching(false);
|
||||
}, [debouncedSearch, onSearchChange, setIsSearching]);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setLocalSearch(value);
|
||||
setIsSearching(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
width="25%"
|
||||
minWidth={150}
|
||||
ml="auto"
|
||||
mt={2}
|
||||
>
|
||||
<Search
|
||||
options={monitors}
|
||||
filteredBy="name"
|
||||
inputValue={localSearch}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
SearchComponent.propTypes = {
|
||||
monitors: PropTypes.array,
|
||||
onSearchChange: PropTypes.func,
|
||||
setIsSearching: PropTypes.func,
|
||||
};
|
||||
|
||||
export default SearchComponent;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useDebounce = (value, delay) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
return debouncedValue;
|
||||
};
|
||||
|
||||
export default useDebounce;
|
||||
@@ -25,7 +25,7 @@ import PageSpeedDetails from "../Pages/PageSpeed/Details/";
|
||||
import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx";
|
||||
|
||||
// Infrastructure
|
||||
import Infrastructure from "../Pages/Infrastructure/Monitors/index.jsx";
|
||||
import Infrastructure from "../Pages/Infrastructure/Monitors";
|
||||
import InfrastructureCreate from "../Pages/Infrastructure/Create/index.jsx";
|
||||
import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx";
|
||||
|
||||
@@ -147,7 +147,13 @@ const Routes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure"
|
||||
element={<Infrastructure />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<Infrastructure />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/create"
|
||||
|
||||
@@ -276,6 +276,15 @@
|
||||
"weight": "Weight"
|
||||
}
|
||||
}
|
||||
},
|
||||
"infrastructure": {
|
||||
"table": {
|
||||
"headers": {
|
||||
"cpu": "CPU",
|
||||
"disk": "Disk",
|
||||
"memory": "Memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"incidentsTableNoIncidents": "No incidents recorded",
|
||||
|
||||
Reference in New Issue
Block a user