@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
package/es/utils/index.js CHANGED
@@ -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
  * ***********************************
@@ -10,11 +10,12 @@
10
10
  //#region src/utils/index.ts
11
11
  let _uid = 0;
12
12
  function uid(prefix = "lv") {
13
- return `${prefix}-${++_uid}-${Math.random().toString(36).slice(2, 8)}`;
13
+ const random = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 8);
14
+ return `${prefix}-${++_uid}-${random}`;
14
15
  }
15
16
  function formatDate(value, fmt = "YYYY-MM-DD HH:mm:ss") {
16
17
  const d = value instanceof Date ? value : new Date(value);
17
- if (isNaN(d.getTime())) return String(value);
18
+ if (Number.isNaN(d.getTime())) return String(value);
18
19
  const pad = (n) => String(n).padStart(2, "0");
19
20
  const year = String(d.getFullYear());
20
21
  const month = pad(d.getMonth() + 1);
@@ -27,6 +28,7 @@ function formatDate(value, fmt = "YYYY-MM-DD HH:mm:ss") {
27
28
  function formatRelativeTime(value) {
28
29
  const d = value instanceof Date ? value : new Date(value);
29
30
  const diff = Date.now() - d.getTime();
31
+ if (!Number.isFinite(diff) || diff < 0) return formatDate(d);
30
32
  if (diff < 6e4) return "刚刚";
31
33
  if (diff < 36e5) return `${Math.floor(diff / 6e4)} \u5206\u949F\u524D`;
32
34
  if (diff < 864e5) return `${Math.floor(diff / 36e5)} \u5C0F\u65F6\u524D`;
@@ -45,15 +47,58 @@ function escapeHtml(str) {
45
47
  }
46
48
  function debounce(fn, delay) {
47
49
  let timer = null;
50
+ const wait = Number.isFinite(delay) && delay > 0 ? delay : 0;
48
51
  return ((...args) => {
49
52
  if (timer) clearTimeout(timer);
50
- timer = setTimeout(() => fn(...args), delay);
53
+ timer = setTimeout(() => {
54
+ timer = null;
55
+ fn(...args);
56
+ }, wait);
51
57
  });
52
58
  }
53
59
  function deepClone(obj) {
54
60
  if (obj === null || typeof obj !== "object") return obj;
61
+ if (typeof structuredClone === "function") try {
62
+ return structuredClone(obj);
63
+ } catch {}
64
+ const seen = /* @__PURE__ */ new WeakMap();
65
+ const clone = (value) => {
66
+ if (value === null || typeof value !== "object") return value;
67
+ if (seen.has(value)) return seen.get(value);
68
+ if (value instanceof Date) return new Date(value.getTime());
69
+ if (value instanceof RegExp) return new RegExp(value.source, value.flags);
70
+ if (value instanceof Map) {
71
+ const result2 = /* @__PURE__ */ new Map();
72
+ seen.set(value, result2);
73
+ value.forEach((mapValue, key) => result2.set(clone(key), clone(mapValue)));
74
+ return result2;
75
+ }
76
+ if (value instanceof Set) {
77
+ const result2 = /* @__PURE__ */ new Set();
78
+ seen.set(value, result2);
79
+ value.forEach((setValue) => result2.add(clone(setValue)));
80
+ return result2;
81
+ }
82
+ if (Array.isArray(value)) {
83
+ const result2 = [];
84
+ seen.set(value, result2);
85
+ value.forEach((item) => result2.push(clone(item)));
86
+ return result2;
87
+ }
88
+ const result = Object.create(Object.getPrototypeOf(value));
89
+ seen.set(value, result);
90
+ for (const key of Reflect.ownKeys(value)) {
91
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
92
+ if (!descriptor || !("value" in descriptor)) continue;
93
+ Object.defineProperty(result, key, {
94
+ ...descriptor,
95
+ value: clone(descriptor.value)
96
+ });
97
+ }
98
+ return result;
99
+ };
55
100
  try {
56
- return JSON.parse(JSON.stringify(obj));
101
+ return clone(obj);
57
102
  } catch {
58
103
  return obj;
59
104
  }
@@ -61,7 +106,10 @@ function deepClone(obj) {
61
106
  function formatValue(val) {
62
107
  if (val === null || val === void 0) return "-";
63
108
  if (typeof val === "object") try {
64
- return JSON.stringify(val, null, 2);
109
+ return JSON.stringify(val, (_key, value) => {
110
+ if (typeof value === "bigint") return `${value}n`;
111
+ return value;
112
+ }, 2);
65
113
  } catch {
66
114
  return String(val);
67
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/utils/index.ts"],"sourcesContent":["/**\n * 工具函数\n */\n\nlet _uid = 0\n\n/** 生成唯一 ID */\nexport function uid(prefix = 'lv'): string {\n return `${prefix}-${++_uid}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/** 格式化日期 */\nexport function formatDate(value: string | number | Date, fmt = 'YYYY-MM-DD HH:mm:ss'): string {\n const d = value instanceof Date ? value : new Date(value)\n if (isNaN(d.getTime())) return String(value)\n\n const pad = (n: number) => String(n).padStart(2, '0')\n const year = String(d.getFullYear())\n const month = pad(d.getMonth() + 1)\n const day = pad(d.getDate())\n const hours = pad(d.getHours())\n const minutes = pad(d.getMinutes())\n const seconds = pad(d.getSeconds())\n\n return fmt\n .replace('YYYY', year)\n .replace('MM', month)\n .replace('DD', day)\n .replace('HH', hours)\n .replace('mm', minutes)\n .replace('ss', seconds)\n}\n\n/** 格式化相对时间 */\nexport function formatRelativeTime(value: string | number | Date): string {\n const d = value instanceof Date ? value : new Date(value)\n const now = Date.now()\n const diff = now - d.getTime()\n\n if (diff < 60_000) return '刚刚'\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`\n if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`\n if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)} 天前`\n return formatDate(d)\n}\n\n/** HTML 转义 */\nexport function escapeHtml(str: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n }\n return str.replace(/[&<>\"']/g, (c) => map[c] || c)\n}\n\n/** 防抖 */\nexport function debounce<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {\n let timer: ReturnType<typeof setTimeout> | null = null\n return ((...args: unknown[]) => {\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => fn(...args), delay)\n }) as unknown as T\n}\n\n/** 深拷贝简易实现 */\nexport function deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== 'object') return obj\n try {\n return JSON.parse(JSON.stringify(obj))\n } catch {\n return obj\n }\n}\n\n/** 将值格式化为可展示的字符串 */\nexport function formatValue(val: unknown): string {\n if (val === null || val === undefined) return '-'\n if (typeof val === 'object') {\n try {\n return JSON.stringify(val, null, 2)\n } catch {\n return String(val)\n }\n }\n return String(val)\n}\n"],"mappings":";;;;;;;;;;AAIA,IAAI,OAAO;AAGJ,SAAS,IAAI,SAAS,MAAc;CACzC,OAAO,GAAG,OAAM,GAAI,EAAE,KAAI,GAAI,KAAK,OAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,MAAM,GAAG,CAAC;AACrE;AAGO,SAAS,WAAW,OAA+B,MAAM,uBAA+B;CAC7F,MAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;CACxD,IAAI,MAAM,EAAE,QAAQ,CAAC,GAAG,OAAO,OAAO,KAAK;CAE3C,MAAM,OAAO,MAAc,OAAO,CAAC,CAAA,CAAE,SAAS,GAAG,GAAG;CACpD,MAAM,OAAO,OAAO,EAAE,YAAY,CAAC;CACnC,MAAM,QAAQ,IAAI,EAAE,SAAS,IAAI,CAAC;CAClC,MAAM,MAAM,IAAI,EAAE,QAAQ,CAAC;CAC3B,MAAM,QAAQ,IAAI,EAAE,SAAS,CAAC;CAC9B,MAAM,UAAU,IAAI,EAAE,WAAW,CAAC;CAClC,MAAM,UAAU,IAAI,EAAE,WAAW,CAAC;CAElC,OAAO,IACJ,QAAQ,QAAQ,IAAI,CAAA,CACpB,QAAQ,MAAM,KAAK,CAAA,CACnB,QAAQ,MAAM,GAAG,CAAA,CACjB,QAAQ,MAAM,KAAK,CAAA,CACnB,QAAQ,MAAM,OAAO,CAAA,CACrB,QAAQ,MAAM,OAAO;AAC1B;AAGO,SAAS,mBAAmB,OAAuC;CACxE,MAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;CAExD,MAAM,OADM,KAAK,IACJ,IAAM,EAAE,QAAQ;CAE7B,IAAI,OAAO,KAAQ,OAAO;CAC1B,IAAI,OAAO,MAAW,OAAO,GAAG,KAAK,MAAM,OAAO,GAAM,EAAC;CACzD,IAAI,OAAO,OAAY,OAAO,GAAG,KAAK,MAAM,OAAO,IAAS,EAAC;CAC7D,IAAI,OAAO,QAAa,OAAO,GAAG,KAAK,MAAM,OAAO,KAAU,EAAC;CAC/D,OAAO,WAAW,CAAC;AACrB;AAGO,SAAS,WAAW,KAAqB;CAC9C,MAAM,MAA8B;EAClC,KAAK;EACL,KAAK;EACL,KAAK;EACL,MAAK;EACL,KAAK;CACP;CACA,OAAO,IAAI,QAAQ,aAAa,MAAM,IAAI,MAAM,CAAC;AACnD;AAGO,SAAS,SAAiD,IAAO,OAAkB;CACxF,IAAI,QAA8C;CAClD,SAAQ,GAAI,SAAoB;EAC9B,IAAI,OAAO,aAAa,KAAK;EAC7B,QAAQ,iBAAiB,GAAG,GAAG,IAAI,GAAG,KAAK;CAC7C;AACF;AAGO,SAAS,UAAa,KAAW;CACtC,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU,OAAO;CACpD,IAAI;EACF,OAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;CACvC,QAAQ;EACN,OAAO;CACT;AACF;AAGO,SAAS,YAAY,KAAsB;CAChD,IAAI,QAAQ,QAAQ,QAAQ,KAAA,GAAW,OAAO;CAC9C,IAAI,OAAO,QAAQ,UACjB,IAAI;EACF,OAAO,KAAK,UAAU,KAAK,MAAM,CAAC;CACpC,QAAQ;EACN,OAAO,OAAO,GAAG;CACnB;CAEF,OAAO,OAAO,GAAG;AACnB"}
1
+ {"version":3,"file":"index.js","names":["result"],"sources":["../../src/utils/index.ts"],"sourcesContent":["/**\r\n * 工具函数\r\n */\r\n\r\nlet _uid = 0\r\n\r\n/** 生成唯一 ID */\r\nexport function uid(prefix = 'lv'): string {\r\n const random =\r\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\r\n ? crypto.randomUUID().slice(0, 8)\r\n : Math.random().toString(36).slice(2, 8)\r\n return `${prefix}-${++_uid}-${random}`\r\n}\r\n\r\n/** 格式化日期 */\r\nexport function formatDate(\r\n value: string | number | Date,\r\n fmt = 'YYYY-MM-DD HH:mm:ss'\r\n): string {\r\n const d = value instanceof Date ? value : new Date(value)\r\n if (Number.isNaN(d.getTime())) return String(value)\r\n\r\n const pad = (n: number) => String(n).padStart(2, '0')\r\n const year = String(d.getFullYear())\r\n const month = pad(d.getMonth() + 1)\r\n const day = pad(d.getDate())\r\n const hours = pad(d.getHours())\r\n const minutes = pad(d.getMinutes())\r\n const seconds = pad(d.getSeconds())\r\n\r\n return fmt\r\n .replace('YYYY', year)\r\n .replace('MM', month)\r\n .replace('DD', day)\r\n .replace('HH', hours)\r\n .replace('mm', minutes)\r\n .replace('ss', seconds)\r\n}\r\n\r\n/** 格式化相对时间 */\r\nexport function formatRelativeTime(value: string | number | Date): string {\r\n const d = value instanceof Date ? value : new Date(value)\r\n const now = Date.now()\r\n const diff = now - d.getTime()\r\n\r\n if (!Number.isFinite(diff) || diff < 0) return formatDate(d)\r\n if (diff < 60_000) return '刚刚'\r\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`\r\n if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`\r\n if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)} 天前`\r\n return formatDate(d)\r\n}\r\n\r\n/** HTML 转义 */\r\nexport function escapeHtml(str: string): string {\r\n const map: Record<string, string> = {\r\n '&': '&amp;',\r\n '<': '&lt;',\r\n '>': '&gt;',\r\n '\"': '&quot;',\r\n \"'\": '&#39;',\r\n }\r\n return str.replace(/[&<>\"']/g, c => map[c] || c)\r\n}\r\n\r\n/** 防抖 */\r\nexport function debounce<T extends (...args: unknown[]) => void>(\r\n fn: T,\r\n delay: number\r\n): T {\r\n let timer: ReturnType<typeof setTimeout> | null = null\r\n const wait = Number.isFinite(delay) && delay > 0 ? delay : 0\r\n return ((...args: unknown[]) => {\r\n if (timer) clearTimeout(timer)\r\n timer = setTimeout(() => {\r\n timer = null\r\n fn(...args)\r\n }, wait)\r\n }) as unknown as T\r\n}\r\n\r\n/** 深拷贝值,同时保留 Date、Map、Set 和循环引用。 */\r\nexport function deepClone<T>(obj: T): T {\r\n if (obj === null || typeof obj !== 'object') return obj\r\n\r\n if (typeof structuredClone === 'function') {\r\n try {\r\n return structuredClone(obj)\r\n } catch {\r\n // structuredClone rejects functions and a few host objects; use the\r\n // compatibility clone below so configuration callbacks stay intact.\r\n }\r\n }\r\n\r\n const seen = new WeakMap<object, unknown>()\r\n\r\n const clone = (value: unknown): unknown => {\r\n if (value === null || typeof value !== 'object') return value\r\n if (seen.has(value)) return seen.get(value)\r\n\r\n if (value instanceof Date) return new Date(value.getTime())\r\n if (value instanceof RegExp) return new RegExp(value.source, value.flags)\r\n\r\n if (value instanceof Map) {\r\n const result = new Map<unknown, unknown>()\r\n seen.set(value, result)\r\n value.forEach((mapValue, key) => result.set(clone(key), clone(mapValue)))\r\n return result\r\n }\r\n\r\n if (value instanceof Set) {\r\n const result = new Set<unknown>()\r\n seen.set(value, result)\r\n value.forEach(setValue => result.add(clone(setValue)))\r\n return result\r\n }\r\n\r\n if (Array.isArray(value)) {\r\n const result: unknown[] = []\r\n seen.set(value, result)\r\n value.forEach(item => result.push(clone(item)))\r\n return result\r\n }\r\n\r\n const result = Object.create(Object.getPrototypeOf(value)) as Record<\r\n string,\r\n unknown\r\n >\r\n seen.set(value, result)\r\n for (const key of Reflect.ownKeys(value)) {\r\n const descriptor = Object.getOwnPropertyDescriptor(value, key)\r\n if (!descriptor || !('value' in descriptor)) continue\r\n Object.defineProperty(result, key, {\r\n ...descriptor,\r\n value: clone(descriptor.value),\r\n })\r\n }\r\n return result\r\n }\r\n\r\n try {\r\n return clone(obj) as T\r\n } catch {\r\n return obj\r\n }\r\n}\r\n\r\n/** 将值格式化为可展示的字符串 */\r\nexport function formatValue(val: unknown): string {\r\n if (val === null || val === undefined) return '-'\r\n if (typeof val === 'object') {\r\n try {\r\n return JSON.stringify(\r\n val,\r\n (_key, value) => {\r\n if (typeof value === 'bigint') return `${value}n`\r\n return value\r\n },\r\n 2\r\n )\r\n } catch {\r\n return String(val)\r\n }\r\n }\r\n return String(val)\r\n}\r\n"],"mappings":";;;;;;;;;;AAIA,IAAI,OAAO;AAGJ,SAAS,IAAI,SAAS,MAAc;CACzC,MAAM,SACJ,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,aAC1D,OAAO,WAAW,CAAA,CAAE,MAAM,GAAG,CAAC,IAC9B,KAAK,OAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,MAAM,GAAG,CAAC;CAC3C,OAAO,GAAG,OAAM,GAAI,EAAE,KAAI,GAAI;AAChC;AAGO,SAAS,WACd,OACA,MAAM,uBACE;CACR,MAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;CACxD,IAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,GAAG,OAAO,OAAO,KAAK;CAElD,MAAM,OAAO,MAAc,OAAO,CAAC,CAAA,CAAE,SAAS,GAAG,GAAG;CACpD,MAAM,OAAO,OAAO,EAAE,YAAY,CAAC;CACnC,MAAM,QAAQ,IAAI,EAAE,SAAS,IAAI,CAAC;CAClC,MAAM,MAAM,IAAI,EAAE,QAAQ,CAAC;CAC3B,MAAM,QAAQ,IAAI,EAAE,SAAS,CAAC;CAC9B,MAAM,UAAU,IAAI,EAAE,WAAW,CAAC;CAClC,MAAM,UAAU,IAAI,EAAE,WAAW,CAAC;CAElC,OAAO,IACJ,QAAQ,QAAQ,IAAI,CAAA,CACpB,QAAQ,MAAM,KAAK,CAAA,CACnB,QAAQ,MAAM,GAAG,CAAA,CACjB,QAAQ,MAAM,KAAK,CAAA,CACnB,QAAQ,MAAM,OAAO,CAAA,CACrB,QAAQ,MAAM,OAAO;AAC1B;AAGO,SAAS,mBAAmB,OAAuC;CACxE,MAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;CAExD,MAAM,OADM,KAAK,IACJ,IAAM,EAAE,QAAQ;CAE7B,IAAI,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,GAAG,OAAO,WAAW,CAAC;CAC3D,IAAI,OAAO,KAAQ,OAAO;CAC1B,IAAI,OAAO,MAAW,OAAO,GAAG,KAAK,MAAM,OAAO,GAAM,EAAC;CACzD,IAAI,OAAO,OAAY,OAAO,GAAG,KAAK,MAAM,OAAO,IAAS,EAAC;CAC7D,IAAI,OAAO,QAAa,OAAO,GAAG,KAAK,MAAM,OAAO,KAAU,EAAC;CAC/D,OAAO,WAAW,CAAC;AACrB;AAGO,SAAS,WAAW,KAAqB;CAC9C,MAAM,MAA8B;EAClC,KAAK;EACL,KAAK;EACL,KAAK;EACL,MAAK;EACL,KAAK;CACP;CACA,OAAO,IAAI,QAAQ,aAAY,MAAK,IAAI,MAAM,CAAC;AACjD;AAGO,SAAS,SACd,IACA,OACG;CACH,IAAI,QAA8C;CAClD,MAAM,OAAO,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ;CAC3D,SAAQ,GAAI,SAAoB;EAC9B,IAAI,OAAO,aAAa,KAAK;EAC7B,QAAQ,iBAAiB;GACvB,QAAQ;GACR,GAAG,GAAG,IAAI;EACZ,GAAG,IAAI;CACT;AACF;AAGO,SAAS,UAAa,KAAW;CACtC,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU,OAAO;CAEpD,IAAI,OAAO,oBAAoB,YAC7B,IAAI;EACF,OAAO,gBAAgB,GAAG;CAC5B,QAAQ,CAGR;CAGF,MAAM,uBAAO,IAAI,QAAyB;CAE1C,MAAM,SAAS,UAA4B;EACzC,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;EACxD,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO,KAAK,IAAI,KAAK;EAE1C,IAAI,iBAAiB,MAAM,OAAO,IAAI,KAAK,MAAM,QAAQ,CAAC;EAC1D,IAAI,iBAAiB,QAAQ,OAAO,IAAI,OAAO,MAAM,QAAQ,MAAM,KAAK;EAExE,IAAI,iBAAiB,KAAK;GACxB,MAAMA,0BAAS,IAAI,IAAsB;GACzC,KAAK,IAAI,OAAOA,OAAM;GACtB,MAAM,SAAS,UAAU,QAAQA,QAAO,IAAI,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,CAAC;GACxE,OAAOA;EACT;EAEA,IAAI,iBAAiB,KAAK;GACxB,MAAMA,0BAAS,IAAI,IAAa;GAChC,KAAK,IAAI,OAAOA,OAAM;GACtB,MAAM,SAAQ,aAAYA,QAAO,IAAI,MAAM,QAAQ,CAAC,CAAC;GACrD,OAAOA;EACT;EAEA,IAAI,MAAM,QAAQ,KAAK,GAAG;GACxB,MAAMA,UAAoB,CAAC;GAC3B,KAAK,IAAI,OAAOA,OAAM;GACtB,MAAM,SAAQ,SAAQA,QAAO,KAAK,MAAM,IAAI,CAAC,CAAC;GAC9C,OAAOA;EACT;EAEA,MAAM,SAAS,OAAO,OAAO,OAAO,eAAe,KAAK,CAAC;EAIzD,KAAK,IAAI,OAAO,MAAM;EACtB,KAAA,MAAW,OAAO,QAAQ,QAAQ,KAAK,GAAG;GACxC,MAAM,aAAa,OAAO,yBAAyB,OAAO,GAAG;GAC7D,IAAI,CAAC,cAAc,EAAE,WAAW,aAAa;GAC7C,OAAO,eAAe,QAAQ,KAAK;IACjC,GAAG;IACH,OAAO,MAAM,WAAW,KAAK;GAC/B,CAAC;EACH;EACA,OAAO;CACT;CAEA,IAAI;EACF,OAAO,MAAM,GAAG;CAClB,QAAQ;EACN,OAAO;CACT;AACF;AAGO,SAAS,YAAY,KAAsB;CAChD,IAAI,QAAQ,QAAQ,QAAQ,KAAA,GAAW,OAAO;CAC9C,IAAI,OAAO,QAAQ,UACjB,IAAI;EACF,OAAO,KAAK,UACV,MACC,MAAM,UAAU;GACf,IAAI,OAAO,UAAU,UAAU,OAAO,GAAG,MAAK;GAC9C,OAAO;EACT,GACA,CACF;CACF,QAAQ;EACN,OAAO,OAAO,GAAG;CACnB;CAEF,OAAO,OAAO,GAAG;AACnB"}
@@ -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,21 +13,25 @@ var EventEmitter = class {
13
13
  this.listeners = /* @__PURE__ */ new Map();
14
14
  }
15
15
  on(event, callback) {
16
+ if (!event || typeof callback !== "function") return () => void 0;
16
17
  if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
17
18
  this.listeners.get(event).add(callback);
18
19
  return () => this.off(event, callback);
19
20
  }
20
21
  off(event, callback) {
21
- this.listeners.get(event)?.delete(callback);
22
+ const listeners = this.listeners.get(event);
23
+ if (!listeners) return;
24
+ listeners.delete(callback);
25
+ if (listeners.size === 0) this.listeners.delete(event);
22
26
  }
23
27
  emit(event, ...args) {
24
- this.listeners.get(event)?.forEach((cb) => {
25
- try {
26
- cb(...args);
27
- } catch (e) {
28
- console.error(`[LogViewer] Event handler error (${event}):`, e);
29
- }
30
- });
28
+ const listeners = this.listeners.get(event);
29
+ if (!listeners) return;
30
+ for (const cb of [...listeners]) try {
31
+ cb(...args);
32
+ } catch (e) {
33
+ console.error(`[LogViewer] Event handler error (${event}):`, e);
34
+ }
31
35
  }
32
36
  removeAllListeners(event) {
33
37
  if (event) this.listeners.delete(event);
@@ -1 +1 @@
1
- {"version":3,"file":"EventEmitter.js","names":[],"sources":["../../src/core/EventEmitter.ts"],"sourcesContent":["/**\n * 简单的事件发射器\n */\nexport class EventEmitter {\n private listeners = new Map<string, Set<(...args: unknown[]) => void>>()\n\n on(event: string, callback: (...args: unknown[]) => void): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set())\n }\n this.listeners.get(event)!.add(callback)\n return () => this.off(event, callback)\n }\n\n off(event: string, callback: (...args: unknown[]) => void): void {\n this.listeners.get(event)?.delete(callback)\n }\n\n emit(event: string, ...args: unknown[]): void {\n this.listeners.get(event)?.forEach((cb) => {\n try {\n cb(...args)\n } catch (e) {\n console.error(`[LogViewer] Event handler error (${event}):`, e)\n }\n })\n }\n\n removeAllListeners(event?: string): void {\n if (event) {\n this.listeners.delete(event)\n } else {\n this.listeners.clear()\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAGO,IAAM,eAAN,MAAmB;CAAnB,cAAA;EACL,KAAQ,4BAAY,IAAI,IAA+C;CAAA;CAEvE,GAAG,OAAe,UAAoD;EACpE,IAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAC3B,KAAK,UAAU,IAAI,uBAAO,IAAI,IAAI,CAAC;EAErC,KAAK,UAAU,IAAI,KAAK,CAAA,CAAG,IAAI,QAAQ;EACvC,aAAa,KAAK,IAAI,OAAO,QAAQ;CACvC;CAEA,IAAI,OAAe,UAA8C;EAC/D,KAAK,UAAU,IAAI,KAAK,CAAA,EAAG,OAAO,QAAQ;CAC5C;CAEA,KAAK,OAAA,GAAkB,MAAuB;EAC5C,KAAK,UAAU,IAAI,KAAK,CAAA,EAAG,SAAS,OAAO;GACzC,IAAI;IACF,GAAG,GAAG,IAAI;GACZ,SAAS,GAAG;IACV,QAAQ,MAAM,oCAAoC,MAAK,KAAM,CAAC;GAChE;EACF,CAAC;CACH;CAEA,mBAAmB,OAAsB;EACvC,IAAI,OACF,KAAK,UAAU,OAAO,KAAK;OAE3B,KAAK,UAAU,MAAM;CAEzB;AACF"}
1
+ {"version":3,"file":"EventEmitter.js","names":[],"sources":["../../src/core/EventEmitter.ts"],"sourcesContent":["/**\r\n * 简单的事件发射器\r\n */\r\nexport class EventEmitter {\r\n private listeners = new Map<string, Set<(...args: unknown[]) => void>>()\r\n\r\n on(event: string, callback: (...args: unknown[]) => void): () => void {\r\n if (!event || typeof callback !== 'function') {\r\n return () => undefined\r\n }\r\n if (!this.listeners.has(event)) {\r\n this.listeners.set(event, new Set())\r\n }\r\n const listeners = this.listeners.get(event)!\r\n listeners.add(callback)\r\n return () => this.off(event, callback)\r\n }\r\n\r\n off(event: string, callback: (...args: unknown[]) => void): void {\r\n const listeners = this.listeners.get(event)\r\n if (!listeners) return\r\n listeners.delete(callback)\r\n if (listeners.size === 0) this.listeners.delete(event)\r\n }\r\n\r\n emit(event: string, ...args: unknown[]): void {\r\n const listeners = this.listeners.get(event)\r\n if (!listeners) return\r\n\r\n // Snapshot the set so listeners can unsubscribe or subscribe while the\r\n // current event is being delivered without changing this dispatch cycle.\r\n for (const cb of [...listeners]) {\r\n try {\r\n cb(...args)\r\n } catch (e) {\r\n console.error(`[LogViewer] Event handler error (${event}):`, e)\r\n }\r\n }\r\n }\r\n\r\n removeAllListeners(event?: string): void {\r\n if (event) {\r\n this.listeners.delete(event)\r\n } else {\r\n this.listeners.clear()\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;AAGO,IAAM,eAAN,MAAmB;CAAnB,cAAA;EACL,KAAQ,4BAAY,IAAI,IAA+C;CAAA;CAEvE,GAAG,OAAe,UAAoD;EACpE,IAAI,CAAC,SAAS,OAAO,aAAa,YAChC,aAAa,KAAA;EAEf,IAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAC3B,KAAK,UAAU,IAAI,uBAAO,IAAI,IAAI,CAAC;EAGrC,KADuB,UAAU,IAAI,KACrC,CAAA,CAAU,IAAI,QAAQ;EACtB,aAAa,KAAK,IAAI,OAAO,QAAQ;CACvC;CAEA,IAAI,OAAe,UAA8C;EAC/D,MAAM,YAAY,KAAK,UAAU,IAAI,KAAK;EAC1C,IAAI,CAAC,WAAW;EAChB,UAAU,OAAO,QAAQ;EACzB,IAAI,UAAU,SAAS,GAAG,KAAK,UAAU,OAAO,KAAK;CACvD;CAEA,KAAK,OAAA,GAAkB,MAAuB;EAC5C,MAAM,YAAY,KAAK,UAAU,IAAI,KAAK;EAC1C,IAAI,CAAC,WAAW;EAIhB,KAAA,MAAW,MAAM,CAAC,GAAG,SAAS,GAC5B,IAAI;GACF,GAAG,GAAG,IAAI;EACZ,SAAS,GAAG;GACV,QAAQ,MAAM,oCAAoC,MAAK,KAAM,CAAC;EAChE;CAEJ;CAEA,mBAAmB,OAAsB;EACvC,IAAI,OACF,KAAK,UAAU,OAAO,KAAK;OAE3B,KAAK,UAAU,MAAM;CAEzB;AACF"}
@@ -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
+ import { ACTION_LABELS } from "../types/index.js";
10
11
  import { EventEmitter } from "./EventEmitter.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
  }