数据源设置审计
-
+ 全部数据源的配置操作记录,按时间倒序展示,包含已移除的连接。
+ {rows.length ? (
+
+
+
+
+ | 时间 |
+ 操作人 |
+ 数据源 |
+ 操作 |
+ 变更内容 |
+
+
+
+ {rows.map((row) => (
+
+ |
+
+ |
+ {row.actor || '未记录'} |
+ {row.source_name} |
+ {sourceAuditActions[row.action] ?? '配置操作'} |
+ {row.detail} |
+
+ ))}
+
+
+
+ ) : null}
+ {loading ? 加载操作记录中…
: null}
{error ? (
- {error}
+ {error}{' '}
+
- ) : loading ? (
- 加载中…
- ) : !rows.length ? (
- 暂无设置变更记录。
- ) : (
-
- {rows.map((row) => (
- -
- 修订 {row.revision_number}
-
- {row.state === 'active'
- ? '当前生效'
- : row.state === 'retired'
- ? '历史版本'
- : row.state === 'validated'
- ? '待启用'
- : '草稿或失败'}
-
-
- {row.created_by} · {new Date(row.created_at).toLocaleString('zh-CN')} ·{' '}
- {row.enabled ? '允许自动刷新' : '已暂停'} ·{' '}
- {row.refresh_interval_seconds ? `周期 ${row.refresh_interval_seconds} 秒` : '仅手动'}
-
-
- ))}
-
- )}
+ ) : null}
+ {!loading && !error && !rows.length ? 暂无数据源配置操作记录。
: null}
+ {rows.length ? (
+
+ 已展示 {rows.length} / {total} 条记录
+
+ ) : null}
+ {!loading && !error && rows.length < total ? (
+
+ ) : null}
)
}
diff --git a/web/src/sourceSettingsAudit.test.ts b/web/src/sourceSettingsAudit.test.ts
new file mode 100644
index 0000000..571f8aa
--- /dev/null
+++ b/web/src/sourceSettingsAudit.test.ts
@@ -0,0 +1,40 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { fetchSourceAudit } from './sourceSettingsAudit'
+afterEach(() => vi.unstubAllGlobals())
+describe('all-source operation audit', () => {
+ it('requests paginated global history without a source selector', async () => {
+ const page = {
+ items: [
+ {
+ id: '1',
+ connection_id: 'removed',
+ source_name: '已移除来源',
+ action: 'archived',
+ actor: 'tester',
+ created_at: '2026-10-27T02:00:00Z',
+ detail: '移除连接',
+ },
+ ],
+ total: 101,
+ limit: 100,
+ offset: 100,
+ }
+ const fetcher = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: page }) })
+ vi.stubGlobal('fetch', fetcher)
+ expect(await fetchSourceAudit(100)).toEqual(page)
+ expect(fetcher.mock.calls[0][0]).toBe('/api/v1/source-connections/audit?limit=100&offset=100')
+ })
+ it('does not disguise errors as empty history', async () => {
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))
+ await expect(fetchSourceAudit()).rejects.toThrow('无法读取')
+ })
+ it('rejects malformed history', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi
+ .fn()
+ .mockResolvedValue({ ok: true, json: async () => ({ data: { items: [], total: '0', offset: 0, limit: 100 } }) })
+ )
+ await expect(fetchSourceAudit()).rejects.toThrow('格式无效')
+ })
+})
diff --git a/web/src/sourceSettingsAudit.ts b/web/src/sourceSettingsAudit.ts
new file mode 100644
index 0000000..73cf6cf
--- /dev/null
+++ b/web/src/sourceSettingsAudit.ts
@@ -0,0 +1,48 @@
+export type SourceAuditItem = {
+ id: string
+ connection_id: string
+ source_name: string
+ action: string
+ actor: string
+ created_at: string
+ detail: string
+}
+export type SourceAuditPage = { items: SourceAuditItem[]; total: number; limit: number; offset: number }
+export async function fetchSourceAudit(offset = 0, signal?: AbortSignal): Promise