breadcrumbs

This commit is contained in:
Alex Holliday committed 2026-02-02 21:51:06 +00:00
1 parent a912a5ebb8
commit 56ead03416
8 files changed
+186 -17

No files matched your search

@@ -91,7 +91,7 @@ export const BasePage = ({
spacing={theme.spacing(10)}
{...props}
>
<Breadcrumb />
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
{children}
</Stack>
);
@@ -11,13 +11,64 @@ const isId = (segment: string): boolean => {
const actionSegments = ["create", "configure"];
export const Breadcrumb = () => {
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 (
<MuiBreadcrumbs
separator={
<ChevronRight
size={16}
strokeWidth={1.5}
/>
}
sx={{
fontSize: "14px",
marginBottom: theme.spacing(6),
"& .MuiBreadcrumbs-separator": {
color: theme.palette.text.secondary,
},
}}
>
{breadcrumbOverride.map((item, index) => {
const isLast = index === breadcrumbOverride.length - 1;
return (
<Typography
key={index}
sx={{
fontSize: "14px",
fontWeight: isLast ? 600 : 400,
color: isLast ? theme.palette.primary.main : theme.palette.text.secondary,
}}
>
{item}
</Typography>
);
})}
</MuiBreadcrumbs>
);
}
// 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");
+12 -2
View File
@@ -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<HTMLInputElement, SwitchProps>(
function SwitchComponent({ sx, ...props }, ref) {
interface SwitchComponentProps extends SwitchProps {
dualOption?: boolean;
}
export const SwitchComponent = forwardRef<HTMLInputElement, SwitchComponentProps>(
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<HTMLInputElement, SwitchProps>(
},
},
},
...(dualOption && {
"& .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
},
}),
},
...additionalSx,
]}
@@ -72,7 +72,7 @@ export const HeaderStatusPageControls = ({
</>
)}
</Stack>
{isAdmin && (
{isAdmin && !isPublic && (
<Button
variant="contained"
color="secondary"
@@ -1,11 +1,104 @@
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 MontorsListProps {
stautsPage: StatusPage;
monitors: Monitor[];
interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"];
}
export const MonitorsList = ({ stautsPage, monitors }: MontorsListProps) => {
return <div>Monitors List</div>;
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 (
<Stack gap={theme.spacing(8)}>
{statusPage.showCharts !== false && (
<Stack
direction={"row"}
alignItems={"center"}
>
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
<SwitchComponent
dualOption
value={chartType}
checked={chartType === "histogram"}
onChange={(e) => {
setChartType(e.target.checked ? "histogram" : "heatmap");
}}
/>
<Typography>
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
</Typography>
</Stack>
)}
{monitors?.map((monitor) => {
const status = determineState(monitor);
return (
<BaseBox
key={monitor.id}
padding={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={theme.spacing(4)}
mb={statusPage.showCharts !== false ? theme.spacing(4) : 0}
>
<Box>
<Typography variant="h6">{monitor.name}</Typography>
{showURL && (
<Typography
variant="body2"
color="text.secondary"
>
{monitor.url}
</Typography>
)}
</Box>
<StatusLabel
status={status === "up"}
isActive={monitor.isActive}
/>
</Stack>
{statusPage.showCharts !== false && (
<Box>
{chartType === "histogram" ? (
<HistogramResponseTime
checks={monitor?.checks?.slice().reverse() ?? []}
/>
) : (
<HeatmapResponseTime
checks={monitor?.checks?.slice().reverse() ?? []}
/>
)}
</Box>
)}
</BaseBox>
);
})}
</Stack>
);
};
+11 -7
View File
@@ -3,6 +3,7 @@ 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";
@@ -11,6 +12,7 @@ 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();
@@ -24,18 +26,21 @@ const StatusPageView = () => {
const statusPage = data?.statusPage;
const monitors = data?.monitors ?? [];
if (!statusPage || !monitors) {
return null;
}
if (!statusPage) return null;
if (monitors.length === 0) {
return "poo";
let sx: React.CSSProperties = {};
if (isPublic) {
sx.paddingTop = theme.spacing(20);
sx.paddingLeft = "20vw";
sx.paddingRight = "20vw";
}
return (
<BasePage
loading={isLoading}
error={error}
sx={sx}
breadcrumbOverride={isPublic ? [] : undefined}
>
<HeaderStatusPageControls
isAdmin={isAdmin}
@@ -45,10 +50,9 @@ const StatusPageView = () => {
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
<StatusBar monitors={monitors} />
<MonitorsList
stautsPage={statusPage}
statusPage={statusPage}
monitors={monitors}
/>
<pre> {JSON.stringify(data, null, 2)}</pre>
</BasePage>
);
};
+7
View File
@@ -2,6 +2,13 @@ import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor";
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
import type { ValueType } from "@/Components/v2/design-elements/StatusLabel";
export const determineState = (monitor: Monitor) => {
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<MonitorType, string> = {
http: "uptime",
+4
View File
@@ -479,6 +479,10 @@
"allDown": "All systems down",
"degraded": "Degraded performance",
"unknown": "Unknown status"
},
"monitorsList": {
"chartTypeHeatmap": "Heatmap",
"chartTypeHistogram": "Histogram"
}
}
},