mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
feat: initial db export implementation
This commit is contained in:
@ -30,6 +30,7 @@ module.exports = {
|
||||
functionAdd: true,
|
||||
schedulerAdd: true,
|
||||
schemaEdit: true,
|
||||
schemaExport: true,
|
||||
tableSettings: true,
|
||||
viewSettings: true,
|
||||
triggerSettings: true,
|
||||
|
@ -27,6 +27,7 @@ module.exports = {
|
||||
routineAdd: true,
|
||||
functionAdd: true,
|
||||
databaseEdit: false,
|
||||
schemaExport: true,
|
||||
tableSettings: true,
|
||||
viewSettings: true,
|
||||
triggerSettings: true,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { app, ipcMain } from 'electron';
|
||||
import { app, ipcMain, dialog } from 'electron';
|
||||
|
||||
export default () => {
|
||||
ipcMain.on('close-app', () => {
|
||||
@ -9,4 +9,12 @@ export default () => {
|
||||
const key = false;
|
||||
event.returnValue = key;
|
||||
});
|
||||
|
||||
ipcMain.handle('showOpenDialog', (event, options) => {
|
||||
return dialog.showOpenDialog(options);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-download-dir-path', () => {
|
||||
return app.getPath('downloads');
|
||||
});
|
||||
};
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { ipcMain, dialog, Notification } from 'electron';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import MysqlExporter from '../libs/exporters/sql/MysqlExporter';
|
||||
|
||||
export default connections => {
|
||||
let exporter = null;
|
||||
|
||||
ipcMain.handle('create-schema', async (event, params) => {
|
||||
try {
|
||||
await connections[params.uid].createSchema(params);
|
||||
@ -37,9 +42,16 @@ export default connections => {
|
||||
|
||||
ipcMain.handle('get-schema-collation', async (event, params) => {
|
||||
try {
|
||||
const collation = await connections[params.uid].getDatabaseCollation(params);
|
||||
const collation = await connections[params.uid].getDatabaseCollation(
|
||||
params
|
||||
);
|
||||
|
||||
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
|
||||
return {
|
||||
status: 'success',
|
||||
response: collation.rows.length
|
||||
? collation.rows[0].DEFAULT_COLLATION_NAME
|
||||
: ''
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
@ -48,7 +60,9 @@ export default connections => {
|
||||
|
||||
ipcMain.handle('get-structure', async (event, params) => {
|
||||
try {
|
||||
const structure = await connections[params.uid].getStructure(params.schemas);
|
||||
const structure = await connections[params.uid].getStructure(
|
||||
params.schemas
|
||||
);
|
||||
|
||||
return { status: 'success', response: structure };
|
||||
}
|
||||
@ -152,4 +166,98 @@ export default connections => {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('export', async (event, { uid, ...rest }) => {
|
||||
if (exporter !== null) return;
|
||||
|
||||
const type = connections[uid]._client;
|
||||
|
||||
switch (type) {
|
||||
case 'mysql':
|
||||
exporter = new MysqlExporter(connections[uid], rest);
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
status: 'error',
|
||||
response: `${type} exporter not aviable`
|
||||
};
|
||||
}
|
||||
|
||||
const outputFileName = path.basename(rest.outputFile);
|
||||
|
||||
if (fs.existsSync(rest.outputFile)) {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
message: `File ${outputFileName} already exists. Do you want to replace it?`,
|
||||
detail:
|
||||
'A file with the same name already exists in the target folder. Replacing it will overwrite its current contents.',
|
||||
buttons: ['Cancel', 'Replace'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
});
|
||||
|
||||
if (result.response !== 1) {
|
||||
exporter = null;
|
||||
return { status: 'error', response: 'Operation aborted' };
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exporter.once('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
exporter.once('end', () => {
|
||||
resolve({ cancelled: exporter.isCancelled });
|
||||
});
|
||||
|
||||
exporter.on('progress', state => {
|
||||
event.sender.send('export-progress', state);
|
||||
});
|
||||
|
||||
exporter.run();
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.cancelled) {
|
||||
new Notification({
|
||||
title: 'Export finished',
|
||||
body: `Finished exporting to ${outputFileName}`
|
||||
}).show();
|
||||
}
|
||||
return { status: 'success', response };
|
||||
})
|
||||
.catch(err => {
|
||||
new Notification({
|
||||
title: 'Export error',
|
||||
body: err.toString()
|
||||
}).show();
|
||||
|
||||
return { status: 'error', response: err.toString() };
|
||||
})
|
||||
.finally(() => {
|
||||
exporter.removeAllListeners();
|
||||
exporter = null;
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('abort-export', async event => {
|
||||
let willAbort = false;
|
||||
|
||||
if (exporter) {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
message: 'Are you sure you want to abort the export',
|
||||
buttons: ['Cancel', 'Abort'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
});
|
||||
|
||||
if (result.response === 1) {
|
||||
willAbort = true;
|
||||
exporter.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'success', response: { willAbort } };
|
||||
});
|
||||
};
|
||||
|
73
src/main/libs/exporters/BaseExporter.js
Normal file
73
src/main/libs/exporters/BaseExporter.js
Normal file
@ -0,0 +1,73 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
export class BaseExporter extends EventEmitter {
|
||||
constructor (options) {
|
||||
super();
|
||||
this._options = options;
|
||||
this._isCancelled = false;
|
||||
this._outputStream = fs.createWriteStream(this._options.outputFile, {
|
||||
flags: 'w'
|
||||
});
|
||||
this._state = {};
|
||||
|
||||
this._outputStream.once('error', err => {
|
||||
this._isCancelled = true;
|
||||
this.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
async run () {
|
||||
try {
|
||||
this.emit('start', this);
|
||||
await this.dump();
|
||||
}
|
||||
catch (err) {
|
||||
this.emit('error', err);
|
||||
throw err;
|
||||
}
|
||||
finally {
|
||||
this._outputStream.end();
|
||||
this.emit('end');
|
||||
}
|
||||
}
|
||||
|
||||
get isCancelled () {
|
||||
return this._isCancelled;
|
||||
}
|
||||
|
||||
outputFileExists () {
|
||||
return fs.existsSync(this._options.outputFile);
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this._isCancelled = true;
|
||||
this.emit('cancel');
|
||||
this.emitUpdate({ op: 'cancelling' });
|
||||
}
|
||||
|
||||
emitUpdate (state) {
|
||||
this.emit('progress', { ...this._state, ...state });
|
||||
}
|
||||
|
||||
writeString (data) {
|
||||
if (this._isCancelled) return;
|
||||
|
||||
try {
|
||||
fs.accessSync(this._options.outputFile);
|
||||
}
|
||||
catch (err) {
|
||||
this._isCancelled = true;
|
||||
|
||||
const fileName = path.basename(this._options.outputFile);
|
||||
this.emit('error', `The file ${fileName} is not accessible`);
|
||||
}
|
||||
|
||||
this._outputStream.write(data);
|
||||
}
|
||||
|
||||
dump () {
|
||||
throw new Error('Exporter must implement the "dump" method');
|
||||
}
|
||||
}
|
35
src/main/libs/exporters/ExporterFactory.js
Normal file
35
src/main/libs/exporters/ExporterFactory.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { MysqlExporter } from './sql/MysqlExporter';
|
||||
|
||||
export class ExporterFactory {
|
||||
/**
|
||||
* Returns a data exporter class instance.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {String} args.client
|
||||
* @param {Object} args.params
|
||||
* @param {String} args.params.host
|
||||
* @param {Number} args.params.port
|
||||
* @param {String} args.params.password
|
||||
* @param {String=} args.params.database
|
||||
* @param {String=} args.params.schema
|
||||
* @param {String} args.params.ssh.host
|
||||
* @param {String} args.params.ssh.username
|
||||
* @param {String} args.params.ssh.password
|
||||
* @param {Number} args.params.ssh.port
|
||||
* @param {Number=} args.poolSize
|
||||
* @returns Exporter Instance
|
||||
* @memberof ExporterFactory
|
||||
*/
|
||||
static get (args) {
|
||||
switch (type) {
|
||||
case 'mysql':
|
||||
exporter = new MysqlExporter(connections[uid], rest);
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
status: 'error',
|
||||
response: `${type} exporter not aviable`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
108
src/main/libs/exporters/sql/MysqlExporter.js
Normal file
108
src/main/libs/exporters/sql/MysqlExporter.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { SqlExporter } from './SqlExporter';
|
||||
import { BLOB, BIT } from 'common/fieldTypes';
|
||||
import hexToBinary from 'common/libs/hexToBinary';
|
||||
|
||||
export default class MysqlExporter extends SqlExporter {
|
||||
async getSqlHeader () {
|
||||
let dump = await super.getSqlHeader();
|
||||
dump += `
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
SET NAMES utf8mb4;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE='NO_AUTO_VALUE_ON_ZERO', SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`;
|
||||
|
||||
return dump;
|
||||
}
|
||||
|
||||
async getFooter () {
|
||||
return `/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;`;
|
||||
}
|
||||
|
||||
async getCreateTable (tableName) {
|
||||
const { rows } = await this._client.raw(`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``);
|
||||
|
||||
if (rows.length !== 1)
|
||||
return '';
|
||||
|
||||
return rows[0]['Create Table'] + ';';
|
||||
}
|
||||
|
||||
getDropTable (tableName) {
|
||||
return `DROP TABLE IF EXISTS \`${tableName}\`;`;
|
||||
}
|
||||
|
||||
async getTableInsert (tableName) {
|
||||
let rowCount = 0;
|
||||
let sqlStr = '';
|
||||
|
||||
const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\``);
|
||||
if (countResults.rows.length === 1)
|
||||
rowCount = countResults.rows[0].count;
|
||||
|
||||
if (rowCount > 0) {
|
||||
const columns = await this._client.getTableColumns({ table: tableName, schema: this.schemaName });
|
||||
const columnNames = columns.map(col => '`' + col.name + '`');
|
||||
const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(', ')}) VALUES`;
|
||||
|
||||
const tableResult = await this._client.raw(`SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\``);
|
||||
|
||||
sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`;
|
||||
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`;
|
||||
sqlStr += '\n\n';
|
||||
|
||||
sqlStr += insertStmt;
|
||||
sqlStr += '\n';
|
||||
|
||||
for (const row of tableResult.rows) {
|
||||
sqlStr += '\t(';
|
||||
|
||||
for (const i in columns) {
|
||||
const column = columns[i];
|
||||
const val = row[column.name];
|
||||
|
||||
if (val === null)
|
||||
sqlStr += 'NULL';
|
||||
|
||||
else if (BIT.includes(column.type))
|
||||
sqlStr += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
|
||||
|
||||
else if (BLOB.includes(column.type))
|
||||
sqlStr += `X'${val.toString('hex').toUpperCase()}'`;
|
||||
|
||||
else if (val === '')
|
||||
sqlStr += '\'\'';
|
||||
|
||||
else
|
||||
sqlStr += typeof val === 'string' ? this.escapeAndQuote(val) : val;
|
||||
|
||||
if (parseInt(i) !== columns.length - 1)
|
||||
sqlStr += ', ';
|
||||
}
|
||||
|
||||
sqlStr += '),\n';
|
||||
}
|
||||
|
||||
sqlStr += '\n';
|
||||
|
||||
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`;
|
||||
sqlStr += 'UNLOCK TABLES;';
|
||||
}
|
||||
|
||||
return sqlStr;
|
||||
}
|
||||
|
||||
escapeAndQuote (value) {
|
||||
if (!value) return null;
|
||||
return `'${value.replaceAll(/'/g, '\'\'')}'`;
|
||||
}
|
||||
}
|
122
src/main/libs/exporters/sql/SqlExporter.js
Normal file
122
src/main/libs/exporters/sql/SqlExporter.js
Normal file
@ -0,0 +1,122 @@
|
||||
import { app } from 'electron';
|
||||
import moment from 'moment';
|
||||
import { BaseExporter } from '../BaseExporter';
|
||||
|
||||
export class SqlExporter extends BaseExporter {
|
||||
constructor (client, options) {
|
||||
super(options);
|
||||
this._client = client;
|
||||
this._commentChar = '#';
|
||||
}
|
||||
|
||||
get schemaName () {
|
||||
return this._options.schema;
|
||||
}
|
||||
|
||||
get host () {
|
||||
return this._client._params.host;
|
||||
}
|
||||
|
||||
async getServerVersion () {
|
||||
const version = await this._client.getVersion();
|
||||
return `${version.name} ${version.number}`;
|
||||
}
|
||||
|
||||
async dump () {
|
||||
const exportState = {
|
||||
totalItems: this._options.items.length,
|
||||
currentItemIndex: 0,
|
||||
currentItem: '',
|
||||
op: ''
|
||||
};
|
||||
|
||||
const header = await this.getSqlHeader();
|
||||
this.writeString(header);
|
||||
this.writeString('\n\n\n');
|
||||
|
||||
for (const item of this._options.items) {
|
||||
// user abort operation
|
||||
if (this.isCancelled)
|
||||
return;
|
||||
|
||||
// skip item if not set to output any detail for them
|
||||
if (!item.includeStructure && !item.includeContent && !item.includeDropStatement)
|
||||
continue;
|
||||
|
||||
exportState.currentItemIndex++;
|
||||
exportState.currentItem = item.table;
|
||||
exportState.op = 'PROCESSING';
|
||||
|
||||
this.emitUpdate(exportState);
|
||||
|
||||
const tableHeader = this.buildComment(`Dump of table ${item.table}\n------------------------------------------------------------`);
|
||||
this.writeString(tableHeader);
|
||||
this.writeString('\n\n');
|
||||
|
||||
if (item.includeDropStatement) {
|
||||
const dropTableSyntax = this.getDropTable(item.table);
|
||||
this.writeString(dropTableSyntax);
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
|
||||
if (item.includeStructure) {
|
||||
const createTableSyntax = await this.getCreateTable(item.table);
|
||||
this.writeString(createTableSyntax);
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
|
||||
if (item.includeContent) {
|
||||
exportState.op = 'FETCH';
|
||||
this.emitUpdate(exportState);
|
||||
const tableInsertSyntax = await this.getTableInsert(item.table);
|
||||
|
||||
exportState.op = 'WRITE';
|
||||
this.emitUpdate(exportState);
|
||||
this.writeString(tableInsertSyntax);
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
|
||||
const footer = await this.getFooter();
|
||||
this.writeString(footer);
|
||||
}
|
||||
|
||||
buildComment (text) {
|
||||
return text.split('\n').map(txt => `${this._commentChar} ${txt}`).join('\n');
|
||||
}
|
||||
|
||||
async getSqlHeader () {
|
||||
const serverVersion = await this.getServerVersion();
|
||||
const header = `************************************************************
|
||||
Antares - SQL Client
|
||||
Version ${app.getVersion()}
|
||||
|
||||
https://antares-sql.app/
|
||||
https://github.com/Fabio286/antares
|
||||
|
||||
Host: ${this.host} (${serverVersion})
|
||||
Database: ${this.schemaName}
|
||||
Generation time: ${moment().format()}
|
||||
************************************************************`;
|
||||
|
||||
return this.buildComment(header);
|
||||
}
|
||||
|
||||
async getFooter () {
|
||||
return '';
|
||||
}
|
||||
|
||||
getCreateTable (tableName) {
|
||||
throw new Error('Sql Exporter must implement the "getCreateTable" method');
|
||||
}
|
||||
|
||||
getDropTable (tableName) {
|
||||
throw new Error('Sql Exporter must implement the "getDropTable" method');
|
||||
}
|
||||
|
||||
getTableInsert (tableName) {
|
||||
throw new Error('Sql Exporter must implement the "getTableInsert" method');
|
||||
}
|
||||
}
|
19
src/main/workers/ExportService.js
Normal file
19
src/main/workers/ExportService.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { Worker, isMainThread, workerData, parentPort } from 'worker_threads';
|
||||
import
|
||||
|
||||
if (isMainThread) {
|
||||
module.exports = function run (workerData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(__filename, { workerData });
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0)
|
||||
reject(new Error(`Worker stopped with exit code ${code}`));
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
304
src/renderer/components/ModalExportSchema.vue
Normal file
304
src/renderer/components/ModalExportSchema.vue
Normal file
@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div class="modal-container p-0">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
|
||||
<span class="cut-text">{{ $t('message.exportSchema') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="col-3">
|
||||
<label class="form-label">{{ $t('message.directoryPath') }}</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<fieldset class="input-group">
|
||||
<input
|
||||
v-model="basePath"
|
||||
class="form-input"
|
||||
type="text"
|
||||
required
|
||||
readonly
|
||||
:placeholder="$t('message.schemaName')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary input-group-btn"
|
||||
@click.prevent="openPathDialog"
|
||||
>
|
||||
{{ $t('word.change') }}
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns mb-2">
|
||||
<div class="column col-auto d-flex p-0 text-italic ">
|
||||
<i class="mdi mdi-file-document-outline mr-2" />
|
||||
{{ filename }}
|
||||
</div>
|
||||
|
||||
<div class="column col-auto col-ml-auto p-0">
|
||||
<button class="btn btn-dark btn-sm" @click="uncheckAllTables">
|
||||
<i class="mdi mdi-file-tree-outline" />
|
||||
</button>
|
||||
<button class="btn btn-dark btn-sm" @click="checkAllTables">
|
||||
<i class="mdi mdi-file-tree" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workspace-query-results">
|
||||
<div ref="table" class="table table-hover">
|
||||
<div class="thead">
|
||||
<div class="tr">
|
||||
<div class="th c-hand" style="width: 50%;">
|
||||
<div class="table-column-title">
|
||||
<span>Table</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th c-hand">
|
||||
<div class="table-column-title">
|
||||
<span>Structure</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th c-hand">
|
||||
<div class="table-column-title">
|
||||
<span>Content</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th c-hand">
|
||||
<div class="table-column-title">
|
||||
<span>Drop</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbody">
|
||||
<div
|
||||
v-for="item in tables"
|
||||
:key="item.name"
|
||||
class="tr"
|
||||
>
|
||||
<div class="td">
|
||||
{{ item.table }}
|
||||
</div>
|
||||
<div class="td">
|
||||
<label class="form-checkbox m-0 px-2">
|
||||
<input
|
||||
v-model="item.includeStructure"
|
||||
type="checkbox"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="td">
|
||||
<label class="form-checkbox m-0 px-2">
|
||||
<input
|
||||
v-model="item.includeContent"
|
||||
type="checkbox"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="td">
|
||||
<label class="form-checkbox m-0 px-2">
|
||||
<input
|
||||
v-model="item.includeDropStatement"
|
||||
type="checkbox"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="progressPercentage > 0">
|
||||
<progress
|
||||
class="progress"
|
||||
:value="progressPercentage"
|
||||
max="100"
|
||||
/>
|
||||
<p class="empty-subtitle">
|
||||
{{ progressPercentage }}% - {{ progressStatus }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-primary mr-2"
|
||||
:class="{'loading': isExporting}"
|
||||
:disabled="isExporting"
|
||||
@click.stop="startExport"
|
||||
>
|
||||
{{ $t('word.export') }}
|
||||
</button>
|
||||
<button class="btn btn-link" @click.stop="closeModal">
|
||||
{{ $t('word.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import moment from 'moment';
|
||||
import Application from '@/ipc-api/Application';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
|
||||
export default {
|
||||
name: 'ModalExportSchema',
|
||||
|
||||
props: {
|
||||
selectedSchema: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isExporting: false,
|
||||
progressPercentage: 0,
|
||||
progressStatus: '',
|
||||
tables: [],
|
||||
basePath: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace',
|
||||
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||
}),
|
||||
currentWorkspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
},
|
||||
schemaItems () {
|
||||
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
|
||||
if (db)
|
||||
return db.tables.filter(table => table.type === 'table');
|
||||
|
||||
return [];
|
||||
},
|
||||
filename () {
|
||||
const date = moment().format('YYYY-MM-DD');
|
||||
return `${this.selectedSchema}_${date}.sql`;
|
||||
},
|
||||
dumpFilePath () {
|
||||
return `${this.basePath}/${this.filename}`;
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
this.basePath = await Application.getDownloadPathDirectory();
|
||||
this.tables = this.schemaItems.map(item => ({
|
||||
table: item.name,
|
||||
includeStructure: true,
|
||||
includeContent: true,
|
||||
includeDropStatement: true
|
||||
}));
|
||||
|
||||
ipcRenderer.on('export-progress', this.updateProgress);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
ipcRenderer.off('export-progress', this.updateProgress);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
refreshSchema: 'workspaces/refreshSchema'
|
||||
}),
|
||||
async startExport () {
|
||||
this.isExporting = true;
|
||||
const { uid } = this.currentWorkspace;
|
||||
const params = {
|
||||
uid,
|
||||
schema: this.selectedSchema,
|
||||
outputFile: this.dumpFilePath,
|
||||
items: [...this.tables]
|
||||
};
|
||||
|
||||
const result = await Schema.export(params);
|
||||
if (result) {
|
||||
if (result.status === 'success')
|
||||
this.progressStatus = result.response.cancelled ? 'Aborted' : 'Completed!';
|
||||
|
||||
else
|
||||
this.progressStatus = result.response;
|
||||
}
|
||||
|
||||
this.isExporting = false;
|
||||
},
|
||||
updateProgress (event, state) {
|
||||
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
|
||||
this.progressStatus = state.op + ' ' + state.currentItem;
|
||||
},
|
||||
async closeModal () {
|
||||
let willClose = true;
|
||||
if (this.isExporting) {
|
||||
willClose = false;
|
||||
const { response } = await Schema.abortExport();
|
||||
willClose = response.willAbort;
|
||||
}
|
||||
|
||||
if (willClose)
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
},
|
||||
checkAllTables () {
|
||||
this.tables = this.tables.map(item => ({ table: item.table, includeStructure: true, includeContent: true, includeDropStatement: true }));
|
||||
},
|
||||
uncheckAllTables () {
|
||||
this.tables = this.tables.map(item => ({ table: item.table, includeStructure: false, includeContent: false, includeDropStatement: false }));
|
||||
},
|
||||
async openPathDialog () {
|
||||
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
|
||||
if (result && !result.canceled)
|
||||
this.basePath = result.filePaths[0];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.workspace-query-results {
|
||||
flex: 1 1 auto;
|
||||
|
||||
.table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
min-height: 0.8rem;
|
||||
padding: 0;
|
||||
|
||||
.form-icon {
|
||||
top: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
|
||||
.modal-container {
|
||||
max-width: 800px;
|
||||
|
||||
.modal-body {
|
||||
height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -58,6 +58,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="workspace.customizations.schemaExport"
|
||||
class="context-element"
|
||||
@click="showExportSchemaModal"
|
||||
>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-down text-light pr-1" /> {{ $t('word.export') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="workspace.customizations.schemaEdit"
|
||||
class="context-element"
|
||||
@ -91,6 +98,11 @@
|
||||
:selected-schema="selectedSchema"
|
||||
@close="hideEditModal"
|
||||
/>
|
||||
<ModalExportSchema
|
||||
v-if="isExportSchemaModal"
|
||||
:selected-schema="selectedSchema"
|
||||
@close="hideExportSchemaModal"
|
||||
/>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
||||
@ -99,6 +111,7 @@ import { mapGetters, mapActions } from 'vuex';
|
||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import ModalEditSchema from '@/components/ModalEditSchema';
|
||||
import ModalExportSchema from '@/components/ModalExportSchema';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
|
||||
export default {
|
||||
@ -106,7 +119,8 @@ export default {
|
||||
components: {
|
||||
BaseContextMenu,
|
||||
ConfirmModal,
|
||||
ModalEditSchema
|
||||
ModalEditSchema,
|
||||
ModalExportSchema
|
||||
},
|
||||
props: {
|
||||
contextEvent: MouseEvent,
|
||||
@ -115,7 +129,8 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
isDeleteModal: false,
|
||||
isEditModal: false
|
||||
isEditModal: false,
|
||||
isExportSchemaModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -166,6 +181,12 @@ export default {
|
||||
this.isEditModal = false;
|
||||
this.closeContext();
|
||||
},
|
||||
showExportSchemaModal () {
|
||||
this.isExportSchemaModal = true;
|
||||
},
|
||||
hideExportSchemaModal () {
|
||||
this.isExportSchemaModal = false;
|
||||
},
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
|
@ -121,7 +121,8 @@ module.exports = {
|
||||
history: 'History',
|
||||
select: 'Select',
|
||||
passphrase: 'Passphrase',
|
||||
filter: 'Filter'
|
||||
filter: 'Filter',
|
||||
change: 'Change'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Welcome to Antares SQL Client!',
|
||||
@ -246,7 +247,9 @@ module.exports = {
|
||||
thereIsNoQueriesYet: 'There is no queries yet',
|
||||
searchForQueries: 'Search for queries',
|
||||
killProcess: 'Kill process',
|
||||
closeTab: 'Close tab'
|
||||
closeTab: 'Close tab',
|
||||
exportSchema: 'Export schema',
|
||||
directoryPath: 'Directory path'
|
||||
},
|
||||
faker: {
|
||||
address: 'Address',
|
||||
|
@ -121,7 +121,8 @@ module.exports = {
|
||||
history: 'Cronologia',
|
||||
select: 'Seleziona',
|
||||
passphrase: 'Passphrase',
|
||||
filter: 'Filtra'
|
||||
filter: 'Filtra',
|
||||
change: 'Cambia'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Benvenuto in Antares SQL Client!',
|
||||
@ -233,7 +234,9 @@ module.exports = {
|
||||
duplicateTable: 'Duplica tabella',
|
||||
noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:',
|
||||
noSchema: 'Nessuno schema',
|
||||
restorePreviourSession: 'Ripristina sessione precedente'
|
||||
restorePreviourSession: 'Ripristina sessione precedente',
|
||||
exportSchema: 'Esporta schema',
|
||||
directoryPath: 'Percorso directory'
|
||||
},
|
||||
faker: {
|
||||
address: 'Indirizzo',
|
||||
|
@ -5,4 +5,12 @@ export default class {
|
||||
static getKey (params) {
|
||||
return ipcRenderer.sendSync('get-key', params);
|
||||
}
|
||||
|
||||
static showOpenDialog (options) {
|
||||
return ipcRenderer.invoke('showOpenDialog', options);
|
||||
}
|
||||
|
||||
static getDownloadPathDirectory () {
|
||||
return ipcRenderer.invoke('get-download-dir-path');
|
||||
}
|
||||
}
|
||||
|
@ -53,4 +53,12 @@ export default class {
|
||||
static rawQuery (params) {
|
||||
return ipcRenderer.invoke('raw-query', params);
|
||||
}
|
||||
|
||||
static export (params) {
|
||||
return ipcRenderer.invoke('export', params);
|
||||
}
|
||||
|
||||
static abortExport () {
|
||||
return ipcRenderer.invoke('abort-export');
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user