@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.
- package/bundle/openbridge-webcomponents.bundle.js +438 -238
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +295 -37
- package/dist/components/alert-list-details-experimental/alert-list-details-experimental.css.js +3 -7
- package/dist/components/alert-list-details-experimental/alert-list-details-experimental.css.js.map +1 -1
- package/dist/components/alert-list-details-experimental/alert-list-details-experimental.d.ts +133 -13
- package/dist/components/alert-list-details-experimental/alert-list-details-experimental.d.ts.map +1 -1
- package/dist/components/alert-list-details-experimental/alert-list-details-experimental.js +330 -233
- package/dist/components/alert-list-details-experimental/alert-list-details-experimental.js.map +1 -1
- package/dist/components/table/table.d.ts.map +1 -1
- package/dist/components/table/table.js +10 -2
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/user-button/user-button.css.js +68 -1
- package/dist/components/user-button/user-button.css.js.map +1 -1
- package/dist/components/user-button/user-button.d.ts +8 -1
- package/dist/components/user-button/user-button.d.ts.map +1 -1
- package/dist/components/user-button/user-button.js +12 -3
- package/dist/components/user-button/user-button.js.map +1 -1
- package/dist/components/user-menu/user-menu.css.js +15 -0
- package/dist/components/user-menu/user-menu.css.js.map +1 -1
- package/dist/components/user-menu/user-menu.d.ts +22 -2
- package/dist/components/user-menu/user-menu.d.ts.map +1 -1
- package/dist/components/user-menu/user-menu.js +34 -2
- package/dist/components/user-menu/user-menu.js.map +1 -1
- package/dist/generated/locales/es-419.d.ts +1 -0
- package/dist/generated/locales/es-419.d.ts.map +1 -1
- package/dist/generated/locales/es-419.js +1 -0
- package/dist/generated/locales/es-419.js.map +1 -1
- package/dist/generated/locales/fi-FI.d.ts +1 -0
- package/dist/generated/locales/fi-FI.d.ts.map +1 -1
- package/dist/generated/locales/fi-FI.js +1 -0
- package/dist/generated/locales/fi-FI.js.map +1 -1
- package/package.json +2 -2
- package/src/components/alert-list-details-experimental/alert-list-details-experimental.css +3 -7
- package/src/components/alert-list-details-experimental/alert-list-details-experimental.spec.ts +188 -0
- package/src/components/alert-list-details-experimental/alert-list-details-experimental.stories.ts +142 -33
- package/src/components/alert-list-details-experimental/alert-list-details-experimental.ts +490 -252
- package/src/components/table/table.ts +15 -2
- package/src/components/user-button/user-button.css +27 -1
- package/src/components/user-button/user-button.stories.ts +29 -0
- package/src/components/user-button/user-button.ts +22 -3
- package/src/components/user-menu/user-menu.css +8 -0
- package/src/components/user-menu/user-menu.stories.ts +53 -0
- package/src/components/user-menu/user-menu.ts +55 -4
- package/src/generated/locales/es-419.ts +1 -0
- 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
|
|
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
|
|
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
|
|
59
|
-
|
|
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:
|
|
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 (
|
|
115
|
+
else if (filterMode === FilterModes.UNACKED)
|
|
68
116
|
return {
|
|
69
|
-
name:
|
|
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 (
|
|
127
|
+
else if (filterMode === FilterModes.SHELVED)
|
|
80
128
|
return {
|
|
81
|
-
name:
|
|
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 (
|
|
135
|
+
else if (filterMode === FilterModes.BLOCKED)
|
|
88
136
|
return {
|
|
89
|
-
name:
|
|
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 (
|
|
143
|
+
else if (filterMode === FilterModes.RECTIFIED)
|
|
96
144
|
return {
|
|
97
|
-
name:
|
|
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
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
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})
|
|
424
|
+
@property({type: String}) filterMode: FilterModes = FilterModes.ALL;
|
|
124
425
|
@property({type: Array}) alerts: Alert[] = [];
|
|
125
|
-
@property({type:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
156
|
-
|
|
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', {
|
|
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
|
|
165
|
-
|
|
522
|
+
const {rowId, columnKey} = e.detail;
|
|
523
|
+
const alert = this.alertByRowId.get(rowId);
|
|
524
|
+
if (alert) {
|
|
166
525
|
this.dispatchEvent(
|
|
167
|
-
new CustomEvent('
|
|
168
|
-
detail: {
|
|
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
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
|
270
|
-
|
|
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
|
|
274
|
-
return
|
|
597
|
+
private get metadata() {
|
|
598
|
+
return getFilterModeData(this.filterMode);
|
|
275
599
|
}
|
|
276
600
|
|
|
277
601
|
private buildVisibleRows(): ObcTableRow[] {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
|
631
|
+
return rowId;
|
|
356
632
|
}
|
|
357
633
|
|
|
358
634
|
private buildRowCells(
|
|
359
635
|
alert: Alert
|
|
360
636
|
): Record<string, ObcTableCellData | undefined> {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
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.
|
|
665
|
+
.columns=${this.tableColumns}
|
|
435
666
|
.striped=${true}
|
|
436
|
-
.showHeader=${
|
|
667
|
+
.showHeader=${this.showHeader}
|
|
437
668
|
@row-click=${this.onRowClick}
|
|
438
669
|
@cell-button-click=${this.onCellButtonClick}
|
|
439
670
|
@expand-toggle=${this.onExpandToggle}
|
|
440
|
-
|
|
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>
|