settings
This commit is contained in:
1 parent
4a79013ef2
commit
b5bdd73a90
5 files changed
+200
-3
No files matched your search
@@ -0,0 +1,98 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { ResponsiveStyleValue } from "@mui/system";
|
||||
import type { ICheck } from "@/types/check";
|
||||
import { normalizeResponseTimes } from "@/utils/DataUtils";
|
||||
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
|
||||
|
||||
interface HistogramResponseTimeProps {
|
||||
checks: ICheck[];
|
||||
height?: number | string;
|
||||
gap?: ResponsiveStyleValue<number | string>;
|
||||
}
|
||||
|
||||
const DEFAULT_HEIGHT = 50;
|
||||
|
||||
export const HistogramResponseTime = ({
|
||||
checks,
|
||||
height = DEFAULT_HEIGHT,
|
||||
gap,
|
||||
}: HistogramResponseTimeProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!Array.isArray(checks) || checks.length === 0) return null;
|
||||
|
||||
const normalized = normalizeResponseTimes(checks.slice(-25), "responseTime");
|
||||
let data = Array<any>();
|
||||
|
||||
if (!normalized || normalized.length === 0) {
|
||||
data = [];
|
||||
}
|
||||
if (normalized.length !== 25) {
|
||||
const placeholders = Array(25 - normalized.length).fill({
|
||||
status: "placeholder",
|
||||
});
|
||||
data = [...normalized, ...placeholders];
|
||||
} else {
|
||||
data = normalized;
|
||||
}
|
||||
|
||||
const chartHeight = typeof height === "number" ? `${height}px` : height;
|
||||
const gridGap = gap ?? theme.spacing(0.5);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(25, 1fr)",
|
||||
gap: gridGap,
|
||||
alignItems: "end",
|
||||
height: chartHeight,
|
||||
}}
|
||||
>
|
||||
{data.map((check, index) => {
|
||||
const isPlaceholder = (check as any).status === "placeholder";
|
||||
const heightPct = `${Math.max(0, Math.min(100, (check as any).normalResponseTime ?? 0))}%`;
|
||||
const barColor =
|
||||
check.status === "up"
|
||||
? theme.palette.success.lowContrast
|
||||
: theme.palette.error.lowContrast;
|
||||
const bar = (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: theme.spacing(1),
|
||||
bgcolor: theme.palette.primary.lowContrast,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: isPlaceholder ? 0 : heightPct,
|
||||
bgcolor: barColor,
|
||||
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return isPlaceholder ? (
|
||||
<Box key={index}>{bar}</Box>
|
||||
) : (
|
||||
<HeatmapResponseTimeTooltip key={index} check={check}>
|
||||
{bar}
|
||||
</HeatmapResponseTimeTooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
export type ChartType = "heatmap" | "histogram";
|
||||
|
||||
interface UiState {
|
||||
language: string;
|
||||
mode: string;
|
||||
sidebarOpen: boolean;
|
||||
timezone: string;
|
||||
chartType: ChartType;
|
||||
}
|
||||
|
||||
const initialState: UiState = {
|
||||
@@ -13,6 +16,7 @@ const initialState: UiState = {
|
||||
mode: "dark",
|
||||
sidebarOpen: true,
|
||||
timezone: "America/Vancouver",
|
||||
chartType: "heatmap",
|
||||
};
|
||||
|
||||
export const uiSlice = createSlice({
|
||||
@@ -28,12 +32,20 @@ export const uiSlice = createSlice({
|
||||
setTimezone: (state, action: PayloadAction<string>) => {
|
||||
state.timezone = action.payload;
|
||||
},
|
||||
setChartType: (state, action: PayloadAction<ChartType>) => {
|
||||
state.chartType = action.payload;
|
||||
},
|
||||
setSidebarOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.sidebarOpen = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setLanguage, setMode, setSidebarOpen, setTimezone } =
|
||||
uiSlice.actions;
|
||||
export const {
|
||||
setLanguage,
|
||||
setMode,
|
||||
setSidebarOpen,
|
||||
setTimezone,
|
||||
setChartType,
|
||||
} = uiSlice.actions;
|
||||
export default uiSlice.reducer;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useMemo } from "react";
|
||||
import type { ICheck } from "@/types/check";
|
||||
import { HeatmapResponseTime } from "@/components/common/HeatmapResponseTime";
|
||||
import { HistogramResponseTime } from "@/components/common/HistogramResponseTime";
|
||||
import type { ChartType } from "@/features/uiSlice";
|
||||
|
||||
const buildDummyChecks = (): ICheck[] => {
|
||||
const now = Date.now();
|
||||
const arr: ICheck[] = [] as any;
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const ms = Math.floor(50 + Math.random() * 800);
|
||||
arr.push({
|
||||
_id: `chk-${i}`,
|
||||
metadata: { monitorId: "m1", type: "http", teamId: "t1" },
|
||||
status: Math.random() > 0.3 ? "up" : "down",
|
||||
message: "",
|
||||
responseTime: ms,
|
||||
normalResponseTime: 0,
|
||||
httpStatusCode: 200,
|
||||
ack: false,
|
||||
expiry: new Date(now - i * 60000).toISOString(),
|
||||
createdAt: new Date(now - i * 60000).toISOString(),
|
||||
updatedAt: new Date(now - i * 60000).toISOString(),
|
||||
timings: {
|
||||
start: "",
|
||||
socket: "",
|
||||
lookup: "",
|
||||
connect: "",
|
||||
secureConnect: "",
|
||||
response: "",
|
||||
end: "",
|
||||
phases: {
|
||||
wait: 0,
|
||||
dns: 0,
|
||||
tcp: 0,
|
||||
tls: 0,
|
||||
request: 0,
|
||||
firstByte: 0,
|
||||
download: 0,
|
||||
total: ms,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
const DummyChart = ({ type }: { type: ChartType }) => {
|
||||
const checks = useMemo(() => buildDummyChecks(), []);
|
||||
|
||||
return type === "heatmap" ? (
|
||||
<HeatmapResponseTime checks={checks} />
|
||||
) : (
|
||||
<HistogramResponseTime checks={checks} />
|
||||
);
|
||||
};
|
||||
|
||||
export default DummyChart;
|
||||
@@ -5,9 +5,10 @@ import MenuItem from "@mui/material/MenuItem";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { SettingsForm } from "@/pages/settings/SettingsForm";
|
||||
|
||||
import type { ChartType } from "@/features/uiSlice";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { setTimezone, setMode } from "@/features/uiSlice";
|
||||
import { setTimezone, setMode, setChartType } from "@/features/uiSlice";
|
||||
import { useAppSelector, useAppDispatch } from "@/hooks/AppHooks";
|
||||
import timezones from "@/utils/timezones.json";
|
||||
import type { ITimeZone } from "@/types/timezone";
|
||||
@@ -16,6 +17,7 @@ import { useGet, usePatch, usePost } from "@/hooks/UseApi";
|
||||
import type { ApiResponse } from "@/hooks/UseApi";
|
||||
import { z } from "zod";
|
||||
import { systemSettingsSchema } from "@/validation/zod";
|
||||
import DummyChart from "./DummyChart";
|
||||
|
||||
const SettingsPage = () => {
|
||||
type FormValues = z.infer<typeof systemSettingsSchema>;
|
||||
@@ -29,6 +31,7 @@ const SettingsPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const timezone = useAppSelector((state) => state.ui.timezone);
|
||||
const mode = useAppSelector((state) => state.ui.mode);
|
||||
const chartType = useAppSelector((state) => state.ui.chartType);
|
||||
const [selectedTimezone, setSelectedTimezone] = useState<ITimeZone | null>(
|
||||
null
|
||||
);
|
||||
@@ -84,6 +87,7 @@ const SettingsPage = () => {
|
||||
<MenuItem value="dark">Dark</MenuItem>
|
||||
</Select>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
<Typography>Language</Typography>
|
||||
<LanguageSelector />
|
||||
@@ -91,6 +95,30 @@ const SettingsPage = () => {
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title="Chart settings"
|
||||
subtitle="Choose what type of Uptime chart you want to see on the dashboard and status page"
|
||||
leftContent={<DummyChart type={chartType} />}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
<Typography>Chart Type</Typography>
|
||||
<Select
|
||||
value={chartType}
|
||||
onChange={(e: SelectChangeEvent<string>) => {
|
||||
dispatch(setChartType(e.target.value as ChartType));
|
||||
}}
|
||||
>
|
||||
<MenuItem value="heatmap">Heatmap</MenuItem>
|
||||
<MenuItem value="histogram">Histogram</MenuItem>
|
||||
</Select>
|
||||
<Typography>
|
||||
{chartType === "heatmap"
|
||||
? "Heatmap: colored line indicates status"
|
||||
: "Bar chart: color denotes status (up/down). height denotes response time"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
{hasMaster && (
|
||||
<SettingsForm
|
||||
onSubmit={onSubmit}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table, Pagination, StatusLabel } from "@/components/design-elements";
|
||||
import { HeatmapResponseTime } from "@/components/common/HeatmapResponseTime";
|
||||
import { HistogramResponseTime } from "@/components/common/HistogramResponseTime";
|
||||
import type { Header } from "@/components/design-elements/Table";
|
||||
import { ActionsMenu } from "@/components/actions-menu";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
|
||||
Reference in new issue
Block a user