Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions apps/web/src/page/overview/overview.scene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,7 @@ describe("overview", () => {
given(loaded([snapshotRow("checkout")], [anomaly])),
themeResolved(),
cursorMounted(),
expectView(text("Open:")).toExist(),
expectView(text("1 anomaly")).toExist(),
expectView(role("link", { name: /1 open anomaly/ })).toHaveAttr("href", "/alerts?signal=anomaly"),
expectView(text("Firing now:")).not.toExist(),
)
scene(
Expand All @@ -311,8 +310,7 @@ describe("overview", () => {
cursorMounted(),
expectView(text("Firing now:")).toExist(),
expectView(text("1 incident · 1 warning")).toExist(),
expectView(text("Open:")).toExist(),
expectView(text("1 anomaly")).toExist(),
expectView(role("link", { name: /1 open anomaly/ })).toHaveAttr("href", "/alerts?signal=anomaly"),
)
})

Expand Down
95 changes: 39 additions & 56 deletions apps/web/src/page/overview/view-hero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { card, cardPanel } from "@maple/views/ui/card"
import { statusDot } from "@maple/views/ui/status-dot"
import { typography } from "@maple/views/ui/typography"
import { hstack } from "@maple/views/ui/stack"
import { textLink } from "@maple/views/ui/text-link"
import { alertsHref } from "../alerts/shared/links"
import { Array as Arr } from "effect"
import type { Incident } from "./model"

// The one-line alert strip above the health rail; nothing when all is clear.
// Open anomalies count apart from firing incidents.
// Open anomalies count apart from firing incidents and stay quiet.

export type HeroCounts = Readonly<{
/** Open alert incidents, anomaly rules' excluded. */
Expand Down Expand Up @@ -38,14 +40,6 @@ export const heroCountsOf = (
}
}

type Tone = "crit" | "warn"

const TONE_TEXT = { crit: "text-severity-error", warn: "text-severity-warn" } satisfies Record<Tone, string>
const TONE_CARD = {
crit: "border-severity-error-a3 bg-severity-error/[0.04]",
warn: "border-severity-warn-a3 bg-severity-warn/[0.04]",
} satisfies Record<Tone, string>

const joinParts = (parts: ReadonlyArray<string>): string =>
Arr.join(
Arr.filter(parts, (part) => part !== ""),
Expand All @@ -55,70 +49,59 @@ const joinParts = (parts: ReadonlyArray<string>): string =>
const countOf = (count: number, singular: string, plural?: string): string =>
count > 0 ? `${count} ${pluralize(count, singular, plural)}` : ""

type Segment = Readonly<{ tone: Tone; title: string; detail: string }>
// Anomalies are hints, not pages: a quiet link to the inbox, never a tinted card of their own.
const anomalyLink = <M>(h: HtmlBuilder<M>, count: number): Html =>
textLink(h, { href: alertsHref({ signal: "anomaly" }), tone: "muted", className: "shrink-0 text-xs" }, [
statusDot(h, { tone: "warn" }),
`${countOf(count, "open anomaly", "open anomalies")} →`,
])

// One segment of the strip: its dot, its title in its tone, then the muted detail.
const segment = <M>(h: HtmlBuilder<M>, { tone, title, detail }: Segment): Html =>
hstack(h, { gap: "2", shrink: true }, [
statusDot(h, { tone }),
typography(h, { size: "sm", truncate: true }, [
typography(h, { weight: "medium", className: TONE_TEXT[tone] }, [title]),
typography(h, { tone: "muted", className: "ml-1" }, [detail]),
]),
const rulesCount = <M>(h: HtmlBuilder<M>, counts: HeroCounts): Html =>
typography(h, { size: "xs", tone: "muted", numeric: true, className: "shrink-0" }, [
typography(h, { tone: "foreground", weight: "medium" }, [String(counts.rulesEnabled)]),
typography(h, { tone: "subtle" }, ["/"]),
`${counts.rulesTotal} rules`,
])

const strip = <M>(
h: HtmlBuilder<M>,
segments: Arr.NonEmptyReadonlyArray<Segment>,
counts: HeroCounts,
): Html =>
const firingStrip = <M>(h: HtmlBuilder<M>, counts: HeroCounts): Html =>
card(
h,
[
cardPanel(
h,
[
hstack(h, { gap: "6", shrink: true, wrap: true, className: "gap-y-1" }, [
...Arr.map(segments, (part) => segment(h, part)),
]),
typography(h, { size: "xs", tone: "muted", numeric: true, className: "shrink-0" }, [
typography(h, { tone: "foreground", weight: "medium" }, [
String(counts.rulesEnabled),
hstack(h, { gap: "2", shrink: true }, [
statusDot(h, { tone: "crit" }),
typography(h, { size: "sm", truncate: true }, [
typography(h, { weight: "medium", className: "text-severity-error" }, [
"Firing now:",
]),
typography(h, { tone: "muted", className: "ml-1" }, [
joinParts([
countOf(counts.openCount, "incident"),
countOf(counts.criticalCount, "critical", "critical"),
countOf(counts.warningCount, "warning"),
]),
]),
]),
typography(h, { tone: "subtle" }, ["/"]),
`${counts.rulesTotal} rules`,
]),
hstack(h, { gap: "4" }, [
...(counts.anomalyCount > 0 ? [anomalyLink(h, counts.anomalyCount)] : []),
Comment thread
Makisuo marked this conversation as resolved.
Resolved
rulesCount(h, counts),
]),
],
"flex items-center justify-between gap-4 px-4 py-2",
"flex flex-wrap items-center justify-between gap-x-4 gap-y-1 px-4 py-2",
),
],
// The strip takes the worst segment's tone.
TONE_CARD[segments[0].tone],
"border-severity-error-a3 bg-severity-error/[0.04]",
)

/**
* The alert strip, absent when all clear: firing incidents (red) and open anomalies (amber) side by
* side, one rules count. Anomaly rules' incidents are open anomalies, never firing.
* The alert strip, absent when all clear. Firing incidents get the red card; open anomalies (anomaly
* rules' incidents, never firing) are a muted link beside it, or alone on a line when nothing fires.
*/
export const alertFiringHero = <M>(h: HtmlBuilder<M>, counts: HeroCounts): ReadonlyArray<Html> => {
const firing: ReadonlyArray<Segment> =
counts.openCount > 0
? [
{
tone: "crit",
title: "Firing now:",
detail: joinParts([
countOf(counts.openCount, "incident"),
countOf(counts.criticalCount, "critical", "critical"),
countOf(counts.warningCount, "warning"),
]),
},
]
: []
const anomalies: ReadonlyArray<Segment> =
counts.anomalyCount > 0
? [{ tone: "warn", title: "Open:", detail: countOf(counts.anomalyCount, "anomaly", "anomalies") }]
: []
const segments = [...firing, ...anomalies]
return Arr.isReadonlyArrayNonEmpty(segments) ? [strip(h, segments, counts)] : []
if (counts.openCount > 0) return [firingStrip(h, counts)]
if (counts.anomalyCount > 0) return [hstack(h, { justify: "end" }, [anomalyLink(h, counts.anomalyCount)])]
return []
}
Loading