diff --git a/client/src/components/common/HistogramResponseTime.tsx b/client/src/components/common/HistogramResponseTime.tsx new file mode 100644 index 000000000..69c2e99b1 --- /dev/null +++ b/client/src/components/common/HistogramResponseTime.tsx @@ -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; +} + +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(); + + 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 ( + + + {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 = ( + + + + ); + + return isPlaceholder ? ( + {bar} + ) : ( + + {bar} + + ); + })} + + + ); +}; diff --git a/client/src/features/uiSlice.ts b/client/src/features/uiSlice.ts index a701c3ada..a6899ee3f 100644 --- a/client/src/features/uiSlice.ts +++ b/client/src/features/uiSlice.ts @@ -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) => { state.timezone = action.payload; }, + setChartType: (state, action: PayloadAction) => { + state.chartType = action.payload; + }, setSidebarOpen: (state, action: PayloadAction) => { 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; diff --git a/client/src/pages/settings/DummyChart.tsx b/client/src/pages/settings/DummyChart.tsx new file mode 100644 index 000000000..202736cbd --- /dev/null +++ b/client/src/pages/settings/DummyChart.tsx @@ -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" ? ( + + ) : ( + + ); +}; + +export default DummyChart; diff --git a/client/src/pages/settings/Settings.tsx b/client/src/pages/settings/Settings.tsx index 9b651b837..db6c51968 100644 --- a/client/src/pages/settings/Settings.tsx +++ b/client/src/pages/settings/Settings.tsx @@ -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; @@ -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( null ); @@ -84,6 +87,7 @@ const SettingsPage = () => { Dark + Language @@ -91,6 +95,30 @@ const SettingsPage = () => { } /> + } + rightContent={ + + Chart Type + + + {chartType === "heatmap" + ? "Heatmap: colored line indicates status" + : "Bar chart: color denotes status (up/down). height denotes response time"} + + + } + /> {hasMaster && (