diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx index de20fc005..e5091091e 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -91,7 +91,7 @@ export const BasePage = ({ spacing={theme.spacing(10)} {...props} > - + {children} ); diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx index e0a310c9f..ae3e768c2 100644 --- a/client/src/Components/v2/design-elements/Breadcrumb.tsx +++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx @@ -4,6 +4,7 @@ import { Link, useLocation } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; import { ChevronRight } from "lucide-react"; import { useTranslation } from "react-i18next"; +import type { ReactNode } from "react"; const isId = (segment: string): boolean => { return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment); @@ -11,28 +12,8 @@ const isId = (segment: string): boolean => { const actionSegments = ["create", "configure"]; -export const Breadcrumb = () => { - const { t } = useTranslation(); +const BreadcrumbWrapper = ({ children }: { children: ReactNode }) => { const theme = useTheme(); - const location = useLocation(); - - const segments = location.pathname.split("/").filter((x) => x); - - // Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create" - const basePage = segments[0] || t("common.breadcrumbs.home"); - - const secondSegment = segments[1]; - const isActionPage = secondSegment && actionSegments.includes(secondSegment); // create/config - const isDetailsPage = secondSegment && isId(secondSegment); // details - const hasSubPage = isActionPage || isDetailsPage; - - const getSubPageLabel = (): string => { - if (isActionPage) { - return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1); - } - return t("common.breadcrumbs.details"); - }; - return ( { }, }} > + {children} + + ); +}; + +export const Breadcrumb = ({ + breadcrumbOverride, +}: { + breadcrumbOverride?: string[] | undefined; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const location = useLocation(); + + // If override is an empty array, hide entirely + if (breadcrumbOverride !== undefined && breadcrumbOverride.length === 0) { + return null; + } + + // If override has items, render them directly + if (breadcrumbOverride !== undefined && breadcrumbOverride.length > 0) { + return ( + + {breadcrumbOverride.map((item, index) => { + const isLast = index === breadcrumbOverride.length - 1; + return ( + + {item} + + ); + })} + + ); + } + + // Default behavior: use location pathname + const segments = location.pathname.split("/").filter((x) => x); + + if (segments.length === 0) { + return null; + } + + // Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create" + const basePage = segments[0] || t("common.breadcrumbs.home"); + + const secondSegment = segments[1]; + const isActionPage = secondSegment && actionSegments.includes(secondSegment); + const isDetailsPage = secondSegment && isId(secondSegment); + const hasSubPage = isActionPage || isDetailsPage; + + const getSubPageLabel = (): string => { + if (isActionPage) { + return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1); + } + return t("common.breadcrumbs.details"); + }; + + return ( + {hasSubPage ? ( { {getSubPageLabel()} )} - + ); }; diff --git a/client/src/Components/v2/inputs/Switch.tsx b/client/src/Components/v2/inputs/Switch.tsx index b8ae8c1ec..05be73a24 100644 --- a/client/src/Components/v2/inputs/Switch.tsx +++ b/client/src/Components/v2/inputs/Switch.tsx @@ -3,8 +3,12 @@ import Switch from "@mui/material/Switch"; import type { SwitchProps } from "@mui/material/Switch"; import { useTheme } from "@mui/material/styles"; -export const SwitchComponent = forwardRef( - function SwitchComponent({ sx, ...props }, ref) { +interface SwitchComponentProps extends SwitchProps { + dualOption?: boolean; +} + +export const SwitchComponent = forwardRef( + function SwitchComponent({ sx, dualOption = false, ...props }, ref) { const theme = useTheme(); const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; @@ -28,6 +32,12 @@ export const SwitchComponent = forwardRef( }, }, }, + ...(dualOption && { + "& .MuiSwitch-track": { + backgroundColor: theme.palette.primary.main, + opacity: 1, + }, + }), }, ...additionalSx, ]} diff --git a/client/src/Components/v2/monitors/HeaderMonitorControls.tsx b/client/src/Components/v2/monitors/HeaderMonitorControls.tsx index d1a0d078d..6d321cbb8 100644 --- a/client/src/Components/v2/monitors/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/monitors/HeaderMonitorControls.tsx @@ -95,7 +95,7 @@ export const HeaderMonitorControls = ({ await refetch(); }} > - {monitor?.isActive ? t("pause") : t("resume")} + {monitor?.isActive ? t("common.buttons.pause") : t("common.buttons.resume")} )} {isAdmin && ( @@ -105,7 +105,7 @@ export const HeaderMonitorControls = ({ startIcon={} onClick={() => navigate(`/${path}/configure/${monitor.id}`)} > - Configure + {t("common.buttons.configure")} )} @@ -155,7 +155,7 @@ export const HeaderDeleteControls = ({ await refetch(); }} > - {monitor?.isActive ? t("pause") : t("resume")} + {monitor?.isActive ? t("common.buttons.pause") : t("common.buttons.resume")} )} {isAdmin && ( diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/index.jsx index 4eb171232..c5aee4b0a 100644 --- a/client/src/Pages/StatusPage/Create/index.jsx +++ b/client/src/Pages/StatusPage/Create/index.jsx @@ -17,7 +17,6 @@ import { useNavigate } from "react-router-dom"; import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete.jsx"; //Constants const ERROR_TAB_MAPPING = [ ["companyName", "url", "timezone", "color", "isPublished", "logo"], @@ -60,7 +59,7 @@ const CreateStatusPage = () => { const [createStatusPage] = useCreateStatusPage(isCreate); const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] = useStatusPageFetch(isCreate, url); - const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url); + // const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url); // Handlers const handleFormChange = (e) => { @@ -133,7 +132,7 @@ const CreateStatusPage = () => { const handleDelete = async () => { setIsDeleteOpen(false); // Start deletion process but don't wait for it - deleteStatusPage(); + // deleteStatusPage(); // Immediately navigate away to prevent additional fetches for the deleted page navigate("/status"); }; @@ -252,7 +251,7 @@ const CreateStatusPage = () => { justifyContent="flex-end" > - - - ); -}; - -Controls.propTypes = { - type: PropTypes.string, - url: PropTypes.string, -}; - -const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const publicUrl = `/status/uptime/public/${url}`; - - return ( - - - {"Company - - {statusPage?.companyName} - - {statusPage?.isPublished && !isPublic && ( - { - window.open(publicUrl, "_blank", "noopener,noreferrer"); - }} - sx={{ - display: "inline-flex", - ":hover": { - cursor: "pointer", - borderBottom: 1, - }, - }} - > - {t("publicLink")} - - - )} - - - {isPublic && } - - ); -}; - -ControlsHeader.propTypes = { - url: PropTypes.string, - statusPage: PropTypes.object, - isPublic: PropTypes.bool, - type: PropTypes.string, -}; - -export default ControlsHeader; diff --git a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx new file mode 100644 index 000000000..4fc2e41e1 --- /dev/null +++ b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx @@ -0,0 +1,87 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Icon } from "@/Components/v2/design-elements"; +import { Button } from "@/Components/v2/inputs"; +import { Settings, ExternalLink } from "lucide-react"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import type { StatusPage } from "@/Types/StatusPage"; + +interface HeaderStatusPageControlsProps { + isAdmin: boolean; + statusPage: StatusPage; + isPublic?: boolean; +} +export const HeaderStatusPageControls = ({ + isAdmin, + statusPage, + isPublic = false, +}: HeaderStatusPageControlsProps) => { + const theme = useTheme(); + const navigate = useNavigate(); + const { t } = useTranslation(); + return ( + + + + {statusPage?.companyName} + + {statusPage?.isPublished && !isPublic && ( + <> + { + window.open( + `/status/uptime/public/${statusPage.url}`, + "_blank", + "noopener,noreferrer" + ); + }} + sx={{ + borderBottom: 1, + borderColor: "transparent", + ":hover": { + cursor: "pointer", + borderBottom: 1, + }, + }} + > + {t("publicLink")} + + + + + + )} + + {isAdmin && !isPublic && ( + + )} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx new file mode 100644 index 000000000..d559e7c82 --- /dev/null +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -0,0 +1,113 @@ +import Stack from "@mui/material/Stack"; +import { useTranslation } from "react-i18next"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/v2/common"; +import { StatusLabel, BaseBox } from "@/Components/v2/design-elements"; +import { SwitchComponent } from "@/Components/v2/inputs"; + +import { useTheme } from "@mui/material/styles"; +import { determineState } from "@/Utils/MonitorUtils"; +import { useSelector } from "react-redux"; +import { useState } from "react"; +import type { Monitor } from "@/Types/Monitor"; +import type { StatusPage } from "@/Types/StatusPage"; +import type { RootState } from "@/Types/state"; + +interface StatusPageMonitor extends Monitor { + checks?: Monitor["recentChecks"]; +} + +interface MonitorsListProps { + statusPage: StatusPage; + monitors: StatusPageMonitor[]; +} + +export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const showURL = useSelector((state: RootState) => state.ui?.showURL); + + const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram"); + + return ( + + {statusPage.showCharts !== false && ( + + {t("pages.statusPages.monitorsList.chartTypeHeatmap")} + { + setChartType(e.target.checked ? "histogram" : "heatmap"); + }} + /> + + {t("pages.statusPages.monitorsList.chartTypeHistogram")} + + + )} + + {monitors?.map((monitor) => { + const status = determineState(monitor); + return ( + + + + + {monitor.name} + + {showURL && ( + + {monitor.url} + + )} + + + + {statusPage.showCharts !== false && ( + + {chartType === "histogram" ? ( + + ) : ( + + )} + + )} + + ); + })} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx deleted file mode 100644 index 33f771233..000000000 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// Components -import { Stack, Box } from "@mui/material"; -import Host from "@/Components/v1/Host/index.jsx"; -import StatusPageBarChart from "@/Components/v1/Charts/StatusPageBarChart/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; - -//Utils -import { useTheme } from "@mui/material/styles"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; -import PropTypes from "prop-types"; - -import { useSelector } from "react-redux"; - -const MonitorsList = ({ - isLoading = false, - shouldRender = true, - monitors = [], - statusPage = {}, -}) => { - const theme = useTheme(); - const { determineState } = useMonitorUtils(); - - const { showURL } = useSelector((state) => state.ui); - - return ( - <> - {monitors?.map((monitor) => { - const status = determineState(monitor); - return ( - - - - {statusPage.showCharts !== false && ( - - - - )} - - - ); - })} - - ); -}; - -MonitorsList.propTypes = { - monitors: PropTypes.array.isRequired, - statusPage: PropTypes.object, -}; - -export default MonitorsList; diff --git a/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx b/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx deleted file mode 100644 index 482c490fb..000000000 --- a/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx new file mode 100644 index 000000000..c5aca4754 --- /dev/null +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx @@ -0,0 +1,62 @@ +import { AlertTriangle, CircleCheck } from "lucide-react"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; + +import { useTranslation } from "react-i18next"; +import type { Theme } from "@mui/material"; +import { useTheme } from "@mui/material"; +import type { Monitor } from "@/Types/Monitor"; + +const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => { + const monitorsStatus: Record = { + icon: , + }; + + if (monitors.every((monitor) => monitor.status === true)) { + monitorsStatus.msg = t("pages.statusPages.statusBar.allUp"); + monitorsStatus.color = theme.palette.success.main; + monitorsStatus.icon = ; + } + + if (monitors.every((monitor) => monitor.status === false)) { + monitorsStatus.msg = t("pages.statusPages.statusBar.allDown"); + monitorsStatus.color = theme.palette.error.main; + } + + if (monitors.some((monitor) => monitor.status === false)) { + monitorsStatus.msg = t("pages.statusPages.statusBar.degraded"); + monitorsStatus.color = theme.palette.warning.main; + } + + // Paused or unknown + if (monitors.some((monitor) => typeof monitor.status === "undefined")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.unknown"); + monitorsStatus.color = theme.palette.warning.main; + } + return monitorsStatus; +}; + +interface StatusBarProps { + monitors: Monitor[]; +} + +export const StatusBar = ({ monitors }: StatusBarProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const monitorsStatus = getMonitorStatus(monitors, theme, t); + + return ( + + {monitorsStatus.icon} + {monitorsStatus.msg} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx deleted file mode 100644 index 7b9460068..000000000 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx +++ /dev/null @@ -1,77 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; - -// Utils -import { useTheme } from "@mui/material/styles"; -import PropTypes from "prop-types"; - -const getMonitorStatus = (monitors, theme) => { - const monitorsStatus = { - icon: ( - - ), - }; - if (monitors.every((monitor) => monitor.status === true)) { - monitorsStatus.msg = "All systems operational"; - monitorsStatus.color = theme.palette.success.lowContrast; - monitorsStatus.icon = ( - - ); - } - - if (monitors.every((monitor) => monitor.status === false)) { - monitorsStatus.msg = "All systems down"; - monitorsStatus.color = theme.palette.error.lowContrast; - } - - if (monitors.some((monitor) => monitor.status === false)) { - monitorsStatus.msg = "Degraded performance"; - monitorsStatus.color = theme.palette.warning.lowContrast; - } - - // Paused or unknown - if (monitors.some((monitor) => typeof monitor.status === "undefined")) { - monitorsStatus.msg = "Unknown status"; - monitorsStatus.color = theme.palette.warning.lowContrast; - } - return monitorsStatus; -}; - -const StatusBar = ({ monitors }) => { - const theme = useTheme(); - - if (typeof monitors === "undefined") return; - - const monitorsStatus = getMonitorStatus(monitors, theme); - return ( - - {monitorsStatus.icon} - {/* CAIO_REVIEW */} - {monitorsStatus.msg} - - ); -}; - -export default StatusBar; - -StatusBar.propTypes = { - status: PropTypes.object, -}; diff --git a/client/src/Pages/StatusPage/Status/index.jsx b/client/src/Pages/StatusPage/Status/index.jsx deleted file mode 100644 index adcd9c2c7..000000000 --- a/client/src/Pages/StatusPage/Status/index.jsx +++ /dev/null @@ -1,166 +0,0 @@ -// Components -import { Typography, Stack } from "@mui/material"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import AdminLink from "./Components/AdminLink/index.jsx"; -import ControlsHeader from "./Components/ControlsHeader/index.jsx"; -import SkeletonLayout from "./Components/Skeleton/index.jsx"; -import StatusBar from "./Components/StatusBar/index.jsx"; -import MonitorsList from "./Components/MonitorsList/index.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import TextLink from "@/Components/v1/TextLink/index.jsx"; - -// Utils -import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx"; -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useLocation } from "react-router-dom"; -import { useParams } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -const PublicStatus = () => { - const { url } = useParams(); - - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - const location = useLocation(); - const isAdmin = useIsAdmin(); - - const [statusPage, monitors, isLoading, networkError] = useStatusPageFetch(false, url); - - // Breadcrumbs - const crumbs = [ - { name: t("statusBreadCrumbsStatusPages"), path: "/status" }, - { name: t("statusBreadCrumbsDetails"), path: `/status/uptime/${statusPage?.url}` }, - ]; - - // Setup - let sx = { paddingLeft: theme.spacing(20), paddingRight: theme.spacing(20) }; - let link = undefined; - const isPublic = location.pathname.startsWith("/status/uptime/public"); - // Public status page - if (isPublic && statusPage && statusPage.showAdminLoginLink === true) { - sx = { - paddingTop: theme.spacing(20), - paddingLeft: "20vw", - paddingRight: "20vw", - }; - link = ; - } - - // Loading - if (isLoading) { - return ; - } - - if (monitors?.length === 0) { - return ( - - - {"Theres nothing here yet"} - - {isAdmin && ( - - )} - - ); - } - - // Error fetching data - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Public status page fallback - if (!isLoading && typeof statusPage === "undefined" && isPublic) { - return ( - - - - {t("statusPageStatus")} - - {t("statusPageStatusContactAdmin")} - - - ); - } - - // Finished loading, but status page is not public - if (!isLoading && isPublic && statusPage.isPublished === false) { - return ( - - - - {t("statusPageStatusNotPublic")} - - {t("statusPageStatusContactAdmin")} - - - ); - } - - // Status page doesn't exist - if (!isLoading && typeof statusPage === "undefined") { - return ( - - - {t("statusPageStatusNoPage")} - - {t("statusPageStatusContactAdmin")} - - ); - } - - return ( - - {!isPublic && } - - {t("statusPageStatusServiceStatus")} - - - {link} - - ); -}; - -export default PublicStatus; diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx new file mode 100644 index 000000000..41373b19a --- /dev/null +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -0,0 +1,60 @@ +import { BasePage } from "@/Components/v2/design-elements"; +import { StatusBar } from "@/Pages/StatusPage/Status/Components/StatusBar"; +import { MonitorsList } from "@/Pages/StatusPage/Status/Components/MonitorsList"; +import Typography from "@mui/material/Typography"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import { useLocation, useParams } from "react-router-dom"; +import { useGet } from "@/Hooks/UseApi"; +import type { StatusPageResponse } from "@/Types/StatusPage"; +import { HeaderStatusPageControls } from "./Components/HeaderStatusPageControls"; + +const StatusPageView = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const { url } = useParams(); + const isAdmin = useIsAdmin(); + const location = useLocation(); + const isPublic = location.pathname.startsWith("/status/uptime/public"); + + const apiUrl = url ? `/status-page/${url}?type=uptime` : null; + + const { data, isLoading, error } = useGet(apiUrl); + + const statusPage = data?.statusPage; + const monitors = data?.monitors ?? []; + + if (!statusPage) return null; + + let sx: React.CSSProperties = {}; + if (isPublic) { + sx.paddingTop = theme.spacing(20); + sx.paddingLeft = "20vw"; + sx.paddingRight = "20vw"; + } + + return ( + + + {t("statusPageStatusServiceStatus")} + + + + ); +}; + +export default StatusPageView; diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx new file mode 100644 index 000000000..1741aeadf --- /dev/null +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx @@ -0,0 +1,109 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Table, type Header, ValueLabel } from "@/Components/v2/design-elements"; +import { ExternalLink } from "lucide-react"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; + +import type { StatusPage } from "@/Types/StatusPage"; + +interface StatusPagesTableProps { + data: StatusPage[]; +} + +export const StatusPagesTable = ({ data }: StatusPagesTableProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + + const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => { + if (row.isPublished) { + e.stopPropagation(); + const url = `/status/uptime/public/${row.url}`; + window.open(url, "_blank", "noopener,noreferrer"); + } + }; + + const getHeaders = (): Header[] => { + return [ + { + id: "name", + content: t("pages.statusPages.table.headers.name"), + render: (row) => row.companyName, + }, + { + id: "url", + content: t("pages.statusPages.table.headers.url"), + render: (row) => { + const content = row.isPublished + ? `/${row.url}` + : t("pages.statusPages.table.unpublished"); + return ( + handleUrlClick(e, row)} + sx={{ + ...(row.isPublished && { + display: "inline-flex", + ":hover": { + cursor: "pointer", + borderBottom: 1, + }, + }), + }} + > + {content} + {row.isPublished && } + + ); + }, + }, + { + id: "type", + content: t("common.table.headers.type"), + render: (row) => row.type, + }, + { + id: "status", + content: t("common.table.headers.status"), + render: (row) => { + return ( + + ); + }, + }, + ]; + }; + + const handleRowClick = (statusPage: StatusPage) => { + navigate(`/status/uptime/${statusPage.url}`); + }; + + return ( + + + + ); +}; + +export default StatusPagesTable; diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx deleted file mode 100644 index 9882239bd..000000000 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import DataTable from "@/Components/v1/Table/index.jsx"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import { Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -const StatusPagesTable = ({ data }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const headers = [ - { - id: "name", - content: t("statusPageName"), - render: (row) => { - return row.companyName; - }, - }, - { - id: "url", - content: t("publicURL"), - onClick: (e, row) => { - if (row.isPublished) { - e.stopPropagation(); - const url = `/status/uptime/public/${row.url}`; - window.open(url, "_blank", "noopener,noreferrer"); - } - }, - render: (row) => { - const content = row.isPublished ? `/${row.url}` : "Unpublished"; - return ( - - {content} - {row.isPublished && ( - - )} - - ); - }, - }, - { - id: "type", - content: t("type"), - render: (row) => { - return row.type; - }, - }, - { - id: "status", - content: t("status"), - render: (row) => { - const status = row.isPublished ? "published" : "unpublished"; - return ( - - ); - }, - }, - ]; - - const handleRowClick = (statusPage) => { - navigate(`/status/uptime/${statusPage.url}`); - }; - - return ( - { - handleRowClick(row); - }, - }} - headers={headers} - data={data} - /> - ); -}; - -export default StatusPagesTable; diff --git a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx deleted file mode 100644 index 9336e5d31..000000000 --- a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -const useStatusPagesFetch = () => { - const { user } = useSelector((state) => state.auth); - - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [statusPages, setStatusPages] = useState(undefined); - - useEffect(() => { - const fetchStatusPages = async () => { - try { - const res = await networkService.getStatusPagesByTeamId(); - setStatusPages(res?.data?.data || []); - } catch (error) { - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - fetchStatusPages(); - }, [user]); - return [isLoading, networkError, statusPages]; -}; - -export { useStatusPagesFetch }; diff --git a/client/src/Pages/StatusPage/StatusPages/index.jsx b/client/src/Pages/StatusPage/StatusPages/index.jsx deleted file mode 100644 index 7137d01ad..000000000 --- a/client/src/Pages/StatusPage/StatusPages/index.jsx +++ /dev/null @@ -1,43 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; -import StatusPagesTable from "./Components/StatusPagesTable/index.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; -// Utils -import { useTheme } from "@emotion/react"; -import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -const BREADCRUMBS = [{ name: `Status Pages`, path: "" }]; - -const StatusPages = () => { - // Utils - const theme = useTheme(); - const isAdmin = useIsAdmin(); - const [isLoading, networkError, statusPages] = useStatusPagesFetch(); - - return ( - <> - - - - - - - - - ); -}; - -export default StatusPages; diff --git a/client/src/Pages/StatusPage/StatusPages/index.tsx b/client/src/Pages/StatusPage/StatusPages/index.tsx new file mode 100644 index 000000000..6a34e587d --- /dev/null +++ b/client/src/Pages/StatusPage/StatusPages/index.tsx @@ -0,0 +1,41 @@ +import { BasePageWithStates } from "@/Components/v2/design-elements"; +import { StatusPagesTable } from "./Components/StatusPagesTable"; +import { useGet } from "@/Hooks/UseApi"; +import type { StatusPage } from "@/Types/StatusPage"; +import { useTranslation } from "react-i18next"; +import { HeaderCreate } from "@/Components/v2/common"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; + +const StatusPages = () => { + const { t } = useTranslation(); + + const { + data: statusPages, + isLoading, + error, + } = useGet("/status-page/team"); + + const isAdmin = useIsAdmin(); + + return ( + + + + + ); +}; + +export default StatusPages; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index ecda66cd1..bbba8eac0 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -37,8 +37,8 @@ import Incidents from "../Pages/Incidents/"; // Status pages import CreateStatus from "../Pages/StatusPage/Create/index.jsx"; -import StatusPages from "../Pages/StatusPage/StatusPages/index.jsx"; -import Status from "../Pages/StatusPage/Status/index.jsx"; +import StatusPages from "../Pages/StatusPage/StatusPages"; +import Status from "../Pages/StatusPage/Status"; import Notifications from "../Pages/Notifications"; import CreateNotifications from "../Pages/Notifications/create"; @@ -227,12 +227,24 @@ const Routes = () => { } + element={ + <> + + + + + } /> } + element={ + <> + + + + + } /> { /> } + element={ + <> + + + + + } /> { + if (typeof monitor === "undefined") return "pending"; + if (monitor?.isActive === false) return "paused"; + if (monitor?.status === undefined) return "pending"; + return monitor?.status == true ? "up" : "down"; +}; + export const getMonitorPath = (type: MonitorType): string => { const pathMap: Record = { http: "uptime", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index f4564109f..88f1ffc7f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -22,7 +22,8 @@ "cancel": "Cancel", "confirm": "Confirm", "save": "Save", - "test": "Test" + "test": "Test", + "configure": "Configure" }, "alerts": { @@ -452,6 +453,37 @@ } } } + }, + "statusPages": { + "title": "Status Pages", + "table": { + "headers": { + "name": "Status page name", + "url": "Public URL" + }, + "published": "Published", + "unpublished": "Unpublished" + }, + "fallback": { + "actionButton": "Create a status page!", + "checks": [ + "Monitor and display the health of your services in real time", + "Track multiple services and share their status", + "Keep users informed about outages and performance" + ], + "title": "A status page is used to:" + }, + "serviceStauts": "Service stauts", + "statusBar": { + "allUp": "All systems operational", + "allDown": "All systems down", + "degraded": "Degraded performance", + "unknown": "Unknown status" + }, + "monitorsList": { + "chartTypeHeatmap": "Heatmap", + "chartTypeHistogram": "Histogram" + } } }, "used": "Used", @@ -490,8 +522,6 @@ "cancel": "Cancel", "close": "Close", "type": "Type", - "statusPageName": "Status page name", - "publicURL": "Public URL", "repeat": "Repeat", "edit": "Edit", "createA": "Create a", @@ -516,10 +546,6 @@ "showCharts": "Show charts", "showUptimePercentage": "Show uptime percentage", "removeLogo": "Remove Logo", - "statusPageStatus": "A public status page is not set up.", - "statusPageStatusContactAdmin": "Please contact to your administrator", - "statusPageStatusNotPublic": "This status page is not public.", - "statusPageStatusNoPage": "There's no status page here.", "statusPageStatusServiceStatus": "Service status", "deleteStatusPage": "Do you want to delete this status page?", "deleteStatusPageConfirm": "Yes, delete status page", @@ -589,6 +615,10 @@ "getToken": "Get token", "emailToken": "E-mail token", "table": { + "headers": { + "name": "Status page name", + "url": "Public URL" + }, "name": "Name", "email": "Email", "role": "Role", @@ -793,6 +823,10 @@ } }, "table": { + "headers": { + "name": "Status page name", + "url": "Public URL" + }, "timestamp": "Timestamp", "level": "Level", "service": "Service", @@ -973,6 +1007,10 @@ "save": "Save" }, "table": { + "headers": { + "name": "Status page name", + "url": "Public URL" + }, "actionHeader": "Action", "roleHeader": "Role" }, diff --git a/server/src/controllers/statusPageController.ts b/server/src/controllers/statusPageController.ts index 18428d424..3271e39a1 100644 --- a/server/src/controllers/statusPageController.ts +++ b/server/src/controllers/statusPageController.ts @@ -81,7 +81,7 @@ class StatusPageController { const settings = await this.settingsService.getDBSettings(); const showURL = settings.showURL; - const monitors = await this.monitorsRepository.findByIdsWithChecks(statusPage.monitors); + const monitors = await this.monitorsRepository.findByIds(statusPage.monitors); const normalizedMonitors = monitors.map((monitor) => { const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100); if (!showURL) {