implement DU status page
This commit is contained in:
1 parent
08b0ed9d3b
commit
6a1675aa4a
7 files changed
+420
-4
No files matched your search
+22
@@ -0,0 +1,22 @@
|
||||
const VisuallyHiddenInput = ({ onChange }) => {
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={onChange}
|
||||
style={{
|
||||
clip: "rect(0 0 0 0)",
|
||||
clipPath: "inset(50%)",
|
||||
height: 1,
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: "nowrap",
|
||||
width: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisuallyHiddenInput;
|
||||
@@ -0,0 +1,217 @@
|
||||
// Components
|
||||
import { Stack, Typography, Button, Box } from "@mui/material";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import VisuallyHiddenInput from "./Components/VisuallyHiddenInput";
|
||||
import Image from "../../../Components/Image";
|
||||
import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { statusPageValidation } from "../../../Validation/validation";
|
||||
import { buildErrors } from "../../../Validation/error";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const CreateStatus = () => {
|
||||
const theme = useTheme();
|
||||
const { monitorId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isCreate = location.pathname.startsWith("/distributed-uptime/status/create");
|
||||
const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate);
|
||||
const BREADCRUMBS = [
|
||||
{ name: "distributed uptime", path: "/distributed-uptime" },
|
||||
{ name: "details", path: `/distributed-uptime/${monitorId}` },
|
||||
{ name: "create status page", path: `` },
|
||||
];
|
||||
// Local state
|
||||
const [form, setForm] = useState({
|
||||
isPublished: false,
|
||||
url: Math.floor(Math.random() * 1000000).toFixed(0),
|
||||
logo: undefined,
|
||||
companyName: "",
|
||||
monitors: [monitorId],
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleFormChange = (e) => {
|
||||
const { name, value, checked, type } = e.target;
|
||||
|
||||
// Check for errors
|
||||
const { error } = statusPageValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => buildErrors(prev, name, error));
|
||||
|
||||
if (type === "checkbox") {
|
||||
setForm({ ...form, [name]: checked });
|
||||
return;
|
||||
}
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
|
||||
const handleImageUpload = (e) => {
|
||||
const img = e.target?.files?.[0];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
logo: img,
|
||||
}));
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
let logoToSubmit = undefined;
|
||||
|
||||
// Handle image
|
||||
if (typeof form.logo !== "undefined") {
|
||||
logoToSubmit = {
|
||||
src: URL.createObjectURL(form.logo),
|
||||
name: form.logo.name,
|
||||
type: form.logo.type,
|
||||
size: form.logo.size,
|
||||
};
|
||||
}
|
||||
const formToSubmit = { ...form };
|
||||
if (typeof logoToSubmit !== "undefined") {
|
||||
formToSubmit.logo = logoToSubmit;
|
||||
}
|
||||
|
||||
// Validate
|
||||
const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false });
|
||||
if (typeof error === "undefined") {
|
||||
const success = await createStatusPage({ form: formToSubmit });
|
||||
if (success) {
|
||||
createToast({ body: "Status page created successfully" });
|
||||
navigate(`/distributed-uptime/status/${form.url}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newErrors = {};
|
||||
error?.details?.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors((prev) => ({ ...prev, ...newErrors }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Typography variant="h1">
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
>
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h2"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
status page
|
||||
</Typography>
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Stack>
|
||||
<Typography component="h2">Access</Typography>
|
||||
<Typography component="p">
|
||||
If your status page is ready, you can mark it as published.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(18)}>
|
||||
<Checkbox
|
||||
id="publish"
|
||||
name="isPublished"
|
||||
label={`Published and visible to the public`}
|
||||
isChecked={form.isPublished}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Basic Information</Typography>
|
||||
<Typography component="p">
|
||||
Define company name and the subdomain that your status page points to.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(18)}>
|
||||
<TextInput
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
type="text"
|
||||
label="Company name"
|
||||
placeholder="Company name"
|
||||
value={form.companyName}
|
||||
onChange={handleFormChange}
|
||||
helperText={errors["companyName"]}
|
||||
error={errors["companyName"] ? true : false}
|
||||
/>
|
||||
<TextInput
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
label="Your status page address"
|
||||
value={form.url}
|
||||
onChange={handleFormChange}
|
||||
helperText={errors["url"]}
|
||||
error={errors["url"] ? true : false}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Logo</Typography>
|
||||
<Typography component="p">Upload a logo for your status page </Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.spacing(18)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Image
|
||||
src={form.logo ? URL.createObjectURL(form.logo) : undefined}
|
||||
alt="Logo"
|
||||
minWidth={"300px"}
|
||||
minHeight={"100px"}
|
||||
maxWidth={"300px"}
|
||||
maxHeight={"300px"}
|
||||
placeholder={LogoPlaceholder}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
component="label"
|
||||
role={undefined}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Upload logo
|
||||
<VisuallyHiddenInput onChange={handleImageUpload} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStatus;
|
||||
@@ -10,21 +10,23 @@ import StatBoxes from "./Components/StatBoxes";
|
||||
import MonitorHeader from "./Components/MonitorHeader";
|
||||
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useSubscribeToDetails from "./Hooks/useSubscribeToDetails";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useSubscribeToDetails } from "./Hooks/useSubscribeToDetails";
|
||||
|
||||
const DistributedUptimeDetails = () => {
|
||||
const { monitorId } = useParams();
|
||||
|
||||
// Local State
|
||||
const [dateRange, setDateRange] = useState("day");
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
|
||||
useSubscribeToDetails({ monitorId, dateRange });
|
||||
// Constants
|
||||
@@ -65,6 +67,11 @@ const DistributedUptimeDetails = () => {
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
label="Create status page"
|
||||
isAdmin={isAdmin}
|
||||
path={`/distributed-uptime/status/create/${monitorId}`}
|
||||
/>
|
||||
<MonitorHeader monitor={monitor} />
|
||||
<StatBoxes
|
||||
monitor={monitor}
|
||||
|
||||
@@ -79,7 +79,7 @@ const MonitorTable = ({ isLoading, monitors }) => {
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/distributed-uptime/${row.id}`);
|
||||
navigate(`/distributed-uptime/${row._id}`);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const useStatusPageFetchByUrl = ({ url }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [statusPage, setStatusPage] = useState(undefined);
|
||||
const [monitorId, setMonitorId] = useState(undefined);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
useEffect(() => {
|
||||
const fetchStatusPageByUrl = async () => {
|
||||
try {
|
||||
const response = await networkService.getStatusPageByUrl({ authToken, url });
|
||||
if (!response?.data?.data) return;
|
||||
const statusPage = response.data.data;
|
||||
setStatusPage(statusPage);
|
||||
setMonitorId(statusPage?.monitors[0]);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchStatusPageByUrl();
|
||||
}, [authToken, url]);
|
||||
|
||||
return [isLoading, networkError, statusPage, monitorId];
|
||||
};
|
||||
|
||||
export { useStatusPageFetchByUrl };
|
||||
@@ -0,0 +1,135 @@
|
||||
//Components
|
||||
import DistributedUptimeMap from "../Details/Components/DistributedUptimeMap";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import DeviceTicker from "../Details/Components/DeviceTicker";
|
||||
import DistributedUptimeResponseChart from "../Details/Components/DistributedUptimeResponseChart";
|
||||
import NextExpectedCheck from "../Details/Components/NextExpectedCheck";
|
||||
import Footer from "../Details/Components/Footer";
|
||||
import StatBoxes from "../Details/Components/StatBoxes";
|
||||
import ControlsHeader from "../../StatusPage/Status/Components/ControlsHeader";
|
||||
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useSubscribeToDetails } from "../Details/Hooks/useSubscribeToDetails";
|
||||
import { useStatusPageFetchByUrl } from "./Hooks/useStatusPageFetchByUrl";
|
||||
import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
const DistributedUptimeStatus = () => {
|
||||
const { url } = useParams();
|
||||
// Local State
|
||||
const [dateRange, setDateRange] = useState("day");
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const [statusPageIsLoading, statusPageNetworkError, statusPage, monitorId] =
|
||||
useStatusPageFetchByUrl({
|
||||
url,
|
||||
});
|
||||
|
||||
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
|
||||
useSubscribeToDetails({ monitorId, dateRange });
|
||||
|
||||
const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => {
|
||||
navigate("/distributed-uptime");
|
||||
}, url);
|
||||
// Constants
|
||||
const BREADCRUMBS = [
|
||||
{ name: "Distributed Uptime", path: "/distributed-uptime" },
|
||||
{ name: "status", path: `` },
|
||||
];
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
Network error
|
||||
</Typography>
|
||||
<Typography>Please check your connection</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof monitor === "undefined" || monitor.totalChecks === 0) {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<GenericFallback>
|
||||
<Typography>There is no check history for this monitor yet.</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<ControlsHeader
|
||||
statusPage={statusPage}
|
||||
isDeleting={isDeleting}
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
/>
|
||||
<StatBoxes
|
||||
monitor={monitor}
|
||||
lastUpdateTrigger={lastUpdateTrigger}
|
||||
/>
|
||||
<NextExpectedCheck
|
||||
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
|
||||
interval={monitor?.interval ?? 0}
|
||||
trigger={lastUpdateTrigger}
|
||||
/>
|
||||
<MonitorTimeFrameHeader
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<DistributedUptimeMap
|
||||
checks={monitor?.groupedMapChecks ?? []}
|
||||
height={"100%"}
|
||||
width={"100%"}
|
||||
/>
|
||||
<DeviceTicker
|
||||
width={"25vw"}
|
||||
data={monitor?.latestChecks ?? []}
|
||||
connectionStatus={connectionStatus}
|
||||
/>
|
||||
</Stack>
|
||||
<Footer />
|
||||
<Dialog
|
||||
// open={isOpen.deleteStats}
|
||||
title="Do you want to delete this status page?"
|
||||
onConfirm={() => {
|
||||
deleteStatusPage();
|
||||
setIsDeleteOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteOpen(false);
|
||||
}}
|
||||
open={isDeleteOpen}
|
||||
confirmationButtonLabel="Yes, delete status page"
|
||||
description="Once deleted, your status page cannot be retrieved."
|
||||
isLoading={isDeleting || isLoading}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistributedUptimeStatus;
|
||||
@@ -126,7 +126,7 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => openDetails(row.id),
|
||||
onRowClick: (row) => openDetails(row._id),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user