diff --git a/ui/src/pages/DashboardTab/DashboardL1.tsx b/ui/src/pages/DashboardTab/DashboardL1.tsx
index d091eaff..ed1fc556 100644
--- a/ui/src/pages/DashboardTab/DashboardL1.tsx
+++ b/ui/src/pages/DashboardTab/DashboardL1.tsx
@@ -456,14 +456,9 @@ export default function DashboardL1() {
onItemClick={(id) => navigate(`/dashboard/${id}`)}
/>
-
+
+
+
)}
diff --git a/ui/src/pages/DashboardTab/DashboardL2.tsx b/ui/src/pages/DashboardTab/DashboardL2.tsx
index d16b65d7..702d8076 100644
--- a/ui/src/pages/DashboardTab/DashboardL2.tsx
+++ b/ui/src/pages/DashboardTab/DashboardL2.tsx
@@ -437,14 +437,9 @@ export default function DashboardL2() {
onItemClick={(id) => navigate(`/dashboard/${appId}/${id}`)}
/>
-
+
+
+
)}
diff --git a/ui/src/pages/DashboardTab/DashboardTab.module.css b/ui/src/pages/DashboardTab/DashboardTab.module.css
index 41a1b400..f9f9abe2 100644
--- a/ui/src/pages/DashboardTab/DashboardTab.module.css
+++ b/ui/src/pages/DashboardTab/DashboardTab.module.css
@@ -140,6 +140,37 @@
gap: 16px;
}
+/* Toggle button row */
+.toggleRow {
+ display: flex;
+ gap: 2px;
+ padding: 0 12px 4px;
+}
+
+.toggleBtn {
+ padding: 3px 10px;
+ font-size: 11px;
+ font-family: var(--font-mono);
+ color: var(--text-muted);
+ background: transparent;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.toggleBtn:hover {
+ color: var(--text-primary);
+ border-color: var(--border);
+}
+
+.toggleActive {
+ color: var(--text-primary);
+ background: var(--bg-inset);
+ border-color: var(--border);
+ font-weight: 600;
+}
+
/* Errors section */
.errorsSection {
background: var(--bg-surface);
diff --git a/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx b/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx
index abeacff7..345440bb 100644
--- a/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx
+++ b/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx
@@ -1,9 +1,10 @@
-import { useMemo } from 'react';
+import { useMemo, useState } from 'react';
import {
ScatterChart, Scatter, XAxis, YAxis, Tooltip,
ResponsiveContainer, Rectangle,
} from 'recharts';
import { rechartsTheme } from '@cameleer/design-system';
+import styles from './DashboardTab.module.css';
export interface PunchcardCell {
weekday: number;
@@ -14,9 +15,10 @@ export interface PunchcardCell {
interface PunchcardHeatmapProps {
cells: PunchcardCell[];
- mode: 'transactions' | 'errors';
}
+type Mode = 'transactions' | 'errors';
+
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
function transactionColor(ratio: number): string {
@@ -43,7 +45,6 @@ function HeatmapCell(props: Record) {
cx: number; cy: number; payload: HeatmapPoint;
};
if (!payload) return null;
- // Cell size: chart area / grid divisions. Approximate from scatter positioning.
const cellW = 32;
const cellH = 10;
return (
@@ -66,70 +67,89 @@ function formatHour(value: number): string {
return String(value).padStart(2, '0');
}
-export function PunchcardHeatmap({ cells, mode }: PunchcardHeatmapProps) {
- const data: HeatmapPoint[] = useMemo(() => {
- const values = cells.map(c => mode === 'errors' ? c.failedCount : c.totalCount);
- const maxVal = Math.max(...values, 1);
+function buildGrid(cells: PunchcardCell[], mode: Mode): HeatmapPoint[] {
+ const values = cells.map(c => mode === 'errors' ? c.failedCount : c.totalCount);
+ const maxVal = Math.max(...values, 1);
- // Build full 7x24 grid
- const points: HeatmapPoint[] = [];
- const cellMap = new Map();
- for (const c of cells) cellMap.set(`${c.weekday}-${c.hour}`, c);
+ const cellMap = new Map();
+ for (const c of cells) cellMap.set(`${c.weekday}-${c.hour}`, c);
- for (let d = 0; d < 7; d++) {
- for (let h = 0; h < 24; h++) {
- const cell = cellMap.get(`${d}-${h}`);
- const val = cell ? (mode === 'errors' ? cell.failedCount : cell.totalCount) : 0;
- const ratio = maxVal > 0 ? val / maxVal : 0;
- points.push({
- weekday: d,
- hour: h,
- value: val,
- fill: mode === 'errors' ? errorColor(ratio) : transactionColor(ratio),
- });
- }
+ const points: HeatmapPoint[] = [];
+ for (let d = 0; d < 7; d++) {
+ for (let h = 0; h < 24; h++) {
+ const cell = cellMap.get(`${d}-${h}`);
+ const val = cell ? (mode === 'errors' ? cell.failedCount : cell.totalCount) : 0;
+ const ratio = maxVal > 0 ? val / maxVal : 0;
+ points.push({
+ weekday: d,
+ hour: h,
+ value: val,
+ fill: mode === 'errors' ? errorColor(ratio) : transactionColor(ratio),
+ });
}
- return points;
- }, [cells, mode]);
+ }
+ return points;
+}
+
+export function PunchcardHeatmap({ cells }: PunchcardHeatmapProps) {
+ const [mode, setMode] = useState('transactions');
+
+ const data = useMemo(() => buildGrid(cells, mode), [cells, mode]);
return (
-
-
-
-
- {
- const p = entry.payload;
- if (!p) return '';
- return [`${p.value.toLocaleString()} ${mode}`, `${DAYS[p.weekday]} ${formatHour(p.hour)}:00`];
- }}
- labelFormatter={() => ''}
- />
- )} />}
- isAnimationActive={false}
- />
-
-
+
+
+
+
+
+
+
+
+
+ {
+ const p = entry.payload;
+ if (!p) return '';
+ return [`${p.value.toLocaleString()} ${mode}`, `${DAYS[p.weekday]} ${formatHour(p.hour)}:00`];
+ }}
+ labelFormatter={() => ''}
+ />
+ )} />}
+ isAnimationActive={false}
+ />
+
+
+
);
}