dbdiff-app 0.1.0
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.
- package/README.md +73 -0
- package/bin/cli.js +83 -0
- package/bin/install-local.js +57 -0
- package/electron/generate-icon.mjs +54 -0
- package/electron/icon.icns +0 -0
- package/electron/icon.png +0 -0
- package/electron/icon.svg +21 -0
- package/electron/main.js +169 -0
- package/electron/patch-dev-plist.js +31 -0
- package/electron/preload.cjs +18 -0
- package/electron/wait-for-vite.js +43 -0
- package/index.html +13 -0
- package/package.json +91 -0
- package/public/favicon.svg +15 -0
- package/public/vite.svg +1 -0
- package/server/export.ts +57 -0
- package/server/index.ts +392 -0
- package/src/App.css +1 -0
- package/src/App.tsx +543 -0
- package/src/assets/react.svg +1 -0
- package/src/components/CommandPalette.tsx +243 -0
- package/src/components/ConnectedView.tsx +78 -0
- package/src/components/ConnectionPicker.tsx +381 -0
- package/src/components/ConsoleView.tsx +360 -0
- package/src/components/CsvExportModal.tsx +144 -0
- package/src/components/DataGrid/DataGrid.tsx +262 -0
- package/src/components/DataGrid/DataGridCell.tsx +73 -0
- package/src/components/DataGrid/DataGridHeader.tsx +89 -0
- package/src/components/DataGrid/index.ts +20 -0
- package/src/components/DataGrid/types.ts +63 -0
- package/src/components/DataGrid/useColumnResize.ts +153 -0
- package/src/components/DataGrid/useDataGridSelection.ts +340 -0
- package/src/components/DataGrid/utils.ts +184 -0
- package/src/components/DatabaseMenu.tsx +93 -0
- package/src/components/DatabaseSwitcher.tsx +208 -0
- package/src/components/DiffView.tsx +215 -0
- package/src/components/EditConnectionModal.tsx +417 -0
- package/src/components/ErrorBoundary.tsx +69 -0
- package/src/components/GlobalShortcuts.tsx +201 -0
- package/src/components/InnerTabBar.tsx +129 -0
- package/src/components/JsonTreeViewer.tsx +387 -0
- package/src/components/MemberAccessEditor.tsx +443 -0
- package/src/components/MembersModal.tsx +446 -0
- package/src/components/NewConnectionModal.tsx +274 -0
- package/src/components/Resizer.tsx +66 -0
- package/src/components/ScanSuccessModal.tsx +113 -0
- package/src/components/ShortcutSettingsModal.tsx +318 -0
- package/src/components/Sidebar.tsx +532 -0
- package/src/components/TabBar.tsx +188 -0
- package/src/components/TableView.tsx +2147 -0
- package/src/components/ThemeToggle.tsx +44 -0
- package/src/components/index.ts +17 -0
- package/src/constants.ts +12 -0
- package/src/electron.d.ts +12 -0
- package/src/index.css +44 -0
- package/src/main.tsx +13 -0
- package/src/stores/hooks.ts +1146 -0
- package/src/stores/index.ts +12 -0
- package/src/stores/store.ts +1514 -0
- package/src/stores/useCloudSync.ts +274 -0
- package/src/stores/useSyncDatabase.ts +422 -0
- package/src/types.ts +277 -0
- package/src/utils/csv.ts +27 -0
- package/src/vite-env.d.ts +2 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/tsconfig.server.json +14 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import Fuse from "fuse.js";
|
|
3
|
+
import { useHotkey } from "../stores/hooks";
|
|
4
|
+
import { useStore } from "../stores/store";
|
|
5
|
+
|
|
6
|
+
interface DatabaseSwitcherProps {
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DatabaseSwitcher({ onClose }: DatabaseSwitcherProps) {
|
|
11
|
+
const [query, setQuery] = useState("");
|
|
12
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
13
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
14
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
const databaseConfigs = useStore((state) => state.databaseConfigs);
|
|
17
|
+
const connectionTabs = useStore((state) => state.connectionTabs);
|
|
18
|
+
const selectConnectionTab = useStore((state) => state.selectConnectionTab);
|
|
19
|
+
const createConnectionTab = useStore((state) => state.createConnectionTab);
|
|
20
|
+
const connectToDatabase = useStore((state) => state.connectToDatabase);
|
|
21
|
+
|
|
22
|
+
// Build list with "already open" status
|
|
23
|
+
const items = useMemo(() => {
|
|
24
|
+
return databaseConfigs.map((config) => {
|
|
25
|
+
const openTab = connectionTabs.find(
|
|
26
|
+
(t) => t.databaseConfigId === config.id,
|
|
27
|
+
);
|
|
28
|
+
return {
|
|
29
|
+
id: config.id,
|
|
30
|
+
name: config.display.name,
|
|
31
|
+
color: config.display.color,
|
|
32
|
+
connectionString: `${config.connection.host}:${config.connection.port}/${config.connection.database}`,
|
|
33
|
+
isOpen: !!openTab,
|
|
34
|
+
openTabId: openTab?.id ?? null,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}, [databaseConfigs, connectionTabs]);
|
|
38
|
+
|
|
39
|
+
// Fuse.js for fuzzy search
|
|
40
|
+
const fuse = useMemo(
|
|
41
|
+
() =>
|
|
42
|
+
new Fuse(items, {
|
|
43
|
+
keys: ["name", "connectionString"],
|
|
44
|
+
threshold: 0.4,
|
|
45
|
+
ignoreLocation: true,
|
|
46
|
+
}),
|
|
47
|
+
[items],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const filteredItems = useMemo(() => {
|
|
51
|
+
if (!query.trim()) return items;
|
|
52
|
+
return fuse.search(query).map((r) => r.item);
|
|
53
|
+
}, [fuse, items, query]);
|
|
54
|
+
|
|
55
|
+
// Reset selection when filter changes
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setSelectedIndex(0);
|
|
58
|
+
}, [query]);
|
|
59
|
+
|
|
60
|
+
// Auto-focus input
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
inputRef.current?.focus();
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
// Scroll selected item into view
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const listElement = listRef.current;
|
|
68
|
+
if (!listElement) return;
|
|
69
|
+
const selectedElement = listElement.children[selectedIndex] as HTMLElement;
|
|
70
|
+
if (selectedElement) {
|
|
71
|
+
selectedElement.scrollIntoView({ block: "nearest" });
|
|
72
|
+
}
|
|
73
|
+
}, [selectedIndex]);
|
|
74
|
+
|
|
75
|
+
useHotkey("closeModal", onClose);
|
|
76
|
+
|
|
77
|
+
const handleSelect = useCallback(
|
|
78
|
+
(item: (typeof items)[0]) => {
|
|
79
|
+
if (item.isOpen && item.openTabId) {
|
|
80
|
+
// Switch to existing tab
|
|
81
|
+
selectConnectionTab(item.openTabId);
|
|
82
|
+
} else {
|
|
83
|
+
// Open new connection tab and connect
|
|
84
|
+
createConnectionTab();
|
|
85
|
+
// connectToDatabase operates on the active tab, which is the one we just created
|
|
86
|
+
connectToDatabase(item.id);
|
|
87
|
+
}
|
|
88
|
+
onClose();
|
|
89
|
+
},
|
|
90
|
+
[selectConnectionTab, createConnectionTab, connectToDatabase, onClose],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const handleKeyDown = useCallback(
|
|
94
|
+
(e: React.KeyboardEvent) => {
|
|
95
|
+
switch (e.key) {
|
|
96
|
+
case "ArrowDown":
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
setSelectedIndex((i) => Math.min(i + 1, filteredItems.length - 1));
|
|
99
|
+
break;
|
|
100
|
+
case "ArrowUp":
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
103
|
+
break;
|
|
104
|
+
case "Enter":
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
if (filteredItems[selectedIndex]) {
|
|
107
|
+
handleSelect(filteredItems[selectedIndex]);
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case "Escape":
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
onClose();
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
[filteredItems, selectedIndex, handleSelect, onClose],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]">
|
|
121
|
+
{/* Backdrop */}
|
|
122
|
+
<div
|
|
123
|
+
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
124
|
+
onClick={onClose}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
{/* Content */}
|
|
128
|
+
<div className="relative bg-white dark:bg-[#1a1a1a] rounded-xl shadow-2xl w-full max-w-lg mx-4 border border-stone-200 dark:border-white/10 overflow-hidden">
|
|
129
|
+
{/* Search input */}
|
|
130
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-stone-200 dark:border-white/10">
|
|
131
|
+
<svg
|
|
132
|
+
className="w-5 h-5 text-tertiary"
|
|
133
|
+
fill="none"
|
|
134
|
+
stroke="currentColor"
|
|
135
|
+
viewBox="0 0 24 24"
|
|
136
|
+
>
|
|
137
|
+
<path
|
|
138
|
+
strokeLinecap="round"
|
|
139
|
+
strokeLinejoin="round"
|
|
140
|
+
strokeWidth={2}
|
|
141
|
+
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"
|
|
142
|
+
/>
|
|
143
|
+
</svg>
|
|
144
|
+
<input
|
|
145
|
+
ref={inputRef}
|
|
146
|
+
type="text"
|
|
147
|
+
value={query}
|
|
148
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
149
|
+
onKeyDown={handleKeyDown}
|
|
150
|
+
placeholder="Switch database..."
|
|
151
|
+
className="flex-1 bg-transparent text-primary placeholder-tertiary outline-none text-sm"
|
|
152
|
+
/>
|
|
153
|
+
<kbd className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
|
|
154
|
+
esc
|
|
155
|
+
</kbd>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Results list */}
|
|
159
|
+
<div ref={listRef} className="max-h-80 overflow-y-auto">
|
|
160
|
+
{filteredItems.length === 0 ? (
|
|
161
|
+
<div className="px-4 py-8 text-center text-tertiary text-sm">
|
|
162
|
+
{items.length === 0
|
|
163
|
+
? "No databases configured"
|
|
164
|
+
: "No matching databases"}
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
filteredItems.map((item, index) => (
|
|
168
|
+
<div
|
|
169
|
+
key={item.id}
|
|
170
|
+
onClick={() => handleSelect(item)}
|
|
171
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
172
|
+
className={`
|
|
173
|
+
flex items-center gap-3 px-4 py-2.5 cursor-pointer
|
|
174
|
+
${
|
|
175
|
+
index === selectedIndex
|
|
176
|
+
? "bg-stone-100 dark:bg-white/5"
|
|
177
|
+
: "hover:bg-stone-50 dark:hover:bg-white/[0.02]"
|
|
178
|
+
}
|
|
179
|
+
`}
|
|
180
|
+
>
|
|
181
|
+
<span
|
|
182
|
+
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
|
|
183
|
+
style={{ backgroundColor: item.color }}
|
|
184
|
+
/>
|
|
185
|
+
<div className="flex-1 min-w-0">
|
|
186
|
+
<span className="text-primary text-sm">{item.name}</span>
|
|
187
|
+
<span className="text-tertiary text-xs ml-2">
|
|
188
|
+
{item.connectionString}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
{item.isOpen && (
|
|
192
|
+
<span className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
|
|
193
|
+
open
|
|
194
|
+
</span>
|
|
195
|
+
)}
|
|
196
|
+
{index === selectedIndex && (
|
|
197
|
+
<kbd className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
|
|
198
|
+
↵
|
|
199
|
+
</kbd>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
))
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { DiffResponse, DiffTableResult } from "../types";
|
|
2
|
+
import { formatCellValue } from "./DataGrid/utils";
|
|
3
|
+
|
|
4
|
+
interface DiffViewProps {
|
|
5
|
+
diffResult: DiffResponse;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function DiffView({ diffResult }: DiffViewProps) {
|
|
9
|
+
if (diffResult.tables.length === 0) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex items-center justify-center h-full text-tertiary text-[13px]">
|
|
12
|
+
No changes detected
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const totalChanges = diffResult.tables.reduce(
|
|
18
|
+
(sum, t) => sum + t.deleted.length + t.added.length + t.modified.length,
|
|
19
|
+
0,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (totalChanges === 0) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex items-center justify-center h-full text-tertiary text-[13px]">
|
|
25
|
+
No changes detected — all rows unchanged
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="h-full flex flex-col">
|
|
32
|
+
<div className="flex-shrink-0 flex items-center px-4 py-2 border-b border-stone-200 dark:border-white/[0.06] bg-stone-50 dark:bg-white/[0.02]">
|
|
33
|
+
<span className="text-[12px] text-secondary">
|
|
34
|
+
Diff preview (transaction rolled back)
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="flex-1 min-h-0 overflow-auto">
|
|
38
|
+
{diffResult.tables.map((table) => (
|
|
39
|
+
<DiffTable key={table.tableName} table={table} />
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function DiffTable({ table }: { table: DiffTableResult }) {
|
|
47
|
+
const parts: string[] = [];
|
|
48
|
+
if (table.added.length > 0) parts.push(`${table.added.length} added`);
|
|
49
|
+
if (table.modified.length > 0)
|
|
50
|
+
parts.push(`${table.modified.length} modified`);
|
|
51
|
+
if (table.deleted.length > 0) parts.push(`${table.deleted.length} deleted`);
|
|
52
|
+
|
|
53
|
+
const colNames = table.columns.map((c) => c.name);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="pb-4 h-full flex flex-col">
|
|
57
|
+
{/* Table header */}
|
|
58
|
+
<div className="sticky top-0 z-10 flex items-center gap-2 px-4 py-2 bg-stone-100 dark:bg-white/[0.04] border-b border-stone-200 dark:border-white/[0.06]">
|
|
59
|
+
<span className="text-[13px] font-medium text-primary font-mono">
|
|
60
|
+
{table.tableName}
|
|
61
|
+
</span>
|
|
62
|
+
<span className="text-[12px] text-tertiary">{parts.join(", ")}</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Diff table */}
|
|
66
|
+
<div className="overflow-x-auto flex-1">
|
|
67
|
+
<table className="w-full text-[12px] font-mono border-collapse">
|
|
68
|
+
<thead>
|
|
69
|
+
<tr className="border-b border-stone-200 dark:border-white/[0.06]">
|
|
70
|
+
<th className="w-6 px-1 py-1.5 text-center text-tertiary font-normal" />
|
|
71
|
+
{colNames.map((col) => (
|
|
72
|
+
<th
|
|
73
|
+
key={col}
|
|
74
|
+
className="px-3 py-1.5 text-left font-medium text-secondary whitespace-nowrap"
|
|
75
|
+
>
|
|
76
|
+
{col}
|
|
77
|
+
</th>
|
|
78
|
+
))}
|
|
79
|
+
</tr>
|
|
80
|
+
</thead>
|
|
81
|
+
<tbody>
|
|
82
|
+
{/* Deleted rows */}
|
|
83
|
+
{table.deleted.map((row, i) => (
|
|
84
|
+
<DiffRow
|
|
85
|
+
key={`del-${i}`}
|
|
86
|
+
type="deleted"
|
|
87
|
+
row={row}
|
|
88
|
+
colNames={colNames}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
|
|
92
|
+
{/* Modified rows */}
|
|
93
|
+
{table.modified.map((mod, i) => (
|
|
94
|
+
<ModifiedRows
|
|
95
|
+
key={`mod-${i}`}
|
|
96
|
+
before={mod.before}
|
|
97
|
+
after={mod.after}
|
|
98
|
+
changedColumns={mod.changedColumns}
|
|
99
|
+
colNames={colNames}
|
|
100
|
+
/>
|
|
101
|
+
))}
|
|
102
|
+
|
|
103
|
+
{/* Added rows */}
|
|
104
|
+
{table.added.map((row, i) => (
|
|
105
|
+
<DiffRow
|
|
106
|
+
key={`add-${i}`}
|
|
107
|
+
type="added"
|
|
108
|
+
row={row}
|
|
109
|
+
colNames={colNames}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
|
|
113
|
+
{/* Unchanged summary */}
|
|
114
|
+
{table.unchangedCount > 0 && (
|
|
115
|
+
<tr>
|
|
116
|
+
<td
|
|
117
|
+
colSpan={colNames.length + 1}
|
|
118
|
+
className="px-4 py-2 text-center text-tertiary text-[11px] border-t border-stone-200 dark:border-white/[0.06]"
|
|
119
|
+
>
|
|
120
|
+
{table.unchangedCount} row
|
|
121
|
+
{table.unchangedCount !== 1 ? "s" : ""} unchanged
|
|
122
|
+
</td>
|
|
123
|
+
</tr>
|
|
124
|
+
)}
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function DiffRow({
|
|
133
|
+
type,
|
|
134
|
+
row,
|
|
135
|
+
colNames,
|
|
136
|
+
highlightCols,
|
|
137
|
+
}: {
|
|
138
|
+
type: "added" | "deleted";
|
|
139
|
+
row: Record<string, unknown>;
|
|
140
|
+
colNames: string[];
|
|
141
|
+
highlightCols?: Set<string>;
|
|
142
|
+
}) {
|
|
143
|
+
const isAdded = type === "added";
|
|
144
|
+
const bgClass = isAdded
|
|
145
|
+
? "bg-green-50 dark:bg-green-950/30"
|
|
146
|
+
: "bg-red-50 dark:bg-red-950/30";
|
|
147
|
+
const textClass = isAdded
|
|
148
|
+
? "text-green-800 dark:text-green-300"
|
|
149
|
+
: "text-red-800 dark:text-red-300";
|
|
150
|
+
const gutterClass = isAdded
|
|
151
|
+
? "text-green-500 dark:text-green-400"
|
|
152
|
+
: "text-red-500 dark:text-red-400";
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<tr className={bgClass}>
|
|
156
|
+
<td
|
|
157
|
+
className={`px-1 py-1 text-center font-bold select-none ${gutterClass}`}
|
|
158
|
+
>
|
|
159
|
+
{isAdded ? "+" : "\u2212"}
|
|
160
|
+
</td>
|
|
161
|
+
{colNames.map((col) => {
|
|
162
|
+
const emphasize = highlightCols?.has(col);
|
|
163
|
+
const value = row[col];
|
|
164
|
+
return (
|
|
165
|
+
<td
|
|
166
|
+
key={col}
|
|
167
|
+
className={`px-3 py-1 whitespace-nowrap ${textClass} ${
|
|
168
|
+
emphasize
|
|
169
|
+
? isAdded
|
|
170
|
+
? "bg-green-200/50 dark:bg-green-800/40 font-medium"
|
|
171
|
+
: "bg-red-200/50 dark:bg-red-800/40 font-medium"
|
|
172
|
+
: ""
|
|
173
|
+
}`}
|
|
174
|
+
>
|
|
175
|
+
{value === null ? (
|
|
176
|
+
<span className="italic opacity-60">NULL</span>
|
|
177
|
+
) : (
|
|
178
|
+
formatCellValue(value)
|
|
179
|
+
)}
|
|
180
|
+
</td>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</tr>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ModifiedRows({
|
|
188
|
+
before,
|
|
189
|
+
after,
|
|
190
|
+
changedColumns,
|
|
191
|
+
colNames,
|
|
192
|
+
}: {
|
|
193
|
+
before: Record<string, unknown>;
|
|
194
|
+
after: Record<string, unknown>;
|
|
195
|
+
changedColumns: string[];
|
|
196
|
+
colNames: string[];
|
|
197
|
+
}) {
|
|
198
|
+
const changedSet = new Set(changedColumns);
|
|
199
|
+
return (
|
|
200
|
+
<>
|
|
201
|
+
<DiffRow
|
|
202
|
+
type="deleted"
|
|
203
|
+
row={before}
|
|
204
|
+
colNames={colNames}
|
|
205
|
+
highlightCols={changedSet}
|
|
206
|
+
/>
|
|
207
|
+
<DiffRow
|
|
208
|
+
type="added"
|
|
209
|
+
row={after}
|
|
210
|
+
colNames={colNames}
|
|
211
|
+
highlightCols={changedSet}
|
|
212
|
+
/>
|
|
213
|
+
</>
|
|
214
|
+
);
|
|
215
|
+
}
|