Merge branch 'master' into feat/details-chart
This commit is contained in:
8 files changed
+267
-54
No files matched your search
@@ -0,0 +1,121 @@
|
||||
.MuiTable-root .host-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.MuiTable-root .host-row a {
|
||||
width: var(--env-var-img-width-2);
|
||||
height: var(--env-var-img-width-2);
|
||||
color: var(--env-var-color-5);
|
||||
margin-right: 10px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.MuiTable-root .host-row a svg {
|
||||
width: var(--env-var-font-size-large);
|
||||
height: var(--env-var-font-size-large);
|
||||
}
|
||||
.MuiTable-root .host-row .host-percentage {
|
||||
font-size: var(--env-var-font-size-small);
|
||||
}
|
||||
|
||||
.MuiTable-root .host-name {
|
||||
width: fit-content;
|
||||
margin-right: 10px;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.MuiPaper-root:has(table.MuiTable-root) {
|
||||
box-shadow: none;
|
||||
border: solid 1px var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.MuiTable-root
|
||||
.MuiTableBody-root
|
||||
.MuiTableRow-root:last-child
|
||||
.MuiTableCell-root {
|
||||
border: none;
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root,
|
||||
.MuiTable-root .MuiTableRow-root:hover {
|
||||
background-color: var(--env-var-color-13);
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root .MuiTableCell-root,
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
|
||||
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);
|
||||
font-weight: 500;
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root span {
|
||||
display: inline-block;
|
||||
height: 17px;
|
||||
width: 20px;
|
||||
overflow: hidden;
|
||||
margin-bottom: -3px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root span svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
|
||||
color: var(--env-var-color-5);
|
||||
padding: 6px var(--env-var-spacing-2);
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableRow-root {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.MuiPaper-root + .MuiPagination-root {
|
||||
flex: 1;
|
||||
margin-top: 35px;
|
||||
border: solid 1px var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul {
|
||||
justify-content: center;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li button {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
font-weight: 500;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button,
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button {
|
||||
border: solid 1px var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button {
|
||||
padding: 0 var(--env-var-spacing-1) 0 var(--env-var-spacing-1-plus);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button {
|
||||
padding: 0 var(--env-var-spacing-1-plus) 0 var(--env-var-spacing-1);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button::after,
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button::before {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button::after {
|
||||
content: "Previous";
|
||||
margin-left: 15px;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button::before {
|
||||
content: "Next";
|
||||
margin-right: 15px;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root .MuiPaginationItem-root.Mui-selected {
|
||||
background-color: var(--env-var-color-15);
|
||||
}
|
||||
@@ -6,10 +6,14 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
TablePagination,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
} from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
|
||||
/**
|
||||
* BasicTable Component
|
||||
@@ -66,17 +70,12 @@ const BasicTable = ({ data, paginated }) => {
|
||||
// Add headers to props validation
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const rowsPerPage = 5;
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0); // Reset to first page after changing rows per page
|
||||
};
|
||||
|
||||
let displayData = [];
|
||||
|
||||
if (data && data.rows) {
|
||||
@@ -118,14 +117,29 @@ const BasicTable = ({ data, paginated }) => {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginated === true && (
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[1, 5, 10, 25]}
|
||||
component="div"
|
||||
count={data.rows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
<Pagination
|
||||
count={Math.ceil(data.rows.length / rowsPerPage)}
|
||||
page={page + 1}
|
||||
onChange={(event, value) => handleChangePage(event, value - 1)}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.host-row {
|
||||
/* .host-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -109,4 +109,4 @@
|
||||
.monitors .MuiPagination-root ul li:last-child button::before {
|
||||
content: "Next";
|
||||
margin-right: 15px;
|
||||
}
|
||||
} */
|
||||
@@ -5,6 +5,7 @@ import BasicTable from "../BasicTable";
|
||||
import OpenInNewPage from "../../assets/icons/open-in-new-page.svg?react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import StatusLabel from "../StatusLabel";
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
|
||||
/**
|
||||
* Host component.
|
||||
@@ -54,7 +55,17 @@ const MonitorTable = ({ monitors = [] }) => {
|
||||
const data = {
|
||||
cols: [
|
||||
{ id: 1, name: "Host" },
|
||||
{ id: 2, name: "Status" },
|
||||
{
|
||||
id: 2,
|
||||
name: (
|
||||
<>
|
||||
Status
|
||||
<span>
|
||||
<ArrowDownwardRoundedIcon />
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ id: 3, name: "Response Time" },
|
||||
{ id: 4, name: "Actions" },
|
||||
],
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
.main-content>div:nth-child(2){
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
/* overflow-y: auto; */
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,10 +1,40 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import BasicTable from "../../Components/BasicTable";
|
||||
import MonitorDetailsAreaChart from "../../Components/Charts/MonitorDetailsAreaChart";
|
||||
import StatusLabel from "../../Components/StatusLabel";
|
||||
|
||||
const formatDuration = (ms) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
let dateStr = "";
|
||||
|
||||
days && (dateStr += `${days}d `);
|
||||
hours && (dateStr += `${hours % 24}h `);
|
||||
minutes && (dateStr += `${minutes % 60}m `);
|
||||
seconds && (dateStr += `${seconds % 60}s `);
|
||||
|
||||
dateStr === "" && (dateStr = "0s");
|
||||
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
const StatBox = ({ title, value }) => {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<Typography variant="h4">{value}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Details page component displaying monitor details and related information.
|
||||
* @component
|
||||
@@ -29,10 +59,22 @@ const DetailsPage = () => {
|
||||
{ id: 3, name: "Message" },
|
||||
],
|
||||
rows: res.data.data.checks.map((check, idx) => {
|
||||
const params = {
|
||||
status: check.status === true ? "up" : "down",
|
||||
backgroundColor:
|
||||
check.status === true
|
||||
? "var(--env-var-color-20)"
|
||||
: "var(--env-var-color-21)",
|
||||
statusDotColor:
|
||||
check.status === true
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-19)",
|
||||
};
|
||||
|
||||
return {
|
||||
id: check._id,
|
||||
data: [
|
||||
{ id: idx, data: check.status ? "Up" : "Down" },
|
||||
{ id: idx, data: <StatusLabel params={params} /> },
|
||||
{ id: idx + 1, data: new Date(check.createdAt).toLocaleString() },
|
||||
{ id: idx + 2, data: check.statusCode },
|
||||
],
|
||||
@@ -52,43 +94,47 @@ const DetailsPage = () => {
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Uptime duration in ms.
|
||||
*/
|
||||
|
||||
// TODO: This can be done more efficiently by iteratting backwards
|
||||
// and breaking when the first down check is found, calculate current time - downtime
|
||||
const calculateUptimeDuration = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let longestDuration = 0;
|
||||
let lastDownTimestamp = null;
|
||||
let currentDuration = longestDuration;
|
||||
const arr = [...checks];
|
||||
|
||||
// Loop over the checks to find the most recent uptime duration
|
||||
checks.forEach((check) => {
|
||||
let lastDownTimestamp = null;
|
||||
let lastUpTimestamp = null;
|
||||
|
||||
arr.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
||||
arr.forEach((check) => {
|
||||
if (check.status === false) {
|
||||
lastDownTimestamp = check.createdAt;
|
||||
} else if (check.status === true && lastDownTimestamp) {
|
||||
currentDuration = check.createdAt - lastDownTimestamp;
|
||||
if (currentDuration > longestDuration) {
|
||||
longestDuration = currentDuration;
|
||||
}
|
||||
lastDownTimestamp = null;
|
||||
lastDownTimestamp = new Date(check.createdAt);
|
||||
} else if (check.status === true) {
|
||||
lastUpTimestamp = new Date(check.createdAt);
|
||||
}
|
||||
});
|
||||
lastDownTimestamp = null;
|
||||
return longestDuration;
|
||||
|
||||
if (lastDownTimestamp === null) {
|
||||
return new Date() - new Date(arr[0].createdAt);
|
||||
} else if (lastUpTimestamp === null) {
|
||||
return 0;
|
||||
} else if (lastDownTimestamp && lastUpTimestamp) {
|
||||
return lastUpTimestamp - lastDownTimestamp;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get timestamp of the most recent check.
|
||||
* Helper function to get duration since last check
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
const getLastCheckedTimestamp = (checks) => {
|
||||
const getLastChecked = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
|
||||
const mostRecentCheck = checks[0];
|
||||
return mostRecentCheck.createdAt;
|
||||
return new Date() - new Date(checks[0].createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -101,18 +147,41 @@ const DetailsPage = () => {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
return checks.reduce((acc, check) => {
|
||||
check.status === false ? (acc += 1) : acc;
|
||||
return check.status === false ? (acc += 1) : acc;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="monitor-details"
|
||||
style={{
|
||||
padding: `${theme.content.pY} ${theme.content.pX}`,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<StatBox
|
||||
title="Currently up for"
|
||||
value={formatDuration(calculateUptimeDuration(monitor.checks))}
|
||||
/>
|
||||
<StatBox
|
||||
title="Last checked"
|
||||
value={`${formatDuration(getLastChecked(monitor.checks))} ago`}
|
||||
/>
|
||||
<StatBox title="Incidents" value={countIncidents(monitor.checks)} />
|
||||
</div>
|
||||
<div style={{ height: "10rem" }}>
|
||||
<MonitorDetailsAreaChart checks={monitor.checks} />
|
||||
</div>
|
||||
<Typography component="h1" mb={theme.gap.small}>
|
||||
History
|
||||
</Typography>
|
||||
<BasicTable data={data} paginated={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
export default DetailsPage;
|
||||
+9
-11
@@ -13,15 +13,13 @@ import { injectStore } from "./Utils/axiosConfig.js";
|
||||
injectStore(store);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
Reference in new issue
Block a user