diff --git a/apps/dashboard/src/app/(dashboard)/overview/page.tsx b/apps/dashboard/src/app/(dashboard)/overview/page.tsx index 653586aa0..4fc6bc768 100644 --- a/apps/dashboard/src/app/(dashboard)/overview/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/overview/page.tsx @@ -22,6 +22,7 @@ import { ActivityCalendarLegend, ActivityCalendarLegendItem, ActivityCalendarLegendSeparator, + type ActivityLink, } from "@/components/content/activity-calendar"; import { EmptyStateContainer, @@ -139,31 +140,57 @@ function dayEntries(items: OverviewEvent[], date: Date, now: Date): DayEntry[] { return entries.sort((a, b) => rank(b) - rank(a)); } -function linkOf(event: OverviewEvent): { href: string; title: string } { +/** Same type labels as the list rows below the calendar. */ +function linkOf(event: OverviewEvent): ActivityLink { switch (event.type) { case "incident": return { href: `/monitors/${event.incident.monitor.id}/incidents`, title: event.incident.monitor.name, + kind: "Downtime", }; case "managedIncident": return { href: `/incidents/${event.incident.id}`, title: event.incident.title, + kind: "Incident", }; case "report": return { href: `/status-pages/${event.report.pageId}/status-reports/${event.report.id}`, title: event.report.title, + kind: "Report", }; case "maintenance": return { href: `/status-pages/${event.maintenance.pageId}/maintenances/${event.maintenance.id}`, title: event.maintenance.title, + kind: "Maintenance", }; } } +/** A day's links, one per href; downtime carries that monitor's time down that day. */ +function dayLinks(items: OverviewEvent[], date: Date, now: Date) { + const links = new Map(); + const downtime = new Map(); + for (const event of items) { + const link = linkOf(event); + links.set(link.href, link); + if (event.type === "incident") { + downtime.set(link.href, [ + ...(downtime.get(link.href) ?? []), + spanOf(event), + ]); + } + } + for (const [href, spans] of downtime) { + const link = links.get(href); + if (link) link.meta = formatDurationShort(spansOnDay(date, spans, now)); + } + return [...links.values()]; +} + function spanOf(event: OverviewEvent): ActivitySpan { if (event.type === "report") return reportSpan(event.report); if (event.type === "managedIncident") return incidentSpan(event.incident); @@ -242,7 +269,6 @@ export default function Page() { const days = groupByDay(range, events, spanOf, now).map(({ date, items }) => { if (items.length === 0) return { date }; const [lead, ...others] = dayEntries(items, date, now); - const links = new Map(items.map(linkOf).map((l) => [l.href, l])); // dashed only when the day holds nothing but maintenance still ahead const upcoming = items.every( (e) => e.type === "maintenance" && e.maintenance.from > now, @@ -260,7 +286,7 @@ export default function Page() { ), ...(upcoming ? ["Upcoming"] : []), ], - links: [...links.values()], + links: dayLinks(items, date, now), }; }); const anyUpcoming = days.some((day) => day.upcoming); diff --git a/apps/dashboard/src/components/content/activity-calendar.tsx b/apps/dashboard/src/components/content/activity-calendar.tsx index 7e3b55146..87c53636c 100644 --- a/apps/dashboard/src/components/content/activity-calendar.tsx +++ b/apps/dashboard/src/components/content/activity-calendar.tsx @@ -24,14 +24,22 @@ import { activityColorVariants, } from "./activity"; +export type ActivityLink = { + href: string; + title: string; + kind?: string; + /** Mono aside after the title, e.g. "12m". */ + meta?: string; +}; + export type ActivityDay = Partial & { date: Date; /** Scheduled, not recorded: a dashed ring over the lightest planned fill. */ upcoming?: boolean; /** Tooltip lines under the date; with the date they make the aria-label. */ lines?: string[]; - /** Opens a popover on click listing these, e.g. the reports behind the mark. */ - links?: { href: string; title: string }[]; + /** Opens a popover on click listing these; `kind` labels each in a mixed calendar. */ + links?: ActivityLink[]; }; /** Sets the minimum cell size; the grid stretches cells to fill the width above it. */ @@ -255,6 +263,7 @@ function ActivityCalendarDay({ const date = format(day.date, "EEE, MMM d"); const lines = day.lines ?? []; const linked = !!day.links?.length; + const kinds = day.links?.some((link) => link.kind); const cell = (