Skip to content
Tier

Monitor card

Pro

A rule that watches something and says when it needs a person: what it watches, when it runs, the condition it alerts on, what it last found, its state in a word beside an icon ("Watching", "Needs attention", "Paused", "Not known"), and an on/off switch showing what the service reports.

The switch never claims a change that hasn't happened. While onToggle's promise runs it stays where it was, the card says "Turning on" or "Turning off", and the switch is aria-disabled with those words as its reason; when the promise settles the switch shows the on the app passes back. A rejection says why, once, and leaves the switch as reported. Focus stays on the switch throughout. With on null the service didn't say, and the card says so instead of drawing a switch. Without onToggle there is nothing to turn it with, so on is said in words ("On", "Off").

A monitor that can't run stays on the page and says why (unavailable): its switch is locked with the reason, and the reason is printed on the card beside a lock. layout="row" draws the same fields as a compact row for a list, beside ScheduleList.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0 and 4.3.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/monitor-card

Brings in: bidi, heading-level, heading-scope, locale, locked, relative-time, switch, tokens, use-announcer, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { Activity, BadgeCheck, Database, KeyRound, ShieldAlert, UserMinus } from "lucide-react"
import { MonitorCard, type MonitorFinding, type MonitorState } from "@/components/candor/monitor-card"

type Monitor = {
  id: string
  name: string
  category: string
  icon: typeof Activity
  watches: string
  runs: string
  alertWhen: string
  state: MonitorState
  on: boolean
  finding?: MonitorFinding
  lastRunAt?: string | null
  unavailable?: string
}

const hoursAgo = (h: number) => new Date(Date.now() - h * 3_600_000).toISOString()

const start: Monitor[] = [
  { id: "certs", name: "Certificate expiry", category: "Security", icon: BadgeCheck, watches: "The signing certificates every connection uses.", runs: "Daily", alertWhen: "30 days before one expires", state: "attention", on: true, finding: { text: "Payments signing certificate expires in 21 days", at: hoursAgo(5) }, lastRunAt: hoursAgo(5) },
  { id: "keys", name: "Key rotation", category: "Security", icon: KeyRound, watches: "How old each API key is.", runs: "Weekly", alertWhen: "A key is older than 90 days", state: "watching", on: true, lastRunAt: hoursAgo(30) },
  { id: "leavers", name: "Leavers with access", category: "People", icon: UserMinus, watches: "Accounts of people who have left and can still sign in.", runs: "On every directory sync", alertWhen: "A leaver can still sign in", state: "paused", on: false, lastRunAt: hoursAgo(200) },
  { id: "storage", name: "Storage growth", category: "Usage", icon: Database, watches: "How fast stored data grows.", runs: "Daily", alertWhen: "Growth passes 5% in a week", state: "watching", on: true, lastRunAt: null },
  { id: "events", name: "Unusual sign-ins", category: "Security", icon: ShieldAlert, watches: "Sign-ins from new places and devices.", runs: "Hourly", alertWhen: "Three new places in an hour", state: "unknown", on: false, unavailable: "Needs the event log add-on. This workspace isn’t licensed for it, so the monitor can’t run." },
  { id: "api", name: "API limits", category: "Usage", icon: Activity, watches: "How close each integration is to its daily API limit.", runs: "Hourly", alertWhen: "An integration passes 80% of its limit", state: "unknown", on: false, unavailable: "Needs read access to usage, which the connection doesn’t have." },
]

export default function Example() {
  const [monitors, setMonitors] = React.useState(start)
  // Stands in for your service: it answers after a moment, and the card shows what it reports.
  const toggle = (id: string) => async (on: boolean) => {
    await new Promise((resolve) => setTimeout(resolve, 900))
    if (id === "leavers" && on) throw new Error("the directory connection has expired")
    setMonitors((all) => all.map((m) => (m.id === id ? { ...m, on, state: on ? "watching" : "paused" } : m)))
  }
  return (
    <div className="flex flex-col gap-6">
      <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
        {monitors.map((m) => (
          <MonitorCard key={m.id} {...m} onToggle={toggle(m.id)} />
        ))}
      </div>
      <div className="flex flex-col gap-2">
        {monitors.slice(0, 3).map((m) => (
          <MonitorCard key={m.id} layout="row" {...m} onToggle={toggle(m.id)} />
        ))}
      </div>
    </div>
  )
}

<MonitorCard>

PropTypeDefaultDescription
namerequiredstring—The monitor's name. It names the switch.
categorystring | undefined—The kind of monitor, shown under its name: "Security", "Usage".
iconLucideIcon | undefined—An icon for the monitor, drawn in a tile beside its name.
watchesrequiredstring—What it watches, in a sentence.
runsrequiredstring—When it runs, in words: "Daily", "On every sync".
alertWhenrequiredstring—The condition it alerts on, in words: "30 days before a certificate expires".
staterequiredMonitorState—Its state as the service reports it.
stateLabelstring | undefined—The app's word for the state, in place of Candor's.
findingMonitorFinding | undefined—What it last found: a tool's text, drawn as plain text with a warning icon.
lastRunAtstring | null | undefined—When it last ran (ISO date); null when it hasn't. Left out, nothing is said.
onboolean | null | undefined—Whether it is on, as the service reports it; null when the service didn't say. Left out, there is no switch. Without `onToggle` (or `unavailable`), it is said in words ("On", "Off") rather than drawn as a switch.
onToggle((on: boolean) => Promise<void> | void) | undefined—Turns it on or off. The switch stays as reported until the promise settles and the app passes the new `on`.
unavailablestring | undefined—Why it can't run (a licence, a missing permission). The switch stays, locked with this reason, which is also printed on the card.
layout"card" | "row" | undefined"card"`"card"` (the default) or `"row"`, a compact row for a list.
headingLevelHeadingLevel | undefined—The level of the name's heading, so the card fits under the page's own headings. Defaults to one below the nearest `HeadingScope`, else 2 (`<h2>`).
classNamestring | undefined—Extra classes for the outer element.

Strings

The fixed words, in English by default. Change any of them under monitorCard in <LocaleProvider strings={{ … }}>; the table is exported as monitorCardStrings.

KeyEnglish
watchingWatching
attentionNeeds attention
pausedPaused
unknownNot known
runsRuns
alertWhenAlerts when
lastRanLast ran {when}
neverRanHasn’t run yet
foundAtFound {when}
turningOnTurning on
turningOffTurning off
onUnknownNot known whether it’s on
failedOnCouldn’t turn it on: {reason}
failedOffCouldn’t turn it off: {reason}
noReasonthe service didn’t say why
isOnOn
isOffOff

Types

MonitorState
A monitor's state as the service reports it.
"watching" | "attention" | "paused" | "unknown"
MonitorFinding
What a monitor last found: a line of a tool's text, and when.
{ /** What it found, as text (a tool's words): "Signing certificate expires in 21 days". */ text: string; /** When it was found (ISO date); null when that isn't recorded. */ at?: string | null | undefined; }

Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

HeadingLevel from @/lib/candor/heading-level
A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6