Present RDM audit records in business language
Why: Project members need to understand who changed what, why, and with what impact without reading operation codes or raw audit payloads. Evidence: A RED presentation test first failed on the missing humanization layer; 276 frontend tests passed, design guard 10 checks passed, knip stayed at baseline 43, and npm run build passed. Impact: Humanize tree, relation, state, and operation vocabulary; render structured reason, impact, source, and outcome fields; raw event detail is no longer shown in the audit timeline. Refs: TT-U01, TT-U02, TT-U03, TT-U04; two trees one graph stage-five plan
This commit is contained in:
@@ -5,6 +5,10 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { api } from '../api'
|
||||
import { Chip, Collapsible, EmptyState, SectionLabel } from '@ui'
|
||||
import { classifyRdmError } from '../rdmErrors'
|
||||
import {
|
||||
describeAuditEvent, humanizeKind, humanizeRelation, humanizeState,
|
||||
type AuditEventViewInput,
|
||||
} from '../rdmAuditPresentation'
|
||||
import { store, useStore } from '../store'
|
||||
import { exportNeedsDraft } from '../workspaces'
|
||||
|
||||
@@ -17,7 +21,7 @@ interface ListShell<T = any> { items: T[]; nextCursor?: string; hasMore: boolean
|
||||
interface SummaryData { projectId: string; counts: Record<string, number>; watermark: number; lastOpAt: string; coverage?: CoverageItem[] }
|
||||
interface TreeItem { kind: string; id: string; title: string; state?: string }
|
||||
interface RelationEntry { layer: string; edgeId?: string; kind?: string; type?: string; state: string; depth?: number; from: any; to: any; conf?: number }
|
||||
interface EventDTO { seq: number; at: string; op: string; actor: string; source: string; target: { kind: string; id: string }; detail: string; operationId: string; disclosure: string }
|
||||
interface EventDTO extends AuditEventViewInput {}
|
||||
interface OpGroup { operationId: string; actor: string; at: string; events: EventDTO[] }
|
||||
interface ObjectDetail { kind: string; id: string; title?: string; state?: string; versions?: any[]; formal?: any; labels?: any; batch?: string; ver?: number; relations: RelationEntry[]; recentEvents: EventDTO[]; coverage?: CoverageItem[] }
|
||||
interface HistoryData { projectId: string; watermark: number; atSeq: number; object?: any; formal?: any; formalMissing?: string; countOnlySnapshots?: number; coverage?: CoverageItem[]; empty?: boolean }
|
||||
@@ -32,6 +36,18 @@ const TABS = [
|
||||
] as const
|
||||
type TabId = (typeof TABS)[number]['id']
|
||||
|
||||
const OPERATION_FILTERS = [
|
||||
['', '全部操作'],
|
||||
['doc.instantiate', '创建文档实例'],
|
||||
['doc.section.add', '新增文档章节'],
|
||||
['note.add', '新增项目知识'],
|
||||
['relation.add', '建立追溯关系'],
|
||||
['proposal.create', '提出结构变更'],
|
||||
['proposal.apply', '应用结构变更'],
|
||||
['risk.create', '发现项目风险'],
|
||||
['risk.answer', '回答风险澄清'],
|
||||
] as const
|
||||
|
||||
/** 错误态(增量 2):分类文案(能力缺失/登录失效/查询失败)+ 原始回执折叠供诊断;
|
||||
* 「无可审计项目」由调用处独立空态承担,不走本组件。 */
|
||||
function RdmErrorState({ raw }: { raw: string }) {
|
||||
@@ -347,9 +363,9 @@ function TreeList({ pid, kind }: { pid: string; kind: 'notes' | 'docs' }) {
|
||||
<button className={`rdm-audit-tree-node${selected?.id === item.id ? ' sel' : ''}`}
|
||||
onClick={() => selectNode(item)}>
|
||||
{item.kind === 'doc' && <span className="rdm-audit-tree-caret">{children[item.id] ? '▾' : '▸'}</span>}
|
||||
<span className="rdm-audit-tree-kind">{item.kind}</span>
|
||||
<span className="rdm-audit-tree-kind">{humanizeKind(item.kind)}</span>
|
||||
<span className="rdm-audit-tree-title">{item.title}</span>
|
||||
{item.state && <Chip tone={item.state === 'baselined' ? 'ok' : 'plain'}>{item.state}</Chip>}
|
||||
{item.state && <Chip tone={item.state === 'baselined' ? 'ok' : item.state === 'candidate' ? 'warn' : 'plain'}>{humanizeState(item.state)}</Chip>}
|
||||
</button>
|
||||
{item.kind === 'doc' && children[item.id] && (
|
||||
<div className="rdm-audit-tree-children">
|
||||
@@ -357,9 +373,9 @@ function TreeList({ pid, kind }: { pid: string; kind: 'notes' | 'docs' }) {
|
||||
{children[item.id].map(sub => (
|
||||
<button key={sub.id} className={`rdm-audit-tree-node${selected?.id === sub.id ? ' sel' : ''}`}
|
||||
onClick={() => selectNode(sub)}>
|
||||
<span className="rdm-audit-tree-kind">{sub.kind}</span>
|
||||
<span className="rdm-audit-tree-kind">{humanizeKind(sub.kind)}</span>
|
||||
<span className="rdm-audit-tree-title">{sub.title}</span>
|
||||
{sub.state && <Chip tone={sub.state === 'baselined' ? 'ok' : 'plain'}>{sub.state}</Chip>}
|
||||
{sub.state && <Chip tone={sub.state === 'baselined' ? 'ok' : sub.state === 'candidate' ? 'warn' : 'plain'}>{humanizeState(sub.state)}</Chip>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -440,7 +456,7 @@ function TraceTab({ pid }: { pid: string }) {
|
||||
<div className="rdm-audit-trace-head">
|
||||
<button className="btn v-ghost" onClick={() => setSel(null)}>← 返回</button>
|
||||
<strong>{sel.title}</strong>
|
||||
<Chip tone="plain">{sel.kind}:{sel.id}</Chip>
|
||||
<Chip tone="plain">{humanizeKind(sel.kind)} · {sel.id}</Chip>
|
||||
</div>
|
||||
{err && <RdmErrorState raw={err} />}
|
||||
{!relations && !err && <div className="empty-note">加载中…</div>}
|
||||
@@ -464,12 +480,13 @@ function TraceTab({ pid }: { pid: string }) {
|
||||
|
||||
function RelationRow({ r }: { r: RelationEntry }) {
|
||||
const other = r.from?.id && r.to?.id ? (r.from?.id < r.to?.id ? r.from : r.to) : (r.from || r.to)
|
||||
const readable = humanizeRelation(r)
|
||||
return (
|
||||
<div className="rdm-audit-rel">
|
||||
<Chip tone={r.layer === 'compliance' ? 'info' : 'plain'}>{r.layer}</Chip>
|
||||
<span className="rdm-audit-rel-type">{r.type || r.kind || '关联'}</span>
|
||||
<span className="rdm-audit-rel-endpoint">{other?.kind}:{other?.id}{other?.ver ? `@${other.ver}` : ''}</span>
|
||||
<Chip tone={r.state === 'active' ? 'ok' : r.state === 'candidate' ? 'warn' : 'plain'}>{r.state}</Chip>
|
||||
<Chip tone={r.layer === 'compliance' ? 'info' : 'plain'}>{readable.layer}</Chip>
|
||||
<span className="rdm-audit-rel-type">{readable.type}</span>
|
||||
<span className="rdm-audit-rel-endpoint">{humanizeKind(other?.kind)}“{other?.title || other?.id || '未知对象'}”{other?.ver ? `(版本 ${other.ver})` : ''}</span>
|
||||
<Chip tone={r.state === 'active' ? 'ok' : r.state === 'candidate' ? 'warn' : 'plain'}>{readable.state}</Chip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -521,8 +538,9 @@ function TimelineTab({ pid }: { pid: string }) {
|
||||
<div className="rdm-audit-timeline-filters">
|
||||
<input className="ui-input" placeholder="过滤操作者…" value={filterActor}
|
||||
onChange={e => setFilterActor(e.target.value)} />
|
||||
<input className="ui-input" placeholder="过滤操作类型…" value={filterOp}
|
||||
onChange={e => setFilterOp(e.target.value)} />
|
||||
<select className="ui-input" aria-label="按操作筛选" value={filterOp} onChange={e => setFilterOp(e.target.value)}>
|
||||
{OPERATION_FILTERS.map(([value, label]) => <option key={value || 'all'} value={value}>{label}</option>)}
|
||||
</select>
|
||||
<button className={`btn v-ghost${historyMode ? ' active' : ''}`} onClick={() => setHistoryMode(!historyMode)}>
|
||||
{historyMode ? '← 返回当前' : '历史模式'}
|
||||
</button>
|
||||
@@ -573,27 +591,38 @@ function TimelineTab({ pid }: { pid: string }) {
|
||||
}
|
||||
|
||||
function OpGroupView({ group }: { group: OpGroup }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const first = group.events[0] ? describeAuditEvent(group.events[0]) : null
|
||||
return (
|
||||
<div className="rdm-audit-opgroup">
|
||||
<button className="rdm-audit-opgroup-head" onClick={() => setOpen(!open)}>
|
||||
<span className="rdm-audit-opgroup-actor">{group.actor}</span>
|
||||
<Collapsible id={`rdm-audit-operation-${group.operationId}`} trigger={(
|
||||
<span className="rdm-audit-opgroup-head">
|
||||
<span className="rdm-audit-opgroup-actor">{first?.subject || group.actor}</span>
|
||||
<span className="rdm-audit-opgroup-action">{first?.action || '项目操作'}</span>
|
||||
<span className="rdm-audit-opgroup-time">{new Date(group.at).toLocaleString('zh-CN')}</span>
|
||||
<Chip tone="plain">{group.events.length} 个事件</Chip>
|
||||
</button>
|
||||
{open && group.events.map(ev => <EventRow key={ev.seq} ev={ev} />)}
|
||||
<Chip tone="plain">包含 {group.events.length} 项记录</Chip>
|
||||
</span>
|
||||
)}>
|
||||
{group.events.map(ev => <EventRow key={ev.seq} ev={ev} />)}
|
||||
</Collapsible>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EventRow({ ev }: { ev: EventDTO }) {
|
||||
const view = describeAuditEvent(ev)
|
||||
return (
|
||||
<div className="rdm-audit-event">
|
||||
<span className="rdm-audit-event-time">{new Date(ev.at).toLocaleTimeString('zh-CN')}</span>
|
||||
<span className="rdm-audit-event-op">{ev.op}</span>
|
||||
<span className="rdm-audit-event-actor">{ev.actor}</span>
|
||||
{ev.detail && <span className="rdm-audit-event-detail">{ev.detail.slice(0, 80)}</span>}
|
||||
{ev.disclosure !== 'ok' && <Chip tone="warn">{ev.disclosure === 'model-candidate' ? '模型候选' : '来源受限'}</Chip>}
|
||||
<div className="rdm-audit-event-head">
|
||||
<span className="rdm-audit-event-time">{new Date(ev.at).toLocaleString('zh-CN')}</span>
|
||||
<strong className="rdm-audit-event-action">{view.action}</strong>
|
||||
<Chip tone={ev.disclosure === 'ok' && view.outcome === '已生效' ? 'ok' : 'warn'}>{view.outcome}</Chip>
|
||||
</div>
|
||||
<div className="rdm-audit-event-summary">{view.subject}处理了{view.object}</div>
|
||||
<dl className="rdm-audit-event-fields">
|
||||
<div><dt>原因</dt><dd>{view.reason}</dd></div>
|
||||
<div><dt>影响</dt><dd>{view.impact}</dd></div>
|
||||
<div><dt>依据</dt><dd>{view.source}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -604,8 +633,8 @@ function ObjectDetailView({ detail }: { detail: ObjectDetail }) {
|
||||
return (
|
||||
<div className="rdm-audit-detail-body">
|
||||
<div><strong>{detail.title || detail.id}</strong></div>
|
||||
<div>{detail.kind}:{detail.id}{detail.ver ? `@${detail.ver}` : ''}</div>
|
||||
{detail.state && <div>状态:<Chip tone={detail.state === 'baselined' ? 'ok' : 'plain'}>{detail.state}</Chip></div>}
|
||||
<div>{humanizeKind(detail.kind)} · 对象编号 {detail.id}{detail.ver ? ` · 版本 ${detail.ver}` : ''}</div>
|
||||
{detail.state && <div>状态:<Chip tone={detail.state === 'baselined' ? 'ok' : detail.state === 'candidate' ? 'warn' : 'plain'}>{humanizeState(detail.state)}</Chip></div>}
|
||||
{detail.labels && <div>标签:{detail.labels.stage}{detail.labels.topics?.length ? ` / ${detail.labels.topics.join(', ')}` : ''}</div>}
|
||||
{detail.versions && detail.versions.length > 0 && (
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describeAuditEvent, humanizeKind, humanizeRelation, humanizeState } from './rdmAuditPresentation'
|
||||
|
||||
describe('RDM audit presentation', () => {
|
||||
it('turns a structured document event into a readable action and impact', () => {
|
||||
expect(describeAuditEvent({
|
||||
seq: 18,
|
||||
at: '2026-10-15T08:00:00Z',
|
||||
op: 'doc.instantiate',
|
||||
actor: 'alice',
|
||||
source: 'template:dhf-proposal@v2',
|
||||
target: { kind: 'doc', id: 'doc-1', title: '项目建议书' },
|
||||
detail: '{"raw":"must not be shown"}',
|
||||
operationId: 'op-1',
|
||||
disclosure: 'ok',
|
||||
result: 'committed',
|
||||
impact: '新增 12 个递归节点',
|
||||
reason: '从已发布模板建立项目实例',
|
||||
})).toEqual({
|
||||
action: '创建文档实例',
|
||||
subject: 'Alice',
|
||||
object: '文档“项目建议书”',
|
||||
outcome: '已生效',
|
||||
reason: '从已发布模板建立项目实例',
|
||||
impact: '新增 12 个递归节点',
|
||||
source: '模板:DHF 项目建议书(版本 v2)',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not expose unknown operation codes or raw JSON as user-facing text', () => {
|
||||
const view = describeAuditEvent({
|
||||
seq: 19,
|
||||
at: '2026-10-15T08:01:00Z',
|
||||
op: 'internal.weird_opcode',
|
||||
actor: 'user-42',
|
||||
source: 'host',
|
||||
target: { kind: 'node', id: 'node-secret' },
|
||||
detail: '{"secret":"raw payload"}',
|
||||
operationId: 'op-2',
|
||||
disclosure: 'restricted',
|
||||
})
|
||||
expect(view.action).toBe('更新项目内容')
|
||||
expect(JSON.stringify(view)).not.toContain('internal.weird_opcode')
|
||||
expect(JSON.stringify(view)).not.toContain('raw payload')
|
||||
expect(view.outcome).toBe('部分信息受限')
|
||||
})
|
||||
|
||||
it('humanizes tree, relationship, and lifecycle vocabulary', () => {
|
||||
expect(humanizeKind('section')).toBe('章节')
|
||||
expect(humanizeState('candidate')).toBe('待确认')
|
||||
expect(humanizeRelation({ layer: 'compliance', type: 'verifies', state: 'active' })).toEqual({
|
||||
layer: '合规关系',
|
||||
type: '验证',
|
||||
state: '已生效',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,134 @@
|
||||
export interface AuditEventViewInput {
|
||||
seq: number
|
||||
at: string
|
||||
op: string
|
||||
actor: string
|
||||
source: string
|
||||
target: { kind: string; id: string; title?: string }
|
||||
detail?: string
|
||||
operationId: string
|
||||
disclosure: string
|
||||
summary?: string
|
||||
result?: string
|
||||
reason?: string
|
||||
impact?: string
|
||||
}
|
||||
|
||||
interface AuditEventPresentation {
|
||||
action: string
|
||||
subject: string
|
||||
object: string
|
||||
outcome: string
|
||||
reason: string
|
||||
impact: string
|
||||
source: string
|
||||
}
|
||||
|
||||
const KIND_LABELS: Record<string, string> = {
|
||||
doc: '文档', section: '章节', node: '节点', note: '知识', slice: '资料切片',
|
||||
requirement: '要求', risk: '风险', proposal: '变更提案', relation: '追溯关系',
|
||||
template: '模板', project: '项目', conclusion: '结论', issue: '问题',
|
||||
}
|
||||
|
||||
const STATE_LABELS: Record<string, string> = {
|
||||
active: '已生效', candidate: '待确认', pending: '待处理', baselined: '已建立基线',
|
||||
approved: '已批准', rejected: '已拒绝', revoked: '已撤销', superseded: '已被取代',
|
||||
failed: '失败', committed: '已生效', accepted: '已接受', cancelled: '已取消',
|
||||
unknown: '结果未知', restricted: '部分信息受限',
|
||||
}
|
||||
|
||||
const OP_LABELS: Record<string, string> = {
|
||||
'doc.instantiate': '创建文档实例',
|
||||
'doc.section.add': '新增文档章节',
|
||||
'doc.section.update': '更新文档章节',
|
||||
'doc.node.add': '新增文档节点',
|
||||
'doc.node.update': '更新文档节点',
|
||||
'doc.approve': '批准文档版本',
|
||||
'note.add': '新增项目知识',
|
||||
'note.update': '更新项目知识',
|
||||
'relation.add': '建立追溯关系',
|
||||
'relation.invalidate': '标记关系待复核',
|
||||
'proposal.create': '提出结构变更',
|
||||
'proposal.apply': '应用结构变更',
|
||||
'proposal.reject': '拒绝结构变更',
|
||||
'risk.create': '发现项目风险',
|
||||
'risk.answer': '回答风险澄清',
|
||||
'risk.resolve': '完成风险处理',
|
||||
'req.add': '新增项目要求',
|
||||
'req.update': '更新项目要求',
|
||||
}
|
||||
|
||||
const RELATION_LABELS: Record<string, string> = {
|
||||
verifies: '验证', supports: '支持', derives: '派生自', impacts: '影响',
|
||||
traces: '追溯到', replaces: '取代', conflicts: '存在冲突', references: '引用',
|
||||
}
|
||||
|
||||
const LAYER_LABELS: Record<string, string> = {
|
||||
compliance: '合规关系', knowledge: '知识关系', document: '文档关系', project: '项目关系',
|
||||
}
|
||||
|
||||
export function humanizeKind(kind: string): string {
|
||||
return KIND_LABELS[kind] || '项目对象'
|
||||
}
|
||||
|
||||
export function humanizeState(state: string): string {
|
||||
return STATE_LABELS[state] || '状态已记录'
|
||||
}
|
||||
|
||||
function humanizeOperation(op: string): string {
|
||||
if (OP_LABELS[op]) return OP_LABELS[op]
|
||||
if (op.startsWith('doc.')) return '更新文档内容'
|
||||
if (op.startsWith('note.') || op.startsWith('knowledge.')) return '更新项目知识'
|
||||
if (op.startsWith('relation.') || op.startsWith('trace.')) return '更新追溯关系'
|
||||
if (op.startsWith('risk.')) return '处理项目风险'
|
||||
if (op.startsWith('proposal.')) return '处理结构变更'
|
||||
return '更新项目内容'
|
||||
}
|
||||
|
||||
function humanizeActor(actor: string): string {
|
||||
if (!actor) return '未知成员'
|
||||
if (/^[a-z][a-z0-9._-]*$/i.test(actor) && !actor.includes('-') && !actor.includes('_')) {
|
||||
return actor.charAt(0).toUpperCase() + actor.slice(1)
|
||||
}
|
||||
return actor
|
||||
}
|
||||
|
||||
function humanizeSource(source: string): string {
|
||||
if (!source) return '当时未记录来源'
|
||||
const template = /^template:([^@]+)@(.+)$/.exec(source)
|
||||
if (template) {
|
||||
const title = template[1] === 'dhf-proposal' ? 'DHF 项目建议书' : template[1]
|
||||
return `模板:${title}(版本 ${template[2]})`
|
||||
}
|
||||
if (source === 'host') return '系统受信操作记录'
|
||||
if (source.startsWith('message:')) return '项目对话'
|
||||
if (source.startsWith('artifact:')) return '项目文件'
|
||||
return '已登记项目来源'
|
||||
}
|
||||
|
||||
function humanizeOutcome(input: AuditEventViewInput): string {
|
||||
if (input.disclosure !== 'ok') return input.disclosure === 'model-candidate' ? '模型候选,尚未确认' : '部分信息受限'
|
||||
return humanizeState(input.result || 'committed')
|
||||
}
|
||||
|
||||
export function describeAuditEvent(input: AuditEventViewInput): AuditEventPresentation {
|
||||
const targetTitle = input.target?.title ? `“${input.target.title}”` : ''
|
||||
return {
|
||||
action: input.summary || humanizeOperation(input.op),
|
||||
subject: humanizeActor(input.actor),
|
||||
object: `${humanizeKind(input.target?.kind)}${targetTitle}`,
|
||||
outcome: humanizeOutcome(input),
|
||||
reason: input.reason || '未记录具体原因',
|
||||
impact: input.impact || '影响范围尚未记录',
|
||||
source: humanizeSource(input.source),
|
||||
}
|
||||
}
|
||||
|
||||
export function humanizeRelation(input: { layer?: string; type?: string; kind?: string; state?: string }) {
|
||||
const type = input.type || input.kind || ''
|
||||
return {
|
||||
layer: LAYER_LABELS[input.layer || ''] || '项目关系',
|
||||
type: RELATION_LABELS[type] || '关联',
|
||||
state: humanizeState(input.state || ''),
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -625,12 +625,17 @@ button { font-family: inherit; font-size: inherit; color: inherit; }
|
||||
.rdm-audit-opgroup-head { display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 10px; background: var(--bg-panel); border: none; cursor: pointer; font-size: var(--fs-ui-sm); color: var(--text-dim); text-align: left; }
|
||||
.rdm-audit-opgroup-head:hover { background: var(--bg-hover); }
|
||||
.rdm-audit-opgroup-actor { font-weight: 550; color: var(--text); }
|
||||
.rdm-audit-opgroup-action { color: var(--text-dim); }
|
||||
.rdm-audit-opgroup-time { font-size: var(--fs-ui-xs); color: var(--text-faint); }
|
||||
.rdm-audit-event { display: flex; align-items: center; gap: 8px; padding: 4px 10px; font-size: var(--fs-ui-sm); border-top: 1px solid var(--border-soft); }
|
||||
.rdm-audit-event-time { font-size: var(--fs-ui-xs); color: var(--text-faint); min-width: 5em; }
|
||||
.rdm-audit-event-op { font-family: var(--mono); font-size: var(--fs-ui-xs); color: var(--accent-text); min-width: 8em; }
|
||||
.rdm-audit-event-actor { color: var(--text-dim); min-width: 4em; }
|
||||
.rdm-audit-event-detail { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-dim); }
|
||||
.rdm-audit-event { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; font-size: var(--fs-ui-sm); border-top: 1px solid var(--border-soft); }
|
||||
.rdm-audit-event-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.rdm-audit-event-time { font-size: var(--fs-ui-xs); color: var(--text-dim); }
|
||||
.rdm-audit-event-action { color: var(--text); }
|
||||
.rdm-audit-event-summary { color: var(--text); }
|
||||
.rdm-audit-event-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; }
|
||||
.rdm-audit-event-fields > div { min-width: 0; padding: 8px; background: var(--bg-raised); border-radius: var(--radius-sm); }
|
||||
.rdm-audit-event-fields dt { color: var(--text-dim); font-size: var(--fs-ui-xs); }
|
||||
.rdm-audit-event-fields dd { margin: 3px 0 0; color: var(--text); overflow-wrap: anywhere; }
|
||||
.rdm-audit-coverage { margin-top: 12px; }
|
||||
.rdm-audit-export { margin-top: 16px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.rdm-audit-export-row { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; }
|
||||
@@ -644,3 +649,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; }
|
||||
.rdm-audit-err { display: flex; flex-direction: column; gap: 8px; }
|
||||
.rdm-audit-err-toggle { font-size: var(--fs-ui-caption); color: var(--text-dim); }
|
||||
.rdm-audit-err-raw { display: block; font-family: var(--mono); font-size: var(--fs-code); color: var(--text-dim); background: var(--bg-inset); border-radius: var(--radius-sm); padding: 8px 12px; white-space: pre-wrap; word-break: break-all; }
|
||||
@media (max-width: 760px) {
|
||||
.rdm-audit-event-fields { grid-template-columns: 1fr; }
|
||||
.rdm-audit-trace-cols { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user