@unofficialbox/box-open-elements 0.5.0 → 0.7.0
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/README.md +27 -73
- package/dist/components/collections/table.d.ts +113 -3
- package/dist/components/collections/table.js +540 -37
- package/dist/components/feedback/due-badge.d.ts +38 -0
- package/dist/components/feedback/due-badge.js +168 -0
- package/dist/components/feedback/due-types.d.ts +32 -0
- package/dist/components/feedback/due-types.js +91 -0
- package/dist/components/feedback/progress-steps.d.ts +51 -5
- package/dist/components/feedback/progress-steps.js +200 -22
- package/dist/components/feedback/stage-path.d.ts +44 -0
- package/dist/components/feedback/stage-path.js +235 -0
- package/dist/components/forms/select.d.ts +21 -0
- package/dist/components/forms/select.js +100 -3
- package/dist/components/forms/text-field.d.ts +21 -0
- package/dist/components/forms/text-field.js +100 -1
- package/dist/components/layout/nav-sidebar.d.ts +14 -0
- package/dist/components/layout/nav-sidebar.js +49 -0
- package/dist/components/layout/split-view.d.ts +14 -0
- package/dist/components/layout/split-view.js +80 -1
- package/dist/components/overlays/command-palette.d.ts +62 -0
- package/dist/components/overlays/command-palette.js +537 -0
- package/dist/components/overlays/command-types.d.ts +90 -0
- package/dist/components/overlays/command-types.js +259 -0
- package/dist/components/overlays/drawer.d.ts +18 -0
- package/dist/components/overlays/drawer.js +135 -9
- package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
- package/dist/components/overlays/shortcuts-overlay.js +385 -0
- package/dist/components/overlays/tooltip.js +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +2 -0
- package/dist/core/offset-window.d.ts +70 -0
- package/dist/core/offset-window.js +134 -0
- package/dist/core/virtualize.d.ts +71 -0
- package/dist/core/virtualize.js +67 -0
- package/dist/element-maps.d.ts +292 -0
- package/dist/element-maps.js +1 -0
- package/dist/entries/activity-density.d.ts +1 -0
- package/dist/entries/activity-density.js +1 -0
- package/dist/entries/agent-chat.d.ts +1 -0
- package/dist/entries/agent-chat.js +1 -0
- package/dist/entries/audit-log.d.ts +1 -0
- package/dist/entries/audit-log.js +1 -0
- package/dist/entries/command-palette.d.ts +1 -0
- package/dist/entries/command-palette.js +1 -0
- package/dist/entries/compare-view.d.ts +1 -0
- package/dist/entries/compare-view.js +1 -0
- package/dist/entries/diff-viewer.d.ts +1 -0
- package/dist/entries/diff-viewer.js +1 -0
- package/dist/entries/due-badge.d.ts +1 -0
- package/dist/entries/due-badge.js +1 -0
- package/dist/entries/form-wizard.d.ts +1 -0
- package/dist/entries/form-wizard.js +1 -0
- package/dist/entries/lineage-graph.d.ts +1 -0
- package/dist/entries/lineage-graph.js +1 -0
- package/dist/entries/notification-bell.d.ts +1 -0
- package/dist/entries/notification-bell.js +1 -0
- package/dist/entries/notification-inbox.d.ts +1 -0
- package/dist/entries/notification-inbox.js +1 -0
- package/dist/entries/provenance-strip.d.ts +1 -0
- package/dist/entries/provenance-strip.js +1 -0
- package/dist/entries/run-trace.d.ts +1 -0
- package/dist/entries/run-trace.js +1 -0
- package/dist/entries/shortcuts-overlay.d.ts +1 -0
- package/dist/entries/shortcuts-overlay.js +1 -0
- package/dist/entries/signature-ceremony.d.ts +1 -0
- package/dist/entries/signature-ceremony.js +1 -0
- package/dist/entries/stage-path.d.ts +1 -0
- package/dist/entries/stage-path.js +1 -0
- package/dist/entries/timeline.d.ts +1 -0
- package/dist/entries/timeline.js +1 -0
- package/dist/entries/version-graph.d.ts +1 -0
- package/dist/entries/version-graph.js +1 -0
- package/dist/entries/version-list.d.ts +1 -0
- package/dist/entries/version-list.js +1 -0
- package/dist/entries/wizard-summary.d.ts +1 -0
- package/dist/entries/wizard-summary.js +1 -0
- package/dist/entries/work-queue.d.ts +1 -0
- package/dist/entries/work-queue.js +1 -0
- package/dist/entries/workload-board.d.ts +1 -0
- package/dist/entries/workload-board.js +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
- package/dist/patterns/agent-chat/agent-chat.js +820 -0
- package/dist/patterns/agent-chat/controller.d.ts +27 -0
- package/dist/patterns/agent-chat/controller.js +182 -0
- package/dist/patterns/agent-chat/index.d.ts +3 -0
- package/dist/patterns/agent-chat/index.js +6 -0
- package/dist/patterns/agent-chat/types.d.ts +109 -0
- package/dist/patterns/agent-chat/types.js +1 -0
- package/dist/patterns/audit/activity-density.d.ts +68 -0
- package/dist/patterns/audit/activity-density.js +428 -0
- package/dist/patterns/audit/audit-log.d.ts +182 -0
- package/dist/patterns/audit/audit-log.js +1082 -0
- package/dist/patterns/audit/index.d.ts +3 -0
- package/dist/patterns/audit/index.js +7 -0
- package/dist/patterns/audit/shared.d.ts +10 -0
- package/dist/patterns/audit/shared.js +50 -0
- package/dist/patterns/audit/types.d.ts +143 -0
- package/dist/patterns/audit/types.js +327 -0
- package/dist/patterns/audit/window.d.ts +116 -0
- package/dist/patterns/audit/window.js +132 -0
- package/dist/patterns/content-explorer/actions/controller.js +12 -2
- package/dist/patterns/content-explorer/box-transport.js +117 -25
- package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
- package/dist/patterns/content-explorer/content-explorer.js +36 -3
- package/dist/patterns/content-explorer/contracts.d.ts +24 -1
- package/dist/patterns/content-explorer/contracts.js +68 -11
- package/dist/patterns/content-explorer/controller.d.ts +16 -1
- package/dist/patterns/content-explorer/controller.js +130 -4
- package/dist/patterns/content-explorer/navigation/controller.js +11 -9
- package/dist/patterns/content-explorer/types.d.ts +58 -1
- package/dist/patterns/content-picker/content-picker.d.ts +71 -0
- package/dist/patterns/content-picker/content-picker.js +747 -0
- package/dist/patterns/content-picker/controller.d.ts +38 -0
- package/dist/patterns/content-picker/controller.js +141 -0
- package/dist/patterns/content-picker/index.d.ts +3 -0
- package/dist/patterns/content-picker/index.js +6 -0
- package/dist/patterns/content-picker/types.d.ts +52 -0
- package/dist/patterns/content-picker/types.js +26 -0
- package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
- package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
- package/dist/patterns/content-sidebar/index.d.ts +2 -0
- package/dist/patterns/content-sidebar/index.js +4 -0
- package/dist/patterns/content-sidebar/types.d.ts +18 -0
- package/dist/patterns/content-sidebar/types.js +20 -0
- package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
- package/dist/patterns/content-uploader/box-transport.js +54 -0
- package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
- package/dist/patterns/content-uploader/content-uploader.js +559 -0
- package/dist/patterns/content-uploader/controller.d.ts +39 -0
- package/dist/patterns/content-uploader/controller.js +227 -0
- package/dist/patterns/content-uploader/index.d.ts +4 -0
- package/dist/patterns/content-uploader/index.js +8 -0
- package/dist/patterns/content-uploader/types.d.ts +120 -0
- package/dist/patterns/content-uploader/types.js +38 -0
- package/dist/patterns/diff/compare-view.d.ts +47 -0
- package/dist/patterns/diff/compare-view.js +276 -0
- package/dist/patterns/diff/diff-viewer.d.ts +42 -0
- package/dist/patterns/diff/diff-viewer.js +434 -0
- package/dist/patterns/diff/engine.d.ts +22 -0
- package/dist/patterns/diff/engine.js +276 -0
- package/dist/patterns/diff/index.d.ts +4 -0
- package/dist/patterns/diff/index.js +8 -0
- package/dist/patterns/diff/types.d.ts +69 -0
- package/dist/patterns/diff/types.js +24 -0
- package/dist/patterns/form-wizard/controller.d.ts +39 -0
- package/dist/patterns/form-wizard/controller.js +157 -0
- package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
- package/dist/patterns/form-wizard/form-wizard.js +468 -0
- package/dist/patterns/form-wizard/index.d.ts +4 -0
- package/dist/patterns/form-wizard/index.js +8 -0
- package/dist/patterns/form-wizard/types.d.ts +109 -0
- package/dist/patterns/form-wizard/types.js +83 -0
- package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
- package/dist/patterns/form-wizard/wizard-summary.js +324 -0
- package/dist/patterns/internal/safe-href.d.ts +17 -0
- package/dist/patterns/internal/safe-href.js +17 -0
- package/dist/patterns/lineage/index.d.ts +3 -0
- package/dist/patterns/lineage/index.js +6 -0
- package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
- package/dist/patterns/lineage/lineage-graph.js +434 -0
- package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
- package/dist/patterns/lineage/provenance-strip.js +194 -0
- package/dist/patterns/lineage/types.d.ts +53 -0
- package/dist/patterns/lineage/types.js +49 -0
- package/dist/patterns/notifications/index.d.ts +3 -0
- package/dist/patterns/notifications/index.js +6 -0
- package/dist/patterns/notifications/notification-bell.d.ts +38 -0
- package/dist/patterns/notifications/notification-bell.js +198 -0
- package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
- package/dist/patterns/notifications/notification-inbox.js +537 -0
- package/dist/patterns/notifications/types.d.ts +61 -0
- package/dist/patterns/notifications/types.js +108 -0
- package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
- package/dist/patterns/preview/annotation-inspector.js +239 -94
- package/dist/patterns/preview/annotation-thread.d.ts +25 -5
- package/dist/patterns/preview/annotation-thread.js +233 -82
- package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
- package/dist/patterns/preview/annotation-toolbar.js +162 -96
- package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
- package/dist/patterns/preview/content-preview-adapter.js +26 -1
- package/dist/patterns/preview/preview-element.d.ts +15 -2
- package/dist/patterns/preview/preview-element.js +223 -52
- package/dist/patterns/preview/provider-adapter.d.ts +47 -0
- package/dist/patterns/preview/provider-adapter.js +17 -0
- package/dist/patterns/run/index.d.ts +2 -0
- package/dist/patterns/run/index.js +4 -0
- package/dist/patterns/run/run-trace.d.ts +40 -0
- package/dist/patterns/run/run-trace.js +441 -0
- package/dist/patterns/run/types.d.ts +66 -0
- package/dist/patterns/run/types.js +152 -0
- package/dist/patterns/signature/index.d.ts +2 -0
- package/dist/patterns/signature/index.js +4 -0
- package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
- package/dist/patterns/signature/signature-ceremony.js +317 -0
- package/dist/patterns/signature/types.d.ts +68 -0
- package/dist/patterns/signature/types.js +89 -0
- package/dist/patterns/timeline/index.d.ts +2 -0
- package/dist/patterns/timeline/index.js +4 -0
- package/dist/patterns/timeline/timeline.d.ts +27 -0
- package/dist/patterns/timeline/timeline.js +430 -0
- package/dist/patterns/timeline/types.d.ts +36 -0
- package/dist/patterns/timeline/types.js +27 -0
- package/dist/patterns/versions/graph-layout.d.ts +43 -0
- package/dist/patterns/versions/graph-layout.js +159 -0
- package/dist/patterns/versions/index.d.ts +4 -0
- package/dist/patterns/versions/index.js +8 -0
- package/dist/patterns/versions/shared.d.ts +4 -0
- package/dist/patterns/versions/shared.js +13 -0
- package/dist/patterns/versions/types.d.ts +37 -0
- package/dist/patterns/versions/types.js +39 -0
- package/dist/patterns/versions/version-graph.d.ts +36 -0
- package/dist/patterns/versions/version-graph.js +438 -0
- package/dist/patterns/versions/version-list.d.ts +39 -0
- package/dist/patterns/versions/version-list.js +407 -0
- package/dist/patterns/work-queue/controller.d.ts +26 -0
- package/dist/patterns/work-queue/controller.js +150 -0
- package/dist/patterns/work-queue/index.d.ts +4 -0
- package/dist/patterns/work-queue/index.js +7 -0
- package/dist/patterns/work-queue/types.d.ts +128 -0
- package/dist/patterns/work-queue/types.js +45 -0
- package/dist/patterns/work-queue/work-queue.d.ts +48 -0
- package/dist/patterns/work-queue/work-queue.js +534 -0
- package/dist/patterns/work-queue/workload-board.d.ts +53 -0
- package/dist/patterns/work-queue/workload-board.js +608 -0
- package/package.json +89 -5
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
import { AUDIT_GROUP_BY_LABELS, AUDIT_GROUP_BY_VALUES, filterAuditEvents, groupAuditEvents, hasAuditFacets, resolveAuditGroupBy, summarizeAuditFacets, toAuditCsv, } from "./types.js";
|
|
2
|
+
import { auditToneColor, escapeHtml, formatAuditTimestamp, resolveReferenceTime, } from "./shared.js";
|
|
3
|
+
import { isSafeHref } from "../internal/safe-href.js";
|
|
4
|
+
import { isTimelineEventRecord, resolveTimelineTone } from "../timeline/types.js";
|
|
5
|
+
import { BaseElement, sameRowWindow } from "../../core/index.js";
|
|
6
|
+
import { buildAuditIndex, flattenAuditRows, planAuditWindow } from "./window.js";
|
|
7
|
+
import { boeInputControlStyles, boePanel, boeRadius } from "../../foundations/geometry/index.js";
|
|
8
|
+
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* How many times the element will adopt a fresh height measurement for one row
|
|
11
|
+
* set. A hard stop, not a tuning knob: without it a non-uniform list can adopt,
|
|
12
|
+
* re-window, re-measure and adopt again on every animation frame forever.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_HEIGHT_ADOPTIONS = 3;
|
|
15
|
+
const DEFAULT_TAG_NAME = "box-audit-log";
|
|
16
|
+
/** Facet attribute ↔ `AuditFacets` key. One row per facet, no other mapping. */
|
|
17
|
+
const FACET_ATTRIBUTES = [
|
|
18
|
+
["actor", "facet-actor"],
|
|
19
|
+
["action", "facet-action"],
|
|
20
|
+
["correlationId", "facet-correlation-id"],
|
|
21
|
+
["from", "facet-from"],
|
|
22
|
+
["to", "facet-to"],
|
|
23
|
+
];
|
|
24
|
+
const elementStyles = `
|
|
25
|
+
[hidden] {
|
|
26
|
+
display: none !important;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:host {
|
|
30
|
+
display: block;
|
|
31
|
+
color: inherit;
|
|
32
|
+
font: inherit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[part="panel"] {
|
|
36
|
+
display: grid;
|
|
37
|
+
gap: ${boePanel.gap};
|
|
38
|
+
padding: ${boePanel.padding};
|
|
39
|
+
border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 82%, transparent);
|
|
40
|
+
border-radius: ${boePanel.radius};
|
|
41
|
+
background: ${boePanel.background};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[part="title"] {
|
|
45
|
+
margin: 0;
|
|
46
|
+
font: inherit;
|
|
47
|
+
font-size: 1.1rem;
|
|
48
|
+
font-weight: 700;
|
|
49
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[part="toolbar"] {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
align-items: end;
|
|
56
|
+
gap: 0.6rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[part="group-by"] {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
62
|
+
border-radius: 999px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[part="group-by-option"] {
|
|
67
|
+
appearance: none;
|
|
68
|
+
font: inherit;
|
|
69
|
+
font-size: 0.8rem;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
padding: 0.3rem 0.7rem;
|
|
72
|
+
border: none;
|
|
73
|
+
background: transparent;
|
|
74
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[part="group-by-option"][aria-pressed="true"] {
|
|
80
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 12%, transparent);
|
|
81
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[part="facet"] {
|
|
85
|
+
display: grid;
|
|
86
|
+
gap: 0.2rem;
|
|
87
|
+
font-size: 0.72rem;
|
|
88
|
+
font-weight: 700;
|
|
89
|
+
letter-spacing: 0.06em;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
${boeInputControlStyles('[part="facet"] select, [part="facet"] input')}
|
|
95
|
+
|
|
96
|
+
[part="facet"] select,
|
|
97
|
+
[part="facet"] input {
|
|
98
|
+
font: inherit;
|
|
99
|
+
font-size: 0.85rem;
|
|
100
|
+
text-transform: none;
|
|
101
|
+
letter-spacing: normal;
|
|
102
|
+
font-weight: 400;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
[part="clear-filters"],
|
|
106
|
+
[part="export"],
|
|
107
|
+
[part="drilldown-clear"] {
|
|
108
|
+
appearance: none;
|
|
109
|
+
font: inherit;
|
|
110
|
+
font-size: 0.8rem;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
padding: 0.35rem 0.7rem;
|
|
113
|
+
border-radius: ${boeRadius.control};
|
|
114
|
+
border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
115
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
116
|
+
color: var(--boe-token-text-text, #222222);
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
[part="clear-filters"]:hover,
|
|
121
|
+
[part="export"]:hover,
|
|
122
|
+
[part="drilldown-clear"]:hover {
|
|
123
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
124
|
+
border-color: var(--boe-token-stroke-stroke-hover, #bcbcbc);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[part="drilldown"] {
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-wrap: wrap;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 0.5rem;
|
|
132
|
+
padding: 0.4rem 0.6rem;
|
|
133
|
+
border-radius: ${boeRadius.med};
|
|
134
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 8%, transparent);
|
|
135
|
+
font-size: 0.82rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[part="drilldown-id"] {
|
|
139
|
+
font-family: ui-monospace, monospace;
|
|
140
|
+
font-weight: 700;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
[part="result-summary"] {
|
|
144
|
+
margin: 0;
|
|
145
|
+
font-size: 0.8rem;
|
|
146
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
[part="groups"] {
|
|
150
|
+
display: grid;
|
|
151
|
+
gap: 0.5rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Windowing needs a bounded viewport to scroll within:
|
|
155
|
+
--boe-audit-height is the host's knob, defaulting to a page-sized
|
|
156
|
+
band. */
|
|
157
|
+
:host([virtualize]) [part="groups"] {
|
|
158
|
+
max-block-size: var(--boe-audit-height, 32rem);
|
|
159
|
+
overflow-y: auto;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[part="spacer"] {
|
|
163
|
+
/* The grid gap must not apply to the spacers, or the scroll range
|
|
164
|
+
gains a gap's height at each end and the content drifts. */
|
|
165
|
+
margin-block: calc(-0.25rem);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
[part="group"] {
|
|
169
|
+
border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 70%, transparent);
|
|
170
|
+
border-radius: ${boeRadius.med};
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
[part="group-heading"] {
|
|
175
|
+
margin: 0;
|
|
176
|
+
font: inherit;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
[part="group-toggle"] {
|
|
180
|
+
appearance: none;
|
|
181
|
+
display: flex;
|
|
182
|
+
inline-size: 100%;
|
|
183
|
+
align-items: baseline;
|
|
184
|
+
gap: 0.5rem;
|
|
185
|
+
padding: 0.5rem 0.65rem;
|
|
186
|
+
border: none;
|
|
187
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
188
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
189
|
+
font: inherit;
|
|
190
|
+
font-weight: 700;
|
|
191
|
+
text-align: start;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
[part="group-toggle"]::before {
|
|
196
|
+
content: "▾";
|
|
197
|
+
font-size: 0.7rem;
|
|
198
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
[part="group-toggle"][aria-expanded="false"]::before {
|
|
202
|
+
content: "▸";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
[part="group-count"] {
|
|
206
|
+
margin-left: auto;
|
|
207
|
+
font-size: 0.76rem;
|
|
208
|
+
font-weight: 600;
|
|
209
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
[part="events"] {
|
|
213
|
+
list-style: none;
|
|
214
|
+
margin: 0;
|
|
215
|
+
padding: 0;
|
|
216
|
+
display: grid;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[part="event"] {
|
|
220
|
+
display: grid;
|
|
221
|
+
gap: 0.25rem;
|
|
222
|
+
padding: 0.5rem 0.65rem;
|
|
223
|
+
border-top: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 55%, transparent);
|
|
224
|
+
border-left: 3px solid var(--audit-tone, transparent);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
[part="event-topline"] {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-wrap: wrap;
|
|
230
|
+
align-items: baseline;
|
|
231
|
+
gap: 0.4rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
[part="event-action"] {
|
|
235
|
+
appearance: none;
|
|
236
|
+
padding: 0;
|
|
237
|
+
border: none;
|
|
238
|
+
background: none;
|
|
239
|
+
font: inherit;
|
|
240
|
+
font-weight: 600;
|
|
241
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
242
|
+
text-align: start;
|
|
243
|
+
cursor: pointer;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
[part="event-action"]:hover {
|
|
247
|
+
text-decoration: underline;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
[part="actor"] {
|
|
251
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
252
|
+
font-size: 0.82rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
[part="badge"] {
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
padding: 0.12rem 0.42rem;
|
|
258
|
+
border-radius: 999px;
|
|
259
|
+
background: color-mix(in srgb, var(--audit-tone, var(--boe-token-surface-surface-brand, #0061d5)) 12%, transparent);
|
|
260
|
+
color: color-mix(in srgb, var(--audit-tone, var(--boe-token-surface-surface-brand, #0061d5)) 74%, black 26%);
|
|
261
|
+
font-size: 0.72rem;
|
|
262
|
+
font-weight: 600;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
[part="timestamp"] {
|
|
266
|
+
margin-left: auto;
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
font-size: 0.75rem;
|
|
269
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
[part="event-summary"] {
|
|
273
|
+
margin: 0;
|
|
274
|
+
font-size: 0.85rem;
|
|
275
|
+
line-height: 1.5;
|
|
276
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
[part="evidence"] {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-wrap: wrap;
|
|
282
|
+
gap: 0.35rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
[part="evidence-link"] {
|
|
286
|
+
appearance: none;
|
|
287
|
+
display: inline-flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
padding: 0.18rem 0.5rem;
|
|
290
|
+
border: 1px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 30%, transparent);
|
|
291
|
+
border-radius: 999px;
|
|
292
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 6%, transparent);
|
|
293
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
294
|
+
font: inherit;
|
|
295
|
+
font-size: 0.74rem;
|
|
296
|
+
font-weight: 600;
|
|
297
|
+
text-decoration: none;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
[part="correlation"] {
|
|
302
|
+
appearance: none;
|
|
303
|
+
justify-self: start;
|
|
304
|
+
padding: 0.1rem 0.35rem;
|
|
305
|
+
border: 1px dashed color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 90%, transparent);
|
|
306
|
+
border-radius: ${boeRadius.size};
|
|
307
|
+
background: none;
|
|
308
|
+
font-family: ui-monospace, monospace;
|
|
309
|
+
font-size: 0.7rem;
|
|
310
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
[part="correlation"]:hover {
|
|
315
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
316
|
+
border-color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 45%, transparent);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
[part="empty"] {
|
|
320
|
+
padding: ${boePanel.padding};
|
|
321
|
+
border-radius: ${boeRadius.large};
|
|
322
|
+
border: 1px dashed color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 70%, transparent);
|
|
323
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
[part="group-by-option"]:focus-visible,
|
|
327
|
+
[part="group-toggle"]:focus-visible,
|
|
328
|
+
[part="event-action"]:focus-visible,
|
|
329
|
+
[part="evidence-link"]:focus-visible,
|
|
330
|
+
[part="correlation"]:focus-visible,
|
|
331
|
+
[part="clear-filters"]:focus-visible,
|
|
332
|
+
[part="export"]:focus-visible,
|
|
333
|
+
[part="drilldown-clear"]:focus-visible {
|
|
334
|
+
outline: none;
|
|
335
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, transparent);
|
|
336
|
+
}
|
|
337
|
+
`;
|
|
338
|
+
/**
|
|
339
|
+
* The aggregation, faceting, and export layer over the `box-timeline` event
|
|
340
|
+
* contract (opportunity 3 of `plans/component-opportunities.md`): the same
|
|
341
|
+
* records the flat feed renders, grouped into collapsible day/actor/action
|
|
342
|
+
* sections with counts, narrowed by facets and a date range, drillable to one
|
|
343
|
+
* workflow run by correlation id, and exportable as CSV.
|
|
344
|
+
*
|
|
345
|
+
* Aggregation is client-side and pure. `virtualize` windows the log for
|
|
346
|
+
* production-scale volumes; server-side paging remains a tracked depth
|
|
347
|
+
* limitation.
|
|
348
|
+
*/
|
|
349
|
+
export class AuditLog extends BaseElement {
|
|
350
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
351
|
+
static get observedAttributes() {
|
|
352
|
+
return [
|
|
353
|
+
"events",
|
|
354
|
+
"exportable",
|
|
355
|
+
"facet-action",
|
|
356
|
+
"facet-actor",
|
|
357
|
+
"facet-correlation-id",
|
|
358
|
+
"facet-from",
|
|
359
|
+
"facet-to",
|
|
360
|
+
"group-by",
|
|
361
|
+
"heading",
|
|
362
|
+
"reference-time",
|
|
363
|
+
"virtualize",
|
|
364
|
+
"row-height",
|
|
365
|
+
"heading-height",
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
toolbarEl;
|
|
369
|
+
groupByEl;
|
|
370
|
+
actorSelectEl;
|
|
371
|
+
actionSelectEl;
|
|
372
|
+
fromInputEl;
|
|
373
|
+
toInputEl;
|
|
374
|
+
clearFiltersEl;
|
|
375
|
+
exportEl;
|
|
376
|
+
drilldownEl;
|
|
377
|
+
drilldownIdEl;
|
|
378
|
+
summaryEl;
|
|
379
|
+
groupsEl;
|
|
380
|
+
emptyEl;
|
|
381
|
+
titleEl;
|
|
382
|
+
/** Section keys the reader has collapsed. Absent means expanded. */
|
|
383
|
+
collapsedGroups = new Set();
|
|
384
|
+
/** The row window the last render used; null when it rendered everything. */
|
|
385
|
+
lastWindow = null;
|
|
386
|
+
scrollFrame = 0;
|
|
387
|
+
measuredHeights = null;
|
|
388
|
+
measureFrame = 0;
|
|
389
|
+
/** Row set the adoption budget below is counted against. */
|
|
390
|
+
adoptedFor = null;
|
|
391
|
+
adoptionCount = 0;
|
|
392
|
+
viewportObserver = null;
|
|
393
|
+
contentSignature = "";
|
|
394
|
+
optionsSignature = "";
|
|
395
|
+
/** Raw `events` attribute the parse cache was built from. */
|
|
396
|
+
eventsRaw = null;
|
|
397
|
+
eventsCache = [];
|
|
398
|
+
get heading() {
|
|
399
|
+
return this.getAttribute("heading") ?? "Audit log";
|
|
400
|
+
}
|
|
401
|
+
set heading(value) {
|
|
402
|
+
this.setAttribute("heading", value);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Audit records; JSON payloads are validated per record before rendering.
|
|
406
|
+
*
|
|
407
|
+
* Memoized on the raw attribute so a single keystroke in the date field
|
|
408
|
+
* does not re-parse and re-validate the whole payload once per reader
|
|
409
|
+
* (`update`, `visibleEvents`, and each click-handler branch all read it).
|
|
410
|
+
* The cache invalidates itself when the attribute changes; the returned
|
|
411
|
+
* array is a copy, so a caller cannot mutate the log out from under it.
|
|
412
|
+
*/
|
|
413
|
+
get events() {
|
|
414
|
+
const raw = this.getAttribute("events");
|
|
415
|
+
if (!raw) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
if (raw !== this.eventsRaw) {
|
|
419
|
+
this.eventsRaw = raw;
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(raw);
|
|
422
|
+
this.eventsCache =
|
|
423
|
+
Array.isArray(parsed) && parsed.every(isTimelineEventRecord)
|
|
424
|
+
? parsed
|
|
425
|
+
: [];
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
this.eventsCache = [];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return [...this.eventsCache];
|
|
432
|
+
}
|
|
433
|
+
set events(value) {
|
|
434
|
+
if (value.length) {
|
|
435
|
+
this.setAttribute("events", JSON.stringify(value));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this.removeAttribute("events");
|
|
439
|
+
}
|
|
440
|
+
get groupBy() {
|
|
441
|
+
return resolveAuditGroupBy(this.getAttribute("group-by"));
|
|
442
|
+
}
|
|
443
|
+
set groupBy(value) {
|
|
444
|
+
this.setAttribute("group-by", resolveAuditGroupBy(value));
|
|
445
|
+
}
|
|
446
|
+
get facets() {
|
|
447
|
+
const facets = {};
|
|
448
|
+
for (const [key, attribute] of FACET_ATTRIBUTES) {
|
|
449
|
+
const value = this.getAttribute(attribute);
|
|
450
|
+
if (value) {
|
|
451
|
+
facets[key] = value;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return facets;
|
|
455
|
+
}
|
|
456
|
+
/** Replaces the whole selection — an omitted or empty facet is cleared. */
|
|
457
|
+
set facets(value) {
|
|
458
|
+
for (const [key, attribute] of FACET_ATTRIBUTES) {
|
|
459
|
+
const next = value[key];
|
|
460
|
+
if (next) {
|
|
461
|
+
this.setAttribute(attribute, next);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
this.removeAttribute(attribute);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
get exportable() {
|
|
469
|
+
return this.hasAttribute("exportable");
|
|
470
|
+
}
|
|
471
|
+
set exportable(value) {
|
|
472
|
+
this.toggleAttribute("exportable", value);
|
|
473
|
+
}
|
|
474
|
+
get referenceTime() {
|
|
475
|
+
return this.getAttribute("reference-time") ?? "";
|
|
476
|
+
}
|
|
477
|
+
set referenceTime(value) {
|
|
478
|
+
this.setAttribute("reference-time", value);
|
|
479
|
+
}
|
|
480
|
+
/** The events currently on screen: the full set narrowed by the facets. */
|
|
481
|
+
get visibleEvents() {
|
|
482
|
+
return filterAuditEvents(this.events, this.facets);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The sections the current facets and grouping produce.
|
|
486
|
+
*
|
|
487
|
+
* Public because windowing takes away the alternative: a host used to be
|
|
488
|
+
* able to read the sections off the DOM, and once only a slice is rendered,
|
|
489
|
+
* that stops being the whole picture.
|
|
490
|
+
*/
|
|
491
|
+
get groups() {
|
|
492
|
+
return this.currentGroups();
|
|
493
|
+
}
|
|
494
|
+
toggleGroup(key, expanded) {
|
|
495
|
+
const next = expanded ?? this.collapsedGroups.has(key);
|
|
496
|
+
if (next) {
|
|
497
|
+
this.collapsedGroups.delete(key);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
this.collapsedGroups.add(key);
|
|
501
|
+
}
|
|
502
|
+
this.dispatchEvent(new CustomEvent("group-toggled", {
|
|
503
|
+
bubbles: true,
|
|
504
|
+
composed: true,
|
|
505
|
+
detail: { key, expanded: next },
|
|
506
|
+
}));
|
|
507
|
+
if (this.isRendered) {
|
|
508
|
+
this.update();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
expandAll() {
|
|
512
|
+
this.collapsedGroups.clear();
|
|
513
|
+
if (this.isRendered) {
|
|
514
|
+
this.update();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
collapseAll() {
|
|
518
|
+
for (const group of this.currentGroups()) {
|
|
519
|
+
this.collapsedGroups.add(group.key);
|
|
520
|
+
}
|
|
521
|
+
if (this.isRendered) {
|
|
522
|
+
this.update();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Export what is on screen, not the whole set — an export that silently
|
|
527
|
+
* widens past the reader's filters is a compliance hazard. Returns the CSV
|
|
528
|
+
* and emits `export-requested` so the host owns delivery (download, mail,
|
|
529
|
+
* archive API).
|
|
530
|
+
*/
|
|
531
|
+
exportCsv() {
|
|
532
|
+
const events = this.visibleEvents;
|
|
533
|
+
const csv = toAuditCsv(events);
|
|
534
|
+
this.dispatchEvent(new CustomEvent("export-requested", {
|
|
535
|
+
bubbles: true,
|
|
536
|
+
composed: true,
|
|
537
|
+
detail: { format: "csv", csv, events },
|
|
538
|
+
}));
|
|
539
|
+
return csv;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Watch the scroller so a measurable — or resized — viewport re-plans.
|
|
543
|
+
*
|
|
544
|
+
* Separate from `setupListeners` because that runs *once*, on the element's
|
|
545
|
+
* first connection, while `disconnectedCallback` tears the observer down
|
|
546
|
+
* every time. A log removed and re-inserted would otherwise come back with
|
|
547
|
+
* its listeners intact and its observer gone, so resizing its container
|
|
548
|
+
* would silently stop re-planning. Idempotent, so both callers are safe.
|
|
549
|
+
*/
|
|
550
|
+
observeViewport() {
|
|
551
|
+
if (this.viewportObserver || typeof ResizeObserver === "undefined")
|
|
552
|
+
return;
|
|
553
|
+
this.viewportObserver = new ResizeObserver(() => {
|
|
554
|
+
if (!this.virtualize)
|
|
555
|
+
return;
|
|
556
|
+
const next = this.currentPlan().window;
|
|
557
|
+
if (this.lastWindow && sameRowWindow(this.lastWindow, next))
|
|
558
|
+
return;
|
|
559
|
+
this.update();
|
|
560
|
+
});
|
|
561
|
+
this.viewportObserver.observe(this.groupsEl);
|
|
562
|
+
}
|
|
563
|
+
connectedCallback() {
|
|
564
|
+
super.connectedCallback();
|
|
565
|
+
// Re-arm after a disconnect; no-op on the first connection, where
|
|
566
|
+
// setupListeners has already done it.
|
|
567
|
+
this.observeViewport();
|
|
568
|
+
}
|
|
569
|
+
disconnectedCallback() {
|
|
570
|
+
// The observer holds a reference to the scroller and the pending frames
|
|
571
|
+
// hold one to the element; a log removed mid-scroll should not keep either
|
|
572
|
+
// alive, or re-render into a shadow root nobody is looking at.
|
|
573
|
+
this.viewportObserver?.disconnect();
|
|
574
|
+
this.viewportObserver = null;
|
|
575
|
+
if (this.scrollFrame)
|
|
576
|
+
cancelAnimationFrame(this.scrollFrame);
|
|
577
|
+
if (this.measureFrame)
|
|
578
|
+
cancelAnimationFrame(this.measureFrame);
|
|
579
|
+
this.scrollFrame = 0;
|
|
580
|
+
this.measureFrame = 0;
|
|
581
|
+
super.disconnectedCallback?.();
|
|
582
|
+
}
|
|
583
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
584
|
+
// A different grouping dimension mints different section keys, so the
|
|
585
|
+
// collapse state from the old dimension cannot be carried over.
|
|
586
|
+
if (name === "group-by" && oldValue !== newValue) {
|
|
587
|
+
this.collapsedGroups.clear();
|
|
588
|
+
}
|
|
589
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Render only the sections near the viewport. Opt-in for the same reason
|
|
593
|
+
* `box-table`'s is: a windowed surface needs a bounded height to scroll
|
|
594
|
+
* within, and silently windowing an unbounded one would render nothing.
|
|
595
|
+
*
|
|
596
|
+
* Unlike the table, this *can* window a grouped, variable-height surface —
|
|
597
|
+
* it plans over a cumulative offset index rather than a row count times a
|
|
598
|
+
* row height. Grouping is the whole point of an audit log, so refusing to
|
|
599
|
+
* window it would mean refusing the only shape it comes in.
|
|
600
|
+
*/
|
|
601
|
+
get virtualize() {
|
|
602
|
+
return this.hasAttribute("virtualize");
|
|
603
|
+
}
|
|
604
|
+
set virtualize(value) {
|
|
605
|
+
this.toggleAttribute("virtualize", Boolean(value));
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Starting height estimates, in pixels — not contracts. The real heights
|
|
609
|
+
* depend on density, font, and how much an event's summary wraps; after the
|
|
610
|
+
* first paint the element measures one of each and uses that instead.
|
|
611
|
+
*/
|
|
612
|
+
get rowHeight() {
|
|
613
|
+
return this.measuredHeights?.event ?? this.declaredHeight("row-height", 76);
|
|
614
|
+
}
|
|
615
|
+
get headingHeight() {
|
|
616
|
+
return this.measuredHeights?.heading ?? this.declaredHeight("heading-height", 40);
|
|
617
|
+
}
|
|
618
|
+
set rowHeight(value) {
|
|
619
|
+
this.measuredHeights = null;
|
|
620
|
+
this.setAttribute("row-height", String(value));
|
|
621
|
+
}
|
|
622
|
+
set headingHeight(value) {
|
|
623
|
+
this.measuredHeights = null;
|
|
624
|
+
this.setAttribute("heading-height", String(value));
|
|
625
|
+
}
|
|
626
|
+
declaredHeight(attribute, fallback) {
|
|
627
|
+
const raw = Number(this.getAttribute(attribute));
|
|
628
|
+
return Number.isFinite(raw) && raw > 0 ? raw : fallback;
|
|
629
|
+
}
|
|
630
|
+
/** The window the last render used, or null when it rendered every section. */
|
|
631
|
+
get renderedWindow() {
|
|
632
|
+
return this.virtualize ? this.lastWindow : null;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Adopt the real rendered heights, once, when they differ from the ones the
|
|
636
|
+
* last plan used. Re-planning with the measured heights settles immediately —
|
|
637
|
+
* the next measurement matches — so this cannot loop.
|
|
638
|
+
*/
|
|
639
|
+
/** Mean rendered height of the elements matching a part, or 0 if none. */
|
|
640
|
+
meanHeight(part) {
|
|
641
|
+
const nodes = Array.from(this.groupsEl.querySelectorAll(`[part="${part}"]`));
|
|
642
|
+
if (nodes.length === 0)
|
|
643
|
+
return 0;
|
|
644
|
+
const total = nodes.reduce((sum, node) => sum + node.getBoundingClientRect().height, 0);
|
|
645
|
+
return total / nodes.length;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Adopt the real rendered heights when they differ from the ones in use.
|
|
649
|
+
*
|
|
650
|
+
* Two things here exist because the obvious version ran away. Sampling *one*
|
|
651
|
+
* event row and adopting its height looks fine and is not: event rows are not
|
|
652
|
+
* uniform — `summary`, `evidence` and `correlationId` are each optional — so
|
|
653
|
+
* adopting the first rendered row's height moves the window, which renders a
|
|
654
|
+
* different first row, which measures differently, which moves the window.
|
|
655
|
+
* Measured in Chromium with every third event several lines taller: **55 full
|
|
656
|
+
* rebuilds in 0.9 seconds while nothing was scrolling**, the sampled height
|
|
657
|
+
* flipping between 33px and 166.5px.
|
|
658
|
+
*
|
|
659
|
+
* So: average across every rendered row rather than sampling one, which makes
|
|
660
|
+
* the estimate both more representative and far less sensitive to which rows
|
|
661
|
+
* happen to be on screen. And bound the adoptions per row set anyway, because
|
|
662
|
+
* "the average is stable" is an argument about typical data, and this is the
|
|
663
|
+
* code path where being wrong costs a frame-rate collapse rather than a
|
|
664
|
+
* slightly wrong scrollbar.
|
|
665
|
+
*/
|
|
666
|
+
measureRowHeights() {
|
|
667
|
+
if (!this.virtualize || this.measureFrame)
|
|
668
|
+
return;
|
|
669
|
+
const signature = this.heightSignature();
|
|
670
|
+
if (signature !== this.adoptedFor) {
|
|
671
|
+
this.adoptedFor = signature;
|
|
672
|
+
this.adoptionCount = 0;
|
|
673
|
+
}
|
|
674
|
+
if (this.adoptionCount >= MAX_HEIGHT_ADOPTIONS)
|
|
675
|
+
return;
|
|
676
|
+
const headingHeight = this.meanHeight("group-heading");
|
|
677
|
+
const eventHeight = this.meanHeight("event");
|
|
678
|
+
if (headingHeight <= 0 || eventHeight <= 0)
|
|
679
|
+
return;
|
|
680
|
+
if (Math.abs(headingHeight - this.headingHeight) < 1 &&
|
|
681
|
+
Math.abs(eventHeight - this.rowHeight) < 1) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.adoptionCount++;
|
|
685
|
+
this.measuredHeights = { heading: headingHeight, event: eventHeight };
|
|
686
|
+
this.measureFrame = requestAnimationFrame(() => {
|
|
687
|
+
this.measureFrame = 0;
|
|
688
|
+
this.update();
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* What counts as "the same row set" for the adoption budget: change the data,
|
|
693
|
+
* the grouping, the filters or the collapse state and the rows genuinely
|
|
694
|
+
* differ, so measuring again is warranted.
|
|
695
|
+
*/
|
|
696
|
+
heightSignature() {
|
|
697
|
+
return JSON.stringify([
|
|
698
|
+
this.getAttribute("events") ?? "",
|
|
699
|
+
this.groupBy,
|
|
700
|
+
this.facets,
|
|
701
|
+
[...this.collapsedGroups].sort(),
|
|
702
|
+
]);
|
|
703
|
+
}
|
|
704
|
+
currentPlan() {
|
|
705
|
+
const { rows, index } = this.cachedRows();
|
|
706
|
+
return planAuditWindow(rows, { heading: this.headingHeight, event: this.rowHeight }, {
|
|
707
|
+
viewportHeight: this.groupsEl.clientHeight,
|
|
708
|
+
scrollTop: this.groupsEl.scrollTop,
|
|
709
|
+
contentHeight: this.groupsEl.scrollHeight,
|
|
710
|
+
}, index);
|
|
711
|
+
}
|
|
712
|
+
now() {
|
|
713
|
+
return resolveReferenceTime(this.getAttribute("reference-time"));
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Grouping, flattening and indexing for the current data, computed once and
|
|
717
|
+
* reused until something that affects them changes.
|
|
718
|
+
*
|
|
719
|
+
* The scroll path asks "did the window move?" on every frame that is not
|
|
720
|
+
* skipped, and answering it used to re-parse the whole `events` attribute,
|
|
721
|
+
* re-filter, re-group, re-flatten and rebuild the offset array — then
|
|
722
|
+
* `update()` did all of it again. That is O(n) twice per frame on a surface
|
|
723
|
+
* whose entire purpose is not being O(n) at scale.
|
|
724
|
+
*/
|
|
725
|
+
rowCache = null;
|
|
726
|
+
cachedRows() {
|
|
727
|
+
const heights = { heading: this.headingHeight, event: this.rowHeight };
|
|
728
|
+
const signature = JSON.stringify([
|
|
729
|
+
this.getAttribute("events") ?? "",
|
|
730
|
+
this.groupBy,
|
|
731
|
+
this.facets,
|
|
732
|
+
this.referenceTime,
|
|
733
|
+
[...this.collapsedGroups].sort(),
|
|
734
|
+
heights,
|
|
735
|
+
]);
|
|
736
|
+
if (this.rowCache?.signature === signature)
|
|
737
|
+
return this.rowCache;
|
|
738
|
+
const groups = groupAuditEvents(this.visibleEvents, this.groupBy, this.now());
|
|
739
|
+
const rows = flattenAuditRows(groups, this.collapsedGroups);
|
|
740
|
+
this.rowCache = { signature, groups, rows, index: buildAuditIndex(rows, heights) };
|
|
741
|
+
return this.rowCache;
|
|
742
|
+
}
|
|
743
|
+
currentGroups() {
|
|
744
|
+
return this.cachedRows().groups;
|
|
745
|
+
}
|
|
746
|
+
patchFacets(patch) {
|
|
747
|
+
this.facets = { ...this.facets, ...patch };
|
|
748
|
+
this.dispatchEvent(new CustomEvent("facets-changed", {
|
|
749
|
+
bubbles: true,
|
|
750
|
+
composed: true,
|
|
751
|
+
detail: { facets: this.facets },
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
eventHtml(event) {
|
|
755
|
+
const tone = resolveTimelineTone(event.tone);
|
|
756
|
+
const timestamp = formatAuditTimestamp(event.timestamp);
|
|
757
|
+
const evidence = (event.evidence ?? [])
|
|
758
|
+
.map(entry => {
|
|
759
|
+
const attrs = `part="evidence-link" data-event-id="${escapeHtml(event.id)}" data-evidence-id="${escapeHtml(entry.id)}"`;
|
|
760
|
+
// An unsafe scheme renders as a button: the chip still works as an
|
|
761
|
+
// intent, it just never becomes a navigable link.
|
|
762
|
+
return entry.href && isSafeHref(entry.href)
|
|
763
|
+
? `<a ${attrs} href="${escapeHtml(entry.href)}">${escapeHtml(entry.label)}</a>`
|
|
764
|
+
: `<button type="button" ${attrs}>${escapeHtml(entry.label)}</button>`;
|
|
765
|
+
})
|
|
766
|
+
.join("");
|
|
767
|
+
return `
|
|
768
|
+
<li part="event" data-event-id="${escapeHtml(event.id)}" data-tone="${escapeHtml(tone)}" style="--audit-tone:${auditToneColor(tone)};">
|
|
769
|
+
<div part="event-topline">
|
|
770
|
+
<button type="button" part="event-action" data-event-id="${escapeHtml(event.id)}">${escapeHtml(event.action)}</button>
|
|
771
|
+
${event.actor ? `<span part="actor">${escapeHtml(event.actor.name)}</span>` : ""}
|
|
772
|
+
${event.badge ? `<span part="badge">${escapeHtml(event.badge)}</span>` : ""}
|
|
773
|
+
${timestamp ? `<time part="timestamp" datetime="${escapeHtml(event.timestamp ?? "")}">${escapeHtml(timestamp)}</time>` : ""}
|
|
774
|
+
</div>
|
|
775
|
+
${event.summary ? `<p part="event-summary">${escapeHtml(event.summary)}</p>` : ""}
|
|
776
|
+
${evidence ? `<div part="evidence">${evidence}</div>` : ""}
|
|
777
|
+
${event.correlationId
|
|
778
|
+
? `<button type="button" part="correlation" data-correlation-id="${escapeHtml(event.correlationId)}" aria-label="Show only workflow run ${escapeHtml(event.correlationId)}">${escapeHtml(event.correlationId)}</button>`
|
|
779
|
+
: ""}
|
|
780
|
+
</li>
|
|
781
|
+
`;
|
|
782
|
+
}
|
|
783
|
+
groupHtml(group, index, slice) {
|
|
784
|
+
const toggleId = `audit-group-toggle-${String(index)}`;
|
|
785
|
+
const bodyId = `audit-group-body-${String(index)}`;
|
|
786
|
+
// Windowed: only the events inside the window. The heading always renders
|
|
787
|
+
// for an intersecting section — [part="group-body"] is aria-labelledby the
|
|
788
|
+
// toggle inside it, so a section without its heading is both unlabelled and
|
|
789
|
+
// impossible to collapse.
|
|
790
|
+
const events = slice ? group.events.slice(slice.start, slice.end) : group.events;
|
|
791
|
+
const actors = group.actorCount
|
|
792
|
+
? ` · ${String(group.actorCount)} ${group.actorCount === 1 ? "actor" : "actors"}`
|
|
793
|
+
: "";
|
|
794
|
+
return `
|
|
795
|
+
<section part="group" data-group-key="${escapeHtml(group.key)}">
|
|
796
|
+
<h3 part="group-heading">
|
|
797
|
+
<button type="button" part="group-toggle" id="${toggleId}" aria-controls="${bodyId}" aria-expanded="true" data-group-key="${escapeHtml(group.key)}">
|
|
798
|
+
<span part="group-label">${escapeHtml(group.label)}</span>
|
|
799
|
+
<span part="group-count">${String(group.count)} ${group.count === 1 ? "event" : "events"}${actors}</span>
|
|
800
|
+
</button>
|
|
801
|
+
</h3>
|
|
802
|
+
<div part="group-body" id="${bodyId}" role="region" aria-labelledby="${toggleId}">
|
|
803
|
+
<ol part="events" role="list"${slice && slice.start > 0 ? ` start="${String(slice.start + 1)}"` : ""}>${events.map(event => this.eventHtml(event)).join("")}</ol>
|
|
804
|
+
</div>
|
|
805
|
+
</section>
|
|
806
|
+
`;
|
|
807
|
+
}
|
|
808
|
+
renderOptions(select, values, allLabel) {
|
|
809
|
+
select.innerHTML = [
|
|
810
|
+
`<option value="">${escapeHtml(allLabel)}</option>`,
|
|
811
|
+
// The empty value is the all-values option, so unattributed events are
|
|
812
|
+
// reachable by grouping rather than by faceting.
|
|
813
|
+
...values
|
|
814
|
+
.filter(entry => entry.value !== "")
|
|
815
|
+
.map(entry => `<option value="${escapeHtml(entry.value)}">${escapeHtml(entry.label)} (${String(entry.count)})</option>`),
|
|
816
|
+
].join("");
|
|
817
|
+
}
|
|
818
|
+
renderTemplate() {
|
|
819
|
+
if (!this.shadowRoot) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
this.shadowRoot.innerHTML = `
|
|
823
|
+
<style>${elementStyles}</style>
|
|
824
|
+
<section part="panel" aria-labelledby="audit-title">
|
|
825
|
+
<h2 part="title" id="audit-title"></h2>
|
|
826
|
+
<div part="toolbar" role="group" aria-label="Audit filters">
|
|
827
|
+
<div part="group-by" role="group" aria-label="Group by">
|
|
828
|
+
${AUDIT_GROUP_BY_VALUES.map(value => `<button type="button" part="group-by-option" data-group-by="${value}" aria-pressed="false">${AUDIT_GROUP_BY_LABELS[value]}</button>`).join("")}
|
|
829
|
+
</div>
|
|
830
|
+
<label part="facet">Actor<select part="facet-actor"></select></label>
|
|
831
|
+
<label part="facet">Action<select part="facet-action"></select></label>
|
|
832
|
+
<label part="facet">From<input type="date" part="facet-from"></label>
|
|
833
|
+
<label part="facet">To<input type="date" part="facet-to"></label>
|
|
834
|
+
<button type="button" part="clear-filters" hidden>Clear filters</button>
|
|
835
|
+
<button type="button" part="export" hidden>Export CSV</button>
|
|
836
|
+
</div>
|
|
837
|
+
<div part="drilldown" hidden>
|
|
838
|
+
<span>Workflow run</span>
|
|
839
|
+
<span part="drilldown-id"></span>
|
|
840
|
+
<button type="button" part="drilldown-clear">Clear drill-down</button>
|
|
841
|
+
</div>
|
|
842
|
+
<p part="result-summary" role="status"></p>
|
|
843
|
+
<div part="groups"></div>
|
|
844
|
+
<div part="empty" hidden>No audit events match these filters.</div>
|
|
845
|
+
</section>
|
|
846
|
+
`;
|
|
847
|
+
const root = this.shadowRoot;
|
|
848
|
+
this.titleEl = root.querySelector('[part="title"]');
|
|
849
|
+
this.toolbarEl = root.querySelector('[part="toolbar"]');
|
|
850
|
+
this.groupByEl = root.querySelector('[part="group-by"]');
|
|
851
|
+
this.actorSelectEl = root.querySelector('[part="facet-actor"]');
|
|
852
|
+
this.actionSelectEl = root.querySelector('[part="facet-action"]');
|
|
853
|
+
this.fromInputEl = root.querySelector('[part="facet-from"]');
|
|
854
|
+
this.toInputEl = root.querySelector('[part="facet-to"]');
|
|
855
|
+
this.clearFiltersEl = root.querySelector('[part="clear-filters"]');
|
|
856
|
+
this.exportEl = root.querySelector('[part="export"]');
|
|
857
|
+
this.drilldownEl = root.querySelector('[part="drilldown"]');
|
|
858
|
+
this.drilldownIdEl = root.querySelector('[part="drilldown-id"]');
|
|
859
|
+
this.summaryEl = root.querySelector('[part="result-summary"]');
|
|
860
|
+
this.groupsEl = root.querySelector('[part="groups"]');
|
|
861
|
+
this.emptyEl = root.querySelector('[part="empty"]');
|
|
862
|
+
}
|
|
863
|
+
setupListeners() {
|
|
864
|
+
// A windowed surface cannot bootstrap itself from an unmeasured viewport:
|
|
865
|
+
// an empty scroller is zero pixels tall, a zero-height viewport plans an
|
|
866
|
+
// empty window, and an empty window renders nothing to measure. Left alone
|
|
867
|
+
// the log stays blank until something else happens to trigger a render.
|
|
868
|
+
// The observer breaks that circle on the first layout, and keeps earning
|
|
869
|
+
// its place afterwards — a resized container changes how much fits.
|
|
870
|
+
this.observeViewport();
|
|
871
|
+
// Scroll fires continuously; coalesce to one re-render per frame, and skip
|
|
872
|
+
// entirely while the resolved slice is unchanged — most scroll events move
|
|
873
|
+
// the viewport within the sections already rendered.
|
|
874
|
+
this.groupsEl.addEventListener("scroll", () => {
|
|
875
|
+
if (!this.virtualize || this.scrollFrame)
|
|
876
|
+
return;
|
|
877
|
+
this.scrollFrame = requestAnimationFrame(() => {
|
|
878
|
+
this.scrollFrame = 0;
|
|
879
|
+
const next = this.currentPlan().window;
|
|
880
|
+
if (this.lastWindow && sameRowWindow(this.lastWindow, next))
|
|
881
|
+
return;
|
|
882
|
+
this.update();
|
|
883
|
+
});
|
|
884
|
+
}, { passive: true });
|
|
885
|
+
// The toolbar is built once and patched in place, so a select stays open
|
|
886
|
+
// and a half-typed date survives every re-render of the sections below.
|
|
887
|
+
this.toolbarEl.addEventListener("click", event => {
|
|
888
|
+
const target = event.target;
|
|
889
|
+
const option = target.closest('[part="group-by-option"]');
|
|
890
|
+
if (option && this.toolbarEl.contains(option)) {
|
|
891
|
+
const next = resolveAuditGroupBy(option.getAttribute("data-group-by"));
|
|
892
|
+
if (next !== this.groupBy) {
|
|
893
|
+
this.groupBy = next;
|
|
894
|
+
this.dispatchEvent(new CustomEvent("group-by-changed", {
|
|
895
|
+
bubbles: true,
|
|
896
|
+
composed: true,
|
|
897
|
+
detail: { groupBy: next },
|
|
898
|
+
}));
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (target.closest('[part="clear-filters"]')) {
|
|
903
|
+
this.patchFacets({
|
|
904
|
+
actor: undefined,
|
|
905
|
+
action: undefined,
|
|
906
|
+
correlationId: undefined,
|
|
907
|
+
from: undefined,
|
|
908
|
+
to: undefined,
|
|
909
|
+
});
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (target.closest('[part="export"]')) {
|
|
913
|
+
this.exportCsv();
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
this.toolbarEl.addEventListener("change", event => {
|
|
917
|
+
const target = event.target;
|
|
918
|
+
if (target === this.actorSelectEl) {
|
|
919
|
+
this.patchFacets({ actor: this.actorSelectEl.value || undefined });
|
|
920
|
+
}
|
|
921
|
+
else if (target === this.actionSelectEl) {
|
|
922
|
+
this.patchFacets({ action: this.actionSelectEl.value || undefined });
|
|
923
|
+
}
|
|
924
|
+
else if (target === this.fromInputEl) {
|
|
925
|
+
this.patchFacets({ from: this.fromInputEl.value || undefined });
|
|
926
|
+
}
|
|
927
|
+
else if (target === this.toInputEl) {
|
|
928
|
+
this.patchFacets({ to: this.toInputEl.value || undefined });
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
this.drilldownEl.addEventListener("click", event => {
|
|
932
|
+
if (event.target.closest('[part="drilldown-clear"]')) {
|
|
933
|
+
this.patchFacets({ correlationId: undefined });
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
this.groupsEl.addEventListener("click", event => {
|
|
937
|
+
const target = event.target;
|
|
938
|
+
const toggle = target.closest('[part="group-toggle"]');
|
|
939
|
+
if (toggle && this.groupsEl.contains(toggle)) {
|
|
940
|
+
this.toggleGroup(toggle.getAttribute("data-group-key") ?? "");
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const correlation = target.closest('[part="correlation"]');
|
|
944
|
+
if (correlation && this.groupsEl.contains(correlation)) {
|
|
945
|
+
const correlationId = correlation.getAttribute("data-correlation-id") ?? "";
|
|
946
|
+
this.patchFacets({ correlationId });
|
|
947
|
+
this.dispatchEvent(new CustomEvent("correlation-selected", {
|
|
948
|
+
bubbles: true,
|
|
949
|
+
composed: true,
|
|
950
|
+
detail: {
|
|
951
|
+
correlationId,
|
|
952
|
+
events: filterAuditEvents(this.events, { correlationId }),
|
|
953
|
+
},
|
|
954
|
+
}));
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const evidenceLink = target.closest('[part="evidence-link"]');
|
|
958
|
+
if (evidenceLink && this.groupsEl.contains(evidenceLink)) {
|
|
959
|
+
const source = this.events.find(entry => entry.id === (evidenceLink.getAttribute("data-event-id") ?? ""));
|
|
960
|
+
const evidence = source?.evidence?.find(entry => entry.id === (evidenceLink.getAttribute("data-evidence-id") ?? ""));
|
|
961
|
+
if (evidence) {
|
|
962
|
+
this.dispatchEvent(new CustomEvent("evidence-selected", {
|
|
963
|
+
bubbles: true,
|
|
964
|
+
composed: true,
|
|
965
|
+
detail: { event: source, evidence },
|
|
966
|
+
}));
|
|
967
|
+
}
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const action = target.closest('[part="event-action"]');
|
|
971
|
+
if (action && this.groupsEl.contains(action)) {
|
|
972
|
+
const source = this.events.find(entry => entry.id === (action.getAttribute("data-event-id") ?? ""));
|
|
973
|
+
if (source) {
|
|
974
|
+
this.dispatchEvent(new CustomEvent("event-selected", {
|
|
975
|
+
bubbles: true,
|
|
976
|
+
composed: true,
|
|
977
|
+
detail: { event: source },
|
|
978
|
+
}));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
update() {
|
|
984
|
+
if (!this.groupsEl) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
this.titleEl.textContent = this.heading;
|
|
988
|
+
const all = this.events;
|
|
989
|
+
const facets = this.facets;
|
|
990
|
+
const visible = filterAuditEvents(all, facets);
|
|
991
|
+
const groupBy = this.groupBy;
|
|
992
|
+
const groups = groupAuditEvents(visible, groupBy, this.now());
|
|
993
|
+
for (const option of Array.from(this.groupByEl.querySelectorAll('[part="group-by-option"]'))) {
|
|
994
|
+
option.setAttribute("aria-pressed", option.getAttribute("data-group-by") === groupBy ? "true" : "false");
|
|
995
|
+
}
|
|
996
|
+
// Facet options come from the unfiltered set, so choosing one facet never
|
|
997
|
+
// empties another's list.
|
|
998
|
+
const summary = summarizeAuditFacets(all);
|
|
999
|
+
const optionsSignature = JSON.stringify(summary);
|
|
1000
|
+
if (optionsSignature !== this.optionsSignature) {
|
|
1001
|
+
this.optionsSignature = optionsSignature;
|
|
1002
|
+
this.renderOptions(this.actorSelectEl, summary.actors, "All actors");
|
|
1003
|
+
this.renderOptions(this.actionSelectEl, summary.actions, "All actions");
|
|
1004
|
+
}
|
|
1005
|
+
this.actorSelectEl.value = facets.actor ?? "";
|
|
1006
|
+
this.actionSelectEl.value = facets.action ?? "";
|
|
1007
|
+
this.fromInputEl.value = facets.from ?? "";
|
|
1008
|
+
this.toInputEl.value = facets.to ?? "";
|
|
1009
|
+
this.clearFiltersEl.hidden = !hasAuditFacets(facets);
|
|
1010
|
+
this.exportEl.hidden = !this.exportable;
|
|
1011
|
+
this.drilldownEl.hidden = !facets.correlationId;
|
|
1012
|
+
this.drilldownIdEl.textContent = facets.correlationId ?? "";
|
|
1013
|
+
this.summaryEl.textContent =
|
|
1014
|
+
visible.length === all.length
|
|
1015
|
+
? `${String(all.length)} ${all.length === 1 ? "event" : "events"} in ${String(groups.length)} ${groups.length === 1 ? "group" : "groups"}`
|
|
1016
|
+
: `Showing ${String(visible.length)} of ${String(all.length)} events in ${String(groups.length)} ${groups.length === 1 ? "group" : "groups"}`;
|
|
1017
|
+
// The section markup depends on content only. Collapse state is applied
|
|
1018
|
+
// separately below, so expanding a section never rebuilds the log.
|
|
1019
|
+
// Windowed renders depend on the scroll position too, so the resolved
|
|
1020
|
+
// slice joins the signature — otherwise scrolling would keep the sections
|
|
1021
|
+
// that were already on screen and never draw the ones scrolled into view.
|
|
1022
|
+
const plan = this.virtualize ? this.currentPlan() : null;
|
|
1023
|
+
this.lastWindow = plan?.window ?? null;
|
|
1024
|
+
const contentSignature = JSON.stringify([
|
|
1025
|
+
this.getAttribute("events") ?? "",
|
|
1026
|
+
groupBy,
|
|
1027
|
+
facets,
|
|
1028
|
+
this.referenceTime,
|
|
1029
|
+
// The whole rendered geometry, not just the slice: collapsing a section
|
|
1030
|
+
// *below* the window leaves the slice and the top spacer untouched while
|
|
1031
|
+
// making the log shorter, and a stale bottom spacer outlives the content
|
|
1032
|
+
// it described.
|
|
1033
|
+
plan
|
|
1034
|
+
? [plan.window.startIndex, plan.window.endIndex, plan.paddingTop, plan.paddingBottom]
|
|
1035
|
+
: null,
|
|
1036
|
+
]);
|
|
1037
|
+
if (contentSignature !== this.contentSignature) {
|
|
1038
|
+
this.contentSignature = contentSignature;
|
|
1039
|
+
const active = this.shadowRoot?.activeElement;
|
|
1040
|
+
const focusKey = active && this.groupsEl.contains(active)
|
|
1041
|
+
? {
|
|
1042
|
+
part: active.getAttribute("part") ?? "",
|
|
1043
|
+
groupKey: active.getAttribute("data-group-key"),
|
|
1044
|
+
eventId: active.getAttribute("data-event-id"),
|
|
1045
|
+
}
|
|
1046
|
+
: null;
|
|
1047
|
+
const spacer = (height, position) => height > 0
|
|
1048
|
+
? `<div part="spacer" data-position="${position}" aria-hidden="true" style="block-size:${String(height)}px;"></div>`
|
|
1049
|
+
: "";
|
|
1050
|
+
this.groupsEl.innerHTML = plan
|
|
1051
|
+
? spacer(plan.paddingTop, "top") +
|
|
1052
|
+
plan.groups
|
|
1053
|
+
.map(planned => this.groupHtml(groups[planned.groupIndex], planned.groupIndex, {
|
|
1054
|
+
start: planned.eventStart,
|
|
1055
|
+
end: planned.eventEnd,
|
|
1056
|
+
}))
|
|
1057
|
+
.join("") +
|
|
1058
|
+
spacer(plan.paddingBottom, "bottom")
|
|
1059
|
+
: groups.map((group, index) => this.groupHtml(group, index)).join("");
|
|
1060
|
+
if (focusKey?.part) {
|
|
1061
|
+
const restored = Array.from(this.groupsEl.querySelectorAll(`[part="${focusKey.part}"]`)).find(node => node.getAttribute("data-group-key") === focusKey.groupKey &&
|
|
1062
|
+
node.getAttribute("data-event-id") === focusKey.eventId);
|
|
1063
|
+
restored?.focus();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
for (const section of Array.from(this.groupsEl.querySelectorAll('[part="group"]'))) {
|
|
1067
|
+
const key = section.getAttribute("data-group-key") ?? "";
|
|
1068
|
+
const expanded = !this.collapsedGroups.has(key);
|
|
1069
|
+
section.querySelector('[part="group-toggle"]')?.setAttribute("aria-expanded", String(expanded));
|
|
1070
|
+
const body = section.querySelector('[part="group-body"]');
|
|
1071
|
+
if (body) {
|
|
1072
|
+
body.hidden = !expanded;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
this.groupsEl.hidden = groups.length === 0;
|
|
1076
|
+
this.emptyEl.hidden = groups.length > 0;
|
|
1077
|
+
if (this.virtualize) {
|
|
1078
|
+
this.measureRowHeights();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
AuditLog.register();
|