Skip to content

Release health drilldown

Master-detail for on-call engineers. A deploy table built on md-table: each row pairs a version with an environment md-chip, an md-status-dot whose meaning is carried in words, and a 24-hour error-rate md-sparkline on a shared scale. Clicking a row opens a modal md-side-sheet with the full story — stacked 5xx/4xx errors in an md-area-chart, fleet adoption in an md-meter — and an md-split-button whose default action rolls back to the previous version, with variations in a paired menu.

Live preview — click a deploy row to open the drilldown, then try Roll back
checkout-service — recent deploys Error rate is the last 24 hours; all sparklines share a 0–6% scale.
Version Environment Status Error rate (24 h) v2.14.0 Healthy 0.4% v2.14.1-rc.2 Degraded 2.8% v2.14.1-canary Failing 5.1% v2.13.2 Healthy 0.3%
Share of pods currently running this version.
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-table-container variant="outlined">
  <md-table id="deploys" label="Recent deploys"
    column-template="minmax(130px, 1.1fr) 120px 140px minmax(150px, 1.4fr)">
    <md-table-head>
      <md-table-row rowgroup="head">
        <md-table-cell head scope="col">Version</md-table-cell>
        <md-table-cell head scope="col">Environment</md-table-cell>
        <md-table-cell head scope="col">Status</md-table-cell>
        <md-table-cell head scope="col">Error rate (24 h)</md-table-cell>
      </md-table-row>
    </md-table-head>
    <md-table-body>
      <md-table-row value="v2.14.0" clickable>
        <md-table-cell>v2.14.0</md-table-cell>
        <md-table-cell><md-chip label="Production" color="primary" appearance="filled"></md-chip></md-table-cell>
        <md-table-cell>
          <md-status-dot size="small" state="online" inline></md-status-dot>
          Healthy
        </md-table-cell>
        <md-table-cell>
          0.4% <md-sparkline color="success" min="0" max="6" height="24px" show-tooltip="false"></md-sparkline>
        </md-table-cell>
      </md-table-row>
    </md-table-body>
  </md-table>
</md-table-container>

<md-side-sheet id="detail" variant="modal" headline="Deploy detail" top-divider bottom-divider>
  <md-area-chart id="errors" label="Errors by class" height="200px" legend="bottom"></md-area-chart>
  <md-meter id="adoption" label="Fleet adoption" show-label show-value></md-meter>
  <md-split-button slot="actions" id="rollback-btn" icon="undo" label="Roll back"
    menu-label="More rollback options" controls="rollback-menu"></md-split-button>
  </md-side-sheet>

  <md-menu id="rollback-menu" anchor="rollback-btn" placement="top-start">
    <md-menu-item headline="Roll back and pause auto-deploy"></md-menu-item>
    <md-menu-item headline="Roll back canary only"></md-menu-item>
  </md-menu>

<script type="module">
  const table = document.querySelector('[data-deploys]');
  const sheet = document.querySelector('[data-detail]');
  const chart = document.querySelector('[data-errors]');
  const meter = document.querySelector('[data-adoption]');
  const split = document.querySelector('[data-rollback]');
  const menu  = document.getElementById('rh-rollback-menu');
  const meta  = document.querySelector('[data-meta]');

  const hours = ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00'];

  const deploys = {
    'v2.14.0': {
      env: 'Production', previous: 'v2.13.2', adoption: 97,
      meta: 'Deployed 06:12 UTC by mira.chen · commit 8f3d21a',
      spark: [0.5, 0.4, 0.6, 0.5, 0.4, 0.3, 0.4, 0.4],
      fivexx: [2, 1, 3, 2, 1, 1, 2, 2],
      fourxx: [14, 11, 16, 12, 10, 9, 12, 11]
    },
    'v2.14.1-rc.2': {
      env: 'Staging', previous: 'v2.14.0', adoption: 100,
      meta: 'Deployed 11:47 UTC by dan.kovacs · commit 51c9e02',
      spark: [0.9, 1.2, 1.4, 1.8, 2.1, 2.4, 2.6, 2.8],
      fivexx: [6, 9, 11, 14, 17, 19, 22, 24],
      fourxx: [20, 24, 28, 31, 35, 38, 41, 44]
    },
    'v2.14.1-canary': {
      env: 'Canary', previous: 'v2.14.0', adoption: 5,
      meta: 'Deployed 13:05 UTC by mira.chen · commit b74e9c1',
      spark: [0.6, 0.8, 1.4, 2.2, 3.1, 4.0, 4.7, 5.1],
      fivexx: [4, 7, 12, 19, 27, 34, 41, 46],
      fourxx: [10, 12, 15, 18, 22, 25, 29, 33]
    },
    'v2.13.2': {
      env: 'Production', previous: 'v2.13.1', adoption: 3,
      meta: 'Deployed Mon 09:20 UTC by priya.n · commit 2ab0d77',
      spark: [0.4, 0.3, 0.4, 0.3, 0.3, 0.4, 0.3, 0.3],
      fivexx: [1, 1, 2, 1, 1, 1, 1, 1],
      fourxx: [9, 8, 10, 9, 8, 9, 8, 8]
    }
  };

  // Feed each row's sparkline. Arrays never cross the attribute boundary,
  // so data / labels / valueFormatter are set as JS properties.
  document.querySelectorAll('[data-spark]').forEach((el) => {
    const d = deploys[el.dataset.spark];
    el.data = d.spark;
    el.labels = hours;
    el.valueFormatter = (v) => (v == null ? '0' : v) + '%';
  });

  let current = null;

  // Master -> detail: mdRowClick carries the row's value.
  table.addEventListener('mdRowClick', (e) => {
    const d = deploys[e.detail.value];
    if (!d) return;
    current = e.detail.value;

    sheet.headline = e.detail.value + ' · ' + d.env;
    meta.textContent = d.meta;

    chart.xAxis = { data: hours };
    chart.series = [
      { label: '5xx', data: d.fivexx, color: 'error' },
      { label: '4xx', data: d.fourxx, color: 'warning' }
    ];
    chart.valueFormatter = (v) => (v == null ? 0 : v) + '/min';

    meter.value = d.adoption;
    meter.color = d.adoption >= 90 ? 'success' : 'primary';

    split.label = 'Roll back to ' + d.previous;
    sheet.show();
  });

  // The sheet slides in from off-screen; re-measure the canvas once open.
  sheet.addEventListener('mdOpen', () => { chart.resize(); });

  // Split button: the component renders the button, we drive the menu.
  split.addEventListener('mdTrailingClick', async (e) => {
    if (e.detail.checked) await menu.show();
    else await menu.close();
  });
  // Keep the chevron truthful when the menu closes by outside click / Escape.
  menu.addEventListener('mdClose', () => { split.trailingChecked = false; });

  function rollback() {
    const row = table.querySelector('md-table-row[value="' + current + '"]');
    if (row) {
      const dot = row.querySelector('md-status-dot');
      const text = row.querySelector('[data-status]');
      dot.state = 'busy';
      dot.live = true;
      text.textContent = 'Rolling back';
      row.highlight = true;
      setTimeout(() => {
        dot.state = 'offline';
        dot.live = false;
        text.textContent = 'Rolled back';
      }, 2200);
    }
    sheet.close();
  }

  split.addEventListener('mdLeadingClick', rollback);
  menu.addEventListener('mdClick', rollback);
</script>
ComponentRole in this screen
md-tableThe master list. Rows are hand-authored, clickable, and carry a valuemdRowClick reports which deploy was chosen, and column-template declares the four tracks explicitly.
md-chipThe environment tag (appearance="filled", semantic color role names — no raw hex).
md-status-dotThe health pip. Left unlabelled, it is decorative (role="presentation"); the adjacent text carries the state as words. live pulses only on the actively failing canary.
md-sparklineThe 24-hour error-rate trend. All four share min="0" max="6" so they read on one scale, and show-tooltip="false" keeps the hover card off the row’s click target.
md-side-sheetThe drilldown surface. variant="modal" gives it a scrim, a focus trap and Escape handling; bottom-divider separates the rollback actions row.
md-area-chartStacked (the default) 5xx over 4xx error volume — composition of the total error load over the day.
md-meterFleet adoption as a role="meter" reading — value clamped, announced once via aria-valuetext.
md-split-button + md-menu“Roll back to v2.13.2” as the default action; the trailing chevron opens a menu of genuine variations of that same action.
  • Row clicks never fight embedded controls. mdRowClick is suppressed for clicks landing on interactive descendants (the md-chip is on that list), and a clickable row is a real tab stop — Enter and Space open the drilldown from the keyboard.
  • Comparable sparklines, by contract. Each md-sparkline auto-scales to its own data by default, which would make a 0.4% row look as dramatic as a 5.1% one — pinning the same min/max on all four is what makes the column honest.
  • The modal sheet owns its accessibility. role="dialog" with aria-modal, focus moved in on open, trapped while open, and restored to the clicked row on close — no wiring in the demo does any of that.
  • The chevron cannot lie. md-split-button flips trailing-checked itself and mirrors it to aria-expanded; the one thing it cannot know — the menu closing via outside click or Escape — is resynced from the menu’s mdClose.
  • Status is words, not colour. The dot is aria-hidden and the state (“Healthy”, “Failing”, “Rolling back”) lives in adjacent text, so the column survives forced-colors mode and screen readers alike (WCAG 1.4.1).
  • Persistent inspector: switch the sheet to variant="standard" and place it beside the table — no scrim, no focus trap, and the table stays interactive while the detail panel is open.
  • Bulk rollback: add selection="multiple" to the table plus slotted md-checkbox cells; the table auto-wires them and mdSelectionChange reports the selected versions for a batch action.
  • Long deploy history: wrap in md-table-container max-height="420px" with frozen-header, so the header stays put while the body scrolls.
  • Discrete periods: variant="bar" on the sparklines reads better when the error rate is bucketed per deploy window rather than sampled continuously.
  • Recipe: Two-factor verification — another full screen where component contracts (form participation, focus management) do the heavy lifting.
  • Component manuals: md-table — the grid architecture and selection model; md-side-sheet — the standard vs modal accessibility contracts.