Merge pull request #3984 from bluewave-labs/feat/dashboard-add-new-cards-options
feat(dashboard): add currently down and lowest uptime dashboard cards
This commit is contained in:
7 files changed
+213
-12
No files matched your search
@@ -1,4 +1,8 @@
|
||||
import { dashboardCardKeys, type DashboardCardKey } from "@/Types/Dashboard";
|
||||
import {
|
||||
dashboardCardKeys,
|
||||
type DashboardCardKey,
|
||||
type DashboardSortOrder,
|
||||
} from "@/Types/Dashboard";
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
type ThemeMode = "light" | "dark";
|
||||
@@ -38,6 +42,7 @@ interface UIState {
|
||||
starPromptOpen: boolean;
|
||||
chartType: ChartType;
|
||||
dashboardVisibleCards: DashboardCardKey[];
|
||||
dashboardUptimeSortOrder: DashboardSortOrder;
|
||||
}
|
||||
|
||||
const initialMode: ThemeMode = window?.matchMedia?.("(prefers-color-scheme: dark)")
|
||||
@@ -78,6 +83,7 @@ const initialState: UIState = {
|
||||
starPromptOpen: true,
|
||||
chartType: "histogram",
|
||||
dashboardVisibleCards: [...dashboardCardKeys],
|
||||
dashboardUptimeSortOrder: "ascending",
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
@@ -124,6 +130,9 @@ const uiSlice = createSlice({
|
||||
setDashboardVisibleCards: (state, action: PayloadAction<DashboardCardKey[]>) => {
|
||||
state.dashboardVisibleCards = action.payload;
|
||||
},
|
||||
setDashboardUptimeSortOrder: (state, action: PayloadAction<DashboardSortOrder>) => {
|
||||
state.dashboardUptimeSortOrder = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -141,4 +150,5 @@ export const {
|
||||
setStarPromptOpen,
|
||||
setChartType,
|
||||
setDashboardVisibleCards,
|
||||
setDashboardUptimeSortOrder,
|
||||
} = uiSlice.actions;
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Checkbox, Dialog } from "@/Components/inputs";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard";
|
||||
|
||||
interface EditCardsModalProps {
|
||||
@@ -18,16 +21,19 @@ export const EditCardsModal = ({
|
||||
onClose,
|
||||
onSave,
|
||||
}: EditCardsModalProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [draft, setDraft] = useState<Set<DashboardCardKey>>(new Set(visibleCards));
|
||||
const [cardsSelection, setCardsSelection] = useState<Set<DashboardCardKey>>(
|
||||
new Set(visibleCards)
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setDraft(new Set(visibleCards));
|
||||
setCardsSelection(new Set(visibleCards));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleToggle = (key: DashboardCardKey) => {
|
||||
setDraft((prev) => {
|
||||
setCardsSelection((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
@@ -38,26 +44,40 @@ export const EditCardsModal = ({
|
||||
});
|
||||
};
|
||||
|
||||
const sortedCardKeys = useMemo(
|
||||
() =>
|
||||
[...dashboardCardKeys].sort((a, b) =>
|
||||
t(`pages.dashboard.cards.${a}`).localeCompare(t(`pages.dashboard.cards.${b}`))
|
||||
),
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title={t("pages.dashboard.editCardsModal.title")}
|
||||
content={t("pages.dashboard.editCardsModal.description")}
|
||||
onCancel={handleClose}
|
||||
onConfirm={() => onSave(draft)}
|
||||
onConfirm={() => onSave(cardsSelection)}
|
||||
confirmText={t("pages.dashboard.editCardsModal.done")}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<Stack>
|
||||
{dashboardCardKeys.map((key: DashboardCardKey) => (
|
||||
<Stack gap={theme.spacing(LAYOUT.XXS)}>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.disabled}
|
||||
>
|
||||
{t("pages.dashboard.editCardsModal.groups.uptime")}
|
||||
</Typography>
|
||||
{sortedCardKeys.map((key: DashboardCardKey) => (
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
label={t(`pages.dashboard.cards.${key}`)}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={draft.has(key)}
|
||||
disabled={draft.has(key) && draft.size === 1}
|
||||
checked={cardsSelection.has(key)}
|
||||
disabled={cardsSelection.has(key) && cardsSelection.size === 1}
|
||||
onChange={() => handleToggle(key)}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DashboardCard } from "@/Pages/Dashboard/components/DashboardCard";
|
||||
import { Dot } from "@/Components/design-elements";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
interface CurrentlyDownCardProps {
|
||||
monitors: Monitor[];
|
||||
}
|
||||
|
||||
export const CurrentlyDownCard = ({ monitors }: CurrentlyDownCardProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const downMonitors = monitors.filter((monitor) => monitor.status === "down");
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.currentlyDown")}
|
||||
topRight={downMonitors.length > 0 ? downMonitors.length : undefined}
|
||||
>
|
||||
{downMonitors.length === 0 ? (
|
||||
<Typography
|
||||
fontSize={typographyLevels.m}
|
||||
color={theme.palette.text.disabled}
|
||||
>
|
||||
{t("pages.dashboard.currentlyDown.empty")}
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
{downMonitors.map((monitor) => (
|
||||
<Stack
|
||||
key={monitor.id}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<Dot color={theme.palette.error.main} />
|
||||
<Typography
|
||||
fontSize={typographyLevels.m}
|
||||
color={theme.palette.text.primary}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import { Icon } from "@/Components/design-elements";
|
||||
import { DashboardCard } from "@/Pages/Dashboard/components/DashboardCard";
|
||||
import { BarRow } from "@/Pages/Dashboard/components/cards/BarRow";
|
||||
import { getUptimePercentageColor } from "@/Utils/MonitorUtils";
|
||||
import { formatPercentage } from "@/Utils/FormatUtils";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { setDashboardUptimeSortOrder } from "@/Features/UI/uiSlice";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { AppDispatch, RootState } from "@/store";
|
||||
|
||||
const TOP_N = 5;
|
||||
|
||||
interface UptimeCardProps {
|
||||
monitors: Monitor[];
|
||||
}
|
||||
|
||||
export const UptimeCard = ({ monitors }: UptimeCardProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const sortOrder = useSelector((state: RootState) => state.ui.dashboardUptimeSortOrder);
|
||||
|
||||
const direction = sortOrder === "ascending" ? 1 : -1;
|
||||
const ranked = monitors
|
||||
.filter((monitor) => monitor.status !== "paused")
|
||||
.filter(
|
||||
(monitor): monitor is Monitor & { uptimePercentage: number } =>
|
||||
typeof monitor.uptimePercentage === "number"
|
||||
)
|
||||
.sort((a, b) => direction * (a.uptimePercentage - b.uptimePercentage))
|
||||
.slice(0, TOP_N);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.uptime")}
|
||||
topRight={
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={t("pages.dashboard.uptimeCard.toggleSortOrder")}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
setDashboardUptimeSortOrder(
|
||||
sortOrder === "ascending" ? "descending" : "ascending"
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon={sortOrder === "ascending" ? ArrowUp : ArrowDown}
|
||||
size={16}
|
||||
color={theme.palette.text.secondary}
|
||||
/>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{ranked.length === 0 ? (
|
||||
<Typography
|
||||
fontSize={typographyLevels.m}
|
||||
color={theme.palette.text.disabled}
|
||||
>
|
||||
{t("pages.dashboard.noData")}
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
{ranked.map((monitor) => {
|
||||
const paletteKey = getUptimePercentageColor(monitor.uptimePercentage);
|
||||
const color = theme.palette[paletteKey].main;
|
||||
return (
|
||||
<BarRow
|
||||
key={monitor.id}
|
||||
label={monitor.name}
|
||||
value={formatPercentage(monitor.uptimePercentage)}
|
||||
fillRatio={monitor.uptimePercentage}
|
||||
valueColor={color}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,8 @@ import { setDashboardVisibleCards } from "@/Features/UI/uiSlice";
|
||||
import type { RootState, AppDispatch } from "@/store";
|
||||
|
||||
import { MonitorStatusCard } from "@/Pages/Dashboard/components/cards/MonitorStatusCard";
|
||||
import { CurrentlyDownCard } from "@/Pages/Dashboard/components/cards/CurrentlyDownCard";
|
||||
import { UptimeCard } from "@/Pages/Dashboard/components/cards/UptimeCard";
|
||||
import { MonitorsByTypeCard } from "@/Pages/Dashboard/components/cards/MonitorsByTypeCard";
|
||||
import { EditCardsModal } from "@/Pages/Dashboard/components/EditCardsModal";
|
||||
import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard";
|
||||
@@ -48,6 +50,8 @@ const Dashboard = () => {
|
||||
|
||||
const cardComponents: Record<DashboardCardKey, ReactNode> = {
|
||||
monitorStatus: <MonitorStatusCard summary={summary} />,
|
||||
currentlyDown: <CurrentlyDownCard monitors={monitors ?? []} />,
|
||||
uptime: <UptimeCard monitors={monitors ?? []} />,
|
||||
monitorsByType: <MonitorsByTypeCard monitorsByType={monitorsByType} />,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
export const dashboardCardKeys = ["monitorStatus", "monitorsByType"] as const;
|
||||
export const dashboardCardKeys = [
|
||||
"monitorStatus",
|
||||
"currentlyDown",
|
||||
"uptime",
|
||||
"monitorsByType",
|
||||
] as const;
|
||||
export type DashboardCardKey = (typeof dashboardCardKeys)[number];
|
||||
|
||||
export const dashboardSortOrders = ["ascending", "descending"] as const;
|
||||
export type DashboardSortOrder = (typeof dashboardSortOrders)[number];
|
||||
@@ -1936,16 +1936,27 @@
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": "Monitor status",
|
||||
"currentlyDown": "Currently down",
|
||||
"uptime": "Monitor uptime",
|
||||
"monitorsByType": "Monitors by type"
|
||||
},
|
||||
"uptimeCard": {
|
||||
"toggleSortOrder": "Toggle sort order"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"empty": "No monitors are down at this moment."
|
||||
},
|
||||
"header": {
|
||||
"title": "Dashboard",
|
||||
"description": "See every monitor type in one place and spot what needs attention."
|
||||
},
|
||||
"editCards": "Edit cards",
|
||||
"editCardsModal": {
|
||||
"title": "Edit cards",
|
||||
"title": "Dashboard cards",
|
||||
"description": "Select the cards to display on your dashboard.",
|
||||
"groups": {
|
||||
"uptime": "Uptime"
|
||||
},
|
||||
"done": "Done"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user