@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.
- package/CHANGELOG.md +52 -0
- package/README.md +175 -37
- package/dist/index.js +614 -299
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +548 -1
- package/dist/index.min.js.map +1 -0
- package/es/.dts-output-manifest.json +1 -0
- package/es/.dts.tsbuildinfo +1 -0
- package/es/core/EventEmitter.d.ts +0 -1
- package/es/core/EventEmitter.js +13 -9
- package/es/core/EventEmitter.js.map +1 -1
- package/es/core/LogViewer.d.ts +14 -11
- package/es/core/LogViewer.js +243 -64
- package/es/core/LogViewer.js.map +1 -1
- package/es/filters/FilterEngine.d.ts +0 -1
- package/es/filters/FilterEngine.js +31 -26
- package/es/filters/FilterEngine.js.map +1 -1
- package/es/index.d.ts +7 -7
- package/es/index.js +3 -3
- package/es/index.js.map +1 -1
- package/es/renderers/DiffRenderer.d.ts +0 -1
- package/es/renderers/DiffRenderer.js +13 -3
- package/es/renderers/DiffRenderer.js.map +1 -1
- package/es/renderers/TimelineRenderer.d.ts +7 -3
- package/es/renderers/TimelineRenderer.js +56 -22
- package/es/renderers/TimelineRenderer.js.map +1 -1
- package/es/styles/index.d.ts +1 -5
- package/es/styles/index.js +167 -131
- package/es/styles/index.js.map +1 -1
- package/es/types/index.d.ts +4 -1
- package/es/types/index.js +1 -1
- package/es/types/index.js.map +1 -1
- package/es/utils/index.d.ts +1 -2
- package/es/utils/index.js +54 -6
- package/es/utils/index.js.map +1 -1
- package/esm/core/EventEmitter.js +13 -9
- package/esm/core/EventEmitter.js.map +1 -1
- package/esm/core/LogViewer.js +243 -64
- package/esm/core/LogViewer.js.map +1 -1
- package/esm/filters/FilterEngine.js +31 -26
- package/esm/filters/FilterEngine.js.map +1 -1
- package/esm/index.js +2 -2
- package/esm/index.js.map +1 -1
- package/esm/renderers/DiffRenderer.js +13 -3
- package/esm/renderers/DiffRenderer.js.map +1 -1
- package/esm/renderers/TimelineRenderer.js +56 -22
- package/esm/renderers/TimelineRenderer.js.map +1 -1
- package/esm/styles/index.js +167 -131
- package/esm/styles/index.js.map +1 -1
- package/esm/types/index.js +1 -1
- package/esm/types/index.js.map +1 -1
- package/esm/utils/index.js +54 -6
- package/esm/utils/index.js.map +1 -1
- package/lib/core/EventEmitter.cjs +13 -9
- package/lib/core/EventEmitter.cjs.map +1 -1
- package/{esm/core/EventEmitter.d.ts → lib/core/EventEmitter.d.cts} +0 -1
- package/lib/core/LogViewer.cjs +247 -68
- package/lib/core/LogViewer.cjs.map +1 -1
- package/{esm/core/LogViewer.d.ts → lib/core/LogViewer.d.cts} +14 -11
- package/lib/filters/FilterEngine.cjs +31 -26
- package/lib/filters/FilterEngine.cjs.map +1 -1
- package/lib/filters/{FilterEngine.d.ts → FilterEngine.d.cts} +0 -1
- package/lib/index.cjs +3 -3
- package/lib/index.cjs.map +1 -1
- package/{esm/index.d.ts → lib/index.d.cts} +7 -7
- package/lib/renderers/DiffRenderer.cjs +13 -3
- package/lib/renderers/DiffRenderer.cjs.map +1 -1
- package/lib/renderers/{DiffRenderer.d.ts → DiffRenderer.d.cts} +0 -1
- package/lib/renderers/TimelineRenderer.cjs +55 -21
- package/lib/renderers/TimelineRenderer.cjs.map +1 -1
- package/lib/renderers/{TimelineRenderer.d.ts → TimelineRenderer.d.cts} +7 -3
- package/lib/styles/index.cjs +167 -131
- package/lib/styles/index.cjs.map +1 -1
- package/{esm/styles/index.d.ts → lib/styles/index.d.cts} +1 -5
- package/lib/types/index.cjs +1 -1
- package/lib/types/index.cjs.map +1 -1
- package/{esm/types/index.d.ts → lib/types/index.d.cts} +4 -1
- package/lib/utils/index.cjs +54 -6
- package/lib/utils/index.cjs.map +1 -1
- package/{esm/utils/index.d.ts → lib/utils/index.d.cts} +1 -2
- package/package.json +15 -9
- package/es/core/EventEmitter.d.ts.map +0 -1
- package/es/core/LogViewer.d.ts.map +0 -1
- package/es/filters/FilterEngine.d.ts.map +0 -1
- package/es/index.d.ts.map +0 -1
- package/es/renderers/DiffRenderer.d.ts.map +0 -1
- package/es/renderers/TimelineRenderer.d.ts.map +0 -1
- package/es/styles/index.d.ts.map +0 -1
- package/es/types/index.d.ts.map +0 -1
- package/es/utils/index.d.ts.map +0 -1
- package/esm/core/EventEmitter.d.ts.map +0 -1
- package/esm/core/LogViewer.d.ts.map +0 -1
- package/esm/filters/FilterEngine.d.ts +0 -26
- package/esm/filters/FilterEngine.d.ts.map +0 -1
- package/esm/index.d.ts.map +0 -1
- package/esm/renderers/DiffRenderer.d.ts +0 -18
- package/esm/renderers/DiffRenderer.d.ts.map +0 -1
- package/esm/renderers/TimelineRenderer.d.ts +0 -22
- package/esm/renderers/TimelineRenderer.d.ts.map +0 -1
- package/esm/styles/index.d.ts.map +0 -1
- package/esm/types/index.d.ts.map +0 -1
- package/esm/utils/index.d.ts.map +0 -1
- package/lib/core/EventEmitter.d.ts +0 -11
- package/lib/core/LogViewer.d.ts +0 -46
- package/lib/index.d.ts +0 -26
- package/lib/styles/index.d.ts +0 -9
- package/lib/types/index.d.ts +0 -113
- package/lib/utils/index.d.ts +0 -18
package/dist/index.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* ***********************************
|
|
3
3
|
* @composy/log-viewer-core v0.0.1 *
|
|
4
4
|
* Built with rollup *
|
|
5
|
-
* Build time: 2024-
|
|
5
|
+
* Build time: 2024-10-12 17:29:20 *
|
|
6
6
|
* Build mode: production *
|
|
7
7
|
* Minified: No *
|
|
8
8
|
* ***********************************
|
|
9
9
|
*/
|
|
10
10
|
(function(global, factory) {
|
|
11
|
-
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.
|
|
11
|
+
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.LDesignLogViewerCore = {}));
|
|
12
12
|
})(this, function(exports) {
|
|
13
13
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
14
14
|
//#region src/types/index.ts
|
|
@@ -45,21 +45,25 @@
|
|
|
45
45
|
this.listeners = /* @__PURE__ */ new Map();
|
|
46
46
|
}
|
|
47
47
|
on(event, callback) {
|
|
48
|
+
if (!event || typeof callback !== "function") return () => void 0;
|
|
48
49
|
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
49
50
|
this.listeners.get(event).add(callback);
|
|
50
51
|
return () => this.off(event, callback);
|
|
51
52
|
}
|
|
52
53
|
off(event, callback) {
|
|
53
|
-
this.listeners.get(event)
|
|
54
|
+
const listeners = this.listeners.get(event);
|
|
55
|
+
if (!listeners) return;
|
|
56
|
+
listeners.delete(callback);
|
|
57
|
+
if (listeners.size === 0) this.listeners.delete(event);
|
|
54
58
|
}
|
|
55
59
|
emit(event, ...args) {
|
|
56
|
-
this.listeners.get(event)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
}
|
|
60
|
+
const listeners = this.listeners.get(event);
|
|
61
|
+
if (!listeners) return;
|
|
62
|
+
for (const cb of [...listeners]) try {
|
|
63
|
+
cb(...args);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error(`[LogViewer] Event handler error (${event}):`, e);
|
|
66
|
+
}
|
|
63
67
|
}
|
|
64
68
|
removeAllListeners(event) {
|
|
65
69
|
if (event) this.listeners.delete(event);
|
|
@@ -73,39 +77,44 @@
|
|
|
73
77
|
* 对日志条目进行多维筛选
|
|
74
78
|
*/
|
|
75
79
|
filter(entries, config) {
|
|
76
|
-
let result = entries;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
let result = Array.isArray(entries) ? entries.slice() : [];
|
|
81
|
+
const normalizedConfig = config || {};
|
|
82
|
+
if (normalizedConfig.user?.trim()) {
|
|
83
|
+
const keyword = normalizedConfig.user.trim().toLowerCase();
|
|
84
|
+
result = result.filter((e) => e.user?.toLowerCase().includes(keyword) || e.userId?.toLowerCase().includes(keyword));
|
|
80
85
|
}
|
|
81
|
-
if (
|
|
82
|
-
const actions = Array.isArray(
|
|
83
|
-
result = result.filter((e) => actions.includes(e.action));
|
|
86
|
+
if (normalizedConfig.action) {
|
|
87
|
+
const actions = Array.isArray(normalizedConfig.action) ? normalizedConfig.action : [normalizedConfig.action];
|
|
88
|
+
if (actions.length > 0) result = result.filter((e) => actions.includes(e.action));
|
|
84
89
|
}
|
|
85
|
-
if (
|
|
86
|
-
const mod =
|
|
87
|
-
result = result.filter((e) => e.module
|
|
90
|
+
if (normalizedConfig.module?.trim()) {
|
|
91
|
+
const mod = normalizedConfig.module.trim().toLowerCase();
|
|
92
|
+
result = result.filter((e) => e.module?.toLowerCase().includes(mod));
|
|
88
93
|
}
|
|
89
|
-
if (
|
|
90
|
-
const ip =
|
|
91
|
-
result = result.filter((e) => e.ip
|
|
94
|
+
if (normalizedConfig.ip?.trim()) {
|
|
95
|
+
const ip = normalizedConfig.ip.trim().toLowerCase();
|
|
96
|
+
result = result.filter((e) => e.ip?.toLowerCase().includes(ip));
|
|
92
97
|
}
|
|
93
|
-
if (
|
|
94
|
-
const [start, end] =
|
|
98
|
+
if (normalizedConfig.timeRange) {
|
|
99
|
+
const [start, end] = normalizedConfig.timeRange;
|
|
95
100
|
const startTime = new Date(start).getTime();
|
|
96
101
|
const endTime = new Date(end).getTime();
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
|
|
103
|
+
const lower = Math.min(startTime, endTime);
|
|
104
|
+
const upper = Math.max(startTime, endTime);
|
|
105
|
+
result = result.filter((e) => {
|
|
106
|
+
const t = new Date(e.timestamp).getTime();
|
|
107
|
+
return Number.isFinite(t) && t >= lower && t <= upper;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
101
110
|
}
|
|
102
|
-
if (
|
|
103
|
-
const kw =
|
|
111
|
+
if (normalizedConfig.keyword?.trim()) {
|
|
112
|
+
const kw = normalizedConfig.keyword.trim().toLowerCase();
|
|
104
113
|
result = result.filter((e) => {
|
|
105
114
|
return [
|
|
106
|
-
e.user,
|
|
115
|
+
e.user || "",
|
|
107
116
|
e.action,
|
|
108
|
-
e.module,
|
|
117
|
+
e.module || "",
|
|
109
118
|
e.detail || "",
|
|
110
119
|
e.ip || "",
|
|
111
120
|
e.id
|
|
@@ -118,13 +127,13 @@
|
|
|
118
127
|
* 获取所有唯一操作人
|
|
119
128
|
*/
|
|
120
129
|
getUniqueUsers(entries) {
|
|
121
|
-
return [...new Set(entries.map((e) => e.user))].sort();
|
|
130
|
+
return [...new Set(entries.map((e) => e.user).filter(Boolean))].sort();
|
|
122
131
|
}
|
|
123
132
|
/**
|
|
124
133
|
* 获取所有唯一模块
|
|
125
134
|
*/
|
|
126
135
|
getUniqueModules(entries) {
|
|
127
|
-
return [...new Set(entries.map((e) => e.module))].sort();
|
|
136
|
+
return [...new Set(entries.map((e) => e.module).filter(Boolean))].sort();
|
|
128
137
|
}
|
|
129
138
|
/**
|
|
130
139
|
* 获取所有唯一操作类型
|
|
@@ -137,11 +146,12 @@
|
|
|
137
146
|
//#region src/utils/index.ts
|
|
138
147
|
let _uid = 0;
|
|
139
148
|
function uid(prefix = "lv") {
|
|
140
|
-
|
|
149
|
+
const random = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 8);
|
|
150
|
+
return `${prefix}-${++_uid}-${random}`;
|
|
141
151
|
}
|
|
142
152
|
function formatDate(value, fmt = "YYYY-MM-DD HH:mm:ss") {
|
|
143
153
|
const d = value instanceof Date ? value : new Date(value);
|
|
144
|
-
if (isNaN(d.getTime())) return String(value);
|
|
154
|
+
if (Number.isNaN(d.getTime())) return String(value);
|
|
145
155
|
const pad = (n) => String(n).padStart(2, "0");
|
|
146
156
|
const year = String(d.getFullYear());
|
|
147
157
|
const month = pad(d.getMonth() + 1);
|
|
@@ -154,6 +164,7 @@
|
|
|
154
164
|
function formatRelativeTime(value) {
|
|
155
165
|
const d = value instanceof Date ? value : new Date(value);
|
|
156
166
|
const diff = Date.now() - d.getTime();
|
|
167
|
+
if (!Number.isFinite(diff) || diff < 0) return formatDate(d);
|
|
157
168
|
if (diff < 6e4) return "刚刚";
|
|
158
169
|
if (diff < 36e5) return `${Math.floor(diff / 6e4)} \u5206\u949F\u524D`;
|
|
159
170
|
if (diff < 864e5) return `${Math.floor(diff / 36e5)} \u5C0F\u65F6\u524D`;
|
|
@@ -172,15 +183,58 @@
|
|
|
172
183
|
}
|
|
173
184
|
function debounce(fn, delay) {
|
|
174
185
|
let timer = null;
|
|
186
|
+
const wait = Number.isFinite(delay) && delay > 0 ? delay : 0;
|
|
175
187
|
return ((...args) => {
|
|
176
188
|
if (timer) clearTimeout(timer);
|
|
177
|
-
timer = setTimeout(() =>
|
|
189
|
+
timer = setTimeout(() => {
|
|
190
|
+
timer = null;
|
|
191
|
+
fn(...args);
|
|
192
|
+
}, wait);
|
|
178
193
|
});
|
|
179
194
|
}
|
|
180
195
|
function deepClone(obj) {
|
|
181
196
|
if (obj === null || typeof obj !== "object") return obj;
|
|
197
|
+
if (typeof structuredClone === "function") try {
|
|
198
|
+
return structuredClone(obj);
|
|
199
|
+
} catch {}
|
|
200
|
+
const seen = /* @__PURE__ */ new WeakMap();
|
|
201
|
+
const clone = (value) => {
|
|
202
|
+
if (value === null || typeof value !== "object") return value;
|
|
203
|
+
if (seen.has(value)) return seen.get(value);
|
|
204
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
205
|
+
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
|
|
206
|
+
if (value instanceof Map) {
|
|
207
|
+
const result2 = /* @__PURE__ */ new Map();
|
|
208
|
+
seen.set(value, result2);
|
|
209
|
+
value.forEach((mapValue, key) => result2.set(clone(key), clone(mapValue)));
|
|
210
|
+
return result2;
|
|
211
|
+
}
|
|
212
|
+
if (value instanceof Set) {
|
|
213
|
+
const result2 = /* @__PURE__ */ new Set();
|
|
214
|
+
seen.set(value, result2);
|
|
215
|
+
value.forEach((setValue) => result2.add(clone(setValue)));
|
|
216
|
+
return result2;
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(value)) {
|
|
219
|
+
const result2 = [];
|
|
220
|
+
seen.set(value, result2);
|
|
221
|
+
value.forEach((item) => result2.push(clone(item)));
|
|
222
|
+
return result2;
|
|
223
|
+
}
|
|
224
|
+
const result = Object.create(Object.getPrototypeOf(value));
|
|
225
|
+
seen.set(value, result);
|
|
226
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
227
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
228
|
+
if (!descriptor || !("value" in descriptor)) continue;
|
|
229
|
+
Object.defineProperty(result, key, {
|
|
230
|
+
...descriptor,
|
|
231
|
+
value: clone(descriptor.value)
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
};
|
|
182
236
|
try {
|
|
183
|
-
return
|
|
237
|
+
return clone(obj);
|
|
184
238
|
} catch {
|
|
185
239
|
return obj;
|
|
186
240
|
}
|
|
@@ -188,13 +242,73 @@
|
|
|
188
242
|
function formatValue(val) {
|
|
189
243
|
if (val === null || val === void 0) return "-";
|
|
190
244
|
if (typeof val === "object") try {
|
|
191
|
-
return JSON.stringify(val,
|
|
245
|
+
return JSON.stringify(val, (_key, value) => {
|
|
246
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
247
|
+
return value;
|
|
248
|
+
}, 2);
|
|
192
249
|
} catch {
|
|
193
250
|
return String(val);
|
|
194
251
|
}
|
|
195
252
|
return String(val);
|
|
196
253
|
}
|
|
197
254
|
//#endregion
|
|
255
|
+
//#region src/renderers/DiffRenderer.ts
|
|
256
|
+
var DiffRenderer = class {
|
|
257
|
+
/**
|
|
258
|
+
* 渲染详情面板(包含 diff 表格和 metadata)
|
|
259
|
+
*/
|
|
260
|
+
renderDetail(entry) {
|
|
261
|
+
const container = document.createElement("div");
|
|
262
|
+
container.className = "lv-detail";
|
|
263
|
+
if (entry.changes && entry.changes.length > 0) container.appendChild(this.renderDiffTable(entry.changes));
|
|
264
|
+
if (entry.metadata && Object.keys(entry.metadata).length > 0) {
|
|
265
|
+
const meta = document.createElement("div");
|
|
266
|
+
meta.className = "lv-metadata";
|
|
267
|
+
meta.textContent = formatValue(entry.metadata);
|
|
268
|
+
container.appendChild(meta);
|
|
269
|
+
}
|
|
270
|
+
return container;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 渲染 Diff 表格
|
|
274
|
+
*/
|
|
275
|
+
renderDiffTable(changes) {
|
|
276
|
+
const table = document.createElement("table");
|
|
277
|
+
table.className = "lv-diff-table";
|
|
278
|
+
const thead = document.createElement("thead");
|
|
279
|
+
const headerRow = document.createElement("tr");
|
|
280
|
+
for (const label of [
|
|
281
|
+
"字段",
|
|
282
|
+
"变更前",
|
|
283
|
+
"变更后"
|
|
284
|
+
]) {
|
|
285
|
+
const th = document.createElement("th");
|
|
286
|
+
th.textContent = label;
|
|
287
|
+
headerRow.appendChild(th);
|
|
288
|
+
}
|
|
289
|
+
thead.appendChild(headerRow);
|
|
290
|
+
table.appendChild(thead);
|
|
291
|
+
const tbody = document.createElement("tbody");
|
|
292
|
+
for (const change of changes) {
|
|
293
|
+
const tr = document.createElement("tr");
|
|
294
|
+
const tdField = document.createElement("td");
|
|
295
|
+
tdField.textContent = change.label || change.field;
|
|
296
|
+
tr.appendChild(tdField);
|
|
297
|
+
const tdOld = document.createElement("td");
|
|
298
|
+
tdOld.className = "lv-diff-old";
|
|
299
|
+
tdOld.textContent = formatValue(change.oldValue);
|
|
300
|
+
tr.appendChild(tdOld);
|
|
301
|
+
const tdNew = document.createElement("td");
|
|
302
|
+
tdNew.className = "lv-diff-new";
|
|
303
|
+
tdNew.textContent = formatValue(change.newValue);
|
|
304
|
+
tr.appendChild(tdNew);
|
|
305
|
+
tbody.appendChild(tr);
|
|
306
|
+
}
|
|
307
|
+
table.appendChild(tbody);
|
|
308
|
+
return table;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
//#endregion
|
|
198
312
|
//#region src/renderers/TimelineRenderer.ts
|
|
199
313
|
const ANOMALY_LABELS = {
|
|
200
314
|
"off-hours": "非工作时间",
|
|
@@ -202,29 +316,53 @@
|
|
|
202
316
|
"high-frequency": "高频操作",
|
|
203
317
|
none: ""
|
|
204
318
|
};
|
|
319
|
+
const ANOMALY_LABELS_EN = {
|
|
320
|
+
"off-hours": "Off hours",
|
|
321
|
+
sensitive: "Sensitive",
|
|
322
|
+
"high-frequency": "High frequency",
|
|
323
|
+
none: ""
|
|
324
|
+
};
|
|
325
|
+
const ACTION_LABELS_EN = {
|
|
326
|
+
create: "Create",
|
|
327
|
+
update: "Update",
|
|
328
|
+
delete: "Delete",
|
|
329
|
+
query: "Query",
|
|
330
|
+
login: "Login",
|
|
331
|
+
logout: "Logout",
|
|
332
|
+
export: "Export",
|
|
333
|
+
import: "Import",
|
|
334
|
+
approve: "Approve",
|
|
335
|
+
reject: "Reject",
|
|
336
|
+
other: "Other"
|
|
337
|
+
};
|
|
205
338
|
var TimelineRenderer = class {
|
|
206
339
|
/**
|
|
207
340
|
* 渲染时间线
|
|
208
341
|
*/
|
|
209
|
-
render(entries, expandedIds) {
|
|
342
|
+
render(entries, expandedIds, options = {}) {
|
|
210
343
|
const container = document.createElement("div");
|
|
211
344
|
container.className = "lv-timeline";
|
|
212
345
|
if (entries.length === 0) {
|
|
213
|
-
|
|
346
|
+
const emptyIcon = document.createElement("div");
|
|
347
|
+
emptyIcon.className = "lv-empty-icon";
|
|
348
|
+
emptyIcon.textContent = "--";
|
|
349
|
+
const emptyText = document.createElement("div");
|
|
350
|
+
emptyText.textContent = options.locale === "en-US" ? "No log entries" : "暂无日志记录";
|
|
351
|
+
container.append(emptyIcon, emptyText);
|
|
214
352
|
container.className = "";
|
|
215
353
|
return container;
|
|
216
354
|
}
|
|
217
|
-
for (const entry of entries) container.appendChild(this.renderItem(entry, expandedIds.has(entry.id)));
|
|
355
|
+
for (const entry of entries) container.appendChild(this.renderItem(entry, expandedIds.has(entry.id), options));
|
|
218
356
|
return container;
|
|
219
357
|
}
|
|
220
358
|
/**
|
|
221
359
|
* 渲染单条时间线条目
|
|
222
360
|
*/
|
|
223
|
-
renderItem(entry, expanded) {
|
|
361
|
+
renderItem(entry, expanded, options = {}) {
|
|
224
362
|
const item = document.createElement("div");
|
|
225
363
|
item.className = "lv-timeline-item";
|
|
226
364
|
item.setAttribute("data-id", entry.id);
|
|
227
|
-
const isAnomaly = entry.anomaly && entry.anomaly !== "none";
|
|
365
|
+
const isAnomaly = options.showAnomalyBadge !== false && entry.anomaly && entry.anomaly !== "none";
|
|
228
366
|
const dot = document.createElement("div");
|
|
229
367
|
dot.className = `lv-timeline-dot${isAnomaly ? " lv-timeline-dot--anomaly" : ""}`;
|
|
230
368
|
item.appendChild(dot);
|
|
@@ -241,9 +379,8 @@
|
|
|
241
379
|
user.textContent = entry.user;
|
|
242
380
|
header.appendChild(user);
|
|
243
381
|
const actionTag = document.createElement("span");
|
|
244
|
-
actionTag.className =
|
|
245
|
-
actionTag.textContent = ACTION_LABELS[entry.action] || entry.action;
|
|
246
|
-
actionTag.style.backgroundColor = ACTION_COLORS[entry.action] || "#8c8c8c";
|
|
382
|
+
actionTag.className = `lv-action-tag lv-action-tag--${Object.keys(ACTION_LABELS).includes(entry.action) ? entry.action : "other"}`;
|
|
383
|
+
actionTag.textContent = (options.locale === "en-US" ? ACTION_LABELS_EN[entry.action] : ACTION_LABELS[entry.action]) || entry.action;
|
|
247
384
|
header.appendChild(actionTag);
|
|
248
385
|
const moduleBadge = document.createElement("span");
|
|
249
386
|
moduleBadge.className = "lv-timeline-module";
|
|
@@ -255,10 +392,10 @@
|
|
|
255
392
|
ip.textContent = entry.ip;
|
|
256
393
|
header.appendChild(ip);
|
|
257
394
|
}
|
|
258
|
-
if (isAnomaly) {
|
|
395
|
+
if (options.showAnomalyBadge !== false && isAnomaly) {
|
|
259
396
|
const badge = document.createElement("span");
|
|
260
397
|
badge.className = "lv-anomaly-badge";
|
|
261
|
-
badge.textContent = ANOMALY_LABELS[entry.anomaly];
|
|
398
|
+
badge.textContent = (options.locale === "en-US" ? ANOMALY_LABELS_EN[entry.anomaly] : ANOMALY_LABELS)[entry.anomaly] || entry.anomaly;
|
|
262
399
|
header.appendChild(badge);
|
|
263
400
|
}
|
|
264
401
|
card.appendChild(header);
|
|
@@ -268,9 +405,11 @@
|
|
|
268
405
|
detail.textContent = entry.detail;
|
|
269
406
|
card.appendChild(detail);
|
|
270
407
|
}
|
|
271
|
-
if (entry.changes && entry.changes.length > 0) {
|
|
272
|
-
const hint = document.createElement("
|
|
408
|
+
if (options.showDiff !== false && entry.changes && entry.changes.length > 0) {
|
|
409
|
+
const hint = document.createElement("button");
|
|
410
|
+
hint.type = "button";
|
|
273
411
|
hint.className = "lv-timeline-expand-hint";
|
|
412
|
+
hint.setAttribute("aria-expanded", String(expanded));
|
|
274
413
|
hint.textContent = expanded ? "[ 收起变更详情 ]" : `[ \u67E5\u770B\u53D8\u66F4\u8BE6\u60C5 (${entry.changes.length} \u4E2A\u5B57\u6BB5) ]`;
|
|
275
414
|
card.appendChild(hint);
|
|
276
415
|
}
|
|
@@ -283,86 +422,48 @@
|
|
|
283
422
|
renderPagination(page, pageSize, total) {
|
|
284
423
|
const container = document.createElement("div");
|
|
285
424
|
container.className = "lv-pagination";
|
|
286
|
-
const
|
|
425
|
+
const safePageSize = Number.isFinite(pageSize) && pageSize > 0 ? Math.floor(pageSize) : 1;
|
|
426
|
+
const safeTotal = Number.isFinite(total) && total > 0 ? Math.floor(total) : 0;
|
|
427
|
+
const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
|
|
428
|
+
const totalPages = Math.ceil(safeTotal / safePageSize);
|
|
287
429
|
if (totalPages <= 1) return container;
|
|
288
430
|
const prev = document.createElement("button");
|
|
431
|
+
prev.type = "button";
|
|
289
432
|
prev.className = "lv-pagination-btn";
|
|
290
433
|
prev.textContent = "<";
|
|
291
|
-
prev.disabled =
|
|
292
|
-
prev.setAttribute("
|
|
434
|
+
prev.disabled = safePage <= 1;
|
|
435
|
+
prev.setAttribute("aria-label", "Previous page");
|
|
436
|
+
prev.setAttribute("data-page", String(safePage - 1));
|
|
293
437
|
container.appendChild(prev);
|
|
294
438
|
const maxVisible = 7;
|
|
295
|
-
let startPage = Math.max(1,
|
|
439
|
+
let startPage = Math.max(1, safePage - Math.floor(maxVisible / 2));
|
|
296
440
|
const endPage = Math.min(totalPages, startPage + maxVisible - 1);
|
|
297
441
|
if (endPage - startPage < maxVisible - 1) startPage = Math.max(1, endPage - maxVisible + 1);
|
|
298
442
|
for (let i = startPage; i <= endPage; i++) {
|
|
299
443
|
const btn = document.createElement("button");
|
|
300
|
-
btn.
|
|
444
|
+
btn.type = "button";
|
|
445
|
+
btn.className = `lv-pagination-btn${i === safePage ? " lv-pagination-btn--active" : ""}`;
|
|
301
446
|
btn.textContent = String(i);
|
|
302
447
|
btn.setAttribute("data-page", String(i));
|
|
448
|
+
if (i === safePage) btn.setAttribute("aria-current", "page");
|
|
303
449
|
container.appendChild(btn);
|
|
304
450
|
}
|
|
305
451
|
const next = document.createElement("button");
|
|
452
|
+
next.type = "button";
|
|
306
453
|
next.className = "lv-pagination-btn";
|
|
307
454
|
next.textContent = ">";
|
|
308
|
-
next.disabled =
|
|
309
|
-
next.setAttribute("
|
|
455
|
+
next.disabled = safePage >= totalPages;
|
|
456
|
+
next.setAttribute("aria-label", "Next page");
|
|
457
|
+
next.setAttribute("data-page", String(safePage + 1));
|
|
310
458
|
container.appendChild(next);
|
|
311
459
|
const info = document.createElement("span");
|
|
312
460
|
info.className = "lv-pagination-info";
|
|
313
|
-
info.textContent = `\u5171 ${
|
|
461
|
+
info.textContent = `\u5171 ${safeTotal} \u6761`;
|
|
314
462
|
container.appendChild(info);
|
|
315
463
|
return container;
|
|
316
464
|
}
|
|
317
465
|
};
|
|
318
466
|
//#endregion
|
|
319
|
-
//#region src/renderers/DiffRenderer.ts
|
|
320
|
-
var DiffRenderer = class {
|
|
321
|
-
/**
|
|
322
|
-
* 渲染详情面板(包含 diff 表格和 metadata)
|
|
323
|
-
*/
|
|
324
|
-
renderDetail(entry) {
|
|
325
|
-
const container = document.createElement("div");
|
|
326
|
-
container.className = "lv-detail";
|
|
327
|
-
if (entry.changes && entry.changes.length > 0) container.appendChild(this.renderDiffTable(entry.changes));
|
|
328
|
-
if (entry.metadata && Object.keys(entry.metadata).length > 0) {
|
|
329
|
-
const meta = document.createElement("div");
|
|
330
|
-
meta.className = "lv-metadata";
|
|
331
|
-
meta.textContent = JSON.stringify(entry.metadata, null, 2);
|
|
332
|
-
container.appendChild(meta);
|
|
333
|
-
}
|
|
334
|
-
return container;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* 渲染 Diff 表格
|
|
338
|
-
*/
|
|
339
|
-
renderDiffTable(changes) {
|
|
340
|
-
const table = document.createElement("table");
|
|
341
|
-
table.className = "lv-diff-table";
|
|
342
|
-
const thead = document.createElement("thead");
|
|
343
|
-
thead.innerHTML = `<tr><th>\u5B57\u6BB5</th><th>\u53D8\u66F4\u524D</th><th>\u53D8\u66F4\u540E</th></tr>`;
|
|
344
|
-
table.appendChild(thead);
|
|
345
|
-
const tbody = document.createElement("tbody");
|
|
346
|
-
for (const change of changes) {
|
|
347
|
-
const tr = document.createElement("tr");
|
|
348
|
-
const tdField = document.createElement("td");
|
|
349
|
-
tdField.textContent = change.label || change.field;
|
|
350
|
-
tr.appendChild(tdField);
|
|
351
|
-
const tdOld = document.createElement("td");
|
|
352
|
-
tdOld.className = "lv-diff-old";
|
|
353
|
-
tdOld.textContent = formatValue(change.oldValue);
|
|
354
|
-
tr.appendChild(tdOld);
|
|
355
|
-
const tdNew = document.createElement("td");
|
|
356
|
-
tdNew.className = "lv-diff-new";
|
|
357
|
-
tdNew.textContent = formatValue(change.newValue);
|
|
358
|
-
tr.appendChild(tdNew);
|
|
359
|
-
tbody.appendChild(tr);
|
|
360
|
-
}
|
|
361
|
-
table.appendChild(tbody);
|
|
362
|
-
return table;
|
|
363
|
-
}
|
|
364
|
-
};
|
|
365
|
-
//#endregion
|
|
366
467
|
//#region src/styles/index.ts
|
|
367
468
|
const STYLE_ID = "ldesign-log-viewer-styles";
|
|
368
469
|
function injectStyles() {
|
|
@@ -376,54 +477,54 @@
|
|
|
376
477
|
const CSS_TEXT = `
|
|
377
478
|
/* ===== CSS Variables ===== */
|
|
378
479
|
.lv-root {
|
|
379
|
-
--lv-bg:
|
|
380
|
-
--lv-bg-secondary:
|
|
381
|
-
--lv-bg-hover:
|
|
382
|
-
--lv-border:
|
|
383
|
-
--lv-text:
|
|
384
|
-
--lv-text-secondary:
|
|
385
|
-
--lv-text-muted:
|
|
386
|
-
--lv-primary:
|
|
387
|
-
--lv-primary-light:
|
|
388
|
-
--lv-shadow:
|
|
389
|
-
--lv-timeline-line:
|
|
390
|
-
--lv-timeline-dot:
|
|
391
|
-
--lv-diff-add-bg:
|
|
392
|
-
--lv-diff-add-text:
|
|
393
|
-
--lv-diff-del-bg:
|
|
394
|
-
--lv-diff-del-text:
|
|
395
|
-
--lv-anomaly-bg:
|
|
396
|
-
--lv-anomaly-border:
|
|
397
|
-
--lv-radius:
|
|
480
|
+
--lv-bg: var(--ld-white);
|
|
481
|
+
--lv-bg-secondary: var(--ld-bg-surface-soft);
|
|
482
|
+
--lv-bg-hover: var(--ld-primary-50);
|
|
483
|
+
--lv-border: var(--ld-border);
|
|
484
|
+
--lv-text: var(--ld-text-primary);
|
|
485
|
+
--lv-text-secondary: var(--ld-text-secondary);
|
|
486
|
+
--lv-text-muted: var(--ld-text-muted);
|
|
487
|
+
--lv-primary: var(--ld-primary-500);
|
|
488
|
+
--lv-primary-light: var(--ld-primary-50);
|
|
489
|
+
--lv-shadow: color-mix(in srgb, var(--ld-text-primary) 4%, transparent);
|
|
490
|
+
--lv-timeline-line: var(--ld-border);
|
|
491
|
+
--lv-timeline-dot: var(--ld-primary-500);
|
|
492
|
+
--lv-diff-add-bg: var(--ld-success-50);
|
|
493
|
+
--lv-diff-add-text: var(--ld-success-700);
|
|
494
|
+
--lv-diff-del-bg: var(--ld-danger-50);
|
|
495
|
+
--lv-diff-del-text: var(--ld-danger-700);
|
|
496
|
+
--lv-anomaly-bg: var(--ld-warning-50);
|
|
497
|
+
--lv-anomaly-border: var(--ld-warning-300);
|
|
498
|
+
--lv-radius: var(--ld-radius-md);
|
|
398
499
|
--lv-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
|
399
500
|
--lv-font-mono: 'Fira Code', 'SF Mono', Consolas, monospace;
|
|
400
501
|
}
|
|
401
502
|
|
|
402
503
|
.lv-root[data-theme="dark"] {
|
|
403
|
-
--lv-bg:
|
|
404
|
-
--lv-bg-secondary:
|
|
405
|
-
--lv-bg-hover:
|
|
406
|
-
--lv-border:
|
|
407
|
-
--lv-text:
|
|
408
|
-
--lv-text-secondary:
|
|
409
|
-
--lv-text-muted:
|
|
410
|
-
--lv-primary:
|
|
411
|
-
--lv-primary-light:
|
|
412
|
-
--lv-shadow:
|
|
413
|
-
--lv-timeline-line:
|
|
414
|
-
--lv-timeline-dot:
|
|
415
|
-
--lv-diff-add-bg:
|
|
416
|
-
--lv-diff-add-text:
|
|
417
|
-
--lv-diff-del-bg:
|
|
418
|
-
--lv-diff-del-text:
|
|
419
|
-
--lv-anomaly-bg:
|
|
420
|
-
--lv-anomaly-border:
|
|
504
|
+
--lv-bg: var(--ld-bg-surface);
|
|
505
|
+
--lv-bg-secondary: var(--ld-bg-surface-soft);
|
|
506
|
+
--lv-bg-hover: var(--ld-primary-50);
|
|
507
|
+
--lv-border: var(--ld-border-strong);
|
|
508
|
+
--lv-text: var(--ld-text-primary);
|
|
509
|
+
--lv-text-secondary: var(--ld-text-secondary);
|
|
510
|
+
--lv-text-muted: var(--ld-text-muted);
|
|
511
|
+
--lv-primary: var(--ld-primary-400);
|
|
512
|
+
--lv-primary-light: var(--ld-primary-50);
|
|
513
|
+
--lv-shadow: color-mix(in srgb, var(--ld-text-primary) 20%, transparent);
|
|
514
|
+
--lv-timeline-line: var(--ld-border-strong);
|
|
515
|
+
--lv-timeline-dot: var(--ld-primary-400);
|
|
516
|
+
--lv-diff-add-bg: var(--ld-success-50);
|
|
517
|
+
--lv-diff-add-text: var(--ld-success-700);
|
|
518
|
+
--lv-diff-del-bg: var(--ld-danger-50);
|
|
519
|
+
--lv-diff-del-text: var(--ld-danger-700);
|
|
520
|
+
--lv-anomaly-bg: var(--ld-warning-50);
|
|
521
|
+
--lv-anomaly-border: var(--ld-warning-700);
|
|
421
522
|
}
|
|
422
523
|
|
|
423
524
|
/* ===== Root ===== */
|
|
424
525
|
.lv-root {
|
|
425
526
|
font-family: var(--lv-font);
|
|
426
|
-
font-size:
|
|
527
|
+
font-size: var(--ld-px-14);
|
|
427
528
|
color: var(--lv-text);
|
|
428
529
|
background: var(--lv-bg);
|
|
429
530
|
line-height: 1.6;
|
|
@@ -433,38 +534,38 @@
|
|
|
433
534
|
.lv-filter {
|
|
434
535
|
display: flex;
|
|
435
536
|
flex-wrap: wrap;
|
|
436
|
-
gap:
|
|
437
|
-
padding:
|
|
537
|
+
gap: var(--ld-px-10);
|
|
538
|
+
padding: var(--ld-px-16);
|
|
438
539
|
background: var(--lv-bg);
|
|
439
|
-
border:
|
|
540
|
+
border: var(--ld-px-1) solid var(--lv-border);
|
|
440
541
|
border-radius: var(--lv-radius);
|
|
441
|
-
margin-bottom:
|
|
542
|
+
margin-bottom: var(--ld-px-16);
|
|
442
543
|
align-items: center;
|
|
443
544
|
}
|
|
444
545
|
|
|
445
546
|
.lv-filter-item {
|
|
446
547
|
display: flex;
|
|
447
548
|
align-items: center;
|
|
448
|
-
gap:
|
|
449
|
-
font-size:
|
|
549
|
+
gap: var(--ld-px-6);
|
|
550
|
+
font-size: var(--ld-px-13);
|
|
450
551
|
}
|
|
451
552
|
|
|
452
553
|
.lv-filter-label {
|
|
453
554
|
color: var(--lv-text-secondary);
|
|
454
|
-
font-size:
|
|
555
|
+
font-size: var(--ld-px-12);
|
|
455
556
|
white-space: nowrap;
|
|
456
557
|
}
|
|
457
558
|
|
|
458
559
|
.lv-filter-input {
|
|
459
|
-
padding:
|
|
460
|
-
border:
|
|
461
|
-
border-radius:
|
|
462
|
-
font-size:
|
|
560
|
+
padding: var(--ld-px-5) var(--ld-px-10);
|
|
561
|
+
border: var(--ld-px-1) solid var(--lv-border);
|
|
562
|
+
border-radius: var(--ld-px-6);
|
|
563
|
+
font-size: var(--ld-px-13);
|
|
463
564
|
background: var(--lv-bg);
|
|
464
565
|
color: var(--lv-text);
|
|
465
566
|
outline: none;
|
|
466
567
|
transition: border-color 0.2s;
|
|
467
|
-
min-width:
|
|
568
|
+
min-width: var(--ld-px-120);
|
|
468
569
|
}
|
|
469
570
|
|
|
470
571
|
.lv-filter-input:focus {
|
|
@@ -472,10 +573,10 @@
|
|
|
472
573
|
}
|
|
473
574
|
|
|
474
575
|
.lv-filter-select {
|
|
475
|
-
padding:
|
|
476
|
-
border:
|
|
477
|
-
border-radius:
|
|
478
|
-
font-size:
|
|
576
|
+
padding: var(--ld-px-5) var(--ld-px-10);
|
|
577
|
+
border: var(--ld-px-1) solid var(--lv-border);
|
|
578
|
+
border-radius: var(--ld-px-6);
|
|
579
|
+
font-size: var(--ld-px-13);
|
|
479
580
|
background: var(--lv-bg);
|
|
480
581
|
color: var(--lv-text);
|
|
481
582
|
outline: none;
|
|
@@ -485,11 +586,11 @@
|
|
|
485
586
|
.lv-filter-btn {
|
|
486
587
|
display: inline-flex;
|
|
487
588
|
align-items: center;
|
|
488
|
-
gap:
|
|
489
|
-
padding:
|
|
490
|
-
font-size:
|
|
491
|
-
border:
|
|
492
|
-
border-radius:
|
|
589
|
+
gap: var(--ld-px-4);
|
|
590
|
+
padding: var(--ld-px-5) var(--ld-px-14);
|
|
591
|
+
font-size: var(--ld-px-13);
|
|
592
|
+
border: var(--ld-px-1) solid var(--lv-border);
|
|
593
|
+
border-radius: var(--ld-px-6);
|
|
493
594
|
background: var(--lv-bg);
|
|
494
595
|
color: var(--lv-text);
|
|
495
596
|
cursor: pointer;
|
|
@@ -504,7 +605,7 @@
|
|
|
504
605
|
.lv-filter-btn--primary {
|
|
505
606
|
background: var(--lv-primary);
|
|
506
607
|
border-color: var(--lv-primary);
|
|
507
|
-
color:
|
|
608
|
+
color: var(--ld-white);
|
|
508
609
|
}
|
|
509
610
|
|
|
510
611
|
.lv-filter-btn--primary:hover {
|
|
@@ -514,135 +615,171 @@
|
|
|
514
615
|
/* ===== Timeline ===== */
|
|
515
616
|
.lv-timeline {
|
|
516
617
|
position: relative;
|
|
517
|
-
padding-left:
|
|
618
|
+
padding-left: var(--ld-px-28);
|
|
518
619
|
}
|
|
519
620
|
|
|
520
621
|
.lv-timeline::before {
|
|
521
622
|
content: '';
|
|
522
623
|
position: absolute;
|
|
523
|
-
left:
|
|
624
|
+
left: var(--ld-px-9);
|
|
524
625
|
top: 0;
|
|
525
626
|
bottom: 0;
|
|
526
|
-
width:
|
|
627
|
+
width: var(--ld-px-2);
|
|
527
628
|
background: var(--lv-timeline-line);
|
|
528
629
|
}
|
|
529
630
|
|
|
530
631
|
.lv-timeline-item {
|
|
531
632
|
position: relative;
|
|
532
|
-
padding: 0 0
|
|
633
|
+
padding: 0 0 var(--ld-px-20) 0;
|
|
533
634
|
}
|
|
534
635
|
|
|
535
636
|
.lv-timeline-dot {
|
|
536
637
|
position: absolute;
|
|
537
|
-
left: -
|
|
538
|
-
top:
|
|
539
|
-
width:
|
|
540
|
-
height:
|
|
638
|
+
left: calc(-1 * var(--ld-px-25));
|
|
639
|
+
top: var(--ld-px-6);
|
|
640
|
+
width: var(--ld-px-12);
|
|
641
|
+
height: var(--ld-px-12);
|
|
541
642
|
border-radius: 50%;
|
|
542
643
|
background: var(--lv-bg);
|
|
543
|
-
border:
|
|
644
|
+
border: var(--ld-px-2) solid var(--lv-timeline-dot);
|
|
544
645
|
z-index: 1;
|
|
545
646
|
}
|
|
546
647
|
|
|
547
648
|
.lv-timeline-dot--anomaly {
|
|
548
|
-
border-color:
|
|
549
|
-
background:
|
|
649
|
+
border-color: var(--ld-danger-500);
|
|
650
|
+
background: var(--ld-danger-50);
|
|
550
651
|
}
|
|
551
652
|
|
|
552
653
|
.lv-timeline-card {
|
|
553
654
|
background: var(--lv-bg);
|
|
554
|
-
border:
|
|
655
|
+
border: var(--ld-px-1) solid var(--lv-border);
|
|
555
656
|
border-radius: var(--lv-radius);
|
|
556
|
-
padding:
|
|
657
|
+
padding: var(--ld-px-14) var(--ld-px-18);
|
|
557
658
|
cursor: pointer;
|
|
558
659
|
transition: all 0.2s;
|
|
559
|
-
box-shadow: 0
|
|
660
|
+
box-shadow: 0 var(--ld-px-1) var(--ld-px-3) var(--lv-shadow);
|
|
560
661
|
}
|
|
561
662
|
|
|
562
663
|
.lv-timeline-card:hover {
|
|
563
664
|
border-color: var(--lv-primary);
|
|
564
|
-
box-shadow: 0
|
|
665
|
+
box-shadow: 0 var(--ld-px-2) var(--ld-px-8) var(--lv-shadow);
|
|
565
666
|
}
|
|
566
667
|
|
|
567
668
|
.lv-timeline-card--anomaly {
|
|
568
|
-
border-left:
|
|
669
|
+
border-left: var(--ld-px-3) solid var(--lv-anomaly-border);
|
|
569
670
|
background: var(--lv-anomaly-bg);
|
|
570
671
|
}
|
|
571
672
|
|
|
572
673
|
.lv-timeline-header {
|
|
573
674
|
display: flex;
|
|
574
675
|
align-items: center;
|
|
575
|
-
gap:
|
|
676
|
+
gap: var(--ld-px-10);
|
|
576
677
|
flex-wrap: wrap;
|
|
577
|
-
margin-bottom:
|
|
678
|
+
margin-bottom: var(--ld-px-4);
|
|
578
679
|
}
|
|
579
680
|
|
|
580
681
|
.lv-timeline-time {
|
|
581
|
-
font-size:
|
|
682
|
+
font-size: var(--ld-px-12);
|
|
582
683
|
color: var(--lv-text-muted);
|
|
583
684
|
font-family: var(--lv-font-mono);
|
|
584
685
|
}
|
|
585
686
|
|
|
586
687
|
.lv-timeline-user {
|
|
587
688
|
font-weight: 600;
|
|
588
|
-
font-size:
|
|
689
|
+
font-size: var(--ld-px-14);
|
|
589
690
|
}
|
|
590
691
|
|
|
591
692
|
.lv-action-tag {
|
|
592
693
|
display: inline-block;
|
|
593
|
-
padding:
|
|
594
|
-
font-size:
|
|
595
|
-
border-radius:
|
|
694
|
+
padding: var(--ld-px-1) var(--ld-px-8);
|
|
695
|
+
font-size: var(--ld-px-12);
|
|
696
|
+
border-radius: var(--ld-px-4);
|
|
596
697
|
font-weight: 500;
|
|
597
|
-
color:
|
|
698
|
+
color: var(--ld-white);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.lv-action-tag--create,
|
|
702
|
+
.lv-action-tag--approve {
|
|
703
|
+
background: var(--ld-success-500);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.lv-action-tag--update,
|
|
707
|
+
.lv-action-tag--login {
|
|
708
|
+
background: var(--ld-primary-500);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.lv-action-tag--delete,
|
|
712
|
+
.lv-action-tag--reject {
|
|
713
|
+
background: var(--ld-danger-500);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.lv-action-tag--query,
|
|
717
|
+
.lv-action-tag--other {
|
|
718
|
+
background: var(--ld-text-secondary);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.lv-action-tag--logout {
|
|
722
|
+
background: var(--ld-primary-700);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.lv-action-tag--export {
|
|
726
|
+
background: var(--ld-warning-500);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.lv-action-tag--import {
|
|
730
|
+
background: var(--ld-success-500);
|
|
598
731
|
}
|
|
599
732
|
|
|
600
733
|
.lv-timeline-module {
|
|
601
|
-
font-size:
|
|
734
|
+
font-size: var(--ld-px-12);
|
|
602
735
|
color: var(--lv-text-secondary);
|
|
603
736
|
background: var(--lv-bg-secondary);
|
|
604
|
-
padding:
|
|
605
|
-
border-radius:
|
|
737
|
+
padding: var(--ld-px-1) var(--ld-px-8);
|
|
738
|
+
border-radius: var(--ld-px-4);
|
|
606
739
|
}
|
|
607
740
|
|
|
608
741
|
.lv-timeline-ip {
|
|
609
|
-
font-size:
|
|
742
|
+
font-size: var(--ld-px-12);
|
|
610
743
|
color: var(--lv-text-muted);
|
|
611
744
|
font-family: var(--lv-font-mono);
|
|
612
745
|
}
|
|
613
746
|
|
|
614
747
|
.lv-anomaly-badge {
|
|
615
|
-
font-size:
|
|
616
|
-
padding:
|
|
617
|
-
border-radius:
|
|
618
|
-
background:
|
|
619
|
-
color:
|
|
620
|
-
border:
|
|
748
|
+
font-size: var(--ld-px-11);
|
|
749
|
+
padding: var(--ld-px-1) var(--ld-px-6);
|
|
750
|
+
border-radius: var(--ld-px-3);
|
|
751
|
+
background: var(--ld-warning-50);
|
|
752
|
+
color: var(--ld-warning-700);
|
|
753
|
+
border: var(--ld-px-1) solid var(--ld-warning-300);
|
|
621
754
|
}
|
|
622
755
|
|
|
623
756
|
.lv-timeline-detail {
|
|
624
|
-
font-size:
|
|
757
|
+
font-size: var(--ld-px-13);
|
|
625
758
|
color: var(--lv-text-secondary);
|
|
626
|
-
margin-top:
|
|
759
|
+
margin-top: var(--ld-px-6);
|
|
627
760
|
}
|
|
628
761
|
|
|
629
762
|
.lv-timeline-expand-hint {
|
|
630
|
-
|
|
763
|
+
border: 0;
|
|
764
|
+
padding: 0;
|
|
765
|
+
background: transparent;
|
|
766
|
+
font: inherit;
|
|
767
|
+
font-size: var(--ld-px-12);
|
|
631
768
|
color: var(--lv-primary);
|
|
632
|
-
margin-top:
|
|
769
|
+
margin-top: var(--ld-px-6);
|
|
633
770
|
cursor: pointer;
|
|
634
771
|
}
|
|
635
772
|
|
|
636
773
|
/* ===== Detail Panel ===== */
|
|
637
774
|
.lv-detail {
|
|
638
|
-
margin-top:
|
|
639
|
-
padding-top:
|
|
640
|
-
border-top:
|
|
775
|
+
margin-top: var(--ld-px-12);
|
|
776
|
+
padding-top: var(--ld-px-12);
|
|
777
|
+
border-top: var(--ld-px-1) dashed var(--lv-border);
|
|
641
778
|
animation: lv-slide-down 0.2s ease;
|
|
642
779
|
}
|
|
643
780
|
|
|
644
781
|
@keyframes lv-slide-down {
|
|
645
|
-
from { opacity: 0; transform: translateY(-
|
|
782
|
+
from { opacity: 0; transform: translateY(calc(-1 * var(--ld-px-8))); }
|
|
646
783
|
to { opacity: 1; transform: translateY(0); }
|
|
647
784
|
}
|
|
648
785
|
|
|
@@ -650,25 +787,25 @@
|
|
|
650
787
|
.lv-diff-table {
|
|
651
788
|
width: 100%;
|
|
652
789
|
border-collapse: collapse;
|
|
653
|
-
font-size:
|
|
654
|
-
border:
|
|
655
|
-
border-radius:
|
|
790
|
+
font-size: var(--ld-px-13);
|
|
791
|
+
border: var(--ld-px-1) solid var(--lv-border);
|
|
792
|
+
border-radius: var(--ld-px-6);
|
|
656
793
|
overflow: hidden;
|
|
657
794
|
}
|
|
658
795
|
|
|
659
796
|
.lv-diff-table th {
|
|
660
797
|
background: var(--lv-bg-secondary);
|
|
661
|
-
padding:
|
|
798
|
+
padding: var(--ld-px-8) var(--ld-px-12);
|
|
662
799
|
text-align: left;
|
|
663
800
|
font-weight: 600;
|
|
664
|
-
font-size:
|
|
801
|
+
font-size: var(--ld-px-12);
|
|
665
802
|
color: var(--lv-text-secondary);
|
|
666
|
-
border-bottom:
|
|
803
|
+
border-bottom: var(--ld-px-1) solid var(--lv-border);
|
|
667
804
|
}
|
|
668
805
|
|
|
669
806
|
.lv-diff-table td {
|
|
670
|
-
padding:
|
|
671
|
-
border-bottom:
|
|
807
|
+
padding: var(--ld-px-8) var(--ld-px-12);
|
|
808
|
+
border-bottom: var(--ld-px-1) solid var(--lv-border);
|
|
672
809
|
vertical-align: top;
|
|
673
810
|
}
|
|
674
811
|
|
|
@@ -681,23 +818,23 @@
|
|
|
681
818
|
color: var(--lv-diff-del-text);
|
|
682
819
|
text-decoration: line-through;
|
|
683
820
|
font-family: var(--lv-font-mono);
|
|
684
|
-
font-size:
|
|
821
|
+
font-size: var(--ld-px-12);
|
|
685
822
|
}
|
|
686
823
|
|
|
687
824
|
.lv-diff-new {
|
|
688
825
|
background: var(--lv-diff-add-bg);
|
|
689
826
|
color: var(--lv-diff-add-text);
|
|
690
827
|
font-family: var(--lv-font-mono);
|
|
691
|
-
font-size:
|
|
828
|
+
font-size: var(--ld-px-12);
|
|
692
829
|
}
|
|
693
830
|
|
|
694
831
|
/* ===== Metadata ===== */
|
|
695
832
|
.lv-metadata {
|
|
696
|
-
margin-top:
|
|
833
|
+
margin-top: var(--ld-px-10);
|
|
697
834
|
background: var(--lv-bg-secondary);
|
|
698
|
-
border-radius:
|
|
699
|
-
padding:
|
|
700
|
-
font-size:
|
|
835
|
+
border-radius: var(--ld-px-6);
|
|
836
|
+
padding: var(--ld-px-10) var(--ld-px-14);
|
|
837
|
+
font-size: var(--ld-px-12);
|
|
701
838
|
font-family: var(--lv-font-mono);
|
|
702
839
|
white-space: pre-wrap;
|
|
703
840
|
word-break: break-all;
|
|
@@ -709,21 +846,21 @@
|
|
|
709
846
|
display: flex;
|
|
710
847
|
align-items: center;
|
|
711
848
|
justify-content: center;
|
|
712
|
-
gap:
|
|
713
|
-
padding:
|
|
849
|
+
gap: var(--ld-px-6);
|
|
850
|
+
padding: var(--ld-px-16) 0;
|
|
714
851
|
}
|
|
715
852
|
|
|
716
853
|
.lv-pagination-btn {
|
|
717
854
|
display: inline-flex;
|
|
718
855
|
align-items: center;
|
|
719
856
|
justify-content: center;
|
|
720
|
-
width:
|
|
721
|
-
height:
|
|
722
|
-
border:
|
|
723
|
-
border-radius:
|
|
857
|
+
width: var(--ld-px-32);
|
|
858
|
+
height: var(--ld-px-32);
|
|
859
|
+
border: var(--ld-px-1) solid var(--lv-border);
|
|
860
|
+
border-radius: var(--ld-px-6);
|
|
724
861
|
background: var(--lv-bg);
|
|
725
862
|
color: var(--lv-text);
|
|
726
|
-
font-size:
|
|
863
|
+
font-size: var(--ld-px-13);
|
|
727
864
|
cursor: pointer;
|
|
728
865
|
transition: all 0.15s;
|
|
729
866
|
}
|
|
@@ -736,7 +873,7 @@
|
|
|
736
873
|
.lv-pagination-btn--active {
|
|
737
874
|
background: var(--lv-primary);
|
|
738
875
|
border-color: var(--lv-primary);
|
|
739
|
-
color:
|
|
876
|
+
color: var(--ld-white);
|
|
740
877
|
}
|
|
741
878
|
|
|
742
879
|
.lv-pagination-btn:disabled {
|
|
@@ -745,27 +882,71 @@
|
|
|
745
882
|
}
|
|
746
883
|
|
|
747
884
|
.lv-pagination-info {
|
|
748
|
-
font-size:
|
|
885
|
+
font-size: var(--ld-px-13);
|
|
749
886
|
color: var(--lv-text-secondary);
|
|
750
|
-
margin: 0
|
|
887
|
+
margin: 0 var(--ld-px-8);
|
|
751
888
|
}
|
|
752
889
|
|
|
753
890
|
/* ===== Empty state ===== */
|
|
754
891
|
.lv-empty {
|
|
755
892
|
text-align: center;
|
|
756
|
-
padding:
|
|
893
|
+
padding: var(--ld-px-48) var(--ld-px-16);
|
|
757
894
|
color: var(--lv-text-muted);
|
|
758
|
-
font-size:
|
|
895
|
+
font-size: var(--ld-px-14);
|
|
759
896
|
}
|
|
760
897
|
|
|
761
898
|
.lv-empty-icon {
|
|
762
|
-
font-size:
|
|
763
|
-
margin-bottom:
|
|
899
|
+
font-size: var(--ld-px-48);
|
|
900
|
+
margin-bottom: var(--ld-px-12);
|
|
764
901
|
opacity: 0.3;
|
|
765
902
|
}
|
|
766
903
|
`;
|
|
767
904
|
//#endregion
|
|
768
905
|
//#region src/core/LogViewer.ts
|
|
906
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
907
|
+
const MAX_PAGE_SIZE = 1e3;
|
|
908
|
+
function normalizePageSize(value, fallback = DEFAULT_PAGE_SIZE) {
|
|
909
|
+
const pageSize = Number(value);
|
|
910
|
+
if (!Number.isFinite(pageSize) || pageSize <= 0) return fallback;
|
|
911
|
+
return Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
|
|
912
|
+
}
|
|
913
|
+
function normalizePage(value) {
|
|
914
|
+
const page = Number(value);
|
|
915
|
+
if (!Number.isFinite(page) || page < 1) return 1;
|
|
916
|
+
return Math.floor(page);
|
|
917
|
+
}
|
|
918
|
+
function cloneEntries(entries) {
|
|
919
|
+
if (entries === void 0) return [];
|
|
920
|
+
if (!Array.isArray(entries)) throw new TypeError("[LogViewer] data must be an array");
|
|
921
|
+
return entries.map((entry, index) => {
|
|
922
|
+
if (!entry || typeof entry !== "object") throw new TypeError(`[LogViewer] data[${index}] must be an object`);
|
|
923
|
+
const cloned = deepClone(entry);
|
|
924
|
+
const id = String(cloned.id ?? "").trim();
|
|
925
|
+
if (!id) throw new TypeError(`[LogViewer] data[${index}].id must be a non-empty string`);
|
|
926
|
+
return {
|
|
927
|
+
...cloned,
|
|
928
|
+
id,
|
|
929
|
+
user: String(cloned.user ?? ""),
|
|
930
|
+
module: String(cloned.module ?? ""),
|
|
931
|
+
changes: Array.isArray(cloned.changes) ? cloned.changes : void 0
|
|
932
|
+
};
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function isActionType(value) {
|
|
936
|
+
return Object.keys(ACTION_LABELS).includes(value);
|
|
937
|
+
}
|
|
938
|
+
function sourceEntries(payload) {
|
|
939
|
+
if (Array.isArray(payload)) return payload;
|
|
940
|
+
if (!payload || typeof payload !== "object") throw new TypeError("[LogViewer] dataSource response must be an array");
|
|
941
|
+
const record = payload;
|
|
942
|
+
for (const key of [
|
|
943
|
+
"data",
|
|
944
|
+
"logs",
|
|
945
|
+
"items",
|
|
946
|
+
"entries"
|
|
947
|
+
]) if (Array.isArray(record[key])) return record[key];
|
|
948
|
+
throw new TypeError("[LogViewer] dataSource response must contain an array");
|
|
949
|
+
}
|
|
769
950
|
var LogViewer = class {
|
|
770
951
|
constructor(config) {
|
|
771
952
|
this.emitter = new EventEmitter();
|
|
@@ -777,89 +958,107 @@
|
|
|
777
958
|
this.filter = {};
|
|
778
959
|
this.pagination = {
|
|
779
960
|
page: 1,
|
|
780
|
-
pageSize:
|
|
961
|
+
pageSize: DEFAULT_PAGE_SIZE
|
|
781
962
|
};
|
|
782
963
|
this.theme = "light";
|
|
783
964
|
this.expandedIds = /* @__PURE__ */ new Set();
|
|
965
|
+
this.filterEl = null;
|
|
784
966
|
this.timelineEl = null;
|
|
785
967
|
this.paginationEl = null;
|
|
786
968
|
this.destroyed = false;
|
|
787
|
-
this.
|
|
969
|
+
this.requestId = 0;
|
|
970
|
+
this.abortController = null;
|
|
971
|
+
if (typeof document === "undefined") throw new Error("[LogViewer] A browser document is required");
|
|
788
972
|
const container = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
|
|
789
|
-
if (!container) throw new Error(`[LogViewer] Container not found: ${config.container}`);
|
|
973
|
+
if (!container) throw new Error(`[LogViewer] Container not found: ${String(config.container)}`);
|
|
974
|
+
this.config = { ...config };
|
|
975
|
+
this.theme = config.theme === "dark" ? "dark" : "light";
|
|
976
|
+
this.pagination = {
|
|
977
|
+
page: normalizePage(config.pagination?.page),
|
|
978
|
+
pageSize: normalizePageSize(config.pageSize ?? config.pagination?.pageSize)
|
|
979
|
+
};
|
|
980
|
+
this.filter = deepClone(config.filter || {});
|
|
981
|
+
this.data = cloneEntries(config.data);
|
|
790
982
|
injectStyles();
|
|
791
983
|
this.root = document.createElement("div");
|
|
792
984
|
this.root.className = "lv-root";
|
|
793
|
-
container.appendChild(this.root);
|
|
794
|
-
this.theme = config.theme || "light";
|
|
795
985
|
this.root.setAttribute("data-theme", this.theme);
|
|
796
|
-
|
|
797
|
-
if (config.pagination) Object.assign(this.pagination, config.pagination);
|
|
798
|
-
if (config.filter) this.filter = deepClone(config.filter);
|
|
799
|
-
if (config.data) this.data = [...config.data];
|
|
800
|
-
this.applyFilterAndRender();
|
|
986
|
+
container.appendChild(this.root);
|
|
801
987
|
this.bindEvents();
|
|
988
|
+
this.applyFilterAndRender();
|
|
989
|
+
if (config.dataSource) this.loadData().catch(() => void 0);
|
|
802
990
|
}
|
|
803
991
|
getData() {
|
|
804
|
-
return
|
|
992
|
+
return deepClone(this.data);
|
|
805
993
|
}
|
|
806
994
|
setData(entries) {
|
|
807
|
-
this.
|
|
995
|
+
if (this.destroyed) return;
|
|
996
|
+
this.data = cloneEntries(entries);
|
|
808
997
|
this.pagination.page = 1;
|
|
998
|
+
this.expandedIds.clear();
|
|
809
999
|
this.applyFilterAndRender();
|
|
810
|
-
this.emitter.emit("data:change", this.
|
|
1000
|
+
this.emitter.emit("data:change", this.getData());
|
|
811
1001
|
}
|
|
812
1002
|
appendData(entries) {
|
|
813
|
-
this.
|
|
1003
|
+
if (this.destroyed) return;
|
|
1004
|
+
this.data.push(...cloneEntries(entries));
|
|
814
1005
|
this.applyFilterAndRender();
|
|
815
|
-
this.emitter.emit("data:change", this.
|
|
1006
|
+
this.emitter.emit("data:change", this.getData());
|
|
816
1007
|
}
|
|
817
1008
|
getFilter() {
|
|
818
1009
|
return deepClone(this.filter);
|
|
819
1010
|
}
|
|
820
1011
|
setFilter(filter) {
|
|
821
|
-
this.
|
|
1012
|
+
if (this.destroyed) return;
|
|
1013
|
+
this.filter = deepClone(filter || {});
|
|
822
1014
|
this.pagination.page = 1;
|
|
823
1015
|
this.applyFilterAndRender();
|
|
824
|
-
this.
|
|
825
|
-
this.
|
|
1016
|
+
const current = this.getFilter();
|
|
1017
|
+
this.emitter.emit("filter:change", current);
|
|
1018
|
+
this.config.onFilterChange?.(current);
|
|
826
1019
|
}
|
|
827
1020
|
resetFilter() {
|
|
828
|
-
this.
|
|
829
|
-
this.
|
|
830
|
-
this.applyFilterAndRender();
|
|
831
|
-
this.emitter.emit("filter:change", this.filter);
|
|
1021
|
+
if (this.destroyed) return;
|
|
1022
|
+
this.setFilter({});
|
|
832
1023
|
}
|
|
833
1024
|
getTheme() {
|
|
834
1025
|
return this.theme;
|
|
835
1026
|
}
|
|
836
1027
|
setTheme(theme) {
|
|
1028
|
+
if (this.destroyed) return;
|
|
1029
|
+
if (theme !== "light" && theme !== "dark") throw new TypeError("[LogViewer] theme must be light or dark");
|
|
1030
|
+
if (this.theme === theme) return;
|
|
837
1031
|
this.theme = theme;
|
|
838
1032
|
this.root.setAttribute("data-theme", theme);
|
|
839
1033
|
this.emitter.emit("theme:change", theme);
|
|
840
1034
|
this.config.onThemeChange?.(theme);
|
|
841
1035
|
}
|
|
842
1036
|
expandEntry(id) {
|
|
1037
|
+
if (this.destroyed || this.expandedIds.has(id)) return;
|
|
843
1038
|
this.expandedIds.add(id);
|
|
844
|
-
const entry = this.data.find((
|
|
845
|
-
if (entry) this.emitter.emit("entry:expand", entry);
|
|
1039
|
+
const entry = this.data.find((item) => item.id === id);
|
|
1040
|
+
if (entry) this.emitter.emit("entry:expand", deepClone(entry));
|
|
846
1041
|
this.renderTimeline();
|
|
847
1042
|
}
|
|
848
1043
|
collapseEntry(id) {
|
|
1044
|
+
if (this.destroyed || !this.expandedIds.has(id)) return;
|
|
849
1045
|
this.expandedIds.delete(id);
|
|
850
|
-
const entry = this.data.find((
|
|
851
|
-
if (entry) this.emitter.emit("entry:collapse", entry);
|
|
1046
|
+
const entry = this.data.find((item) => item.id === id);
|
|
1047
|
+
if (entry) this.emitter.emit("entry:collapse", deepClone(entry));
|
|
852
1048
|
this.renderTimeline();
|
|
853
1049
|
}
|
|
854
1050
|
collapseAll() {
|
|
1051
|
+
if (this.destroyed || this.expandedIds.size === 0) return;
|
|
855
1052
|
this.expandedIds.clear();
|
|
856
1053
|
this.renderTimeline();
|
|
857
1054
|
}
|
|
858
1055
|
goToPage(page) {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
this.
|
|
1056
|
+
if (this.destroyed) return;
|
|
1057
|
+
const totalPages = Math.max(1, Math.ceil(this.filteredData.length / this.pagination.pageSize));
|
|
1058
|
+
const nextPage = Math.min(totalPages, Math.max(1, normalizePage(page)));
|
|
1059
|
+
if (nextPage === this.pagination.page) return;
|
|
1060
|
+
this.pagination.page = nextPage;
|
|
1061
|
+
this.emitter.emit("page:change", nextPage);
|
|
863
1062
|
this.renderTimeline();
|
|
864
1063
|
}
|
|
865
1064
|
getPagination() {
|
|
@@ -872,17 +1071,55 @@
|
|
|
872
1071
|
return this.emitter.on(event, callback);
|
|
873
1072
|
}
|
|
874
1073
|
refresh() {
|
|
875
|
-
this.applyFilterAndRender();
|
|
1074
|
+
if (!this.destroyed) this.applyFilterAndRender();
|
|
1075
|
+
}
|
|
1076
|
+
async loadData() {
|
|
1077
|
+
if (this.destroyed || !this.config.dataSource) return;
|
|
1078
|
+
const currentRequest = ++this.requestId;
|
|
1079
|
+
this.abortController?.abort();
|
|
1080
|
+
this.abortController = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
1081
|
+
try {
|
|
1082
|
+
let payload;
|
|
1083
|
+
if (typeof this.config.dataSource === "function") payload = await this.config.dataSource();
|
|
1084
|
+
else {
|
|
1085
|
+
if (typeof fetch !== "function") throw new Error("[LogViewer] fetch is unavailable");
|
|
1086
|
+
const response = await fetch(this.config.dataSource, { signal: this.abortController?.signal });
|
|
1087
|
+
if (!response.ok) throw new Error(`[LogViewer] dataSource request failed: ${response.status}`);
|
|
1088
|
+
payload = await response.json();
|
|
1089
|
+
}
|
|
1090
|
+
if (this.destroyed || currentRequest !== this.requestId) return;
|
|
1091
|
+
this.setData(sourceEntries(payload));
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
if (this.destroyed || typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError") return;
|
|
1094
|
+
this.config.onError?.(error);
|
|
1095
|
+
this.emitter.emit("error", error);
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
876
1098
|
}
|
|
877
1099
|
destroy() {
|
|
878
1100
|
if (this.destroyed) return;
|
|
879
1101
|
this.destroyed = true;
|
|
1102
|
+
this.requestId++;
|
|
1103
|
+
this.abortController?.abort();
|
|
1104
|
+
this.abortController = null;
|
|
880
1105
|
this.emitter.removeAllListeners();
|
|
1106
|
+
this.expandedIds.clear();
|
|
1107
|
+
this.root.replaceChildren();
|
|
881
1108
|
this.root.remove();
|
|
882
1109
|
}
|
|
883
1110
|
applyFilterAndRender() {
|
|
884
1111
|
this.filteredData = this.filterEngine.filter(this.data, this.filter);
|
|
885
|
-
this.filteredData.
|
|
1112
|
+
this.filteredData = this.filteredData.map((entry, index) => ({
|
|
1113
|
+
entry,
|
|
1114
|
+
index
|
|
1115
|
+
})).sort((a, b) => {
|
|
1116
|
+
const left = new Date(a.entry.timestamp).getTime();
|
|
1117
|
+
const right = new Date(b.entry.timestamp).getTime();
|
|
1118
|
+
const leftTime = Number.isFinite(left) ? left : Number.MIN_SAFE_INTEGER;
|
|
1119
|
+
return (Number.isFinite(right) ? right : Number.MIN_SAFE_INTEGER) - leftTime || a.index - b.index;
|
|
1120
|
+
}).map(({ entry }) => entry);
|
|
1121
|
+
const totalPages = Math.max(1, Math.ceil(this.filteredData.length / this.pagination.pageSize));
|
|
1122
|
+
this.pagination.page = Math.min(this.pagination.page, totalPages);
|
|
886
1123
|
this.pagination.total = this.filteredData.length;
|
|
887
1124
|
this.renderTimeline();
|
|
888
1125
|
}
|
|
@@ -891,43 +1128,121 @@
|
|
|
891
1128
|
const start = (page - 1) * pageSize;
|
|
892
1129
|
return this.filteredData.slice(start, start + pageSize);
|
|
893
1130
|
}
|
|
1131
|
+
renderFilterBar() {
|
|
1132
|
+
if (this.config.showFilter === false) return null;
|
|
1133
|
+
const filter = document.createElement("div");
|
|
1134
|
+
filter.className = "lv-filter";
|
|
1135
|
+
filter.setAttribute("role", "search");
|
|
1136
|
+
const addInput = (field, label) => {
|
|
1137
|
+
const item = document.createElement("label");
|
|
1138
|
+
item.className = "lv-filter-item";
|
|
1139
|
+
const text = document.createElement("span");
|
|
1140
|
+
text.className = "lv-filter-label";
|
|
1141
|
+
text.textContent = label;
|
|
1142
|
+
const input = document.createElement("input");
|
|
1143
|
+
input.className = "lv-filter-input";
|
|
1144
|
+
input.type = "search";
|
|
1145
|
+
input.value = String(this.filter[field] || "");
|
|
1146
|
+
input.setAttribute("data-filter-field", field);
|
|
1147
|
+
input.setAttribute("aria-label", label);
|
|
1148
|
+
item.append(text, input);
|
|
1149
|
+
filter.appendChild(item);
|
|
1150
|
+
};
|
|
1151
|
+
addInput("keyword", this.config.locale === "en-US" ? "Keyword" : "关键词");
|
|
1152
|
+
addInput("user", this.config.locale === "en-US" ? "User" : "用户");
|
|
1153
|
+
addInput("module", this.config.locale === "en-US" ? "Module" : "模块");
|
|
1154
|
+
addInput("ip", "IP");
|
|
1155
|
+
const actionItem = document.createElement("label");
|
|
1156
|
+
actionItem.className = "lv-filter-item";
|
|
1157
|
+
const actionLabel = document.createElement("span");
|
|
1158
|
+
actionLabel.className = "lv-filter-label";
|
|
1159
|
+
actionLabel.textContent = this.config.locale === "en-US" ? "Action" : "操作";
|
|
1160
|
+
const action = document.createElement("select");
|
|
1161
|
+
action.className = "lv-filter-select";
|
|
1162
|
+
action.setAttribute("data-filter-field", "action");
|
|
1163
|
+
action.setAttribute("aria-label", actionLabel.textContent);
|
|
1164
|
+
const allOption = document.createElement("option");
|
|
1165
|
+
allOption.value = "";
|
|
1166
|
+
allOption.textContent = this.config.locale === "en-US" ? "All" : "全部";
|
|
1167
|
+
action.appendChild(allOption);
|
|
1168
|
+
for (const [value, label] of Object.entries(ACTION_LABELS)) {
|
|
1169
|
+
const option = document.createElement("option");
|
|
1170
|
+
option.value = value;
|
|
1171
|
+
option.textContent = label;
|
|
1172
|
+
action.appendChild(option);
|
|
1173
|
+
}
|
|
1174
|
+
action.value = Array.isArray(this.filter.action) ? "" : this.filter.action || "";
|
|
1175
|
+
actionItem.append(actionLabel, action);
|
|
1176
|
+
filter.appendChild(actionItem);
|
|
1177
|
+
const reset = document.createElement("button");
|
|
1178
|
+
reset.type = "button";
|
|
1179
|
+
reset.className = "lv-filter-btn";
|
|
1180
|
+
reset.setAttribute("data-filter-reset", "true");
|
|
1181
|
+
reset.textContent = this.config.locale === "en-US" ? "Reset" : "重置";
|
|
1182
|
+
filter.appendChild(reset);
|
|
1183
|
+
return filter;
|
|
1184
|
+
}
|
|
1185
|
+
updateFilterField(field, value) {
|
|
1186
|
+
const next = this.getFilter();
|
|
1187
|
+
if (!value.trim()) delete next[field];
|
|
1188
|
+
else if (field === "action" && isActionType(value)) next.action = value;
|
|
1189
|
+
else if (field !== "action") next[field] = value;
|
|
1190
|
+
this.setFilter(next);
|
|
1191
|
+
}
|
|
894
1192
|
renderTimeline() {
|
|
895
1193
|
if (this.destroyed) return;
|
|
896
1194
|
const pageData = this.getPageData();
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1195
|
+
const timeline = this.timelineRenderer.render(pageData, this.expandedIds, {
|
|
1196
|
+
showDiff: this.config.showDiff,
|
|
1197
|
+
showAnomalyBadge: this.config.showAnomalyBadge,
|
|
1198
|
+
locale: this.config.locale
|
|
1199
|
+
});
|
|
1200
|
+
if (this.config.showDiff !== false) for (const entry of pageData) {
|
|
1201
|
+
if (!this.expandedIds.has(entry.id)) continue;
|
|
1202
|
+
([...timeline.children].find((child) => child.getAttribute("data-id") === entry.id)?.querySelector(".lv-timeline-card"))?.appendChild(this.diffRenderer.renderDetail(entry));
|
|
901
1203
|
}
|
|
902
|
-
|
|
903
|
-
this.timelineEl
|
|
904
|
-
this.
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
this.paginationEl =
|
|
908
|
-
this.root.appendChild(this.
|
|
1204
|
+
this.filterEl?.remove();
|
|
1205
|
+
this.timelineEl?.remove();
|
|
1206
|
+
this.paginationEl?.remove();
|
|
1207
|
+
this.filterEl = this.renderFilterBar();
|
|
1208
|
+
this.timelineEl = timeline;
|
|
1209
|
+
this.paginationEl = this.timelineRenderer.renderPagination(this.pagination.page, this.pagination.pageSize, this.filteredData.length);
|
|
1210
|
+
if (this.filterEl) this.root.appendChild(this.filterEl);
|
|
1211
|
+
this.root.append(this.timelineEl, this.paginationEl);
|
|
909
1212
|
}
|
|
910
1213
|
bindEvents() {
|
|
911
|
-
this.root.addEventListener("
|
|
912
|
-
const target =
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1214
|
+
this.root.addEventListener("change", (event) => {
|
|
1215
|
+
const target = event.target;
|
|
1216
|
+
if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLSelectElement)) return;
|
|
1217
|
+
const field = target.getAttribute("data-filter-field");
|
|
1218
|
+
if (field) this.updateFilterField(field, target.value);
|
|
1219
|
+
});
|
|
1220
|
+
this.root.addEventListener("click", (event) => {
|
|
1221
|
+
const target = event.target;
|
|
1222
|
+
if (!(target instanceof Element)) return;
|
|
1223
|
+
if (target.closest("[data-filter-reset]")) {
|
|
1224
|
+
this.resetFilter();
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const hint = target.closest(".lv-timeline-expand-hint");
|
|
1228
|
+
const id = (hint?.closest(".lv-timeline-item") || target.closest(".lv-timeline-item"))?.getAttribute("data-id");
|
|
1229
|
+
if (hint && id) {
|
|
1230
|
+
if (this.expandedIds.has(id)) this.collapseEntry(id);
|
|
1231
|
+
else this.expandEntry(id);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const pageButton = target.closest(".lv-pagination-btn");
|
|
1235
|
+
if (pageButton && !pageButton.disabled) {
|
|
1236
|
+
const page = Number(pageButton.getAttribute("data-page"));
|
|
1237
|
+
if (Number.isFinite(page)) this.goToPage(page);
|
|
1238
|
+
return;
|
|
926
1239
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1240
|
+
if (!target.closest(".lv-timeline-card") || !id) return;
|
|
1241
|
+
const entry = this.filteredData.find((candidate) => candidate.id === id);
|
|
1242
|
+
if (entry) {
|
|
1243
|
+
const snapshot = deepClone(entry);
|
|
1244
|
+
this.emitter.emit("entry:click", snapshot);
|
|
1245
|
+
this.config.onEntryClick?.(snapshot);
|
|
931
1246
|
}
|
|
932
1247
|
});
|
|
933
1248
|
}
|