mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-25 03:09:32 -06:00
Merge pull request #832 from bluewave-labs/feat/timezones
Feat/timezones
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { formatDate } from "../../../Utils/timeUtils";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./index.css";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const BarChart = ({ checks = [] }) => {
|
||||
const theme = useTheme();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
useEffect(() => {
|
||||
setAnimate(true);
|
||||
@@ -51,7 +53,11 @@ const BarChart = ({ checks = [] }) => {
|
||||
title={
|
||||
<>
|
||||
<Typography>
|
||||
{formatDate(new Date(check.createdAt), { year: undefined })}
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(2)}>
|
||||
<Box
|
||||
|
||||
@@ -11,10 +11,13 @@ import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMemo } from "react";
|
||||
import "./index.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
|
||||
const CustomToolTip = ({ active, payload, label }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const theme = useTheme();
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<Box
|
||||
@@ -35,17 +38,7 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{new Date(label).toLocaleDateString("en-US", {
|
||||
weekday: "short", // Mon
|
||||
month: "long", // July
|
||||
day: "numeric", // 17
|
||||
}) +
|
||||
", " +
|
||||
new Date(label).toLocaleTimeString("en-US", {
|
||||
hour: "numeric", // 12
|
||||
minute: "2-digit", // 15
|
||||
hour12: true, // AM/PM format
|
||||
})}
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
@@ -88,13 +81,10 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
};
|
||||
|
||||
const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
return formatDateWithTz(timestamp, "HH:mm:ss", uiTimezone);
|
||||
};
|
||||
|
||||
const memoizedChecks = useMemo(() => checks, [checks[0]]);
|
||||
|
||||
@@ -14,6 +14,7 @@ const initialState = {
|
||||
},
|
||||
mode: "light",
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
timezone: "America/Toronto",
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
@@ -36,9 +37,17 @@ const uiSlice = createSlice({
|
||||
state.greeting.index = action.payload.index;
|
||||
state.greeting.lastUpdate = action.payload.lastUpdate;
|
||||
},
|
||||
setTimezone(state, action) {
|
||||
state.timezone = action.payload.timezone;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default uiSlice.reducer;
|
||||
export const { setRowsPerPage, toggleSidebar, setMode, setGreeting } =
|
||||
uiSlice.actions;
|
||||
export const {
|
||||
setRowsPerPage,
|
||||
toggleSidebar,
|
||||
setMode,
|
||||
setGreeting,
|
||||
setTimezone,
|
||||
} = uiSlice.actions;
|
||||
|
||||
@@ -21,7 +21,11 @@ import { networkService } from "../../../main";
|
||||
import { StatusLabel } from "../../../Components/Label";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
|
||||
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const theme = useTheme();
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
const [checks, setChecks] = useState([]);
|
||||
@@ -151,6 +155,11 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
const formattedDate = formatDateWithTz(
|
||||
check.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
@@ -162,9 +171,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(check.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{formattedDate}</TableCell>
|
||||
<TableCell>
|
||||
{check.statusCode ? check.statusCode : "N/A"}
|
||||
</TableCell>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
@@ -8,8 +9,9 @@ import {
|
||||
RadialBarChart,
|
||||
RadialBar,
|
||||
} from "recharts";
|
||||
import { formatDate } from "../../../../Utils/timeUtils";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
|
||||
const CustomLabels = ({
|
||||
x,
|
||||
@@ -19,27 +21,35 @@ const CustomLabels = ({
|
||||
lastDataPoint,
|
||||
type,
|
||||
}) => {
|
||||
let options = {
|
||||
month: "short",
|
||||
year: undefined,
|
||||
hour: undefined,
|
||||
minute: undefined,
|
||||
};
|
||||
if (type === "day") delete options.hour;
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
|
||||
|
||||
return (
|
||||
<>
|
||||
<text x={x} y={height} dy={-3} textAnchor="start" fontSize={11}>
|
||||
{formatDate(new Date(firstDataPoint.time), options)}
|
||||
{formatDateWithTz(
|
||||
new Date(firstDataPoint.time),
|
||||
dateFormat,
|
||||
uiTimezone
|
||||
)}
|
||||
</text>
|
||||
<text x={width} y={height} dy={-3} textAnchor="end" fontSize={11}>
|
||||
{formatDate(new Date(lastDataPoint.time), options)}
|
||||
{formatDateWithTz(new Date(lastDataPoint.time), dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const UpBarChart = memo(({ data, type, onBarHover }) => {
|
||||
CustomLabels.propTypes = {
|
||||
x: PropTypes.number.isRequired,
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
firstDataPoint: PropTypes.object.isRequired,
|
||||
lastDataPoint: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const UpBarChart = memo(({ data, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
@@ -122,7 +132,18 @@ export const UpBarChart = memo(({ data, type, onBarHover }) => {
|
||||
);
|
||||
});
|
||||
|
||||
export const DownBarChart = memo(({ data, type, onBarHover }) => {
|
||||
// Add display name for the component
|
||||
UpBarChart.displayName = "UpBarChart";
|
||||
|
||||
// Validate props using PropTypes
|
||||
UpBarChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export { UpBarChart };
|
||||
|
||||
const DownBarChart = memo(({ data, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
@@ -194,7 +215,15 @@ export const DownBarChart = memo(({ data, type, onBarHover }) => {
|
||||
);
|
||||
});
|
||||
|
||||
export const ResponseGaugeChart = ({ data }) => {
|
||||
DownBarChart.displayName = "DownBarChart";
|
||||
DownBarChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export { DownBarChart };
|
||||
|
||||
const ResponseGaugeChart = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let max = 1000; // max ms
|
||||
@@ -281,3 +310,9 @@ export const ResponseGaugeChart = ({ data }) => {
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseGaugeChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export { ResponseGaugeChart };
|
||||
|
||||
@@ -18,10 +18,9 @@ import { StatusLabel } from "../../../../Components/Label";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
import { logger } from "../../../../Utils/Logger";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
|
||||
const PaginationTable = ({ monitorId, dateRange }) => {
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
@@ -29,6 +28,7 @@ const PaginationTable = ({ monitorId, dateRange }) => {
|
||||
page: 0,
|
||||
rowsPerPage: 5,
|
||||
});
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
@@ -119,7 +119,11 @@ const PaginationTable = ({ monitorId, dateRange }) => {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(check.createdAt).toLocaleString()}
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{check.statusCode ? check.statusCode : "N/A"}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
import { networkService } from "../../../main";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import {
|
||||
formatDate,
|
||||
formatDurationRounded,
|
||||
formatDurationSplit,
|
||||
} from "../../../Utils/timeUtils";
|
||||
@@ -35,6 +34,8 @@ import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
import useUtils from "../utils";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
|
||||
/**
|
||||
* Details page component displaying monitor details and related information.
|
||||
* @component
|
||||
@@ -57,6 +58,9 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const fetchMonitor = useCallback(async () => {
|
||||
try {
|
||||
const res = await networkService.getStatsByMonitorId(
|
||||
@@ -95,10 +99,7 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
const date = new Date(year, month - 1, day);
|
||||
|
||||
setCertificateExpiry(
|
||||
formatDate(date, {
|
||||
hour: undefined,
|
||||
minute: undefined,
|
||||
}) ?? "N/A"
|
||||
formatDateWithTz(date, dateFormat, uiTimezone) ?? "N/A"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -367,12 +368,11 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
fontSize={11}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
{formatDate(new Date(hoveredUptimeData.time), {
|
||||
month: "short",
|
||||
year: undefined,
|
||||
minute: undefined,
|
||||
hour: dateRange === "day" ? "numeric" : undefined,
|
||||
})}
|
||||
{formatDateWithTz(
|
||||
hoveredUptimeData.time,
|
||||
dateFormat,
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -417,12 +417,11 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
fontSize={11}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
{formatDate(new Date(hoveredIncidentsData.time), {
|
||||
month: "short",
|
||||
year: undefined,
|
||||
minute: undefined,
|
||||
hour: dateRange === "day" ? "numeric" : undefined,
|
||||
})}
|
||||
{formatDateWithTz(
|
||||
hoveredIncidentsData.time,
|
||||
dateFormat,
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { getLastChecked } from "../../Utils/monitorUtils";
|
||||
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { Box, Grid, Stack, Typography } from "@mui/material";
|
||||
@@ -59,15 +57,6 @@ const Card = ({ monitor }) => {
|
||||
<Typography fontSize={13}>
|
||||
{monitor.url.replace(/^https?:\/\//, "")}
|
||||
</Typography>
|
||||
<Typography mt={theme.spacing(12)}>
|
||||
<Typography component="span" fontWeight={600}>
|
||||
Last checked:{" "}
|
||||
</Typography>
|
||||
{formatDate(getLastChecked(monitor.checks, false))}{" "}
|
||||
<Typography component="span" fontStyle="italic">
|
||||
({formatDurationRounded(getLastChecked(monitor.checks))} ago)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
@@ -14,14 +14,18 @@ import {
|
||||
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import PropTypes from "prop-types";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { setTimezone } from "../../Features/UI/uiSlice";
|
||||
import timezones from "../../Utils/timezones.json";
|
||||
|
||||
const Settings = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const { timezone } = useSelector((state) => state.ui);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// TODO Handle saving
|
||||
const handleSave = async () => {};
|
||||
|
||||
const handleClearStats = async () => {
|
||||
try {
|
||||
@@ -115,25 +119,16 @@ const Settings = ({ isAdmin }) => {
|
||||
<Typography component="span">Display timezone</Typography>- The
|
||||
timezone of the dashboard you publicly display.
|
||||
</Typography>
|
||||
<Typography>
|
||||
<Typography component="span">Server timezone</Typography>- The
|
||||
timezone of your server.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
id="display-timezone"
|
||||
label="Display timezone"
|
||||
value="est"
|
||||
onChange={() => logger.warn("disabled")}
|
||||
items={[{ _id: "est", name: "America / Toronto" }]}
|
||||
/>
|
||||
<Select
|
||||
id="server-timezone"
|
||||
label="Server timezone"
|
||||
value="est"
|
||||
onChange={() => logger.warn("disabled")}
|
||||
items={[{ _id: "est", name: "America / Toronto" }]}
|
||||
value={timezone}
|
||||
onChange={(e) => {
|
||||
dispatch(setTimezone({ timezone: e.target.value }));
|
||||
}}
|
||||
items={timezones}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@@ -232,6 +227,7 @@ const Settings = ({ isAdmin }) => {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export const formatDuration = (ms) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
@@ -76,3 +82,8 @@ export const formatDate = (date, customOptions) => {
|
||||
.toLocaleString("en-US", options)
|
||||
.replace(/\b(AM|PM)\b/g, (match) => match.toLowerCase());
|
||||
};
|
||||
|
||||
export const formatDateWithTz = (timestamp, format, timezone) => {
|
||||
const formattedDate = dayjs(timestamp, timezone).tz(timezone).format(format);
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
1698
Client/src/Utils/timezones.json
Normal file
1698
Client/src/Utils/timezones.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user