diff --git a/Client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/Client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx new file mode 100644 index 000000000..5484202d5 --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx @@ -0,0 +1,106 @@ +// Components +import { Stack, Typography } from "@mui/material"; +import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded"; +import DeleteIcon from "@mui/icons-material/Delete"; + +// Utils +import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; +import { useTheme } from "@emotion/react"; +const MonitorListItem = ({ + monitor, + innerRef, + draggableProps, + dragHandleProps, + onDelete, +}) => { + const theme = useTheme(); + return ( + + + {monitor.name} + { + onDelete(monitor); + }} + /> + + ); +}; + +const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { + const onDelete = (monitorToDelete) => { + const newMonitors = selectedMonitors.filter( + (monitor) => monitor._id !== monitorToDelete._id + ); + setSelectedMonitors(newMonitors); + }; + const reorder = (list, startIndex, endIndex) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; + }; + + const onDragEnd = (result) => { + // dropped outside the list + if (!result.destination) { + return; + } + + const reorderedMonitors = reorder( + selectedMonitors, + result.source.index, + result.destination.index + ); + + setSelectedMonitors(reorderedMonitors); + }; + + return ( + + + {(provided, snapshot) => ( + + {selectedMonitors?.map((monitor, index) => ( + + {(provided, snapshot) => ( + + )} + + ))} + {provided.placeholder} + + )} + + + ); +}; + +export default MonitorList; diff --git a/Client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx b/Client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx new file mode 100644 index 000000000..482c490fb --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx @@ -0,0 +1,14 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx index 763863ec7..6dd08acb0 100644 --- a/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx +++ b/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx @@ -1,12 +1,115 @@ -import { Stack, Typography } from "@mui/material"; +// Components +import { Stack, Typography, Button } from "@mui/material"; import { TabPanel } from "@mui/lab"; +import ConfigBox from "../../../../../Components/ConfigBox"; +import MonitorList from "../MonitorList"; +import Search from "../../../../../Components/Inputs/Search"; +import Checkbox from "../../../../../Components/Inputs/Checkbox"; +// Utils +import { useState } from "react"; +import { useTheme } from "@emotion/react"; +const Content = ({ + tabValue, + form, + monitors, + handleFormChange, + errors, + selectedMonitors, + setSelectedMonitors, +}) => { + // Local state + const [search, setSearch] = useState(""); + + // Handlers + const handleMonitorsChange = (selectedMonitors) => { + handleFormChange({ + target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor._id) }, + }); + setSelectedMonitors(selectedMonitors); + }; + + // Utils + const theme = useTheme(); -const Content = ({ tabValue }) => { return ( - - Content + + + + Status page servers + + You can add any number of servers that you monitor to your status page. You + can also reorder them for the best viewing experience. + + + + + {/* + {" "} + Servers list{" "} + + */} + + + + {errors["monitors"]} + + + + {" "} + + + Features + Show more details on the status page + + + + + + + ); }; diff --git a/Client/src/Pages/StatusPage/Create/Components/Tabs/Skeleton.jsx b/Client/src/Pages/StatusPage/Create/Components/Tabs/Skeleton.jsx new file mode 100644 index 000000000..fb0cd6ca8 --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/Tabs/Skeleton.jsx @@ -0,0 +1,14 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx b/Client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx new file mode 100644 index 000000000..b81b70c7b --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx @@ -0,0 +1,59 @@ +import { TabContext, TabList } from "@mui/lab"; +import { Stack, Tab } from "@mui/material"; +import Settings from "./Settings"; +import Content from "./Content"; +const Tabs = ({ + form, + errors, + monitors, + selectedMonitors, + setSelectedMonitors, + handleFormChange, + handleImageChange, + progress, + removeLogo, + tab, + setTab, + TAB_LIST, +}) => { + return ( + + { + setTab(TAB_LIST.indexOf(tab)); + }} + > + {TAB_LIST.map((tab, idx) => ( + + ))} + + {tab === 0 ? ( + + ) : ( + + )} + + ); +}; + +export default Tabs; diff --git a/Client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/Client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx new file mode 100644 index 000000000..eeb309b28 --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx @@ -0,0 +1,28 @@ +import { useState } from "react"; +import { networkService } from "../../../../main"; +import { useSelector } from "react-redux"; +import { createToast } from "../../../../Utils/toastUtils"; + +const useCreateStatusPage = () => { + const { authToken, user } = useSelector((state) => state.auth); + + const [isLoading, setIsLoading] = useState(false); + const [networkError, setNetworkError] = useState(false); + + const createStatusPage = async ({ form }) => { + try { + await networkService.createStatusPage({ authToken, user, form }); + return true; + } catch (error) { + setNetworkError(true); + createToast({ body: error?.response?.data?.msg ?? error.message }); + return false; + } finally { + setIsLoading(false); + } + }; + + return [createStatusPage, isLoading, networkError]; +}; + +export { useCreateStatusPage }; diff --git a/Client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/Client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx new file mode 100644 index 000000000..4813157d1 --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { useSelector } from "react-redux"; +import { createToast } from "../../../../Utils/toastUtils"; + +const useMonitorsFetch = () => { + const { user, authToken } = useSelector((state) => state.auth); + + const [monitors, setMonitors] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + useEffect(() => { + const fetchMonitors = async () => { + try { + const response = await networkService.getMonitorsByTeamId({ + authToken: authToken, + teamId: user.teamId, + limit: null, // donot return any checks for the monitors + types: ["http"], // status page is available only for the uptime type + }); + setMonitors(response.data.data.monitors); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [authToken, user]); + + return [monitors, isLoading, networkError]; +}; + +export { useMonitorsFetch }; diff --git a/Client/src/Pages/StatusPage/PublicStatus/Hooks/useStatusPageFetch.jsx b/Client/src/Pages/StatusPage/PublicStatus/Hooks/useStatusPageFetch.jsx index 5af297679..8bc7e4f11 100644 --- a/Client/src/Pages/StatusPage/PublicStatus/Hooks/useStatusPageFetch.jsx +++ b/Client/src/Pages/StatusPage/PublicStatus/Hooks/useStatusPageFetch.jsx @@ -1,9 +1,33 @@ import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { useSelector } from "react-redux"; +import { createToast } from "../../../../Utils/toastUtils"; const useStatusPageFetch = () => { const [isLoading, setIsLoading] = useState(true); const [networkError, setNetworkError] = useState(false); const [statusPage, setStatusPage] = useState(undefined); + const { authToken } = useSelector((state) => state.auth); + + useEffect(() => { + const fetchStatusPage = async () => { + try { + const res = await networkService.getStatusPage({ authToken }); + setStatusPage(res.data.data); + } catch (error) { + // If there is a 404, status page is not found + if (error?.response?.status === 404) { + setStatusPage(undefined); + return; + } + createToast({ body: error.message }); + setNetworkError(true); + } finally { + setIsLoading(false); + } + }; + fetchStatusPage(); + }, [authToken]); return [statusPage, isLoading, networkError]; }; diff --git a/Client/src/Pages/StatusPage/PublicStatus/index.jsx b/Client/src/Pages/StatusPage/PublicStatus/index.jsx index f3503e4f5..6e8f6ffb2 100644 --- a/Client/src/Pages/StatusPage/PublicStatus/index.jsx +++ b/Client/src/Pages/StatusPage/PublicStatus/index.jsx @@ -12,7 +12,6 @@ const PublicStatus = () => { const theme = useTheme(); const isAdmin = useIsAdmin(); const [statusPage, isLoading, networkError] = useStatusPageFetch(); - if (networkError === true) { return ( diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 176c6d093..a1e0c0426 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -862,9 +862,9 @@ class NetworkService { ); } - async getStatusPageByUrl(config) { - const { url, authToken } = config; - return this.axiosInstance.get(`/status-page/${url}`, { + async getStatusPage(config) { + const { authToken } = config; + return this.axiosInstance.get(`/status-page`, { headers: { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json", @@ -873,11 +873,28 @@ class NetworkService { } async createStatusPage(config) { - const { url, authToken, data } = config; - return this.axiosInstance.post(`/status-page/${url}`, data, { + const { authToken, user, form } = config; + const fd = new FormData(); + fd.append("isPublished", form.isPublished); + fd.append("companyName", form.companyName); + fd.append("url", form.url); + fd.append("timezone", form.timezone); + fd.append("color", form.color); + fd.append("showCharts", form.showCharts); + fd.append("showUptimePercentage", form.showUptimePercentage); + form.monitors.forEach((monitorId) => { + fd.append("monitors[]", monitorId); + }); + if (form?.logo?.src && form?.logo?.src !== "") { + const imageResult = await axios.get(form.logo.src, { + responseType: "blob", + }); + fd.append("logo", imageResult.data); + } + + return this.axiosInstance.post(`/status-page`, fd, { headers: { Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", }, }); }