Merge pull request #1163 from bluewave-labs/caio/infrastructurePage

Caio/infrastructure page
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-12-01 18:14:48 -08:00
commit 5ada674842
22 files changed
+938 -323

No files matched your search

+2 -2
View File
@@ -27,7 +27,7 @@
font-size: var(--env-var-font-size-medium);
}
.MuiTable-root .MuiTableHead-root .MuiTableCell-root {
padding: var(--env-var-spacing-1) var(--env-var-spacing-2);
padding: var(--env-var-spacing-1);
font-weight: 500;
}
.MuiTable-root .MuiTableHead-root span {
@@ -43,7 +43,7 @@
height: 20px;
}
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
padding: 6px var(--env-var-spacing-2);
padding: var(--env-var-spacing-1);
}
.MuiTable-root .MuiTableBody-root .MuiTableRow-root {
height: 50px;
@@ -4,6 +4,9 @@ import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
/**
* A Performant SVG based circular gauge
*
@@ -11,25 +14,21 @@ import "./index.css";
* @param {Object} props - Component properties
* @param {number} [props.progress=0] - Progress percentage (0-100)
* @param {number} [props.radius=60] - Radius of the gauge circle
* @param {string} [props.color="#000000"] - Color of the progress stroke
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
* @param {number} [props.threshold=50] - Threshold for color change
*
* @example
* <CustomGauge
* progress={75}
* radius={50}
* color="#00ff00"
* strokeWidth={10}
* threshold={50}
* />
*
* @returns {React.ReactElement} Rendered CustomGauge component
*/
const CustomGauge = ({
progress = 0,
radius = 60,
color = "#000000",
strokeWidth = 15,
}) => {
const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
const theme = useTheme();
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
() => ({
@@ -39,10 +38,9 @@ const CustomGauge = ({
}),
[radius, strokeWidth, progress]
);
const [offset, setOffset] = useState(circumference);
const theme = useTheme();
// Handle initial animation
const [offset, setOffset] = useState(circumference);
useEffect(() => {
setOffset(circumference);
const timer = setTimeout(() => {
@@ -52,6 +50,13 @@ const CustomGauge = ({
return () => clearTimeout(timer);
}, [progress, circumference, strokeLength]);
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
const fillColor =
progressWithinRange > threshold
? theme.palette.percentage.uptimePoor
: theme.palette.primary.main;
return (
<Box
className="radial-chart"
@@ -74,7 +79,7 @@ const CustomGauge = ({
/>
<circle
className="radial-chart-progress"
stroke={color}
stroke={fillColor}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={offset}
@@ -97,7 +102,7 @@ const CustomGauge = ({
fill: theme.typography.body2.color,
}}
>
{`${progress.toFixed(2)}%`}
{`${progressWithinRange.toFixed(1)}%`}
</Typography>
</Box>
);
@@ -108,6 +113,6 @@ export default CustomGauge;
CustomGauge.propTypes = {
progress: PropTypes.number,
radius: PropTypes.number,
color: PropTypes.string,
strokeWidth: PropTypes.number,
threshold: PropTypes.number,
};
+63 -76
View File
@@ -2,41 +2,37 @@ import { RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
/* TODO delete component */
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
const DATA = [{ value: MAXIMUM_VALUE }];
const CHART_MAXIMUM_DATA = {
value: MAXIMUM_VALUE,
fill: "transparent",
};
const PROGRESS_THRESHOLD = 50;
const DEFAULT_CONTAINER_HEIGHT = 160;
const TEXT_POSITIONS = {
value: { x: "50%", y: "45%" },
label: { x: "50%", y: "55%" },
};
const COMMON_RADIAL_BAR_PROPS = {
minAngle: 15,
clockWise: true,
dataKey: "value",
cornerRadius: 0,
};
const DEFAULT_WIDTH = 60;
const RADIUS_SIZE = "90%";
const START_ANGLE = 90;
const CHART_RANGE = 360;
const RADIAL_BAR_CHART_PROPS = {
cx: "50%",
cy: "50%",
innerRadius: "45%",
outerRadius: "100%",
startAngle: 90,
endAngle: -270,
innerRadius: RADIUS_SIZE,
outerRadius: RADIUS_SIZE,
barSize: 6,
startAngle: START_ANGLE,
endAngle: START_ANGLE - CHART_RANGE,
margin: { top: 0, right: 0, bottom: 0, left: 0 },
};
const RADIAL_BAR_OVERLAY_PROPS = {
transform: "rotate(180deg)",
position: "absolute",
top: "0",
left: "0",
const RADIAL_BAR_PROPS = {
dataKey: "value",
cornerRadius: 8,
};
const COMMON_HEADER_PROPS = {
textAnchor: "middle",
dominantBaseline: "middle",
Gauge.propTypes = {
progressValue: PropTypes.number.isRequired,
width: PropTypes.number,
};
/**
@@ -46,79 +42,70 @@ const COMMON_HEADER_PROPS = {
* @component
* @param {Object} props - Component props
* @param {number} props.progressValue - The value to display in the gauge (0-100)
* @param {string} props.displayText - The text to display below the progress value
* @param {number} [props.containerHeight=160] - Height of the gauge container in pixels
* @param {Object} [props.gaugeHeader] - Custom styles to apply to the progress value text
* @param {Object} [props.gaugeSubheader] - Custom styles to apply to the display text
*
* @param {number} props.width - Width of the gauge container in pixels
* @returns {JSX.Element} A circular gauge chart with progress value and text
*
* @example
* <Gauge
* progressValue={75}
* displayText="Completion"
* containerHeight={200}
* gaugeHeader={{ fontSize: '24px' }}
* gaugeSubheader={{ fill: 'gray' }}
* width={200}
* />
*/
const Gauge = ({
progressValue,
displayText,
containerHeight,
gaugeHeader,
gaugeSubheader,
}) => {
function Gauge({ progressValue, width = DEFAULT_WIDTH }) {
const theme = useTheme();
const myProgressValue = Math.max(MINIMUM_VALUE, Math.min(progressValue, MAXIMUM_VALUE));
const chartColor =
progressValue > PROGRESS_THRESHOLD
? theme.palette.primary.main
: theme.palette.percentage.uptimePoor;
const chartData = [
CHART_MAXIMUM_DATA,
{
value: myProgressValue,
fill: chartColor,
},
];
return (
<ResponsiveContainer height={containerHeight ?? DEFAULT_CONTAINER_HEIGHT}>
<ResponsiveContainer
aspect={1}
width={width}
style={{ marginInline: "auto" }}
>
<RadialBarChart
{...RADIAL_BAR_CHART_PROPS}
data={DATA}
data={chartData}
>
<RadialBar
{...COMMON_RADIAL_BAR_PROPS}
{...RADIAL_BAR_PROPS}
fill={
progressValue > PROGRESS_THRESHOLD
? theme.palette.primary.main
: theme.palette.percentage.uptimePoor
}
background={{ fill: theme.palette.background.fill }}
data={[{ value: myProgressValue }]}
label={{
position: "center",
content: () => (
<text
x="50%"
y="50%"
textAnchor="middle"
dominantBaseline="middle"
style={{
...theme.typography.body2,
fill: theme.typography.body2.color,
}}
>
{`${myProgressValue}%`}
</text>
),
}}
/>
<RadialBar
{...COMMON_RADIAL_BAR_PROPS}
data={DATA}
style={RADIAL_BAR_OVERLAY_PROPS}
/>
<text
{...TEXT_POSITIONS.value}
role="text"
aria-label={`${myProgressValue}%`}
style={{ ...COMMON_HEADER_PROPS, ...theme.chart.header, ...gaugeHeader }}
>
{`${myProgressValue}%`}
</text>
<text
{...TEXT_POSITIONS.label}
role="text"
aria-label={`${displayText}`}
style={{ ...COMMON_HEADER_PROPS, ...theme.chart.subheader, ...gaugeSubheader }}
>
{`${displayText}`}
</text>
</RadialBarChart>
</ResponsiveContainer>
);
};
}
Gauge.propTypes = {
progressValue: PropTypes.number.isRequired,
displayText: PropTypes.string.isRequired,
containerHeight: PropTypes.number,
gaugeHeader: PropTypes.object,
gaugeSubheader: PropTypes.object,
};
export default Gauge;
export { Gauge };
@@ -161,7 +161,7 @@ const Search = ({
},
}}
sx={{
height: 34,
/* height: 34,*/
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
...sx,
}}
@@ -6,6 +6,13 @@
padding: var(--env-var-spacing-2);
}
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
/* @media (max-width: 1000px) {
.home-layout {
flex-direction: column !important;
}
} */
.home-layout aside {
position: sticky;
top: var(--env-var-spacing-2);
@@ -40,7 +40,7 @@ export const checkInfrastructureEndpointResolution = createAsyncThunk(
const res = await networkService.checkEndpointResolution({
authToken: authToken,
monitorURL: monitorURL,
})
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -53,7 +53,7 @@ export const checkInfrastructureEndpointResolution = createAsyncThunk(
return thunkApi.rejectWithValue(payload);
}
}
)
);
export const getInfrastructureMonitorById = createAsyncThunk(
"infrastructureMonitors/getMonitorById",
@@ -87,6 +87,8 @@ export const getInfrastructureMonitorsByTeamId = createAsyncThunk(
authToken: token,
teamId: user.teamId,
types: ["hardware"],
limit: 1,
rowsPerPage: 0,
});
return res.data;
} catch (error) {
@@ -112,7 +114,7 @@ export const updateInfrastructureMonitor = createAsyncThunk(
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
threshold: monitor.threshold
threshold: monitor.threshold,
};
const res = await networkService.updateMonitor({
authToken: authToken,
@@ -307,7 +309,9 @@ const infrastructureMonitorsSlice = createSlice({
.addCase(getInfrastructureMonitorById.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to get infrastructure monitor";
state.msg = action.payload
? action.payload.msg
: "Failed to get infrastructure monitor";
})
// *****************************************************
// update Monitor
@@ -401,6 +405,7 @@ const infrastructureMonitorsSlice = createSlice({
},
});
export const { setInfrastructureMonitors, clearInfrastructureMonitorState } = infrastructureMonitorsSlice.actions;
export const { setInfrastructureMonitors, clearInfrastructureMonitorState } =
infrastructureMonitorsSlice.actions;
export default infrastructureMonitorsSlice.reducer;
@@ -221,6 +221,7 @@ export const addDemoMonitors = createAsyncThunk(
}
}
);
export const deleteAllMonitors = createAsyncThunk(
"monitors/deleteAllMonitors",
async (data, thunkApi) => {
+11
View File
@@ -0,0 +1,11 @@
import { useSelector } from "react-redux";
const useIsAdmin = () => {
const { user } = useSelector((state) => state.auth);
const isAdmin =
(user?.role?.includes("admin") ?? false) ||
(user?.role?.includes("superadmin") ?? false);
return isAdmin;
};
export { useIsAdmin };
@@ -265,7 +265,8 @@ const CreateInfrastructureMonitor = () => {
<TextInput
type="text"
id="name"
label="Friendly name"
label="Display name"
placeholder="Google"
isOptional={true}
value={infrastructureMonitor.name}
onBlur={handleBlur}
@@ -0,0 +1,223 @@
/* TODO I basically copied and pasted this component from the actionsMenu. Check how we can make it reusable */
import { useRef, useState } from "react";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../../Utils/toastUtils";
import { IconButton, Menu, MenuItem } from "@mui/material";
import Settings from "../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import Dialog from "../../../../Components/Dialog";
import { networkService } from "../../../../Utils/NetworkService.js";
/**
* 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.
* @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 authState = useSelector((state) => state.auth);
const authToken = authState.authToken;
const { isLoading } = useSelector((state) => state.uptimeMonitors);
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 handleRemove = async () => {
try {
await networkService.deleteMonitorById({
authToken,
monitorId: monitor.id,
});
createToast({ body: "Monitor deleted successfully." });
} catch (error) {
createToast({ body: "Failed to delete monitor." });
} finally {
setIsDialogOpen(false);
updateCallback();
}
};
return (
<>
<IconButton
aria-label="monitor actions"
onClick={openMenu}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
ref={anchor}
>
<Settings />
</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.contrastText,
},
},
},
}}
>
{/*
Open site action. Not necessary for infrastructure?
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
*/}
<MenuItem onClick={() => openDetails(monitor.id)}>Details</MenuItem>
{/*
Incidents. Necessary?
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/incidents/${actions.id}`);
}}
>
Incidents
</MenuItem> */}
{/*
Configure. Necessary?
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
)} */}
{/*
Clone. Necessary?
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/create/${actions.id}`);
}}
>
Clone
</MenuItem>
)} */}
{/*
Pause. Necessary?
const handlePause = async () => {
try {
const action = await dispatch(
pauseUptimeMonitor({ authToken, monitorId: monitor._id })
);
if (pauseUptimeMonitor.fulfilled.match(action)) {
updateCallback();
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
createToast({ body: `Monitor ${state} successfully.` });
} else {
throw new Error(action?.error?.message ?? "Failed to pause monitor.");
}
} catch (error) {
logger.error("Error pausing monitor:", monitor._id, error);
createToast({ body: "Failed to pause monitor." });
}
};
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
handlePause(e);
}}
>
{monitor?.isActive === true ? "Pause" : "Resume"}
</MenuItem>
)} */}
{isAdmin && <MenuItem onClick={openRemove}>Remove</MenuItem>}
</Menu>
<Dialog
open={isDialogOpen}
theme={theme}
title="Do you really want to delete this monitor?"
description="Once deleted, this monitor cannot be retrieved."
onCancel={cancelRemove}
confirmationButtonLabel="Delete"
onConfirm={handleRemove}
isLoading={isLoading}
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
/>
</>
);
};
InfrastructureMenu.propTypes = {
monitor: PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
isActive: PropTypes.bool,
}).isRequired,
isAdmin: PropTypes.bool,
updateCallback: PropTypes.func,
};
export { InfrastructureMenu };
@@ -0,0 +1,80 @@
import PropTypes from "prop-types";
import { Box, Button } from "@mui/material";
import LeftArrowDouble from "../../../../../assets/icons/left-arrow-double.svg?react";
import RightArrowDouble from "../../../../../assets/icons/right-arrow-double.svg?react";
import LeftArrow from "../../../../../assets/icons/left-arrow.svg?react";
import RightArrow from "../../../../../assets/icons/right-arrow.svg?react";
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
/**
* Component for pagination actions (first, previous, next, last).
*
* @component
* @param {Object} props
* @param {number} props.count - Total number of items.
* @param {number} props.page - Current page number.
* @param {number} props.rowsPerPage - Number of rows per page.
* @param {function} props.onPageChange - Callback function to handle page change.
*
* @returns {JSX.Element} Pagination actions component.
*/
function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box sx={{ flexShrink: 0, ml: "24px" }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrowDouble />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrowDouble />
</Button>
</Box>
);
}
export { TablePaginationActions };
@@ -0,0 +1,117 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Stack, TablePagination, Typography } from "@mui/material";
import { TablePaginationActions } from "./Actions";
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
Pagination.propTypes = {
monitorCount: PropTypes.number.isRequired, // Total number of items for pagination.
page: PropTypes.number.isRequired, // Current page index.
rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page.
handleChangePage: PropTypes.func.isRequired, // Function to handle page changes.
handleChangeRowsPerPage: PropTypes.func.isRequired, // Function to handle changes in rows per page.
};
const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
/**
* Pagination component for table navigation with customized styling and behavior.
*
* @param {object} props - Component properties.
* @param {number} props.monitorCount - Total number of monitors to paginate.
* @param {number} props.page - Current page index (0-based).
* @param {number} props.rowsPerPage - Number of rows to display per page.
* @param {function} props.handleChangePage - Callback for handling page changes.
* @param {function} props.handleChangeRowsPerPage - Callback for handling changes to rows per page.
* @returns {JSX.Element} The Pagination component.
*/
function Pagination({
monitorCount,
page,
rowsPerPage,
handleChangePage,
handleChangeRowsPerPage,
}) {
const theme = useTheme();
const start = page * rowsPerPage + 1;
const end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount);
const range = `${start} - ${end}`;
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
marginTop={8}
>
<Typography
px={theme.spacing(2)}
variant="body2"
sx={{ opacity: 0.7 }}
>
Showing {range} of {monitorCount} monitor(s)
</Typography>
<TablePagination
component="div"
count={monitorCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
}
slotProps={{
select: {
MenuProps: {
keepMounted: true,
disableScrollLock: true,
PaperProps: {
className: "pagination-dropdown",
sx: {
mt: 0,
mb: theme.spacing(2),
},
},
transformOrigin: { vertical: "bottom", horizontal: "left" },
anchorOrigin: { vertical: "top", horizontal: "left" },
sx: {
mt: theme.spacing(-2),
},
},
inputProps: { id: "pagination-dropdown" },
IconComponent: SelectorVertical,
sx: {
ml: theme.spacing(4),
mr: theme.spacing(12),
minWidth: theme.spacing(20),
textAlign: "left",
"&.Mui-focused > div": {
backgroundColor: theme.palette.background.main,
},
},
},
}}
sx={{
color: theme.palette.text.secondary,
"& svg path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.3,
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
}}
/>
</Stack>
);
}
export { Pagination };
+316 -14
View File
@@ -1,6 +1,70 @@
import { Box, Button } from "@mui/material";
import { Heading } from "../../Components/Heading";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { /* useDispatch, */ useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import useUtils from "../Monitors/utils";
import { jwtDecode } from "jwt-decode";
// import GearIcon from "../../Assets/icons/settings-bold.svg?react";
import CPUChipIcon from "../../assets/icons/cpu-chip.svg?react";
import {
Box,
Button,
IconButton,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import Breadcrumbs from "../../Components/Breadcrumbs";
import { StatusLabel } from "../../Components/Label";
import { Heading } from "../../Components/Heading";
import { Pagination } from "./components/TablePagination";
// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { networkService } from "../../Utils/NetworkService.js";
import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx";
import Host from "../Monitors/Home/host.jsx";
import { useIsAdmin } from "../../Hooks/useIsAdmin.js";
import { InfrastructureMenu } from "./components/Menu";
const columns = [
{ label: "Host" },
{ label: "Status" },
{ label: "Frequency" },
{ label: "CPU" },
{ label: "Mem" },
{ label: "Disk" },
{ label: "Actions" },
];
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
/* TODO
Create reusable table component.
It should receive as a parameter the following object:
tableData = [
columns = [
{
id: example,
label: Example Extendable,
align: "center" | "left" (default)
}
],
rows: [
{
**Number of keys will be equal to number of columns**
key1: string,
key2: number,
key3: Component
}
]
]
Apply to Monitor Table, and Account/Team.
Analyze existing BasicTable
*/
/**
* This is the Infrastructure monitoring page. This is a work in progress
@@ -10,21 +74,259 @@ import { useNavigate } from "react-router-dom";
*/
function Infrastructure() {
const navigate = useNavigate();
/* Adding this custom hook so we can avoid using the HOC approach that can lower performance (we are calling the admin logic N times on initializing the project. using a custom hook will cal it ass needed ) */
const isAdmin = useIsAdmin();
const theme = useTheme();
const navigate = useNavigate();
const navigateToCreate = () => navigate("/infrastructure/create");
const [page, setPage] = useState(0);
/* TODO refactor this, so it is not aware of the MUI implementation. First argument only exists because of MUI. This should require onlu the new page. Adapting for MUI should happen inside of table pagination component */
const handleChangePage = (_, newPage) => {
setPage(newPage);
};
const [rowsPerPage, setRowsPerPage] = useState(5);
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value));
setPage(0);
};
const [monitorState, setMonitorState] = useState({ monitors: [], total: 0 });
const { authToken } = useSelector((state) => state.auth);
const user = jwtDecode(authToken);
const fetchMonitors = async () => {
try {
const response = await networkService.getMonitorsByTeamId({
authToken,
teamId: user.teamId,
limit: 1,
types: ["hardware"],
status: null,
checkOrder: "desc",
normalize: true,
page: page,
rowsPerPage: rowsPerPage,
});
setMonitorState({
monitors: response?.data?.data?.monitors ?? [],
total: response?.data?.data?.monitorCount ?? 0,
});
} catch (error) {
console.error(error);
}
};
useEffect(() => {
fetchMonitors();
}, [page, rowsPerPage]);
const { determineState } = useUtils();
const { monitors, total: totalMonitors } = monitorState;
// do it here
function openDetails(id) {
navigate(`/infrastructure/${id}`);
}
function handleActionMenuDelete() {
fetchMonitors();
}
const monitorsAsRows = monitors.map((monitor) => {
const processor =
((monitor.checks[0]?.cpu?.usage_frequency ?? 0) / 1000).toFixed(2) + " GHz";
const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100;
const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100;
const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100;
const status = determineState(monitor);
const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100)
.toFixed(2)
.toString();
const percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.percentage.uptimePoor
: monitor.uptimePercentage < 0.5
? theme.palette.percentage.uptimeFair
: monitor.uptimePercentage < 0.75
? theme.palette.percentage.uptimeGood
: theme.palette.percentage.uptimeExcellent;
return {
id: monitor._id,
name: monitor.name,
url: monitor.url,
processor,
cpu,
mem,
disk,
status,
uptimePercentage,
percentageColor,
};
});
return (
<Box component="main">
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/infrastructure/create");
<Stack
component="main"
style={{ width: "100%", gap: "1rem" }}
>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
sx={{
justifyContent: "end",
alignItems: "center",
gap: "1rem",
flexWrap: "wrap",
marginBottom: "2rem",
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
<Heading component="h2">Infrastructure monitors</Heading>
</Box>
{/*
This will be removed from here, but keeping the commented code to remind me to add a max width to the greeting component
<Box style={{ maxWidth: "65ch" }}>
<Greeting type="uptime" />
</Box> */}
<Button
variant="contained"
color="primary"
onClick={navigateToCreate}
sx={{ fontWeight: 500 }}
>
Create infrastructure monitor
</Button>
</Stack>
<Stack
sx={{
gap: "1rem",
}}
>
<Stack
direction="row"
sx={{
alignItems: "center",
gap: ".25rem",
flexWrap: "wrap",
}}
>
<Heading component="h2">Infrastructure monitors</Heading>
{/* TODO Correct the class current-monitors-counter, there are some unnecessary things there */}
<Box
component="span"
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{totalMonitors}
</Box>
</Stack>
<TableContainer component={Paper}>
<Table stickyHeader>
<TableHead sx={{ backgroundColor: theme.palette.background.accent }}>
<TableRow>
{columns.map((column, index) => (
<TableCell
key={index}
align={index === 0 ? "left" : "center"}
sx={{
backgroundColor: theme.palette.background.accent,
}}
>
{column.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{monitorsAsRows.map((row) => {
return (
<TableRow
key={row.id}
onClick={() => openDetails(row.id)}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
>
{/* TODO iterate over column and get column id, applying row[column.id] */}
<TableCell>
<Host
title={row.name}
url={row.url}
percentage={row.uptimePercentage}
percentageColor={row.percentageColor}
/>
</TableCell>
<TableCell align="center">
<StatusLabel
status={row.status}
text={row.status}
/* Use capitalize inside of Status Label */
/* Update component so we don't need to pass text and status separately*/
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell align="center">
<Stack
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
gap=".25rem"
>
<CPUChipIcon
width={20}
height={20}
/>
{row.processor}
</Stack>
</TableCell>
<TableCell align="center">
<CustomGauge progress={row.cpu} />
</TableCell>
<TableCell align="center">
<CustomGauge progress={row.mem} />
</TableCell>
<TableCell align="center">
<CustomGauge progress={row.disk} />
</TableCell>
<TableCell align="center">
{/* Get ActionsMenu from Monitor Table and create a component */}
<IconButton
sx={{
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
>
<InfrastructureMenu
monitor={row}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
{/* <GearIcon
width={20}
height={20}
/> */}
</IconButton>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<Pagination
monitorCount={totalMonitors}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Stack>
</Stack>
);
}
@@ -3,6 +3,7 @@ const ROWS_NUMBER = 7;
const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
const TableBodySkeleton = () => {
/* TODO Skeleton does not follow light and dark theme */
return (
<>
{ROWS_ARRAY.map((row) => (
@@ -1,4 +1,15 @@
import PropTypes from "prop-types";
import { useState, useEffect, memo, useCallback, useRef } from "react";
import { useNavigate } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import useUtils from "../../utils";
import { setRowsPerPage } from "../../../../Features/UI/uiSlice";
import { logger } from "../../../../Utils/Logger";
import { jwtDecode } from "jwt-decode";
import { networkService } from "../../../../main";
import {
TableContainer,
Table,
@@ -8,106 +19,18 @@ import {
TableBody,
Paper,
Box,
TablePagination,
Stack,
Typography,
Button,
CircularProgress,
} from "@mui/material";
import ActionsMenu from "../actionsMenu";
import Host from "../host";
import { StatusLabel } from "../../../../Components/Label";
import { TableBodySkeleton } from "./Skeleton";
import BarChart from "../../../../Components/Charts/BarChart";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import { setRowsPerPage } from "../../../../Features/UI/uiSlice";
import { useState, useEffect, memo, useCallback, useRef } from "react";
import { useNavigate } from "react-router";
import { logger } from "../../../../Utils/Logger";
import Host from "../host";
import { StatusLabel } from "../../../../Components/Label";
import { jwtDecode } from "jwt-decode";
import { useDispatch, useSelector } from "react-redux";
import { networkService } from "../../../../main";
import { useTheme } from "@emotion/react";
import BarChart from "../../../../Components/Charts/BarChart";
import LeftArrowDouble from "../../../../assets/icons/left-arrow-double.svg?react";
import RightArrowDouble from "../../../../assets/icons/right-arrow-double.svg?react";
import LeftArrow from "../../../../assets/icons/left-arrow.svg?react";
import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
import ActionsMenu from "../actionsMenu";
import useUtils from "../../utils";
import { TableBodySkeleton } from "./Skeleton";
/**
* Component for pagination actions (first, previous, next, last).
*
* @component
* @param {Object} props
* @param {number} props.count - Total number of items.
* @param {number} props.page - Current page number.
* @param {number} props.rowsPerPage - Number of rows per page.
* @param {function} props.onPageChange - Callback function to handle page change.
*
* @returns {JSX.Element} Pagination actions component.
*/
const TablePaginationActions = (props) => {
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box sx={{ flexShrink: 0, ml: "24px" }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrowDouble />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrowDouble />
</Button>
</Box>
);
};
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
import { Pagination } from "../../../Infrastructure/components/TablePagination";
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
const theme = useTheme();
@@ -117,6 +40,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
const { rowsPerPage } = useSelector((state) => state.ui.monitors);
const authState = useSelector((state) => state.auth);
const [page, setPage] = useState(0);
const [monitors, setMonitors] = useState([]);
const [monitorCount, setMonitorCount] = useState(0);
@@ -191,16 +115,6 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
prevFilter.current = filter;
}, [filter, fetchPage]);
/**
* Helper function to calculate the range of displayed rows.
* @returns {string}
*/
const getRange = () => {
let start = page * rowsPerPage + 1;
let end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount);
return `${start} - ${end}`;
};
const handleSort = async (field) => {
let order = "";
if (sort.field !== field) {
@@ -360,7 +274,10 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
<TableCell>
<Host
key={monitor._id}
params={params}
url={params.url}
title={params.title}
percentageColor={params.percentageColor}
percentage={params.percentage}
/>
</TableCell>
<TableCell>
@@ -392,76 +309,13 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
</TableBody>
</Table>
</TableContainer>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
>
<Typography
px={theme.spacing(2)}
variant="body2"
sx={{ opacity: 0.7 }}
>
Showing {getRange()} of {monitorCount} monitor(s)
</Typography>
<TablePagination
component="div"
count={monitorCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 15, 25]}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
}
slotProps={{
select: {
MenuProps: {
keepMounted: true,
disableScrollLock: true,
PaperProps: {
className: "pagination-dropdown",
sx: {
mt: 0,
mb: theme.spacing(2),
},
},
transformOrigin: { vertical: "bottom", horizontal: "left" },
anchorOrigin: { vertical: "top", horizontal: "left" },
sx: { mt: theme.spacing(-2) },
},
inputProps: { id: "pagination-dropdown" },
IconComponent: SelectorVertical,
sx: {
ml: theme.spacing(4),
mr: theme.spacing(12),
minWidth: theme.spacing(20),
textAlign: "left",
"&.Mui-focused > div": {
backgroundColor: theme.palette.background.main,
},
},
},
}}
sx={{
mt: theme.spacing(6),
color: theme.palette.text.secondary,
"& svg path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.3,
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
}}
/>
</Stack>
<Pagination
monitorCount={monitorCount}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Box>
);
};
+21 -19
View File
@@ -6,12 +6,14 @@ import PropTypes from "prop-types";
*
* @component
* @param {Object} params - An object containing the following properties:
* @param {string} params.url - The URL of the host.
* @param {string} params.title - The name of the host.
* @param {string} params.percentageColor - The color of the percentage text.
* @param {number} params.percentage - The percentage to display.
* @returns {React.ElementType} Returns a div element with the host details.
*/
const Host = ({ params }) => {
const Host = ({ url, title, percentageColor, percentage }) => {
const noTitle = title === undefined || title === url;
return (
<Box className="host">
<Box
@@ -32,30 +34,30 @@ const Host = ({ params }) => {
},
}}
>
{params.title}
{title}
</Box>
<Typography
component="span"
sx={{
color: params.percentageColor,
fontWeight: 500,
ml: "15px",
}}
>
{params.percentage}%
</Typography>
<Box sx={{ opacity: 0.6 }}>{params.url}</Box>
{percentageColor && percentage && (
<Typography
component="span"
sx={{
color: percentageColor,
fontWeight: 500,
ml: "15px",
}}
>
{percentage}%
</Typography>
)}
{!noTitle && <Box sx={{ opacity: 0.6 }}>{url}</Box>}
</Box>
);
};
Host.propTypes = {
params: PropTypes.shape({
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.string,
url: PropTypes.string,
}).isRequired,
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.string,
url: PropTypes.string,
};
export default Host;
+2
View File
@@ -20,6 +20,8 @@ const Monitors = ({ isAdmin }) => {
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
console.log({ monitorState });
useEffect(() => {
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
}, [authState.authToken, dispatch]);
+3 -1
View File
@@ -1,13 +1,15 @@
import { useTheme } from "@mui/material";
const useUtils = () => {
const theme = useTheme();
const determineState = (monitor) => {
if (monitor.isActive === false) return "paused";
if (monitor?.status === undefined) return "pending";
return monitor?.status == true ? "up" : "down";
};
/* TODO Refactor: from here on shouldn't live in a custom hook, but on theme, or constants */
const theme = useTheme();
const statusColor = {
up: theme.palette.success.main,
down: theme.palette.error.main,
+1 -1
View File
@@ -169,7 +169,7 @@ const semanticColors = {
background: {
main: {
light: paletteColors.white,
dark: paletteColors.gray850,
dark: paletteColors.gray890,
},
alt: {
light: paletteColors.gray90,
+4 -14
View File
@@ -32,19 +32,8 @@ const baseTheme = (palette) => ({
fontWeight: 400,
},
},
/* TODO take chart from here, it should live inside of the gauge, and get info from the theme */
chart: {
header: {
fontWeight: 400,
fill: palette.text.tertiary,
fontSize: typographyLevels.m,
},
subheader: {
fontWeight: 400,
fill: palette.text.tertiary,
fontSize: typographyLevels.xs,
},
},
/* TODO we can skip using the callback functions on the next lines since we are already accessing it on line 10. That was the last thing I managed to do, so we are sort of doing it twice*/
spacing: 2,
/* TODO All these should live inside of a component*/
components: {
@@ -217,7 +206,8 @@ const baseTheme = (palette) => ({
borderRadius: theme.shape.borderRadius,
},
"& .MuiInputBase-input": {
height: "var(--env-var-height-2)",
padding: ".75em",
minHeight: "var(--env-var-height-2)",
fontSize: "var(--env-var-font-size-medium)",
fontWeight: 400,
color: palette.text.secondary,
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 2V4M15 2V4M9 20V22M15 20V22M20 9H22M20 14H22M2 9H4M2 14H4M8.8 20H15.2C16.8802 20 17.7202 20 18.362 19.673C18.9265 19.3854 19.3854 18.9265 19.673 18.362C20 17.7202 20 16.8802 20 15.2V8.8C20 7.11984 20 6.27976 19.673 5.63803C19.3854 5.07354 18.9265 4.6146 18.362 4.32698C17.7202 4 16.8802 4 15.2 4H8.8C7.11984 4 6.27976 4 5.63803 4.32698C5.07354 4.6146 4.6146 5.07354 4.32698 5.63803C4 6.27976 4 7.11984 4 8.8V15.2C4 16.8802 4 17.7202 4.32698 18.362C4.6146 18.9265 5.07354 19.3854 5.63803 19.673C6.27976 20 7.11984 20 8.8 20ZM10.6 15H13.4C13.9601 15 14.2401 15 14.454 14.891C14.6422 14.7951 14.7951 14.6422 14.891 14.454C15 14.2401 15 13.9601 15 13.4V10.6C15 10.0399 15 9.75992 14.891 9.54601C14.7951 9.35785 14.6422 9.20487 14.454 9.10899C14.2401 9 13.9601 9 13.4 9H10.6C10.0399 9 9.75992 9 9.54601 9.10899C9.35785 9.20487 9.20487 9.35785 9.10899 9.54601C9 9.75992 9 10.0399 9 10.6V13.4C9 13.9601 9 14.2401 9.10899 14.454C9.20487 14.6422 9.35785 14.7951 9.54601 14.891C9.75992 15 10.0399 15 10.6 15Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,7 +1,29 @@
import HardwareCheck from "../../models/HardwareCheck.js";
import Monitor from "../../models/Monitor.js";
const SERVICE_NAME = "hardwareCheckModule";
const createHardwareCheck = async (hardwareCheckData) => {
try {
const { monitorId, status } = hardwareCheckData;
const n = (await HardwareCheck.countDocuments({ monitorId })) + 1;
const monitor = await Monitor.findById(monitorId);
if (!monitor) {
logger.error({
message: "Monitor not found",
service: SERVICE_NAME,
method: "createHardwareCheck",
details: `monitor ID: ${monitorId}`,
});
}
if (monitor.uptimePercentage === undefined) {
monitor.uptimePercentage = status === true ? 1 : 0;
} else {
monitor.uptimePercentage =
(monitor.uptimePercentage * (n - 1) + (status === true ? 1 : 0)) / n;
}
await monitor.save();
const hardwareCheck = await new HardwareCheck({
...hardwareCheckData,
}).save();