dsh-my-observability 0.3.0 → 0.3.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.
@@ -1,396 +0,0 @@
1
- // ── 审计视图扩展:搜索 / 组合过滤 / 导出 / 统计 / 高亮(replay.js 拆出)────
2
- // 依赖 replay.js(REPLAY_POLL_MS 等常量与 loadReplayData/EventRow)与
3
- // audit-view.js 纯函数(applyAuditFilter 等)。始终以 function 声明提升。
4
-
5
- const REPLAY_POLL_MS = 5000
6
- const ALL_SESSIONS = '*'
7
- const EXPORT_LIMIT_ALL = 20000
8
-
9
- /** `datetime-local` 值 → 毫秒时间戳(空/非法返回 undefined)。 */
10
- function datetimeToMs(value) {
11
- if (typeof value !== 'string' || value.trim() === '') return undefined
12
- const ms = new Date(value).getTime()
13
- return Number.isFinite(ms) ? ms : undefined
14
- }
15
-
16
- /** 是否有活动过滤条件(决定无匹配/空状态展示)。 */
17
- function hasActiveFilter(criteria) {
18
- return (
19
- criteria.keyword !== '' ||
20
- criteria.result !== '' ||
21
- criteria.timeStart !== undefined ||
22
- criteria.timeEnd !== undefined ||
23
- criteria.type !== ''
24
- )
25
- }
26
-
27
- /** 命中关键词高亮(`mark` 包裹命中段;无关键词时原样文本)。 */
28
- function HighlightText({ text, keyword }) {
29
- const segments = highlightSegments(text, keyword)
30
- return createElement(
31
- 'span',
32
- null,
33
- segments.map((seg, index) =>
34
- seg.hit ? createElement('mark', { key: index, className: 'dsh-my-observability-mark' }, seg.text) : seg.text,
35
- ),
36
- )
37
- }
38
-
39
- /** 搜索 / 组合过滤栏:关键词 + 时间范围 + 成功/失败。 */
40
- /** 搜索框行(含清除按钮)。 */
41
- function SearchInput({ keyword, onKeyword, onClear }) {
42
- return createElement(
43
- 'div',
44
- { className: 'dsh-my-observability-search-row' },
45
- createElement('input', {
46
- className: 'dsh-my-observability-input',
47
- type: 'search',
48
- value: keyword,
49
- placeholder: strings.searchPlaceholder(),
50
- onChange: (e) => onKeyword(e.target.value),
51
- }),
52
- keyword !== ''
53
- ? createElement(
54
- 'button',
55
- {
56
- type: 'button',
57
- className: 'dsh-my-observability-iconbtn',
58
- 'aria-label': strings.clearFilters(),
59
- title: strings.clearFilters(),
60
- onClick: onClear,
61
- },
62
- icon.close(15),
63
- )
64
- : null,
65
- )
66
- }
67
-
68
- /** 时间范围行(开始/结束,datetime-local)。 */
69
- function TimeRangeInput({ timeStart, onTimeStart, timeEnd, onTimeEnd }) {
70
- return createElement(
71
- 'div',
72
- { className: 'dsh-my-observability-time-row' },
73
- createElement('label', { className: 'dsh-my-observability-time-label' }, strings.timeStartLabel()),
74
- createElement('input', {
75
- className: 'dsh-my-observability-input dsh-my-observability-time-input',
76
- type: 'datetime-local',
77
- value: timeStart,
78
- onChange: (e) => onTimeStart(e.target.value),
79
- }),
80
- createElement('label', { className: 'dsh-my-observability-time-label' }, strings.timeEndLabel()),
81
- createElement('input', {
82
- className: 'dsh-my-observability-input dsh-my-observability-time-input',
83
- type: 'datetime-local',
84
- value: timeEnd,
85
- onChange: (e) => onTimeEnd(e.target.value),
86
- }),
87
- )
88
- }
89
-
90
- /** 成功/失败结果过滤组。 */
91
- function ResultFilter({ result, onResult }) {
92
- const options = [
93
- ['', strings.filterAllResult()],
94
- ['success', strings.filterSuccess()],
95
- ['fail', strings.filterFail()],
96
- ]
97
- return createElement(
98
- 'div',
99
- { className: 'dsh-my-observability-filters' },
100
- options.map(([value, label]) =>
101
- createElement(
102
- 'button',
103
- {
104
- key: value,
105
- type: 'button',
106
- className: `dsh-my-observability-chip${result === value ? ' dsh-my-observability-chip-active' : ''}`,
107
- 'aria-pressed': result === value,
108
- onClick: () => onResult(value),
109
- },
110
- label,
111
- ),
112
- ),
113
- )
114
- }
115
-
116
- /** 搜索 / 组合过滤栏:关键词 + 时间范围 + 成功/失败。 */
117
- function SearchFilterBar({
118
- keyword,
119
- onKeyword,
120
- timeStart,
121
- onTimeStart,
122
- timeEnd,
123
- onTimeEnd,
124
- result,
125
- onResult,
126
- onClear,
127
- }) {
128
- return createElement(
129
- 'div',
130
- { className: 'dsh-my-observability-toolbar' },
131
- createElement(SearchInput, { keyword, onKeyword, onClear }),
132
- createElement(TimeRangeInput, { timeStart, onTimeStart, timeEnd, onTimeEnd }),
133
- createElement(ResultFilter, { result, onResult }),
134
- )
135
- }
136
-
137
- /** 导出栏:导出范围选择(当前会话/全部会话)+ JSON/CSV 按钮 + 统计开关。 */
138
- function ExportBar({ scope, onScope, onExportJson, onExportCsv, showStats, onToggleStats, disabled }) {
139
- return createElement(
140
- 'div',
141
- { className: 'dsh-my-observability-export' },
142
- createElement(
143
- 'div',
144
- { className: 'dsh-my-observability-toolbar-row' },
145
- createElement(
146
- 'select',
147
- {
148
- className: 'dsh-my-observability-select',
149
- value: scope,
150
- disabled,
151
- onChange: (e) => onScope(e.target.value),
152
- },
153
- createElement('option', { value: 'session' }, strings.scopeSession()),
154
- createElement('option', { value: 'all' }, strings.scopeAll()),
155
- ),
156
- createElement(
157
- 'button',
158
- { type: 'button', className: 'dsh-my-observability-btn', disabled, onClick: onExportJson },
159
- strings.exportJson(),
160
- ),
161
- createElement(
162
- 'button',
163
- { type: 'button', className: 'dsh-my-observability-btn', disabled, onClick: onExportCsv },
164
- strings.exportCsv(),
165
- ),
166
- createElement(
167
- 'button',
168
- {
169
- type: 'button',
170
- className: 'dsh-my-observability-btn',
171
- 'aria-pressed': showStats,
172
- onClick: onToggleStats,
173
- },
174
- strings.statsTitle(),
175
- ),
176
- ),
177
- )
178
- }
179
-
180
- /** 工具调用统计视图(Top N 调用次数 + 失败率)。 */
181
- function StatsPanel({ events }) {
182
- const stats = computeToolStats(events, 5)
183
- if (stats.length === 0)
184
- return createElement('div', { className: 'dsh-my-observability-stats-empty' }, strings.statsEmpty())
185
- return createElement(
186
- 'div',
187
- { className: 'dsh-my-observability-stats' },
188
- createElement('div', { className: 'dsh-my-observability-stats-title' }, strings.statsTitle()),
189
- createElement(
190
- 'table',
191
- { className: 'dsh-my-observability-stats-table' },
192
- createElement(
193
- 'thead',
194
- null,
195
- createElement(
196
- 'tr',
197
- null,
198
- createElement('th', null, strings.statsTool()),
199
- createElement('th', null, strings.statsCalls()),
200
- createElement('th', null, strings.statsFailRate()),
201
- ),
202
- ),
203
- createElement(
204
- 'tbody',
205
- null,
206
- stats.map((s) =>
207
- createElement(
208
- 'tr',
209
- { key: s.tool },
210
- createElement('td', null, s.tool),
211
- createElement('td', null, String(s.calls)),
212
- createElement('td', null, `${(s.failRate * 100).toFixed(1)}%`),
213
- ),
214
- ),
215
- ),
216
- ),
217
- )
218
- }
219
-
220
- /** 无匹配状态(搜索/过滤条件命中 0 条)。 */
221
- function NoMatchesState() {
222
- return createElement(
223
- 'div',
224
- { className: 'dsh-my-observability-empty' },
225
- createElement('span', { className: 'dsh-my-observability-empty-icon' }, icon.search(20)),
226
- createElement('span', null, strings.noMatches()),
227
- )
228
- }
229
-
230
- /** 触发浏览器下载(Blob + a[download])。 */
231
- function downloadAudit(content, format) {
232
- const stamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')
233
- const filename = `${strings.exportFileName()}-${stamp}.${format}`
234
- const mime = format === 'json' ? 'application/json' : 'text/csv;charset=utf-8'
235
- const blob = new Blob([content], { type: mime })
236
- const url = URL.createObjectURL(blob)
237
- const a = document.createElement('a')
238
- a.href = url
239
- a.download = filename
240
- document.body.appendChild(a)
241
- a.click()
242
- a.remove()
243
- URL.revokeObjectURL(url)
244
- }
245
-
246
- /** 拉取全部会话事件(导出范围「全部会话」用),并应用当前过滤条件。 */
247
- async function allSessionEvents(criteria) {
248
- const all = await apiJson(
249
- `/observability/api/events?sessionId=${encodeURIComponent(ALL_SESSIONS)}&limit=${EXPORT_LIMIT_ALL}`,
250
- )
251
- return applyAuditFilter(all, criteria)
252
- }
253
-
254
- /** 导出:依据范围(当前会话=过滤后结果 / 全部会话=拉取后过滤)生成并下载。 */
255
- async function runExport(format, scope, filtered, criteria, onError) {
256
- try {
257
- const dataEvents = scope === 'session' ? filtered : await allSessionEvents(criteria)
258
- downloadAudit(format === 'json' ? auditToJson(dataEvents) : auditToCsv(dataEvents), format)
259
- } catch (err) {
260
- onError(err instanceof Error ? err.message : String(err))
261
- }
262
- }
263
-
264
- /** 轨迹回放面板的数据状态:会话列表 + 选中 + 事件 + 轮询 + 加载/错误。
265
- * 隐藏时暂停轮询,可见时按 REPLAY_POLL_MS 拉取并自动选当前会话。 */
266
- function useReplayDataState(props) {
267
- const currentSession = props?.scope?.sessionId || ''
268
- const visible = props?.visible !== false
269
- const [sessions, setSessions] = useState([])
270
- const [selected, setSelected] = useState('')
271
- const [events, setEvents] = useState([])
272
- const [loading, setLoading] = useState(true)
273
- const [error, setError] = useState('')
274
- const [reloadTick, setReloadTick] = useState(0)
275
- const [resource, setResource] = useState(null)
276
-
277
- // 资源采样轮询(写放大/资源超限预警;可见时 15s 一次,隐藏暂停)
278
- useEffect(() => {
279
- if (!visible) return undefined
280
- let alive = true
281
- const tick = () => {
282
- if (alive)
283
- apiJson('/observability/api/resources')
284
- .then(setResource)
285
- .catch(() => {})
286
- }
287
- tick()
288
- const timer = setInterval(tick, RESOURCE_POLL_MS)
289
- return () => {
290
- alive = false
291
- clearInterval(timer)
292
- }
293
- }, [visible])
294
-
295
- useEffect(() => {
296
- if (!visible) return undefined
297
- let alive = true
298
- const setters = { setSessions, setSelected, setEvents, setError, setLoading }
299
- const tick = () => {
300
- if (alive) void loadReplayData(selected, currentSession, setters)
301
- }
302
- tick()
303
- const timer = setInterval(tick, REPLAY_POLL_MS)
304
- return () => {
305
- alive = false
306
- clearInterval(timer)
307
- }
308
- }, [visible, selected, currentSession, reloadTick])
309
-
310
- const retry = () => {
311
- setError('')
312
- setLoading(true)
313
- setReloadTick((tick) => tick + 1)
314
- }
315
-
316
- return {
317
- currentSession,
318
- resource,
319
- sessions,
320
- selected,
321
- events,
322
- loading,
323
- error,
324
- setSessions,
325
- setSelected,
326
- setEvents,
327
- setLoading,
328
- setError,
329
- retry,
330
- }
331
- }
332
-
333
- /** 轨迹回放面板的视图状态:过滤条件(类型/关键词/时间/成功失败)+ 导出范围 +
334
- * 统计开关,及派生值(过滤结果 / 高亮行 / 可导出)。 */
335
- function useReplayState(props) {
336
- const data = useReplayDataState(props)
337
- const [filter, setFilter] = useState('')
338
- const [keyword, setKeyword] = useState('')
339
- const [timeStart, setTimeStart] = useState('')
340
- const [timeEnd, setTimeEnd] = useState('')
341
- const [result, setResult] = useState('')
342
- const [scope, setScope] = useState('session')
343
- const [showStats, setShowStats] = useState(false)
344
-
345
- const clearFilters = () => {
346
- setKeyword('')
347
- setTimeStart('')
348
- setTimeEnd('')
349
- setResult('')
350
- setFilter('')
351
- }
352
-
353
- const criteria = {
354
- type: filter,
355
- keyword,
356
- timeStart: datetimeToMs(timeStart),
357
- timeEnd: datetimeToMs(timeEnd),
358
- result,
359
- }
360
- const filtered = applyAuditFilter(data.events, criteria)
361
- const hasFilter = hasActiveFilter(criteria)
362
- const rows = filtered.map((event, index) => createElement(EventRow, { key: event.id ?? index, event, keyword }))
363
- const canExport = !data.loading && data.events.length > 0
364
- const onExport = (format) => void runExport(format, scope, filtered, criteria, data.setError)
365
-
366
- return {
367
- resource: data.resource,
368
- sessions: data.sessions,
369
- selected: data.selected,
370
- events: data.events,
371
- loading: data.loading,
372
- error: data.error,
373
- filter,
374
- keyword,
375
- timeStart,
376
- timeEnd,
377
- result,
378
- scope,
379
- showStats,
380
- setSelected: data.setSelected,
381
- setFilter,
382
- setKeyword,
383
- setTimeStart,
384
- setTimeEnd,
385
- setResult,
386
- setScope,
387
- setShowStats,
388
- retry: data.retry,
389
- clearFilters,
390
- filtered,
391
- hasFilter,
392
- rows,
393
- canExport,
394
- onExport,
395
- }
396
- }