@composy/log-viewer-core 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +175 -37
  3. package/dist/index.js +614 -299
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.min.js +548 -1
  6. package/dist/index.min.js.map +1 -0
  7. package/es/.dts-output-manifest.json +1 -0
  8. package/es/.dts.tsbuildinfo +1 -0
  9. package/es/core/EventEmitter.d.ts +0 -1
  10. package/es/core/EventEmitter.js +13 -9
  11. package/es/core/EventEmitter.js.map +1 -1
  12. package/es/core/LogViewer.d.ts +14 -11
  13. package/es/core/LogViewer.js +243 -64
  14. package/es/core/LogViewer.js.map +1 -1
  15. package/es/filters/FilterEngine.d.ts +0 -1
  16. package/es/filters/FilterEngine.js +31 -26
  17. package/es/filters/FilterEngine.js.map +1 -1
  18. package/es/index.d.ts +7 -7
  19. package/es/index.js +3 -3
  20. package/es/index.js.map +1 -1
  21. package/es/renderers/DiffRenderer.d.ts +0 -1
  22. package/es/renderers/DiffRenderer.js +13 -3
  23. package/es/renderers/DiffRenderer.js.map +1 -1
  24. package/es/renderers/TimelineRenderer.d.ts +7 -3
  25. package/es/renderers/TimelineRenderer.js +56 -22
  26. package/es/renderers/TimelineRenderer.js.map +1 -1
  27. package/es/styles/index.d.ts +1 -5
  28. package/es/styles/index.js +167 -131
  29. package/es/styles/index.js.map +1 -1
  30. package/es/types/index.d.ts +4 -1
  31. package/es/types/index.js +1 -1
  32. package/es/types/index.js.map +1 -1
  33. package/es/utils/index.d.ts +1 -2
  34. package/es/utils/index.js +54 -6
  35. package/es/utils/index.js.map +1 -1
  36. package/esm/core/EventEmitter.js +13 -9
  37. package/esm/core/EventEmitter.js.map +1 -1
  38. package/esm/core/LogViewer.js +243 -64
  39. package/esm/core/LogViewer.js.map +1 -1
  40. package/esm/filters/FilterEngine.js +31 -26
  41. package/esm/filters/FilterEngine.js.map +1 -1
  42. package/esm/index.js +2 -2
  43. package/esm/index.js.map +1 -1
  44. package/esm/renderers/DiffRenderer.js +13 -3
  45. package/esm/renderers/DiffRenderer.js.map +1 -1
  46. package/esm/renderers/TimelineRenderer.js +56 -22
  47. package/esm/renderers/TimelineRenderer.js.map +1 -1
  48. package/esm/styles/index.js +167 -131
  49. package/esm/styles/index.js.map +1 -1
  50. package/esm/types/index.js +1 -1
  51. package/esm/types/index.js.map +1 -1
  52. package/esm/utils/index.js +54 -6
  53. package/esm/utils/index.js.map +1 -1
  54. package/lib/core/EventEmitter.cjs +13 -9
  55. package/lib/core/EventEmitter.cjs.map +1 -1
  56. package/{esm/core/EventEmitter.d.ts → lib/core/EventEmitter.d.cts} +0 -1
  57. package/lib/core/LogViewer.cjs +247 -68
  58. package/lib/core/LogViewer.cjs.map +1 -1
  59. package/{esm/core/LogViewer.d.ts → lib/core/LogViewer.d.cts} +14 -11
  60. package/lib/filters/FilterEngine.cjs +31 -26
  61. package/lib/filters/FilterEngine.cjs.map +1 -1
  62. package/lib/filters/{FilterEngine.d.ts → FilterEngine.d.cts} +0 -1
  63. package/lib/index.cjs +3 -3
  64. package/lib/index.cjs.map +1 -1
  65. package/{esm/index.d.ts → lib/index.d.cts} +7 -7
  66. package/lib/renderers/DiffRenderer.cjs +13 -3
  67. package/lib/renderers/DiffRenderer.cjs.map +1 -1
  68. package/lib/renderers/{DiffRenderer.d.ts → DiffRenderer.d.cts} +0 -1
  69. package/lib/renderers/TimelineRenderer.cjs +55 -21
  70. package/lib/renderers/TimelineRenderer.cjs.map +1 -1
  71. package/lib/renderers/{TimelineRenderer.d.ts → TimelineRenderer.d.cts} +7 -3
  72. package/lib/styles/index.cjs +167 -131
  73. package/lib/styles/index.cjs.map +1 -1
  74. package/{esm/styles/index.d.ts → lib/styles/index.d.cts} +1 -5
  75. package/lib/types/index.cjs +1 -1
  76. package/lib/types/index.cjs.map +1 -1
  77. package/{esm/types/index.d.ts → lib/types/index.d.cts} +4 -1
  78. package/lib/utils/index.cjs +54 -6
  79. package/lib/utils/index.cjs.map +1 -1
  80. package/{esm/utils/index.d.ts → lib/utils/index.d.cts} +1 -2
  81. package/package.json +15 -9
  82. package/es/core/EventEmitter.d.ts.map +0 -1
  83. package/es/core/LogViewer.d.ts.map +0 -1
  84. package/es/filters/FilterEngine.d.ts.map +0 -1
  85. package/es/index.d.ts.map +0 -1
  86. package/es/renderers/DiffRenderer.d.ts.map +0 -1
  87. package/es/renderers/TimelineRenderer.d.ts.map +0 -1
  88. package/es/styles/index.d.ts.map +0 -1
  89. package/es/types/index.d.ts.map +0 -1
  90. package/es/utils/index.d.ts.map +0 -1
  91. package/esm/core/EventEmitter.d.ts.map +0 -1
  92. package/esm/core/LogViewer.d.ts.map +0 -1
  93. package/esm/filters/FilterEngine.d.ts +0 -26
  94. package/esm/filters/FilterEngine.d.ts.map +0 -1
  95. package/esm/index.d.ts.map +0 -1
  96. package/esm/renderers/DiffRenderer.d.ts +0 -18
  97. package/esm/renderers/DiffRenderer.d.ts.map +0 -1
  98. package/esm/renderers/TimelineRenderer.d.ts +0 -22
  99. package/esm/renderers/TimelineRenderer.d.ts.map +0 -1
  100. package/esm/styles/index.d.ts.map +0 -1
  101. package/esm/types/index.d.ts.map +0 -1
  102. package/esm/utils/index.d.ts.map +0 -1
  103. package/lib/core/EventEmitter.d.ts +0 -11
  104. package/lib/core/LogViewer.d.ts +0 -46
  105. package/lib/index.d.ts +0 -26
  106. package/lib/styles/index.d.ts +0 -9
  107. package/lib/types/index.d.ts +0 -113
  108. package/lib/utils/index.d.ts +0 -18
@@ -2,18 +2,63 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
9
9
  */
10
10
  import { EventEmitter } from "./EventEmitter.js";
11
+ import { ACTION_LABELS } from "../types/index.js";
11
12
  import { FilterEngine } from "../filters/FilterEngine.js";
12
13
  import { deepClone } from "../utils/index.js";
13
- import { TimelineRenderer } from "../renderers/TimelineRenderer.js";
14
14
  import { DiffRenderer } from "../renderers/DiffRenderer.js";
15
+ import { TimelineRenderer } from "../renderers/TimelineRenderer.js";
15
16
  import { injectStyles } from "../styles/index.js";
16
17
  //#region src/core/LogViewer.ts
18
+ const DEFAULT_PAGE_SIZE = 20;
19
+ const MAX_PAGE_SIZE = 1e3;
20
+ function normalizePageSize(value, fallback = DEFAULT_PAGE_SIZE) {
21
+ const pageSize = Number(value);
22
+ if (!Number.isFinite(pageSize) || pageSize <= 0) return fallback;
23
+ return Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
24
+ }
25
+ function normalizePage(value) {
26
+ const page = Number(value);
27
+ if (!Number.isFinite(page) || page < 1) return 1;
28
+ return Math.floor(page);
29
+ }
30
+ function cloneEntries(entries) {
31
+ if (entries === void 0) return [];
32
+ if (!Array.isArray(entries)) throw new TypeError("[LogViewer] data must be an array");
33
+ return entries.map((entry, index) => {
34
+ if (!entry || typeof entry !== "object") throw new TypeError(`[LogViewer] data[${index}] must be an object`);
35
+ const cloned = deepClone(entry);
36
+ const id = String(cloned.id ?? "").trim();
37
+ if (!id) throw new TypeError(`[LogViewer] data[${index}].id must be a non-empty string`);
38
+ return {
39
+ ...cloned,
40
+ id,
41
+ user: String(cloned.user ?? ""),
42
+ module: String(cloned.module ?? ""),
43
+ changes: Array.isArray(cloned.changes) ? cloned.changes : void 0
44
+ };
45
+ });
46
+ }
47
+ function isActionType(value) {
48
+ return Object.keys(ACTION_LABELS).includes(value);
49
+ }
50
+ function sourceEntries(payload) {
51
+ if (Array.isArray(payload)) return payload;
52
+ if (!payload || typeof payload !== "object") throw new TypeError("[LogViewer] dataSource response must be an array");
53
+ const record = payload;
54
+ for (const key of [
55
+ "data",
56
+ "logs",
57
+ "items",
58
+ "entries"
59
+ ]) if (Array.isArray(record[key])) return record[key];
60
+ throw new TypeError("[LogViewer] dataSource response must contain an array");
61
+ }
17
62
  var LogViewer = class {
18
63
  constructor(config) {
19
64
  this.emitter = new EventEmitter();
@@ -25,89 +70,107 @@ var LogViewer = class {
25
70
  this.filter = {};
26
71
  this.pagination = {
27
72
  page: 1,
28
- pageSize: 20
73
+ pageSize: DEFAULT_PAGE_SIZE
29
74
  };
30
75
  this.theme = "light";
31
76
  this.expandedIds = /* @__PURE__ */ new Set();
77
+ this.filterEl = null;
32
78
  this.timelineEl = null;
33
79
  this.paginationEl = null;
34
80
  this.destroyed = false;
35
- this.config = config;
81
+ this.requestId = 0;
82
+ this.abortController = null;
83
+ if (typeof document === "undefined") throw new Error("[LogViewer] A browser document is required");
36
84
  const container = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
37
- if (!container) throw new Error(`[LogViewer] Container not found: ${config.container}`);
85
+ if (!container) throw new Error(`[LogViewer] Container not found: ${String(config.container)}`);
86
+ this.config = { ...config };
87
+ this.theme = config.theme === "dark" ? "dark" : "light";
88
+ this.pagination = {
89
+ page: normalizePage(config.pagination?.page),
90
+ pageSize: normalizePageSize(config.pageSize ?? config.pagination?.pageSize)
91
+ };
92
+ this.filter = deepClone(config.filter || {});
93
+ this.data = cloneEntries(config.data);
38
94
  injectStyles();
39
95
  this.root = document.createElement("div");
40
96
  this.root.className = "lv-root";
41
- container.appendChild(this.root);
42
- this.theme = config.theme || "light";
43
97
  this.root.setAttribute("data-theme", this.theme);
44
- if (config.pageSize) this.pagination.pageSize = config.pageSize;
45
- if (config.pagination) Object.assign(this.pagination, config.pagination);
46
- if (config.filter) this.filter = deepClone(config.filter);
47
- if (config.data) this.data = [...config.data];
48
- this.applyFilterAndRender();
98
+ container.appendChild(this.root);
49
99
  this.bindEvents();
100
+ this.applyFilterAndRender();
101
+ if (config.dataSource) this.loadData().catch(() => void 0);
50
102
  }
51
103
  getData() {
52
- return [...this.data];
104
+ return deepClone(this.data);
53
105
  }
54
106
  setData(entries) {
55
- this.data = [...entries];
107
+ if (this.destroyed) return;
108
+ this.data = cloneEntries(entries);
56
109
  this.pagination.page = 1;
110
+ this.expandedIds.clear();
57
111
  this.applyFilterAndRender();
58
- this.emitter.emit("data:change", this.data);
112
+ this.emitter.emit("data:change", this.getData());
59
113
  }
60
114
  appendData(entries) {
61
- this.data.push(...entries);
115
+ if (this.destroyed) return;
116
+ this.data.push(...cloneEntries(entries));
62
117
  this.applyFilterAndRender();
63
- this.emitter.emit("data:change", this.data);
118
+ this.emitter.emit("data:change", this.getData());
64
119
  }
65
120
  getFilter() {
66
121
  return deepClone(this.filter);
67
122
  }
68
123
  setFilter(filter) {
69
- this.filter = deepClone(filter);
124
+ if (this.destroyed) return;
125
+ this.filter = deepClone(filter || {});
70
126
  this.pagination.page = 1;
71
127
  this.applyFilterAndRender();
72
- this.emitter.emit("filter:change", this.filter);
73
- this.config.onFilterChange?.(this.filter);
128
+ const current = this.getFilter();
129
+ this.emitter.emit("filter:change", current);
130
+ this.config.onFilterChange?.(current);
74
131
  }
75
132
  resetFilter() {
76
- this.filter = {};
77
- this.pagination.page = 1;
78
- this.applyFilterAndRender();
79
- this.emitter.emit("filter:change", this.filter);
133
+ if (this.destroyed) return;
134
+ this.setFilter({});
80
135
  }
81
136
  getTheme() {
82
137
  return this.theme;
83
138
  }
84
139
  setTheme(theme) {
140
+ if (this.destroyed) return;
141
+ if (theme !== "light" && theme !== "dark") throw new TypeError("[LogViewer] theme must be light or dark");
142
+ if (this.theme === theme) return;
85
143
  this.theme = theme;
86
144
  this.root.setAttribute("data-theme", theme);
87
145
  this.emitter.emit("theme:change", theme);
88
146
  this.config.onThemeChange?.(theme);
89
147
  }
90
148
  expandEntry(id) {
149
+ if (this.destroyed || this.expandedIds.has(id)) return;
91
150
  this.expandedIds.add(id);
92
- const entry = this.data.find((e) => e.id === id);
93
- if (entry) this.emitter.emit("entry:expand", entry);
151
+ const entry = this.data.find((item) => item.id === id);
152
+ if (entry) this.emitter.emit("entry:expand", deepClone(entry));
94
153
  this.renderTimeline();
95
154
  }
96
155
  collapseEntry(id) {
156
+ if (this.destroyed || !this.expandedIds.has(id)) return;
97
157
  this.expandedIds.delete(id);
98
- const entry = this.data.find((e) => e.id === id);
99
- if (entry) this.emitter.emit("entry:collapse", entry);
158
+ const entry = this.data.find((item) => item.id === id);
159
+ if (entry) this.emitter.emit("entry:collapse", deepClone(entry));
100
160
  this.renderTimeline();
101
161
  }
102
162
  collapseAll() {
163
+ if (this.destroyed || this.expandedIds.size === 0) return;
103
164
  this.expandedIds.clear();
104
165
  this.renderTimeline();
105
166
  }
106
167
  goToPage(page) {
107
- const totalPages = Math.ceil(this.filteredData.length / this.pagination.pageSize);
108
- if (page < 1 || page > totalPages) return;
109
- this.pagination.page = page;
110
- this.emitter.emit("page:change", page);
168
+ if (this.destroyed) return;
169
+ const totalPages = Math.max(1, Math.ceil(this.filteredData.length / this.pagination.pageSize));
170
+ const nextPage = Math.min(totalPages, Math.max(1, normalizePage(page)));
171
+ if (nextPage === this.pagination.page) return;
172
+ this.pagination.page = nextPage;
173
+ this.emitter.emit("page:change", nextPage);
111
174
  this.renderTimeline();
112
175
  }
113
176
  getPagination() {
@@ -120,17 +183,55 @@ var LogViewer = class {
120
183
  return this.emitter.on(event, callback);
121
184
  }
122
185
  refresh() {
123
- this.applyFilterAndRender();
186
+ if (!this.destroyed) this.applyFilterAndRender();
187
+ }
188
+ async loadData() {
189
+ if (this.destroyed || !this.config.dataSource) return;
190
+ const currentRequest = ++this.requestId;
191
+ this.abortController?.abort();
192
+ this.abortController = typeof AbortController !== "undefined" ? new AbortController() : null;
193
+ try {
194
+ let payload;
195
+ if (typeof this.config.dataSource === "function") payload = await this.config.dataSource();
196
+ else {
197
+ if (typeof fetch !== "function") throw new Error("[LogViewer] fetch is unavailable");
198
+ const response = await fetch(this.config.dataSource, { signal: this.abortController?.signal });
199
+ if (!response.ok) throw new Error(`[LogViewer] dataSource request failed: ${response.status}`);
200
+ payload = await response.json();
201
+ }
202
+ if (this.destroyed || currentRequest !== this.requestId) return;
203
+ this.setData(sourceEntries(payload));
204
+ } catch (error) {
205
+ if (this.destroyed || typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError") return;
206
+ this.config.onError?.(error);
207
+ this.emitter.emit("error", error);
208
+ throw error;
209
+ }
124
210
  }
125
211
  destroy() {
126
212
  if (this.destroyed) return;
127
213
  this.destroyed = true;
214
+ this.requestId++;
215
+ this.abortController?.abort();
216
+ this.abortController = null;
128
217
  this.emitter.removeAllListeners();
218
+ this.expandedIds.clear();
219
+ this.root.replaceChildren();
129
220
  this.root.remove();
130
221
  }
131
222
  applyFilterAndRender() {
132
223
  this.filteredData = this.filterEngine.filter(this.data, this.filter);
133
- this.filteredData.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
224
+ this.filteredData = this.filteredData.map((entry, index) => ({
225
+ entry,
226
+ index
227
+ })).sort((a, b) => {
228
+ const left = new Date(a.entry.timestamp).getTime();
229
+ const right = new Date(b.entry.timestamp).getTime();
230
+ const leftTime = Number.isFinite(left) ? left : Number.MIN_SAFE_INTEGER;
231
+ return (Number.isFinite(right) ? right : Number.MIN_SAFE_INTEGER) - leftTime || a.index - b.index;
232
+ }).map(({ entry }) => entry);
233
+ const totalPages = Math.max(1, Math.ceil(this.filteredData.length / this.pagination.pageSize));
234
+ this.pagination.page = Math.min(this.pagination.page, totalPages);
134
235
  this.pagination.total = this.filteredData.length;
135
236
  this.renderTimeline();
136
237
  }
@@ -139,43 +240,121 @@ var LogViewer = class {
139
240
  const start = (page - 1) * pageSize;
140
241
  return this.filteredData.slice(start, start + pageSize);
141
242
  }
243
+ renderFilterBar() {
244
+ if (this.config.showFilter === false) return null;
245
+ const filter = document.createElement("div");
246
+ filter.className = "lv-filter";
247
+ filter.setAttribute("role", "search");
248
+ const addInput = (field, label) => {
249
+ const item = document.createElement("label");
250
+ item.className = "lv-filter-item";
251
+ const text = document.createElement("span");
252
+ text.className = "lv-filter-label";
253
+ text.textContent = label;
254
+ const input = document.createElement("input");
255
+ input.className = "lv-filter-input";
256
+ input.type = "search";
257
+ input.value = String(this.filter[field] || "");
258
+ input.setAttribute("data-filter-field", field);
259
+ input.setAttribute("aria-label", label);
260
+ item.append(text, input);
261
+ filter.appendChild(item);
262
+ };
263
+ addInput("keyword", this.config.locale === "en-US" ? "Keyword" : "关键词");
264
+ addInput("user", this.config.locale === "en-US" ? "User" : "用户");
265
+ addInput("module", this.config.locale === "en-US" ? "Module" : "模块");
266
+ addInput("ip", "IP");
267
+ const actionItem = document.createElement("label");
268
+ actionItem.className = "lv-filter-item";
269
+ const actionLabel = document.createElement("span");
270
+ actionLabel.className = "lv-filter-label";
271
+ actionLabel.textContent = this.config.locale === "en-US" ? "Action" : "操作";
272
+ const action = document.createElement("select");
273
+ action.className = "lv-filter-select";
274
+ action.setAttribute("data-filter-field", "action");
275
+ action.setAttribute("aria-label", actionLabel.textContent);
276
+ const allOption = document.createElement("option");
277
+ allOption.value = "";
278
+ allOption.textContent = this.config.locale === "en-US" ? "All" : "全部";
279
+ action.appendChild(allOption);
280
+ for (const [value, label] of Object.entries(ACTION_LABELS)) {
281
+ const option = document.createElement("option");
282
+ option.value = value;
283
+ option.textContent = label;
284
+ action.appendChild(option);
285
+ }
286
+ action.value = Array.isArray(this.filter.action) ? "" : this.filter.action || "";
287
+ actionItem.append(actionLabel, action);
288
+ filter.appendChild(actionItem);
289
+ const reset = document.createElement("button");
290
+ reset.type = "button";
291
+ reset.className = "lv-filter-btn";
292
+ reset.setAttribute("data-filter-reset", "true");
293
+ reset.textContent = this.config.locale === "en-US" ? "Reset" : "重置";
294
+ filter.appendChild(reset);
295
+ return filter;
296
+ }
297
+ updateFilterField(field, value) {
298
+ const next = this.getFilter();
299
+ if (!value.trim()) delete next[field];
300
+ else if (field === "action" && isActionType(value)) next.action = value;
301
+ else if (field !== "action") next[field] = value;
302
+ this.setFilter(next);
303
+ }
142
304
  renderTimeline() {
143
305
  if (this.destroyed) return;
144
306
  const pageData = this.getPageData();
145
- const newTimeline = this.timelineRenderer.render(pageData, this.expandedIds);
146
- for (const entry of pageData) if (this.expandedIds.has(entry.id)) {
147
- const itemEl = newTimeline.querySelector(`[data-id="${entry.id}"] .lv-timeline-card`);
148
- if (itemEl) itemEl.appendChild(this.diffRenderer.renderDetail(entry));
307
+ const timeline = this.timelineRenderer.render(pageData, this.expandedIds, {
308
+ showDiff: this.config.showDiff,
309
+ showAnomalyBadge: this.config.showAnomalyBadge,
310
+ locale: this.config.locale
311
+ });
312
+ if (this.config.showDiff !== false) for (const entry of pageData) {
313
+ if (!this.expandedIds.has(entry.id)) continue;
314
+ ([...timeline.children].find((child) => child.getAttribute("data-id") === entry.id)?.querySelector(".lv-timeline-card"))?.appendChild(this.diffRenderer.renderDetail(entry));
149
315
  }
150
- if (this.timelineEl) this.root.removeChild(this.timelineEl);
151
- this.timelineEl = newTimeline;
152
- this.root.appendChild(this.timelineEl);
153
- const newPagination = this.timelineRenderer.renderPagination(this.pagination.page, this.pagination.pageSize, this.filteredData.length);
154
- if (this.paginationEl) this.root.removeChild(this.paginationEl);
155
- this.paginationEl = newPagination;
156
- this.root.appendChild(this.paginationEl);
316
+ this.filterEl?.remove();
317
+ this.timelineEl?.remove();
318
+ this.paginationEl?.remove();
319
+ this.filterEl = this.renderFilterBar();
320
+ this.timelineEl = timeline;
321
+ this.paginationEl = this.timelineRenderer.renderPagination(this.pagination.page, this.pagination.pageSize, this.filteredData.length);
322
+ if (this.filterEl) this.root.appendChild(this.filterEl);
323
+ this.root.append(this.timelineEl, this.paginationEl);
157
324
  }
158
325
  bindEvents() {
159
- this.root.addEventListener("click", (e) => {
160
- const target = e.target;
161
- const card = target.closest(".lv-timeline-card");
162
- if (card) {
163
- const id = card.closest(".lv-timeline-item")?.getAttribute("data-id");
164
- if (!id) return;
165
- const entry = this.filteredData.find((en) => en.id === id);
166
- if (!entry) return;
167
- if (target.classList.contains("lv-timeline-expand-hint")) {
168
- if (this.expandedIds.has(id)) this.collapseEntry(id);
169
- else this.expandEntry(id);
170
- return;
171
- }
172
- this.emitter.emit("entry:click", entry);
173
- this.config.onEntryClick?.(entry);
326
+ this.root.addEventListener("change", (event) => {
327
+ const target = event.target;
328
+ if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLSelectElement)) return;
329
+ const field = target.getAttribute("data-filter-field");
330
+ if (field) this.updateFilterField(field, target.value);
331
+ });
332
+ this.root.addEventListener("click", (event) => {
333
+ const target = event.target;
334
+ if (!(target instanceof Element)) return;
335
+ if (target.closest("[data-filter-reset]")) {
336
+ this.resetFilter();
337
+ return;
338
+ }
339
+ const hint = target.closest(".lv-timeline-expand-hint");
340
+ const id = (hint?.closest(".lv-timeline-item") || target.closest(".lv-timeline-item"))?.getAttribute("data-id");
341
+ if (hint && id) {
342
+ if (this.expandedIds.has(id)) this.collapseEntry(id);
343
+ else this.expandEntry(id);
344
+ return;
345
+ }
346
+ const pageButton = target.closest(".lv-pagination-btn");
347
+ if (pageButton && !pageButton.disabled) {
348
+ const page = Number(pageButton.getAttribute("data-page"));
349
+ if (Number.isFinite(page)) this.goToPage(page);
350
+ return;
174
351
  }
175
- const pageBtn = target.closest(".lv-pagination-btn");
176
- if (pageBtn && !pageBtn.disabled) {
177
- const page = parseInt(pageBtn.getAttribute("data-page") || "1", 10);
178
- this.goToPage(page);
352
+ if (!target.closest(".lv-timeline-card") || !id) return;
353
+ const entry = this.filteredData.find((candidate) => candidate.id === id);
354
+ if (entry) {
355
+ const snapshot = deepClone(entry);
356
+ this.emitter.emit("entry:click", snapshot);
357
+ this.config.onEntryClick?.(snapshot);
179
358
  }
180
359
  });
181
360
  }
@@ -1 +1 @@
1
- {"version":3,"file":"LogViewer.js","names":[],"sources":["../../src/core/LogViewer.ts"],"sourcesContent":["/**\n * LogViewer 核心引擎\n */\n\nimport type {\n LogEntry,\n LogViewerConfig,\n LogViewerInstance,\n FilterConfig,\n PaginationConfig,\n ThemeMode,\n} from '../types'\nimport { EventEmitter } from './EventEmitter'\nimport { FilterEngine } from '../filters/FilterEngine'\nimport { TimelineRenderer } from '../renderers/TimelineRenderer'\nimport { DiffRenderer } from '../renderers/DiffRenderer'\nimport { injectStyles } from '../styles'\nimport { deepClone } from '../utils'\n\n/**\n * LogViewer - Log Viewer class\n */\nexport class LogViewer implements LogViewerInstance {\n private root: HTMLElement\n private emitter = new EventEmitter()\n private filterEngine = new FilterEngine()\n private timelineRenderer = new TimelineRenderer()\n private diffRenderer = new DiffRenderer()\n\n private data: LogEntry[] = []\n private filteredData: LogEntry[] = []\n private filter: FilterConfig = {}\n private pagination: PaginationConfig = { page: 1, pageSize: 20 }\n private theme: ThemeMode = 'light'\n private expandedIds = new Set<string>()\n private config: LogViewerConfig\n\n private timelineEl: HTMLElement | null = null\n private paginationEl: HTMLElement | null = null\n private destroyed = false\n\n constructor(config: LogViewerConfig) {\n this.config = config\n\n // Resolve container\n const container =\n typeof config.container === 'string'\n ? document.querySelector<HTMLElement>(config.container)\n : config.container\n if (!container) throw new Error(`[LogViewer] Container not found: ${config.container}`)\n\n // Inject global styles\n injectStyles()\n\n // Create root\n this.root = document.createElement('div')\n this.root.className = 'lv-root'\n container.appendChild(this.root)\n\n // Apply config\n this.theme = config.theme || 'light'\n this.root.setAttribute('data-theme', this.theme)\n\n if (config.pageSize) this.pagination.pageSize = config.pageSize\n if (config.pagination) Object.assign(this.pagination, config.pagination)\n if (config.filter) this.filter = deepClone(config.filter)\n\n // Load data\n if (config.data) {\n this.data = [...config.data]\n }\n\n this.applyFilterAndRender()\n this.bindEvents()\n }\n\n // ===== Public API =====\n\n getData(): LogEntry[] {\n return [...this.data]\n }\n\n setData(entries: LogEntry[]): void {\n this.data = [...entries]\n this.pagination.page = 1\n this.applyFilterAndRender()\n this.emitter.emit('data:change', this.data)\n }\n\n appendData(entries: LogEntry[]): void {\n this.data.push(...entries)\n this.applyFilterAndRender()\n this.emitter.emit('data:change', this.data)\n }\n\n getFilter(): FilterConfig {\n return deepClone(this.filter)\n }\n\n setFilter(filter: FilterConfig): void {\n this.filter = deepClone(filter)\n this.pagination.page = 1\n this.applyFilterAndRender()\n this.emitter.emit('filter:change', this.filter)\n this.config.onFilterChange?.(this.filter)\n }\n\n resetFilter(): void {\n this.filter = {}\n this.pagination.page = 1\n this.applyFilterAndRender()\n this.emitter.emit('filter:change', this.filter)\n }\n\n getTheme(): ThemeMode {\n return this.theme\n }\n\n setTheme(theme: ThemeMode): void {\n this.theme = theme\n this.root.setAttribute('data-theme', theme)\n this.emitter.emit('theme:change', theme)\n this.config.onThemeChange?.(theme)\n }\n\n expandEntry(id: string): void {\n this.expandedIds.add(id)\n const entry = this.data.find((e) => e.id === id)\n if (entry) this.emitter.emit('entry:expand', entry)\n this.renderTimeline()\n }\n\n collapseEntry(id: string): void {\n this.expandedIds.delete(id)\n const entry = this.data.find((e) => e.id === id)\n if (entry) this.emitter.emit('entry:collapse', entry)\n this.renderTimeline()\n }\n\n collapseAll(): void {\n this.expandedIds.clear()\n this.renderTimeline()\n }\n\n goToPage(page: number): void {\n const totalPages = Math.ceil(this.filteredData.length / this.pagination.pageSize)\n if (page < 1 || page > totalPages) return\n this.pagination.page = page\n this.emitter.emit('page:change', page)\n this.renderTimeline()\n }\n\n getPagination(): PaginationConfig {\n return {\n ...this.pagination,\n total: this.filteredData.length,\n }\n }\n\n on(event: string, callback: (...args: unknown[]) => void): () => void {\n return this.emitter.on(event, callback)\n }\n\n refresh(): void {\n this.applyFilterAndRender()\n }\n\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.emitter.removeAllListeners()\n this.root.remove()\n }\n\n // ===== Internal =====\n\n private applyFilterAndRender(): void {\n this.filteredData = this.filterEngine.filter(this.data, this.filter)\n this.filteredData.sort(\n (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),\n )\n this.pagination.total = this.filteredData.length\n this.renderTimeline()\n }\n\n private getPageData(): LogEntry[] {\n const { page, pageSize } = this.pagination\n const start = (page - 1) * pageSize\n return this.filteredData.slice(start, start + pageSize)\n }\n\n private renderTimeline(): void {\n if (this.destroyed) return\n\n const pageData = this.getPageData()\n\n // Timeline\n const newTimeline = this.timelineRenderer.render(pageData, this.expandedIds)\n\n // Render detail panels for expanded items\n for (const entry of pageData) {\n if (this.expandedIds.has(entry.id)) {\n const itemEl = newTimeline.querySelector(`[data-id=\"${entry.id}\"] .lv-timeline-card`)\n if (itemEl) {\n itemEl.appendChild(this.diffRenderer.renderDetail(entry))\n }\n }\n }\n\n if (this.timelineEl) {\n this.root.removeChild(this.timelineEl)\n }\n this.timelineEl = newTimeline\n this.root.appendChild(this.timelineEl)\n\n // Pagination\n const newPagination = this.timelineRenderer.renderPagination(\n this.pagination.page,\n this.pagination.pageSize,\n this.filteredData.length,\n )\n if (this.paginationEl) {\n this.root.removeChild(this.paginationEl)\n }\n this.paginationEl = newPagination\n this.root.appendChild(this.paginationEl)\n }\n\n private bindEvents(): void {\n this.root.addEventListener('click', (e) => {\n const target = e.target as HTMLElement\n\n // Timeline card click\n const card = target.closest('.lv-timeline-card') as HTMLElement | null\n if (card) {\n const item = card.closest('.lv-timeline-item') as HTMLElement | null\n const id = item?.getAttribute('data-id')\n if (!id) return\n\n const entry = this.filteredData.find((en) => en.id === id)\n if (!entry) return\n\n // Click on expand hint\n if (target.classList.contains('lv-timeline-expand-hint')) {\n if (this.expandedIds.has(id)) {\n this.collapseEntry(id)\n } else {\n this.expandEntry(id)\n }\n return\n }\n\n // Click on card itself\n this.emitter.emit('entry:click', entry)\n this.config.onEntryClick?.(entry)\n }\n\n // Pagination click\n const pageBtn = target.closest('.lv-pagination-btn') as HTMLButtonElement | null\n if (pageBtn && !pageBtn.disabled) {\n const page = parseInt(pageBtn.getAttribute('data-page') || '1', 10)\n this.goToPage(page)\n }\n })\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LogViewer.js","names":[],"sources":["../../src/core/LogViewer.ts"],"sourcesContent":["/**\n * LogViewer 核心引擎\n */\n\nimport type {\n ActionType,\n FilterConfig,\n LogEntry,\n LogViewerConfig,\n LogViewerInstance,\n PaginationConfig,\n ThemeMode,\n} from '../types'\nimport { ACTION_LABELS } from '../types'\nimport { EventEmitter } from './EventEmitter'\nimport { FilterEngine } from '../filters/FilterEngine'\nimport { DiffRenderer } from '../renderers/DiffRenderer'\nimport { TimelineRenderer } from '../renderers/TimelineRenderer'\nimport { injectStyles } from '../styles'\nimport { deepClone } from '../utils'\n\nconst DEFAULT_PAGE_SIZE = 20\nconst MAX_PAGE_SIZE = 1000\n\nfunction normalizePageSize(\n value: unknown,\n fallback = DEFAULT_PAGE_SIZE\n): number {\n const pageSize = Number(value)\n if (!Number.isFinite(pageSize) || pageSize <= 0) return fallback\n return Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(pageSize)))\n}\n\nfunction normalizePage(value: unknown): number {\n const page = Number(value)\n if (!Number.isFinite(page) || page < 1) return 1\n return Math.floor(page)\n}\n\nfunction cloneEntries(entries: readonly LogEntry[] | undefined): LogEntry[] {\n if (entries === undefined) return []\n if (!Array.isArray(entries))\n throw new TypeError('[LogViewer] data must be an array')\n\n return entries.map((entry, index) => {\n if (!entry || typeof entry !== 'object') {\n throw new TypeError(`[LogViewer] data[${index}] must be an object`)\n }\n\n const cloned = deepClone(entry)\n const id = String(cloned.id ?? '').trim()\n if (!id)\n throw new TypeError(\n `[LogViewer] data[${index}].id must be a non-empty string`\n )\n\n return {\n ...cloned,\n id,\n user: String(cloned.user ?? ''),\n module: String(cloned.module ?? ''),\n changes: Array.isArray(cloned.changes) ? cloned.changes : undefined,\n }\n })\n}\n\nfunction isActionType(value: string): value is ActionType {\n return Object.keys(ACTION_LABELS).includes(value)\n}\n\nfunction sourceEntries(payload: unknown): LogEntry[] {\n if (Array.isArray(payload)) return payload as LogEntry[]\n if (!payload || typeof payload !== 'object') {\n throw new TypeError('[LogViewer] dataSource response must be an array')\n }\n\n const record = payload as Record<string, unknown>\n for (const key of ['data', 'logs', 'items', 'entries']) {\n if (Array.isArray(record[key])) return record[key] as LogEntry[]\n }\n throw new TypeError('[LogViewer] dataSource response must contain an array')\n}\n\n/** LogViewer - framework-neutral audit log viewer. */\nexport class LogViewer implements LogViewerInstance {\n private readonly root: HTMLElement\n private readonly emitter = new EventEmitter()\n private readonly filterEngine = new FilterEngine()\n private readonly timelineRenderer = new TimelineRenderer()\n private readonly diffRenderer = new DiffRenderer()\n private readonly config: LogViewerConfig\n\n private data: LogEntry[] = []\n private filteredData: LogEntry[] = []\n private filter: FilterConfig = {}\n private pagination: PaginationConfig = {\n page: 1,\n pageSize: DEFAULT_PAGE_SIZE,\n }\n private theme: ThemeMode = 'light'\n private expandedIds = new Set<string>()\n private filterEl: HTMLElement | null = null\n private timelineEl: HTMLElement | null = null\n private paginationEl: HTMLElement | null = null\n private destroyed = false\n private requestId = 0\n private abortController: AbortController | null = null\n\n constructor(config: LogViewerConfig) {\n if (typeof document === 'undefined') {\n throw new Error('[LogViewer] A browser document is required')\n }\n\n const container =\n typeof config.container === 'string'\n ? document.querySelector<HTMLElement>(config.container)\n : config.container\n if (!container)\n throw new Error(\n `[LogViewer] Container not found: ${String(config.container)}`\n )\n\n this.config = { ...config }\n this.theme = config.theme === 'dark' ? 'dark' : 'light'\n this.pagination = {\n page: normalizePage(config.pagination?.page),\n pageSize: normalizePageSize(\n config.pageSize ?? config.pagination?.pageSize\n ),\n }\n this.filter = deepClone(config.filter || {})\n this.data = cloneEntries(config.data)\n\n injectStyles()\n this.root = document.createElement('div')\n this.root.className = 'lv-root'\n this.root.setAttribute('data-theme', this.theme)\n container.appendChild(this.root)\n\n this.bindEvents()\n this.applyFilterAndRender()\n\n if (config.dataSource) {\n void this.loadData().catch(() => undefined)\n }\n }\n\n getData(): LogEntry[] {\n return deepClone(this.data)\n }\n\n setData(entries: LogEntry[]): void {\n if (this.destroyed) return\n this.data = cloneEntries(entries)\n this.pagination.page = 1\n this.expandedIds.clear()\n this.applyFilterAndRender()\n this.emitter.emit('data:change', this.getData())\n }\n\n appendData(entries: LogEntry[]): void {\n if (this.destroyed) return\n this.data.push(...cloneEntries(entries))\n this.applyFilterAndRender()\n this.emitter.emit('data:change', this.getData())\n }\n\n getFilter(): FilterConfig {\n return deepClone(this.filter)\n }\n\n setFilter(filter: FilterConfig): void {\n if (this.destroyed) return\n this.filter = deepClone(filter || {})\n this.pagination.page = 1\n this.applyFilterAndRender()\n const current = this.getFilter()\n this.emitter.emit('filter:change', current)\n this.config.onFilterChange?.(current)\n }\n\n resetFilter(): void {\n if (this.destroyed) return\n this.setFilter({})\n }\n\n getTheme(): ThemeMode {\n return this.theme\n }\n\n setTheme(theme: ThemeMode): void {\n if (this.destroyed) return\n if (theme !== 'light' && theme !== 'dark') {\n throw new TypeError('[LogViewer] theme must be light or dark')\n }\n if (this.theme === theme) return\n this.theme = theme\n this.root.setAttribute('data-theme', theme)\n this.emitter.emit('theme:change', theme)\n this.config.onThemeChange?.(theme)\n }\n\n expandEntry(id: string): void {\n if (this.destroyed || this.expandedIds.has(id)) return\n this.expandedIds.add(id)\n const entry = this.data.find(item => item.id === id)\n if (entry) this.emitter.emit('entry:expand', deepClone(entry))\n this.renderTimeline()\n }\n\n collapseEntry(id: string): void {\n if (this.destroyed || !this.expandedIds.has(id)) return\n this.expandedIds.delete(id)\n const entry = this.data.find(item => item.id === id)\n if (entry) this.emitter.emit('entry:collapse', deepClone(entry))\n this.renderTimeline()\n }\n\n collapseAll(): void {\n if (this.destroyed || this.expandedIds.size === 0) return\n this.expandedIds.clear()\n this.renderTimeline()\n }\n\n goToPage(page: number): void {\n if (this.destroyed) return\n const totalPages = Math.max(\n 1,\n Math.ceil(this.filteredData.length / this.pagination.pageSize)\n )\n const nextPage = Math.min(totalPages, Math.max(1, normalizePage(page)))\n if (nextPage === this.pagination.page) return\n this.pagination.page = nextPage\n this.emitter.emit('page:change', nextPage)\n this.renderTimeline()\n }\n\n getPagination(): PaginationConfig {\n return {\n ...this.pagination,\n total: this.filteredData.length,\n }\n }\n\n on(event: string, callback: (...args: unknown[]) => void): () => void {\n return this.emitter.on(event, callback)\n }\n\n refresh(): void {\n if (!this.destroyed) this.applyFilterAndRender()\n }\n\n async loadData(): Promise<void> {\n if (this.destroyed || !this.config.dataSource) return\n\n const currentRequest = ++this.requestId\n this.abortController?.abort()\n this.abortController =\n typeof AbortController !== 'undefined' ? new AbortController() : null\n\n try {\n let payload: unknown\n if (typeof this.config.dataSource === 'function') {\n payload = await this.config.dataSource()\n } else {\n if (typeof fetch !== 'function')\n throw new Error('[LogViewer] fetch is unavailable')\n const response = await fetch(this.config.dataSource, {\n signal: this.abortController?.signal,\n })\n if (!response.ok) {\n throw new Error(\n `[LogViewer] dataSource request failed: ${response.status}`\n )\n }\n payload = await response.json()\n }\n\n if (this.destroyed || currentRequest !== this.requestId) return\n this.setData(sourceEntries(payload))\n } catch (error) {\n if (\n this.destroyed ||\n (typeof DOMException !== 'undefined' &&\n error instanceof DOMException &&\n error.name === 'AbortError')\n ) {\n return\n }\n this.config.onError?.(error)\n this.emitter.emit('error', error)\n throw error\n }\n }\n\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.requestId++\n this.abortController?.abort()\n this.abortController = null\n this.emitter.removeAllListeners()\n this.expandedIds.clear()\n this.root.replaceChildren()\n this.root.remove()\n }\n\n private applyFilterAndRender(): void {\n this.filteredData = this.filterEngine.filter(this.data, this.filter)\n this.filteredData = this.filteredData\n .map((entry, index) => ({ entry, index }))\n .sort((a, b) => {\n const left = new Date(a.entry.timestamp).getTime()\n const right = new Date(b.entry.timestamp).getTime()\n const leftTime = Number.isFinite(left) ? left : Number.MIN_SAFE_INTEGER\n const rightTime = Number.isFinite(right)\n ? right\n : Number.MIN_SAFE_INTEGER\n return rightTime - leftTime || a.index - b.index\n })\n .map(({ entry }) => entry)\n\n const totalPages = Math.max(\n 1,\n Math.ceil(this.filteredData.length / this.pagination.pageSize)\n )\n this.pagination.page = Math.min(this.pagination.page, totalPages)\n this.pagination.total = this.filteredData.length\n this.renderTimeline()\n }\n\n private getPageData(): LogEntry[] {\n const { page, pageSize } = this.pagination\n const start = (page - 1) * pageSize\n return this.filteredData.slice(start, start + pageSize)\n }\n\n private renderFilterBar(): HTMLElement | null {\n if (this.config.showFilter === false) return null\n\n const filter = document.createElement('div')\n filter.className = 'lv-filter'\n filter.setAttribute('role', 'search')\n\n const addInput = (\n field: 'user' | 'module' | 'ip' | 'keyword',\n label: string\n ) => {\n const item = document.createElement('label')\n item.className = 'lv-filter-item'\n const text = document.createElement('span')\n text.className = 'lv-filter-label'\n text.textContent = label\n const input = document.createElement('input')\n input.className = 'lv-filter-input'\n input.type = 'search'\n input.value = String(this.filter[field] || '')\n input.setAttribute('data-filter-field', field)\n input.setAttribute('aria-label', label)\n item.append(text, input)\n filter.appendChild(item)\n }\n\n addInput('keyword', this.config.locale === 'en-US' ? 'Keyword' : '关键词')\n addInput('user', this.config.locale === 'en-US' ? 'User' : '用户')\n addInput('module', this.config.locale === 'en-US' ? 'Module' : '模块')\n addInput('ip', 'IP')\n\n const actionItem = document.createElement('label')\n actionItem.className = 'lv-filter-item'\n const actionLabel = document.createElement('span')\n actionLabel.className = 'lv-filter-label'\n actionLabel.textContent = this.config.locale === 'en-US' ? 'Action' : '操作'\n const action = document.createElement('select')\n action.className = 'lv-filter-select'\n action.setAttribute('data-filter-field', 'action')\n action.setAttribute('aria-label', actionLabel.textContent)\n const allOption = document.createElement('option')\n allOption.value = ''\n allOption.textContent = this.config.locale === 'en-US' ? 'All' : '全部'\n action.appendChild(allOption)\n for (const [value, label] of Object.entries(ACTION_LABELS)) {\n const option = document.createElement('option')\n option.value = value\n option.textContent = label\n action.appendChild(option)\n }\n const selectedAction = Array.isArray(this.filter.action)\n ? ''\n : this.filter.action || ''\n action.value = selectedAction\n actionItem.append(actionLabel, action)\n filter.appendChild(actionItem)\n\n const reset = document.createElement('button')\n reset.type = 'button'\n reset.className = 'lv-filter-btn'\n reset.setAttribute('data-filter-reset', 'true')\n reset.textContent = this.config.locale === 'en-US' ? 'Reset' : '重置'\n filter.appendChild(reset)\n return filter\n }\n\n private updateFilterField(field: string, value: string): void {\n const next = this.getFilter()\n if (!value.trim()) {\n delete next[field as keyof FilterConfig]\n } else if (field === 'action' && isActionType(value)) {\n next.action = value\n } else if (field !== 'action') {\n next[field as 'user' | 'module' | 'ip' | 'keyword'] = value\n }\n this.setFilter(next)\n }\n\n private renderTimeline(): void {\n if (this.destroyed) return\n\n const pageData = this.getPageData()\n const timeline = this.timelineRenderer.render(pageData, this.expandedIds, {\n showDiff: this.config.showDiff,\n showAnomalyBadge: this.config.showAnomalyBadge,\n locale: this.config.locale,\n })\n\n if (this.config.showDiff !== false) {\n for (const entry of pageData) {\n if (!this.expandedIds.has(entry.id)) continue\n const item = [...timeline.children].find(\n child => child.getAttribute('data-id') === entry.id\n )\n const card = item?.querySelector('.lv-timeline-card')\n card?.appendChild(this.diffRenderer.renderDetail(entry))\n }\n }\n\n this.filterEl?.remove()\n this.timelineEl?.remove()\n this.paginationEl?.remove()\n\n this.filterEl = this.renderFilterBar()\n this.timelineEl = timeline\n this.paginationEl = this.timelineRenderer.renderPagination(\n this.pagination.page,\n this.pagination.pageSize,\n this.filteredData.length\n )\n\n if (this.filterEl) this.root.appendChild(this.filterEl)\n this.root.append(this.timelineEl, this.paginationEl)\n }\n\n private bindEvents(): void {\n this.root.addEventListener('change', event => {\n const target = event.target\n if (\n !(target instanceof HTMLInputElement) &&\n !(target instanceof HTMLSelectElement)\n )\n return\n const field = target.getAttribute('data-filter-field')\n if (field) this.updateFilterField(field, target.value)\n })\n\n this.root.addEventListener('click', event => {\n const target = event.target\n if (!(target instanceof Element)) return\n\n const reset = target.closest('[data-filter-reset]')\n if (reset) {\n this.resetFilter()\n return\n }\n\n const hint = target.closest('.lv-timeline-expand-hint')\n const item =\n hint?.closest('.lv-timeline-item') ||\n target.closest('.lv-timeline-item')\n const id = item?.getAttribute('data-id')\n if (hint && id) {\n if (this.expandedIds.has(id)) this.collapseEntry(id)\n else this.expandEntry(id)\n return\n }\n\n const pageButton = target.closest(\n '.lv-pagination-btn'\n ) as HTMLButtonElement | null\n if (pageButton && !pageButton.disabled) {\n const page = Number(pageButton.getAttribute('data-page'))\n if (Number.isFinite(page)) this.goToPage(page)\n return\n }\n\n const card = target.closest('.lv-timeline-card')\n if (!card || !id) return\n const entry = this.filteredData.find(candidate => candidate.id === id)\n if (entry) {\n const snapshot = deepClone(entry)\n this.emitter.emit('entry:click', snapshot)\n this.config.onEntryClick?.(snapshot)\n }\n })\n }\n}\n"],"mappings":"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"}
@@ -23,4 +23,3 @@ export declare class FilterEngine {
23
23
  */
24
24
  getUniqueActions(entries: LogEntry[]): string[];
25
25
  }
26
- //# sourceMappingURL=FilterEngine.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -13,39 +13,44 @@ var FilterEngine = class {
13
13
  * 对日志条目进行多维筛选
14
14
  */
15
15
  filter(entries, config) {
16
- let result = entries;
17
- if (config.user) {
18
- const keyword = config.user.toLowerCase();
19
- result = result.filter((e) => e.user.toLowerCase().includes(keyword) || e.userId && e.userId.toLowerCase().includes(keyword));
16
+ let result = Array.isArray(entries) ? entries.slice() : [];
17
+ const normalizedConfig = config || {};
18
+ if (normalizedConfig.user?.trim()) {
19
+ const keyword = normalizedConfig.user.trim().toLowerCase();
20
+ result = result.filter((e) => e.user?.toLowerCase().includes(keyword) || e.userId?.toLowerCase().includes(keyword));
20
21
  }
21
- if (config.action) {
22
- const actions = Array.isArray(config.action) ? config.action : [config.action];
23
- result = result.filter((e) => actions.includes(e.action));
22
+ if (normalizedConfig.action) {
23
+ const actions = Array.isArray(normalizedConfig.action) ? normalizedConfig.action : [normalizedConfig.action];
24
+ if (actions.length > 0) result = result.filter((e) => actions.includes(e.action));
24
25
  }
25
- if (config.module) {
26
- const mod = config.module.toLowerCase();
27
- result = result.filter((e) => e.module.toLowerCase().includes(mod));
26
+ if (normalizedConfig.module?.trim()) {
27
+ const mod = normalizedConfig.module.trim().toLowerCase();
28
+ result = result.filter((e) => e.module?.toLowerCase().includes(mod));
28
29
  }
29
- if (config.ip) {
30
- const ip = config.ip.toLowerCase();
31
- result = result.filter((e) => e.ip && e.ip.toLowerCase().includes(ip));
30
+ if (normalizedConfig.ip?.trim()) {
31
+ const ip = normalizedConfig.ip.trim().toLowerCase();
32
+ result = result.filter((e) => e.ip?.toLowerCase().includes(ip));
32
33
  }
33
- if (config.timeRange) {
34
- const [start, end] = config.timeRange;
34
+ if (normalizedConfig.timeRange) {
35
+ const [start, end] = normalizedConfig.timeRange;
35
36
  const startTime = new Date(start).getTime();
36
37
  const endTime = new Date(end).getTime();
37
- result = result.filter((e) => {
38
- const t = new Date(e.timestamp).getTime();
39
- return t >= startTime && t <= endTime;
40
- });
38
+ if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
39
+ const lower = Math.min(startTime, endTime);
40
+ const upper = Math.max(startTime, endTime);
41
+ result = result.filter((e) => {
42
+ const t = new Date(e.timestamp).getTime();
43
+ return Number.isFinite(t) && t >= lower && t <= upper;
44
+ });
45
+ }
41
46
  }
42
- if (config.keyword) {
43
- const kw = config.keyword.toLowerCase();
47
+ if (normalizedConfig.keyword?.trim()) {
48
+ const kw = normalizedConfig.keyword.trim().toLowerCase();
44
49
  result = result.filter((e) => {
45
50
  return [
46
- e.user,
51
+ e.user || "",
47
52
  e.action,
48
- e.module,
53
+ e.module || "",
49
54
  e.detail || "",
50
55
  e.ip || "",
51
56
  e.id
@@ -58,13 +63,13 @@ var FilterEngine = class {
58
63
  * 获取所有唯一操作人
59
64
  */
60
65
  getUniqueUsers(entries) {
61
- return [...new Set(entries.map((e) => e.user))].sort();
66
+ return [...new Set(entries.map((e) => e.user).filter(Boolean))].sort();
62
67
  }
63
68
  /**
64
69
  * 获取所有唯一模块
65
70
  */
66
71
  getUniqueModules(entries) {
67
- return [...new Set(entries.map((e) => e.module))].sort();
72
+ return [...new Set(entries.map((e) => e.module).filter(Boolean))].sort();
68
73
  }
69
74
  /**
70
75
  * 获取所有唯一操作类型
@@ -1 +1 @@
1
- {"version":3,"file":"FilterEngine.js","names":[],"sources":["../../src/filters/FilterEngine.ts"],"sourcesContent":["/**\n * 筛选引擎 - 支持按操作人/操作类型/时间范围/模块/IP/关键词筛选\n */\n\nimport type { LogEntry, FilterConfig } from '../types'\n\n/**\n * FilterEngine - Filter Engine class\n */\nexport class FilterEngine {\n /**\n * 对日志条目进行多维筛选\n */\n filter(entries: LogEntry[], config: FilterConfig): LogEntry[] {\n let result = entries\n\n // 按操作人筛选\n if (config.user) {\n const keyword = config.user.toLowerCase()\n result = result.filter(\n (e) =>\n e.user.toLowerCase().includes(keyword) ||\n (e.userId && e.userId.toLowerCase().includes(keyword)),\n )\n }\n\n // 按操作类型筛选\n if (config.action) {\n const actions = Array.isArray(config.action) ? config.action : [config.action]\n result = result.filter((e) => actions.includes(e.action))\n }\n\n // 按模块筛选\n if (config.module) {\n const mod = config.module.toLowerCase()\n result = result.filter((e) => e.module.toLowerCase().includes(mod))\n }\n\n // 按 IP 筛选\n if (config.ip) {\n const ip = config.ip.toLowerCase()\n result = result.filter((e) => e.ip && e.ip.toLowerCase().includes(ip))\n }\n\n // 按时间范围筛选\n if (config.timeRange) {\n const [start, end] = config.timeRange\n const startTime = new Date(start).getTime()\n const endTime = new Date(end).getTime()\n result = result.filter((e) => {\n const t = new Date(e.timestamp).getTime()\n return t >= startTime && t <= endTime\n })\n }\n\n // 按关键词全文搜索\n if (config.keyword) {\n const kw = config.keyword.toLowerCase()\n result = result.filter((e) => {\n const texts = [\n e.user,\n e.action,\n e.module,\n e.detail || '',\n e.ip || '',\n e.id,\n ]\n return texts.some((t) => t.toLowerCase().includes(kw))\n })\n }\n\n return result\n }\n\n /**\n * 获取所有唯一操作人\n */\n getUniqueUsers(entries: LogEntry[]): string[] {\n return [...new Set(entries.map((e) => e.user))].sort()\n }\n\n /**\n * 获取所有唯一模块\n */\n getUniqueModules(entries: LogEntry[]): string[] {\n return [...new Set(entries.map((e) => e.module))].sort()\n }\n\n /**\n * 获取所有唯一操作类型\n */\n getUniqueActions(entries: LogEntry[]): string[] {\n return [...new Set(entries.map((e) => e.action))].sort()\n }\n}\n"],"mappings":";;;;;;;;;;AASO,IAAM,eAAN,MAAmB;;;;CAIxB,OAAO,SAAqB,QAAkC;EAC5D,IAAI,SAAS;EAGb,IAAI,OAAO,MAAM;GACf,MAAM,UAAU,OAAO,KAAK,YAAY;GACxC,SAAS,OAAO,QACb,MACC,EAAE,KAAK,YAAY,CAAA,CAAE,SAAS,OAAO,KACpC,EAAE,UAAU,EAAE,OAAO,YAAY,CAAA,CAAE,SAAS,OAAO,CACxD;EACF;EAGA,IAAI,OAAO,QAAQ;GACjB,MAAM,UAAU,MAAM,QAAQ,OAAO,MAAM,IAAI,OAAO,SAAS,CAAC,OAAO,MAAM;GAC7E,SAAS,OAAO,QAAQ,MAAM,QAAQ,SAAS,EAAE,MAAM,CAAC;EAC1D;EAGA,IAAI,OAAO,QAAQ;GACjB,MAAM,MAAM,OAAO,OAAO,YAAY;GACtC,SAAS,OAAO,QAAQ,MAAM,EAAE,OAAO,YAAY,CAAA,CAAE,SAAS,GAAG,CAAC;EACpE;EAGA,IAAI,OAAO,IAAI;GACb,MAAM,KAAK,OAAO,GAAG,YAAY;GACjC,SAAS,OAAO,QAAQ,MAAM,EAAE,MAAM,EAAE,GAAG,YAAY,CAAA,CAAE,SAAS,EAAE,CAAC;EACvE;EAGA,IAAI,OAAO,WAAW;GACpB,MAAM,CAAC,OAAO,OAAO,OAAO;GAC5B,MAAM,YAAY,IAAI,KAAK,KAAK,CAAA,CAAE,QAAQ;GAC1C,MAAM,UAAU,IAAI,KAAK,GAAG,CAAA,CAAE,QAAQ;GACtC,SAAS,OAAO,QAAQ,MAAM;IAC5B,MAAM,IAAI,IAAI,KAAK,EAAE,SAAS,CAAA,CAAE,QAAQ;IACxC,OAAO,KAAK,aAAa,KAAK;GAChC,CAAC;EACH;EAGA,IAAI,OAAO,SAAS;GAClB,MAAM,KAAK,OAAO,QAAQ,YAAY;GACtC,SAAS,OAAO,QAAQ,MAAM;IAS5B,OAAO;KAPL,EAAE;KACF,EAAE;KACF,EAAE;KACF,EAAE,UAAU;KACZ,EAAE,MAAM;KACR,EAAE;IAEG,CAAA,CAAM,MAAM,MAAM,EAAE,YAAY,CAAA,CAAE,SAAS,EAAE,CAAC;GACvD,CAAC;EACH;EAEA,OAAO;CACT;;;;CAKA,eAAe,SAA+B;EAC5C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA,CAAE,KAAK;CACvD;;;;CAKA,iBAAiB,SAA+B;EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA,CAAE,KAAK;CACzD;;;;CAKA,iBAAiB,SAA+B;EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA,CAAE,KAAK;CACzD;AACF"}
1
+ {"version":3,"file":"FilterEngine.js","names":[],"sources":["../../src/filters/FilterEngine.ts"],"sourcesContent":["/**\r\n * 筛选引擎 - 支持按操作人/操作类型/时间范围/模块/IP/关键词筛选\r\n */\r\n\r\nimport type { LogEntry, FilterConfig } from '../types'\r\n\r\n/**\r\n * FilterEngine - Filter Engine class\r\n */\r\nexport class FilterEngine {\r\n /**\r\n * 对日志条目进行多维筛选\r\n */\r\n filter(entries: LogEntry[], config: FilterConfig): LogEntry[] {\r\n let result = Array.isArray(entries) ? entries.slice() : []\r\n const normalizedConfig = config || {}\r\n\r\n // 按操作人筛选\r\n if (normalizedConfig.user?.trim()) {\r\n const keyword = normalizedConfig.user.trim().toLowerCase()\r\n result = result.filter(\r\n e =>\r\n e.user?.toLowerCase().includes(keyword) ||\r\n e.userId?.toLowerCase().includes(keyword)\r\n )\r\n }\r\n\r\n // 按操作类型筛选\r\n if (normalizedConfig.action) {\r\n const actions = Array.isArray(normalizedConfig.action)\r\n ? normalizedConfig.action\r\n : [normalizedConfig.action]\r\n if (actions.length > 0)\r\n result = result.filter(e => actions.includes(e.action))\r\n }\r\n\r\n // 按模块筛选\r\n if (normalizedConfig.module?.trim()) {\r\n const mod = normalizedConfig.module.trim().toLowerCase()\r\n result = result.filter(e => e.module?.toLowerCase().includes(mod))\r\n }\r\n\r\n // 按 IP 筛选\r\n if (normalizedConfig.ip?.trim()) {\r\n const ip = normalizedConfig.ip.trim().toLowerCase()\r\n result = result.filter(e => e.ip?.toLowerCase().includes(ip))\r\n }\r\n\r\n // 按时间范围筛选\r\n if (normalizedConfig.timeRange) {\r\n const [start, end] = normalizedConfig.timeRange\r\n const startTime = new Date(start).getTime()\r\n const endTime = new Date(end).getTime()\r\n if (Number.isFinite(startTime) && Number.isFinite(endTime)) {\r\n const lower = Math.min(startTime, endTime)\r\n const upper = Math.max(startTime, endTime)\r\n result = result.filter(e => {\r\n const t = new Date(e.timestamp).getTime()\r\n return Number.isFinite(t) && t >= lower && t <= upper\r\n })\r\n }\r\n }\r\n\r\n // 按关键词全文搜索\r\n if (normalizedConfig.keyword?.trim()) {\r\n const kw = normalizedConfig.keyword.trim().toLowerCase()\r\n result = result.filter(e => {\r\n const texts = [\r\n e.user || '',\r\n e.action,\r\n e.module || '',\r\n e.detail || '',\r\n e.ip || '',\r\n e.id,\r\n ]\r\n return texts.some(t => t.toLowerCase().includes(kw))\r\n })\r\n }\r\n\r\n return result\r\n }\r\n\r\n /**\r\n * 获取所有唯一操作人\r\n */\r\n getUniqueUsers(entries: LogEntry[]): string[] {\r\n return [...new Set(entries.map(e => e.user).filter(Boolean))].sort()\r\n }\r\n\r\n /**\r\n * 获取所有唯一模块\r\n */\r\n getUniqueModules(entries: LogEntry[]): string[] {\r\n return [...new Set(entries.map(e => e.module).filter(Boolean))].sort()\r\n }\r\n\r\n /**\r\n * 获取所有唯一操作类型\r\n */\r\n getUniqueActions(entries: LogEntry[]): string[] {\r\n return [...new Set(entries.map(e => e.action))].sort()\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;AASO,IAAM,eAAN,MAAmB;;;;CAIxB,OAAO,SAAqB,QAAkC;EAC5D,IAAI,SAAS,MAAM,QAAQ,OAAO,IAAI,QAAQ,MAAM,IAAI,CAAC;EACzD,MAAM,mBAAmB,UAAU,CAAC;EAGpC,IAAI,iBAAiB,MAAM,KAAK,GAAG;GACjC,MAAM,UAAU,iBAAiB,KAAK,KAAK,CAAA,CAAE,YAAY;GACzD,SAAS,OAAO,QACd,MACE,EAAE,MAAM,YAAY,CAAA,CAAE,SAAS,OAAO,KACtC,EAAE,QAAQ,YAAY,CAAA,CAAE,SAAS,OAAO,CAC5C;EACF;EAGA,IAAI,iBAAiB,QAAQ;GAC3B,MAAM,UAAU,MAAM,QAAQ,iBAAiB,MAAM,IACjD,iBAAiB,SACjB,CAAC,iBAAiB,MAAM;GAC5B,IAAI,QAAQ,SAAS,GACnB,SAAS,OAAO,QAAO,MAAK,QAAQ,SAAS,EAAE,MAAM,CAAC;EAC1D;EAGA,IAAI,iBAAiB,QAAQ,KAAK,GAAG;GACnC,MAAM,MAAM,iBAAiB,OAAO,KAAK,CAAA,CAAE,YAAY;GACvD,SAAS,OAAO,QAAO,MAAK,EAAE,QAAQ,YAAY,CAAA,CAAE,SAAS,GAAG,CAAC;EACnE;EAGA,IAAI,iBAAiB,IAAI,KAAK,GAAG;GAC/B,MAAM,KAAK,iBAAiB,GAAG,KAAK,CAAA,CAAE,YAAY;GAClD,SAAS,OAAO,QAAO,MAAK,EAAE,IAAI,YAAY,CAAA,CAAE,SAAS,EAAE,CAAC;EAC9D;EAGA,IAAI,iBAAiB,WAAW;GAC9B,MAAM,CAAC,OAAO,OAAO,iBAAiB;GACtC,MAAM,YAAY,IAAI,KAAK,KAAK,CAAA,CAAE,QAAQ;GAC1C,MAAM,UAAU,IAAI,KAAK,GAAG,CAAA,CAAE,QAAQ;GACtC,IAAI,OAAO,SAAS,SAAS,KAAK,OAAO,SAAS,OAAO,GAAG;IAC1D,MAAM,QAAQ,KAAK,IAAI,WAAW,OAAO;IACzC,MAAM,QAAQ,KAAK,IAAI,WAAW,OAAO;IACzC,SAAS,OAAO,QAAO,MAAK;KAC1B,MAAM,IAAI,IAAI,KAAK,EAAE,SAAS,CAAA,CAAE,QAAQ;KACxC,OAAO,OAAO,SAAS,CAAC,KAAK,KAAK,SAAS,KAAK;IAClD,CAAC;GACH;EACF;EAGA,IAAI,iBAAiB,SAAS,KAAK,GAAG;GACpC,MAAM,KAAK,iBAAiB,QAAQ,KAAK,CAAA,CAAE,YAAY;GACvD,SAAS,OAAO,QAAO,MAAK;IAS1B,OAAO;KAPL,EAAE,QAAQ;KACV,EAAE;KACF,EAAE,UAAU;KACZ,EAAE,UAAU;KACZ,EAAE,MAAM;KACR,EAAE;IAEG,CAAA,CAAM,MAAK,MAAK,EAAE,YAAY,CAAA,CAAE,SAAS,EAAE,CAAC;GACrD,CAAC;EACH;EAEA,OAAO;CACT;;;;CAKA,eAAe,SAA+B;EAC5C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAI,MAAK,EAAE,IAAI,CAAA,CAAE,OAAO,OAAO,CAAC,CAAC,CAAA,CAAE,KAAK;CACrE;;;;CAKA,iBAAiB,SAA+B;EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAI,MAAK,EAAE,MAAM,CAAA,CAAE,OAAO,OAAO,CAAC,CAAC,CAAA,CAAE,KAAK;CACvE;;;;CAKA,iBAAiB,SAA+B;EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC,CAAA,CAAE,KAAK;CACvD;AACF"}