Component demo · Alarms and status
Alarm Grid
index.htm as it is written with the smart-industrial package
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Alarm Grid Overview Demo</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.AlarmGrid is an ISA-18.2 alarm summary. It extends Table and adds the alarm state model.</h2>
<p>
ISA-18.2 defines the states an alarm moves through. One of them is <em>returned to normal, unacknowledged</em>: the condition cleared before anybody acknowledged it. The alarm grid keeps these alarms on the summary until they are acknowledged, so an intermittent fault stays visible.
</p>
</section>
<section>
<h2>A live summary</h2>
<div id="counts" class="counts"></div>
<smart-alarm-grid id="summary"></smart-alarm-grid>
<div class="demo-actions">
<button type="button" id="ackSelected">Acknowledge selected</button>
<button type="button" id="ackAll">Acknowledge all outstanding</button>
<button type="button" id="shelve">Shelve selected for 30 min</button>
<button type="button" id="raise">Raise a new alarm</button>
</div>
<p class="hint" id="log">Select a row, then acknowledge or shelve it.</p>
</section>
<section>
<h2>Filtering</h2>
<p>
Filters change the view but not the counts. An operator who has filtered the list to one area still needs to know how many unacknowledged alarms the plant has, so <code>counts()</code> always describes the whole alarm list.
</p>
<div class="demo-actions">
<label>State
<select id="stateFilter">
<option value="">all</option>
<option value="unacknowledged">unacknowledged</option>
<option value="acknowledged">acknowledged</option>
<option value="rtn-unacknowledged">returned to normal, unacknowledged</option>
<option value="shelved">shelved</option>
</select>
</label>
<label>Highest priority shown
<select id="priorityFilter">
<option value="">all</option>
<option value="1">1 only</option>
<option value="2">1–2</option>
<option value="3">1–3</option>
</select>
</label>
<label>Sort
<select id="sort">
<option value="priority-then-time" selected>outstanding, then priority, then newest</option>
<option value="time">newest first</option>
<option value="tag">by tag</option>
</select>
</label>
</div>
</section>
<section>
<h2>The states</h2>
<p>
Operator and engineering overrides take precedence over the process condition. Out of service is a maintenance decision and takes precedence over suppression, which is a design decision. A shelved alarm returns when the shelf expires.
</p>
<table class="states-table">
<thead>
<tr><th>State</th><th>Means</th><th>Still needs an operator?</th></tr>
</thead>
<tbody id="statesTable"></tbody>
</table>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmgrid.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* The grid reports and never writes. Every operator action arrives here as an event, and
this file is standing in for the round trip to a control system - in a real plant the
acknowledgement is not true until the DCS says it is, so the display must not assume it. */
const TAGS = [
{ tag: 'PT-2101', area: 'Feed', message: 'Discharge pressure high', priority: 1 },
{ tag: 'TT-2108', area: 'Reactor', message: 'Reactor temperature high-high', priority: 1 },
{ tag: 'LT-2201', area: 'Feed', message: 'Feed tank level low', priority: 2 },
{ tag: 'FT-2104', area: 'Feed', message: 'Feed flow below minimum', priority: 2 },
{ tag: 'ST-2112', area: 'Reactor', message: 'Agitator speed deviation', priority: 3 },
{ tag: 'CV-2140', area: 'Product', message: 'Discharge valve position deviation', priority: 3 },
{ tag: 'TT-2107', area: 'Utilities', message: 'Pre-heater outlet temperature high', priority: 2 },
{ tag: 'VB-3301', area: 'Rotating', message: 'Pump vibration above ISO 10816 zone C', priority: 2 },
{ tag: 'PS-4001', area: 'Utilities', message: 'Instrument air pressure low', priority: 4 },
{ tag: 'FI-5502', area: 'Product', message: 'Product flow totaliser stalled', priority: 4 }
];
function seed() {
const now = Date.now();
return TAGS.map(function (spec, index) {
return {
id: spec.tag,
tag: spec.tag,
area: spec.area,
message: spec.message,
priority: spec.priority,
/* A deliberate spread across the state machine, including two that have
returned to normal without ever being acknowledged. */
active: index % 4 !== 2,
acknowledged: index % 3 === 1,
timestamp: now - (index * 137000)
};
});
}
window.onload = function () {
const summary = document.getElementById('summary'),
counts = document.getElementById('counts'),
log = document.getElementById('log');
let alarms = seed();
summary.alarms = alarms;
summary.selection = true;
summary.showCounts = true;
/* Lists are handed over as new arrays, never mutated in place: the framework compares
property values, so mutating the array the element already holds makes the follow-up
assignment deep-equal and nothing redraws. */
function commit(next) {
alarms = next;
summary.alarms = alarms;
renderCounts();
}
function update(id, changes) {
commit(alarms.map(function (alarm) {
return alarm.id === id ? Object.assign({}, alarm, changes) : alarm;
}));
}
function renderCounts() {
const tally = summary.counts();
counts.innerHTML =
'<span class="count count-outstanding"><b>' + tally.outstanding + '</b> outstanding</span>' +
'<span class="count"><b>' + tally.unacknowledged + '</b> unacknowledged</span>' +
'<span class="count"><b>' + tally['rtn-unacknowledged'] + '</b> returned, unacked</span>' +
'<span class="count"><b>' + tally.acknowledged + '</b> acknowledged</span>' +
'<span class="count"><b>' + tally.shelved + '</b> shelved</span>' +
'<span class="count"><b>' + tally.total + '</b> total</span>';
}
/* ── Operator actions arrive as events ───────────────────────────── */
summary.addEventListener('acknowledge', function (event) {
update(event.detail.id, { acknowledged: true });
log.textContent = 'acknowledged ' + event.detail.id;
});
summary.addEventListener('shelve', function (event) {
update(event.detail.id, { shelvedUntil: event.detail.shelvedUntil });
log.textContent = 'shelved ' + event.detail.id + ' for ' +
Math.round(event.detail.duration / 60000) + ' minutes';
});
summary.addEventListener('unshelve', function (event) {
update(event.detail.id, { shelvedUntil: null });
log.textContent = 'unshelved ' + event.detail.id;
});
/* ── Buttons ─────────────────────────────────────────────────────── */
function selectedId() {
/* By id, never by row index - the grid is sorted and filtered, so an index stops
meaning anything the moment either changes. */
const selected = summary.selectedAlarms();
return selected.length > 0 ? selected[0].id : null;
}
document.getElementById('ackSelected').addEventListener('click', function () {
const id = selectedId();
if (id === null) {
log.textContent = 'Select a row first.';
return;
}
summary.acknowledge(id);
});
document.getElementById('ackAll').addEventListener('click', function () {
summary.acknowledgeAll();
});
document.getElementById('shelve').addEventListener('click', function () {
const id = selectedId();
if (id === null) {
log.textContent = 'Select a row first.';
return;
}
summary.shelve(id, 1800000);
});
let raised = 0;
document.getElementById('raise').addEventListener('click', function () {
raised++;
commit([{
id: 'SIM-' + raised,
tag: 'SIM-' + String(raised).padStart(4, '0'),
area: 'Simulated',
message: 'Simulated process deviation ' + raised,
priority: 1 + (raised % 4),
active: true,
acknowledged: false,
timestamp: Date.now()
}].concat(alarms));
log.textContent = 'raised SIM-' + raised;
});
/* ── Filters change the view, never the counts ───────────────────── */
function applyFilters() {
const state = document.getElementById('stateFilter').value,
priority = document.getElementById('priorityFilter').value,
filters = {};
if (state) {
filters.state = [state];
}
if (priority) {
filters.priority = Number(priority);
}
summary.filters = filters;
}
document.getElementById('stateFilter').addEventListener('change', applyFilters);
document.getElementById('priorityFilter').addEventListener('change', applyFilters);
document.getElementById('sort').addEventListener('change', function () {
summary.defaultSort = this.value;
});
/* ── The states, straight off the element ────────────────────────── */
const rows = {
'unacknowledged': 'In alarm, nobody has looked at it.',
'acknowledged': 'In alarm, an operator has seen it.',
'rtn-unacknowledged': 'The condition cleared on its own before anybody looked.',
'shelved': 'Temporarily removed by an operator. The shelf expires on its own.',
'suppressed': 'Removed by design, for a plant state where it is meaningless.',
'out-of-service': 'Removed for maintenance. Outranks everything else.',
'normal': 'Not in alarm and nothing outstanding.'
};
document.getElementById('statesTable').innerHTML =
summary.constructor.states.map(function (state) {
return '<tr><td><code>' + state + '</code></td><td>' + rows[state] + '</td><td>' +
(summary.constructor.isOutstanding(state) ? 'yes' : 'no') + '</td></tr>';
}).join('');
renderCounts();
/* A slow trickle, so the summary is never static while you look at it. */
setInterval(function () {
const index = Math.floor(Math.random() * alarms.length);
if (alarms[index]) {
update(alarms[index].id, { active: !alarms[index].active });
}
}, 4000);
};
styles.css
smart-alarm-grid {
width: 100%;
--smart-alarm-grid-height: 420px;
}
/* The grid renders its own footer inside its box, so the actions below it need to start a
new block rather than sit in the same flow - otherwise the footer overlaps them. */
.demo-actions {
clear: both;
}
.counts {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-bottom: 8px;
font-size: 12px;
}
.count b {
font-variant-numeric: tabular-nums;
font-size: 14px;
}
.count-outstanding b {
color: var(--smart-industrial-alarm-critical-text, var(--smart-industrial-alarm-critical, #c0392b));
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
.states-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.states-table th,
.states-table td {
padding: 5px 8px;
text-align: left;
border-bottom: 1px solid var(--smart-border, #ddd);
}
.states-table code {
font-size: 12px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Angular, generated from the HTML demo. The frameworks guide has the project setup.
angular/app.component.html
<div class="angular-root" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.AlarmGrid is an ISA-18.2 alarm summary. It extends Table and adds the alarm state model.</h2>
<p>
ISA-18.2 defines the states an alarm moves through. One of them is <em>returned to normal, unacknowledged</em>: the condition cleared before anybody acknowledged it. The alarm grid keeps these alarms on the summary until they are acknowledged, so an intermittent fault stays visible.
</p>
</section>
<section>
<h2>A live summary</h2>
<div id="counts" class="counts"></div>
<smart-alarm-grid id="summary"></smart-alarm-grid>
<div class="demo-actions">
<button type="button" id="ackSelected">Acknowledge selected</button>
<button type="button" id="ackAll">Acknowledge all outstanding</button>
<button type="button" id="shelve">Shelve selected for 30 min</button>
<button type="button" id="raise">Raise a new alarm</button>
</div>
<p class="hint" id="log">Select a row, then acknowledge or shelve it.</p>
</section>
<section>
<h2>Filtering</h2>
<p>
Filters change the view but not the counts. An operator who has filtered the list to one area still needs to know how many unacknowledged alarms the plant has, so <code>counts()</code> always describes the whole alarm list.
</p>
<div class="demo-actions">
<label>State
<select id="stateFilter">
<option value="">all</option>
<option value="unacknowledged">unacknowledged</option>
<option value="acknowledged">acknowledged</option>
<option value="rtn-unacknowledged">returned to normal, unacknowledged</option>
<option value="shelved">shelved</option>
</select>
</label>
<label>Highest priority shown
<select id="priorityFilter">
<option value="">all</option>
<option value="1">1 only</option>
<option value="2">1–2</option>
<option value="3">1–3</option>
</select>
</label>
<label>Sort
<select id="sort">
<option value="priority-then-time" selected>outstanding, then priority, then newest</option>
<option value="time">newest first</option>
<option value="tag">by tag</option>
</select>
</label>
</div>
</section>
<section>
<h2>The states</h2>
<p>
Operator and engineering overrides take precedence over the process condition. Out of service is a maintenance decision and takes precedence over suppression, which is a design decision. A shelved alarm returns when the shelf expires.
</p>
<table class="states-table">
<thead>
<tr><th>State</th><th>Means</th><th>Still needs an operator?</th></tr>
</thead>
<tbody id="statesTable"></tbody>
</table>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { AlarmGridModule } from 'smart-industrial/angular/alarmgrid';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - AlarmGrid in Angular.
*
* The template is the HTML demo's markup: the elements are the wrappers, so what is
* written on them is checked against their inputs. demo.ts drives them the way the
* plant's data would, once every element in the view has rendered.
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [AlarmGridModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
/* The grid reports and never writes. Every operator action arrives here as an event, and
this file is standing in for the round trip to a control system - in a real plant the
acknowledgement is not true until the DCS says it is, so the display must not assume it. */
const TAGS = [
{ tag: 'PT-2101', area: 'Feed', message: 'Discharge pressure high', priority: 1 },
{ tag: 'TT-2108', area: 'Reactor', message: 'Reactor temperature high-high', priority: 1 },
{ tag: 'LT-2201', area: 'Feed', message: 'Feed tank level low', priority: 2 },
{ tag: 'FT-2104', area: 'Feed', message: 'Feed flow below minimum', priority: 2 },
{ tag: 'ST-2112', area: 'Reactor', message: 'Agitator speed deviation', priority: 3 },
{ tag: 'CV-2140', area: 'Product', message: 'Discharge valve position deviation', priority: 3 },
{ tag: 'TT-2107', area: 'Utilities', message: 'Pre-heater outlet temperature high', priority: 2 },
{ tag: 'VB-3301', area: 'Rotating', message: 'Pump vibration above ISO 10816 zone C', priority: 2 },
{ tag: 'PS-4001', area: 'Utilities', message: 'Instrument air pressure low', priority: 4 },
{ tag: 'FI-5502', area: 'Product', message: 'Product flow totaliser stalled', priority: 4 }
];
function seed() {
const now = Date.now();
return TAGS.map(function (spec, index) {
return {
id: spec.tag,
tag: spec.tag,
area: spec.area,
message: spec.message,
priority: spec.priority,
/* A deliberate spread across the state machine, including two that have
returned to normal without ever being acknowledged. */
active: index % 4 !== 2,
acknowledged: index % 3 === 1,
timestamp: now - (index * 137000)
};
});
}
export function runDemo(): void {
const summary = document.getElementById('summary'),
counts = document.getElementById('counts'),
log = document.getElementById('log');
let alarms = seed();
summary.alarms = alarms;
summary.selection = true;
summary.showCounts = true;
/* Lists are handed over as new arrays, never mutated in place: the framework compares
property values, so mutating the array the element already holds makes the follow-up
assignment deep-equal and nothing redraws. */
function commit(next) {
alarms = next;
summary.alarms = alarms;
renderCounts();
}
function update(id, changes) {
commit(alarms.map(function (alarm) {
return alarm.id === id ? Object.assign({}, alarm, changes) : alarm;
}));
}
function renderCounts() {
const tally = summary.counts();
counts.innerHTML =
'<span class="count count-outstanding"><b>' + tally.outstanding + '</b> outstanding</span>' +
'<span class="count"><b>' + tally.unacknowledged + '</b> unacknowledged</span>' +
'<span class="count"><b>' + tally['rtn-unacknowledged'] + '</b> returned, unacked</span>' +
'<span class="count"><b>' + tally.acknowledged + '</b> acknowledged</span>' +
'<span class="count"><b>' + tally.shelved + '</b> shelved</span>' +
'<span class="count"><b>' + tally.total + '</b> total</span>';
}
/* ── Operator actions arrive as events ───────────────────────────── */
summary.addEventListener('acknowledge', function (event) {
update(event.detail.id, { acknowledged: true });
log.textContent = 'acknowledged ' + event.detail.id;
});
summary.addEventListener('shelve', function (event) {
update(event.detail.id, { shelvedUntil: event.detail.shelvedUntil });
log.textContent = 'shelved ' + event.detail.id + ' for ' +
Math.round(event.detail.duration / 60000) + ' minutes';
});
summary.addEventListener('unshelve', function (event) {
update(event.detail.id, { shelvedUntil: null });
log.textContent = 'unshelved ' + event.detail.id;
});
/* ── Buttons ─────────────────────────────────────────────────────── */
function selectedId() {
/* By id, never by row index - the grid is sorted and filtered, so an index stops
meaning anything the moment either changes. */
const selected = summary.selectedAlarms();
return selected.length > 0 ? selected[0].id : null;
}
document.getElementById('ackSelected').addEventListener('click', function () {
const id = selectedId();
if (id === null) {
log.textContent = 'Select a row first.';
return;
}
summary.acknowledge(id);
});
document.getElementById('ackAll').addEventListener('click', function () {
summary.acknowledgeAll();
});
document.getElementById('shelve').addEventListener('click', function () {
const id = selectedId();
if (id === null) {
log.textContent = 'Select a row first.';
return;
}
summary.shelve(id, 1800000);
});
let raised = 0;
document.getElementById('raise').addEventListener('click', function () {
raised++;
commit([{
id: 'SIM-' + raised,
tag: 'SIM-' + String(raised).padStart(4, '0'),
area: 'Simulated',
message: 'Simulated process deviation ' + raised,
priority: 1 + (raised % 4),
active: true,
acknowledged: false,
timestamp: Date.now()
}].concat(alarms));
log.textContent = 'raised SIM-' + raised;
});
/* ── Filters change the view, never the counts ───────────────────── */
function applyFilters() {
const state = document.getElementById('stateFilter').value,
priority = document.getElementById('priorityFilter').value,
filters = {};
if (state) {
filters.state = [state];
}
if (priority) {
filters.priority = Number(priority);
}
summary.filters = filters;
}
document.getElementById('stateFilter').addEventListener('change', applyFilters);
document.getElementById('priorityFilter').addEventListener('change', applyFilters);
document.getElementById('sort').addEventListener('change', function () {
summary.defaultSort = this.value;
});
/* ── The states, straight off the element ────────────────────────── */
const rows = {
'unacknowledged': 'In alarm, nobody has looked at it.',
'acknowledged': 'In alarm, an operator has seen it.',
'rtn-unacknowledged': 'The condition cleared on its own before anybody looked.',
'shelved': 'Temporarily removed by an operator. The shelf expires on its own.',
'suppressed': 'Removed by design, for a plant state where it is meaningless.',
'out-of-service': 'Removed for maintenance. Outranks everything else.',
'normal': 'Not in alarm and nothing outstanding.'
};
document.getElementById('statesTable').innerHTML =
summary.constructor.states.map(function (state) {
return '<tr><td><code>' + state + '</code></td><td>' + rows[state] + '</td><td>' +
(summary.constructor.isOutstanding(state) ? 'yes' : 'no') + '</td></tr>';
}).join('');
renderCounts();
/* A slow trickle, so the summary is never static while you look at it. */
setInterval(function () {
const index = Math.floor(Math.random() * alarms.length);
if (alarms[index]) {
update(alarms[index].id, { active: !alarms[index].active });
}
}, 4000);
}
angular/app.component.css
smart-alarm-grid {
width: 100%;
--smart-alarm-grid-height: 420px;
}
/* The grid renders its own footer inside its box, so the actions below it need to start a
new block rather than sit in the same flow - otherwise the footer overlaps them. */
.demo-actions {
clear: both;
}
.counts {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-bottom: 8px;
font-size: 12px;
}
.count b {
font-variant-numeric: tabular-nums;
font-size: 14px;
}
.count-outstanding b {
color: var(--smart-industrial-alarm-critical-text, var(--smart-industrial-alarm-critical, #c0392b));
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
.states-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.states-table th,
.states-table td {
padding: 5px 8px;
text-align: left;
border-bottom: 1px solid var(--smart-border, #ddd);
}
.states-table code {
font-size: 12px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in React, generated from the HTML demo. The frameworks guide has the project setup.
react/App.jsx
import 'smart-industrial/source/styles/smart.default.css';
import 'smart-industrial/source/styles/smart.industrial-elements.css';
import 'smart-industrial/source/styles/smart.industrial.css';
import 'smart-industrial/source/styles/smart.industrial-dark.css';
import './App.css';
import React from 'react';
import { AlarmGrid } from 'smart-industrial/react/alarmgrid';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* The grid reports and never writes. Every operator action arrives here as an event, and
this file is standing in for the round trip to a control system - in a real plant the
acknowledgement is not true until the DCS says it is, so the display must not assume it. */
const TAGS = [
{ tag: 'PT-2101', area: 'Feed', message: 'Discharge pressure high', priority: 1 },
{ tag: 'TT-2108', area: 'Reactor', message: 'Reactor temperature high-high', priority: 1 },
{ tag: 'LT-2201', area: 'Feed', message: 'Feed tank level low', priority: 2 },
{ tag: 'FT-2104', area: 'Feed', message: 'Feed flow below minimum', priority: 2 },
{ tag: 'ST-2112', area: 'Reactor', message: 'Agitator speed deviation', priority: 3 },
{ tag: 'CV-2140', area: 'Product', message: 'Discharge valve position deviation', priority: 3 },
{ tag: 'TT-2107', area: 'Utilities', message: 'Pre-heater outlet temperature high', priority: 2 },
{ tag: 'VB-3301', area: 'Rotating', message: 'Pump vibration above ISO 10816 zone C', priority: 2 },
{ tag: 'PS-4001', area: 'Utilities', message: 'Instrument air pressure low', priority: 4 },
{ tag: 'FI-5502', area: 'Product', message: 'Product flow totaliser stalled', priority: 4 }
];
function seed() {
const now = Date.now();
return TAGS.map(function (spec, index) {
return {
id: spec.tag,
tag: spec.tag,
area: spec.area,
message: spec.message,
priority: spec.priority,
/* A deliberate spread across the state machine, including two that have
returned to normal without ever being acknowledged. */
active: index % 4 !== 2,
acknowledged: index % 3 === 1,
timestamp: now - (index * 137000)
};
});
}
const App = () => {
const root = React.useRef(null);
React.useEffect(() => {
/* The elements render after React has mounted them, and the demo's logic - written
for a page's load event - reads their parts, so it waits for every element in the
tree to have rendered first. */
const pending = Array.from(root.current.querySelectorAll('*'))
.filter((element) => typeof element.whenRendered === 'function' && !element.isRendered)
.map((element) => element.whenRendered());
let unmounted = false;
Promise.all(pending).then(() => {
if (unmounted) {
return;
}
const summary = document.getElementById('summary'),
counts = document.getElementById('counts'),
log = document.getElementById('log');
let alarms = seed();
summary.alarms = alarms;
summary.selection = true;
summary.showCounts = true;
/* Lists are handed over as new arrays, never mutated in place: the framework compares
property values, so mutating the array the element already holds makes the follow-up
assignment deep-equal and nothing redraws. */
function commit(next) {
alarms = next;
summary.alarms = alarms;
renderCounts();
}
function update(id, changes) {
commit(alarms.map(function (alarm) {
return alarm.id === id ? Object.assign({}, alarm, changes) : alarm;
}));
}
function renderCounts() {
const tally = summary.counts();
counts.innerHTML =
'<span class="count count-outstanding"><b>' + tally.outstanding + '</b> outstanding</span>' +
'<span class="count"><b>' + tally.unacknowledged + '</b> unacknowledged</span>' +
'<span class="count"><b>' + tally['rtn-unacknowledged'] + '</b> returned, unacked</span>' +
'<span class="count"><b>' + tally.acknowledged + '</b> acknowledged</span>' +
'<span class="count"><b>' + tally.shelved + '</b> shelved</span>' +
'<span class="count"><b>' + tally.total + '</b> total</span>';
}
/* ── Operator actions arrive as events ───────────────────────────── */
summary.addEventListener('acknowledge', function (event) {
update(event.detail.id, { acknowledged: true });
log.textContent = 'acknowledged ' + event.detail.id;
});
summary.addEventListener('shelve', function (event) {
update(event.detail.id, { shelvedUntil: event.detail.shelvedUntil });
log.textContent = 'shelved ' + event.detail.id + ' for ' +
Math.round(event.detail.duration / 60000) + ' minutes';
});
summary.addEventListener('unshelve', function (event) {
update(event.detail.id, { shelvedUntil: null });
log.textContent = 'unshelved ' + event.detail.id;
});
/* ── Buttons ─────────────────────────────────────────────────────── */
function selectedId() {
/* By id, never by row index - the grid is sorted and filtered, so an index stops
meaning anything the moment either changes. */
const selected = summary.selectedAlarms();
return selected.length > 0 ? selected[0].id : null;
}
document.getElementById('ackSelected').addEventListener('click', function () {
const id = selectedId();
if (id === null) {
log.textContent = 'Select a row first.';
return;
}
summary.acknowledge(id);
});
document.getElementById('ackAll').addEventListener('click', function () {
summary.acknowledgeAll();
});
document.getElementById('shelve').addEventListener('click', function () {
const id = selectedId();
if (id === null) {
log.textContent = 'Select a row first.';
return;
}
summary.shelve(id, 1800000);
});
let raised = 0;
document.getElementById('raise').addEventListener('click', function () {
raised++;
commit([{
id: 'SIM-' + raised,
tag: 'SIM-' + String(raised).padStart(4, '0'),
area: 'Simulated',
message: 'Simulated process deviation ' + raised,
priority: 1 + (raised % 4),
active: true,
acknowledged: false,
timestamp: Date.now()
}].concat(alarms));
log.textContent = 'raised SIM-' + raised;
});
/* ── Filters change the view, never the counts ───────────────────── */
function applyFilters() {
const state = document.getElementById('stateFilter').value,
priority = document.getElementById('priorityFilter').value,
filters = {};
if (state) {
filters.state = [state];
}
if (priority) {
filters.priority = Number(priority);
}
summary.filters = filters;
}
document.getElementById('stateFilter').addEventListener('change', applyFilters);
document.getElementById('priorityFilter').addEventListener('change', applyFilters);
document.getElementById('sort').addEventListener('change', function () {
summary.defaultSort = this.value;
});
/* ── The states, straight off the element ────────────────────────── */
const rows = {
'unacknowledged': 'In alarm, nobody has looked at it.',
'acknowledged': 'In alarm, an operator has seen it.',
'rtn-unacknowledged': 'The condition cleared on its own before anybody looked.',
'shelved': 'Temporarily removed by an operator. The shelf expires on its own.',
'suppressed': 'Removed by design, for a plant state where it is meaningless.',
'out-of-service': 'Removed for maintenance. Outranks everything else.',
'normal': 'Not in alarm and nothing outstanding.'
};
document.getElementById('statesTable').innerHTML =
summary.constructor.states.map(function (state) {
return '<tr><td><code>' + state + '</code></td><td>' + rows[state] + '</td><td>' +
(summary.constructor.isOutstanding(state) ? 'yes' : 'no') + '</td></tr>';
}).join('');
renderCounts();
/* A slow trickle, so the summary is never static while you look at it. */
setInterval(function () {
const index = Math.floor(Math.random() * alarms.length);
if (alarms[index]) {
update(alarms[index].id, { active: !alarms[index].active });
}
}, 4000);
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.AlarmGrid is an ISA-18.2 alarm summary. It extends Table and adds the alarm state model.</h2>
<p>ISA-18.2 defines the states an alarm moves through. One of them is <em>returned to normal, unacknowledged</em>: the condition cleared before anybody acknowledged it. The alarm grid keeps these alarms on the summary until they are acknowledged, so an intermittent fault stays visible.</p>
</section>
<section>
<h2>A live summary</h2>
<div id="counts" className="counts"></div>
<AlarmGrid id="summary"></AlarmGrid>
<div className="demo-actions">
<button type="button" id="ackSelected">Acknowledge selected</button>
<button type="button" id="ackAll">Acknowledge all outstanding</button>
<button type="button" id="shelve">Shelve selected for 30 min</button>
<button type="button" id="raise">Raise a new alarm</button>
</div>
<p className="hint" id="log">Select a row, then acknowledge or shelve it.</p>
</section>
<section>
<h2>Filtering</h2>
<p>Filters change the view but not the counts. An operator who has filtered the list to one area still needs to know how many unacknowledged alarms the plant has, so <code>counts()</code> always describes the whole alarm list.</p>
<div className="demo-actions">
<label>
State
<select id="stateFilter"><option value="">all</option> <option value="unacknowledged">unacknowledged</option> <option value="acknowledged">acknowledged</option> <option value="rtn-unacknowledged">returned to normal, unacknowledged</option> <option value="shelved">shelved</option></select>
</label>
<label>
Highest priority shown
<select id="priorityFilter"><option value="">all</option> <option value="1">1 only</option> <option value="2">1–2</option> <option value="3">1–3</option></select>
</label>
<label>
Sort
<select id="sort" defaultValue="priority-then-time"><option value="priority-then-time">outstanding, then priority, then newest</option> <option value="time">newest first</option> <option value="tag">by tag</option></select>
</label>
</div>
</section>
<section>
<h2>The states</h2>
<p>Operator and engineering overrides take precedence over the process condition. Out of service is a maintenance decision and takes precedence over suppression, which is a design decision. A shelved alarm returns when the shelf expires.</p>
<table className="states-table">
<thead>
<tr>
<th>State</th>
<th>Means</th>
<th>Still needs an operator?</th>
</tr>
</thead>
<tbody id="statesTable"></tbody>
</table>
</section>
</div>
</div>
);
};
export default App;
react/App.css
smart-alarm-grid {
width: 100%;
--smart-alarm-grid-height: 420px;
}
/* The grid renders its own footer inside its box, so the actions below it need to start a
new block rather than sit in the same flow - otherwise the footer overlaps them. */
.demo-actions {
clear: both;
}
.counts {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-bottom: 8px;
font-size: 12px;
}
.count b {
font-variant-numeric: tabular-nums;
font-size: 14px;
}
.count-outstanding b {
color: var(--smart-industrial-alarm-critical-text, var(--smart-industrial-alarm-critical, #c0392b));
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
.states-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.states-table th,
.states-table td {
padding: 5px 8px;
text-align: left;
border-bottom: 1px solid var(--smart-border, #ddd);
}
.states-table code {
font-size: 12px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Vue, generated from the HTML demo. The frameworks guide has the project setup.
vue/App.vue
<template>
<div class="vue-root" theme="industrial" v-pre>
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.AlarmGrid is an ISA-18.2 alarm summary. It extends Table and adds the alarm state model.</h2>
<p>
ISA-18.2 defines the states an alarm moves through. One of them is <em>returned to normal, unacknowledged</em>: the condition cleared before anybody acknowledged it. The alarm grid keeps these alarms on the summary until they are acknowledged, so an intermittent fault stays visible.
</p>
</section>
<section>
<h2>A live summary</h2>
<div id="counts" class="counts"></div>
<smart-alarm-grid id="summary"></smart-alarm-grid>
<div class="demo-actions">
<button type="button" id="ackSelected">Acknowledge selected</button>
<button type="button" id="ackAll">Acknowledge all outstanding</button>
<button type="button" id="shelve">Shelve selected for 30 min</button>
<button type="button" id="raise">Raise a new alarm</button>
</div>
<p class="hint" id="log">Select a row, then acknowledge or shelve it.</p>
</section>
<section>
<h2>Filtering</h2>
<p>
Filters change the view but not the counts. An operator who has filtered the list to one area still needs to know how many unacknowledged alarms the plant has, so <code>counts()</code> always describes the whole alarm list.
</p>
<div class="demo-actions">
<label>State
<select id="stateFilter">
<option value="">all</option>
<option value="unacknowledged">unacknowledged</option>
<option value="acknowledged">acknowledged</option>
<option value="rtn-unacknowledged">returned to normal, unacknowledged</option>
<option value="shelved">shelved</option>
</select>
</label>
<label>Highest priority shown
<select id="priorityFilter">
<option value="">all</option>
<option value="1">1 only</option>
<option value="2">1–2</option>
<option value="3">1–3</option>
</select>
</label>
<label>Sort
<select id="sort">
<option value="priority-then-time" selected>outstanding, then priority, then newest</option>
<option value="time">newest first</option>
<option value="tag">by tag</option>
</select>
</label>
</div>
</section>
<section>
<h2>The states</h2>
<p>
Operator and engineering overrides take precedence over the process condition. Out of service is a maintenance decision and takes precedence over suppression, which is a design decision. A shelved alarm returns when the shelf expires.
</p>
<table class="states-table">
<thead>
<tr><th>State</th><th>Means</th><th>Still needs an operator?</th></tr>
</thead>
<tbody id="statesTable"></tbody>
</table>
</section>
</div>
</div>
</template>
<script>
import { onMounted } from "vue";
import "smart-industrial/source/styles/smart.default.css";
import "smart-industrial/source/styles/smart.industrial-elements.css";
import "smart-industrial/source/styles/smart.industrial.css";
import "smart-industrial/source/styles/smart.industrial-dark.css";
import "smart-industrial/source/modules/smart.alarmgrid.js";
/* The grid reports and never writes. Every operator action arrives here as an event, and
this file is standing in for the round trip to a control system - in a real plant the
acknowledgement is not true until the DCS says it is, so the display must not assume it. */
const TAGS = [
{ tag: 'PT-2101', area: 'Feed', message: 'Discharge pressure high', priority: 1 },
{ tag: 'TT-2108', area: 'Reactor', message: 'Reactor temperature high-high', priority: 1 },
{ tag: 'LT-2201', area: 'Feed', message: 'Feed tank level low', priority: 2 },
{ tag: 'FT-2104', area: 'Feed', message: 'Feed flow below minimum', priority: 2 },
{ tag: 'ST-2112', area: 'Reactor', message: 'Agitator speed deviation', priority: 3 },
{ tag: 'CV-2140', area: 'Product', message: 'Discharge valve position deviation', priority: 3 },
{ tag: 'TT-2107', area: 'Utilities', message: 'Pre-heater outlet temperature high', priority: 2 },
{ tag: 'VB-3301', area: 'Rotating', message: 'Pump vibration above ISO 10816 zone C', priority: 2 },
{ tag: 'PS-4001', area: 'Utilities', message: 'Instrument air pressure low', priority: 4 },
{ tag: 'FI-5502', area: 'Product', message: 'Product flow totaliser stalled', priority: 4 }
];
function seed() {
const now = Date.now();
return TAGS.map(function (spec, index) {
return {
id: spec.tag,
tag: spec.tag,
area: spec.area,
message: spec.message,
priority: spec.priority,
/* A deliberate spread across the state machine, including two that have
returned to normal without ever being acknowledged. */
active: index % 4 !== 2,
acknowledged: index % 3 === 1,
timestamp: now - (index * 137000)
};
});
}
export default {
name: "app",
setup() {
onMounted(() => {
/* The elements render after Vue has mounted them, and the demo's logic - written for
a page's load event - reads their parts, so it waits for every element in the tree
to have rendered first. */
const pending = Array.from(document.querySelector(".vue-root").querySelectorAll("*"))
.filter((element) => typeof element.whenRendered === "function" && !element.isRendered)
.map((element) => element.whenRendered());
Promise.all(pending).then(() => {
const summary = document.getElementById('summary'),
counts = document.getElementById('counts'),
log = document.getElementById('log');
let alarms = seed();
summary.alarms = alarms;
summary.selection = true;
summary.showCounts = true;
/* Lists are handed over as new arrays, never mutated in place: the framework compares
property values, so mutating the array the element already holds makes the follow-up
assignment deep-equal and nothing redraws. */
function commit(next) {
alarms = next;
summary.alarms = alarms;
renderCounts();
}
function update(id, changes) {
commit(alarms.map(function (alarm) {
return alarm.id === id ? Object.assign({}, alarm, changes) : alarm;
}));
}
function renderCounts() {
const tally = summary.counts();
counts.innerHTML =
'<span class="count count-outstanding"><b>' + tally.outstanding + '</b> outstanding</span>' +
'<span class="count"><b>' + tally.unacknowledged + '</b> unacknowledged</span>' +
'<span class="count"><b>' + tally['rtn-unacknowledged'] + '</b> returned, unacked</span>' +
'<span class="count"><b>' + tally.acknowledged + '</b> acknowledged</span>' +
'<span class="count"><b>' + tally.shelved + '</b> shelved</span>' +
'<span class="count"><b>' + tally.total + '</b> total</span>';
}
/* ── Operator actions arrive as events ───────────────────────────── */
summary.addEventListener('acknowledge', function (event) {
update(event.detail.id, { acknowledged: true });
log.textContent = 'acknowledged ' + event.detail.id;
});
summary.addEventListener('shelve', function (event) {
update(event.detail.id, { shelvedUntil: event.detail.shelvedUntil });
log.textContent = 'shelved ' + event.detail.id + ' for ' +
Math.round(event.detail.duration / 60000) + ' minutes';
});
summary.addEventListener('unshelve', function (event) {
update(event.detail.id, { shelvedUntil: null });
log.textContent = 'unshelved ' + event.detail.id;
});
/* ── Buttons ─────────────────────────────────────────────────────── */
function selectedId() {
/* By id, never by row index - the grid is sorted and filtered, so an index stops
meaning anything the moment either changes. */
const selected = summary.selectedAlarms();
return selected.length > 0 ? selected[0].id : null;
}
document.getElementById('ackSelected').addEventListener('click', function () {
const id = selectedId();
if (id === null) {
log.textContent = 'Select a row first.';
return;
}
summary.acknowledge(id);
});
document.getElementById('ackAll').addEventListener('click', function () {
summary.acknowledgeAll();
});
document.getElementById('shelve').addEventListener('click', function () {
const id = selectedId();
if (id === null) {
log.textContent = 'Select a row first.';
return;
}
summary.shelve(id, 1800000);
});
let raised = 0;
document.getElementById('raise').addEventListener('click', function () {
raised++;
commit([{
id: 'SIM-' + raised,
tag: 'SIM-' + String(raised).padStart(4, '0'),
area: 'Simulated',
message: 'Simulated process deviation ' + raised,
priority: 1 + (raised % 4),
active: true,
acknowledged: false,
timestamp: Date.now()
}].concat(alarms));
log.textContent = 'raised SIM-' + raised;
});
/* ── Filters change the view, never the counts ───────────────────── */
function applyFilters() {
const state = document.getElementById('stateFilter').value,
priority = document.getElementById('priorityFilter').value,
filters = {};
if (state) {
filters.state = [state];
}
if (priority) {
filters.priority = Number(priority);
}
summary.filters = filters;
}
document.getElementById('stateFilter').addEventListener('change', applyFilters);
document.getElementById('priorityFilter').addEventListener('change', applyFilters);
document.getElementById('sort').addEventListener('change', function () {
summary.defaultSort = this.value;
});
/* ── The states, straight off the element ────────────────────────── */
const rows = {
'unacknowledged': 'In alarm, nobody has looked at it.',
'acknowledged': 'In alarm, an operator has seen it.',
'rtn-unacknowledged': 'The condition cleared on its own before anybody looked.',
'shelved': 'Temporarily removed by an operator. The shelf expires on its own.',
'suppressed': 'Removed by design, for a plant state where it is meaningless.',
'out-of-service': 'Removed for maintenance. Outranks everything else.',
'normal': 'Not in alarm and nothing outstanding.'
};
document.getElementById('statesTable').innerHTML =
summary.constructor.states.map(function (state) {
return '<tr><td><code>' + state + '</code></td><td>' + rows[state] + '</td><td>' +
(summary.constructor.isOutstanding(state) ? 'yes' : 'no') + '</td></tr>';
}).join('');
renderCounts();
/* A slow trickle, so the summary is never static while you look at it. */
setInterval(function () {
const index = Math.floor(Math.random() * alarms.length);
if (alarms[index]) {
update(alarms[index].id, { active: !alarms[index].active });
}
}, 4000);
});
});
}
};
</script>
<style>
smart-alarm-grid {
width: 100%;
--smart-alarm-grid-height: 420px;
}
/* The grid renders its own footer inside its box, so the actions below it need to start a
new block rather than sit in the same flow - otherwise the footer overlaps them. */
.demo-actions {
clear: both;
}
.counts {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-bottom: 8px;
font-size: 12px;
}
.count b {
font-variant-numeric: tabular-nums;
font-size: 14px;
}
.count-outstanding b {
color: var(--smart-industrial-alarm-critical-text, var(--smart-industrial-alarm-critical, #c0392b));
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
.states-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.states-table th,
.states-table td {
padding: 5px 8px;
text-align: left;
border-bottom: 1px solid var(--smart-border, #ddd);
}
.states-table code {
font-size: 12px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
</style>
What the demo shows
- A live summary
- Filtering
- The states