@everscribe/components-element 0.1.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/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/index.cjs +2255 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +2253 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2253 @@
|
|
|
1
|
+
import { fetchTokenViaOpts, parseClaims, createEventsStore, createDistinctValuesStore, RELATIVE_TIME_TICK_MS, formatTimeCell, EmbedError, ALL_COLUMNS, generateNLPFilters, COLUMN_LABELS, exportEvents, hasParseableDiff, renderDiff } from '@everscribe/components-core';
|
|
2
|
+
|
|
3
|
+
// src/AuditTrailElement.ts
|
|
4
|
+
|
|
5
|
+
// src/dom.ts
|
|
6
|
+
function h(tag, attrs, ...children) {
|
|
7
|
+
const el = document.createElement(tag);
|
|
8
|
+
if (attrs) {
|
|
9
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
10
|
+
if (v == null || v === false) continue;
|
|
11
|
+
if (k === "class") el.className = String(v);
|
|
12
|
+
else if (k === "html") el.innerHTML = String(v);
|
|
13
|
+
else el.setAttribute(k, String(v));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const c of children.flat()) {
|
|
17
|
+
if (c == null || c === false) continue;
|
|
18
|
+
el.appendChild(typeof c === "object" ? c : document.createTextNode(String(c)));
|
|
19
|
+
}
|
|
20
|
+
return el;
|
|
21
|
+
}
|
|
22
|
+
var COPY_ICON_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
|
23
|
+
var CHECK_ICON_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
|
24
|
+
function openEventDetail(opts) {
|
|
25
|
+
const { event, theme, onClose } = opts;
|
|
26
|
+
let tab = "raw";
|
|
27
|
+
let copied = false;
|
|
28
|
+
const showDiff = hasParseableDiff(event.change);
|
|
29
|
+
const diff = showDiff ? renderDiff(event.change) : { lines: [] };
|
|
30
|
+
const rawJson = JSON.stringify(event, null, 2);
|
|
31
|
+
const highlightedJson = highlightJSON(rawJson);
|
|
32
|
+
const metadataRows = buildMetadataRows(event.metadata);
|
|
33
|
+
const showMetadata = metadataRows.length > 0;
|
|
34
|
+
const portal = h("div", { class: `audit-trail-portal audit-trail-theme-${theme}` });
|
|
35
|
+
const closeBtn = h(
|
|
36
|
+
"button",
|
|
37
|
+
{
|
|
38
|
+
type: "button",
|
|
39
|
+
class: "audit-trail-inspect-close",
|
|
40
|
+
"aria-label": "Close"
|
|
41
|
+
},
|
|
42
|
+
"\xD7"
|
|
43
|
+
);
|
|
44
|
+
const panelContainer = h("div");
|
|
45
|
+
const renderPanel = () => {
|
|
46
|
+
panelContainer.replaceChildren(buildPanel());
|
|
47
|
+
};
|
|
48
|
+
const buildPanel = () => {
|
|
49
|
+
if (tab === "diff" && showDiff) {
|
|
50
|
+
return h(
|
|
51
|
+
"div",
|
|
52
|
+
{ class: "audit-trail-inspect-panel" },
|
|
53
|
+
renderDiffTable(diff.lines)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (tab === "metadata" && showMetadata) {
|
|
57
|
+
return h(
|
|
58
|
+
"div",
|
|
59
|
+
{ class: "audit-trail-inspect-panel" },
|
|
60
|
+
renderMetadataTable(metadataRows)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const codeBlock = h(
|
|
64
|
+
"pre",
|
|
65
|
+
{ class: "audit-trail-code-block" },
|
|
66
|
+
h("code", { html: highlightedJson })
|
|
67
|
+
);
|
|
68
|
+
const copyBtn = h(
|
|
69
|
+
"button",
|
|
70
|
+
{
|
|
71
|
+
type: "button",
|
|
72
|
+
class: "audit-trail-copy-button",
|
|
73
|
+
"aria-label": copied ? "Copied" : "Copy to clipboard",
|
|
74
|
+
title: copied ? "Copied" : "Copy to clipboard",
|
|
75
|
+
html: copied ? CHECK_ICON_SVG : COPY_ICON_SVG
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
copyBtn.addEventListener("click", () => {
|
|
79
|
+
void (async () => {
|
|
80
|
+
try {
|
|
81
|
+
await navigator.clipboard.writeText(rawJson);
|
|
82
|
+
copied = true;
|
|
83
|
+
renderPanel();
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
copied = false;
|
|
86
|
+
if (!disposed && tab === "raw") renderPanel();
|
|
87
|
+
}, 1500);
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
});
|
|
92
|
+
return h(
|
|
93
|
+
"div",
|
|
94
|
+
{ class: "audit-trail-inspect-panel" },
|
|
95
|
+
h(
|
|
96
|
+
"div",
|
|
97
|
+
{ class: "audit-trail-code-block-wrap" },
|
|
98
|
+
codeBlock,
|
|
99
|
+
copyBtn
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
const subtitle = h(
|
|
104
|
+
"p",
|
|
105
|
+
{ class: "audit-trail-inspect-subtitle" },
|
|
106
|
+
h(
|
|
107
|
+
"span",
|
|
108
|
+
{ class: "audit-trail-inspect-subtitle-line" },
|
|
109
|
+
h("code", null, event.action || "-")
|
|
110
|
+
),
|
|
111
|
+
h(
|
|
112
|
+
"span",
|
|
113
|
+
{ class: "audit-trail-inspect-subtitle-line" },
|
|
114
|
+
formatHeaderTimestamp(event.occurred_at)
|
|
115
|
+
),
|
|
116
|
+
h(
|
|
117
|
+
"span",
|
|
118
|
+
{ class: "audit-trail-inspect-subtitle-line" },
|
|
119
|
+
h("code", null, event.id)
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
const tabs = showDiff || showMetadata ? renderTabs(tab, showDiff, showMetadata, (next) => {
|
|
123
|
+
tab = next;
|
|
124
|
+
setSelectedTab(tabsEl, next);
|
|
125
|
+
renderPanel();
|
|
126
|
+
}) : null;
|
|
127
|
+
const tabsEl = tabs;
|
|
128
|
+
const modalChildren = [
|
|
129
|
+
closeBtn,
|
|
130
|
+
h(
|
|
131
|
+
"h2",
|
|
132
|
+
{ id: "audit-trail-inspect-title", class: "audit-trail-inspect-title" },
|
|
133
|
+
"Inspect Event"
|
|
134
|
+
),
|
|
135
|
+
subtitle
|
|
136
|
+
];
|
|
137
|
+
if (tabs) modalChildren.push(tabs);
|
|
138
|
+
modalChildren.push(panelContainer);
|
|
139
|
+
const modal = h(
|
|
140
|
+
"div",
|
|
141
|
+
{
|
|
142
|
+
class: "audit-trail-inspect-modal",
|
|
143
|
+
role: "dialog",
|
|
144
|
+
"aria-modal": "true",
|
|
145
|
+
"aria-labelledby": "audit-trail-inspect-title"
|
|
146
|
+
},
|
|
147
|
+
...modalChildren
|
|
148
|
+
);
|
|
149
|
+
modal.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
150
|
+
const backdrop = h("div", { class: "audit-trail-inspect-backdrop" }, modal);
|
|
151
|
+
portal.appendChild(backdrop);
|
|
152
|
+
let disposed = false;
|
|
153
|
+
const dispose = () => {
|
|
154
|
+
if (disposed) return;
|
|
155
|
+
disposed = true;
|
|
156
|
+
document.removeEventListener("keydown", onKey);
|
|
157
|
+
portal.remove();
|
|
158
|
+
};
|
|
159
|
+
const userClose = () => {
|
|
160
|
+
if (disposed) return;
|
|
161
|
+
dispose();
|
|
162
|
+
onClose();
|
|
163
|
+
};
|
|
164
|
+
const onKey = (e) => {
|
|
165
|
+
if (e.key === "Escape") userClose();
|
|
166
|
+
};
|
|
167
|
+
closeBtn.addEventListener("click", userClose);
|
|
168
|
+
backdrop.addEventListener("mousedown", userClose);
|
|
169
|
+
document.addEventListener("keydown", onKey);
|
|
170
|
+
document.body.appendChild(portal);
|
|
171
|
+
renderPanel();
|
|
172
|
+
queueMicrotask(() => {
|
|
173
|
+
if (!disposed) closeBtn.focus();
|
|
174
|
+
});
|
|
175
|
+
return dispose;
|
|
176
|
+
}
|
|
177
|
+
function renderTabs(initial, showDiff, showMetadata, onSelect) {
|
|
178
|
+
const make = (key, label) => {
|
|
179
|
+
const btn = h(
|
|
180
|
+
"button",
|
|
181
|
+
{
|
|
182
|
+
type: "button",
|
|
183
|
+
role: "tab",
|
|
184
|
+
class: initial === key ? "audit-trail-inspect-tab audit-trail-inspect-tab-active" : "audit-trail-inspect-tab",
|
|
185
|
+
"aria-selected": initial === key ? "true" : "false",
|
|
186
|
+
"data-tab": key
|
|
187
|
+
},
|
|
188
|
+
label
|
|
189
|
+
);
|
|
190
|
+
btn.addEventListener("click", () => onSelect(key));
|
|
191
|
+
return btn;
|
|
192
|
+
};
|
|
193
|
+
const tabs = [make("raw", "Raw")];
|
|
194
|
+
if (showDiff) tabs.push(make("diff", "Diff"));
|
|
195
|
+
if (showMetadata) tabs.push(make("metadata", "Metadata"));
|
|
196
|
+
return h(
|
|
197
|
+
"div",
|
|
198
|
+
{ class: "audit-trail-inspect-tabs", role: "tablist", "aria-label": "View" },
|
|
199
|
+
...tabs
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
function setSelectedTab(tabsEl, tab) {
|
|
203
|
+
if (!tabsEl) return;
|
|
204
|
+
for (const child of Array.from(tabsEl.children)) {
|
|
205
|
+
if (!(child instanceof HTMLElement)) continue;
|
|
206
|
+
const isMatch = child.dataset.tab === tab;
|
|
207
|
+
child.className = isMatch ? "audit-trail-inspect-tab audit-trail-inspect-tab-active" : "audit-trail-inspect-tab";
|
|
208
|
+
child.setAttribute("aria-selected", isMatch ? "true" : "false");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function renderDiffTable(lines) {
|
|
212
|
+
const rows = lines.map(
|
|
213
|
+
(line) => h(
|
|
214
|
+
"tr",
|
|
215
|
+
null,
|
|
216
|
+
h(
|
|
217
|
+
"td",
|
|
218
|
+
{
|
|
219
|
+
class: `audit-trail-diff-cell audit-trail-diff-before-${line.beforeKind || "blank"}`
|
|
220
|
+
},
|
|
221
|
+
h("pre", null, line.before)
|
|
222
|
+
),
|
|
223
|
+
h(
|
|
224
|
+
"td",
|
|
225
|
+
{
|
|
226
|
+
class: `audit-trail-diff-cell audit-trail-diff-after-${line.afterKind || "blank"}`
|
|
227
|
+
},
|
|
228
|
+
h("pre", null, line.after)
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
return h(
|
|
233
|
+
"div",
|
|
234
|
+
{ class: "audit-trail-diff-wrap" },
|
|
235
|
+
h(
|
|
236
|
+
"table",
|
|
237
|
+
{ class: "audit-trail-diff-table" },
|
|
238
|
+
h(
|
|
239
|
+
"thead",
|
|
240
|
+
null,
|
|
241
|
+
h("tr", null, h("th", null, "Before"), h("th", null, "After"))
|
|
242
|
+
),
|
|
243
|
+
h("tbody", null, ...rows)
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
function formatHeaderTimestamp(rfc3339) {
|
|
248
|
+
if (!rfc3339) return "-";
|
|
249
|
+
const d = new Date(rfc3339);
|
|
250
|
+
if (Number.isNaN(d.getTime())) return rfc3339;
|
|
251
|
+
const month = MONTHS[d.getUTCMonth()] ?? "";
|
|
252
|
+
const day = d.getUTCDate();
|
|
253
|
+
const year = d.getUTCFullYear();
|
|
254
|
+
const hh = String(d.getUTCHours()).padStart(2, "0");
|
|
255
|
+
const mm = String(d.getUTCMinutes()).padStart(2, "0");
|
|
256
|
+
const ss = String(d.getUTCSeconds()).padStart(2, "0");
|
|
257
|
+
const ms = String(d.getUTCMilliseconds()).padStart(3, "0");
|
|
258
|
+
return `${month} ${day}, ${year} ${hh}:${mm}:${ss}.${ms} UTC`;
|
|
259
|
+
}
|
|
260
|
+
var MONTHS = [
|
|
261
|
+
"Jan",
|
|
262
|
+
"Feb",
|
|
263
|
+
"Mar",
|
|
264
|
+
"Apr",
|
|
265
|
+
"May",
|
|
266
|
+
"Jun",
|
|
267
|
+
"Jul",
|
|
268
|
+
"Aug",
|
|
269
|
+
"Sep",
|
|
270
|
+
"Oct",
|
|
271
|
+
"Nov",
|
|
272
|
+
"Dec"
|
|
273
|
+
];
|
|
274
|
+
function highlightJSON(json) {
|
|
275
|
+
const safe = escapeHTML(json);
|
|
276
|
+
return safe.replace(
|
|
277
|
+
/("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
278
|
+
(match) => {
|
|
279
|
+
let cls = "audit-trail-json-num";
|
|
280
|
+
if (match.startsWith('"')) {
|
|
281
|
+
cls = /:$/.test(match) ? "audit-trail-json-key" : "audit-trail-json-str";
|
|
282
|
+
} else if (/true|false/.test(match)) {
|
|
283
|
+
cls = "audit-trail-json-bool";
|
|
284
|
+
} else if (/null/.test(match)) {
|
|
285
|
+
cls = "audit-trail-json-null";
|
|
286
|
+
}
|
|
287
|
+
return `<span class="${cls}">${match}</span>`;
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
function escapeHTML(s) {
|
|
292
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
293
|
+
}
|
|
294
|
+
function buildMetadataRows(metadata) {
|
|
295
|
+
if (!metadata) return [];
|
|
296
|
+
const keys = Object.keys(metadata);
|
|
297
|
+
if (keys.length === 0) return [];
|
|
298
|
+
keys.sort();
|
|
299
|
+
return keys.map((k) => {
|
|
300
|
+
const raw = metadata[k];
|
|
301
|
+
return { key: k, value: renderMetadataValue(raw), type: classifyValue(raw) };
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function renderMetadataValue(v) {
|
|
305
|
+
if (v === null || v === void 0) return "";
|
|
306
|
+
if (typeof v === "string") return v;
|
|
307
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
308
|
+
try {
|
|
309
|
+
return JSON.stringify(v);
|
|
310
|
+
} catch {
|
|
311
|
+
return String(v);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function classifyValue(v) {
|
|
315
|
+
if (v === null) return "null";
|
|
316
|
+
if (Array.isArray(v)) return "array";
|
|
317
|
+
if (typeof v === "object") return "object";
|
|
318
|
+
return typeof v;
|
|
319
|
+
}
|
|
320
|
+
function renderMetadataTable(rows) {
|
|
321
|
+
return h(
|
|
322
|
+
"table",
|
|
323
|
+
{ class: "audit-trail-metadata-kv-table" },
|
|
324
|
+
h(
|
|
325
|
+
"thead",
|
|
326
|
+
null,
|
|
327
|
+
h(
|
|
328
|
+
"tr",
|
|
329
|
+
null,
|
|
330
|
+
h("th", { class: "audit-trail-md-col-key" }, "Key"),
|
|
331
|
+
h("th", { class: "audit-trail-md-col-type" }, "Type"),
|
|
332
|
+
h("th", { class: "audit-trail-md-col-value" }, "Value")
|
|
333
|
+
)
|
|
334
|
+
),
|
|
335
|
+
h(
|
|
336
|
+
"tbody",
|
|
337
|
+
null,
|
|
338
|
+
...rows.map(
|
|
339
|
+
(r) => h(
|
|
340
|
+
"tr",
|
|
341
|
+
null,
|
|
342
|
+
h("td", { class: "audit-trail-md-col-key" }, h("code", null, r.key)),
|
|
343
|
+
h(
|
|
344
|
+
"td",
|
|
345
|
+
{ class: "audit-trail-md-col-type" },
|
|
346
|
+
h("span", { class: "audit-trail-muted" }, r.type)
|
|
347
|
+
),
|
|
348
|
+
h("td", { class: "audit-trail-md-col-value" }, h("code", null, r.value))
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/exportModal.ts
|
|
356
|
+
function openExportModal(opts) {
|
|
357
|
+
const { theme, onDownload, onClose } = opts;
|
|
358
|
+
let format = null;
|
|
359
|
+
let status = "idle";
|
|
360
|
+
let errMsg = null;
|
|
361
|
+
const csvCard = renderFormatCard(
|
|
362
|
+
"csv",
|
|
363
|
+
"CSV",
|
|
364
|
+
"Spreadsheet-friendly. Common fields are columns; nested fields (origin, metadata, change) are JSON in single columns.",
|
|
365
|
+
() => selectFormat("csv")
|
|
366
|
+
);
|
|
367
|
+
const jsonCard = renderFormatCard(
|
|
368
|
+
"json",
|
|
369
|
+
"JSON",
|
|
370
|
+
"Full event objects preserving nested structure. Same shape the API returns.",
|
|
371
|
+
() => selectFormat("json")
|
|
372
|
+
);
|
|
373
|
+
const errorSlot = h("div", { class: "audit-trail-export-error-slot" });
|
|
374
|
+
const downloadBtn = h(
|
|
375
|
+
"button",
|
|
376
|
+
{ type: "button", class: "audit-trail-button" },
|
|
377
|
+
"Download"
|
|
378
|
+
);
|
|
379
|
+
downloadBtn.disabled = true;
|
|
380
|
+
const cancelBtn = h(
|
|
381
|
+
"button",
|
|
382
|
+
{
|
|
383
|
+
type: "button",
|
|
384
|
+
class: "audit-trail-button audit-trail-button-secondary"
|
|
385
|
+
},
|
|
386
|
+
"Cancel"
|
|
387
|
+
);
|
|
388
|
+
const modal = h(
|
|
389
|
+
"div",
|
|
390
|
+
{
|
|
391
|
+
class: "audit-trail-export-modal",
|
|
392
|
+
role: "dialog",
|
|
393
|
+
"aria-modal": "true",
|
|
394
|
+
"aria-labelledby": "audit-trail-export-title"
|
|
395
|
+
},
|
|
396
|
+
h(
|
|
397
|
+
"h2",
|
|
398
|
+
{ id: "audit-trail-export-title", class: "audit-trail-export-title" },
|
|
399
|
+
"Export events"
|
|
400
|
+
),
|
|
401
|
+
h(
|
|
402
|
+
"p",
|
|
403
|
+
{ class: "audit-trail-export-blurb" },
|
|
404
|
+
"The current filters and time range are applied. Capped at 100,000 rows - narrow the filters or time window if you hit it."
|
|
405
|
+
),
|
|
406
|
+
h("div", { class: "audit-trail-export-options" }, csvCard, jsonCard),
|
|
407
|
+
errorSlot,
|
|
408
|
+
h("div", { class: "audit-trail-export-actions" }, downloadBtn, cancelBtn)
|
|
409
|
+
);
|
|
410
|
+
const backdrop = h(
|
|
411
|
+
"div",
|
|
412
|
+
{ class: "audit-trail-export-backdrop", role: "presentation" },
|
|
413
|
+
modal
|
|
414
|
+
);
|
|
415
|
+
const portal = h(
|
|
416
|
+
"div",
|
|
417
|
+
{ class: `audit-trail-portal audit-trail-theme-${theme}` },
|
|
418
|
+
backdrop
|
|
419
|
+
);
|
|
420
|
+
const selectFormat = (fmt) => {
|
|
421
|
+
format = fmt;
|
|
422
|
+
setSelected(csvCard, fmt === "csv");
|
|
423
|
+
setSelected(jsonCard, fmt === "json");
|
|
424
|
+
refreshButtons();
|
|
425
|
+
};
|
|
426
|
+
const refreshButtons = () => {
|
|
427
|
+
downloadBtn.disabled = !format || status === "downloading";
|
|
428
|
+
downloadBtn.textContent = status === "downloading" ? "Downloading\u2026" : "Download";
|
|
429
|
+
cancelBtn.disabled = status === "downloading";
|
|
430
|
+
if (status === "error" && errMsg) {
|
|
431
|
+
errorSlot.replaceChildren(
|
|
432
|
+
h(
|
|
433
|
+
"div",
|
|
434
|
+
{ class: "audit-trail-export-error", role: "alert" },
|
|
435
|
+
errMsg
|
|
436
|
+
)
|
|
437
|
+
);
|
|
438
|
+
} else {
|
|
439
|
+
errorSlot.replaceChildren();
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
let disposed = false;
|
|
443
|
+
const dispose = () => {
|
|
444
|
+
if (disposed) return;
|
|
445
|
+
disposed = true;
|
|
446
|
+
document.removeEventListener("keydown", onKey);
|
|
447
|
+
portal.remove();
|
|
448
|
+
};
|
|
449
|
+
const userClose = () => {
|
|
450
|
+
if (disposed) return;
|
|
451
|
+
if (status === "downloading") return;
|
|
452
|
+
dispose();
|
|
453
|
+
onClose();
|
|
454
|
+
};
|
|
455
|
+
const onKey = (e) => {
|
|
456
|
+
if (e.key === "Escape") userClose();
|
|
457
|
+
};
|
|
458
|
+
cancelBtn.addEventListener("click", userClose);
|
|
459
|
+
backdrop.addEventListener("click", (e) => {
|
|
460
|
+
if (e.target !== backdrop) return;
|
|
461
|
+
userClose();
|
|
462
|
+
});
|
|
463
|
+
document.addEventListener("keydown", onKey);
|
|
464
|
+
downloadBtn.addEventListener("click", () => {
|
|
465
|
+
if (!format || status === "downloading") return;
|
|
466
|
+
status = "downloading";
|
|
467
|
+
errMsg = null;
|
|
468
|
+
refreshButtons();
|
|
469
|
+
void (async () => {
|
|
470
|
+
try {
|
|
471
|
+
await onDownload(format);
|
|
472
|
+
if (disposed) return;
|
|
473
|
+
dispose();
|
|
474
|
+
} catch (err) {
|
|
475
|
+
if (disposed) return;
|
|
476
|
+
status = "error";
|
|
477
|
+
errMsg = err instanceof Error ? err.message : "Export failed.";
|
|
478
|
+
refreshButtons();
|
|
479
|
+
}
|
|
480
|
+
})();
|
|
481
|
+
});
|
|
482
|
+
document.body.appendChild(portal);
|
|
483
|
+
return dispose;
|
|
484
|
+
}
|
|
485
|
+
function renderFormatCard(format, title, body, onSelect) {
|
|
486
|
+
const btn = h(
|
|
487
|
+
"button",
|
|
488
|
+
{
|
|
489
|
+
type: "button",
|
|
490
|
+
role: "radio",
|
|
491
|
+
"aria-checked": "false",
|
|
492
|
+
class: "audit-trail-export-card",
|
|
493
|
+
"data-format": format
|
|
494
|
+
},
|
|
495
|
+
h("span", { class: "audit-trail-export-card-title" }, title),
|
|
496
|
+
h("span", { class: "audit-trail-export-card-body" }, body)
|
|
497
|
+
);
|
|
498
|
+
btn.addEventListener("click", onSelect);
|
|
499
|
+
return btn;
|
|
500
|
+
}
|
|
501
|
+
function setSelected(card, selected) {
|
|
502
|
+
card.className = selected ? "audit-trail-export-card audit-trail-export-card-selected" : "audit-trail-export-card";
|
|
503
|
+
card.setAttribute("aria-checked", selected ? "true" : "false");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/filters.ts
|
|
507
|
+
var TIME_PRESETS = [
|
|
508
|
+
{ key: "24h", label: "24h" },
|
|
509
|
+
{ key: "7d", label: "7d" },
|
|
510
|
+
{ key: "30d", label: "30d" },
|
|
511
|
+
{ key: "custom", label: "Custom" },
|
|
512
|
+
{ key: "all", label: "All" }
|
|
513
|
+
];
|
|
514
|
+
function pickInitialTab(v, claims) {
|
|
515
|
+
if (v.nlpQ && claims?.allow_nlp) return "ai";
|
|
516
|
+
if (v.q && claims?.allow_dsl_input) return "query";
|
|
517
|
+
if (hasAnyColumnFilter(v)) return "filters";
|
|
518
|
+
if (claims?.allow_nlp) return "ai";
|
|
519
|
+
return "filters";
|
|
520
|
+
}
|
|
521
|
+
function renderFiltersPanel(opts) {
|
|
522
|
+
const { claims, activeTab } = opts;
|
|
523
|
+
const allowNLP = !!claims?.allow_nlp;
|
|
524
|
+
const allowDSL = !!claims?.allow_dsl_input;
|
|
525
|
+
const tabBar = renderTabBar({ allowNLP, allowDSL, activeTab, onTabChange: opts.onTabChange });
|
|
526
|
+
let body;
|
|
527
|
+
if (activeTab === "ai" && allowNLP) {
|
|
528
|
+
body = renderAITabPanel(opts);
|
|
529
|
+
} else if (activeTab === "query" && allowDSL) {
|
|
530
|
+
body = renderQueryTabPanel(opts);
|
|
531
|
+
} else {
|
|
532
|
+
body = renderFiltersTabPanel(opts);
|
|
533
|
+
}
|
|
534
|
+
return h("div", { class: "audit-trail-filter-panel" }, tabBar, body);
|
|
535
|
+
}
|
|
536
|
+
function renderTabBar(opts) {
|
|
537
|
+
const tabs = [];
|
|
538
|
+
const make = (key, label) => {
|
|
539
|
+
const active = opts.activeTab === key;
|
|
540
|
+
const btn = h(
|
|
541
|
+
"button",
|
|
542
|
+
{
|
|
543
|
+
type: "button",
|
|
544
|
+
role: "tab",
|
|
545
|
+
"aria-selected": active ? "true" : "false",
|
|
546
|
+
class: active ? "audit-trail-filter-mode audit-trail-filter-mode-selected" : "audit-trail-filter-mode"
|
|
547
|
+
},
|
|
548
|
+
label
|
|
549
|
+
);
|
|
550
|
+
btn.addEventListener("click", () => opts.onTabChange(key));
|
|
551
|
+
return btn;
|
|
552
|
+
};
|
|
553
|
+
if (opts.allowNLP) tabs.push(make("ai", "Prompt"));
|
|
554
|
+
tabs.push(make("filters", "Filters"));
|
|
555
|
+
if (opts.allowDSL) tabs.push(make("query", "Query"));
|
|
556
|
+
return h(
|
|
557
|
+
"div",
|
|
558
|
+
{ class: "audit-trail-filter-modes", role: "tablist", "aria-label": "Filter mode" },
|
|
559
|
+
...tabs
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
function renderAITabPanel(opts) {
|
|
563
|
+
const { value, nlpState, onNLPSubmit } = opts;
|
|
564
|
+
const input = h("input", {
|
|
565
|
+
type: "text",
|
|
566
|
+
id: "audit-trail-nlp-input",
|
|
567
|
+
class: "audit-trail-nlp-input",
|
|
568
|
+
placeholder: "e.g. failed logins last 24 hours",
|
|
569
|
+
maxlength: 500,
|
|
570
|
+
"aria-label": "Natural-language filter query",
|
|
571
|
+
value: value.nlpQ ?? ""
|
|
572
|
+
});
|
|
573
|
+
input.value = value.nlpQ ?? "";
|
|
574
|
+
if (nlpState.phase === "loading") input.disabled = true;
|
|
575
|
+
const submit = () => {
|
|
576
|
+
const q = input.value.trim();
|
|
577
|
+
if (!q) return;
|
|
578
|
+
onNLPSubmit(q);
|
|
579
|
+
};
|
|
580
|
+
const label = h(
|
|
581
|
+
"label",
|
|
582
|
+
{ class: "audit-trail-nlp-label", for: "audit-trail-nlp-input" },
|
|
583
|
+
"Ask in plain English",
|
|
584
|
+
h(
|
|
585
|
+
"span",
|
|
586
|
+
{
|
|
587
|
+
class: "audit-trail-badge audit-trail-badge-info",
|
|
588
|
+
title: "Beta"
|
|
589
|
+
},
|
|
590
|
+
"Beta"
|
|
591
|
+
)
|
|
592
|
+
);
|
|
593
|
+
const children = [label, input];
|
|
594
|
+
if (value.q && value.nlpQ) {
|
|
595
|
+
children.push(
|
|
596
|
+
h(
|
|
597
|
+
"div",
|
|
598
|
+
{ class: "audit-trail-nlp-banner" },
|
|
599
|
+
h(
|
|
600
|
+
"p",
|
|
601
|
+
null,
|
|
602
|
+
h("strong", null, "Translated to:"),
|
|
603
|
+
" ",
|
|
604
|
+
h("code", { class: "audit-trail-nlp-translated" }, value.q)
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (nlpState.phase === "error") {
|
|
610
|
+
children.push(
|
|
611
|
+
h("div", { class: "audit-trail-error" }, nlpErrorMessage(nlpState.reason))
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (value.nlpUnsupported && value.nlpUnsupported.length > 0) {
|
|
615
|
+
children.push(renderUnsupportedList(value.nlpUnsupported));
|
|
616
|
+
}
|
|
617
|
+
const btn = h(
|
|
618
|
+
"button",
|
|
619
|
+
{
|
|
620
|
+
type: "button",
|
|
621
|
+
class: "audit-trail-button audit-trail-button-secondary"
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
if (nlpState.phase === "loading") {
|
|
625
|
+
btn.disabled = true;
|
|
626
|
+
btn.appendChild(h("span", { class: "audit-trail-button-spinner" }));
|
|
627
|
+
btn.appendChild(document.createTextNode(" Translating\u2026"));
|
|
628
|
+
} else {
|
|
629
|
+
btn.textContent = "Search";
|
|
630
|
+
}
|
|
631
|
+
const updateBtn = () => {
|
|
632
|
+
if (nlpState.phase === "loading") return;
|
|
633
|
+
btn.disabled = input.value.trim() === "";
|
|
634
|
+
};
|
|
635
|
+
input.addEventListener("input", updateBtn);
|
|
636
|
+
updateBtn();
|
|
637
|
+
btn.addEventListener("click", submit);
|
|
638
|
+
input.addEventListener("keydown", (e) => {
|
|
639
|
+
if (e.key === "Enter") {
|
|
640
|
+
e.preventDefault();
|
|
641
|
+
submit();
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
children.push(h("div", { class: "audit-trail-filter-actions" }, btn));
|
|
645
|
+
return h("form", { class: "audit-trail-nlp-form" }, ...children);
|
|
646
|
+
}
|
|
647
|
+
function nlpErrorMessage(reason) {
|
|
648
|
+
switch (reason) {
|
|
649
|
+
case "not_configured":
|
|
650
|
+
return "Natural-language filtering isn't configured on this server.";
|
|
651
|
+
case "not_allowed":
|
|
652
|
+
return "This embed token does not permit natural-language queries.";
|
|
653
|
+
case "rate_limited":
|
|
654
|
+
return "You've hit the per-hour limit. Try again in a few minutes.";
|
|
655
|
+
case "provider_busy":
|
|
656
|
+
return "The natural-language service is busy right now. Try again in a moment.";
|
|
657
|
+
case "bad_request":
|
|
658
|
+
return "That query couldn't be processed. Try rephrasing.";
|
|
659
|
+
default:
|
|
660
|
+
return "Couldn't process that query. Try simpler terms.";
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function renderUnsupportedList(items) {
|
|
664
|
+
const ul = h(
|
|
665
|
+
"ul",
|
|
666
|
+
null,
|
|
667
|
+
...items.map((u) => h("li", null, `Couldn't apply: ${u}`))
|
|
668
|
+
);
|
|
669
|
+
return h(
|
|
670
|
+
"div",
|
|
671
|
+
{ class: "audit-trail-error audit-trail-nlp-unsupported" },
|
|
672
|
+
ul
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
function renderFiltersTabPanel(opts) {
|
|
676
|
+
const { value, distinct, onChange, onApplied } = opts;
|
|
677
|
+
let actionSelect;
|
|
678
|
+
let actorTypeSelect;
|
|
679
|
+
let targetTypeSelect;
|
|
680
|
+
let tenantSelect = null;
|
|
681
|
+
let resultSelect = null;
|
|
682
|
+
let actorInput;
|
|
683
|
+
let targetIDInput;
|
|
684
|
+
let originIPInput;
|
|
685
|
+
let sinceInput = null;
|
|
686
|
+
let beforeInput = null;
|
|
687
|
+
const timePresets = h(
|
|
688
|
+
"div",
|
|
689
|
+
{
|
|
690
|
+
class: "audit-trail-filter-tabs",
|
|
691
|
+
role: "tablist",
|
|
692
|
+
"aria-label": "Time range"
|
|
693
|
+
},
|
|
694
|
+
...TIME_PRESETS.map((p) => {
|
|
695
|
+
const selected = value.range === p.key;
|
|
696
|
+
const btn = h(
|
|
697
|
+
"button",
|
|
698
|
+
{
|
|
699
|
+
type: "button",
|
|
700
|
+
role: "tab",
|
|
701
|
+
"aria-selected": selected ? "true" : "false",
|
|
702
|
+
class: selected ? "audit-trail-filter-tab audit-trail-filter-tab-selected" : "audit-trail-filter-tab"
|
|
703
|
+
},
|
|
704
|
+
p.label
|
|
705
|
+
);
|
|
706
|
+
btn.addEventListener("click", () => {
|
|
707
|
+
if (p.key === "custom") {
|
|
708
|
+
onChange({ ...value, range: p.key });
|
|
709
|
+
} else {
|
|
710
|
+
onChange({ ...value, range: p.key, since: void 0, before: void 0 });
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
return btn;
|
|
714
|
+
})
|
|
715
|
+
);
|
|
716
|
+
let customRow = null;
|
|
717
|
+
if (value.range === "custom") {
|
|
718
|
+
sinceInput = buildDateInput(value.since ?? "");
|
|
719
|
+
beforeInput = buildDateInput(value.before ?? "");
|
|
720
|
+
customRow = h(
|
|
721
|
+
"div",
|
|
722
|
+
{ class: "audit-trail-filter-row" },
|
|
723
|
+
h(
|
|
724
|
+
"label",
|
|
725
|
+
{ class: "audit-trail-filter-field" },
|
|
726
|
+
h("span", { class: "audit-trail-filter-field-label" }, "From"),
|
|
727
|
+
sinceInput
|
|
728
|
+
),
|
|
729
|
+
h(
|
|
730
|
+
"label",
|
|
731
|
+
{ class: "audit-trail-filter-field" },
|
|
732
|
+
h("span", { class: "audit-trail-filter-field-label" }, "To"),
|
|
733
|
+
beforeInput
|
|
734
|
+
)
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
actionSelect = buildSelect("Action", "All actions", value.action ?? "", distinct.actions);
|
|
738
|
+
actorTypeSelect = buildSelect(
|
|
739
|
+
"Actor type",
|
|
740
|
+
"All actor types",
|
|
741
|
+
value.actorType ?? "",
|
|
742
|
+
distinct.actorTypes
|
|
743
|
+
);
|
|
744
|
+
targetTypeSelect = buildSelect(
|
|
745
|
+
"Target type",
|
|
746
|
+
"All target types",
|
|
747
|
+
value.targetType ?? "",
|
|
748
|
+
distinct.targetTypes
|
|
749
|
+
);
|
|
750
|
+
const dropdownChildren = [actionSelect, actorTypeSelect, targetTypeSelect];
|
|
751
|
+
if (distinct.tenants.length > 0) {
|
|
752
|
+
tenantSelect = buildSelect(
|
|
753
|
+
"Tenant",
|
|
754
|
+
"All tenants",
|
|
755
|
+
value.tenantId ?? "",
|
|
756
|
+
distinct.tenants
|
|
757
|
+
);
|
|
758
|
+
dropdownChildren.push(tenantSelect);
|
|
759
|
+
}
|
|
760
|
+
if (distinct.resultStatuses.length > 0) {
|
|
761
|
+
resultSelect = buildSelect(
|
|
762
|
+
"Result",
|
|
763
|
+
"All results",
|
|
764
|
+
value.resultStatus ?? "",
|
|
765
|
+
distinct.resultStatuses
|
|
766
|
+
);
|
|
767
|
+
dropdownChildren.push(resultSelect);
|
|
768
|
+
}
|
|
769
|
+
const dropdownRow = h(
|
|
770
|
+
"div",
|
|
771
|
+
{ class: "audit-trail-filter-row" },
|
|
772
|
+
...dropdownChildren
|
|
773
|
+
);
|
|
774
|
+
actorInput = buildTextInput(
|
|
775
|
+
value.actor ?? "",
|
|
776
|
+
"Actor (id, name, email)",
|
|
777
|
+
"audit-trail-filter-input audit-trail-filter-actor-input"
|
|
778
|
+
);
|
|
779
|
+
targetIDInput = buildTextInput(
|
|
780
|
+
value.targetId ?? "",
|
|
781
|
+
"Target ID",
|
|
782
|
+
"audit-trail-filter-input audit-trail-filter-wide-input"
|
|
783
|
+
);
|
|
784
|
+
originIPInput = buildTextInput(
|
|
785
|
+
value.originIP ?? "",
|
|
786
|
+
"Origin IP",
|
|
787
|
+
"audit-trail-filter-input audit-trail-filter-wide-input"
|
|
788
|
+
);
|
|
789
|
+
const textRowActor = h("div", { class: "audit-trail-filter-row" }, actorInput);
|
|
790
|
+
const textRowTarget = h("div", { class: "audit-trail-filter-row" }, targetIDInput);
|
|
791
|
+
const textRowOrigin = h("div", { class: "audit-trail-filter-row" }, originIPInput);
|
|
792
|
+
const addBtn = h(
|
|
793
|
+
"button",
|
|
794
|
+
{
|
|
795
|
+
type: "button",
|
|
796
|
+
class: "audit-trail-button audit-trail-button-secondary"
|
|
797
|
+
},
|
|
798
|
+
"Add filters"
|
|
799
|
+
);
|
|
800
|
+
const norm = (s) => s === "" ? void 0 : s;
|
|
801
|
+
const computeDirty = () => {
|
|
802
|
+
if (norm(actionSelect.value) !== value.action) return true;
|
|
803
|
+
if (norm(actorTypeSelect.value) !== value.actorType) return true;
|
|
804
|
+
if (norm(targetTypeSelect.value) !== value.targetType) return true;
|
|
805
|
+
if (tenantSelect && norm(tenantSelect.value) !== value.tenantId) return true;
|
|
806
|
+
if (resultSelect && norm(resultSelect.value) !== value.resultStatus) return true;
|
|
807
|
+
if (norm(actorInput.value) !== value.actor) return true;
|
|
808
|
+
if (norm(targetIDInput.value) !== value.targetId) return true;
|
|
809
|
+
if (norm(originIPInput.value) !== value.originIP) return true;
|
|
810
|
+
if (value.range === "custom") {
|
|
811
|
+
if (norm(sinceInput?.value ?? "") !== value.since) return true;
|
|
812
|
+
if (norm(beforeInput?.value ?? "") !== value.before) return true;
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
};
|
|
816
|
+
const refreshDirty = () => {
|
|
817
|
+
addBtn.disabled = !computeDirty();
|
|
818
|
+
};
|
|
819
|
+
refreshDirty();
|
|
820
|
+
for (const el of [
|
|
821
|
+
actionSelect,
|
|
822
|
+
actorTypeSelect,
|
|
823
|
+
targetTypeSelect,
|
|
824
|
+
tenantSelect,
|
|
825
|
+
resultSelect
|
|
826
|
+
].filter(Boolean)) {
|
|
827
|
+
el.addEventListener("change", refreshDirty);
|
|
828
|
+
}
|
|
829
|
+
for (const el of [actorInput, targetIDInput, originIPInput].filter(
|
|
830
|
+
Boolean
|
|
831
|
+
)) {
|
|
832
|
+
el.addEventListener("input", refreshDirty);
|
|
833
|
+
}
|
|
834
|
+
if (sinceInput) sinceInput.addEventListener("input", refreshDirty);
|
|
835
|
+
if (beforeInput) beforeInput.addEventListener("input", refreshDirty);
|
|
836
|
+
addBtn.addEventListener("click", () => {
|
|
837
|
+
onChange({
|
|
838
|
+
...value,
|
|
839
|
+
action: norm(actionSelect.value),
|
|
840
|
+
actorType: norm(actorTypeSelect.value),
|
|
841
|
+
targetType: norm(targetTypeSelect.value),
|
|
842
|
+
tenantId: tenantSelect ? norm(tenantSelect.value) : value.tenantId,
|
|
843
|
+
resultStatus: resultSelect ? norm(resultSelect.value) : value.resultStatus,
|
|
844
|
+
actor: norm(actorInput.value),
|
|
845
|
+
targetId: norm(targetIDInput.value),
|
|
846
|
+
originIP: norm(originIPInput.value),
|
|
847
|
+
since: value.range === "custom" ? norm(sinceInput?.value ?? "") : value.since,
|
|
848
|
+
before: value.range === "custom" ? norm(beforeInput?.value ?? "") : value.before,
|
|
849
|
+
q: void 0,
|
|
850
|
+
nlpQ: void 0,
|
|
851
|
+
nlpExplanation: void 0,
|
|
852
|
+
nlpUnsupported: void 0
|
|
853
|
+
});
|
|
854
|
+
onApplied?.();
|
|
855
|
+
});
|
|
856
|
+
const addBtnRow = h("div", { class: "audit-trail-filter-actions" }, addBtn);
|
|
857
|
+
const metaSection = renderMetadataFilterSection(value, distinct, onChange, onApplied);
|
|
858
|
+
const children = [timePresets];
|
|
859
|
+
if (customRow) children.push(customRow);
|
|
860
|
+
children.push(
|
|
861
|
+
dropdownRow,
|
|
862
|
+
textRowActor,
|
|
863
|
+
textRowTarget,
|
|
864
|
+
textRowOrigin,
|
|
865
|
+
addBtnRow,
|
|
866
|
+
metaSection
|
|
867
|
+
);
|
|
868
|
+
return h("div", { class: "audit-trail-filter-tab-panel" }, ...children);
|
|
869
|
+
}
|
|
870
|
+
function buildSelect(ariaLabel, emptyLabel, selected, options) {
|
|
871
|
+
const sel = h("select", {
|
|
872
|
+
class: "audit-trail-filter-select",
|
|
873
|
+
"aria-label": ariaLabel
|
|
874
|
+
});
|
|
875
|
+
const empty = h("option", { value: "" }, emptyLabel);
|
|
876
|
+
if (selected === "") empty.selected = true;
|
|
877
|
+
sel.appendChild(empty);
|
|
878
|
+
for (const opt of options) {
|
|
879
|
+
const o = h("option", { value: opt }, opt);
|
|
880
|
+
if (opt === selected) o.selected = true;
|
|
881
|
+
sel.appendChild(o);
|
|
882
|
+
}
|
|
883
|
+
return sel;
|
|
884
|
+
}
|
|
885
|
+
function buildTextInput(value, placeholder, className) {
|
|
886
|
+
const input = h("input", {
|
|
887
|
+
type: "text",
|
|
888
|
+
class: className,
|
|
889
|
+
placeholder,
|
|
890
|
+
value
|
|
891
|
+
});
|
|
892
|
+
input.value = value;
|
|
893
|
+
return input;
|
|
894
|
+
}
|
|
895
|
+
function buildDateInput(value) {
|
|
896
|
+
const input = h("input", {
|
|
897
|
+
type: "datetime-local",
|
|
898
|
+
class: "audit-trail-filter-input",
|
|
899
|
+
value
|
|
900
|
+
});
|
|
901
|
+
input.value = value;
|
|
902
|
+
return input;
|
|
903
|
+
}
|
|
904
|
+
function renderMetadataFilterSection(value, distinct, onChange, onApplied) {
|
|
905
|
+
let variant = "metadata";
|
|
906
|
+
const typeSelect = h("select", null);
|
|
907
|
+
for (const [val, label] of [
|
|
908
|
+
["metadata", "Metadata key/value"],
|
|
909
|
+
["change-field", "Change: a field changed"],
|
|
910
|
+
["change-before", "Change: previous value (before)"],
|
|
911
|
+
["change-after", "Change: new value (after)"]
|
|
912
|
+
]) {
|
|
913
|
+
const o = h("option", { value: val }, label);
|
|
914
|
+
typeSelect.appendChild(o);
|
|
915
|
+
}
|
|
916
|
+
typeSelect.value = variant;
|
|
917
|
+
const mdKey = h("input", {
|
|
918
|
+
type: "text",
|
|
919
|
+
placeholder: "e.g. environment",
|
|
920
|
+
list: "audit-trail-mdk-key-list"
|
|
921
|
+
});
|
|
922
|
+
const mdKeyList = h("datalist", { id: "audit-trail-mdk-key-list" });
|
|
923
|
+
for (const k of distinct.metadataKeys) {
|
|
924
|
+
mdKeyList.appendChild(
|
|
925
|
+
h("option", { value: k.key }, `${k.observed_type} \xB7 ${k.event_count}`)
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const mdOp = h("select", null);
|
|
929
|
+
for (const [val, label] of [
|
|
930
|
+
["eq", "equals"],
|
|
931
|
+
["neq", "not equal"],
|
|
932
|
+
["contains", "contains"],
|
|
933
|
+
["gt", "greater than"],
|
|
934
|
+
["gte", "greater or equal"],
|
|
935
|
+
["lt", "less than"],
|
|
936
|
+
["lte", "less or equal"],
|
|
937
|
+
["between", "between"]
|
|
938
|
+
]) {
|
|
939
|
+
mdOp.appendChild(h("option", { value: val }, label));
|
|
940
|
+
}
|
|
941
|
+
const mdValue = h("input", {
|
|
942
|
+
type: "text",
|
|
943
|
+
placeholder: "e.g. prod"
|
|
944
|
+
});
|
|
945
|
+
const mdValue2 = h("input", {
|
|
946
|
+
type: "text",
|
|
947
|
+
placeholder: "e.g. 1000"
|
|
948
|
+
});
|
|
949
|
+
const cfField = h("input", {
|
|
950
|
+
type: "text",
|
|
951
|
+
placeholder: "e.g. email",
|
|
952
|
+
list: "audit-trail-cf-field-list"
|
|
953
|
+
});
|
|
954
|
+
const cfFieldList = h("datalist", { id: "audit-trail-cf-field-list" });
|
|
955
|
+
for (const c of distinct.changeFields) {
|
|
956
|
+
cfFieldList.appendChild(h("option", { value: c.field }, `${c.event_count} events`));
|
|
957
|
+
}
|
|
958
|
+
const cbField = h("input", {
|
|
959
|
+
type: "text",
|
|
960
|
+
placeholder: "e.g. role",
|
|
961
|
+
list: "audit-trail-cf-field-list"
|
|
962
|
+
});
|
|
963
|
+
const cbValue = h("input", {
|
|
964
|
+
type: "text",
|
|
965
|
+
placeholder: "e.g. user"
|
|
966
|
+
});
|
|
967
|
+
const caField = h("input", {
|
|
968
|
+
type: "text",
|
|
969
|
+
placeholder: "e.g. role",
|
|
970
|
+
list: "audit-trail-cf-field-list"
|
|
971
|
+
});
|
|
972
|
+
const caValue = h("input", {
|
|
973
|
+
type: "text",
|
|
974
|
+
placeholder: "e.g. admin"
|
|
975
|
+
});
|
|
976
|
+
const metadataVariant = h(
|
|
977
|
+
"div",
|
|
978
|
+
{ "data-variant": "metadata" },
|
|
979
|
+
addFilterRow("Key", mdKey, mdKeyList),
|
|
980
|
+
addFilterRow("Operator", mdOp),
|
|
981
|
+
addFilterRow("Value", mdValue),
|
|
982
|
+
addFilterRow("Upper bound", mdValue2)
|
|
983
|
+
);
|
|
984
|
+
const changeFieldVariant = h(
|
|
985
|
+
"div",
|
|
986
|
+
{ "data-variant": "change-field" },
|
|
987
|
+
addFilterRow("Field", cfField, cfFieldList)
|
|
988
|
+
);
|
|
989
|
+
const changeBeforeVariant = h(
|
|
990
|
+
"div",
|
|
991
|
+
{ "data-variant": "change-before" },
|
|
992
|
+
addFilterRow("Field", cbField),
|
|
993
|
+
addFilterRow("Previous value", cbValue)
|
|
994
|
+
);
|
|
995
|
+
const changeAfterVariant = h(
|
|
996
|
+
"div",
|
|
997
|
+
{ "data-variant": "change-after" },
|
|
998
|
+
addFilterRow("Field", caField),
|
|
999
|
+
addFilterRow("New value", caValue)
|
|
1000
|
+
);
|
|
1001
|
+
const variants = [
|
|
1002
|
+
metadataVariant,
|
|
1003
|
+
changeFieldVariant,
|
|
1004
|
+
changeBeforeVariant,
|
|
1005
|
+
changeAfterVariant
|
|
1006
|
+
];
|
|
1007
|
+
const errorEl = h("p", { class: "audit-trail-add-filter-error" }, "");
|
|
1008
|
+
errorEl.style.display = "none";
|
|
1009
|
+
const addBtn = h(
|
|
1010
|
+
"button",
|
|
1011
|
+
{
|
|
1012
|
+
type: "button",
|
|
1013
|
+
class: "audit-trail-button audit-trail-button-secondary"
|
|
1014
|
+
},
|
|
1015
|
+
"Add filter"
|
|
1016
|
+
);
|
|
1017
|
+
const mdValue2Row = metadataVariant.children[3];
|
|
1018
|
+
mdValue2Row.style.display = "none";
|
|
1019
|
+
const refresh = () => {
|
|
1020
|
+
const compiled = compileMetaClause({
|
|
1021
|
+
variant,
|
|
1022
|
+
mdKey: mdKey.value,
|
|
1023
|
+
mdOp: mdOp.value,
|
|
1024
|
+
mdValue: mdValue.value,
|
|
1025
|
+
mdValue2: mdValue2.value,
|
|
1026
|
+
cfField: cfField.value,
|
|
1027
|
+
cbField: cbField.value,
|
|
1028
|
+
cbValue: cbValue.value,
|
|
1029
|
+
caField: caField.value,
|
|
1030
|
+
caValue: caValue.value
|
|
1031
|
+
});
|
|
1032
|
+
addBtn.disabled = compiled.dsl === null;
|
|
1033
|
+
if (compiled.error && /Both bounds/.test(compiled.error)) {
|
|
1034
|
+
errorEl.textContent = compiled.error;
|
|
1035
|
+
errorEl.style.display = "";
|
|
1036
|
+
} else {
|
|
1037
|
+
errorEl.textContent = "";
|
|
1038
|
+
errorEl.style.display = "none";
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
const showVariant = (v) => {
|
|
1042
|
+
variant = v;
|
|
1043
|
+
for (const el of variants) {
|
|
1044
|
+
el.style.display = el.dataset.variant === v ? "" : "none";
|
|
1045
|
+
}
|
|
1046
|
+
refresh();
|
|
1047
|
+
};
|
|
1048
|
+
showVariant("metadata");
|
|
1049
|
+
typeSelect.addEventListener("change", () => showVariant(typeSelect.value));
|
|
1050
|
+
mdOp.addEventListener("change", () => {
|
|
1051
|
+
mdValue2Row.style.display = mdOp.value === "between" ? "" : "none";
|
|
1052
|
+
refresh();
|
|
1053
|
+
});
|
|
1054
|
+
for (const el of [
|
|
1055
|
+
mdKey,
|
|
1056
|
+
mdValue,
|
|
1057
|
+
mdValue2,
|
|
1058
|
+
cfField,
|
|
1059
|
+
cbField,
|
|
1060
|
+
cbValue,
|
|
1061
|
+
caField,
|
|
1062
|
+
caValue
|
|
1063
|
+
]) {
|
|
1064
|
+
el.addEventListener("input", refresh);
|
|
1065
|
+
}
|
|
1066
|
+
addBtn.addEventListener("click", () => {
|
|
1067
|
+
const compiled = compileMetaClause({
|
|
1068
|
+
variant,
|
|
1069
|
+
mdKey: mdKey.value,
|
|
1070
|
+
mdOp: mdOp.value,
|
|
1071
|
+
mdValue: mdValue.value,
|
|
1072
|
+
mdValue2: mdValue2.value,
|
|
1073
|
+
cfField: cfField.value,
|
|
1074
|
+
cbField: cbField.value,
|
|
1075
|
+
cbValue: cbValue.value,
|
|
1076
|
+
caField: caField.value,
|
|
1077
|
+
caValue: caValue.value
|
|
1078
|
+
});
|
|
1079
|
+
if (!compiled.dsl) return;
|
|
1080
|
+
onChange({
|
|
1081
|
+
...value,
|
|
1082
|
+
q: compiled.dsl,
|
|
1083
|
+
nlpQ: void 0,
|
|
1084
|
+
nlpExplanation: void 0,
|
|
1085
|
+
nlpUnsupported: void 0
|
|
1086
|
+
});
|
|
1087
|
+
onApplied?.();
|
|
1088
|
+
});
|
|
1089
|
+
refresh();
|
|
1090
|
+
return h(
|
|
1091
|
+
"div",
|
|
1092
|
+
{ class: "audit-trail-add-filter-inline" },
|
|
1093
|
+
h("h3", { class: "audit-trail-add-filter-heading" }, "Metadata and Changed Fields"),
|
|
1094
|
+
addFilterRow("Filter type", typeSelect),
|
|
1095
|
+
metadataVariant,
|
|
1096
|
+
changeFieldVariant,
|
|
1097
|
+
changeBeforeVariant,
|
|
1098
|
+
changeAfterVariant,
|
|
1099
|
+
errorEl,
|
|
1100
|
+
h(
|
|
1101
|
+
"div",
|
|
1102
|
+
{ class: "audit-trail-filter-actions audit-trail-add-filter-actions" },
|
|
1103
|
+
addBtn
|
|
1104
|
+
)
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
function addFilterRow(label, ...children) {
|
|
1108
|
+
return h(
|
|
1109
|
+
"label",
|
|
1110
|
+
{ class: "audit-trail-add-filter-row" },
|
|
1111
|
+
h("span", { class: "audit-trail-add-filter-label" }, label),
|
|
1112
|
+
...children
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
function compileMetaClause(i) {
|
|
1116
|
+
switch (i.variant) {
|
|
1117
|
+
case "metadata": {
|
|
1118
|
+
const key = i.mdKey.trim();
|
|
1119
|
+
if (!key) return { dsl: null, error: "Key is required" };
|
|
1120
|
+
const path = "metadata." + quoteKeyIfNeeded(key);
|
|
1121
|
+
const v1 = i.mdValue;
|
|
1122
|
+
const v2 = i.mdValue2;
|
|
1123
|
+
switch (i.mdOp) {
|
|
1124
|
+
case "eq":
|
|
1125
|
+
return { dsl: `${path}:${quoteIfNeeded(v1)}` };
|
|
1126
|
+
case "neq":
|
|
1127
|
+
return { dsl: `${path}:!${quoteIfNeeded(v1)}` };
|
|
1128
|
+
case "contains":
|
|
1129
|
+
return { dsl: `${path}:~${quoteIfNeeded(v1)}` };
|
|
1130
|
+
case "gt":
|
|
1131
|
+
return { dsl: `${path}:>${quoteIfNeeded(v1)}` };
|
|
1132
|
+
case "gte":
|
|
1133
|
+
return { dsl: `${path}:>=${quoteIfNeeded(v1)}` };
|
|
1134
|
+
case "lt":
|
|
1135
|
+
return { dsl: `${path}:<${quoteIfNeeded(v1)}` };
|
|
1136
|
+
case "lte":
|
|
1137
|
+
return { dsl: `${path}:<=${quoteIfNeeded(v1)}` };
|
|
1138
|
+
case "between":
|
|
1139
|
+
if (v1 === "" || v2 === "") {
|
|
1140
|
+
return { dsl: null, error: "Both bounds required for between" };
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
dsl: `${path}:[${quoteIfNeeded(v1)} TO ${quoteIfNeeded(v2)}]`
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
return { dsl: null, error: "Unknown operator" };
|
|
1147
|
+
}
|
|
1148
|
+
case "change-field": {
|
|
1149
|
+
const f = i.cfField.trim();
|
|
1150
|
+
if (!f) return { dsl: null, error: "Field is required" };
|
|
1151
|
+
return { dsl: `change.field:${quoteIfNeeded(f)}` };
|
|
1152
|
+
}
|
|
1153
|
+
case "change-before": {
|
|
1154
|
+
const f = i.cbField.trim();
|
|
1155
|
+
if (!f) return { dsl: null, error: "Field is required" };
|
|
1156
|
+
return {
|
|
1157
|
+
dsl: `change.${quoteKeyIfNeeded(f)}.before:${quoteIfNeeded(i.cbValue)}`
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
case "change-after": {
|
|
1161
|
+
const f = i.caField.trim();
|
|
1162
|
+
if (!f) return { dsl: null, error: "Field is required" };
|
|
1163
|
+
return {
|
|
1164
|
+
dsl: `change.${quoteKeyIfNeeded(f)}.after:${quoteIfNeeded(i.caValue)}`
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function quoteIfNeeded(raw) {
|
|
1170
|
+
if (raw === "") return '""';
|
|
1171
|
+
if (/[\s"\(\)\[\]]/.test(raw)) {
|
|
1172
|
+
return '"' + raw.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
1173
|
+
}
|
|
1174
|
+
return raw;
|
|
1175
|
+
}
|
|
1176
|
+
function quoteKeyIfNeeded(raw) {
|
|
1177
|
+
if (/^[A-Za-z_][A-Za-z0-9_-]*$/.test(raw)) return raw;
|
|
1178
|
+
return '"' + raw + '"';
|
|
1179
|
+
}
|
|
1180
|
+
function renderQueryTabPanel(opts) {
|
|
1181
|
+
const { value, onChange } = opts;
|
|
1182
|
+
const input = h("input", {
|
|
1183
|
+
type: "text",
|
|
1184
|
+
id: "audit-trail-query-input",
|
|
1185
|
+
class: "audit-trail-query-input",
|
|
1186
|
+
placeholder: "e.g. action:user.login AND result.status:!ok",
|
|
1187
|
+
"aria-label": "Advanced query (DSL)",
|
|
1188
|
+
spellcheck: "false",
|
|
1189
|
+
value: value.q ?? ""
|
|
1190
|
+
});
|
|
1191
|
+
input.value = value.q ?? "";
|
|
1192
|
+
const label = h(
|
|
1193
|
+
"label",
|
|
1194
|
+
{ class: "audit-trail-query-label", for: "audit-trail-query-input" },
|
|
1195
|
+
"Query ",
|
|
1196
|
+
h(
|
|
1197
|
+
"a",
|
|
1198
|
+
{
|
|
1199
|
+
href: "https://everscribe.io/docs",
|
|
1200
|
+
target: "_blank",
|
|
1201
|
+
rel: "noopener noreferrer"
|
|
1202
|
+
},
|
|
1203
|
+
"(see docs)"
|
|
1204
|
+
)
|
|
1205
|
+
);
|
|
1206
|
+
const btn = h(
|
|
1207
|
+
"button",
|
|
1208
|
+
{
|
|
1209
|
+
type: "button",
|
|
1210
|
+
class: "audit-trail-button audit-trail-button-secondary"
|
|
1211
|
+
},
|
|
1212
|
+
"Search"
|
|
1213
|
+
);
|
|
1214
|
+
const refresh = () => {
|
|
1215
|
+
btn.disabled = input.value.trim() === (value.q ?? "");
|
|
1216
|
+
};
|
|
1217
|
+
refresh();
|
|
1218
|
+
input.addEventListener("input", refresh);
|
|
1219
|
+
const submit = () => {
|
|
1220
|
+
const next = input.value.trim() || void 0;
|
|
1221
|
+
if (next === value.q) return;
|
|
1222
|
+
onChange({
|
|
1223
|
+
...value,
|
|
1224
|
+
q: next,
|
|
1225
|
+
nlpQ: void 0,
|
|
1226
|
+
nlpExplanation: void 0,
|
|
1227
|
+
nlpUnsupported: void 0
|
|
1228
|
+
});
|
|
1229
|
+
};
|
|
1230
|
+
btn.addEventListener("click", submit);
|
|
1231
|
+
input.addEventListener("keydown", (e) => {
|
|
1232
|
+
if (e.key === "Enter") {
|
|
1233
|
+
e.preventDefault();
|
|
1234
|
+
submit();
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
return h(
|
|
1238
|
+
"form",
|
|
1239
|
+
{ class: "audit-trail-query-form" },
|
|
1240
|
+
label,
|
|
1241
|
+
input,
|
|
1242
|
+
h("div", { class: "audit-trail-filter-actions" }, btn)
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
function renderFilterChips(opts) {
|
|
1246
|
+
const { value, onChange } = opts;
|
|
1247
|
+
const chips = [];
|
|
1248
|
+
const colChip = (field, label, raw) => {
|
|
1249
|
+
if (!raw) return;
|
|
1250
|
+
chips.push({
|
|
1251
|
+
label: `${label}: ${raw}`,
|
|
1252
|
+
remove: () => onChange({ ...value, [field]: void 0 })
|
|
1253
|
+
});
|
|
1254
|
+
};
|
|
1255
|
+
colChip("action", "Action", value.action);
|
|
1256
|
+
colChip("actor", "Actor", value.actor);
|
|
1257
|
+
colChip("actorType", "Actor type", value.actorType);
|
|
1258
|
+
colChip("tenantId", "Tenant", value.tenantId);
|
|
1259
|
+
colChip("targetType", "Target type", value.targetType);
|
|
1260
|
+
colChip("targetId", "Target ID", value.targetId);
|
|
1261
|
+
colChip("resultStatus", "Result", value.resultStatus);
|
|
1262
|
+
colChip("originIP", "Origin IP", value.originIP);
|
|
1263
|
+
if (value.q) {
|
|
1264
|
+
for (const clause of parseQClauses(value.q)) {
|
|
1265
|
+
chips.push({
|
|
1266
|
+
label: clause.label,
|
|
1267
|
+
remove: () => {
|
|
1268
|
+
const nextQ = removeClauseFromQ(value.q, clause.raw);
|
|
1269
|
+
onChange({
|
|
1270
|
+
...value,
|
|
1271
|
+
q: nextQ,
|
|
1272
|
+
nlpQ: nextQ ? value.nlpQ : void 0,
|
|
1273
|
+
nlpExplanation: nextQ ? value.nlpExplanation : void 0,
|
|
1274
|
+
nlpUnsupported: nextQ ? value.nlpUnsupported : void 0
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (chips.length === 0) return null;
|
|
1281
|
+
const chipsRow = h(
|
|
1282
|
+
"div",
|
|
1283
|
+
{ class: "audit-trail-filter-chips" },
|
|
1284
|
+
...chips.map((c) => {
|
|
1285
|
+
const btn = h(
|
|
1286
|
+
"button",
|
|
1287
|
+
{
|
|
1288
|
+
type: "button",
|
|
1289
|
+
class: "audit-trail-filter-chip",
|
|
1290
|
+
title: "Remove this filter",
|
|
1291
|
+
"aria-label": `Remove filter: ${c.label}`
|
|
1292
|
+
},
|
|
1293
|
+
h("span", null, c.label),
|
|
1294
|
+
h("span", { "aria-hidden": "true", class: "audit-trail-filter-chip-x" }, "\xD7")
|
|
1295
|
+
);
|
|
1296
|
+
btn.addEventListener("click", c.remove);
|
|
1297
|
+
return btn;
|
|
1298
|
+
})
|
|
1299
|
+
);
|
|
1300
|
+
const clearAllBtn = h(
|
|
1301
|
+
"button",
|
|
1302
|
+
{
|
|
1303
|
+
type: "button",
|
|
1304
|
+
class: "audit-trail-button audit-trail-button-text"
|
|
1305
|
+
},
|
|
1306
|
+
"Clear all"
|
|
1307
|
+
);
|
|
1308
|
+
clearAllBtn.addEventListener("click", () => {
|
|
1309
|
+
onChange({
|
|
1310
|
+
range: value.range,
|
|
1311
|
+
since: value.since,
|
|
1312
|
+
before: value.before
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
return h(
|
|
1316
|
+
"div",
|
|
1317
|
+
{ class: "audit-trail-filter-chips-row" },
|
|
1318
|
+
chipsRow,
|
|
1319
|
+
clearAllBtn
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
function hasAnyColumnFilter(v) {
|
|
1323
|
+
return !!(v.action || v.actor || v.actorType || v.tenantId || v.targetType || v.targetId || v.resultStatus || v.originIP);
|
|
1324
|
+
}
|
|
1325
|
+
function countActiveColumnFilters(v) {
|
|
1326
|
+
let n = 0;
|
|
1327
|
+
if (v.action) n++;
|
|
1328
|
+
if (v.actor) n++;
|
|
1329
|
+
if (v.actorType) n++;
|
|
1330
|
+
if (v.tenantId) n++;
|
|
1331
|
+
if (v.targetType) n++;
|
|
1332
|
+
if (v.targetId) n++;
|
|
1333
|
+
if (v.resultStatus) n++;
|
|
1334
|
+
if (v.originIP) n++;
|
|
1335
|
+
return n;
|
|
1336
|
+
}
|
|
1337
|
+
function parseQClauses(q) {
|
|
1338
|
+
const trimmed = q.trim();
|
|
1339
|
+
if (!trimmed) return [];
|
|
1340
|
+
if (/[()]/.test(trimmed)) return [{ label: trimmed, raw: trimmed }];
|
|
1341
|
+
return trimmed.split(/\s+AND\s+/i).map((s) => s.trim()).filter((s) => s.length > 0).map((s) => ({ label: s, raw: s }));
|
|
1342
|
+
}
|
|
1343
|
+
function removeClauseFromQ(q, raw) {
|
|
1344
|
+
const remaining = parseQClauses(q).filter((c) => c.raw !== raw);
|
|
1345
|
+
if (remaining.length === 0) return void 0;
|
|
1346
|
+
return remaining.map((c) => c.raw).join(" AND ");
|
|
1347
|
+
}
|
|
1348
|
+
function resolveTimeBounds(filters) {
|
|
1349
|
+
switch (filters.range) {
|
|
1350
|
+
case "24h":
|
|
1351
|
+
return { since: relativeIso(24 * 60 * 60 * 1e3) };
|
|
1352
|
+
case "7d":
|
|
1353
|
+
return { since: relativeIso(7 * 24 * 60 * 60 * 1e3) };
|
|
1354
|
+
case "30d":
|
|
1355
|
+
return { since: relativeIso(30 * 24 * 60 * 60 * 1e3) };
|
|
1356
|
+
case "custom":
|
|
1357
|
+
return {
|
|
1358
|
+
since: localToIso(filters.since),
|
|
1359
|
+
before: localToIso(filters.before)
|
|
1360
|
+
};
|
|
1361
|
+
case "all":
|
|
1362
|
+
default:
|
|
1363
|
+
return {};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function relativeIso(ms) {
|
|
1367
|
+
return new Date(Date.now() - ms).toISOString();
|
|
1368
|
+
}
|
|
1369
|
+
function localToIso(value) {
|
|
1370
|
+
if (!value) return void 0;
|
|
1371
|
+
const d = new Date(value);
|
|
1372
|
+
if (Number.isNaN(d.getTime())) return void 0;
|
|
1373
|
+
return d.toISOString();
|
|
1374
|
+
}
|
|
1375
|
+
function renderTable(events, columns, onRowClick) {
|
|
1376
|
+
const headerCells = columns.map(
|
|
1377
|
+
(col) => h(
|
|
1378
|
+
"th",
|
|
1379
|
+
{ scope: "col", class: `audit-trail-th audit-trail-th-${col}` },
|
|
1380
|
+
COLUMN_LABELS[col] ?? col
|
|
1381
|
+
)
|
|
1382
|
+
);
|
|
1383
|
+
const rows = events.map((event) => renderRow(event, columns, onRowClick));
|
|
1384
|
+
return h(
|
|
1385
|
+
"div",
|
|
1386
|
+
{ class: "audit-trail-table-wrap" },
|
|
1387
|
+
h(
|
|
1388
|
+
"table",
|
|
1389
|
+
{ class: "audit-trail-table" },
|
|
1390
|
+
h("thead", null, h("tr", null, ...headerCells)),
|
|
1391
|
+
h("tbody", null, ...rows)
|
|
1392
|
+
)
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
function renderRow(event, columns, onClick) {
|
|
1396
|
+
const interactive = Boolean(onClick);
|
|
1397
|
+
const cells = columns.map(
|
|
1398
|
+
(col) => h(
|
|
1399
|
+
"td",
|
|
1400
|
+
{
|
|
1401
|
+
class: `audit-trail-cell audit-trail-cell-${col}`,
|
|
1402
|
+
"data-label": COLUMN_LABELS[col] ?? col
|
|
1403
|
+
},
|
|
1404
|
+
renderCell(col, event)
|
|
1405
|
+
)
|
|
1406
|
+
);
|
|
1407
|
+
const tr = h(
|
|
1408
|
+
"tr",
|
|
1409
|
+
{
|
|
1410
|
+
class: "audit-trail-row",
|
|
1411
|
+
tabindex: interactive ? "0" : void 0
|
|
1412
|
+
},
|
|
1413
|
+
...cells
|
|
1414
|
+
);
|
|
1415
|
+
if (interactive && onClick) {
|
|
1416
|
+
tr.addEventListener("click", () => onClick(event));
|
|
1417
|
+
tr.addEventListener("keydown", (e) => {
|
|
1418
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1419
|
+
e.preventDefault();
|
|
1420
|
+
onClick(event);
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
return tr;
|
|
1425
|
+
}
|
|
1426
|
+
function renderCell(column, event) {
|
|
1427
|
+
switch (column) {
|
|
1428
|
+
case "result":
|
|
1429
|
+
return renderResult(event.result);
|
|
1430
|
+
case "origin":
|
|
1431
|
+
return renderOrigin(event.origin);
|
|
1432
|
+
case "change":
|
|
1433
|
+
case "metadata":
|
|
1434
|
+
return renderPresence(getField(event, column));
|
|
1435
|
+
default: {
|
|
1436
|
+
const value = getField(event, column);
|
|
1437
|
+
if (value == null) return null;
|
|
1438
|
+
if (column === "occurred_at" && typeof value === "string") {
|
|
1439
|
+
return h(
|
|
1440
|
+
"time",
|
|
1441
|
+
{
|
|
1442
|
+
datetime: value,
|
|
1443
|
+
title: value,
|
|
1444
|
+
// Marker for AuditTrailElement's 30s tick: each render uses
|
|
1445
|
+
// Date.now(), and the timer queries by this attribute to
|
|
1446
|
+
// refresh the relative half without rebuilding the table.
|
|
1447
|
+
"data-occurred-at": value
|
|
1448
|
+
},
|
|
1449
|
+
formatTimeCell(value, Date.now())
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
if (typeof value === "string") return value;
|
|
1453
|
+
if (typeof value === "object") return summarizeObject(value);
|
|
1454
|
+
return String(value);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function getField(event, key) {
|
|
1459
|
+
return event[key];
|
|
1460
|
+
}
|
|
1461
|
+
function renderResult(value) {
|
|
1462
|
+
if (value == null || typeof value !== "object") return "-";
|
|
1463
|
+
const obj = value;
|
|
1464
|
+
const status = typeof obj.status === "string" ? obj.status : "";
|
|
1465
|
+
if (!status) return "-";
|
|
1466
|
+
return h(
|
|
1467
|
+
"span",
|
|
1468
|
+
{ class: `audit-trail-status audit-trail-status-${cssToken(status)}` },
|
|
1469
|
+
status
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
function renderOrigin(value) {
|
|
1473
|
+
if (value == null || typeof value !== "object") return "-";
|
|
1474
|
+
const obj = value;
|
|
1475
|
+
for (const key of ["ip", "hostname", "host"]) {
|
|
1476
|
+
const v = obj[key];
|
|
1477
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
1478
|
+
}
|
|
1479
|
+
return summarizeObject(obj);
|
|
1480
|
+
}
|
|
1481
|
+
function renderPresence(value) {
|
|
1482
|
+
if (value == null) return "-";
|
|
1483
|
+
if (typeof value === "object") {
|
|
1484
|
+
const keys = Object.keys(value);
|
|
1485
|
+
if (keys.length === 0) return "-";
|
|
1486
|
+
return h("span", { class: "audit-trail-cell-presence" }, "View");
|
|
1487
|
+
}
|
|
1488
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1489
|
+
return h("span", { class: "audit-trail-cell-presence" }, "View");
|
|
1490
|
+
}
|
|
1491
|
+
return "-";
|
|
1492
|
+
}
|
|
1493
|
+
function summarizeObject(value) {
|
|
1494
|
+
const obj = value;
|
|
1495
|
+
for (const key of ["name", "email", "id", "type"]) {
|
|
1496
|
+
const v = obj[key];
|
|
1497
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
1498
|
+
}
|
|
1499
|
+
const keys = Object.keys(obj);
|
|
1500
|
+
if (keys.length === 0) return "-";
|
|
1501
|
+
return `{${keys.length} ${keys.length === 1 ? "field" : "fields"}}`;
|
|
1502
|
+
}
|
|
1503
|
+
function cssToken(s) {
|
|
1504
|
+
return s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/AuditTrailElement.ts
|
|
1508
|
+
var DEFAULT_API_BASE = "https://api.everscribe.io/v1/embed";
|
|
1509
|
+
var DEFAULT_PAGE_SIZE = 25;
|
|
1510
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1511
|
+
var DEFAULT_VISIBLE_COLUMNS = [
|
|
1512
|
+
"occurred_at",
|
|
1513
|
+
"action",
|
|
1514
|
+
"actor",
|
|
1515
|
+
"target",
|
|
1516
|
+
"tenant_id",
|
|
1517
|
+
"result"
|
|
1518
|
+
];
|
|
1519
|
+
var OBSERVED = [
|
|
1520
|
+
"token",
|
|
1521
|
+
"token-endpoint",
|
|
1522
|
+
"api-base",
|
|
1523
|
+
"page-size",
|
|
1524
|
+
"poll-interval",
|
|
1525
|
+
"theme",
|
|
1526
|
+
"default-time-range"
|
|
1527
|
+
];
|
|
1528
|
+
var AuditTrailElement = class extends HTMLElement {
|
|
1529
|
+
constructor() {
|
|
1530
|
+
super(...arguments);
|
|
1531
|
+
this.bootstrap = { phase: "idle" };
|
|
1532
|
+
this.bootstrapAbort = null;
|
|
1533
|
+
this.eventsStore = null;
|
|
1534
|
+
this.distinctStore = null;
|
|
1535
|
+
this.eventsUnsub = null;
|
|
1536
|
+
this.distinctUnsub = null;
|
|
1537
|
+
// Latest snapshots cached so render() doesn't have to peek into stores.
|
|
1538
|
+
this.eventsState = null;
|
|
1539
|
+
this.distinctValues = {
|
|
1540
|
+
actions: [],
|
|
1541
|
+
actorTypes: [],
|
|
1542
|
+
targetTypes: [],
|
|
1543
|
+
tenants: [],
|
|
1544
|
+
resultStatuses: [],
|
|
1545
|
+
metadataKeys: [],
|
|
1546
|
+
changeFields: []
|
|
1547
|
+
};
|
|
1548
|
+
// UI state.
|
|
1549
|
+
this.filters = { range: "all" };
|
|
1550
|
+
this.filtersOpen = false;
|
|
1551
|
+
this.activeTab = "filters";
|
|
1552
|
+
this.nlpState = { phase: "idle" };
|
|
1553
|
+
this.nlpAbort = null;
|
|
1554
|
+
this.visibleSet = new Set(DEFAULT_VISIBLE_COLUMNS);
|
|
1555
|
+
// Slot tracking for partial re-renders. bodySlot wraps the table /
|
|
1556
|
+
// state messages so events-store subscriptions can update only the
|
|
1557
|
+
// table region without touching the toolbar (which would clobber
|
|
1558
|
+
// open <details> picker state) or filter inputs (focus + drafts).
|
|
1559
|
+
this.bodySlot = null;
|
|
1560
|
+
// Modal cleanup hooks. Set when a modal is open; null otherwise.
|
|
1561
|
+
this.detailDispose = null;
|
|
1562
|
+
this.exportDispose = null;
|
|
1563
|
+
// 30s timer that refreshes the relative half of each row's
|
|
1564
|
+
// "absolute (N ago)" timestamp without rebuilding the table.
|
|
1565
|
+
this.relativeTimeTimer = null;
|
|
1566
|
+
}
|
|
1567
|
+
static get observedAttributes() {
|
|
1568
|
+
return OBSERVED;
|
|
1569
|
+
}
|
|
1570
|
+
connectedCallback() {
|
|
1571
|
+
this.classList.add("audit-trail-root");
|
|
1572
|
+
this.classList.add(`audit-trail-theme-${this.themeAttr()}`);
|
|
1573
|
+
this.filters = { range: this.defaultTimeRangeAttr() };
|
|
1574
|
+
this.start();
|
|
1575
|
+
this.startRelativeTimeTimer();
|
|
1576
|
+
}
|
|
1577
|
+
disconnectedCallback() {
|
|
1578
|
+
this.cleanup();
|
|
1579
|
+
this.stopRelativeTimeTimer();
|
|
1580
|
+
}
|
|
1581
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
1582
|
+
if (oldVal === newVal) return;
|
|
1583
|
+
if (!this.isConnected) return;
|
|
1584
|
+
if (name === "theme") {
|
|
1585
|
+
const oldClass = `audit-trail-theme-${oldVal === "dark" ? "dark" : "light"}`;
|
|
1586
|
+
const newClass = `audit-trail-theme-${newVal === "dark" ? "dark" : "light"}`;
|
|
1587
|
+
this.classList.remove(oldClass);
|
|
1588
|
+
this.classList.add(newClass);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (name === "token" || name === "token-endpoint") {
|
|
1592
|
+
this.start();
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (name === "default-time-range") {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (this.bootstrap.phase === "ready") this.restartEventsStore();
|
|
1599
|
+
}
|
|
1600
|
+
// Public method, mirrors the React component's refresh hatch. Triggers
|
|
1601
|
+
// a full re-bootstrap (fresh token fetch + fresh stores).
|
|
1602
|
+
refresh() {
|
|
1603
|
+
this.start();
|
|
1604
|
+
}
|
|
1605
|
+
// ---- attributes ----
|
|
1606
|
+
themeAttr() {
|
|
1607
|
+
return this.getAttribute("theme") === "dark" ? "dark" : "light";
|
|
1608
|
+
}
|
|
1609
|
+
apiBaseAttr() {
|
|
1610
|
+
return this.getAttribute("api-base") || DEFAULT_API_BASE;
|
|
1611
|
+
}
|
|
1612
|
+
pageSizeAttr() {
|
|
1613
|
+
const n = Number.parseInt(this.getAttribute("page-size") ?? "", 10);
|
|
1614
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_PAGE_SIZE;
|
|
1615
|
+
}
|
|
1616
|
+
pollIntervalAttr() {
|
|
1617
|
+
const v = this.getAttribute("poll-interval");
|
|
1618
|
+
if (v === null) return DEFAULT_POLL_INTERVAL_MS;
|
|
1619
|
+
const n = Number.parseInt(v, 10);
|
|
1620
|
+
return Number.isFinite(n) ? n : DEFAULT_POLL_INTERVAL_MS;
|
|
1621
|
+
}
|
|
1622
|
+
defaultTimeRangeAttr() {
|
|
1623
|
+
const v = this.getAttribute("default-time-range");
|
|
1624
|
+
if (v === "24h" || v === "7d" || v === "30d" || v === "all") return v;
|
|
1625
|
+
return "all";
|
|
1626
|
+
}
|
|
1627
|
+
// storageKey returns a localStorage key namespaced by the current
|
|
1628
|
+
// token's project (sub) and tenant. Returns null when the element
|
|
1629
|
+
// isn't ready or the token has no usable sub - callers treat null
|
|
1630
|
+
// as "skip persistence."
|
|
1631
|
+
storageKey(suffix) {
|
|
1632
|
+
if (this.bootstrap.phase !== "ready") return null;
|
|
1633
|
+
const sub = this.bootstrap.claims?.sub;
|
|
1634
|
+
if (!sub) return null;
|
|
1635
|
+
const tenant = this.bootstrap.claims?.tenant_id || "_";
|
|
1636
|
+
return `audit-trail:${suffix}:${sub}:${tenant}`;
|
|
1637
|
+
}
|
|
1638
|
+
// ---- bootstrap ----
|
|
1639
|
+
start() {
|
|
1640
|
+
this.disposeModals();
|
|
1641
|
+
this.cleanupStores();
|
|
1642
|
+
this.bootstrapAbort?.abort();
|
|
1643
|
+
this.bootstrapAbort = null;
|
|
1644
|
+
const token = this.getAttribute("token");
|
|
1645
|
+
if (token) {
|
|
1646
|
+
this.setReady(token);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (!this.getAttribute("token-endpoint") && !this.onTokenExpired) {
|
|
1650
|
+
this.bootstrap = { phase: "error", reason: "config" };
|
|
1651
|
+
this.render();
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
this.bootstrap = { phase: "loading" };
|
|
1655
|
+
this.render();
|
|
1656
|
+
const ctrl = new AbortController();
|
|
1657
|
+
this.bootstrapAbort = ctrl;
|
|
1658
|
+
void (async () => {
|
|
1659
|
+
const t = await fetchTokenViaOpts({
|
|
1660
|
+
tokenEndpoint: this.getAttribute("token-endpoint") ?? void 0,
|
|
1661
|
+
onTokenExpired: this.onTokenExpired
|
|
1662
|
+
});
|
|
1663
|
+
if (ctrl.signal.aborted) return;
|
|
1664
|
+
if (!t) {
|
|
1665
|
+
this.bootstrap = { phase: "error", reason: "fetch" };
|
|
1666
|
+
this.render();
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
this.setReady(t);
|
|
1670
|
+
})();
|
|
1671
|
+
}
|
|
1672
|
+
setReady(token) {
|
|
1673
|
+
const claims = parseClaims(token);
|
|
1674
|
+
this.bootstrap = { phase: "ready", token, claims };
|
|
1675
|
+
if (claims?.columns && claims.columns.length > 0) {
|
|
1676
|
+
this.visibleSet = new Set(claims.columns);
|
|
1677
|
+
} else {
|
|
1678
|
+
this.visibleSet = new Set(DEFAULT_VISIBLE_COLUMNS);
|
|
1679
|
+
}
|
|
1680
|
+
this.restoreHiddenColumns();
|
|
1681
|
+
this.restoreFilters();
|
|
1682
|
+
this.activeTab = pickInitialTab(this.filters, claims);
|
|
1683
|
+
this.startEventsStore(token);
|
|
1684
|
+
this.startDistinctStore(token);
|
|
1685
|
+
this.render();
|
|
1686
|
+
}
|
|
1687
|
+
startEventsStore(token) {
|
|
1688
|
+
this.eventsUnsub?.();
|
|
1689
|
+
this.eventsStore?.dispose();
|
|
1690
|
+
const { since, before } = resolveTimeBounds(this.filters);
|
|
1691
|
+
const apiBase = this.apiBaseAttr();
|
|
1692
|
+
const tokenEndpoint = this.getAttribute("token-endpoint") ?? void 0;
|
|
1693
|
+
const events = createEventsStore({
|
|
1694
|
+
apiBase,
|
|
1695
|
+
token,
|
|
1696
|
+
pageSize: this.pageSizeAttr(),
|
|
1697
|
+
pollInterval: this.pollIntervalAttr(),
|
|
1698
|
+
tokenEndpoint,
|
|
1699
|
+
onTokenExpired: this.onTokenExpired,
|
|
1700
|
+
onError: (err) => this.dispatchError(err),
|
|
1701
|
+
since,
|
|
1702
|
+
before,
|
|
1703
|
+
action: this.filters.action,
|
|
1704
|
+
actor: this.filters.actor,
|
|
1705
|
+
actorType: this.filters.actorType,
|
|
1706
|
+
tenantId: this.filters.tenantId,
|
|
1707
|
+
targetType: this.filters.targetType,
|
|
1708
|
+
targetId: this.filters.targetId,
|
|
1709
|
+
resultStatus: this.filters.resultStatus,
|
|
1710
|
+
originIP: this.filters.originIP,
|
|
1711
|
+
q: this.filters.q
|
|
1712
|
+
});
|
|
1713
|
+
this.eventsStore = events;
|
|
1714
|
+
this.eventsState = events.getSnapshot();
|
|
1715
|
+
this.eventsUnsub = events.subscribe(() => {
|
|
1716
|
+
this.eventsState = events.getSnapshot();
|
|
1717
|
+
this.renderBody();
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
startDistinctStore(token) {
|
|
1721
|
+
this.distinctUnsub?.();
|
|
1722
|
+
this.distinctStore?.dispose();
|
|
1723
|
+
const apiBase = this.apiBaseAttr();
|
|
1724
|
+
const tokenEndpoint = this.getAttribute("token-endpoint") ?? void 0;
|
|
1725
|
+
const distinct = createDistinctValuesStore({
|
|
1726
|
+
apiBase,
|
|
1727
|
+
token,
|
|
1728
|
+
tokenEndpoint,
|
|
1729
|
+
onTokenExpired: this.onTokenExpired
|
|
1730
|
+
});
|
|
1731
|
+
this.distinctStore = distinct;
|
|
1732
|
+
this.distinctValues = distinct.getSnapshot();
|
|
1733
|
+
this.distinctUnsub = distinct.subscribe(() => {
|
|
1734
|
+
this.distinctValues = distinct.getSnapshot();
|
|
1735
|
+
this.render();
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
restartEventsStore() {
|
|
1739
|
+
if (this.bootstrap.phase !== "ready") return;
|
|
1740
|
+
this.startEventsStore(this.bootstrap.token);
|
|
1741
|
+
}
|
|
1742
|
+
cleanupStores() {
|
|
1743
|
+
this.eventsUnsub?.();
|
|
1744
|
+
this.distinctUnsub?.();
|
|
1745
|
+
this.eventsStore?.dispose();
|
|
1746
|
+
this.distinctStore?.dispose();
|
|
1747
|
+
this.eventsUnsub = null;
|
|
1748
|
+
this.distinctUnsub = null;
|
|
1749
|
+
this.eventsStore = null;
|
|
1750
|
+
this.distinctStore = null;
|
|
1751
|
+
this.eventsState = null;
|
|
1752
|
+
this.distinctValues = {
|
|
1753
|
+
actions: [],
|
|
1754
|
+
actorTypes: [],
|
|
1755
|
+
targetTypes: [],
|
|
1756
|
+
tenants: [],
|
|
1757
|
+
resultStatuses: [],
|
|
1758
|
+
metadataKeys: [],
|
|
1759
|
+
changeFields: []
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
cleanup() {
|
|
1763
|
+
this.bootstrapAbort?.abort();
|
|
1764
|
+
this.bootstrapAbort = null;
|
|
1765
|
+
this.nlpAbort?.abort();
|
|
1766
|
+
this.nlpAbort = null;
|
|
1767
|
+
this.disposeModals();
|
|
1768
|
+
this.cleanupStores();
|
|
1769
|
+
}
|
|
1770
|
+
startRelativeTimeTimer() {
|
|
1771
|
+
if (this.relativeTimeTimer !== null) return;
|
|
1772
|
+
this.relativeTimeTimer = setInterval(
|
|
1773
|
+
() => this.tickRelativeTimes(),
|
|
1774
|
+
RELATIVE_TIME_TICK_MS
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
stopRelativeTimeTimer() {
|
|
1778
|
+
if (this.relativeTimeTimer === null) return;
|
|
1779
|
+
clearInterval(this.relativeTimeTimer);
|
|
1780
|
+
this.relativeTimeTimer = null;
|
|
1781
|
+
}
|
|
1782
|
+
tickRelativeTimes() {
|
|
1783
|
+
const cells = this.querySelectorAll("time[data-occurred-at]");
|
|
1784
|
+
if (cells.length === 0) return;
|
|
1785
|
+
const now = Date.now();
|
|
1786
|
+
cells.forEach((c) => {
|
|
1787
|
+
const iso = c.getAttribute("data-occurred-at");
|
|
1788
|
+
if (!iso) return;
|
|
1789
|
+
c.textContent = formatTimeCell(iso, now);
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
disposeModals() {
|
|
1793
|
+
this.detailDispose?.();
|
|
1794
|
+
this.detailDispose = null;
|
|
1795
|
+
this.exportDispose?.();
|
|
1796
|
+
this.exportDispose = null;
|
|
1797
|
+
}
|
|
1798
|
+
openDetail(event) {
|
|
1799
|
+
this.detailDispose?.();
|
|
1800
|
+
this.detailDispose = openEventDetail({
|
|
1801
|
+
event,
|
|
1802
|
+
theme: this.themeAttr(),
|
|
1803
|
+
onClose: () => {
|
|
1804
|
+
this.detailDispose = null;
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
openExport() {
|
|
1809
|
+
this.exportDispose?.();
|
|
1810
|
+
this.exportDispose = openExportModal({
|
|
1811
|
+
theme: this.themeAttr(),
|
|
1812
|
+
onDownload: (format) => this.runExport(format),
|
|
1813
|
+
onClose: () => {
|
|
1814
|
+
this.exportDispose = null;
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
async runExport(format) {
|
|
1819
|
+
if (this.bootstrap.phase !== "ready") throw new Error("Not authenticated.");
|
|
1820
|
+
const apiBase = this.apiBaseAttr();
|
|
1821
|
+
const tokenEndpoint = this.getAttribute("token-endpoint") ?? void 0;
|
|
1822
|
+
const { since, before } = resolveTimeBounds(this.filters);
|
|
1823
|
+
const params = {
|
|
1824
|
+
format,
|
|
1825
|
+
since,
|
|
1826
|
+
before,
|
|
1827
|
+
action: this.filters.action,
|
|
1828
|
+
actor: this.filters.actor,
|
|
1829
|
+
actorType: this.filters.actorType,
|
|
1830
|
+
tenantId: this.filters.tenantId,
|
|
1831
|
+
targetType: this.filters.targetType,
|
|
1832
|
+
targetId: this.filters.targetId,
|
|
1833
|
+
resultStatus: this.filters.resultStatus,
|
|
1834
|
+
originIP: this.filters.originIP,
|
|
1835
|
+
q: this.filters.q
|
|
1836
|
+
};
|
|
1837
|
+
const run = (token) => exportEvents({ apiBase, token, params });
|
|
1838
|
+
let result;
|
|
1839
|
+
try {
|
|
1840
|
+
result = await run(this.bootstrap.token);
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
if (err instanceof EmbedError && err.kind === "unauthorized") {
|
|
1843
|
+
const refreshed = await fetchTokenViaOpts({
|
|
1844
|
+
tokenEndpoint,
|
|
1845
|
+
onTokenExpired: this.onTokenExpired
|
|
1846
|
+
});
|
|
1847
|
+
if (!refreshed) throw new Error("Authentication failed.");
|
|
1848
|
+
result = await run(refreshed);
|
|
1849
|
+
} else if (err instanceof EmbedError) {
|
|
1850
|
+
throw new Error(exportErrorMessage(err));
|
|
1851
|
+
} else {
|
|
1852
|
+
throw err;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
triggerBrowserDownload(result.blob, result.filename);
|
|
1856
|
+
}
|
|
1857
|
+
dispatchError(err) {
|
|
1858
|
+
this.dispatchEvent(
|
|
1859
|
+
new CustomEvent("audit-trail-error", {
|
|
1860
|
+
detail: { error: err },
|
|
1861
|
+
bubbles: true
|
|
1862
|
+
})
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
// ---- columns ----
|
|
1866
|
+
get availableColumns() {
|
|
1867
|
+
if (this.bootstrap.phase !== "ready") return ALL_COLUMNS;
|
|
1868
|
+
const cols = this.bootstrap.claims?.columns;
|
|
1869
|
+
if (cols && cols.length > 0) return cols;
|
|
1870
|
+
return ALL_COLUMNS;
|
|
1871
|
+
}
|
|
1872
|
+
get visibleColumns() {
|
|
1873
|
+
return this.availableColumns.filter((c) => this.visibleSet.has(c));
|
|
1874
|
+
}
|
|
1875
|
+
toggleColumn(col) {
|
|
1876
|
+
if (this.visibleSet.has(col)) this.visibleSet.delete(col);
|
|
1877
|
+
else this.visibleSet.add(col);
|
|
1878
|
+
this.persistHiddenColumns();
|
|
1879
|
+
this.render();
|
|
1880
|
+
}
|
|
1881
|
+
restoreHiddenColumns() {
|
|
1882
|
+
const key = this.storageKey("cols");
|
|
1883
|
+
if (!key) return;
|
|
1884
|
+
try {
|
|
1885
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
1886
|
+
if (!raw) return;
|
|
1887
|
+
const hidden = JSON.parse(raw);
|
|
1888
|
+
if (!Array.isArray(hidden)) return;
|
|
1889
|
+
for (const c of hidden) {
|
|
1890
|
+
if (typeof c === "string") this.visibleSet.delete(c);
|
|
1891
|
+
}
|
|
1892
|
+
} catch {
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
persistHiddenColumns() {
|
|
1896
|
+
const key = this.storageKey("cols");
|
|
1897
|
+
if (!key) return;
|
|
1898
|
+
try {
|
|
1899
|
+
const hidden = this.availableColumns.filter((c) => !this.visibleSet.has(c));
|
|
1900
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(hidden));
|
|
1901
|
+
} catch {
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
restoreFilters() {
|
|
1905
|
+
const key = this.storageKey("filters");
|
|
1906
|
+
if (!key) return;
|
|
1907
|
+
try {
|
|
1908
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
1909
|
+
if (!raw) return;
|
|
1910
|
+
const parsed = JSON.parse(raw);
|
|
1911
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
1912
|
+
const range = parsed.range;
|
|
1913
|
+
if (range !== "24h" && range !== "7d" && range !== "30d" && range !== "custom" && range !== "all") return;
|
|
1914
|
+
this.filters = parsed;
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
persistFilters() {
|
|
1919
|
+
const key = this.storageKey("filters");
|
|
1920
|
+
if (!key) return;
|
|
1921
|
+
try {
|
|
1922
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(this.filters));
|
|
1923
|
+
} catch {
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
// ---- filters ----
|
|
1927
|
+
setFilters(next) {
|
|
1928
|
+
this.filters = next;
|
|
1929
|
+
this.persistFilters();
|
|
1930
|
+
this.render();
|
|
1931
|
+
this.restartEventsStore();
|
|
1932
|
+
}
|
|
1933
|
+
setActiveTab(next) {
|
|
1934
|
+
if (this.activeTab === next) return;
|
|
1935
|
+
this.activeTab = next;
|
|
1936
|
+
this.render();
|
|
1937
|
+
}
|
|
1938
|
+
// submitNLP fires the LLM round-trip via the embed NLP endpoint.
|
|
1939
|
+
// Updates nlpState immediately for the spinner, then folds the
|
|
1940
|
+
// result into filters on success or surfaces an error on failure.
|
|
1941
|
+
submitNLP(query) {
|
|
1942
|
+
if (this.bootstrap.phase !== "ready") return;
|
|
1943
|
+
const trimmed = query.trim();
|
|
1944
|
+
if (!trimmed) return;
|
|
1945
|
+
const apiBase = this.apiBaseAttr();
|
|
1946
|
+
const token = this.bootstrap.token;
|
|
1947
|
+
this.nlpAbort?.abort();
|
|
1948
|
+
const ctrl = new AbortController();
|
|
1949
|
+
this.nlpAbort = ctrl;
|
|
1950
|
+
this.nlpState = { phase: "loading" };
|
|
1951
|
+
this.render();
|
|
1952
|
+
void (async () => {
|
|
1953
|
+
try {
|
|
1954
|
+
const result = await generateNLPFilters({
|
|
1955
|
+
apiBase,
|
|
1956
|
+
token,
|
|
1957
|
+
query: trimmed,
|
|
1958
|
+
signal: ctrl.signal
|
|
1959
|
+
});
|
|
1960
|
+
if (ctrl.signal.aborted) return;
|
|
1961
|
+
this.nlpState = { phase: "idle" };
|
|
1962
|
+
this.filters = {
|
|
1963
|
+
...this.filters,
|
|
1964
|
+
q: result.dsl || void 0,
|
|
1965
|
+
nlpQ: trimmed,
|
|
1966
|
+
nlpExplanation: result.explanation,
|
|
1967
|
+
nlpUnsupported: result.unsupported
|
|
1968
|
+
};
|
|
1969
|
+
this.persistFilters();
|
|
1970
|
+
this.render();
|
|
1971
|
+
this.restartEventsStore();
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
1974
|
+
if (err instanceof EmbedError && err.kind === "unauthorized") {
|
|
1975
|
+
const refreshed = await fetchTokenViaOpts({
|
|
1976
|
+
tokenEndpoint: this.getAttribute("token-endpoint") ?? void 0,
|
|
1977
|
+
onTokenExpired: this.onTokenExpired
|
|
1978
|
+
});
|
|
1979
|
+
if (refreshed && !ctrl.signal.aborted) {
|
|
1980
|
+
try {
|
|
1981
|
+
const retry = await generateNLPFilters({
|
|
1982
|
+
apiBase,
|
|
1983
|
+
token: refreshed,
|
|
1984
|
+
query: trimmed,
|
|
1985
|
+
signal: ctrl.signal
|
|
1986
|
+
});
|
|
1987
|
+
this.nlpState = { phase: "idle" };
|
|
1988
|
+
this.filters = {
|
|
1989
|
+
...this.filters,
|
|
1990
|
+
q: retry.dsl || void 0,
|
|
1991
|
+
nlpQ: trimmed,
|
|
1992
|
+
nlpExplanation: retry.explanation,
|
|
1993
|
+
nlpUnsupported: retry.unsupported
|
|
1994
|
+
};
|
|
1995
|
+
this.persistFilters();
|
|
1996
|
+
this.render();
|
|
1997
|
+
this.restartEventsStore();
|
|
1998
|
+
return;
|
|
1999
|
+
} catch (retryErr) {
|
|
2000
|
+
this.nlpState = {
|
|
2001
|
+
phase: "error",
|
|
2002
|
+
reason: classifyNLPError(retryErr)
|
|
2003
|
+
};
|
|
2004
|
+
this.render();
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
this.nlpState = { phase: "error", reason: classifyNLPError(err) };
|
|
2010
|
+
this.render();
|
|
2011
|
+
}
|
|
2012
|
+
})();
|
|
2013
|
+
}
|
|
2014
|
+
// ---- rendering ----
|
|
2015
|
+
render() {
|
|
2016
|
+
this.bodySlot = null;
|
|
2017
|
+
this.replaceChildren(...this.renderChildren());
|
|
2018
|
+
}
|
|
2019
|
+
renderBody() {
|
|
2020
|
+
if (!this.bodySlot) return;
|
|
2021
|
+
this.bodySlot.replaceChildren(...this.renderBodyChildren());
|
|
2022
|
+
}
|
|
2023
|
+
renderChildren() {
|
|
2024
|
+
if (this.bootstrap.phase === "error" && this.bootstrap.reason === "config") {
|
|
2025
|
+
return [
|
|
2026
|
+
this.stateElement(
|
|
2027
|
+
"error",
|
|
2028
|
+
h(
|
|
2029
|
+
"span",
|
|
2030
|
+
null,
|
|
2031
|
+
"Configure ",
|
|
2032
|
+
h("code", null, "token"),
|
|
2033
|
+
", ",
|
|
2034
|
+
h("code", null, "token-endpoint"),
|
|
2035
|
+
", or set the ",
|
|
2036
|
+
h("code", null, "onTokenExpired"),
|
|
2037
|
+
" property."
|
|
2038
|
+
)
|
|
2039
|
+
)
|
|
2040
|
+
];
|
|
2041
|
+
}
|
|
2042
|
+
if (this.bootstrap.phase === "error" && this.bootstrap.reason === "fetch") {
|
|
2043
|
+
const retry = h("button", { type: "button", class: "audit-trail-button" }, "Retry");
|
|
2044
|
+
retry.addEventListener("click", () => this.start());
|
|
2045
|
+
return [this.stateElement("error", h("span", null, "Couldn\u2019t fetch token."), retry)];
|
|
2046
|
+
}
|
|
2047
|
+
if (this.bootstrap.phase !== "ready") {
|
|
2048
|
+
return [this.stateElement("loading", "Loading\u2026")];
|
|
2049
|
+
}
|
|
2050
|
+
if (this.bootstrap.claims === null) {
|
|
2051
|
+
return [this.stateElement("error", "Invalid token.")];
|
|
2052
|
+
}
|
|
2053
|
+
const out = [this.renderToolbar()];
|
|
2054
|
+
const chips = renderFilterChips({
|
|
2055
|
+
value: this.filters,
|
|
2056
|
+
onChange: (next) => this.setFilters(next)
|
|
2057
|
+
});
|
|
2058
|
+
if (chips) out.push(chips);
|
|
2059
|
+
if (this.filtersOpen) {
|
|
2060
|
+
out.push(
|
|
2061
|
+
renderFiltersPanel({
|
|
2062
|
+
value: this.filters,
|
|
2063
|
+
distinct: this.distinctValues,
|
|
2064
|
+
claims: this.bootstrap.claims,
|
|
2065
|
+
activeTab: this.activeTab,
|
|
2066
|
+
nlpState: this.nlpState,
|
|
2067
|
+
onTabChange: (next) => this.setActiveTab(next),
|
|
2068
|
+
onChange: (next) => this.setFilters(next),
|
|
2069
|
+
onNLPSubmit: (q) => this.submitNLP(q),
|
|
2070
|
+
onApplied: () => {
|
|
2071
|
+
this.filtersOpen = false;
|
|
2072
|
+
this.render();
|
|
2073
|
+
}
|
|
2074
|
+
})
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
const body = h("div", { class: "audit-trail-body" });
|
|
2078
|
+
body.replaceChildren(...this.renderBodyChildren());
|
|
2079
|
+
this.bodySlot = body;
|
|
2080
|
+
out.push(body);
|
|
2081
|
+
return out;
|
|
2082
|
+
}
|
|
2083
|
+
renderToolbar() {
|
|
2084
|
+
const livePollActive = this.pollIntervalAttr() > 0 && !resolveTimeBounds(this.filters).before;
|
|
2085
|
+
const live = livePollActive ? this.renderLiveIndicator() : null;
|
|
2086
|
+
const spacer = h("span", { class: "audit-trail-toolbar-spacer" });
|
|
2087
|
+
const filtersToggle = this.renderFiltersToggle();
|
|
2088
|
+
const exportBtn = h(
|
|
2089
|
+
"button",
|
|
2090
|
+
{ type: "button", class: "audit-trail-button" },
|
|
2091
|
+
"Export"
|
|
2092
|
+
);
|
|
2093
|
+
exportBtn.addEventListener("click", () => this.openExport());
|
|
2094
|
+
const picker = this.renderColumnPicker();
|
|
2095
|
+
const children = [];
|
|
2096
|
+
if (live) children.push(live);
|
|
2097
|
+
children.push(spacer, filtersToggle, exportBtn, picker);
|
|
2098
|
+
return h("div", { class: "audit-trail-toolbar" }, ...children);
|
|
2099
|
+
}
|
|
2100
|
+
renderLiveIndicator() {
|
|
2101
|
+
return h(
|
|
2102
|
+
"span",
|
|
2103
|
+
{
|
|
2104
|
+
class: "audit-trail-live",
|
|
2105
|
+
"aria-label": "Live updates",
|
|
2106
|
+
title: "Live updates"
|
|
2107
|
+
},
|
|
2108
|
+
h("span", { class: "audit-trail-live-dot", "aria-hidden": "true" }),
|
|
2109
|
+
h("span", null, "Live")
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
renderFiltersToggle() {
|
|
2113
|
+
const activeCount = countActiveColumnFilters(this.filters) + (this.filters.q ? parseQClauses(this.filters.q).length : 0);
|
|
2114
|
+
const children = [document.createTextNode("Search")];
|
|
2115
|
+
if (activeCount > 0) {
|
|
2116
|
+
children.push(
|
|
2117
|
+
h("span", { class: "audit-trail-filter-toggle-badge" }, String(activeCount))
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
children.push(
|
|
2121
|
+
h(
|
|
2122
|
+
"span",
|
|
2123
|
+
{ class: "audit-trail-filter-toggle-caret", "aria-hidden": "true" },
|
|
2124
|
+
this.filtersOpen ? "\u25B4" : "\u25BE"
|
|
2125
|
+
)
|
|
2126
|
+
);
|
|
2127
|
+
const btn = h(
|
|
2128
|
+
"button",
|
|
2129
|
+
{
|
|
2130
|
+
type: "button",
|
|
2131
|
+
class: "audit-trail-filter-toggle",
|
|
2132
|
+
"aria-expanded": this.filtersOpen ? "true" : "false"
|
|
2133
|
+
},
|
|
2134
|
+
...children
|
|
2135
|
+
);
|
|
2136
|
+
btn.addEventListener("click", () => {
|
|
2137
|
+
this.filtersOpen = !this.filtersOpen;
|
|
2138
|
+
this.render();
|
|
2139
|
+
});
|
|
2140
|
+
return btn;
|
|
2141
|
+
}
|
|
2142
|
+
renderColumnPicker() {
|
|
2143
|
+
const available = this.availableColumns;
|
|
2144
|
+
const visibleCount = available.reduce(
|
|
2145
|
+
(n, c) => this.visibleSet.has(c) ? n + 1 : n,
|
|
2146
|
+
0
|
|
2147
|
+
);
|
|
2148
|
+
const items = available.map((col) => {
|
|
2149
|
+
const checkbox = h("input", { type: "checkbox" });
|
|
2150
|
+
checkbox.checked = this.visibleSet.has(col);
|
|
2151
|
+
checkbox.addEventListener("change", () => this.toggleColumn(col));
|
|
2152
|
+
return h(
|
|
2153
|
+
"label",
|
|
2154
|
+
{ class: "audit-trail-picker-item" },
|
|
2155
|
+
checkbox,
|
|
2156
|
+
h("span", null, COLUMN_LABELS[col] ?? col)
|
|
2157
|
+
);
|
|
2158
|
+
});
|
|
2159
|
+
return h(
|
|
2160
|
+
"details",
|
|
2161
|
+
{ class: "audit-trail-picker" },
|
|
2162
|
+
h(
|
|
2163
|
+
"summary",
|
|
2164
|
+
{ class: "audit-trail-picker-summary" },
|
|
2165
|
+
`Columns (${visibleCount}/${available.length})`
|
|
2166
|
+
),
|
|
2167
|
+
h("div", { class: "audit-trail-picker-content" }, ...items)
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
renderBodyChildren() {
|
|
2171
|
+
const events = this.eventsState?.events ?? [];
|
|
2172
|
+
const status = this.eventsState?.status ?? "loading";
|
|
2173
|
+
const hasMore = (this.eventsState?.nextCursor ?? null) !== null;
|
|
2174
|
+
const cols = this.visibleColumns;
|
|
2175
|
+
if (status === "loading" && events.length === 0) {
|
|
2176
|
+
return [this.stateElement("loading", "Loading\u2026")];
|
|
2177
|
+
}
|
|
2178
|
+
if (status === "expired") {
|
|
2179
|
+
return [this.stateElement("error", "Session expired.")];
|
|
2180
|
+
}
|
|
2181
|
+
if (status === "error") {
|
|
2182
|
+
const retry = h("button", { type: "button", class: "audit-trail-button" }, "Retry");
|
|
2183
|
+
retry.addEventListener("click", () => this.eventsStore?.refresh());
|
|
2184
|
+
return [this.stateElement("error", "Could not load events.", retry)];
|
|
2185
|
+
}
|
|
2186
|
+
if (events.length === 0) {
|
|
2187
|
+
return [this.stateElement("empty", "No events match.")];
|
|
2188
|
+
}
|
|
2189
|
+
if (cols.length === 0) {
|
|
2190
|
+
return [this.stateElement("empty", "No columns selected.")];
|
|
2191
|
+
}
|
|
2192
|
+
const out = [renderTable(events, cols, (event) => this.openDetail(event))];
|
|
2193
|
+
if (hasMore) {
|
|
2194
|
+
const more = h(
|
|
2195
|
+
"button",
|
|
2196
|
+
{ type: "button", class: "audit-trail-button audit-trail-load-more" },
|
|
2197
|
+
"Load more"
|
|
2198
|
+
);
|
|
2199
|
+
more.addEventListener("click", () => this.eventsStore?.loadMore());
|
|
2200
|
+
out.push(more);
|
|
2201
|
+
}
|
|
2202
|
+
return out;
|
|
2203
|
+
}
|
|
2204
|
+
stateElement(kind, ...children) {
|
|
2205
|
+
return h("div", { class: `audit-trail-state audit-trail-state-${kind}` }, ...children);
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
function triggerBrowserDownload(blob, filename) {
|
|
2209
|
+
const url = URL.createObjectURL(blob);
|
|
2210
|
+
const a = document.createElement("a");
|
|
2211
|
+
a.href = url;
|
|
2212
|
+
a.download = filename;
|
|
2213
|
+
document.body.appendChild(a);
|
|
2214
|
+
a.click();
|
|
2215
|
+
document.body.removeChild(a);
|
|
2216
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
2217
|
+
}
|
|
2218
|
+
function classifyNLPError(err) {
|
|
2219
|
+
if (!(err instanceof EmbedError)) return "unknown";
|
|
2220
|
+
if (err.status === 503) {
|
|
2221
|
+
if (err.message?.includes("busy")) return "provider_busy";
|
|
2222
|
+
return "not_configured";
|
|
2223
|
+
}
|
|
2224
|
+
if (err.status === 429) return "rate_limited";
|
|
2225
|
+
if (err.status === 403) return "not_allowed";
|
|
2226
|
+
if (err.kind === "bad_request") return "bad_request";
|
|
2227
|
+
return "unknown";
|
|
2228
|
+
}
|
|
2229
|
+
function exportErrorMessage(err) {
|
|
2230
|
+
switch (err.kind) {
|
|
2231
|
+
case "rate_limited":
|
|
2232
|
+
return "Too many requests. Try again in a moment.";
|
|
2233
|
+
case "bad_request":
|
|
2234
|
+
return err.message || "Bad request.";
|
|
2235
|
+
case "server":
|
|
2236
|
+
return "Server error. Please try again.";
|
|
2237
|
+
case "network":
|
|
2238
|
+
return "Network error. Check your connection.";
|
|
2239
|
+
case "not_found":
|
|
2240
|
+
return "Not found.";
|
|
2241
|
+
case "unauthorized":
|
|
2242
|
+
return "Authentication failed.";
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// src/index.ts
|
|
2247
|
+
if (typeof customElements !== "undefined" && !customElements.get("audit-trail")) {
|
|
2248
|
+
customElements.define("audit-trail", AuditTrailElement);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
export { AuditTrailElement };
|
|
2252
|
+
//# sourceMappingURL=index.js.map
|
|
2253
|
+
//# sourceMappingURL=index.js.map
|