1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

Compare commits

..

8 Commits

23 changed files with 464 additions and 214 deletions

7
.versionrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"types": [
{"type":"feat","section":"Features"},
{"type":"perf","section":"Improvements"},
{"type":"fix","section":"Bug Fixes"}
]
}

View File

@@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.0.15](https://github.com/Fabio286/antares/compare/v0.0.14...v0.0.15) (2021-01-23)
### Features
* functions and schedulers in query suggestions ([8ff6e70](https://github.com/Fabio286/antares/commit/8ff6e70145ed2a207ae8b23a2c688258382a5d74))
* loading animation in properties tabs ([1cf6485](https://github.com/Fabio286/antares/commit/1cf64858964f4894913db42f7c268013bb06e40b))
### Bug Fixes
* error retriving dato of some schedulers ([b9ed8dd](https://github.com/Fabio286/antares/commit/b9ed8dd610e3be1489e01cf53f7d632cb1bd6ac5))
* unable to call stored routines from query tabs ([4923128](https://github.com/Fabio286/antares/commit/4923128236131482ca948ae8052c294bd9269ed0))
### Improvements
* better fields type detection ([4bc9bbf](https://github.com/Fabio286/antares/commit/4bc9bbfb34ebdc51061f718cdf9cbca8507fa0f4))
* big performance improvement in database structure loading ([a11bac5](https://github.com/Fabio286/antares/commit/a11bac504cd4ee865ea6c614a15ee809dc38202e))
### [0.0.14](https://github.com/Fabio286/antares/compare/v0.0.13...v0.0.14) (2021-01-16)

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.14",
"version": "0.0.15",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -67,8 +67,7 @@
"vue-i18n": "^8.22.2",
"vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0",
"vuex-persist": "^3.1.3"
"vuex": "^3.6.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",

View File

@@ -91,7 +91,7 @@ module.exports = [
},
{
name: 'TINYTEXT',
length: true,
length: false,
collation: true,
unsigned: false,
zerofill: false
@@ -119,7 +119,7 @@ module.exports = [
},
{
name: 'JSON',
length: true,
length: false,
collation: true,
unsigned: false,
zerofill: false
@@ -279,7 +279,7 @@ module.exports = [
types: [
{
name: 'UNKNOWN',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false

View File

@@ -7,6 +7,6 @@ export const DATE = ['DATE'];
export const TIME = ['TIME'];
export const DATETIME = ['DATETIME', 'TIMESTAMP'];
export const BLOB = ['BLOB', 'MEDIUMBLOB', 'LONGBLOB'];
export const BLOB = ['BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB'];
export const BIT = ['BIT'];

View File

@@ -46,7 +46,7 @@ export default connections => {
try {
await connection.connect();
const structure = await connection.getStructure();
const structure = await connection.getStructure(new Set());
connections[conn.uid] = connection;

View File

@@ -50,9 +50,9 @@ export default connections => {
}
});
ipcMain.handle('get-structure', async (event, uid) => {
ipcMain.handle('get-structure', async (event, params) => {
try {
const structure = await connections[uid].getStructure();
const structure = await connections[params.uid].getStructure(params.schemas);
return { status: 'success', response: structure };
}

View File

@@ -1,8 +1,97 @@
'use strict';
import mysql from 'mysql';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql';
export class MySQLClient extends AntaresCore {
constructor (args) {
super(args);
this.types = {
0: 'DECIMAL',
1: 'TINYINT',
2: 'SMALLINT',
3: 'INT',
4: 'FLOAT',
5: 'DOUBLE',
6: 'NULL',
7: 'TIMESTAMP',
8: 'BIGINT',
9: 'MEDIUMINT',
10: 'DATE',
11: 'TIME',
12: 'DATETIME',
13: 'YEAR',
14: 'NEWDATE',
15: 'VARCHAR',
16: 'BIT',
17: 'TIMESTAMP2',
18: 'DATETIME2',
19: 'TIME2',
245: 'JSON',
246: 'NEWDECIMAL',
247: 'ENUM',
248: 'SET',
249: 'TINY_BLOB',
250: 'MEDIUM_BLOB',
251: 'LONG_BLOB',
252: 'BLOB',
253: 'VARCHAR',
254: 'CHAR',
255: 'GEOMETRY'
};
}
_getType (field) {
let name = this.types[field.type];
let length = field.length;
if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name))
length = field.decimals;
if (name === 'CHAR' && field.charsetNr === 63)// if binary
name = 'BINARY';
if (name === 'VARCHAR' && field.charsetNr === 63)// if binary
name = 'VARBINARY';
if (name === 'BLOB') {
switch (length) {
case 765:
name = 'TYNITEXT';
break;
case 196605:
name = 'TEXT';
break;
case 50331645:
name = 'MEDIUMTEXT';
break;
case 4294967295:
name = field.charsetNr === 63 ? 'LONGBLOB' : 'LONGTEXT';
break;
case 255:
name = 'TINYBLOB';
break;
case 65535:
name = 'BLOB';
break;
case 16777215:
name = 'MEDIUMBLOB';
break;
default:
name = field.charsetNr === 63 ? 'BLOB' : 'TEXT';
}
}
return { name, length };
}
_getTypeInfo (type) {
return dataTypes
.reduce((acc, group) => [...acc, ...group.types], [])
.filter(_type => _type.name === type.toUpperCase())[0];
}
/**
* @memberof MySQLClient
*/
@@ -31,10 +120,11 @@ export class MySQLClient extends AntaresCore {
}
/**
* @param {Array} schemas list
* @returns {Array.<Object>} databases scructure
* @memberof MySQLClient
*/
async getStructure () {
async getStructure (schemas) {
const { rows: databases } = await this.raw('SHOW DATABASES');
const { rows: functions } = await this.raw('SHOW FUNCTION STATUS');
const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS');
@@ -44,6 +134,8 @@ export class MySQLClient extends AntaresCore {
const triggersArr = [];
for (const db of databases) {
if (!schemas.has(db.Database)) continue;
let { rows: tables } = await this.raw(`SHOW TABLE STATUS FROM \`${db.Database}\``);
if (tables.length) {
tables = tables.map(table => {
@@ -64,109 +156,121 @@ export class MySQLClient extends AntaresCore {
}
return databases.map(db => {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => {
let tableType;
switch (table.Comment) {
case 'VIEW':
tableType = 'view';
break;
default:
tableType = 'table';
break;
}
if (schemas.has(db.Database)) {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => {
let tableType;
switch (table.Comment) {
case 'VIEW':
tableType = 'view';
break;
default:
tableType = 'table';
break;
}
return {
name: table.Name,
type: tableType,
rows: table.Rows,
created: table.Create_time,
updated: table.Update_time,
engine: table.Engine,
comment: table.Comment,
size: table.Data_length + table.Index_length,
autoIncrement: table.Auto_increment,
collation: table.Collation
};
});
// PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
return {
name: procedure.Name,
type: procedure.Type,
definer: procedure.Definer,
created: procedure.Created,
updated: procedure.Modified,
comment: procedure.Comment,
charset: procedure.character_set_client,
security: procedure.Security_type
};
});
// FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => {
return {
name: func.Name,
type: func.Type,
definer: func.Definer,
created: func.Created,
updated: func.Modified,
comment: func.Comment,
charset: func.character_set_client,
security: func.Security_type
};
});
// SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return {
name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION,
type: scheduler.EVENT_TYPE,
definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS,
ends: scheduler.ENDS,
status: scheduler.STATUS,
executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE,
onCompletion: scheduler.ON_COMPLETION,
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
return {
name: trigger.Trigger,
statement: trigger.Statement,
timing: trigger.Timing,
definer: trigger.Definer,
event: trigger.Event,
table: trigger.Table,
sqlMode: trigger.sql_mode,
created: trigger.Created,
charset: trigger.character_set_client
};
});
return {
name: table.Name,
type: tableType,
rows: table.Rows,
created: table.Create_time,
updated: table.Update_time,
engine: table.Engine,
comment: table.Comment,
size: table.Data_length + table.Index_length,
autoIncrement: table.Auto_increment,
collation: table.Collation
name: db.Database,
tables: remappedTables,
functions: remappedFunctions,
procedures: remappedProcedures,
triggers: remappedTriggers,
schedulers: remappedSchedulers
};
});
// PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
}
else {
return {
name: procedure.Name,
type: procedure.Type,
definer: procedure.Definer,
created: procedure.Created,
updated: procedure.Modified,
comment: procedure.Comment,
charset: procedure.character_set_client,
security: procedure.Security_type
name: db.Database,
tables: [],
functions: [],
procedures: [],
triggers: [],
schedulers: []
};
});
// FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => {
return {
name: func.Name,
type: func.Type,
definer: func.Definer,
created: func.Created,
updated: func.Modified,
comment: func.Comment,
charset: func.character_set_client,
security: func.Security_type
};
});
// SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return {
name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION,
type: scheduler.EVENT_TYPE,
definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS,
ends: scheduler.ENDS,
status: scheduler.STATUS,
executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE,
onCompletion: scheduler.ON_COMPLETION,
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
return {
name: trigger.Trigger,
statement: trigger.Statement,
timing: trigger.Timing,
definer: trigger.Definer,
event: trigger.Event,
table: trigger.Table,
sqlMode: trigger.sql_mode,
created: trigger.Created,
charset: trigger.character_set_client
};
});
return {
name: db.Database,
tables: remappedTables,
functions: remappedFunctions,
procedures: remappedProcedures,
triggers: remappedTriggers,
schedulers: remappedSchedulers
};
}
});
}
@@ -279,13 +383,13 @@ export class MySQLClient extends AntaresCore {
}
/**
* SELECT `user`, `host`, IF(LENGTH(password)>0, password, authentication_string) AS `password` FROM `mysql`.`user`
* SELECT `user`, `host`, authentication_string) AS `password` FROM `mysql`.`user`
*
* @returns {Array.<Object>} users list
* @memberof MySQLClient
*/
async getUsers () {
const { rows } = await this.raw('SELECT `user`, `host`, IF(LENGTH(password)>0, password, authentication_string) AS `password` FROM `mysql`.`user`');
const { rows } = await this.raw('SELECT `user`, `host`, authentication_string AS `password` FROM `mysql`.`user`');
return rows.map(row => {
return {
@@ -674,7 +778,7 @@ export class MySQLClient extends AntaresCore {
const results = await this.raw(sql);
return results.rows.map(row => {
const schedule = row['Create Event'].match(/(?<=ON SCHEDULE\n*?\s*?).*?(?=\n)/gs)[0];
const schedule = row['Create Event'];
const execution = schedule.includes('EVERY') ? 'EVERY' : 'ONCE';
const every = execution === 'EVERY' ? row['Create Event'].match(/(?<=EVERY )(\s*([^\s]+)){0,2}/gs)[0].replaceAll('\'', '').split(' ') : [];
const starts = execution === 'EVERY' && schedule.includes('STARTS') ? schedule.match(/(?<=STARTS ').*?(?='\s)/gs)[0] : '';
@@ -683,7 +787,7 @@ export class MySQLClient extends AntaresCore {
return {
definer: row['Create Event'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Event'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
sql: row['Create Event'].match(/(?<=DO )(.*)/gs)[0],
name: row.Event,
comment: row['Create Event'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Event'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
state: row['Create Event'].includes('ENABLE') ? 'ENABLE' : row['Create Event'].includes('DISABLE ON SLAVE') ? 'DISABLE ON SLAVE' : 'DISABLE',
@@ -863,7 +967,8 @@ export class MySQLClient extends AntaresCore {
// ADD FIELDS
additions.forEach(addition => {
const length = addition.numLength || addition.charLength || addition.datePrecision;
const typeInfo = this._getTypeInfo(addition.type);
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN \`${addition.name}\`
${addition.type.toUpperCase()}${length ? `(${length})` : ''}
@@ -900,7 +1005,8 @@ export class MySQLClient extends AntaresCore {
// CHANGE FIELDS
changes.forEach(change => {
const length = change.numLength || change.charLength || change.datePrecision;
const typeInfo = this._getTypeInfo(change.type);
const length = typeInfo.length ? change.numLength || change.charLength || change.datePrecision : false;
alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\`
${change.type.toUpperCase()}${length ? `(${length})` : ''}
@@ -1069,14 +1175,12 @@ export class MySQLClient extends AntaresCore {
const nestTables = args.nest ? '.' : false;
const resultsArr = [];
let paramsArr = [];
let selectedFields = [];
const queries = args.split ? sql.split(';') : [sql];
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
for (const query of queries) {
if (!query) continue;
let fieldsArr = [];
let keysArr = [];
const { rows, report, fields, keys } = await new Promise((resolve, reject) => {
@@ -1086,32 +1190,34 @@ export class MySQLClient extends AntaresCore {
if (err)
reject(err);
else {
const remappedFields = fields
let remappedFields = fields
? fields.map(field => {
if (!field || Array.isArray(field))
return false;
const type = this._getType(field);
return {
name: field.name,
name: field.orgName,
alias: field.name,
orgName: field.orgName,
schema: field.db,
table: field.table,
tableAlias: field.table,
zerofill: field.zerofill,
orgTable: field.orgTable,
type: 'VARCHAR'
type: type.name,
length: type.length
};
})
}).filter(Boolean)
: [];
if (args.details) {
let cachedTable;
if (remappedFields.length) {
selectedFields = remappedFields.map(field => {
return {
name: field.orgName || field.name,
table: field.orgTable || field.table
};
});
paramsArr = remappedFields.map(field => {
if (field.table) cachedTable = field.table;// Needed for some queries on information_schema
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema
return {
table: field.orgTable || cachedTable,
schema: field.schema || 'INFORMATION_SCHEMA'
@@ -1119,43 +1225,16 @@ export class MySQLClient extends AntaresCore {
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
for (const paramObj of paramsArr) {
try { // Table data
if (!paramObj.table || !paramObj.schema) continue;
try { // Column details
const response = await this.getTableColumns(paramObj);
let detailedFields = response.length
? selectedFields.map(selField => {
return response.find(field => field.name.toLowerCase() === selField.name.toLowerCase() && field.table === selField.table);
}).filter(el => !!el)
: [];
if (selectedFields.length) {
detailedFields = detailedFields.map(field => {
const aliasObj = remappedFields.find(resField => resField.orgName === field.name && resField.orgTable === field.table);
return {
...field,
alias: aliasObj.name || field.name,
tableAlias: aliasObj.table || field.table
};
});
}
if (!detailedFields.length) {
detailedFields = remappedFields.map(field => {
const isInFields = fieldsArr.some(f => field.name.toLowerCase() === f.name.toLowerCase() && field.table === f.table);
if (!isInFields) {
return {
...field,
alias: field.name,
tableAlias: field.table
};
}
else
return false;
}).filter(Boolean);
}
fieldsArr = fieldsArr ? [...fieldsArr, ...detailedFields] : detailedFields;
remappedFields = remappedFields.map(field => {
const detailedField = response.find(f => f.name === field.name);
if (field.orgTable === paramObj.table && field.schema === paramObj.schema && detailedField.name === field.orgName)
field = { ...detailedField, ...field };
return field;
});
}
catch (err) {
reject(err);
@@ -1173,9 +1252,9 @@ export class MySQLClient extends AntaresCore {
}
resolve({
rows: Array.isArray(queryResult) ? queryResult : false,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: fieldsArr.length ? fieldsArr : remappedFields,
fields: remappedFields,
keys: keysArr
});
}

View File

@@ -0,0 +1,22 @@
<template>
<div class="empty">
<div class="loading loading-lg" />
</div>
</template>
<script>
export default {
name: 'BaseLoader'
};
</script>
<style scoped>
.empty {
position: absolute;
display: flex;
height: 100%;
flex-direction: column;
left: 0;
justify-content: center;
right: 0;
}
</style>

View File

@@ -253,7 +253,7 @@ export default {
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0;
return field.length;
},
inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type))

View File

@@ -81,6 +81,34 @@ export default {
})
: [];
},
functions () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.functions);
return acc;
}, []).map(func => {
return {
name: `${func.name}()`,
type: 'function'
};
})
: [];
},
schedulers () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.schedulers);
return acc;
}, []).map(scheduler => {
return {
name: scheduler.name,
type: 'scheduler'
};
})
: [];
},
mode () {
switch (this.workspace.client) {
case 'mysql':
@@ -160,7 +188,9 @@ export default {
[
...this.tables,
...this.triggers,
...this.procedures
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,

View File

@@ -3,10 +3,11 @@
<summary
class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}"
@click="changeBreadcrumbs({schema: database.name, table: null})"
@click="selectSchema(database.name)"
@contextmenu.prevent="showDatabaseContext($event, database.name)"
>
<i class="icon mdi mdi-18px mdi-chevron-right" />
<div v-if="isLoading" class="icon loading" />
<i v-else class="icon mdi mdi-18px mdi-chevron-right" />
<i class="database-icon mdi mdi-18px mdi-database mr-1" />
<span>{{ database.name }}</span>
</summary>
@@ -161,13 +162,22 @@ export default {
database: Object,
connection: Object
},
data () {
return {
isLoading: false
};
},
computed: {
...mapGetters({
getLoadedSchemas: 'workspaces/getLoadedSchemas',
getWorkspace: 'workspaces/getWorkspace'
}),
breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs;
},
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
},
maxSize () {
return this.database.tables.reduce((acc, curr) => {
if (curr.size > acc) acc = curr.size;
@@ -180,9 +190,19 @@ export default {
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
refreshSchema: 'workspaces/refreshSchema'
}),
formatBytes,
async selectSchema (schema) {
if (!this.loadedSchemas.has(schema)) {
this.isLoading = true;
await this.refreshSchema({ uid: this.connection.uid, schema });
this.isLoading = false;
}
this.changeBreadcrumbs({ schema, table: null });
},
showDatabaseContext (event, database) {
this.changeBreadcrumbs({ schema: database, table: null });
this.$emit('show-database-context', { event, database });
@@ -230,6 +250,16 @@ export default {
.misc-icon {
opacity: 0.7;
}
.loading {
height: 18px;
width: 18px;
&::after {
height: 0.6rem;
width: 0.6rem;
}
}
}
.misc-name {

View File

@@ -51,7 +51,8 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12">
<div class="workspace-query-results column col-12 p-relative">
<BaseLoader v-if="isLoading" />
<WorkspacePropsTable
v-if="localFields"
ref="indexTable"
@@ -106,6 +107,7 @@
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
@@ -114,6 +116,7 @@ import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal'
export default {
name: 'WorkspacePropsTab',
components: {
BaseLoader,
WorkspacePropsTable,
WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal,
@@ -126,7 +129,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
isOptionsModal: false,
isIndexesModal: false,
@@ -205,8 +208,10 @@ export default {
}),
async getFieldsData () {
if (!this.table) return;
this.localFields = [];
this.newFieldsCounter = 0;
this.isQuering = true;
this.isLoading = true;
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
const params = {
@@ -281,7 +286,7 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;

View File

@@ -43,7 +43,8 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor
v-if="isSelected"
@@ -74,6 +75,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsFunctionOptionsModal from '@/components/WorkspacePropsFunctionOptionsModal';
import WorkspacePropsFunctionParamsModal from '@/components/WorkspacePropsFunctionParamsModal';
@@ -82,6 +84,7 @@ import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspacePropsTabFunction',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsFunctionOptionsModal,
WorkspacePropsFunctionParamsModal
@@ -93,7 +96,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
@@ -166,7 +169,9 @@ export default {
}),
async getFunctionData () {
if (!this.function) return;
this.isQuering = true;
this.isLoading = true;
this.localFunction = { sql: '' };
const params = {
uid: this.connection.uid,
@@ -189,7 +194,7 @@ export default {
}
this.resizeQueryEditor();
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;

View File

@@ -43,7 +43,8 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor
v-if="isSelected"
@@ -75,6 +76,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import Routines from '@/ipc-api/Routines';
@@ -83,6 +85,7 @@ export default {
name: 'WorkspacePropsTabRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal
},
@@ -93,7 +96,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
@@ -166,7 +169,8 @@ export default {
}),
async getRoutineData () {
if (!this.routine) return;
this.isQuering = true;
this.localRoutine = { sql: '' };
this.isLoading = true;
const params = {
uid: this.connection.uid,
@@ -189,7 +193,7 @@ export default {
}
this.resizeQueryEditor();
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;

View File

@@ -114,7 +114,8 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label>
<QueryEditor
v-if="isSelected"
@@ -137,6 +138,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsSchedulerTimingModal from '@/components/WorkspacePropsSchedulerTimingModal';
import Schedulers from '@/ipc-api/Schedulers';
@@ -144,6 +146,7 @@ import Schedulers from '@/ipc-api/Schedulers';
export default {
name: 'WorkspacePropsTabScheduler',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsSchedulerTimingModal
},
@@ -154,7 +157,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
isTimingModal: false,
originalScheduler: null,
@@ -226,7 +229,7 @@ export default {
}),
async getSchedulerData () {
if (!this.scheduler) return;
this.isQuering = true;
this.isLoading = true;
const params = {
uid: this.connection.uid,
@@ -249,7 +252,7 @@ export default {
}
this.resizeQueryEditor();
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;

View File

@@ -95,7 +95,8 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor
v-if="isSelected"
@@ -110,13 +111,15 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import Triggers from '@/ipc-api/Triggers';
export default {
name: 'WorkspacePropsTabTrigger',
components: {
BaseLoader,
QueryEditor
},
props: {
@@ -126,7 +129,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
originalTrigger: null,
localTrigger: { sql: '' },
@@ -197,7 +200,9 @@ export default {
}),
async getTriggerData () {
if (!this.trigger) return;
this.isQuering = true;
this.localTrigger = { sql: '' };
this.isLoading = true;
const params = {
uid: this.connection.uid,
@@ -220,7 +225,7 @@ export default {
}
this.resizeQueryEditor();
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;

View File

@@ -156,7 +156,8 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
<QueryEditor
v-if="isSelected"
@@ -172,12 +173,14 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import Views from '@/ipc-api/Views';
export default {
name: 'WorkspacePropsTabView',
components: {
BaseLoader,
QueryEditor
},
props: {
@@ -187,7 +190,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
originalView: null,
localView: { sql: '' },
@@ -251,7 +254,8 @@ export default {
}),
async getViewData () {
if (!this.view) return;
this.isQuering = true;
this.isLoading = true;
this.localView = { sql: '' };
const params = {
uid: this.connection.uid,
@@ -274,7 +278,7 @@ export default {
}
this.resizeQueryEditor();
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;

View File

@@ -196,7 +196,7 @@ export default {
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0;
return field.length;
},
keyName (key) {
switch (key) {

View File

@@ -18,8 +18,8 @@ export default class {
return ipcRenderer.invoke('delete-database', params);
}
static getStructure (uid) {
return ipcRenderer.invoke('get-structure', uid);
static getStructure (params) {
return ipcRenderer.invoke('get-structure', params);
}
static getCollations (uid) {

View File

@@ -1245,6 +1245,12 @@ ace.define('ace/autocomplete/popup', ['require', 'exports', 'module', 'ace/virtu
case 'routine':
iconClass = 'mdi-sync-circle';
break;
case 'function':
iconClass = 'mdi-arrow-right-bold-box';
break;
case 'scheduler':
iconClass = 'mdi-calendar-clock';
break;
case 'keyword':
iconClass = 'mdi-cube';
break;

View File

@@ -2,7 +2,6 @@
import Vue from 'vue';
import Vuex from 'vuex';
import VuexPersist from 'vuex-persist';
import application from './modules/application.store';
import settings from './modules/settings.store';
@@ -12,15 +11,6 @@ import notifications from './modules/notifications.store';
import ipcUpdates from './plugins/ipcUpdates';
const vuexLocalStorage = new VuexPersist({
key: 'application', // The key to store the state on in the storage provider.
storage: window.localStorage,
reducer: state => ({
connections: state.connections,
settings: state.settings
})
});
Vue.use(Vuex);
export default new Vuex.Store({
@@ -33,7 +23,6 @@ export default new Vuex.Store({
notifications
},
plugins: [
vuexLocalStorage.plugin,
ipcUpdates
]
});

View File

@@ -40,6 +40,9 @@ export default {
.filter(workspace => workspace.connected)
.map(workspace => workspace.uid);
},
getLoadedSchemas: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).loaded_schemas;
},
isUnsavedDiscardModal: state => {
return state.is_unsaved_discard_modal;
}
@@ -65,6 +68,8 @@ export default {
? {
...workspace,
structure: {},
breadcrumbs: {},
loaded_schemas: new Set(),
connected: false
}
: workspace);
@@ -77,6 +82,19 @@ export default {
}
: workspace);
},
REFRESH_SCHEMA (state, { uid, schema, schemaElements }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
const schemaIndex = workspace.structure.findIndex(s => s.name === schema);
if (schemaIndex !== -1)
workspace.structure[schemaIndex] = schemaElements;
else
workspace.structure.push(schemaElements);
}
return workspace;
});
},
REFRESH_COLLATIONS (state, { uid, collations }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
@@ -198,6 +216,13 @@ export default {
},
SET_PENDING_BREADCRUMBS (state, payload) {
state.pending_breadcrumbs = payload;
},
ADD_LOADED_SCHEMA (state, payload) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === payload.uid)
workspace.loaded_schemas.add(payload.schema);
return workspace;
});
}
},
actions: {
@@ -237,9 +262,10 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshStructure ({ dispatch, commit }, uid) {
async refreshStructure ({ dispatch, commit, getters }, uid) {
try {
const { status, response } = await Database.getStructure(uid);
const { status, response } = await Database.getStructure({ uid, schemas: getters.getLoadedSchemas(uid) });
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -249,6 +275,18 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshSchema ({ dispatch, commit }, { uid, schema }) {
try {
const { status, response } = await Database.getStructure({ uid, schemas: new Set([schema]) });
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_SCHEMA', { uid, schema, schemaElements: response.find(_schema => _schema.name === schema) });
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshCollations ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getCollations(uid);
@@ -312,7 +350,8 @@ export default {
variables: [],
collations: [],
users: [],
breadcrumbs: {}
breadcrumbs: {},
loaded_schemas: new Set()
};
commit('ADD_WORKSPACE', workspace);
@@ -349,6 +388,9 @@ export default {
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } });
lastBreadcrumbs = { ...breadcrumbsObj, ...payload };
if (payload.schema)
commit('ADD_LOADED_SCHEMA', { uid: getters.getSelected, schema: payload.schema });
},
newTab ({ commit }, uid) {
const tab = uidGen('T');