refactor(status-page): tighten the refined-theme conversion
Apply review feedback: - Type the styles factory's return as a named RefinedStyles interface so the consumer no longer needs `as Function` / `as any` casts on factory entries - Lift toneColor / toneSoft to shared/overallStatus alongside the existing OverallTone type — the next three themes will need the same mapping - Reuse heatLevel() in the legacy classPrefix branch of ThemedInfrastructure so both API paths share one threshold table - Memoize the styles map in RefinedStatusPage so MUI's sx processor sees referential equality across renders that don't actually change tokens - Make the dual-API gate in ThemedHeatmap / ThemedHistogram require ALL relevant sx props together so a partial migration can't silently fall back to the legacy class path
This commit is contained in:
1 parent
163f5ec1ec
commit
b95d8886b9
6 files changed
+92
-68
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -27,7 +27,7 @@ interface Props {
|
||||
export const RefinedStatusPage = ({ statusPage, monitors }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { tokens, mode } = useStatusPageTheme();
|
||||
const styles = refinedStyles(tokens, mode === "dark");
|
||||
const styles = useMemo(() => refinedStyles(tokens, mode === "dark"), [tokens, mode]);
|
||||
const [chartMode, setChartMode] = useState<"heatmap" | "histogram">("heatmap");
|
||||
|
||||
const overall = resolveOverallStatus(monitors, t);
|
||||
@@ -62,7 +62,7 @@ export const RefinedStatusPage = ({ statusPage, monitors }: Props) => {
|
||||
</Box>
|
||||
|
||||
<Box sx={styles.hero}>
|
||||
<Box sx={(styles.statusDot as Function)(overall.tone)} />
|
||||
<Box sx={styles.statusDot(overall.tone)} />
|
||||
<Box sx={styles.statusCopy}>
|
||||
<Box
|
||||
component="h1"
|
||||
@@ -79,7 +79,7 @@ export const RefinedStatusPage = ({ statusPage, monitors }: Props) => {
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={(styles.heroIcon as Function)(overall.tone)}>{overall.icon}</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
|
||||
{statusPage.showCharts && (
|
||||
@@ -94,7 +94,7 @@ export const RefinedStatusPage = ({ statusPage, monitors }: Props) => {
|
||||
role="radio"
|
||||
aria-checked={chartMode === "heatmap"}
|
||||
onClick={() => setChartMode("heatmap")}
|
||||
sx={(styles.chartSwitchButton as Function)(chartMode === "heatmap")}
|
||||
sx={styles.chartSwitchButton(chartMode === "heatmap")}
|
||||
>
|
||||
{t("pages.statusPages.monitorsList.chartTypeHeatmap")}
|
||||
</Box>
|
||||
@@ -104,7 +104,7 @@ export const RefinedStatusPage = ({ statusPage, monitors }: Props) => {
|
||||
role="radio"
|
||||
aria-checked={chartMode === "histogram"}
|
||||
onClick={() => setChartMode("histogram")}
|
||||
sx={(styles.chartSwitchButton as Function)(chartMode === "histogram")}
|
||||
sx={styles.chartSwitchButton(chartMode === "histogram")}
|
||||
>
|
||||
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
|
||||
</Box>
|
||||
@@ -151,7 +151,7 @@ export const RefinedStatusPage = ({ statusPage, monitors }: Props) => {
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={(styles.badge as Function)(badgeTone)}
|
||||
sx={styles.badge(badgeTone)}
|
||||
>
|
||||
{t(statusBadgeKey[monitor.status])}
|
||||
</Box>
|
||||
@@ -167,10 +167,7 @@ export const RefinedStatusPage = ({ statusPage, monitors }: Props) => {
|
||||
gaugeLabelSx: styles.gaugeLabel,
|
||||
gaugeValueSx: styles.gaugeValue,
|
||||
gaugeBarSx: styles.gaugeBar,
|
||||
gaugeFillSx: styles.gaugeFill as (
|
||||
level: "ok" | "warm" | "hot",
|
||||
width: number
|
||||
) => any,
|
||||
gaugeFillSx: styles.gaugeFill,
|
||||
gaugeSubSx: styles.gaugeSub,
|
||||
}}
|
||||
/>
|
||||
@@ -180,13 +177,13 @@ export const RefinedStatusPage = ({ statusPage, monitors }: Props) => {
|
||||
<ThemedHeatmap
|
||||
checks={monitor.recentChecks ?? []}
|
||||
containerSx={styles.heatmap}
|
||||
cellSx={styles.heatmapCell as (kind: any) => any}
|
||||
cellSx={styles.heatmapCell}
|
||||
/>
|
||||
) : (
|
||||
<ThemedHistogram
|
||||
checks={monitor.recentChecks ?? []}
|
||||
containerSx={styles.histogram}
|
||||
barSx={styles.bar as (kind: any, h: number) => any}
|
||||
barSx={styles.bar}
|
||||
statsSx={styles.chartStats}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,53 @@
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import type { StatusPageThemeTokens } from "../tokens";
|
||||
import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus";
|
||||
|
||||
export type RefinedTone = "up" | "warn" | "down";
|
||||
export type RefinedHeatCell = "fast" | "med" | "slow" | "down" | "empty";
|
||||
export type RefinedBarKind = "up" | "down" | "empty";
|
||||
export type RefinedGaugeFill = "ok" | "warm" | "hot";
|
||||
|
||||
export interface RefinedStyles {
|
||||
page: SxProps<Theme>;
|
||||
top: SxProps<Theme>;
|
||||
brand: SxProps<Theme>;
|
||||
logoMono: SxProps<Theme>;
|
||||
logoImg: SxProps<Theme>;
|
||||
company: SxProps<Theme>;
|
||||
hero: SxProps<Theme>;
|
||||
statusDot: (tone: OverallTone) => SxProps<Theme>;
|
||||
statusCopy: SxProps<Theme>;
|
||||
heroTitle: SxProps<Theme>;
|
||||
heroSub: SxProps<Theme>;
|
||||
heroIcon: (tone: OverallTone) => SxProps<Theme>;
|
||||
chartSwitchWrap: SxProps<Theme>;
|
||||
chartSwitch: SxProps<Theme>;
|
||||
chartSwitchButton: (active: boolean) => SxProps<Theme>;
|
||||
monitorList: SxProps<Theme>;
|
||||
card: SxProps<Theme>;
|
||||
cardRow: SxProps<Theme>;
|
||||
cardLeft: SxProps<Theme>;
|
||||
monitorName: SxProps<Theme>;
|
||||
monitorMeta: SxProps<Theme>;
|
||||
pill: SxProps<Theme>;
|
||||
pillHardware: SxProps<Theme>;
|
||||
monitorUrl: SxProps<Theme>;
|
||||
badge: (tone: OverallTone) => SxProps<Theme>;
|
||||
heatmap: SxProps<Theme>;
|
||||
heatmapCell: (kind: RefinedHeatCell) => SxProps<Theme>;
|
||||
histogram: SxProps<Theme>;
|
||||
bar: (kind: RefinedBarKind, heightPct: number) => SxProps<Theme>;
|
||||
chartStats: SxProps<Theme>;
|
||||
infra: SxProps<Theme>;
|
||||
infraEmpty: SxProps<Theme>;
|
||||
gauge: SxProps<Theme>;
|
||||
gaugeLabel: SxProps<Theme>;
|
||||
gaugeValue: SxProps<Theme>;
|
||||
gaugeBar: SxProps<Theme>;
|
||||
gaugeFill: (level: RefinedGaugeFill, widthPct: number) => SxProps<Theme>;
|
||||
gaugeSub: SxProps<Theme>;
|
||||
footer: SxProps<Theme>;
|
||||
}
|
||||
|
||||
const sansFontStack =
|
||||
'-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif';
|
||||
|
||||
@@ -12,13 +55,10 @@ const cardShadow = "0 1px 2px rgba(10, 16, 32, 0.06), 0 6px 18px rgba(10, 16, 32
|
||||
const cardShadowHover =
|
||||
"0 2px 4px rgba(16, 24, 40, 0.06), 0 10px 24px rgba(16, 24, 40, 0.06)";
|
||||
|
||||
const toneColor = (tone: RefinedTone, t: StatusPageThemeTokens) =>
|
||||
tone === "down" ? t.down : tone === "warn" ? t.warn : t.up;
|
||||
|
||||
const toneSoft = (tone: RefinedTone, t: StatusPageThemeTokens) =>
|
||||
tone === "down" ? t.downSoft : tone === "warn" ? t.warnSoft : t.upSoft;
|
||||
|
||||
export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) => {
|
||||
export const refinedStyles = (
|
||||
tokens: StatusPageThemeTokens,
|
||||
isDark: boolean
|
||||
): RefinedStyles => {
|
||||
const heatCellBg: Record<RefinedHeatCell, string> = {
|
||||
fast: tokens.up,
|
||||
med: `color-mix(in srgb, ${tokens.up} 60%, #ffffff 40%)`,
|
||||
@@ -39,7 +79,7 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
hot: tokens.down,
|
||||
};
|
||||
|
||||
const sx: Record<string, any> = {
|
||||
return {
|
||||
page: {
|
||||
flex: "1 0 auto",
|
||||
maxWidth: 960,
|
||||
@@ -78,11 +118,7 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
fontWeight: 700,
|
||||
fontSize: 13,
|
||||
},
|
||||
logoImg: {
|
||||
maxHeight: 32,
|
||||
maxWidth: 120,
|
||||
objectFit: "contain",
|
||||
},
|
||||
logoImg: { maxHeight: 32, maxWidth: 120, objectFit: "contain" },
|
||||
company: { fontSize: 14 },
|
||||
|
||||
hero: {
|
||||
@@ -96,7 +132,7 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
boxShadow: cardShadow,
|
||||
mb: "20px",
|
||||
},
|
||||
statusDot: (tone: RefinedTone) => ({
|
||||
statusDot: (tone) => ({
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: "50%",
|
||||
@@ -114,17 +150,13 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
color: tokens.text,
|
||||
},
|
||||
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
|
||||
heroIcon: (tone: RefinedTone) => ({
|
||||
heroIcon: (tone) => ({
|
||||
color: toneColor(tone, tokens),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}),
|
||||
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
mb: "12px",
|
||||
},
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "12px" },
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
@@ -133,7 +165,7 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
p: "3px",
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active: boolean) => ({
|
||||
chartSwitchButton: (active) => ({
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
@@ -199,29 +231,19 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
borderRadius: "999px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
pillHardware: isDark
|
||||
? {
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "999px",
|
||||
fontWeight: 600,
|
||||
color: "#60a5fa",
|
||||
border: "1px solid rgba(96, 165, 250, 0.3)",
|
||||
background: "rgba(96, 165, 250, 0.1)",
|
||||
}
|
||||
: {
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "999px",
|
||||
fontWeight: 600,
|
||||
color: "#1d4ed8",
|
||||
border: "1px solid rgba(29, 78, 216, 0.3)",
|
||||
background: "rgba(29, 78, 216, 0.06)",
|
||||
},
|
||||
pillHardware: {
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "999px",
|
||||
fontWeight: 600,
|
||||
color: isDark ? "#60a5fa" : "#1d4ed8",
|
||||
border: `1px solid ${
|
||||
isDark ? "rgba(96, 165, 250, 0.3)" : "rgba(29, 78, 216, 0.3)"
|
||||
}`,
|
||||
background: isDark ? "rgba(96, 165, 250, 0.1)" : "rgba(29, 78, 216, 0.06)",
|
||||
},
|
||||
monitorUrl: {
|
||||
fontSize: 12,
|
||||
color: tokens.textMuted,
|
||||
@@ -232,7 +254,7 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
maxWidth: 280,
|
||||
},
|
||||
|
||||
badge: (tone: RefinedTone) => ({
|
||||
badge: (tone) => ({
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
padding: "4px 10px",
|
||||
@@ -249,7 +271,7 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
gap: "3px",
|
||||
height: 42,
|
||||
},
|
||||
heatmapCell: (kind: RefinedHeatCell) => ({
|
||||
heatmapCell: (kind) => ({
|
||||
borderRadius: "2px",
|
||||
background: heatCellBg[kind],
|
||||
opacity: kind === "empty" ? 0.4 : 1,
|
||||
@@ -265,7 +287,7 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
alignItems: "flex-end",
|
||||
height: 42,
|
||||
},
|
||||
bar: (kind: RefinedBarKind, heightPct: number) => ({
|
||||
bar: (kind, heightPct) => ({
|
||||
background: barBg[kind],
|
||||
borderRadius: "2px",
|
||||
minHeight: 3,
|
||||
@@ -318,7 +340,7 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
overflow: "hidden",
|
||||
mt: "8px",
|
||||
},
|
||||
gaugeFill: (level: RefinedGaugeFill, widthPct: number) => ({
|
||||
gaugeFill: (level, widthPct) => ({
|
||||
display: "block",
|
||||
height: "100%",
|
||||
background: gaugeFillBg[level],
|
||||
@@ -347,6 +369,4 @@ export const refinedStyles = (tokens: StatusPageThemeTokens, isDark: boolean) =>
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return sx;
|
||||
};
|
||||
@@ -30,7 +30,7 @@ const classify = (check: CheckSnapshot): Exclude<HeatCellKind, "empty"> => {
|
||||
|
||||
export const ThemedHeatmap = ({ checks, containerSx, cellSx, classPrefix }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const useSxApi = Boolean(cellSx);
|
||||
const useSxApi = Boolean(containerSx && cellSx);
|
||||
|
||||
const source = checks.slice(0, CELLS).reverse();
|
||||
const padded: (CheckSnapshot | null)[] = [
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ThemedHistogram = ({
|
||||
statsGap = 1,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const useSxApi = Boolean(barSx);
|
||||
const useSxApi = Boolean(containerSx && barSx && statsSx);
|
||||
|
||||
const { padded, max, avg, peak } = useMemo(() => {
|
||||
const source = checks.slice(0, CELLS).reverse();
|
||||
|
||||
@@ -118,8 +118,8 @@ export const ThemedInfrastructure = ({ monitor, sxApi, classPrefix }: Props) =>
|
||||
return (
|
||||
<Box className={`${classPrefix}-infra`}>
|
||||
{gauges.map((g) => {
|
||||
const heatClass =
|
||||
g.value > 85 ? `${classPrefix}-hot` : g.value > 70 ? `${classPrefix}-warm` : "";
|
||||
const level = heatLevel(g.value);
|
||||
const heatClass = level === "ok" ? "" : `${classPrefix}-${level}`;
|
||||
return (
|
||||
<Box
|
||||
key={g.key}
|
||||
|
||||
@@ -9,9 +9,16 @@ import {
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
|
||||
import type { StatusPageThemeTokens } from "../tokens";
|
||||
|
||||
export type OverallTone = "up" | "warn" | "down";
|
||||
|
||||
export const toneColor = (tone: OverallTone, t: StatusPageThemeTokens): string =>
|
||||
tone === "down" ? t.down : tone === "warn" ? t.warn : t.up;
|
||||
|
||||
export const toneSoft = (tone: OverallTone, t: StatusPageThemeTokens): string =>
|
||||
tone === "down" ? t.downSoft : tone === "warn" ? t.warnSoft : t.upSoft;
|
||||
|
||||
export interface OverallStatus {
|
||||
tone: OverallTone;
|
||||
message: string;
|
||||
|
||||
Reference in new issue
Block a user