@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,7 @@
|
|
|
1
|
+
// Aggregation model over the timeline event contract: grouping, faceting,
|
|
2
|
+
// CSV export, and the calendar-density window — all pure and DOM-free.
|
|
3
|
+
export * from "./types.js";
|
|
4
|
+
// Grouped, faceted, drillable audit surface
|
|
5
|
+
export * from "./audit-log.js";
|
|
6
|
+
// Managerial throughput heatmap
|
|
7
|
+
export * from "./activity-density.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const escapeHtml: (value: string) => string;
|
|
2
|
+
export declare const auditToneColor: (tone: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* Format an event instant for a row. Rendered in UTC to match the UTC day
|
|
5
|
+
* grouping — a row must never appear to sit outside the day section holding
|
|
6
|
+
* it, which is exactly what a locale-local time would produce near midnight.
|
|
7
|
+
*/
|
|
8
|
+
export declare const formatAuditTimestamp: (value: string | undefined) => string;
|
|
9
|
+
/** Reference time for deterministic day labels; falls back to the wall clock. */
|
|
10
|
+
export declare const resolveReferenceTime: (raw: string | null) => Date;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { formatUtcDay } from "./types.js";
|
|
2
|
+
// Internal helpers shared by the two audit projections. Kept in one place so
|
|
3
|
+
// escaping and formatting can never drift between the log and the density
|
|
4
|
+
// strip — they render the same records.
|
|
5
|
+
export const escapeHtml = (value) => value
|
|
6
|
+
.replaceAll("&", "&")
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll('"', """)
|
|
10
|
+
.replaceAll("'", "'");
|
|
11
|
+
export const auditToneColor = (tone) => {
|
|
12
|
+
switch (tone) {
|
|
13
|
+
case "brand":
|
|
14
|
+
return "var(--boe-token-surface-surface-brand, #0061d5)";
|
|
15
|
+
case "success":
|
|
16
|
+
return "var(--boe-token-surface-status-surface-success, #26a27b)";
|
|
17
|
+
case "warning":
|
|
18
|
+
return "var(--boe-token-surface-status-surface-warning, #f5b31b)";
|
|
19
|
+
case "error":
|
|
20
|
+
return "var(--boe-token-surface-status-surface-error, #ed3757)";
|
|
21
|
+
default:
|
|
22
|
+
return "var(--boe-token-text-text-secondary, #6f6f6f)";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
26
|
+
/**
|
|
27
|
+
* Format an event instant for a row. Rendered in UTC to match the UTC day
|
|
28
|
+
* grouping — a row must never appear to sit outside the day section holding
|
|
29
|
+
* it, which is exactly what a locale-local time would produce near midnight.
|
|
30
|
+
*/
|
|
31
|
+
export const formatAuditTimestamp = (value) => {
|
|
32
|
+
if (!value) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
const date = new Date(value);
|
|
36
|
+
if (Number.isNaN(date.getTime())) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
return `${formatUtcDay(date)}, ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())} UTC`;
|
|
40
|
+
};
|
|
41
|
+
/** Reference time for deterministic day labels; falls back to the wall clock. */
|
|
42
|
+
export const resolveReferenceTime = (raw) => {
|
|
43
|
+
if (raw) {
|
|
44
|
+
const parsed = new Date(raw);
|
|
45
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return new Date();
|
|
50
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { TimelineEvent } from "../timeline/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The audit layer reads the same append-only event contract `box-timeline`
|
|
4
|
+
* renders. The timeline is the flat feed; this pattern is the aggregation,
|
|
5
|
+
* faceting, drill-down, and export layer over identical records, so a host
|
|
6
|
+
* feeds both surfaces from one source without a second model.
|
|
7
|
+
*/
|
|
8
|
+
export type AuditEvent = TimelineEvent;
|
|
9
|
+
/** The dimension sections are grouped by. */
|
|
10
|
+
export type AuditGroupBy = "day" | "actor" | "action";
|
|
11
|
+
export declare const AUDIT_GROUP_BY_VALUES: readonly AuditGroupBy[];
|
|
12
|
+
export declare const AUDIT_GROUP_BY_LABELS: Record<AuditGroupBy, string>;
|
|
13
|
+
export declare const resolveAuditGroupBy: (value: string | null | undefined) => AuditGroupBy;
|
|
14
|
+
/**
|
|
15
|
+
* Facet selection. Every facet is an exact match except `from`/`to`, which
|
|
16
|
+
* bound the event instant inclusively.
|
|
17
|
+
*/
|
|
18
|
+
export interface AuditFacets {
|
|
19
|
+
/** Matches `actor.name` exactly. */
|
|
20
|
+
actor?: string;
|
|
21
|
+
/** Matches `action` exactly. */
|
|
22
|
+
action?: string;
|
|
23
|
+
/** The workflow-run drill-down. */
|
|
24
|
+
correlationId?: string;
|
|
25
|
+
/** Inclusive ISO lower bound. */
|
|
26
|
+
from?: string;
|
|
27
|
+
/** Inclusive ISO upper bound. */
|
|
28
|
+
to?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface AuditGroup {
|
|
31
|
+
/** Stable identity for the section — the collapse state is keyed by it. */
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
events: AuditEvent[];
|
|
35
|
+
count: number;
|
|
36
|
+
/** Distinct attributed actors in the group; 0 when every event is unattributed. */
|
|
37
|
+
actorCount: number;
|
|
38
|
+
}
|
|
39
|
+
export interface AuditFacetValue {
|
|
40
|
+
value: string;
|
|
41
|
+
label: string;
|
|
42
|
+
count: number;
|
|
43
|
+
}
|
|
44
|
+
export interface AuditFacetSummary {
|
|
45
|
+
actors: AuditFacetValue[];
|
|
46
|
+
actions: AuditFacetValue[];
|
|
47
|
+
}
|
|
48
|
+
export declare const AUDIT_UNDATED_KEY = "undated";
|
|
49
|
+
export declare const AUDIT_UNATTRIBUTED_KEY = "";
|
|
50
|
+
/**
|
|
51
|
+
* The event's instant in epoch milliseconds, or `null` when it carries no
|
|
52
|
+
* usable timestamp. Undated events are a real case in imported audit data —
|
|
53
|
+
* they sort last rather than being dropped.
|
|
54
|
+
*/
|
|
55
|
+
export declare const auditEventInstant: (event: AuditEvent) => number | null;
|
|
56
|
+
/**
|
|
57
|
+
* The UTC calendar day (`YYYY-MM-DD`) an event belongs to, or `null` when it
|
|
58
|
+
* is undated. Day keys and day labels are both derived in UTC so a viewer's
|
|
59
|
+
* timezone can never split one audit day across two sections. A host that
|
|
60
|
+
* wants local-day semantics offsets the timestamps it supplies.
|
|
61
|
+
*/
|
|
62
|
+
export declare const resolveAuditDay: (event: AuditEvent) => string | null;
|
|
63
|
+
/**
|
|
64
|
+
* The one absolute-day formatter — `Aug 13, 2026`, always in UTC. Accepts a
|
|
65
|
+
* `YYYY-MM-DD` key or an instant. Every audit surface formats days through
|
|
66
|
+
* this, so a rendered date can never disagree with the UTC day key that
|
|
67
|
+
* grouped it. An unparseable key is returned unchanged.
|
|
68
|
+
*/
|
|
69
|
+
export declare const formatUtcDay: (value: Date | string) => string;
|
|
70
|
+
/** The short UTC month name — `Aug` — for sparse calendar column headers. */
|
|
71
|
+
export declare const formatUtcMonth: (value: Date | string) => string;
|
|
72
|
+
/**
|
|
73
|
+
* Label a `YYYY-MM-DD` day key, with Today/Yesterday relative labels resolved
|
|
74
|
+
* against the caller's reference time so the output is deterministic rather
|
|
75
|
+
* than clock-dependent.
|
|
76
|
+
*/
|
|
77
|
+
export declare const formatAuditDay: (day: string, now?: Date) => string;
|
|
78
|
+
/**
|
|
79
|
+
* Apply the facet selection. An empty string is "not selected" rather than a
|
|
80
|
+
* match on empty — the same meaning `""` carries as an unset attribute and as
|
|
81
|
+
* a select's all-values option, so the DOM and the engine never disagree.
|
|
82
|
+
*
|
|
83
|
+
* A date bound excludes undated events: an event with no timestamp cannot be
|
|
84
|
+
* shown to fall inside the window, and an audit surface must not imply it does.
|
|
85
|
+
*/
|
|
86
|
+
export declare const filterAuditEvents: (events: readonly AuditEvent[], facets: AuditFacets) => AuditEvent[];
|
|
87
|
+
/** True when any facet narrows the set — drives the Clear-filters affordance. */
|
|
88
|
+
export declare const hasAuditFacets: (facets: AuditFacets) => boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Group events into collapsible sections.
|
|
91
|
+
*
|
|
92
|
+
* Day sections run newest first with undated events in a trailing section;
|
|
93
|
+
* actor and action sections run by descending count, ties broken by label so
|
|
94
|
+
* the order never depends on input order. Events inside every section are
|
|
95
|
+
* newest first.
|
|
96
|
+
*/
|
|
97
|
+
export declare const groupAuditEvents: (events: readonly AuditEvent[], groupBy: AuditGroupBy, now?: Date) => AuditGroup[];
|
|
98
|
+
/**
|
|
99
|
+
* Available facet values with counts, derived from the *unfiltered* set so
|
|
100
|
+
* selecting one facet never removes the options for another — a filter UI
|
|
101
|
+
* that can dead-end is worse than no counts.
|
|
102
|
+
*/
|
|
103
|
+
export declare const summarizeAuditFacets: (events: readonly AuditEvent[]) => AuditFacetSummary;
|
|
104
|
+
export declare const AUDIT_CSV_COLUMNS: readonly ["id", "timestamp", "actor", "action", "summary", "badge", "correlationId", "evidence"];
|
|
105
|
+
/**
|
|
106
|
+
* Render events as RFC 4180 CSV for the export toolbar. Every field is
|
|
107
|
+
* quoted, embedded quotes are doubled, and a value that would otherwise be
|
|
108
|
+
* read as a spreadsheet formula is prefixed so it stays text.
|
|
109
|
+
*/
|
|
110
|
+
export declare const toAuditCsv: (events: readonly AuditEvent[]) => string;
|
|
111
|
+
export interface ActivityDensityCell {
|
|
112
|
+
/** UTC calendar day, `YYYY-MM-DD`. */
|
|
113
|
+
date: string;
|
|
114
|
+
count: number;
|
|
115
|
+
/** 0 for an empty day, otherwise 1–4 scaled against the busiest day in the window. */
|
|
116
|
+
level: number;
|
|
117
|
+
/** Column index, oldest week first. */
|
|
118
|
+
week: number;
|
|
119
|
+
/** Row index, 0 = Sunday. */
|
|
120
|
+
weekday: number;
|
|
121
|
+
}
|
|
122
|
+
export interface ActivityDensity {
|
|
123
|
+
cells: ActivityDensityCell[];
|
|
124
|
+
weeks: number;
|
|
125
|
+
total: number;
|
|
126
|
+
max: number;
|
|
127
|
+
/** First day in the window (the Sunday that opens the oldest column). */
|
|
128
|
+
start: string;
|
|
129
|
+
/** Last day in the window — the reference day. */
|
|
130
|
+
end: string;
|
|
131
|
+
}
|
|
132
|
+
export interface ActivityDensityOptions {
|
|
133
|
+
/** Reference day; the window ends on it. */
|
|
134
|
+
now: Date;
|
|
135
|
+
/** Number of week columns, clamped to 1–53. Defaults to 12. */
|
|
136
|
+
weeks?: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build the calendar heatmap window: whole week columns ending on the
|
|
140
|
+
* reference day's week, rows Sunday → Saturday. Pure and UTC-based, so the
|
|
141
|
+
* same events and reference time always produce the same grid.
|
|
142
|
+
*/
|
|
143
|
+
export declare const computeActivityDensity: (events: readonly AuditEvent[], options: ActivityDensityOptions) => ActivityDensity;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
export const AUDIT_GROUP_BY_VALUES = ["day", "actor", "action"];
|
|
2
|
+
export const AUDIT_GROUP_BY_LABELS = {
|
|
3
|
+
day: "Day",
|
|
4
|
+
actor: "Actor",
|
|
5
|
+
action: "Action",
|
|
6
|
+
};
|
|
7
|
+
export const resolveAuditGroupBy = (value) => value && AUDIT_GROUP_BY_VALUES.includes(value)
|
|
8
|
+
? value
|
|
9
|
+
: "day";
|
|
10
|
+
export const AUDIT_UNDATED_KEY = "undated";
|
|
11
|
+
export const AUDIT_UNATTRIBUTED_KEY = "";
|
|
12
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* The event's instant in epoch milliseconds, or `null` when it carries no
|
|
15
|
+
* usable timestamp. Undated events are a real case in imported audit data —
|
|
16
|
+
* they sort last rather than being dropped.
|
|
17
|
+
*/
|
|
18
|
+
export const auditEventInstant = (event) => {
|
|
19
|
+
if (!event.timestamp) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const value = new Date(event.timestamp).getTime();
|
|
23
|
+
return Number.isNaN(value) ? null : value;
|
|
24
|
+
};
|
|
25
|
+
/** Newest first; undated events sink to the end without reordering among themselves. */
|
|
26
|
+
const byNewestFirst = (left, right) => {
|
|
27
|
+
const a = auditEventInstant(left);
|
|
28
|
+
const b = auditEventInstant(right);
|
|
29
|
+
if (a === b) {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
if (a === null) {
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
if (b === null) {
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
return b - a;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The UTC calendar day (`YYYY-MM-DD`) an event belongs to, or `null` when it
|
|
42
|
+
* is undated. Day keys and day labels are both derived in UTC so a viewer's
|
|
43
|
+
* timezone can never split one audit day across two sections. A host that
|
|
44
|
+
* wants local-day semantics offsets the timestamps it supplies.
|
|
45
|
+
*/
|
|
46
|
+
export const resolveAuditDay = (event) => {
|
|
47
|
+
const instant = auditEventInstant(event);
|
|
48
|
+
if (instant === null) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return new Date(instant).toISOString().slice(0, 10);
|
|
52
|
+
};
|
|
53
|
+
const MONTHS = [
|
|
54
|
+
"Jan",
|
|
55
|
+
"Feb",
|
|
56
|
+
"Mar",
|
|
57
|
+
"Apr",
|
|
58
|
+
"May",
|
|
59
|
+
"Jun",
|
|
60
|
+
"Jul",
|
|
61
|
+
"Aug",
|
|
62
|
+
"Sep",
|
|
63
|
+
"Oct",
|
|
64
|
+
"Nov",
|
|
65
|
+
"Dec",
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* The one absolute-day formatter — `Aug 13, 2026`, always in UTC. Accepts a
|
|
69
|
+
* `YYYY-MM-DD` key or an instant. Every audit surface formats days through
|
|
70
|
+
* this, so a rendered date can never disagree with the UTC day key that
|
|
71
|
+
* grouped it. An unparseable key is returned unchanged.
|
|
72
|
+
*/
|
|
73
|
+
export const formatUtcDay = (value) => {
|
|
74
|
+
const date = typeof value === "string" ? new Date(`${value}T00:00:00.000Z`) : value;
|
|
75
|
+
if (Number.isNaN(date.getTime())) {
|
|
76
|
+
return typeof value === "string" ? value : "";
|
|
77
|
+
}
|
|
78
|
+
return `${MONTHS[date.getUTCMonth()]} ${String(date.getUTCDate())}, ${String(date.getUTCFullYear())}`;
|
|
79
|
+
};
|
|
80
|
+
/** The short UTC month name — `Aug` — for sparse calendar column headers. */
|
|
81
|
+
export const formatUtcMonth = (value) => {
|
|
82
|
+
const date = typeof value === "string" ? new Date(`${value}T00:00:00.000Z`) : value;
|
|
83
|
+
return Number.isNaN(date.getTime()) ? "" : MONTHS[date.getUTCMonth()];
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Label a `YYYY-MM-DD` day key, with Today/Yesterday relative labels resolved
|
|
87
|
+
* against the caller's reference time so the output is deterministic rather
|
|
88
|
+
* than clock-dependent.
|
|
89
|
+
*/
|
|
90
|
+
export const formatAuditDay = (day, now) => {
|
|
91
|
+
const parsed = new Date(`${day}T00:00:00.000Z`);
|
|
92
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
93
|
+
return day;
|
|
94
|
+
}
|
|
95
|
+
if (now) {
|
|
96
|
+
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
97
|
+
const delta = today - parsed.getTime();
|
|
98
|
+
if (delta === 0) {
|
|
99
|
+
return "Today";
|
|
100
|
+
}
|
|
101
|
+
if (delta === DAY_MS) {
|
|
102
|
+
return "Yesterday";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return formatUtcDay(parsed);
|
|
106
|
+
};
|
|
107
|
+
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a bound to an instant. A date-only bound (what `<input type="date">`
|
|
110
|
+
* produces) covers the whole UTC day, so `to: "2026-08-13"` includes
|
|
111
|
+
* everything that happened on the 13th rather than only its first
|
|
112
|
+
* millisecond. A full ISO instant is used verbatim.
|
|
113
|
+
*/
|
|
114
|
+
const resolveBound = (value, edge) => {
|
|
115
|
+
if (!value) {
|
|
116
|
+
return Number.NaN;
|
|
117
|
+
}
|
|
118
|
+
const resolved = DATE_ONLY.test(value)
|
|
119
|
+
? `${value}${edge === "start" ? "T00:00:00.000Z" : "T23:59:59.999Z"}`
|
|
120
|
+
: value;
|
|
121
|
+
return new Date(resolved).getTime();
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Apply the facet selection. An empty string is "not selected" rather than a
|
|
125
|
+
* match on empty — the same meaning `""` carries as an unset attribute and as
|
|
126
|
+
* a select's all-values option, so the DOM and the engine never disagree.
|
|
127
|
+
*
|
|
128
|
+
* A date bound excludes undated events: an event with no timestamp cannot be
|
|
129
|
+
* shown to fall inside the window, and an audit surface must not imply it does.
|
|
130
|
+
*/
|
|
131
|
+
export const filterAuditEvents = (events, facets) => {
|
|
132
|
+
const from = resolveBound(facets.from, "start");
|
|
133
|
+
const to = resolveBound(facets.to, "end");
|
|
134
|
+
const hasFrom = !Number.isNaN(from);
|
|
135
|
+
const hasTo = !Number.isNaN(to);
|
|
136
|
+
return events.filter(event => {
|
|
137
|
+
if (facets.actor && (event.actor?.name ?? "") !== facets.actor) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (facets.action && event.action !== facets.action) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (facets.correlationId && event.correlationId !== facets.correlationId) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (!hasFrom && !hasTo) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const instant = auditEventInstant(event);
|
|
150
|
+
if (instant === null) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return (!hasFrom || instant >= from) && (!hasTo || instant <= to);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
/** True when any facet narrows the set — drives the Clear-filters affordance. */
|
|
157
|
+
export const hasAuditFacets = (facets) => Object.values(facets).some(value => value !== undefined && value !== "");
|
|
158
|
+
/**
|
|
159
|
+
* Group events into collapsible sections.
|
|
160
|
+
*
|
|
161
|
+
* Day sections run newest first with undated events in a trailing section;
|
|
162
|
+
* actor and action sections run by descending count, ties broken by label so
|
|
163
|
+
* the order never depends on input order. Events inside every section are
|
|
164
|
+
* newest first.
|
|
165
|
+
*/
|
|
166
|
+
export const groupAuditEvents = (events, groupBy, now) => {
|
|
167
|
+
const seeds = new Map();
|
|
168
|
+
const seedFor = (key, label) => {
|
|
169
|
+
let seed = seeds.get(key);
|
|
170
|
+
if (!seed) {
|
|
171
|
+
seed = { key, label, events: [] };
|
|
172
|
+
seeds.set(key, seed);
|
|
173
|
+
}
|
|
174
|
+
return seed;
|
|
175
|
+
};
|
|
176
|
+
for (const event of events) {
|
|
177
|
+
if (groupBy === "day") {
|
|
178
|
+
const day = resolveAuditDay(event);
|
|
179
|
+
seedFor(day ?? AUDIT_UNDATED_KEY, day ? formatAuditDay(day, now) : "Undated").events.push(event);
|
|
180
|
+
}
|
|
181
|
+
else if (groupBy === "actor") {
|
|
182
|
+
const name = event.actor?.name ?? "";
|
|
183
|
+
seedFor(name || AUDIT_UNATTRIBUTED_KEY, name || "Unattributed").events.push(event);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
seedFor(event.action, event.action).events.push(event);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const groups = [...seeds.values()].map(seed => {
|
|
190
|
+
const actors = new Set();
|
|
191
|
+
for (const event of seed.events) {
|
|
192
|
+
if (event.actor?.name) {
|
|
193
|
+
actors.add(event.actor.name);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
key: seed.key,
|
|
198
|
+
label: seed.label,
|
|
199
|
+
events: [...seed.events].sort(byNewestFirst),
|
|
200
|
+
count: seed.events.length,
|
|
201
|
+
actorCount: actors.size,
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
if (groupBy === "day") {
|
|
205
|
+
return groups.sort((left, right) => {
|
|
206
|
+
if (left.key === AUDIT_UNDATED_KEY || right.key === AUDIT_UNDATED_KEY) {
|
|
207
|
+
return left.key === right.key ? 0 : left.key === AUDIT_UNDATED_KEY ? 1 : -1;
|
|
208
|
+
}
|
|
209
|
+
return left.key < right.key ? 1 : left.key > right.key ? -1 : 0;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const trailingKey = groupBy === "actor" ? AUDIT_UNATTRIBUTED_KEY : null;
|
|
213
|
+
return groups.sort((left, right) => {
|
|
214
|
+
if (trailingKey !== null && (left.key === trailingKey || right.key === trailingKey)) {
|
|
215
|
+
return left.key === right.key ? 0 : left.key === trailingKey ? 1 : -1;
|
|
216
|
+
}
|
|
217
|
+
if (left.count !== right.count) {
|
|
218
|
+
return right.count - left.count;
|
|
219
|
+
}
|
|
220
|
+
return left.label < right.label ? -1 : left.label > right.label ? 1 : 0;
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
const summarize = (values) => {
|
|
224
|
+
const counts = new Map();
|
|
225
|
+
for (const value of values) {
|
|
226
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
227
|
+
}
|
|
228
|
+
return [...counts.entries()]
|
|
229
|
+
.map(([value, count]) => ({ value, label: value || "Unattributed", count }))
|
|
230
|
+
.sort((left, right) => left.count !== right.count
|
|
231
|
+
? right.count - left.count
|
|
232
|
+
: left.label < right.label
|
|
233
|
+
? -1
|
|
234
|
+
: left.label > right.label
|
|
235
|
+
? 1
|
|
236
|
+
: 0);
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Available facet values with counts, derived from the *unfiltered* set so
|
|
240
|
+
* selecting one facet never removes the options for another — a filter UI
|
|
241
|
+
* that can dead-end is worse than no counts.
|
|
242
|
+
*/
|
|
243
|
+
export const summarizeAuditFacets = (events) => ({
|
|
244
|
+
actors: summarize(events.map(event => event.actor?.name ?? "")),
|
|
245
|
+
actions: summarize(events.map(event => event.action)),
|
|
246
|
+
});
|
|
247
|
+
/** Formulas are data in an audit export, never something a spreadsheet should run. */
|
|
248
|
+
const FORMULA_PREFIXES = ["=", "+", "-", "@", "\t", "\r"];
|
|
249
|
+
const csvField = (value) => {
|
|
250
|
+
const neutralized = FORMULA_PREFIXES.some(prefix => value.startsWith(prefix))
|
|
251
|
+
? `'${value}`
|
|
252
|
+
: value;
|
|
253
|
+
return `"${neutralized.replaceAll('"', '""')}"`;
|
|
254
|
+
};
|
|
255
|
+
export const AUDIT_CSV_COLUMNS = [
|
|
256
|
+
"id",
|
|
257
|
+
"timestamp",
|
|
258
|
+
"actor",
|
|
259
|
+
"action",
|
|
260
|
+
"summary",
|
|
261
|
+
"badge",
|
|
262
|
+
"correlationId",
|
|
263
|
+
"evidence",
|
|
264
|
+
];
|
|
265
|
+
/**
|
|
266
|
+
* Render events as RFC 4180 CSV for the export toolbar. Every field is
|
|
267
|
+
* quoted, embedded quotes are doubled, and a value that would otherwise be
|
|
268
|
+
* read as a spreadsheet formula is prefixed so it stays text.
|
|
269
|
+
*/
|
|
270
|
+
export const toAuditCsv = (events) => {
|
|
271
|
+
const rows = events.map(event => [
|
|
272
|
+
event.id,
|
|
273
|
+
event.timestamp ?? "",
|
|
274
|
+
event.actor?.name ?? "",
|
|
275
|
+
event.action,
|
|
276
|
+
event.summary ?? "",
|
|
277
|
+
event.badge ?? "",
|
|
278
|
+
event.correlationId ?? "",
|
|
279
|
+
(event.evidence ?? [])
|
|
280
|
+
.map(entry => (entry.href ? `${entry.label} (${entry.href})` : entry.label))
|
|
281
|
+
.join("; "),
|
|
282
|
+
]
|
|
283
|
+
.map(csvField)
|
|
284
|
+
.join(","));
|
|
285
|
+
return [AUDIT_CSV_COLUMNS.map(csvField).join(","), ...rows].join("\r\n");
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Build the calendar heatmap window: whole week columns ending on the
|
|
289
|
+
* reference day's week, rows Sunday → Saturday. Pure and UTC-based, so the
|
|
290
|
+
* same events and reference time always produce the same grid.
|
|
291
|
+
*/
|
|
292
|
+
export const computeActivityDensity = (events, options) => {
|
|
293
|
+
const weeks = Math.min(53, Math.max(1, Math.trunc(options.weeks ?? 12)));
|
|
294
|
+
const now = options.now;
|
|
295
|
+
const endDay = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
296
|
+
const endWeekday = new Date(endDay).getUTCDay();
|
|
297
|
+
// Open on the Sunday of the oldest column so every column is a whole week.
|
|
298
|
+
const startDay = endDay - endWeekday * DAY_MS - (weeks - 1) * 7 * DAY_MS;
|
|
299
|
+
const counts = new Map();
|
|
300
|
+
for (const event of events) {
|
|
301
|
+
const day = resolveAuditDay(event);
|
|
302
|
+
if (day) {
|
|
303
|
+
counts.set(day, (counts.get(day) ?? 0) + 1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const cells = [];
|
|
307
|
+
let total = 0;
|
|
308
|
+
let max = 0;
|
|
309
|
+
for (let offset = 0; startDay + offset * DAY_MS <= endDay; offset += 1) {
|
|
310
|
+
const date = new Date(startDay + offset * DAY_MS).toISOString().slice(0, 10);
|
|
311
|
+
const count = counts.get(date) ?? 0;
|
|
312
|
+
total += count;
|
|
313
|
+
max = Math.max(max, count);
|
|
314
|
+
cells.push({ date, count, level: 0, week: Math.floor(offset / 7), weekday: offset % 7 });
|
|
315
|
+
}
|
|
316
|
+
for (const cell of cells) {
|
|
317
|
+
cell.level = cell.count === 0 ? 0 : Math.min(4, Math.max(1, Math.ceil((cell.count / max) * 4)));
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
cells,
|
|
321
|
+
weeks,
|
|
322
|
+
total,
|
|
323
|
+
max,
|
|
324
|
+
start: new Date(startDay).toISOString().slice(0, 10),
|
|
325
|
+
end: new Date(endDay).toISOString().slice(0, 10),
|
|
326
|
+
};
|
|
327
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windowing plan for the grouped audit log.
|
|
3
|
+
*
|
|
4
|
+
* `box-table` can refuse to window expandable rows because a flat table has an
|
|
5
|
+
* alternative: render everything. A ten-thousand-event audit log does not —
|
|
6
|
+
* grouping is the whole point of the surface, and grouped rows are exactly what
|
|
7
|
+
* fixed-height windowing cannot describe. So this is the case that needs the
|
|
8
|
+
* cumulative-offset engine (`src/core/offset-window.ts`) rather than an excuse.
|
|
9
|
+
*
|
|
10
|
+
* The log is windowed as one flat sequence of rows — each group contributes a
|
|
11
|
+
* heading row, then one row per event while it is expanded — because a single
|
|
12
|
+
* day's section can hold thousands of events on its own. Windowing whole groups
|
|
13
|
+
* would leave that case exactly as slow as it is now.
|
|
14
|
+
*
|
|
15
|
+
* The rendered DOM is still nested, so the flat window has to be turned back
|
|
16
|
+
* into sections. That is what `planAuditWindow` does, and the reason it exists
|
|
17
|
+
* as a pure function is the usual one: jsdom has no layout, so a DOM-level test
|
|
18
|
+
* of a windowed surface can only ever observe zeroes.
|
|
19
|
+
*
|
|
20
|
+
* ## The straddling group
|
|
21
|
+
*
|
|
22
|
+
* A group whose heading has scrolled above the viewport still needs its heading
|
|
23
|
+
* rendered: `[part="group-body"]` is `aria-labelledby` its toggle, and a
|
|
24
|
+
* section that arrives without its own heading is both unlabelled and
|
|
25
|
+
* impossible to collapse. So an intersecting group always renders its heading —
|
|
26
|
+
* which means rendering one heading the offset index has already accounted for
|
|
27
|
+
* in `paddingTop`.
|
|
28
|
+
*
|
|
29
|
+
* At most one group can straddle the top edge, so that is a single correction:
|
|
30
|
+
* take the heading's height back out of the top spacer. The events below then
|
|
31
|
+
* land where the scrollbar says they should, and the reclaimed heading sits
|
|
32
|
+
* directly above them.
|
|
33
|
+
*/
|
|
34
|
+
import type { OffsetIndex } from "../../core/offset-window.js";
|
|
35
|
+
import type { RowWindow } from "../../core/virtualize.js";
|
|
36
|
+
import type { AuditGroup } from "./types.js";
|
|
37
|
+
/** One row of the flattened log: a section heading, or an event inside one. */
|
|
38
|
+
export interface AuditRow {
|
|
39
|
+
kind: "heading" | "event";
|
|
40
|
+
/** Index into the groups array. */
|
|
41
|
+
groupIndex: number;
|
|
42
|
+
/** Index within that group's events; absent on a heading row. */
|
|
43
|
+
eventIndex?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AuditRowHeights {
|
|
46
|
+
heading: number;
|
|
47
|
+
event: number;
|
|
48
|
+
}
|
|
49
|
+
/** What to render for one group intersecting the window. */
|
|
50
|
+
export interface PlannedGroup {
|
|
51
|
+
groupIndex: number;
|
|
52
|
+
/** First event of this group to render, inclusive. */
|
|
53
|
+
eventStart: number;
|
|
54
|
+
/** Last event of this group to render, exclusive. */
|
|
55
|
+
eventEnd: number;
|
|
56
|
+
/**
|
|
57
|
+
* True when this group's own heading row is inside the window. False means
|
|
58
|
+
* the heading scrolled above the viewport and is being rendered anyway —
|
|
59
|
+
* `paddingTop` has already been corrected for it.
|
|
60
|
+
*/
|
|
61
|
+
headingInWindow: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface AuditWindowPlan {
|
|
64
|
+
groups: PlannedGroup[];
|
|
65
|
+
paddingTop: number;
|
|
66
|
+
paddingBottom: number;
|
|
67
|
+
totalHeight: number;
|
|
68
|
+
/** The underlying row window, for cheap "did the slice change?" comparisons. */
|
|
69
|
+
window: RowWindow;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The log as a flat row sequence. A collapsed group contributes its heading
|
|
73
|
+
* only — which is exactly why the heights vary and the fixed-height engine
|
|
74
|
+
* cannot describe this surface.
|
|
75
|
+
*/
|
|
76
|
+
export declare const flattenAuditRows: (groups: readonly AuditGroup[], collapsedKeys: ReadonlySet<string>) => AuditRow[];
|
|
77
|
+
export declare const auditRowHeights: (rows: readonly AuditRow[], heights: AuditRowHeights) => number[];
|
|
78
|
+
/**
|
|
79
|
+
* The offset index for a row set, so a caller can build it once and reuse it
|
|
80
|
+
* across scroll frames. The index only changes when the rows or the heights do.
|
|
81
|
+
*/
|
|
82
|
+
export declare const buildAuditIndex: (rows: readonly AuditRow[], heights: AuditRowHeights) => OffsetIndex;
|
|
83
|
+
/**
|
|
84
|
+
* Which sections to render, and the spacer heights around them.
|
|
85
|
+
*
|
|
86
|
+
* Returns an empty plan for an empty window rather than a plan with no groups
|
|
87
|
+
* and stale padding, so a caller can treat "nothing to render" uniformly.
|
|
88
|
+
*/
|
|
89
|
+
export declare const planAuditWindow: (rows: readonly AuditRow[], heights: AuditRowHeights, viewport: {
|
|
90
|
+
viewportHeight: number;
|
|
91
|
+
scrollTop: number;
|
|
92
|
+
overscan?: number;
|
|
93
|
+
/**
|
|
94
|
+
* The scroller's real `scrollHeight`, when the caller can measure it.
|
|
95
|
+
*
|
|
96
|
+
* Two heights per row kind is an estimate, and over thousands of rows the
|
|
97
|
+
* estimate and the rendered reality drift apart — measured at ~0.9% on a
|
|
98
|
+
* 2,000-event log, which sounds harmless and is not: if the estimate runs
|
|
99
|
+
* *long*, scrolling to the real bottom leaves the plan believing there is
|
|
100
|
+
* still a screenful below, and the last rows can never be reached. Content
|
|
101
|
+
* you cannot scroll to is worse than a slightly wrong scrollbar.
|
|
102
|
+
*
|
|
103
|
+
* Given the real height, the scroll position is read as a fraction of the
|
|
104
|
+
* real range and applied to the estimated one, so both ends line up: at the
|
|
105
|
+
* bottom of the scroller the plan is at the bottom of the log.
|
|
106
|
+
*/
|
|
107
|
+
contentHeight?: number;
|
|
108
|
+
},
|
|
109
|
+
/**
|
|
110
|
+
* A prebuilt index for these rows and heights, from `buildAuditIndex`.
|
|
111
|
+
*
|
|
112
|
+
* Optional purely for callers that plan once. The scroll path should pass one:
|
|
113
|
+
* building the index is O(n), and rebuilding it per frame would put the O(n)
|
|
114
|
+
* back that the binary search exists to remove.
|
|
115
|
+
*/
|
|
116
|
+
prebuilt?: OffsetIndex) => AuditWindowPlan;
|