feat: mysql export for trigger, views, schedulers, functions and routines

This commit is contained in:
Giulio Ganci 2021-10-31 17:22:59 +01:00
parent 0de2321920
commit b2a5b40c03
7 changed files with 545 additions and 159 deletions

View File

@ -167,14 +167,12 @@ export default connections => {
}
});
ipcMain.handle('export', async (event, { uid, ...rest }) => {
ipcMain.handle('export', async (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) return;
const type = connections[uid]._client;
switch (type) {
case 'mysql':
exporter = new MysqlExporter(connections[uid], rest);
exporter = new MysqlExporter(connections[uid], tables, rest);
break;
default:
return {

View File

@ -3,8 +3,9 @@ import path from 'path';
import EventEmitter from 'events';
export class BaseExporter extends EventEmitter {
constructor (options) {
constructor (tables, options) {
super();
this._tables = tables;
this._options = options;
this._isCancelled = false;
this._outputStream = fs.createWriteStream(this._options.outputFile, {

View File

@ -13,28 +13,36 @@ export default class MysqlExporter extends SqlExporter {
/*!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' */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`;
return dump;
}
async getFooter () {
const footer = await super.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 */;`;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
${footer}
`;
}
async getCreateTable (tableName) {
const { rows } = await this._client.raw(`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``);
const { rows } = await this._client.raw(
`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``
);
if (rows.length !== 1)
return '';
if (rows.length !== 1) return '';
return rows[0]['Create Table'] + ';';
const col = 'Create View' in rows[0] ? 'Create View' : 'Create Table';
return rows[0][col] + ';';
}
getDropTable (tableName) {
@ -45,54 +53,82 @@ SET NAMES utf8mb4;
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;
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 });
let queryLength = 0;
let rowsWritten = 0;
const { sqlInsertDivider, sqlInsertAfter } = this._options;
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 insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(
', '
)}) VALUES`;
const tableResult = await this._client.raw(`SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\``);
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 rowIndex in tableResult.rows) {
const row = tableResult.rows[rowIndex];
let sqlInsertString = '';
if (
(sqlInsertDivider === 'bytes' &&
queryLength >= sqlInsertAfter * 1024) ||
(sqlInsertDivider === 'rows' && rowsWritten === sqlInsertAfter)
) {
sqlInsertString += `;\n${insertStmt}\n\t(`;
queryLength = 0;
rowsWritten = 0;
}
else if (parseInt(rowIndex) === 0) sqlInsertString += '\n\t(';
else sqlInsertString += ',\n\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'))}'`;
if (val === null) sqlInsertString += 'NULL';
else if (BIT.includes(column.type)) {
sqlInsertString += `b'${hexToBinary(
Buffer.from(val).toString('hex')
)}'`;
}
else if (BLOB.includes(column.type))
sqlStr += `X'${val.toString('hex').toUpperCase()}'`;
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString +=
typeof val === 'string' ? this.escapeAndQuote(val) : val;
}
else if (val === '')
sqlStr += '\'\'';
else
sqlStr += typeof val === 'string' ? this.escapeAndQuote(val) : val;
if (parseInt(i) !== columns.length - 1)
sqlStr += ', ';
if (parseInt(i) !== columns.length - 1) sqlInsertString += ', ';
}
sqlStr += '),\n';
sqlInsertString += ')';
sqlStr += sqlInsertString;
queryLength += sqlInsertString.length;
rowsWritten++;
}
sqlStr += '\n';
sqlStr += ';\n\n';
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`;
sqlStr += 'UNLOCK TABLES;';
@ -101,6 +137,173 @@ SET NAMES utf8mb4;
return sqlStr;
}
async getViews () {
const { rows: views } = await this._client.raw(
`SHOW TABLE STATUS FROM \`${this.schemaName}\` WHERE Comment = 'VIEW'`
);
let sqlString = '';
for (const view of views) {
sqlString += `DROP VIEW IF EXISTS \`${view.Name}\`;\n`;
const viewSyntax = await this.getCreateTable(view.Name);
sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', '');
sqlString += '\n';
}
return sqlString;
}
async getTriggers () {
const { rows: triggers } = await this._client.raw(
`SHOW TRIGGERS FROM \`${this.schemaName}\``
);
const generatedTables = this._tables
.filter(t => t.includeStructure)
.map(t => t.table);
let sqlString = '';
for (const trigger of triggers) {
const {
Trigger: name,
Timing: timing,
Event: event,
Table: table,
Statement: statement,
sql_mode: sqlMode
} = trigger;
if (!generatedTables.includes(table)) continue;
const definer = this.getEscapedDefiner(trigger.Definer);
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}" */;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += '/*!50003 CREATE*/ ';
sqlString += `/*!50017 DEFINER=${definer}*/ `;
sqlString += `/*!50003 TRIGGER \`${name}\` ${timing} ${event} ON ${table} FOR EACH ROW ${statement}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE */;\n\n';
}
return sqlString;
}
async getSchedulers () {
const { rows: schedulers } = await this._client.raw(
`SELECT *, EVENT_SCHEMA AS \`Db\`, EVENT_NAME AS \`Name\` FROM information_schema.\`EVENTS\` WHERE EVENT_SCHEMA = '${this.schemaName}'`
);
let sqlString = '';
for (const scheduler of schedulers) {
const {
EVENT_NAME: name,
SQL_MODE: sqlMode,
EVENT_TYPE: type,
INTERVAL_VALUE: intervalValue,
INTERVAL_FIELD: intervalField,
STARTS: starts,
ENDS: ends,
EXECUTE_AT: at,
ON_COMPLETION: onCompletion,
STATUS: status,
EVENT_DEFINITION: definition
} = scheduler;
const definer = this.getEscapedDefiner(scheduler.DEFINER);
const comment = this.escapeAndQuote(scheduler.EVENT_COMMENT);
sqlString += `/*!50106 DROP EVENT IF EXISTS \`${name}\` */;\n`;
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE='${sqlMode}' */;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += '/*!50106 CREATE*/ ';
sqlString += `/*!50117 DEFINER=${definer}*/ `;
sqlString += `/*!50106 EVENT \`${name}\` ON SCHEDULE `;
if (type === 'RECURRING') {
sqlString += `EVERY ${intervalValue} ${intervalField} STARTS '${starts}' `;
if (ends) sqlString += `ENDS '${ends}' `;
}
else sqlString += `AT '${at}' `;
sqlString += `ON COMPLETION ${onCompletion} ${
status === 'disabled' ? 'DISABLE' : 'ENABLE'
} COMMENT ${comment || '\'\''} DO ${definition}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n';
}
return sqlString;
}
async getFunctions () {
const { rows: functions } = await this._client.raw(
`SHOW FUNCTION STATUS WHERE \`Db\` = '${this.schemaName}';`
);
let sqlString = '';
for (const func of functions) {
sqlString += await this.getRoutineSyntax(
func.Name,
func.Type,
func.Definer
);
}
return sqlString;
}
async getRoutines () {
const { rows: routines } = await this._client.raw(
`SHOW PROCEDURE STATUS WHERE \`Db\` = '${this.schemaName}';`
);
let sqlString = '';
for (const routine of routines) {
sqlString += await this.getRoutineSyntax(
routine.Name,
routine.Type,
routine.Definer
);
}
return sqlString;
}
async getRoutineSyntax (name, type, definer) {
const { rows: routines } = await this._client.raw(
`SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\``
);
if (routines.length === 0) return '';
const routine = routines[0];
const { sql_mode: sqlMode, 'Create Function': createProcedure } = routine;
const startOffset = createProcedure.indexOf(type);
const procedureBody = createProcedure.substring(startOffset);
let sqlString = 'DELIMITER ;;\n';
sqlString = `/*!50003 DROP ${type} IF EXISTS ${name}*/;;\n`;
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}"*/;;\n`;
sqlString += `/*!50003 CREATE*/ /*!50020 DEFINER=${definer}*/ /*!50003 ${procedureBody}*/;;\n`;
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n';
sqlString += 'DELIMITER ;\n';
return sqlString;
}
getEscapedDefiner (definer) {
return definer
.split('@')
.map(part => '`' + part + '`')
.join('@');
}
escapeAndQuote (value) {
if (!value) return null;
return `'${value.replaceAll(/'/g, '\'\'')}'`;

View File

@ -3,8 +3,8 @@ import moment from 'moment';
import { BaseExporter } from '../BaseExporter';
export class SqlExporter extends BaseExporter {
constructor (client, options) {
super(options);
constructor (client, tables, options) {
super(tables, options);
this._client = client;
this._commentChar = '#';
}
@ -23,8 +23,15 @@ export class SqlExporter extends BaseExporter {
}
async dump () {
const { includes } = this._options;
const extraItems = Object.keys(includes).filter(key => includes[key]);
const totalTableToProcess = this._tables.filter(
t => t.includeStructure || t.includeContent || t.includeDropStatement
).length;
const processingItemCount = totalTableToProcess + extraItems.length;
const exportState = {
totalItems: this._options.items.length,
totalItems: processingItemCount,
currentItemIndex: 0,
currentItem: '',
op: ''
@ -34,13 +41,16 @@ export class SqlExporter extends BaseExporter {
this.writeString(header);
this.writeString('\n\n\n');
for (const item of this._options.items) {
for (const item of this._tables) {
// user abort operation
if (this.isCancelled)
return;
if (this.isCancelled) return;
// skip item if not set to output any detail for them
if (!item.includeStructure && !item.includeContent && !item.includeDropStatement)
if (
!item.includeStructure &&
!item.includeContent &&
!item.includeDropStatement
)
continue;
exportState.currentItemIndex++;
@ -49,7 +59,9 @@ export class SqlExporter extends BaseExporter {
this.emitUpdate(exportState);
const tableHeader = this.buildComment(`Dump of table ${item.table}\n------------------------------------------------------------`);
const tableHeader = this.buildComment(
`Dump of table ${item.table}\n------------------------------------------------------------`
);
this.writeString(tableHeader);
this.writeString('\n\n');
@ -79,12 +91,38 @@ export class SqlExporter extends BaseExporter {
this.writeString('\n\n');
}
for (const item of extraItems) {
const processingMethod = `get${item.charAt(0).toUpperCase() +
item.slice(1)}`;
exportState.currentItemIndex++;
exportState.currentItem = item;
exportState.op = 'PROCESSING';
this.emitUpdate(exportState);
if (this[processingMethod]) {
const data = await this[processingMethod]();
if (data !== '') {
const header =
this.buildComment(
`Dump of ${item}\n------------------------------------------------------------`
) + '\n\n';
this.writeString(header);
this.writeString(data);
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');
return text
.split('\n')
.map(txt => `${this._commentChar} ${txt}`)
.join('\n');
}
async getSqlHeader () {
@ -105,11 +143,13 @@ Generation time: ${moment().format()}
}
async getFooter () {
return '';
return this.buildComment(`Dump completed on ${moment().format()}`);
}
getCreateTable (tableName) {
throw new Error('Sql Exporter must implement the "getCreateTable" method');
throw new Error(
'Sql Exporter must implement the "getCreateTable" method'
);
}
getDropTable (tableName) {
@ -117,6 +157,8 @@ Generation time: ${moment().format()}
}
getTableInsert (tableName) {
throw new Error('Sql Exporter must implement the "getTableInsert" method');
throw new Error(
'Sql Exporter must implement the "getTableInsert" method'
);
}
}

View File

@ -37,111 +37,168 @@
</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 class="columns export-options">
<div class="column col-8 left">
<div class="columns mb-2">
<div class="column col-auto d-flex text-italic ">
<i class="mdi mdi-file-document-outline mr-2" />
{{ filename }}
</div>
<div class="column col-auto col-ml-auto ">
<button class="btn btn-dark btn-sm" @click="refresh">
<i class="mdi mdi-database-refresh" />
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isRefreshing"
@click="uncheckAllTables"
>
<i class="mdi mdi-file-tree-outline" />
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isRefreshing"
@click="checkAllTables"
>
<i class="mdi mdi-file-tree" />
</button>
</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>{{ $t('word.table') }}</span>
</div>
</div>
<div class="th c-hand text-center">
<div class="table-column-title">
<span>{{ $t('word.structure') }}</span>
</div>
</div>
<div class="th c-hand text-center">
<div class="table-column-title">
<span>{{ $t('word.content') }}</span>
</div>
</div>
<div class="th c-hand text-center">
<div class="table-column-title">
<span>{{ $t('word.drop') }}</span>
</div>
</div>
</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 class="tbody">
<div
v-for="item in tables"
:key="item.name"
class="tr"
>
<div class="td">
{{ item.table }}
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeStructure"
type="checkbox"
><i class="form-icon" />
</label>
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeContent"
type="checkbox"
><i class="form-icon" />
</label>
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeDropStatement"
type="checkbox"
><i class="form-icon" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="column col-4">
<h4>
{{ $t('word.options') }}
</h4>
<span>{{ $t('word.includes') }}:</span>
<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>
<label
v-for="(_, key) in options.includes"
:key="key"
class="form-checkbox"
>
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $t(`word.${key}`) }}
</label>
<div class="mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}:
</div>
<div class="columns">
<div class="column col-6">
<input
v-model.number="options.sqlInsertAfter"
type="number"
class="form-input"
value="250"
>
</div>
<div class="column col-6">
<select v-model="options.sqlInsertDivider" class="form-select">
<option value="bytes">
KiB
</option>
<option value="rows">
{{ $t('word.rows') }}
</option>
</select>
</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 class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div v-if="progressPercentage > 0" class="export-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }}
</span>
<progress
class="progress d-block"
:value="progressPercentage"
max="100"
/>
</div>
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
<button
class="btn btn-primary mr-2"
:class="{'loading': isExporting}"
:disabled="isExporting || isRefreshing"
autofocus
@click.prevent="startExport"
>
{{ $t('word.export') }}
</button>
</div>
</div>
</div>
</div>
@ -151,6 +208,7 @@
import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
import customizations from 'common/customizations';
import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema';
@ -163,9 +221,15 @@ export default {
data () {
return {
isExporting: false,
isRefreshing: false,
progressPercentage: 0,
progressStatus: '',
tables: [],
options: {
includes: {},
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
},
basePath: ''
};
},
@ -194,7 +258,7 @@ export default {
}
},
async created () {
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
await this.refresh();
window.addEventListener('keydown', this.onKey);
this.basePath = await Application.getDownloadPathDirectory();
this.tables = this.schemaItems.map(item => ({
@ -204,6 +268,14 @@ export default {
includeDropStatement: true
}));
const structure = ['views', 'triggers', 'routines', 'functions', 'schedulers', 'triggerFunctions'];
structure.forEach(feat => {
const val = customizations[this.currentWorkspace.client][feat];
if (val)
this.$set(this.options.includes, feat, true);
});
ipcRenderer.on('export-progress', this.updateProgress);
},
beforeDestroy () {
@ -216,18 +288,20 @@ export default {
}),
async startExport () {
this.isExporting = true;
const { uid } = this.currentWorkspace;
const { uid, client } = this.currentWorkspace;
const params = {
uid,
type: client,
schema: this.selectedSchema,
outputFile: this.dumpFilePath,
items: [...this.tables]
tables: [...this.tables],
...this.options
};
const result = await Schema.export(params);
if (result) {
if (result.status === 'success')
this.progressStatus = result.response.cancelled ? 'Aborted' : 'Completed!';
this.progressStatus = result.response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else
this.progressStatus = result.response;
@ -237,7 +311,17 @@ export default {
},
updateProgress (event, state) {
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
this.progressStatus = state.op + ' ' + state.currentItem;
switch (state.op) {
case 'PROCESSING':
this.progressStatus = this.$t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
this.progressStatus = this.$t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
this.progressStatus = this.$t('message.writingTableExport', { table: state.currentItem });
break;
}
},
async closeModal () {
let willClose = true;
@ -261,6 +345,11 @@ export default {
uncheckAllTables () {
this.tables = this.tables.map(item => ({ table: item.table, includeStructure: false, includeContent: false, includeDropStatement: false }));
},
async refresh () {
this.isRefreshing = true;
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
this.isRefreshing = false;
},
async openPathDialog () {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
@ -271,9 +360,19 @@ export default {
</script>
<style lang="scss" scoped>
.workspace-query-results {
flex: 1 1 auto;
.export-options {
flex: 1;
overflow: hidden;
.left {
display: flex;
flex-direction: column;
flex: 1;
}
}
.workspace-query-results {
flex: 1 0 1px;
.table {
width: 100% !important;
}
@ -292,13 +391,22 @@ export default {
.modal-container {
max-width: 800px;
}
.modal-body {
height: 60vh;
max-height: 60vh;
display: flex;
flex-direction: column;
}
}
.modal-footer {
display: flex;
}
}
.progress-status {
font-style: italic;
font-size: 80%;
}
</style>

View File

@ -122,7 +122,17 @@ module.exports = {
select: 'Select',
passphrase: 'Passphrase',
filter: 'Filter',
change: 'Change'
change: 'Change',
views: 'Views',
triggers: 'Triggers',
routines: 'Routines',
functions: 'Functions',
schedulers: 'Schedulers',
includes: 'Includes',
drop: 'Drop',
rows: 'Rows',
completed: 'Completed',
aborted: 'Aborted'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@ -145,10 +155,12 @@ module.exports = {
downloadingUpdate: 'Downloading update',
updateDownloaded: 'Update downloaded',
restartToInstall: 'Restart Antares to install',
unableEditFieldWithoutPrimary: 'Unable to edit a field without a primary key in resultset',
unableEditFieldWithoutPrimary:
'Unable to edit a field without a primary key in resultset',
editCell: 'Edit cell',
deleteRows: 'Delete row | Delete {count} rows',
confirmToDeleteRows: 'Do you confirm to delete one row? | Do you confirm to delete {count} rows?',
confirmToDeleteRows:
'Do you confirm to delete one row? | Do you confirm to delete {count} rows?',
notificationsTimeout: 'Notifications timeout',
uploadFile: 'Upload file',
addNewRow: 'Add new row',
@ -176,7 +188,8 @@ module.exports = {
deleteTable: 'Delete table',
emptyCorfirm: 'Do you confirm to empty',
unsavedChanges: 'Unsaved changes',
discardUnsavedChanges: 'You have some unsaved changes. Closing this tab these changes will be discarded.',
discardUnsavedChanges:
'You have some unsaved changes. Closing this tab these changes will be discarded.',
thereAreNoIndexes: 'There are no indexes',
thereAreNoForeign: 'There are no foreign keys',
createNewForeign: 'Create new foreign key',
@ -249,7 +262,11 @@ module.exports = {
killProcess: 'Kill process',
closeTab: 'Close tab',
exportSchema: 'Export schema',
directoryPath: 'Directory path'
directoryPath: 'Directory path',
newInserStmtEvery: 'New INSERT statement every',
processingTableExport: 'Processing {table}',
fechingTableExport: 'Fetching {table} data',
writingTableExport: 'Writing {table} data'
},
faker: {
address: 'Address',

View File

@ -122,7 +122,17 @@ module.exports = {
select: 'Seleziona',
passphrase: 'Passphrase',
filter: 'Filtra',
change: 'Cambia'
change: 'Cambia',
views: 'Viste',
triggers: 'Trigger',
routines: 'Routine',
functions: 'Function',
schedulers: 'Scheduler',
includes: 'Includi',
drop: 'Drop',
rows: 'Righe',
completed: 'Completato',
aborted: 'Annullato'
},
message: {
appWelcome: 'Benvenuto in Antares SQL Client!',
@ -145,10 +155,12 @@ module.exports = {
downloadingUpdate: 'Download dell\'aggiornamento',
updateDownloaded: 'Aggiornamento scaricato',
restartToInstall: 'Riavvia Antares per installare l\'aggiornamento',
unableEditFieldWithoutPrimary: 'Impossibile modificare il campo senza una primary key nel resultset',
unableEditFieldWithoutPrimary:
'Impossibile modificare il campo senza una primary key nel resultset',
editCell: 'Modifica cella',
deleteRows: 'Elimina riga | Elimina {count} righe',
confirmToDeleteRows: 'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?',
confirmToDeleteRows:
'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?',
notificationsTimeout: 'Timeout Notifiche',
uploadFile: 'Carica file',
addNewRow: 'Aggiungi nuova riga',
@ -176,7 +188,8 @@ module.exports = {
deleteTable: 'Cancella tabella',
emptyCorfirm: 'Confermi di voler svuotare',
unsavedChanges: 'Modifiche non salvate',
discardUnsavedChanges: 'Hai modifiche non salvate. Lasciando questa scheda le modifiche saranno scartate.',
discardUnsavedChanges:
'Hai modifiche non salvate. Lasciando questa scheda le modifiche saranno scartate.',
thereAreNoIndexes: 'Non ci sono indici',
thereAreNoForeign: 'Non ci sono chiavi esterne',
createNewForeign: 'Crea nuova chiave esterna',
@ -236,7 +249,11 @@ module.exports = {
noSchema: 'Nessuno schema',
restorePreviourSession: 'Ripristina sessione precedente',
exportSchema: 'Esporta schema',
directoryPath: 'Percorso directory'
directoryPath: 'Percorso directory',
newInserStmtEvery: 'Nuova istruzione INSERT ogni',
processingTableExport: 'Processo {table}',
fechingTableExport: 'Ricavo i dati {table}',
writingTableExport: 'Scrittura dati {table}'
},
faker: {
address: 'Indirizzo',