@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.153 → 2.0.0-next.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +438 -238
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +295 -37
  4. package/dist/components/alert-list-details-experimental/alert-list-details-experimental.css.js +3 -7
  5. package/dist/components/alert-list-details-experimental/alert-list-details-experimental.css.js.map +1 -1
  6. package/dist/components/alert-list-details-experimental/alert-list-details-experimental.d.ts +133 -13
  7. package/dist/components/alert-list-details-experimental/alert-list-details-experimental.d.ts.map +1 -1
  8. package/dist/components/alert-list-details-experimental/alert-list-details-experimental.js +330 -233
  9. package/dist/components/alert-list-details-experimental/alert-list-details-experimental.js.map +1 -1
  10. package/dist/components/table/table.d.ts.map +1 -1
  11. package/dist/components/table/table.js +10 -2
  12. package/dist/components/table/table.js.map +1 -1
  13. package/dist/components/user-button/user-button.css.js +68 -1
  14. package/dist/components/user-button/user-button.css.js.map +1 -1
  15. package/dist/components/user-button/user-button.d.ts +8 -1
  16. package/dist/components/user-button/user-button.d.ts.map +1 -1
  17. package/dist/components/user-button/user-button.js +12 -3
  18. package/dist/components/user-button/user-button.js.map +1 -1
  19. package/dist/components/user-menu/user-menu.css.js +15 -0
  20. package/dist/components/user-menu/user-menu.css.js.map +1 -1
  21. package/dist/components/user-menu/user-menu.d.ts +22 -2
  22. package/dist/components/user-menu/user-menu.d.ts.map +1 -1
  23. package/dist/components/user-menu/user-menu.js +34 -2
  24. package/dist/components/user-menu/user-menu.js.map +1 -1
  25. package/dist/generated/locales/es-419.d.ts +1 -0
  26. package/dist/generated/locales/es-419.d.ts.map +1 -1
  27. package/dist/generated/locales/es-419.js +1 -0
  28. package/dist/generated/locales/es-419.js.map +1 -1
  29. package/dist/generated/locales/fi-FI.d.ts +1 -0
  30. package/dist/generated/locales/fi-FI.d.ts.map +1 -1
  31. package/dist/generated/locales/fi-FI.js +1 -0
  32. package/dist/generated/locales/fi-FI.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/components/alert-list-details-experimental/alert-list-details-experimental.css +3 -7
  35. package/src/components/alert-list-details-experimental/alert-list-details-experimental.spec.ts +188 -0
  36. package/src/components/alert-list-details-experimental/alert-list-details-experimental.stories.ts +142 -33
  37. package/src/components/alert-list-details-experimental/alert-list-details-experimental.ts +490 -252
  38. package/src/components/table/table.ts +15 -2
  39. package/src/components/user-button/user-button.css +27 -1
  40. package/src/components/user-button/user-button.stories.ts +29 -0
  41. package/src/components/user-button/user-button.ts +22 -3
  42. package/src/components/user-menu/user-menu.css +8 -0
  43. package/src/components/user-menu/user-menu.stories.ts +53 -0
  44. package/src/components/user-menu/user-menu.ts +55 -4
  45. package/src/generated/locales/es-419.ts +1 -0
  46. package/src/generated/locales/fi-FI.ts +1 -0
@@ -1,8 +1,9 @@
1
- import {LitElement, html, unsafeCSS} from 'lit';
1
+ import {LitElement, PropertyValues, html, unsafeCSS} from 'lit';
2
2
  import {customElement} from '../../decorator.js';
3
3
  import compentStyle from './alert-list-details-experimental.css?inline';
4
4
  import {msg} from '@lit/localize';
5
5
  import {property, query, state} from 'lit/decorators.js';
6
+ import {repeat} from 'lit/directives/repeat.js';
6
7
  import '../icon-button/icon-button.js';
7
8
  import '../button/button.js';
8
9
  import '../../icons/icon-silence-iec.js';
@@ -39,7 +40,7 @@ import {
39
40
  } from '../table/table.js';
40
41
  import '../scrollbar/scrollbar.js';
41
42
 
42
- export enum AlertListMode {
43
+ export enum FilterModes {
43
44
  UNACKED = 'unacked',
44
45
  ALL = 'all',
45
46
  SHELVED = 'shelved',
@@ -47,26 +48,73 @@ export enum AlertListMode {
47
48
  RECTIFIED = 'rectified',
48
49
  }
49
50
 
50
- export type ObcAckClickEvent = CustomEvent<{
51
+ export type ObcAlertListCellClickEvent = CustomEvent<{
51
52
  alert: Alert;
53
+ columnKey: string;
54
+ rowId: string;
52
55
  }>;
53
56
 
54
57
  export type ObcRowClickEvent = CustomEvent<{
55
58
  alert: Alert;
59
+ rowId: string;
56
60
  }>;
57
61
 
58
- export function getAlertListModeData(selectedMode: AlertListMode) {
59
- if (selectedMode === AlertListMode.ALL)
62
+ export interface AlertListColumnBase {
63
+ /** Unique within the list; part of the cell slot name. */
64
+ key: string;
65
+ label: string;
66
+ /** CSS grid track size. Defaults to `1fr` for the first column, `min-content` for the rest. */
67
+ width?: string;
68
+ dividerRight?: boolean;
69
+ }
70
+
71
+ /** A column whose cells the list renders from `cell(alert)`. */
72
+ export interface AlertListDataColumn extends AlertListColumnBase {
73
+ cell: (alert: Alert) => ObcTableCellData | undefined;
74
+ /** Makes the column sortable. */
75
+ compare?: (a: Alert, b: Alert) => number;
76
+ sortDirection?: 'asc' | 'desc';
77
+ }
78
+
79
+ /** A column whose cells the consumer supplies through `cell-<key>:<rowId>` slots. */
80
+ export interface AlertListSlotColumn extends AlertListColumnBase {
81
+ slot: true;
82
+ }
83
+
84
+ export type AlertListColumn = AlertListDataColumn | AlertListSlotColumn;
85
+
86
+ export type AlertListColumnOptions = Partial<AlertListColumnBase>;
87
+
88
+ /** One cell of a slot column, for every row including rows in collapsed groups. */
89
+ export interface AlertListCellSlot {
90
+ name: string;
91
+ alert: Alert;
92
+ rowId: string;
93
+ columnKey: string;
94
+ }
95
+
96
+ export type ObcAlertListCellSlotsChangeEvent = CustomEvent<AlertListCellSlot[]>;
97
+
98
+ export interface AlertListRow {
99
+ rowId: string;
100
+ parentRowId?: string;
101
+ alert: Alert;
102
+ level: number;
103
+ expandable: boolean;
104
+ }
105
+
106
+ export function getFilterModeData(filterMode: FilterModes) {
107
+ if (filterMode === FilterModes.ALL)
60
108
  return {
61
- name: AlertListMode.ALL,
109
+ name: FilterModes.ALL,
62
110
  title: msg('All'),
63
111
  emptyTitle: msg('No active alerts'),
64
112
  emptyIcon: html`<obi-alerts></obi-alerts>`,
65
113
  filter: (alert: Alert) => !isShelved(alert) && isActive(alert),
66
114
  };
67
- else if (selectedMode === AlertListMode.UNACKED)
115
+ else if (filterMode === FilterModes.UNACKED)
68
116
  return {
69
- name: AlertListMode.UNACKED,
117
+ name: FilterModes.UNACKED,
70
118
  title: msg('Unacked'),
71
119
  emptyTitle: msg('No unacknowledged alerts'),
72
120
  emptyIcon: html`<obi-unacknowledged></obi-unacknowledged>`,
@@ -76,31 +124,31 @@ export function getAlertListModeData(selectedMode: AlertListMode) {
76
124
  !excludedFromUnackedFilter(alert.type) &&
77
125
  !isShelved(alert),
78
126
  };
79
- else if (selectedMode === AlertListMode.SHELVED)
127
+ else if (filterMode === FilterModes.SHELVED)
80
128
  return {
81
- name: AlertListMode.SHELVED,
129
+ name: FilterModes.SHELVED,
82
130
  title: msg('Shelved'),
83
131
  emptyTitle: msg('No shelved alerts'),
84
132
  emptyIcon: html`<obi-alerts-shelf></obi-alerts-shelf>`,
85
133
  filter: (alert: Alert) => isShelved(alert),
86
134
  };
87
- else if (selectedMode === AlertListMode.BLOCKED)
135
+ else if (filterMode === FilterModes.BLOCKED)
88
136
  return {
89
- name: AlertListMode.BLOCKED,
137
+ name: FilterModes.BLOCKED,
90
138
  title: msg('Blocked'),
91
139
  emptyTitle: msg('No blocked alerts'),
92
140
  emptyIcon: html`<obi-alerts-active></obi-alerts-active>`,
93
141
  filter: (alert: Alert) => isBlocked(alert),
94
142
  };
95
- else if (selectedMode === AlertListMode.RECTIFIED)
143
+ else if (filterMode === FilterModes.RECTIFIED)
96
144
  return {
97
- name: AlertListMode.RECTIFIED,
145
+ name: FilterModes.RECTIFIED,
98
146
  title: msg('Rectified'),
99
147
  emptyTitle: msg('No rectified alerts'),
100
148
  emptyIcon: html`<obi-alarm-rectified-iec></obi-alarm-rectified-iec>`,
101
149
  filter: (alert: Alert) => !isActive(alert),
102
150
  };
103
- else throw new Error('Invalid selected mode');
151
+ else throw new Error('Invalid filter mode');
104
152
  }
105
153
 
106
154
  export function canAckFilter(filter: (alert: Alert) => boolean) {
@@ -111,23 +159,278 @@ export function canAckFilter(filter: (alert: Alert) => boolean) {
111
159
  filter(alert);
112
160
  }
113
161
 
162
+ /** Name of the slot that fills the cell of a slot column in one row. */
163
+ export function alertListCellSlotName(columnKey: string, rowId: string) {
164
+ // Row ids are URI-encoded too, so neither part can contain the `:` separator.
165
+ return `cell-${encodeURIComponent(columnKey)}:${rowId}`;
166
+ }
167
+
168
+ /** Alert icon, text and source; sorted by priority. */
169
+ export function statusColumn(
170
+ options: AlertListColumnOptions = {}
171
+ ): AlertListDataColumn {
172
+ return {
173
+ key: 'status',
174
+ label: 'Status',
175
+ sortDirection: 'desc',
176
+ compare: comparePriorityAlerts,
177
+ cell: (alert) => ({
178
+ type: ObcTableCellType.Regular,
179
+ largeIcon: true,
180
+ text: alert.text,
181
+ title: alert.source,
182
+ noWrap: true,
183
+ icon: html`<obc-alert-icon
184
+ .type=${alert.type}
185
+ .acknowledged=${isAcknowledged(alert)}
186
+ .active=${isActive(alert)}
187
+ ></obc-alert-icon>`,
188
+ }),
189
+ ...options,
190
+ };
191
+ }
192
+
193
+ /** ACK button, or the no-ack icon, for alerts that await acknowledgement. The button fires `cell-click`. */
194
+ export function ackColumn(
195
+ options: AlertListColumnOptions = {}
196
+ ): AlertListDataColumn {
197
+ return {
198
+ key: 'ack',
199
+ label: 'ACK-status',
200
+ cell: (alert) => {
201
+ if (
202
+ isAcknowledged(alert) ||
203
+ !isActive(alert) ||
204
+ !requiresAcknowledgement(alert.type)
205
+ ) {
206
+ return {type: ObcTableCellType.Regular};
207
+ }
208
+ if (alert.noAck) {
209
+ const icon = usesAlarmNoAckIcon(alert.type)
210
+ ? html`<obi-alarm-noack-iec usecsscolor></obi-alarm-noack-iec>`
211
+ : html`<obi-warning-noack-iec usecsscolor></obi-warning-noack-iec>`;
212
+ return {
213
+ type: ObcTableCellType.Regular,
214
+ largeIcon: true,
215
+ icon,
216
+ align: 'center',
217
+ };
218
+ }
219
+ return {type: ObcTableCellType.Button, text: msg('ACK')};
220
+ },
221
+ ...options,
222
+ };
223
+ }
224
+
225
+ /** Activation time; sorted by time. */
226
+ export function timeColumn({
227
+ formatter = (time: Date) =>
228
+ time.toLocaleTimeString(undefined, {hour12: false}),
229
+ ...options
230
+ }: AlertListColumnOptions & {
231
+ formatter?: (time: Date) => string;
232
+ } = {}): AlertListDataColumn {
233
+ return {
234
+ key: 'time',
235
+ label: 'Activated',
236
+ compare: (a, b) => new Date(a.time).getTime() - new Date(b.time).getTime(),
237
+ cell: (alert) => ({
238
+ type: ObcTableCellType.Regular,
239
+ text: formatter(alert.time),
240
+ align: 'center',
241
+ neutral: true,
242
+ }),
243
+ ...options,
244
+ };
245
+ }
246
+
247
+ /** Tag ID prefixed with `#`; sorted alphabetically. */
248
+ export function tagIdColumn(
249
+ options: AlertListColumnOptions = {}
250
+ ): AlertListDataColumn {
251
+ return {
252
+ key: 'tagId',
253
+ label: 'Tag ID',
254
+ compare: (a, b) => a.tagId.localeCompare(b.tagId),
255
+ cell: (alert) => ({
256
+ type: ObcTableCellType.Regular,
257
+ text: '#' + alert.tagId,
258
+ align: 'right',
259
+ }),
260
+ ...options,
261
+ };
262
+ }
263
+
264
+ function isSlotColumn(column: AlertListColumn): column is AlertListSlotColumn {
265
+ return 'slot' in column && column.slot === true;
266
+ }
267
+
268
+ /** Row ids join URI-encoded alert ids with `/`, so the last `/` separates the parent. */
269
+ function parentRowIdOf(rowId: string): string | undefined {
270
+ const separatorIndex = rowId.lastIndexOf('/');
271
+ return separatorIndex === -1 ? undefined : rowId.slice(0, separatorIndex);
272
+ }
273
+
274
+ /** Keeps column keys clear of the row fields `obc-table` reserves (`id`, `level`, …). */
275
+ const TABLE_KEY_PREFIX = 'column-';
276
+
277
+ function walkAlertRows(
278
+ alerts: Alert[],
279
+ isExpanded: (rowId: string) => boolean
280
+ ): AlertListRow[] {
281
+ const alertIds = new Set(alerts.map((alert) => alert.id));
282
+ const membersByGroupId = new Map<string, Alert[]>();
283
+ const roots: Alert[] = [];
284
+ for (const alert of alerts) {
285
+ const groupIds = (alert.memberOf ?? []).filter(
286
+ (groupId) => groupId !== alert.id && alertIds.has(groupId)
287
+ );
288
+ if (groupIds.length === 0) {
289
+ roots.push(alert);
290
+ continue;
291
+ }
292
+ for (const groupId of groupIds) {
293
+ const members = membersByGroupId.get(groupId) ?? [];
294
+ members.push(alert);
295
+ membersByGroupId.set(groupId, members);
296
+ }
297
+ }
298
+
299
+ const reachable = new Set<string>();
300
+ const markReachable = (alert: Alert) => {
301
+ if (reachable.has(alert.id)) {
302
+ return;
303
+ }
304
+ reachable.add(alert.id);
305
+ for (const member of membersByGroupId.get(alert.id) ?? []) {
306
+ markReachable(member);
307
+ }
308
+ };
309
+ roots.forEach(markReachable);
310
+ for (const alert of alerts) {
311
+ if (!reachable.has(alert.id)) {
312
+ roots.push(alert);
313
+ markReachable(alert);
314
+ }
315
+ }
316
+
317
+ const rows: AlertListRow[] = [];
318
+ const visit = (
319
+ alert: Alert,
320
+ level: number,
321
+ parentRowId: string | undefined,
322
+ ancestors: Set<string>
323
+ ) => {
324
+ const segment = encodeURIComponent(alert.id);
325
+ const rowId =
326
+ parentRowId === undefined ? segment : `${parentRowId}/${segment}`;
327
+ const members = membersByGroupId.get(alert.id) ?? [];
328
+ // A member can name a group that is also its own descendant.
329
+ const expandableMembers = members.filter(
330
+ (member) => !ancestors.has(member.id)
331
+ );
332
+
333
+ rows.push({
334
+ rowId,
335
+ parentRowId,
336
+ alert,
337
+ level,
338
+ expandable: expandableMembers.length > 0,
339
+ });
340
+
341
+ if (!isExpanded(rowId)) {
342
+ return;
343
+ }
344
+ const nextAncestors = new Set(ancestors).add(alert.id);
345
+ for (const member of expandableMembers) {
346
+ visit(member, level + 1, rowId, nextAncestors);
347
+ }
348
+ };
349
+
350
+ for (const alert of roots) {
351
+ visit(alert, 0, undefined, new Set());
352
+ }
353
+ return rows;
354
+ }
355
+
114
356
  /**
115
- * @availableWhen timeFormatter showTime==true
357
+ * Every row `obc-alert-list-details-experimental` can show for these alerts
358
+ * and filter mode, rows inside collapsed groups included. An alert that is a member
359
+ * of several groups gets one row, and one `rowId`, under each.
360
+ */
361
+ export function getAlertRows(
362
+ alerts: Alert[],
363
+ filterMode: FilterModes
364
+ ): AlertListRow[] {
365
+ const {filter} = getFilterModeData(filterMode);
366
+ return walkAlertRows(alerts.filter(filter), () => true);
367
+ }
368
+
369
+ /**
370
+ * `<obc-alert-list-details-experimental>` lists alerts in a table with
371
+ * consumer-defined columns, grouping alerts through `memberOf`.
372
+ *
373
+ * ## Features
374
+ * - **Columns:** `columns` sets which columns show and in what order. A data
375
+ * column renders each cell from `cell(alert)`; a slot column renders
376
+ * whatever the consumer places in the cell's slot.
377
+ * - **Column factories:** `statusColumn()`, `ackColumn()`, `timeColumn()` and
378
+ * `tagIdColumn()` build the standard data columns; each takes overrides
379
+ * such as `label`, `width` or `dividerRight`.
380
+ * - **Filter modes:** `filterMode` lists unacknowledged, all, shelved,
381
+ * blocked or rectified alerts, with an empty state per filter mode.
382
+ * - **Grouping:** an alert listing group ids in `memberOf` renders under each
383
+ * of those groups; groups nest and can be collapsed.
384
+ * - **Selection:** `selectedRowId` highlights one row. When a collapsed group
385
+ * hides it, the nearest visible group row is highlighted instead.
386
+ *
387
+ * ## Usage Guidelines
388
+ * - Use a slot column when the cell content must be owned by the consumer,
389
+ * for example a button the application disables or removes later. Slotted
390
+ * content stays in the light DOM, so it can be looked up by id.
391
+ * - Slot names are `cell-<key>:<rowId>`. `cellSlots` lists every one, with its
392
+ * alert, row id and column key, and `cell-slots-change` fires when the list
393
+ * changes. The Svelte wrapper renders its `cell` snippet once per entry. An
394
+ * alert in two groups has two rows, so it gets two entries.
395
+ * - Without a wrapper, build the names from `getAlertRows(alerts, filterMode)`
396
+ * and `alertListCellSlotName(key, rowId)`, or read `cellSlots`.
397
+ * - Clicks on buttons, links and inputs in a cell do not fire `row-click`.
398
+ * - The consumer owns the selection: set `selectedRowId` from `row-click`, and
399
+ * set it to `undefined` on a second click to unselect. The list never changes
400
+ * it; clear it when the row is no longer in `getAlertRows(alerts, filterMode)`.
401
+ *
402
+ * ## Example
403
+ * ```html
404
+ * <obc-alert-list-details-experimental>
405
+ * <obc-button slot="cell-ack:radar" id="ack-radar">ACK</obc-button>
406
+ * </obc-alert-list-details-experimental>
407
+ * ```
408
+ * with `columns` set to `[statusColumn(), {key: 'ack', label: 'ACK-status', slot: true}]`.
409
+ *
410
+ * @property filterMode - Which alerts to list.
411
+ * @property alerts - Alerts to list.
412
+ * @property columns - Columns in display order.
413
+ * @property showHeader - Whether to show the column header row.
116
414
  * @property defaultExpanded - Whether groups start expanded. Set false to open the list collapsed.
117
- * @fires {ObcAckClickEvent} ack-click - Fired when the user clicks the "ACK" button.
415
+ * @property selectedRowId - Row id to highlight, as given by `row-click` or `getAlertRows()`. Nothing is highlighted when no row has this id.
416
+ * @slot cell-<key>:<rowId> - Content of the cell in slot column `<key>` for row `<rowId>`.
417
+ * @fires {ObcAlertListCellClickEvent} cell-click - Fired when the user clicks a button rendered by a data column, such as the one from `ackColumn()`.
118
418
  * @fires {ObcRowClickEvent} row-click - Fired when the user clicks a row.
419
+ * @fires {ObcAlertListCellSlotsChangeEvent} cell-slots-change - Fired when `cellSlots` changes; the detail is the new list.
119
420
  * @experimental
120
421
  */
121
422
  @customElement('obc-alert-list-details-experimental')
122
423
  export class ObcAlertListDetailsExperimental extends LitElement {
123
- @property({type: String}) selectedMode: AlertListMode = AlertListMode.ALL;
424
+ @property({type: String}) filterMode: FilterModes = FilterModes.ALL;
124
425
  @property({type: Array}) alerts: Alert[] = [];
125
- @property({type: Boolean}) showTime: boolean = false;
126
- @property({attribute: false}) timeFormatter: (time: Date) => string = (
127
- time: Date
128
- ) => time.toLocaleTimeString(undefined, {hour12: false});
129
- @property({type: Boolean}) small: boolean = false;
426
+ @property({type: Array, attribute: false}) columns: AlertListColumn[] = [
427
+ statusColumn(),
428
+ ackColumn({dividerRight: true}),
429
+ tagIdColumn(),
430
+ ];
431
+ @property({type: Boolean, attribute: false}) showHeader: boolean = true;
130
432
  @property({type: Boolean, attribute: false}) defaultExpanded: boolean = true;
433
+ @property({type: String}) selectedRowId?: string;
131
434
 
132
435
  @query('obc-table')
133
436
  private alertList!: ObcTable;
@@ -135,6 +438,58 @@ export class ObcAlertListDetailsExperimental extends LitElement {
135
438
  @state() private expansionOverrides = new Map<string, boolean>();
136
439
 
137
440
  private alertByRowId = new Map<string, Alert>();
441
+ private allRowIds = new Set<string>();
442
+
443
+ private _cellSlots: AlertListCellSlot[] = [];
444
+ private cellSlotsChanged = false;
445
+
446
+ /** Slot of every cell in a slot column, rows in collapsed groups included. */
447
+ get cellSlots(): AlertListCellSlot[] {
448
+ return this._cellSlots;
449
+ }
450
+
451
+ override willUpdate(changed: PropertyValues<this>) {
452
+ if (
453
+ !changed.has('alerts') &&
454
+ !changed.has('columns') &&
455
+ !changed.has('filterMode')
456
+ ) {
457
+ return;
458
+ }
459
+ const rows = getAlertRows(this.alerts, this.filterMode);
460
+ this.allRowIds = new Set(rows.map((row) => row.rowId));
461
+ const slotColumns = this.columns.filter(isSlotColumn);
462
+ const next = getAlertRows(this.alerts, this.filterMode).flatMap((row) =>
463
+ slotColumns.map((column) => ({
464
+ name: alertListCellSlotName(column.key, row.rowId),
465
+ alert: row.alert,
466
+ rowId: row.rowId,
467
+ columnKey: column.key,
468
+ }))
469
+ );
470
+ const unchanged =
471
+ next.length === this._cellSlots.length &&
472
+ next.every(
473
+ (slot, index) =>
474
+ slot.name === this._cellSlots[index].name &&
475
+ slot.alert === this._cellSlots[index].alert
476
+ );
477
+ if (!unchanged) {
478
+ this._cellSlots = next;
479
+ this.cellSlotsChanged = true;
480
+ }
481
+ }
482
+
483
+ override updated() {
484
+ if (this.cellSlotsChanged) {
485
+ this.cellSlotsChanged = false;
486
+ this.dispatchEvent(
487
+ new CustomEvent('cell-slots-change', {
488
+ detail: this._cellSlots,
489
+ }) as ObcAlertListCellSlotsChangeEvent
490
+ );
491
+ }
492
+ }
138
493
 
139
494
  public getVisibleAlerts(): Alert[] {
140
495
  const seen = new Set<string>();
@@ -152,22 +507,30 @@ export class ObcAlertListDetailsExperimental extends LitElement {
152
507
  }
153
508
 
154
509
  private onRowClick(e: ObcTableRowClickEvent) {
155
- const row = this.alertByRowId.get(e.detail.row.id);
156
- if (row) {
510
+ const rowId = e.detail.row.id;
511
+ const alert = this.alertByRowId.get(rowId);
512
+ if (alert) {
157
513
  this.dispatchEvent(
158
- new CustomEvent('row-click', {detail: {alert: row}}) as ObcRowClickEvent
514
+ new CustomEvent('row-click', {
515
+ detail: {alert, rowId},
516
+ }) as ObcRowClickEvent
159
517
  );
160
518
  }
161
519
  }
162
520
 
163
521
  private onCellButtonClick(e: ObcTableCellClickEvent) {
164
- const row = this.alertByRowId.get(e.detail.rowId);
165
- if (row) {
522
+ const {rowId, columnKey} = e.detail;
523
+ const alert = this.alertByRowId.get(rowId);
524
+ if (alert) {
166
525
  this.dispatchEvent(
167
- new CustomEvent('ack-click', {
168
- detail: {alert: row},
526
+ new CustomEvent('cell-click', {
527
+ detail: {
528
+ alert,
529
+ columnKey: columnKey.slice(TABLE_KEY_PREFIX.length),
530
+ rowId,
531
+ },
169
532
  bubbles: false,
170
- }) as ObcAckClickEvent
533
+ }) as ObcAlertListCellClickEvent
171
534
  );
172
535
  }
173
536
  }
@@ -182,243 +545,110 @@ export class ObcAlertListDetailsExperimental extends LitElement {
182
545
  return this.expansionOverrides.get(rowId) ?? this.defaultExpanded;
183
546
  }
184
547
 
185
- private get columns() {
186
- if (this.small) {
187
- const columns: ObcTableColumn<ObcTableCellData, ObcTableRow>[] = [
188
- {
189
- label: 'Status',
190
- key: 'status',
191
- sortDirection: 'desc',
192
- sortable: true,
193
- compareFunction: (_a, _b, aRow, bRow) => {
194
- const aAlert = this.alertByRowId.get(aRow.id);
195
- const bAlert = this.alertByRowId.get(bRow.id);
196
- if (aAlert && bAlert) {
197
- return comparePriorityAlerts(aAlert, bAlert);
198
- }
199
- return 0;
200
- },
201
- },
202
- ];
203
- if (this.showTime) {
204
- columns.push({
205
- label: 'Activated',
206
- key: 'time',
207
- });
548
+ private compareRows(
549
+ compare: (a: Alert, b: Alert) => number,
550
+ aRow: ObcTableRow,
551
+ bRow: ObcTableRow
552
+ ) {
553
+ const aAlert = this.alertByRowId.get(aRow.id);
554
+ const bAlert = this.alertByRowId.get(bRow.id);
555
+ return aAlert && bAlert ? compare(aAlert, bAlert) : 0;
556
+ }
557
+
558
+ private get tableColumns(): ObcTableColumn[] {
559
+ return this.columns.map((column): ObcTableColumn => {
560
+ const base = {
561
+ label: column.label,
562
+ key: TABLE_KEY_PREFIX + column.key,
563
+ dividerRight: column.dividerRight,
564
+ };
565
+ if (isSlotColumn(column)) {
566
+ return {
567
+ ...base,
568
+ renderCell: (_value, row) =>
569
+ html`<slot
570
+ name=${alertListCellSlotName(column.key, row.id)}
571
+ ></slot>`,
572
+ };
208
573
  }
209
- columns.push({
210
- label: 'ACK-status',
211
- key: 'action',
212
- });
213
- return columns;
214
- } else {
215
- const columns: ObcTableColumn<ObcTableCellData, ObcTableRow>[] = [
216
- {
217
- label: 'Status',
218
- key: 'status',
219
- sortDirection: 'desc',
220
- sortable: true,
221
- compareFunction: (_a, _b, aRow, bRow) => {
222
- const aAlert = this.alertByRowId.get(aRow.id);
223
- const bAlert = this.alertByRowId.get(bRow.id);
224
- if (aAlert && bAlert) {
225
- return comparePriorityAlerts(aAlert, bAlert);
226
- }
227
- return 0;
228
- },
229
- },
230
- {
231
- label: 'ACK-status',
232
- key: 'action',
233
- dividerRight: true,
234
- },
235
- ];
236
- if (this.showTime) {
237
- columns.push({
238
- label: 'Activated',
239
- key: 'time',
574
+ const {compare} = column;
575
+ if (compare) {
576
+ return {
577
+ ...base,
240
578
  sortable: true,
241
- compareFunction: (_a, _b, aRow, bRow) => {
242
- const aAlert = this.alertByRowId.get(aRow.id);
243
- const bAlert = this.alertByRowId.get(bRow.id);
244
- if (aAlert && bAlert) {
245
- const aTime = new Date(aAlert.time);
246
- const bTime = new Date(bAlert.time);
247
- return aTime.getTime() - bTime.getTime();
248
- }
249
- return 0;
250
- },
251
- });
579
+ sortDirection: column.sortDirection,
580
+ compareFunction: (_a, _b, aRow, bRow) =>
581
+ this.compareRows(compare, aRow, bRow),
582
+ };
252
583
  }
253
- columns.push({
254
- label: 'Tag ID',
255
- key: 'tagId',
256
- sortable: true,
257
- compareFunction: (a, b) => {
258
- const aText =
259
- a?.type === ObcTableCellType.Regular ? String(a.text ?? '') : '';
260
- const bText =
261
- b?.type === ObcTableCellType.Regular ? String(b.text ?? '') : '';
262
- return aText.localeCompare(bText);
263
- },
264
- });
265
- return columns;
266
- }
584
+ return base;
585
+ });
267
586
  }
268
587
 
269
- private get metadata() {
270
- return getAlertListModeData(this.selectedMode);
588
+ private get gridColumns() {
589
+ const columnTracks = this.columns.map(
590
+ (column, index) => column.width ?? (index === 0 ? '1fr' : 'min-content')
591
+ );
592
+ // The empty trailing track takes the table body's end padding and
593
+ // scrollbar gutter, which obc-table's subgrids add to the last track.
594
+ return [...columnTracks, 'min-content'].join(' ');
271
595
  }
272
596
 
273
- private get filteredAlerts() {
274
- return this.alerts.filter(this.metadata.filter);
597
+ private get metadata() {
598
+ return getFilterModeData(this.filterMode);
275
599
  }
276
600
 
277
601
  private buildVisibleRows(): ObcTableRow[] {
278
- const alerts = this.filteredAlerts;
279
- const alertIds = new Set(alerts.map((alert) => alert.id));
280
- const membersByGroupId = new Map<string, Alert[]>();
281
- const roots: Alert[] = [];
282
- for (const alert of alerts) {
283
- const groupIds = (alert.memberOf ?? []).filter(
284
- (groupId) => groupId !== alert.id && alertIds.has(groupId)
285
- );
286
- if (groupIds.length === 0) {
287
- roots.push(alert);
288
- continue;
289
- }
290
- for (const groupId of groupIds) {
291
- const members = membersByGroupId.get(groupId) ?? [];
292
- members.push(alert);
293
- membersByGroupId.set(groupId, members);
294
- }
295
- }
602
+ const rows = walkAlertRows(
603
+ this.alerts.filter(this.metadata.filter),
604
+ (rowId) => this.isExpanded(rowId)
605
+ );
606
+ this.alertByRowId = new Map(rows.map((row) => [row.rowId, row.alert]));
607
+ const highlightedRowId = this.highlightedRowId();
608
+ return rows.map((row) => ({
609
+ ...this.buildRowCells(row.alert),
610
+ id: row.rowId,
611
+ parentId: row.parentRowId,
612
+ level: row.level,
613
+ expandable: row.expandable,
614
+ expanded: this.isExpanded(row.rowId),
615
+ selected: row.rowId === highlightedRowId,
616
+ }));
617
+ }
296
618
 
297
- const reachable = new Set<string>();
298
- const markReachable = (alert: Alert) => {
299
- if (reachable.has(alert.id)) {
300
- return;
301
- }
302
- reachable.add(alert.id);
303
- for (const member of membersByGroupId.get(alert.id) ?? []) {
304
- markReachable(member);
305
- }
306
- };
307
- roots.forEach(markReachable);
308
- for (const alert of alerts) {
309
- if (!reachable.has(alert.id)) {
310
- roots.push(alert);
311
- markReachable(alert);
312
- }
619
+ /** The selected row, or the nearest visible group row when a collapsed group hides it. */
620
+ private highlightedRowId(): string | undefined {
621
+ if (
622
+ this.selectedRowId === undefined ||
623
+ !this.allRowIds.has(this.selectedRowId)
624
+ ) {
625
+ return undefined;
313
626
  }
314
-
315
- const rows: ObcTableRow[] = [];
316
- this.alertByRowId = new Map();
317
- const visit = (
318
- alert: Alert,
319
- level: number,
320
- parentRowId: string | undefined,
321
- ancestors: Set<string>
322
- ) => {
323
- const segment = encodeURIComponent(alert.id);
324
- const rowId =
325
- parentRowId === undefined ? segment : `${parentRowId}/${segment}`;
326
- const members = membersByGroupId.get(alert.id) ?? [];
327
- // A member can name a group that is also its own descendant.
328
- const expandableMembers = members.filter(
329
- (member) => !ancestors.has(member.id)
330
- );
331
- const expanded = this.isExpanded(rowId);
332
-
333
- this.alertByRowId.set(rowId, alert);
334
- rows.push({
335
- ...this.buildRowCells(alert),
336
- id: rowId,
337
- parentId: parentRowId,
338
- level,
339
- expandable: expandableMembers.length > 0,
340
- expanded,
341
- });
342
-
343
- if (!expanded) {
344
- return;
345
- }
346
- const nextAncestors = new Set(ancestors).add(alert.id);
347
- for (const member of expandableMembers) {
348
- visit(member, level + 1, rowId, nextAncestors);
349
- }
350
- };
351
-
352
- for (const alert of roots) {
353
- visit(alert, 0, undefined, new Set());
627
+ let rowId: string | undefined = this.selectedRowId;
628
+ while (rowId !== undefined && !this.alertByRowId.has(rowId)) {
629
+ rowId = parentRowIdOf(rowId);
354
630
  }
355
- return rows;
631
+ return rowId;
356
632
  }
357
633
 
358
634
  private buildRowCells(
359
635
  alert: Alert
360
636
  ): Record<string, ObcTableCellData | undefined> {
361
- let action: ObcTableCellData = {
362
- type: ObcTableCellType.Regular,
363
- };
364
- if (
365
- !isAcknowledged(alert) &&
366
- isActive(alert) &&
367
- requiresAcknowledgement(alert.type)
368
- ) {
369
- if (alert.noAck) {
370
- const icon = usesAlarmNoAckIcon(alert.type)
371
- ? html`<obi-alarm-noack-iec usecsscolor></obi-alarm-noack-iec>`
372
- : html`<obi-warning-noack-iec usecsscolor></obi-warning-noack-iec>`;
373
- action = {
374
- type: ObcTableCellType.Regular,
375
- largeIcon: true,
376
- icon,
377
- align: 'center',
378
- };
379
- } else {
380
- action = {
381
- type: ObcTableCellType.Button,
382
- text: msg('ACK'),
383
- };
384
- }
637
+ const cells: Record<string, ObcTableCellData | undefined> = {};
638
+ for (const column of this.columns) {
639
+ cells[TABLE_KEY_PREFIX + column.key] = isSlotColumn(column)
640
+ ? // obc-table skips renderCell for an undefined value.
641
+ {type: ObcTableCellType.Regular}
642
+ : column.cell(alert);
385
643
  }
644
+ return cells;
645
+ }
386
646
 
387
- const status: ObcTableCellData = {
388
- type: ObcTableCellType.Regular,
389
- largeIcon: true,
390
- text: alert.text,
391
- title: alert.source,
392
- noWrap: true,
393
- icon: html`<obc-alert-icon
394
- .type=${alert.type}
395
- .acknowledged=${isAcknowledged(alert)}
396
- .active=${isActive(alert)}
397
- ></obc-alert-icon>`,
398
- };
399
-
400
- const time: ObcTableCellData | undefined = this.showTime
401
- ? {
402
- type: ObcTableCellType.Regular,
403
- text: this.timeFormatter(alert.time),
404
- align: 'center',
405
- neutral: true,
406
- }
407
- : undefined;
408
-
409
- const tagId: ObcTableCellData | undefined = this.small
410
- ? undefined
411
- : {
412
- type: ObcTableCellType.Regular,
413
- text: '#' + alert.id,
414
- align: 'right',
415
- };
416
- return {
417
- status,
418
- time,
419
- action,
420
- tagId,
421
- };
647
+ private slotNames(rows: ObcTableRow[]) {
648
+ const slotColumns = this.columns.filter(isSlotColumn);
649
+ return rows.flatMap((row) =>
650
+ slotColumns.map((column) => alertListCellSlotName(column.key, row.id))
651
+ );
422
652
  }
423
653
 
424
654
  override render() {
@@ -426,18 +656,26 @@ export class ObcAlertListDetailsExperimental extends LitElement {
426
656
  const data = this.buildVisibleRows();
427
657
 
428
658
  return html`
429
- <div class="wrapper ${this.small ? 'small' : ''}">
659
+ <div class="wrapper">
430
660
  ${data.length > 0
431
661
  ? html` <obc-table
432
662
  class="alert-list"
663
+ style="--alert-list-grid-columns: ${this.gridColumns}"
433
664
  .data=${data}
434
- .columns=${this.columns}
665
+ .columns=${this.tableColumns}
435
666
  .striped=${true}
436
- .showHeader=${!this.small}
667
+ .showHeader=${this.showHeader}
437
668
  @row-click=${this.onRowClick}
438
669
  @cell-button-click=${this.onCellButtonClick}
439
670
  @expand-toggle=${this.onExpandToggle}
440
- ></obc-table>
671
+ >
672
+ ${repeat(
673
+ this.slotNames(data),
674
+ (name) => name,
675
+ // Forwards the host's slot into the one obc-table renders in the cell.
676
+ (name) => html`<slot name=${name} slot=${name}></slot>`
677
+ )}
678
+ </obc-table>
441
679
  <div class="spacer"></div>`
442
680
  : html` <div class="empty-list">
443
681
  <div class="icon">${selectedList.emptyIcon}</div>