-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDiffTool.tsx
More file actions
118 lines (111 loc) · 4.65 KB
/
Copy pathDiffTool.tsx
File metadata and controls
118 lines (111 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import type { JSX } from 'react';
import { useMemo, useState } from 'react';
import { useAnalysisStore } from '../stores/analysisStore';
import { useProjectStore } from '../stores/projectStore';
import { useUiStore } from '../stores/uiStore';
import { Button, Select, EmptyState, Badge, TextArea } from '../components/ui/primitives';
import { Icon } from '../components/ui/Icons';
import { FeatureShell } from './FeatureShell';
import { diffSpecs } from '../core/openapi/diff';
import { parseYaml } from '../core/yaml';
import { downloadText, groupBy } from '../core/utils/misc';
import type { ApiDiffChange } from '../types';
export function DiffTool(): JSX.Element {
const project = useProjectStore((s) => s.project);
const doc = useAnalysisStore((s) => s.doc);
const toast = useUiStore((s) => s.toast);
const [otherText, setOtherText] = useState('');
const [otherName, setOtherName] = useState('Comparison spec');
const otherDoc = useMemo(() => {
if (!otherText.trim()) return undefined;
try {
return parseYaml(otherText);
} catch {
return undefined;
}
}, [otherText]);
const report = useMemo(() => {
if (!doc || !otherDoc) return undefined;
try {
return diffSpecs(doc, otherDoc);
} catch (err) {
toast('error', 'Diff failed', err instanceof Error ? err.message : String(err));
return undefined;
}
}, [doc, otherDoc, toast]);
const changes = report?.changes ?? [];
return (
<FeatureShell
title="API Diff"
subtitle="Compare two OpenAPI documents for breaking and non-breaking changes."
actions={
<div className="row">
<Select value={otherName} onChange={(e) => setOtherName(e.target.value)} style={{ width: 160 }}>
<option value="Comparison spec">Comparison spec</option>
{project?.files.map((f) => <option key={f.id} value={f.name}>{f.name}</option>)}
</Select>
{report && (
<Button variant="ghost" size="sm" onClick={() => downloadText('diff-report.md', diffReportMd(report.changes), 'text/markdown')}>
<Icon name="download" size={13} /> Report
</Button>
)}
</div>
}
>
<div className="grid-2" style={{ alignItems: 'start' }}>
<div className="card">
<div className="card-title" style={{ marginBottom: 8 }}>Paste the other spec</div>
<TextArea rows={16} className="mono" value={otherText} onChange={(e) => setOtherText(e.target.value)} placeholder="openapi: 3.1.0 info: title: ..." />
<div className="muted small" style={{ marginTop: 6 }}>
{otherDoc ? 'Comparison spec parsed.' : 'Waiting for a parseable spec…'}
</div>
</div>
<div className="stack">
{report ? (
<>
<div className="row">
<Badge tone="green">{report.addedEndpoints.length} added</Badge>
<Badge tone="red">{report.removedEndpoints.length} removed</Badge>
<Badge tone="amber">{report.breaking.length} breaking</Badge>
<Badge tone="blue">{report.nonBreaking.length} non-breaking</Badge>
</div>
<div className="card" style={{ maxHeight: 480, overflowY: 'auto' }}>
<ChangesList changes={changes} />
</div>
</>
) : (
<EmptyState title="No diff yet" hint="Paste or load a second spec to compute a diff against the current one." />
)}
</div>
</div>
</FeatureShell>
);
}
function ChangesList({ changes }: { changes: ApiDiffChange[] }): JSX.Element {
const grouped = groupBy(changes, (c) => c.category);
return (
<div className="stack">
{Object.entries(grouped).map(([cat, items]) => (
<div key={cat}>
<div className="label" style={{ marginBottom: 4 }}>{cat}</div>
{items.map((c) => (
<div key={c.id} className="row" style={{ alignItems: 'baseline' }}>
<Badge tone={c.kind === 'added' ? 'green' : c.kind === 'removed' ? 'red' : c.changeClass === 'breaking' ? 'red' : c.changeClass === 'non-breaking' ? 'amber' : 'gray'}>
{c.kind}
</Badge>
<span className="mono small" style={{ color: 'var(--fg-2)' }}>{c.path}</span>
<span className="small" style={{ color: 'var(--fg-1)' }}>{c.description}</span>
</div>
))}
</div>
))}
</div>
);
}
function diffReportMd(changes: ApiDiffChange[]): string {
const lines = ['# API Diff Report', ''];
for (const c of changes) {
lines.push(`- **${c.kind}** (${c.changeClass}) \`${c.path}\` — ${c.description}`);
}
return lines.join('\n');
}