mysqlsyncr 1.0.4 → 1.0.7
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/package.json +7 -2
- package/src/apply.js +16 -11
- package/src/cli.js +3 -2
- package/src/differences.js +45 -22
- package/src/dump.js +37 -1
- package/src/index.js +58 -40
- package/src/parsers.js +2 -1
- package/src/socketServer.js +96 -0
- package/ui/.editorconfig +7 -0
- package/ui/.prettierrc.json +6 -0
- package/ui/README.md +40 -0
- package/ui/dist/spa/assets/KFOkCnqEu92Fr1MmgVxIIzQ-Y2mpUUkw.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmEU9fBBc--ViM7Ag4Q.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmSU5fBBc--CrFzpgl6.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmWUlfBBc--CW3_XjG_.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmYUtfBBc--Dr8fnyGc.woff +0 -0
- package/ui/dist/spa/assets/KFOmCnqEu92Fr1Mu4mxM-DAYh6T4l.woff +0 -0
- package/ui/dist/spa/assets/MainLayout-C27l9N4F.js +1 -0
- package/ui/dist/spa/assets/axios-Y2H2Igoj.js +1 -0
- package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
- package/ui/dist/spa/assets/index-DKK0oGss.js +30 -0
- package/ui/dist/spa/assets/index-DME6fUxJ.css +5 -0
- package/ui/dist/spa/favicon.ico +0 -0
- package/ui/dist/spa/icons/favicon-128x128.png +0 -0
- package/ui/dist/spa/icons/favicon-16x16.png +0 -0
- package/ui/dist/spa/icons/favicon-32x32.png +0 -0
- package/ui/dist/spa/icons/favicon-96x96.png +0 -0
- package/ui/dist/spa/index.html +3 -0
- package/ui/eslint.config.js +73 -0
- package/ui/index.html +21 -0
- package/ui/jsconfig.json +3 -0
- package/ui/package.json +42 -0
- package/ui/postcss.config.js +29 -0
- package/ui/public/favicon.ico +0 -0
- package/ui/public/icons/favicon-128x128.png +0 -0
- package/ui/public/icons/favicon-16x16.png +0 -0
- package/ui/public/icons/favicon-32x32.png +0 -0
- package/ui/public/icons/favicon-96x96.png +0 -0
- package/ui/quasar.config.js +215 -0
- package/ui/src/App.vue +7 -0
- package/ui/src/assets/quasar-logo-vertical.svg +15 -0
- package/ui/src/boot/.gitkeep +0 -0
- package/ui/src/boot/axios.js +24 -0
- package/ui/src/components/EssentialLink.vue +44 -0
- package/ui/src/css/app.scss +1 -0
- package/ui/src/css/quasar.variables.scss +25 -0
- package/ui/src/layouts/MainLayout.vue +13 -0
- package/ui/src/pages/ApplyChanges.vue +46 -0
- package/ui/src/pages/CompareDatabase.vue +57 -0
- package/ui/src/pages/DatabaseManager.vue +42 -0
- package/ui/src/pages/DumpDatabase.vue +41 -0
- package/ui/src/pages/ErrorNotFound.vue +27 -0
- package/ui/src/pages/MainComponent copy.vue +322 -0
- package/ui/src/pages/MainComponent.vue +894 -0
- package/ui/src/router/index.js +30 -0
- package/ui/src/router/routes.js +20 -0
- package/ui/src/stores/example-store.js +21 -0
- package/ui/src/stores/index.js +20 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-page>
|
|
3
|
+
<div class="q-mb-md">
|
|
4
|
+
<q-btn @click="openSettings" label="Settings" color="primary" />
|
|
5
|
+
<q-btn
|
|
6
|
+
@click="
|
|
7
|
+
Object.keys(existingDatabases).length === 0
|
|
8
|
+
? createNewDump()
|
|
9
|
+
: createNewDump(true)
|
|
10
|
+
"
|
|
11
|
+
:label="
|
|
12
|
+
Object.keys(existingDatabases).length === 0 ? 'Get New Dump' : 'Refresh Dump'
|
|
13
|
+
"
|
|
14
|
+
color="secondary"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<q-card v-if="!isDbSetup" class="q-mb-md">
|
|
19
|
+
<q-card-section>
|
|
20
|
+
<div class="text-h6">Database Not Set Up</div>
|
|
21
|
+
<div>Please configure your database settings.</div>
|
|
22
|
+
<q-btn @click="openSettings" label="Open Settings" color="primary" />
|
|
23
|
+
</q-card-section>
|
|
24
|
+
</q-card>
|
|
25
|
+
|
|
26
|
+
<q-card v-if="Object.keys(existingDatabases).length > 0" class="q-mb-md">
|
|
27
|
+
<q-card-section>
|
|
28
|
+
<div class="text-h6">Database Structure</div>
|
|
29
|
+
<q-tree
|
|
30
|
+
:nodes="databaseTree"
|
|
31
|
+
node-key="id"
|
|
32
|
+
v-model:expanded="expandedNodes"
|
|
33
|
+
@lazy-load="onLazyLoad"
|
|
34
|
+
>
|
|
35
|
+
<template v-slot:default-header="prop">
|
|
36
|
+
<div class="row items-center full-width">
|
|
37
|
+
<div
|
|
38
|
+
class="row items-center flex-grow-1"
|
|
39
|
+
@click.stop="
|
|
40
|
+
!['category', 'database', 'subcategory'].includes(
|
|
41
|
+
prop.node.type,
|
|
42
|
+
) &&
|
|
43
|
+
prop.node.diffCount &&
|
|
44
|
+
showIssueDetails(prop.node.issueData, prop.node)
|
|
45
|
+
"
|
|
46
|
+
>
|
|
47
|
+
<q-icon
|
|
48
|
+
v-if="prop.node.icon"
|
|
49
|
+
:name="prop.node.icon"
|
|
50
|
+
:color="prop.node.iconColor"
|
|
51
|
+
size="sm"
|
|
52
|
+
class="q-mr-sm"
|
|
53
|
+
/>
|
|
54
|
+
<div>{{ prop.node.label }}</div>
|
|
55
|
+
<q-badge
|
|
56
|
+
v-if="prop.node.diffCount"
|
|
57
|
+
color="negative"
|
|
58
|
+
class="q-ml-sm"
|
|
59
|
+
>
|
|
60
|
+
{{ prop.node.diffCount }}
|
|
61
|
+
</q-badge>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Fix button with better spacing -->
|
|
65
|
+
<q-btn
|
|
66
|
+
v-if="
|
|
67
|
+
['issue-group', 'issues', 'category', 'database'].includes(
|
|
68
|
+
prop.node.type,
|
|
69
|
+
) && prop.node.diffCount
|
|
70
|
+
"
|
|
71
|
+
flat
|
|
72
|
+
dense
|
|
73
|
+
color="primary"
|
|
74
|
+
:loading="isNodeFixing[prop.node.id]"
|
|
75
|
+
@click.stop="fixNodeIssues(prop.node)"
|
|
76
|
+
icon="build"
|
|
77
|
+
size="sm"
|
|
78
|
+
class="q-ml-md"
|
|
79
|
+
>
|
|
80
|
+
<q-tooltip>Fix All Issues</q-tooltip>
|
|
81
|
+
</q-btn>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
</q-tree>
|
|
85
|
+
</q-card-section>
|
|
86
|
+
</q-card>
|
|
87
|
+
|
|
88
|
+
<q-dialog v-model="settingsDialog" persistent>
|
|
89
|
+
<q-card>
|
|
90
|
+
<q-card-section>
|
|
91
|
+
<div class="text-h6">Database Settings</div>
|
|
92
|
+
<q-input v-model="host" label="Database Host" required />
|
|
93
|
+
<q-input v-model="port" label="Database Port" type="number" required />
|
|
94
|
+
<q-input v-model="user" label="Database User" required />
|
|
95
|
+
<q-input
|
|
96
|
+
v-model="password"
|
|
97
|
+
label="Database Password"
|
|
98
|
+
type="password"
|
|
99
|
+
required
|
|
100
|
+
autocomplete="off"
|
|
101
|
+
/>
|
|
102
|
+
<q-input v-model="database" label="Database Name" required />
|
|
103
|
+
<q-input v-model="dbFolder" label="Database Folder" required />
|
|
104
|
+
</q-card-section>
|
|
105
|
+
<q-card-actions>
|
|
106
|
+
<q-btn @click="saveSettings" label="Save" color="primary" />
|
|
107
|
+
<q-btn @click="closeSettings" label="Cancel" color="secondary" />
|
|
108
|
+
</q-card-actions>
|
|
109
|
+
</q-card>
|
|
110
|
+
</q-dialog>
|
|
111
|
+
|
|
112
|
+
<!-- Issue Details Dialog -->
|
|
113
|
+
<q-dialog v-model="issueDialog">
|
|
114
|
+
<q-card style="min-width: 800px; width: 90vw; max-width: 1200px">
|
|
115
|
+
<q-card-section>
|
|
116
|
+
<div class="text-h6">Issue Details</div>
|
|
117
|
+
</q-card-section>
|
|
118
|
+
|
|
119
|
+
<!-- Single Issue View -->
|
|
120
|
+
<template v-if="selectedIssues.length === 1">
|
|
121
|
+
<q-card-section class="q-pt-none">
|
|
122
|
+
<div class="text-subtitle2 q-mb-sm">
|
|
123
|
+
Issue Type:
|
|
124
|
+
<q-badge color="negative">{{
|
|
125
|
+
selectedIssues[0].type?.replace(/_/g, ' ')
|
|
126
|
+
}}</q-badge>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="text-subtitle2 q-mb-sm">Name: {{ selectedIssues[0].Name }}</div>
|
|
129
|
+
|
|
130
|
+
<div v-if="selectedIssues[0].tableName" class="q-mb-sm">
|
|
131
|
+
<strong>Table:</strong> {{ selectedIssues[0].tableName }}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div v-if="selectedIssues[0].createSQL" class="q-mt-md">
|
|
135
|
+
<div class="text-subtitle2 q-mb-sm">SQL Definition:</div>
|
|
136
|
+
<q-card class="bg-grey-2">
|
|
137
|
+
<q-card-section class="q-pa-md">
|
|
138
|
+
<pre class="sql-code">{{ selectedIssues[0].createSQL }}</pre>
|
|
139
|
+
</q-card-section>
|
|
140
|
+
</q-card>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div v-if="selectedIssues[0].Definition" class="q-mt-md">
|
|
144
|
+
<div class="text-subtitle2 q-mb-sm">Definition:</div>
|
|
145
|
+
<q-card class="bg-grey-2">
|
|
146
|
+
<q-card-section class="q-pa-md">
|
|
147
|
+
<pre class="sql-code">{{ selectedIssues[0].Definition }}</pre>
|
|
148
|
+
</q-card-section>
|
|
149
|
+
</q-card>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div v-if="selectedIssues[0].currentField" class="q-mt-md">
|
|
153
|
+
<div class="text-subtitle2 q-mb-sm">Field Comparison:</div>
|
|
154
|
+
<q-markup-table flat bordered>
|
|
155
|
+
<thead>
|
|
156
|
+
<tr>
|
|
157
|
+
<th class="text-left">Property</th>
|
|
158
|
+
<th class="text-left">Current</th>
|
|
159
|
+
<th class="text-left">Expected</th>
|
|
160
|
+
</tr>
|
|
161
|
+
</thead>
|
|
162
|
+
<tbody>
|
|
163
|
+
<tr v-for="(value, key) in selectedIssues[0].currentField" :key="key">
|
|
164
|
+
<td>{{ key }}</td>
|
|
165
|
+
<td>{{ value }}</td>
|
|
166
|
+
<td :class="{ 'text-negative': value !== selectedIssues[0].field?.[key] }">
|
|
167
|
+
{{ selectedIssues[0].field?.[key] }}
|
|
168
|
+
</td>
|
|
169
|
+
</tr>
|
|
170
|
+
</tbody>
|
|
171
|
+
</q-markup-table>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<q-btn
|
|
175
|
+
@click="fixNodeIssues(this.selectedNode)"
|
|
176
|
+
:loading="isNodeFixing['single-issue']"
|
|
177
|
+
color="primary"
|
|
178
|
+
class="q-mt-md"
|
|
179
|
+
label="Fix Issue"
|
|
180
|
+
/>
|
|
181
|
+
</q-card-section>
|
|
182
|
+
</template>
|
|
183
|
+
|
|
184
|
+
<!-- Multiple Issues View -->
|
|
185
|
+
<template v-else>
|
|
186
|
+
<q-card-section class="q-pt-none">
|
|
187
|
+
<q-table
|
|
188
|
+
:rows="selectedIssues"
|
|
189
|
+
:columns="[
|
|
190
|
+
{
|
|
191
|
+
name: 'type',
|
|
192
|
+
label: 'Type',
|
|
193
|
+
field: (row) => row.type?.replace(/_/g, ' '),
|
|
194
|
+
},
|
|
195
|
+
{ name: 'tableName', label: 'Table', field: 'tableName' },
|
|
196
|
+
{ name: 'actions', label: 'Actions', field: 'actions' },
|
|
197
|
+
]"
|
|
198
|
+
row-key="id"
|
|
199
|
+
dense
|
|
200
|
+
>
|
|
201
|
+
<template v-slot:body="props">
|
|
202
|
+
<q-tr :props="props">
|
|
203
|
+
<q-td key="type">{{ props.row.type?.replace(/_/g, ' ') }}</q-td>
|
|
204
|
+
<q-td key="tableName">{{ props.row.tableName }}</q-td>
|
|
205
|
+
<q-td key="actions">
|
|
206
|
+
<q-btn
|
|
207
|
+
flat
|
|
208
|
+
dense
|
|
209
|
+
color="primary"
|
|
210
|
+
icon="visibility"
|
|
211
|
+
@click="showSingleIssue(props.row)"
|
|
212
|
+
>
|
|
213
|
+
<q-tooltip>View Details</q-tooltip>
|
|
214
|
+
</q-btn>
|
|
215
|
+
<q-btn
|
|
216
|
+
flat
|
|
217
|
+
dense
|
|
218
|
+
color="positive"
|
|
219
|
+
icon="build"
|
|
220
|
+
:loading="isNodeFixing[`issue-${props.rowIndex}`]"
|
|
221
|
+
@click="
|
|
222
|
+
fixNodeIssues({
|
|
223
|
+
type: 'issue',
|
|
224
|
+
id: this.selectedNode.id,
|
|
225
|
+
issueData: props.row,
|
|
226
|
+
})
|
|
227
|
+
"
|
|
228
|
+
>
|
|
229
|
+
<q-tooltip>Fix Issue</q-tooltip>
|
|
230
|
+
</q-btn>
|
|
231
|
+
</q-td>
|
|
232
|
+
</q-tr>
|
|
233
|
+
</template>
|
|
234
|
+
</q-table>
|
|
235
|
+
|
|
236
|
+
<q-btn
|
|
237
|
+
@click="fixNodeIssues({ type: 'issues', children: selectedIssues })"
|
|
238
|
+
:loading="isNodeFixing['all-issues']"
|
|
239
|
+
color="primary"
|
|
240
|
+
class="q-mt-md"
|
|
241
|
+
label="Fix All Issues"
|
|
242
|
+
/>
|
|
243
|
+
</q-card-section>
|
|
244
|
+
</template>
|
|
245
|
+
|
|
246
|
+
<q-card-actions align="right">
|
|
247
|
+
<q-btn flat label="Close" color="primary" v-close-popup />
|
|
248
|
+
</q-card-actions>
|
|
249
|
+
</q-card>
|
|
250
|
+
</q-dialog>
|
|
251
|
+
</q-page>
|
|
252
|
+
</template>
|
|
253
|
+
|
|
254
|
+
<script>
|
|
255
|
+
import axios from 'axios'
|
|
256
|
+
|
|
257
|
+
export default {
|
|
258
|
+
data() {
|
|
259
|
+
return {
|
|
260
|
+
isDbSetup: false,
|
|
261
|
+
differences: {},
|
|
262
|
+
columns: [
|
|
263
|
+
{ name: 'table', label: 'Table', align: 'left', field: 'table' },
|
|
264
|
+
{ name: 'difference', label: 'Difference', align: 'left', field: 'difference' },
|
|
265
|
+
],
|
|
266
|
+
settingsDialog: false,
|
|
267
|
+
host: '127.0.0.1',
|
|
268
|
+
port: 3306,
|
|
269
|
+
user: 'root',
|
|
270
|
+
password: '',
|
|
271
|
+
database: '',
|
|
272
|
+
dbFolder: 'db-dump',
|
|
273
|
+
existingDatabases: [],
|
|
274
|
+
databaseColumns: [
|
|
275
|
+
{ name: 'name', label: 'Database Name', align: 'left', field: 'name' },
|
|
276
|
+
{ name: 'size', label: 'Size', align: 'left', field: 'size' },
|
|
277
|
+
],
|
|
278
|
+
databaseTree: [],
|
|
279
|
+
expandedNodes: [],
|
|
280
|
+
issueDialog: false,
|
|
281
|
+
selectedIssues: [],
|
|
282
|
+
selectedNode: null,
|
|
283
|
+
isNodeFixing: {}, // Tracks loading state for each node
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
methods: {
|
|
287
|
+
openSettings() {
|
|
288
|
+
this.settingsDialog = true
|
|
289
|
+
},
|
|
290
|
+
closeSettings() {
|
|
291
|
+
this.settingsDialog = false
|
|
292
|
+
},
|
|
293
|
+
saveSettings() {
|
|
294
|
+
const dbConfig = {
|
|
295
|
+
host: this.host,
|
|
296
|
+
port: this.port,
|
|
297
|
+
user: this.user,
|
|
298
|
+
password: this.password,
|
|
299
|
+
database: this.database,
|
|
300
|
+
output: this.dbFolder,
|
|
301
|
+
}
|
|
302
|
+
localStorage.setItem('dbConfig', JSON.stringify(dbConfig))
|
|
303
|
+
this.isDbSetup = true
|
|
304
|
+
this.closeSettings()
|
|
305
|
+
this.refreshDifferences()
|
|
306
|
+
},
|
|
307
|
+
async refreshDifferences() {
|
|
308
|
+
if (!this.isDbSetup) return
|
|
309
|
+
try {
|
|
310
|
+
const response = await axios.post('http://localhost:3000/api/compare', {
|
|
311
|
+
config: {
|
|
312
|
+
host: this.host,
|
|
313
|
+
port: this.port,
|
|
314
|
+
user: this.user,
|
|
315
|
+
password: this.password,
|
|
316
|
+
output: this.dbFolder,
|
|
317
|
+
database: this.database,
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
this.differences = response.data
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('Error fetching differences:', error)
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
async getDatabasesFromExistingDumps() {
|
|
326
|
+
try {
|
|
327
|
+
const response = await axios.post(
|
|
328
|
+
`http://localhost:3000/api/getDatabasesFromExistingDumps`,
|
|
329
|
+
{
|
|
330
|
+
config: {
|
|
331
|
+
host: this.host,
|
|
332
|
+
port: this.port,
|
|
333
|
+
user: this.user,
|
|
334
|
+
password: this.password,
|
|
335
|
+
output: this.dbFolder,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
)
|
|
339
|
+
if (response.data && typeof response.data === 'object') {
|
|
340
|
+
this.existingDatabases = response.data
|
|
341
|
+
this.buildDatabaseTree()
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('Error fetching databases from existing dumps:', error)
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
async buildDatabaseTree() {
|
|
348
|
+
if (Object.keys(this.differences).length === 0) {
|
|
349
|
+
await this.fetchAllDifferences()
|
|
350
|
+
}
|
|
351
|
+
this.databaseTree = Object.entries(this.existingDatabases).map(([dbName, db]) => {
|
|
352
|
+
console.log('Building database tree for:', dbName, this.differences)
|
|
353
|
+
const dbDiffs = this.differences[dbName] || []
|
|
354
|
+
const totalIssues = dbDiffs.length
|
|
355
|
+
console.log(`Database ${dbName} differences ${totalIssues}:`, dbDiffs)
|
|
356
|
+
return {
|
|
357
|
+
id: `db-${dbName}`,
|
|
358
|
+
label: dbName,
|
|
359
|
+
type: 'database',
|
|
360
|
+
lazy: true,
|
|
361
|
+
icon: totalIssues > 0 ? 'warning' : 'check_circle',
|
|
362
|
+
iconColor: totalIssues > 0 ? 'negative' : 'positive',
|
|
363
|
+
diffCount: totalIssues || null,
|
|
364
|
+
dbData: db,
|
|
365
|
+
dbName: dbName,
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
console.log('Database tree:', this.databaseTree)
|
|
370
|
+
this.expandedNodes = []
|
|
371
|
+
},
|
|
372
|
+
onLazyLoad({ node, done, fail }) {
|
|
373
|
+
console.log('Lazy loading node:', node)
|
|
374
|
+
try {
|
|
375
|
+
if (node.type === 'database') {
|
|
376
|
+
console.log('Loading database children for:', node.dbName)
|
|
377
|
+
const db = node.dbData
|
|
378
|
+
const dbDiffs = this.differences[node.dbName] || []
|
|
379
|
+
|
|
380
|
+
// Count issues by main category
|
|
381
|
+
const tableIssues = dbDiffs.filter(
|
|
382
|
+
(d) =>
|
|
383
|
+
d.type.includes('table') ||
|
|
384
|
+
d.type.includes('field') ||
|
|
385
|
+
d.type.includes('index') ||
|
|
386
|
+
d.type.includes('trigger'),
|
|
387
|
+
)
|
|
388
|
+
console.log('Table issues:', tableIssues)
|
|
389
|
+
|
|
390
|
+
const procedureIssues = dbDiffs.filter((d) => d.type.includes('procedure'))
|
|
391
|
+
const viewIssues = dbDiffs.filter((d) => d.type.includes('view'))
|
|
392
|
+
|
|
393
|
+
const children = [
|
|
394
|
+
{
|
|
395
|
+
id: `${node.dbName}-tables`,
|
|
396
|
+
label: 'Tables',
|
|
397
|
+
type: 'category',
|
|
398
|
+
lazy: true,
|
|
399
|
+
icon: tableIssues.length > 0 ? 'warning' : 'check_circle',
|
|
400
|
+
iconColor: tableIssues.length > 0 ? 'negative' : 'positive',
|
|
401
|
+
diffCount: tableIssues.length || null,
|
|
402
|
+
},
|
|
403
|
+
db.procedures && {
|
|
404
|
+
id: `${node.dbName}-procedures`,
|
|
405
|
+
label: 'Procedures',
|
|
406
|
+
type: 'category',
|
|
407
|
+
lazy: true,
|
|
408
|
+
icon: procedureIssues.length > 0 ? 'warning' : 'check_circle',
|
|
409
|
+
iconColor: procedureIssues.length > 0 ? 'negative' : 'positive',
|
|
410
|
+
diffCount: procedureIssues.length || null,
|
|
411
|
+
},
|
|
412
|
+
db.views && {
|
|
413
|
+
id: `${node.dbName}-views`,
|
|
414
|
+
label: 'Views',
|
|
415
|
+
type: 'category',
|
|
416
|
+
lazy: true,
|
|
417
|
+
icon: viewIssues.length > 0 ? 'warning' : 'check_circle',
|
|
418
|
+
iconColor: viewIssues.length > 0 ? 'negative' : 'positive',
|
|
419
|
+
diffCount: viewIssues.length || null,
|
|
420
|
+
},
|
|
421
|
+
].filter(Boolean)
|
|
422
|
+
|
|
423
|
+
console.log('Database children:', children)
|
|
424
|
+
done(children)
|
|
425
|
+
} else if (node.type === 'category') {
|
|
426
|
+
console.log('Loading category children for:', node.id)
|
|
427
|
+
const dbName = node.id.split('-')[0]
|
|
428
|
+
const dbDiffs = this.differences[dbName] || []
|
|
429
|
+
|
|
430
|
+
if (node.id.includes('-tables')) {
|
|
431
|
+
const tableIssues = dbDiffs.filter(
|
|
432
|
+
(d) =>
|
|
433
|
+
d.type.includes('table') ||
|
|
434
|
+
d.type.includes('field') ||
|
|
435
|
+
d.type.includes('index') ||
|
|
436
|
+
d.type.includes('trigger'),
|
|
437
|
+
)
|
|
438
|
+
console.log('Table category issues:', tableIssues)
|
|
439
|
+
|
|
440
|
+
const children = []
|
|
441
|
+
|
|
442
|
+
// // Add issues section if there are any
|
|
443
|
+
// if (tableIssues.length > 0) {
|
|
444
|
+
// children.push({
|
|
445
|
+
// id: `${dbName}-tables-issues`,
|
|
446
|
+
// label: 'Issues',
|
|
447
|
+
// type: 'issues',
|
|
448
|
+
// icon: 'warning',
|
|
449
|
+
// iconColor: 'negative',
|
|
450
|
+
// diffCount: tableIssues.length,
|
|
451
|
+
// children: this.createTableIssueGroups(dbName, tableIssues),
|
|
452
|
+
// })
|
|
453
|
+
// }
|
|
454
|
+
|
|
455
|
+
// Add actual tables
|
|
456
|
+
const tables = this.existingDatabases[dbName].tables
|
|
457
|
+
console.log('Tables:', tables)
|
|
458
|
+
|
|
459
|
+
children.push(
|
|
460
|
+
...Object.entries(tables).map(([tableName, table]) => {
|
|
461
|
+
const specificTableIssues = tableIssues.filter(
|
|
462
|
+
(d) => d.tableName === tableName,
|
|
463
|
+
)
|
|
464
|
+
console.log(`Issues for table ${tableName}:`, specificTableIssues)
|
|
465
|
+
const hasIssues = specificTableIssues.length > 0
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
id: `${dbName}-table-${tableName}`,
|
|
469
|
+
label: tableName,
|
|
470
|
+
type: 'table',
|
|
471
|
+
lazy: true,
|
|
472
|
+
icon: hasIssues ? 'warning' : 'check_circle',
|
|
473
|
+
iconColor: hasIssues ? 'negative' : 'positive',
|
|
474
|
+
diffCount: hasIssues ? specificTableIssues.length : null,
|
|
475
|
+
tableData: table,
|
|
476
|
+
issueData: specificTableIssues,
|
|
477
|
+
}
|
|
478
|
+
}),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
console.log('Category children:', children)
|
|
482
|
+
done(children)
|
|
483
|
+
} else if (node.id.includes('-procedures')) {
|
|
484
|
+
const children = []
|
|
485
|
+
const procedures = this.existingDatabases[dbName].procedures
|
|
486
|
+
children.push(
|
|
487
|
+
...Object.entries(procedures).map(([index, procedure]) => {
|
|
488
|
+
const procedureName = procedure.Name
|
|
489
|
+
const specificProcedureIssues = dbDiffs.filter(
|
|
490
|
+
(d) => d.type.includes('procedure') && d.Name === procedureName,
|
|
491
|
+
)
|
|
492
|
+
console.log(
|
|
493
|
+
`Issues for procedure ${procedureName}:`,
|
|
494
|
+
specificProcedureIssues,
|
|
495
|
+
)
|
|
496
|
+
const hasIssues = specificProcedureIssues.length > 0
|
|
497
|
+
return {
|
|
498
|
+
id: `${dbName}-procedure-${procedureName}`,
|
|
499
|
+
label: procedureName,
|
|
500
|
+
index: index,
|
|
501
|
+
type: 'procedure',
|
|
502
|
+
lazy: false,
|
|
503
|
+
icon: hasIssues ? 'warning' : 'check_circle',
|
|
504
|
+
iconColor: hasIssues ? 'negative' : 'positive',
|
|
505
|
+
diffCount: hasIssues ? specificProcedureIssues.length : null,
|
|
506
|
+
procedureData: procedure,
|
|
507
|
+
issueData: specificProcedureIssues,
|
|
508
|
+
}
|
|
509
|
+
}),
|
|
510
|
+
)
|
|
511
|
+
done(children)
|
|
512
|
+
} else if (node.id.includes('-views')) {
|
|
513
|
+
const children = []
|
|
514
|
+
const views = this.existingDatabases[dbName].views
|
|
515
|
+
console.log('Views:', views)
|
|
516
|
+
children.push(
|
|
517
|
+
...Object.entries(views).map(([name, view]) => ({
|
|
518
|
+
id: `${dbName}-view-${name}`,
|
|
519
|
+
label: name,
|
|
520
|
+
type: 'view',
|
|
521
|
+
lazy: false,
|
|
522
|
+
view: view,
|
|
523
|
+
})),
|
|
524
|
+
)
|
|
525
|
+
done(children)
|
|
526
|
+
}
|
|
527
|
+
} else if (node.type === 'table') {
|
|
528
|
+
console.log('Loading table children for:', node.id)
|
|
529
|
+
const table = node.tableData
|
|
530
|
+
console.log('Table data:', table)
|
|
531
|
+
|
|
532
|
+
if (!table) {
|
|
533
|
+
console.error('No table data found for node:', node)
|
|
534
|
+
done([])
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const children = []
|
|
539
|
+
|
|
540
|
+
if (table.columns?.length > 0) {
|
|
541
|
+
children.push({
|
|
542
|
+
id: `${node.id}-columns`,
|
|
543
|
+
label: `Columns (${table.columns.length})`,
|
|
544
|
+
type: 'subcategory',
|
|
545
|
+
lazy: false,
|
|
546
|
+
children: table.columns.map(column => {
|
|
547
|
+
let details = [];
|
|
548
|
+
if (column.AutoIncrement) details.push('AUTO_INCREMENT');
|
|
549
|
+
if (column.NotNull) details.push('NOT NULL');
|
|
550
|
+
if (column.Default !== undefined) details.push(`DEFAULT ${column.Default}`);
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
id: `${node.id}-column-${column.Field}`,
|
|
554
|
+
label: `${column.Field} (${column.Type})${details.length ? ' ' + details.join(', ') : ''}`,
|
|
555
|
+
type: 'column',
|
|
556
|
+
expandable: false,
|
|
557
|
+
columnData: column
|
|
558
|
+
};
|
|
559
|
+
})
|
|
560
|
+
})
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (table.indexes?.length > 0) {
|
|
564
|
+
children.push({
|
|
565
|
+
id: `${node.id}-indexes`,
|
|
566
|
+
label: `Indexes (${table.indexes.length})`,
|
|
567
|
+
type: 'subcategory',
|
|
568
|
+
lazy: false,
|
|
569
|
+
children: table.indexes.map(index => ({
|
|
570
|
+
id: `${node.id}-index-${index.Name}`,
|
|
571
|
+
label: `${index.Name}${index.Primary ? ' (PRIMARY)' : ''} [${index.ColumnName.join(', ')}]`,
|
|
572
|
+
type: 'index',
|
|
573
|
+
expandable: false,
|
|
574
|
+
indexData: index
|
|
575
|
+
}))
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
console.log('Table children:', children)
|
|
580
|
+
done(children)
|
|
581
|
+
}
|
|
582
|
+
} catch (error) {
|
|
583
|
+
console.error('Error in lazy loading:', error)
|
|
584
|
+
fail()
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
createTableIssueGroups(dbName, issues) {
|
|
588
|
+
const groupedIssues = issues.reduce((acc, issue) => {
|
|
589
|
+
const type = issue.type
|
|
590
|
+
acc[type] = acc[type] || { count: 0, items: [] }
|
|
591
|
+
acc[type].count++
|
|
592
|
+
acc[type].items.push(issue)
|
|
593
|
+
return acc
|
|
594
|
+
}, {})
|
|
595
|
+
|
|
596
|
+
return Object.entries(groupedIssues).map(([type, group]) => ({
|
|
597
|
+
id: `${dbName}-tables-${type}`,
|
|
598
|
+
label: type
|
|
599
|
+
.split('_')
|
|
600
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
601
|
+
.join(' '),
|
|
602
|
+
type: 'issue-group',
|
|
603
|
+
icon: 'warning',
|
|
604
|
+
iconColor: 'negative',
|
|
605
|
+
diffCount: group.count,
|
|
606
|
+
children: group.items.map((issue) => ({
|
|
607
|
+
id: `${dbName}-tables-${type}-${issue.tableName || issue.Name}`,
|
|
608
|
+
label: issue.tableName || issue.Name,
|
|
609
|
+
type: 'issue',
|
|
610
|
+
icon: 'warning',
|
|
611
|
+
iconColor: 'negative',
|
|
612
|
+
expandable: false,
|
|
613
|
+
issueData: issue,
|
|
614
|
+
})),
|
|
615
|
+
}))
|
|
616
|
+
},
|
|
617
|
+
async fetchAllDifferences() {
|
|
618
|
+
const response = await axios.post('http://localhost:3000/api/compare', {
|
|
619
|
+
config: {
|
|
620
|
+
host: this.host,
|
|
621
|
+
port: this.port,
|
|
622
|
+
user: this.user,
|
|
623
|
+
password: this.password,
|
|
624
|
+
output: this.dbFolder,
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
this.differences = response.data
|
|
628
|
+
console.log('Differences in fetchAllDifferences:', this.differences)
|
|
629
|
+
},
|
|
630
|
+
async fetchDifferencesForSelectedDb() {
|
|
631
|
+
if (Object.keys(this.existingDatabases).length > 0) {
|
|
632
|
+
const selectedDbName = Object.keys(this.existingDatabases)[0]
|
|
633
|
+
const response = await axios.post('http://localhost:3000/api/compare', {
|
|
634
|
+
config: {
|
|
635
|
+
host: this.host,
|
|
636
|
+
port: this.port,
|
|
637
|
+
user: this.user,
|
|
638
|
+
password: this.password,
|
|
639
|
+
output: this.dbFolder,
|
|
640
|
+
database: selectedDbName,
|
|
641
|
+
},
|
|
642
|
+
})
|
|
643
|
+
this.differences = response.data
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
async createNewDump(overwrite = false) {
|
|
647
|
+
console.log('Creating a new dump...')
|
|
648
|
+
if(overwrite){
|
|
649
|
+
this.$q.dialog({
|
|
650
|
+
title: 'Overwrite',
|
|
651
|
+
message: 'Are you sure you want to overwrite the existing dump?',
|
|
652
|
+
ok: 'Overwrite',
|
|
653
|
+
cancel: 'Cancel',
|
|
654
|
+
}).onOk(async () => {
|
|
655
|
+
console.log('Overwriting existing dump')
|
|
656
|
+
await this.createNewDump(false)
|
|
657
|
+
})
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
let response = await axios
|
|
662
|
+
.post('http://localhost:3000/api/dump', {
|
|
663
|
+
config: {
|
|
664
|
+
host: this.host,
|
|
665
|
+
port: this.port,
|
|
666
|
+
user: this.user,
|
|
667
|
+
password: this.password,
|
|
668
|
+
output: this.dbFolder,
|
|
669
|
+
// database: this.database,
|
|
670
|
+
},
|
|
671
|
+
})
|
|
672
|
+
.catch((error) => {
|
|
673
|
+
if (error.response.data.message) {
|
|
674
|
+
this.$q.dialog({
|
|
675
|
+
title: 'Error',
|
|
676
|
+
message: error.response.data.message,
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
console.error('Error creating new dump:', error)
|
|
680
|
+
return
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
if (!response) return
|
|
684
|
+
if (response.data.message) {
|
|
685
|
+
this.$q.dialog({
|
|
686
|
+
title: 'Error',
|
|
687
|
+
message: response.data.message,
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
this.differences = await this.fetchAllDifferences()
|
|
691
|
+
console.log('Differences in createNewDump:', this.differences)
|
|
692
|
+
if (
|
|
693
|
+
response.data &&
|
|
694
|
+
typeof response.data === 'object' &&
|
|
695
|
+
Object.keys(response.data).length > 0
|
|
696
|
+
) {
|
|
697
|
+
this.existingDatabases = response.data
|
|
698
|
+
this.buildDatabaseTree()
|
|
699
|
+
} else {
|
|
700
|
+
this.existingDatabases = {}
|
|
701
|
+
this.$q.notify({
|
|
702
|
+
message: 'Error creating new dump',
|
|
703
|
+
color: 'red',
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
showIssueDetails(issue, node) {
|
|
708
|
+
console.log('Showing issue details:', issue, "from node:", node)
|
|
709
|
+
this.selectedIssues = Array.isArray(issue) ? issue : [issue]
|
|
710
|
+
this.selectedNode = node;
|
|
711
|
+
this.issueDialog = true
|
|
712
|
+
},
|
|
713
|
+
showSingleIssue(issue, dbName) {
|
|
714
|
+
this.selectedIssues = [issue]
|
|
715
|
+
this.selectedIssueDbName = dbName;
|
|
716
|
+
this.issueDialog = true
|
|
717
|
+
},
|
|
718
|
+
async fixNodeIssues(node) {
|
|
719
|
+
if (!node) return
|
|
720
|
+
try {
|
|
721
|
+
let issues = []
|
|
722
|
+
let dbName = null;
|
|
723
|
+
// Set loading state for this node
|
|
724
|
+
this.isNodeFixing[node.id] = true
|
|
725
|
+
this.isNodeFixing = { ...this.isNodeFixing } // Trigger reactivity
|
|
726
|
+
|
|
727
|
+
if(node.issueData){
|
|
728
|
+
issues = Array.isArray(node.issueData) ? node.issueData : [node.issueData];
|
|
729
|
+
}else{
|
|
730
|
+
issues = await this.collectIssuesForNode(node)
|
|
731
|
+
}
|
|
732
|
+
dbName = this.getDbNameFromNode(node)
|
|
733
|
+
|
|
734
|
+
if (!issues.length){
|
|
735
|
+
console.error('No issues found to fix', node);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
const response = await axios.post('http://localhost:3000/api/apply', {
|
|
741
|
+
config: {
|
|
742
|
+
host: this.host,
|
|
743
|
+
port: this.port,
|
|
744
|
+
user: this.user,
|
|
745
|
+
password: this.password,
|
|
746
|
+
output: this.dbFolder,
|
|
747
|
+
},
|
|
748
|
+
database: dbName,
|
|
749
|
+
diffs: issues,
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
if (response.data.message) {
|
|
753
|
+
this.$q.notify({
|
|
754
|
+
type: 'positive',
|
|
755
|
+
message: `Fixed ${issues.length} issue(s) successfully`,
|
|
756
|
+
position: 'top',
|
|
757
|
+
})
|
|
758
|
+
// Refresh the differences to update the UI
|
|
759
|
+
|
|
760
|
+
await this.fetchAllDifferences()
|
|
761
|
+
await this.buildDatabaseTree()
|
|
762
|
+
}
|
|
763
|
+
} catch (error) {
|
|
764
|
+
console.error('Error fixing issues:', error)
|
|
765
|
+
this.$q.notify({
|
|
766
|
+
type: 'negative',
|
|
767
|
+
message: error.response?.data?.message || 'Error fixing issues',
|
|
768
|
+
position: 'top',
|
|
769
|
+
})
|
|
770
|
+
} finally {
|
|
771
|
+
this.isNodeFixing[node.id] = false
|
|
772
|
+
this.isNodeFixing = { ...this.isNodeFixing } // Trigger reactivity
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
getDbNameFromNode(node) {
|
|
776
|
+
// Extract database name from node ID (assumes format: "dbName-something-etc")
|
|
777
|
+
if(node.id.startsWith('db-')){
|
|
778
|
+
return node.id.split('-')[1]
|
|
779
|
+
}
|
|
780
|
+
if (!node.id) return null, console.error('Node ID is required to get db name', node);
|
|
781
|
+
console.log('Node ID:', node.id)
|
|
782
|
+
return node.id.split('-')[0]
|
|
783
|
+
},
|
|
784
|
+
async collectIssuesForNode(node) {
|
|
785
|
+
const issues = []
|
|
786
|
+
console.log('Collecting issues for node:', node)
|
|
787
|
+
switch (node.type) {
|
|
788
|
+
case 'database':
|
|
789
|
+
//collect issues from the differences for the selected db
|
|
790
|
+
console.log('Collecting issues for database:', this.getDbNameFromNode(node))
|
|
791
|
+
issues.push(...this.differences[this.getDbNameFromNode(node)])
|
|
792
|
+
break
|
|
793
|
+
case 'issue':
|
|
794
|
+
if (node.issueData) issues.push(node.issueData)
|
|
795
|
+
break
|
|
796
|
+
|
|
797
|
+
case 'issue-group':
|
|
798
|
+
// Collect all issues in this group
|
|
799
|
+
if (node.children) {
|
|
800
|
+
issues.push(
|
|
801
|
+
...node.children
|
|
802
|
+
.filter((child) => child.type === 'issue')
|
|
803
|
+
.map((child) => child.issueData)
|
|
804
|
+
.filter(Boolean),
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
break
|
|
808
|
+
|
|
809
|
+
case 'issues':
|
|
810
|
+
case 'category':
|
|
811
|
+
// For categories or issues sections, collect all nested issues
|
|
812
|
+
if (node.children) {
|
|
813
|
+
for (const child of node.children) {
|
|
814
|
+
if (child.type === 'issue-group' && child.children) {
|
|
815
|
+
issues.push(
|
|
816
|
+
...child.children
|
|
817
|
+
.filter((grandChild) => grandChild.type === 'issue')
|
|
818
|
+
.map((grandChild) => grandChild.issueData)
|
|
819
|
+
.filter(Boolean),
|
|
820
|
+
)
|
|
821
|
+
}else if(child.issueData){
|
|
822
|
+
if(Array.isArray(child.issueData)){
|
|
823
|
+
issues.push(...child.issueData)
|
|
824
|
+
}else if (child.issueData.type){
|
|
825
|
+
issues.push(child.issueData)
|
|
826
|
+
}else{
|
|
827
|
+
console.error('Unknown issue data:', child.issueData)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}else{
|
|
832
|
+
//need to collect issues from the differences for the selected db and type
|
|
833
|
+
const selectedDbName = this.getDbNameFromNode(node)
|
|
834
|
+
console.log('Selected DB Name:', selectedDbName)
|
|
835
|
+
let type = node.id.split('-')[1]
|
|
836
|
+
const dbDifferences = this.differences[selectedDbName]
|
|
837
|
+
console.log('DB Differences:', dbDifferences)
|
|
838
|
+
if(type === 'tables'){
|
|
839
|
+
issues.push(...dbDifferences.filter(diff => diff.type.includes('table')))
|
|
840
|
+
}else if(type === 'views'){
|
|
841
|
+
issues.push(...dbDifferences.filter(diff => diff.type.includes('view')))
|
|
842
|
+
}else if(type === 'procedures'){
|
|
843
|
+
issues.push(...dbDifferences.filter(diff => diff.type.includes('procedure')))
|
|
844
|
+
}else{
|
|
845
|
+
console.error('Unknown type:', type)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
}
|
|
849
|
+
break
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return issues
|
|
853
|
+
},
|
|
854
|
+
async applyFix() {
|
|
855
|
+
if (!this.selectedIssues.length) return
|
|
856
|
+
|
|
857
|
+
try {
|
|
858
|
+
this.isApplying = true
|
|
859
|
+
await this.fixNodeIssues(this.selectedNode)
|
|
860
|
+
this.issueDialog = false
|
|
861
|
+
} finally {
|
|
862
|
+
this.isApplying = false
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
mounted() {
|
|
867
|
+
const dbConfig = localStorage.getItem('dbConfig')
|
|
868
|
+
if (dbConfig) {
|
|
869
|
+
const config = JSON.parse(dbConfig)
|
|
870
|
+
this.host = config.host
|
|
871
|
+
this.port = config.port
|
|
872
|
+
this.user = config.user
|
|
873
|
+
this.password = config.password
|
|
874
|
+
this.database = config.database
|
|
875
|
+
this.dbFolder = config.output
|
|
876
|
+
this.isDbSetup = true
|
|
877
|
+
this.getDatabasesFromExistingDumps()
|
|
878
|
+
this.fetchAllDifferences()
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
}
|
|
882
|
+
</script>
|
|
883
|
+
|
|
884
|
+
<style>
|
|
885
|
+
.sql-code {
|
|
886
|
+
white-space: pre-wrap;
|
|
887
|
+
word-wrap: break-word;
|
|
888
|
+
font-family: monospace;
|
|
889
|
+
margin: 0;
|
|
890
|
+
font-size: 14px;
|
|
891
|
+
line-height: 1.5;
|
|
892
|
+
overflow-x: auto;
|
|
893
|
+
}
|
|
894
|
+
</style>
|