Skip to content
UI

Data display

How Zenox shows tables, identifiers, times, empty values and severity, the content analysts read all day.

Most Zenox screens are lists of alerts, events, assets and findings. Analysts scan them for hours, compare them with other tools and cite them in reports, so these rules favor values that are exact, aligned and comparable over values that are short or friendly.

Tables

Open alerts, most recent first.
IDAlertAssetSeverityEvents
ALR-4821Impossible travel sign-inm.rocha@acme.comCritical3
ALR-4820Outbound traffic to a known C2 domainsrv-web-02High1,284
ALR-4817RDP exposed to the internetvm-jump-01High57
ALR-4815TLS certificate expires in 5 daysportal.acme.comMedium1
ALR-4809Unsigned PowerShell script ranwks-0193Low12
function TableDemo() {
  const alerts = [
    {
      id: "ALR-4821",
      title: "Impossible travel sign-in",
      asset: "m.rocha@acme.com",
      severity: "critical",
      events: 3,
    },
    {
      id: "ALR-4820",
      title: "Outbound traffic to a known C2 domain",
      asset: "srv-web-02",
      severity: "high",
      events: 1284,
    },
    {
      id: "ALR-4817",
      title: "RDP exposed to the internet",
      asset: "vm-jump-01",
      severity: "high",
      events: 57,
    },
    {
      id: "ALR-4815",
      title: "TLS certificate expires in 5 days",
      asset: "portal.acme.com",
      severity: "medium",
      events: 1,
    },
    {
      id: "ALR-4809",
      title: "Unsigned PowerShell script ran",
      asset: "wks-0193",
      severity: "low",
      events: 12,
    },
  ] as const

  return (
    <Table containerLabel="Open alerts">
      <TableCaption>Open alerts, most recent first.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>ID</TableHead>
          <TableHead>Alert</TableHead>
          <TableHead>Asset</TableHead>
          <TableHead>Severity</TableHead>
          <TableHead className="text-right">Events</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {alerts.map((alert) => (
          <TableRow key={alert.id}>
            <TableCell className="font-mono text-text-muted">
              {alert.id}
            </TableCell>
            <TableCell className="font-medium">{alert.title}</TableCell>
            <TableCell className="font-mono">{alert.asset}</TableCell>
            <TableCell>
              <SeverityBadge level={alert.severity} />
            </TableCell>
            <TableCell className="text-right tabular-nums">
              {alert.events.toLocaleString("en-US")}
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Alignment

Column holds Align Notes
Text, names, titles Left
Identifiers: IDs, hashes, IPs, hosts, e-mails Left font-mono
Numbers you compare: counts, sizes, scores, prices Right tabular-nums, so digits line up
Dates and times Left One format per column, so the values line up by length
Severity, status Left A badge with its label
Row actions Right, in the last column See Action placement

The header of a column takes the alignment of its values.

Rows

  • One line per row. Cells do not wrap. Truncate long text at the end (max-w-* on the cell, truncate and title on the text), and show the full value in the item’s detail view.
  • Default density. Use the Table’s own row height and cell padding. Do not pad cells to make a table look airier: it costs the rows the user came to see.
  • Sort by the question the page answers. Alerts by severity, then newest first; events and audit entries newest first. Show the sort on the column header.
  • Keep the header in view. A long table scrolls inside its own container with a sticky header: containerClassName="max-h-96" on Table and sticky on TableHeader.
  • Selection is a checkbox in the first column; bulk actions appear in the toolbar (see Actions on several items).

Paging

Paginate the lists users work through (alerts, assets, findings, audit entries), and show the total: “1 to 50 of 1,204”. A page number can be cited, and a position survives a reload. Infinite scroll is for feeds read once, never for lists that are exported, audited or compared.

Values

Empty values

Show an empty value with Empty Value: a muted em dash, with words for screen readers. A blank cell is ambiguous (no value, still loading, or a rendering error), and “N/A”, “null” or “-” read as data.

<TableCell>{asset.owner ?? <EmptyValue />}</TableCell>

This placeholder is the one use of the em dash in the interface; in sentences, the writing rules apply. When the absence itself means something, say it instead: “Never scanned”, “No owner”, “Not reported by the agent”.

Identifiers

  • Hashes, IPs, hosts, domains, keys and IDs use font-mono and offer Copy through Value Actions where the user may paste them elsewhere.
  • In tables, truncate at the end and give the full value in the title and through Copy. In detail views, show the whole value; a value longer than the line wraps at any character.
  • Inside a sentence, follow zx/inline-code in Layout and craft.

Numbers

  • Format through the app’s formatters, in the user’s locale: “1,204” in English, “1.204” in Portuguese and Spanish. Never format by hand.
  • Units take a space (“16 GB”, “250 ms”); percentages do not (“98%”).
  • Compact notation (“1.2K”) only in Stat Tile figures (with the exact value in exactValue) and chart axes. Tables show the exact number.
  • A KPI figure is never colored by its state; a Stat Tile tone marks the line that says why it needs attention. A change against a previous period is a delta, colored by whether it is good news, not by its direction.
  • Booleans are words or a status: “Enabled”, “Disabled”. Never “true” and “false”, and never a check mark alone.

Dates and times

  • Absolute by default, in the user’s time zone, through the app’s date formatters. Event, alert and audit lists show the date and the time to the second: analysts correlate events across tools by timestamp.
  • Relative times only for freshness, where the age matters more than the moment: “Synced 5 min ago”, “Last seen 2 h ago” in a summary. Put the absolute value in a tooltip.
  • Say which time zone wherever times are compared: in the column header’s tooltip, or as the UTC offset in a detail view. Exports and API values use ISO 8601 with the offset.
  • Follow the locale’s clock and order: the formatter writes 24-hour time for Portuguese and Spanish. Never hard-code a date format.

Severity and status

CriticalHighMediumLowUnknown
<>
  <SeverityBadge level="critical" />
  <SeverityBadge level="high" />
  <SeverityBadge level="medium" />
  <SeverityBadge level="low" />
  <SeverityBadge level="unknown" />
</>
  • Severity says how bad something is: Critical, High, Medium, Low, Info. Keep that order everywhere: sorting, filters, legends, charts and counts. Show it with Severity Badge, which pairs the color with the label.
  • Status says where an item is in its workflow: Open, Acknowledged, Resolved. Show it with Status Badge, never in severity colors: a resolved critical alert is still critical.
  • In a fixed layout, such as a row of stat tiles, keep the levels with a count of zero so the positions do not move. In an inline summary, leave them out.