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

Compare commits

..

28 Commits

Author SHA1 Message Date
8dfaa3b7be chore(release): 0.0.19 2021-03-05 17:29:12 +01:00
5d7efa75b7 fix(UI): modal processes list does not regain size on window resize 2021-03-05 17:23:13 +01:00
4862d51fba fix(UI): wrong height in scrolling tables in some cases 2021-03-05 17:10:52 +01:00
07f60c3917 feat(UI): modal that shows process query 2021-03-04 19:34:18 +01:00
049143d143 feat: processes list tool 2021-03-03 19:31:05 +01:00
db4430609e feat(MySQL): support to new mysql8 authentication, closes #45 2021-03-02 12:03:01 +01:00
71b4310117 feat: context menu shortcut to set NULL a table cell 2021-02-28 21:45:38 +01:00
201fad9265 fix(MySQL): wrong TIMESTAMP fields length 2021-02-27 18:30:34 +01:00
45351faeae feat(UI): esc key to cancel cell edit 2021-02-27 18:29:47 +01:00
b4ead6992c perf(UI): improvements of date time inputs 2021-02-27 17:52:54 +01:00
b1ea32b680 feat: setting to enable beta updates (future use) 2021-02-27 17:28:01 +01:00
39ca1974bc perf(UI): big performance improvement in tables rendering 2021-02-26 22:31:05 +01:00
777b73fa6f feat(UI): query duration calc 2021-02-26 18:45:00 +01:00
4494e637f7 chore(release): 0.0.18 2021-02-25 19:09:02 +01:00
219da0aba4 feat(UI): run procedures/functions from sidebar context menu 2021-02-25 17:43:23 +01:00
7e8167154f feat(UI): run routines/functions from settings tab 2021-02-25 12:39:50 +01:00
3aa2159a1a fix(MySQL): issue obtaining routine/function parameters 2021-02-24 19:45:27 +01:00
76d92cd106 fix: issue managing function/routine parameters 2021-02-24 12:46:31 +01:00
c8545a250b fix(UI): elements from previous selected schemas in query suggestions 2021-02-22 19:14:02 +01:00
dbab06fcb8 fix(UI): data tab opened when non-table element is selected 2021-02-22 11:10:04 +01:00
b54fefbf25 feat(UI): context menu for input and textarea tags 2021-02-21 21:24:25 +01:00
9a1bf32128 feat(UI): html, xml, json, svg and yaml editor modes in long text fields edit 2021-02-21 19:22:03 +01:00
110b0b414c feat(UI): sticky schema names in explore bar 2021-02-20 18:55:08 +01:00
2f58007af4 feat(UI): search filter in explore bar 2021-02-20 11:55:34 +01:00
9b60bfff8d build: dropped use of lodash 2021-02-19 17:41:33 +01:00
3b37b7432e fix: disabled sort for fields without a name property 2021-02-18 18:12:36 +01:00
7c4ca999ce fix: prevents F5 shortcut to run in non-selected workspaces 2021-02-18 18:01:12 +01:00
94c4952319 fix: support of bit fields in table filler 2021-02-18 15:26:17 +01:00
42 changed files with 1721 additions and 258 deletions

View File

@@ -2,6 +2,55 @@
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.19](https://github.com/Fabio286/antares/compare/v0.0.18...v0.0.19) (2021-03-05)
### Features
* **UI:** modal that shows process query ([07f60c3](https://github.com/Fabio286/antares/commit/07f60c39173e9db452909d74573c3aecf4b6466b))
* processes list tool ([049143d](https://github.com/Fabio286/antares/commit/049143d143d8187bfcb6377f2bf374c471c28046))
* **MySQL:** support to new mysql8 authentication, closes [#45](https://github.com/Fabio286/antares/issues/45) ([db44306](https://github.com/Fabio286/antares/commit/db4430609e22816f4a3a8ecdbf61e9b51cde2579))
* context menu shortcut to set NULL a table cell ([71b4310](https://github.com/Fabio286/antares/commit/71b43101172c27d810d533c936534bcf089dbca2))
* **UI:** esc key to cancel cell edit ([45351fa](https://github.com/Fabio286/antares/commit/45351faeaea6ad4af8280da6c0ec1b4ded0f86fe))
* setting to enable beta updates (future use) ([b1ea32b](https://github.com/Fabio286/antares/commit/b1ea32b68024593481120e2ef67642e127554888))
* **UI:** query duration calc ([777b73f](https://github.com/Fabio286/antares/commit/777b73fa6f9d6f7806e0d3d07589dfaee0c40786))
### Bug Fixes
* **MySQL:** wrong TIMESTAMP fields length ([201fad9](https://github.com/Fabio286/antares/commit/201fad9265e01e19ae4c8dc16bff84ee9dc9c894))
* **UI:** modal processes list does not regain size on window resize ([5d7efa7](https://github.com/Fabio286/antares/commit/5d7efa75b76f59b85654603b4df8035a36af0576))
* **UI:** wrong height in scrolling tables in some cases ([4862d51](https://github.com/Fabio286/antares/commit/4862d51fba863e055ca6735586b0abe4894037eb))
### Improvements
* **UI:** big performance improvement in tables rendering ([39ca197](https://github.com/Fabio286/antares/commit/39ca1974bcea84c9047779893ed3f458300474e3))
* **UI:** improvements of date time inputs ([b4ead69](https://github.com/Fabio286/antares/commit/b4ead6992c8ddc172380a97e7aa125e0dd078f1e))
### [0.0.18](https://github.com/Fabio286/antares/compare/v0.0.17...v0.0.18) (2021-02-25)
### Features
* **UI:** context menu for input and textarea tags ([b54fefb](https://github.com/Fabio286/antares/commit/b54fefbf255c91f3cdd0c52b917b239ea40dae16))
* **UI:** html, xml, json, svg and yaml editor modes in long text fields edit ([9a1bf32](https://github.com/Fabio286/antares/commit/9a1bf3212850d3783ac6382f8a980eb25c4f4226))
* **UI:** run procedures/functions from sidebar context menu ([219da0a](https://github.com/Fabio286/antares/commit/219da0aba46839295807a01056199d42584e0af9))
* **UI:** run routines/functions from settings tab ([7e81671](https://github.com/Fabio286/antares/commit/7e8167154ffd64e46d78a50cd13cf90cee61370c))
* **UI:** search filter in explore bar ([2f58007](https://github.com/Fabio286/antares/commit/2f58007af4f32207fe4be9fc15bb43916e8e0abc))
* **UI:** sticky schema names in explore bar ([110b0b4](https://github.com/Fabio286/antares/commit/110b0b414cef25395ae28493032c2048307e4783))
### Bug Fixes
* **MySQL:** issue obtaining routine/function parameters ([3aa2159](https://github.com/Fabio286/antares/commit/3aa2159a1ae3b6785646fc7ac1e866cb0277d087))
* issue managing function/routine parameters ([76d92cd](https://github.com/Fabio286/antares/commit/76d92cd106b0409e8752c43d7d587d09dd3e1e32))
* **UI:** data tab opened when non-table element is selected ([dbab06f](https://github.com/Fabio286/antares/commit/dbab06fcb80ccdae85215ceae9ba24af3e7ff263))
* **UI:** elements from previous selected schemas in query suggestions ([c8545a2](https://github.com/Fabio286/antares/commit/c8545a250bc696c339e418a69a2942d5bac10cbf))
* disabled sort for fields without a name property ([3b37b74](https://github.com/Fabio286/antares/commit/3b37b7432e969e6315ebd6ed74905ec3980a049c))
* prevents F5 shortcut to run in non-selected workspaces ([7c4ca99](https://github.com/Fabio286/antares/commit/7c4ca999ce64af8077d29e419f430d5beaaead93))
* support of bit fields in table filler ([94c4952](https://github.com/Fabio286/antares/commit/94c49523197284510361ebbb6984816a3bb1b243))
### [0.0.17](https://github.com/Fabio286/antares/compare/v0.0.16...v0.0.17) (2021-02-17)

View File

@@ -74,8 +74,8 @@ Depending on your distribution, you will need to run the following command:
- [x] MySQL/MariaDB
- [ ] PostgreSQL
- [ ] MSSQL
- [ ] SQLite
- [ ] MSSQL
- [ ] OracleDB
- [ ] More...

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.17",
"version": "0.0.19",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -58,38 +58,37 @@
"electron-updater": "^4.3.5",
"faker": "^5.3.1",
"keytar": "^7.3.0",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"mssql": "^6.2.3",
"mysql": "^2.18.1",
"mysql2": "^2.2.5",
"pg": "^8.5.1",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"v-mask": "^2.2.4",
"vue-i18n": "^8.22.4",
"vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.2",
"electron": "^11.2.1",
"electron": "^11.3.0",
"electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.18.0",
"eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-vue": "^7.5.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-vue": "^7.6.0",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.1",
"standard-version": "^9.1.0",
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"stylelint-scss": "^3.19.0",
"vue": "^2.6.12",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.46.0"

View File

@@ -0,0 +1 @@
module.exports = [];

View File

@@ -105,6 +105,17 @@ export default connections => {
}
});
ipcMain.handle('get-processes', async (event, uid) => {
try {
const result = await connections[uid].getProcesses();
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('use-schema', async (event, { uid, schema }) => {
if (!schema) return;

View File

@@ -81,6 +81,8 @@ export default (connections) => {
escapedParam = `b'${sqlEscaper(params.content)}'`;
reload = true;
}
else if (params.content === null)
escapedParam = 'NULL';
else
escapedParam = `"${sqlEscaper(params.content)}"`;
@@ -232,6 +234,8 @@ export default (connections) => {
else
escapedParam = '""';
}
else if (BIT.includes(type))
escapedParam = `b'${sqlEscaper(params.row[key].value)}'`;
else
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;

View File

@@ -1,8 +1,10 @@
import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
let mainWindow;
autoUpdater.allowPrerelease = true;
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
export default () => {
ipcMain.on('check-for-updates', event => {

View File

@@ -1,5 +1,5 @@
'use strict';
import mysql from 'mysql';
import mysql from 'mysql2';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql';
@@ -43,17 +43,21 @@ export class MySQLClient extends AntaresCore {
}
_getType (field) {
let name = this.types[field.type];
let length = field.length;
let name = this.types[field.columnType];
let length = field.columnLength;
if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name))
length = field.decimals;
if (name === 'CHAR' && field.charsetNr === 63)// if binary
name = 'BINARY';
if (name === 'TIMESTAMP')
length = 0;
if (name === 'VARCHAR' && field.charsetNr === 63)// if binary
name = 'VARBINARY';
if (field.charsetNr === 63) { // if binary
if (name === 'CHAR')
name = 'BINARY';
else if (name === 'VARCHAR')
name = 'VARBINARY';
}
if (name === 'BLOB') {
switch (length) {
@@ -551,18 +555,18 @@ export class MySQLClient extends AntaresCore {
}
const parameters = row['Create Procedure']
.match(/(?<=\().*?(?=\))/s)[0]
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return {
name: param[1] ? param[1].replaceAll('`', '') : '',
type: type[0],
length: +type[1],
type: type[0].replaceAll('\n', ''),
length: +type[1] ? +type[1].replace(/\D/g, '') : '',
context: param[0] ? param[0].replace('\n', '') : ''
};
}).filter(el => el.name);
@@ -578,7 +582,7 @@ export class MySQLClient extends AntaresCore {
return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters,
parameters: parameters || [],
name: row.Procedure,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
@@ -628,10 +632,12 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async createRoutine (routine) {
const parameters = routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
const parameters = 'parameters' in routine
? routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters})
LANGUAGE SQL
@@ -671,9 +677,10 @@ export class MySQLClient extends AntaresCore {
}
const parameters = row['Create Function']
.match(/(?<=\().*?(?=\))/s)[0]
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
@@ -682,7 +689,7 @@ export class MySQLClient extends AntaresCore {
return {
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0],
length: +type[1]
length: +type[1] ? +type[1].replace(/\D/g, '') : ''
};
}).filter(el => el.name);
@@ -699,7 +706,7 @@ export class MySQLClient extends AntaresCore {
return {
definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters,
parameters: parameters || [],
name: row.Function,
comment: row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Function'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
@@ -949,6 +956,25 @@ export class MySQLClient extends AntaresCore {
}, {});
}
async getProcesses () {
const sql = 'SELECT `ID`, `USER`, `HOST`, `DB`, `COMMAND`, `TIME`, `STATE`, LEFT(`INFO`, 51200) AS `INFO` FROM `information_schema`.`PROCESSLIST`';
const { rows } = await this.raw(sql);
return rows.map(row => {
return {
id: row.ID,
user: row.USER,
host: row.HOST,
db: row.DB,
command: row.COMMAND,
time: row.TIME,
state: row.STATE,
info: row.INFO
};
});
}
/**
* CREATE TABLE
*
@@ -1203,10 +1229,13 @@ export class MySQLClient extends AntaresCore {
for (const query of queries) {
if (!query) continue;
const timeStart = new Date();
let timeStop;
let keysArr = [];
const { rows, report, fields, keys } = await new Promise((resolve, reject) => {
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
this._connection.query({ sql: query, nestTables }, async (err, response, fields) => {
timeStop = new Date();
const queryResult = response;
if (err)
@@ -1223,10 +1252,9 @@ export class MySQLClient extends AntaresCore {
name: field.orgName,
alias: field.name,
orgName: field.orgName,
schema: field.db,
schema: field.schema,
table: field.table,
tableAlias: field.table,
zerofill: field.zerofill,
orgTable: field.orgTable,
type: type.name,
length: type.length
@@ -1274,6 +1302,7 @@ export class MySQLClient extends AntaresCore {
}
resolve({
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: remappedFields,
@@ -1283,7 +1312,7 @@ export class MySQLClient extends AntaresCore {
});
});
resultsArr.push({ rows, report, fields, keys });
resultsArr.push({ rows, report, fields, keys, duration });
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;

View File

@@ -24,7 +24,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { ipcRenderer } from 'electron';
import { ipcRenderer, remote } from 'electron';
export default {
name: 'App',
@@ -54,6 +54,45 @@ export default {
},
mounted () {
ipcRenderer.send('check-for-updates');
const Menu = remote.Menu;
const InputMenu = Menu.buildFromTemplate([
{
label: this.$t('word.cut'),
role: 'cut'
},
{
label: this.$t('word.copy'),
role: 'copy'
},
{
label: this.$t('word.paste'),
role: 'paste'
},
{
type: 'separator'
},
{
label: this.$t('message.selectAll'),
role: 'selectall'
}
]);
document.body.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
let node = e.target;
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup(remote.getCurrentWindow());
break;
}
node = node.parentNode;
}
});
},
methods: {
...mapActions({

View File

@@ -24,7 +24,7 @@
<slot name="body" />
</div>
</div>
<div class="modal-footer">
<div v-if="!hideFooter" class="modal-footer">
<button
class="btn btn-primary mr-2"
@click.stop="confirmModal"
@@ -51,6 +51,10 @@ export default {
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small'
},
hideFooter: {
type: Boolean,
default: false
},
confirmText: String,
cancelText: String
},
@@ -74,6 +78,12 @@ export default {
else return '';
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
confirmModal () {
this.$emit('confirm');
@@ -82,6 +92,11 @@ export default {
hideModal () {
this.$emit('hide');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.hideModal();
}
}
};

View File

@@ -0,0 +1,129 @@
<template>
<div class="editor-wrapper">
<div
:id="`editor-${id}`"
class="editor"
:class="editorClass"
:style="{height: `${height}px`}"
/>
</div>
</template>
<script>
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import { mapGetters } from 'vuex';
export default {
name: 'BaseTextEditor',
props: {
value: String,
mode: { type: String, default: 'text' },
editorClass: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 }
},
data () {
return {
editor: null,
id: null
};
},
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
})
},
watch: {
mode () {
if (this.editor)
this.editor.session.setMode(`ace/mode/${this.mode}`);
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
autoComplete () {
if (this.editor) {
this.editor.setOptions({
enableLiveAutocompletion: this.autoComplete
});
}
},
lineWrap () {
if (this.editor) {
this.editor.setOptions({
wrap: this.lineWrap
});
}
}
},
created () {
this.id = this._uid;
},
mounted () {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly
});
this.editor.setOptions({
enableBasicAutocompletion: false,
wrap: this.lineWrap,
enableSnippets: false,
enableLiveAutocompletion: false
});
this.editor.session.on('change', () => {
const content = this.editor.getValue();
this.$emit('update:value', content);
});
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
this.editor.resize();
}, 20);
}
setTimeout(() => {
this.editor.resize();
}, 20);
}
};
</script>
<style lang="scss" scoped>
.editor-wrapper {
border-bottom: 1px solid #444;
.editor {
width: 100%;
}
}
.ace_.mdi {
display: inline-block;
width: 17px;
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
background-color: #c9561a99;
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover {
background-color: #c9571a33;
border: none;
}
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight {
color: #e0d00c;
}
</style>

View File

@@ -41,14 +41,16 @@ export default {
localScrollElement: null
};
},
watch: {
scrollElement () {
this.setScrollElement();
}
},
mounted () {
this._checkScrollPosition = this.checkScrollPosition.bind(this);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
this.setScrollElement();
},
beforeDestroy () {
this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition);
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
},
methods: {
checkScrollPosition (e) {
@@ -58,7 +60,7 @@ export default {
this.updateWindow(e);
}, 200);
},
updateWindow (e) {
updateWindow () {
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
const totalScrollHeight = this.items.length * this.itemHeight;
const offset = 50;
@@ -74,6 +76,14 @@ export default {
this.topHeight = firstCutIndex * this.itemHeight;
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
},
setScrollElement () {
if (this.localScrollElement)
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this.checkScrollPosition);
}
}
};

View File

@@ -90,7 +90,7 @@
</template>
<script>
import { mask } from 'vue-the-mask';
import { VueMaskDirective } from 'v-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import BaseUploadInput from '@/components/BaseUploadInput';
import ForeignKeySelect from '@/components/ForeignKeySelect';
@@ -103,7 +103,7 @@ export default {
BaseUploadInput
},
directives: {
mask
mask: VueMaskDirective
},
props: {
type: String,
@@ -136,6 +136,8 @@ export default {
this.localType = 'datetime';
else if (TIME.includes(this.type))
this.localType = 'time';
else
this.localType = 'none';
return FakerMethods.getGroupsByType(this.localType);
},

View File

@@ -0,0 +1,102 @@
<template>
<ConfirmModal
:confirm-text="$t('word.run')"
:cancel-text="$t('word.cancel')"
size="400"
@confirm="runRoutine"
@hide="closeModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" /> {{ $t('word.parameters') }}: {{ localRoutine.name }}
</div>
</template>
<div slot="body">
<div class="content">
<form class="form-horizontal">
<div
v-for="(parameter, i) in localRoutine.parameters"
:key="parameter._id"
class="form-group"
>
<div class="col-3">
<label class="form-label">{{ parameter.name }}</label>
</div>
<div class="col-9">
<div class="input-group">
<input
:ref="i === 0 ? 'firstInput' : ''"
v-model="values[parameter.name]"
class="form-input"
type="text"
>
<span class="input-group-addon field-type" :class="`type-${parameter.type.toLowerCase()}`">
{{ parameter.type }} {{ parameter.length | wrapNumber }}
</span>
</div>
</div>
</div>
</form>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalAskParameters',
components: {
ConfirmModal
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
localRoutine: Object
},
data () {
return {
values: {}
};
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput[0].focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr) => {
const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr];
acc.push(value);
return acc;
}, []);
this.$emit('confirm', valArr);
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style scoped>
.field-type {
font-size: 0.6rem;
}
</style>

View File

@@ -184,8 +184,7 @@
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect';
@@ -195,9 +194,6 @@ export default {
components: {
FakerSelect
},
directives: {
mask
},
filters: {
wrapNumber (num) {
if (!num) return '';
@@ -262,6 +258,9 @@ export default {
if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
if (DATETIME.includes(field.type)) {
if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
let datePrecision = '';

View File

@@ -118,7 +118,7 @@
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { VueMaskDirective } from 'v-mask';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect';
@@ -129,7 +129,7 @@ export default {
ForeignKeySelect
},
directives: {
mask
mask: VueMaskDirective
},
filters: {
wrapNumber (num) {

View File

@@ -0,0 +1,310 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-memory mr-1" /> {{ $t('message.processesList') }}: {{ connectionName }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="processes-toolbar py-2 px-4">
<div class="dropdown">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
title="F5"
@click="getProcessesList"
>
<span>{{ $t('word.refresh') }}</span>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
</button>
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
<i class="mdi mdi-24px mdi-menu-down" />
</div>
<div class="menu px-3">
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input
v-model="autorefreshTimer"
class="slider no-border"
type="range"
min="0"
max="15"
step="0.5"
@change="setRefreshInterval"
>
</div>
</div>
</div>
<div class="workspace-query-info">
<div v-if="sortedResults.length">
{{ $t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b>
</div>
</div>
</div>
<div class="modal-body py-0 workspace-query-results">
<div
ref="tableWrapper"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="(field, index) in fields"
:key="index"
class="th c-hand"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field)">
<span>{{ field.toUpperCase() }}</span>
<i
v-if="currentSort === field"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
</div>
</div>
</div>
</div>
</div>
<BaseVirtualScroll
ref="resultTable"
:items="sortedResults"
:item-height="22"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<ProcessesListRow
v-for="row in items"
:key="row._id"
class="process-row"
:row="row"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
</template>
</BaseVirtualScroll>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ProcessesListRow from '@/components/ProcessesListRow';
export default {
name: 'ModalProcessesList',
components: {
BaseVirtualScroll,
ProcessesListRow
},
props: {
connection: Object
},
data () {
return {
resultsSize: 1000,
isQuering: false,
autorefreshTimer: 0,
refreshInterval: null,
results: [],
fields: [],
currentSort: '',
currentSortDir: 'asc',
scrollElement: null
};
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName'
}),
connectionName () {
return this.getConnectionName(this.connection.uid);
},
sortedResults () {
if (this.currentSort) {
return [...this.results].sort((a, b) => {
let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
if (this.currentSortDir === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier;
return 0;
});
}
else
return this.results;
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper)
this.scrollElement = this.$refs.tableWrapper;
},
mounted () {
this.getProcessesList();
window.addEventListener('resize', this.resizeResults);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async getProcessesList () {
this.isQuering = true;
// if table changes clear cached values
if (this.lastTable !== this.table)
this.results = [];
try { // Table data
const { status, response } = await Database.getProcesses(this.connection.uid);
if (status === 'success') {
this.results = response;
this.fields = response.length ? Object.keys(response[0]) : [];
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
setRefreshInterval () {
this.clearRefresh();
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.getProcessesList();
}, this.autorefreshTimer * 1000);
}
},
clearRefresh () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el) {
const size = el.offsetHeight;
this.resultsSize = size;
}
this.$refs.resultTable.updateWindow();
}
},
refreshScroller () {
this.resizeResults();
},
sort (field) {
if (field === this.currentSort) {
if (this.currentSortDir === 'asc')
this.currentSortDir = 'desc';
else
this.resetSort();
}
else {
this.currentSortDir = 'asc';
this.currentSort = field;
}
},
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
},
stopRefresh () {
this.autorefreshTimer = 0;
this.clearRefresh();
},
contextMenu () {},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
if (e.key === 'F5')
this.getProcessesList();
}
}
};
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.column-resizable {
&:hover,
&:active {
resize: horizontal;
overflow: hidden;
}
}
.table-column-title {
display: flex;
align-items: center;
}
.sort-icon {
font-size: 0.7rem;
line-height: 1;
margin-left: 0.2rem;
}
.result-tabs {
background: transparent !important;
margin: 0;
}
.modal {
align-items: flex-start;
.modal-container {
max-width: 75vw;
margin-top: 10vh;
.modal-body {
height: 80vh;
}
}
}
.processes-toolbar {
display: flex;
justify-content: space-between;
}
</style>

View File

@@ -36,11 +36,17 @@
{{ $t('message.restartToInstall') }}
</button>
</div>
<div class="form-group mt-4">
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
<input type="checkbox" :checked="allowPrerelease">
<i class="form-icon" /> {{ $t('message.includeBetaUpdates') }}
</label>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import { ipcRenderer } from 'electron';
export default {
@@ -48,7 +54,8 @@ export default {
computed: {
...mapGetters({
updateStatus: 'application/getUpdateStatus',
downloadPercentage: 'application/getDownloadProgress'
downloadPercentage: 'application/getDownloadProgress',
allowPrerelease: 'settings/getAllowPrerelease'
}),
updateMessage () {
switch (this.updateStatus) {
@@ -70,11 +77,17 @@ export default {
}
},
methods: {
...mapActions({
changeAllowPrerelease: 'settings/changeAllowPrerelease'
}),
checkForUpdates () {
ipcRenderer.send('check-for-updates');
},
restartToUpdate () {
ipcRenderer.send('restart-to-update');
},
toggleAllowPrerelease () {
this.changeAllowPrerelease(!this.allowPrerelease);
}
}
};

View File

@@ -0,0 +1,155 @@
<template>
<div class="tr" @click="selectRow($event, row._id)">
<div
v-for="(col, cKey) in row"
v-show="cKey !== '_id'"
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</template>
</div>
<ConfirmModal
v-if="isInfoModal"
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
size="medium"
:hide-footer="true"
@hide="hideInfoModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
</div>
</template>
<div :slot="'body'">
<div>
<div>
<TextEditor
:value="row.info || ''"
editor-class="textarea-editor"
:mode="editorMode"
:read-only="true"
/>
</div>
</div>
</div>
</ConfirmModal>
</div>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'ProcessesListRow',
components: {
ConfirmModal,
TextEditor
},
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
}
},
props: {
row: Object
},
data () {
return {
isInlineEditor: {},
isInfoModal: false,
editorMode: 'sql'
};
},
computed: {},
watch: {
fields () {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: {
isNull (value) {
return value === null ? ' is-null' : '';
},
selectRow (event, row) {
this.$emit('select-row', event, row);
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
},
hideInfoModal () {
this.isInfoModal = false;
},
dblClick (col) {
if (col !== 'info') return;
this.$emit('stop-refresh');
this.isInfoModal = true;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
}
}
};
</script>
<style lang="scss">
.editable-field {
margin: 0;
border: none;
line-height: 1;
width: 100%;
position: absolute;
left: 0;
right: 0;
}
.cell-content {
display: block;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.textarea-editor {
height: 50vh !important;
}
.editor-field-info {
margin-top: 0.4rem;
display: flex;
justify-content: space-between;
white-space: normal;
}
.editor-buttons {
display: flex;
justify-content: space-evenly;
.btn {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="editor-wrapper">
<div
ref="editor"
:id="`editor-${id}`"
class="editor"
:style="{height: `${height}px`}"
/>
@@ -12,7 +12,7 @@
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools';
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
export default {
@@ -20,6 +20,7 @@ export default {
props: {
value: String,
workspace: Object,
isSelected: Boolean,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
@@ -29,14 +30,17 @@ export default {
return {
editor: null,
fields: [],
baseCompleter: []
customCompleter: [],
id: null,
lastSchema: null
};
},
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
lineWrap: 'settings/getLineWrap',
baseCompleter: 'application/getBaseCompleter'
}),
tables () {
return this.workspace
@@ -163,10 +167,24 @@ export default {
wrap: this.lineWrap
});
}
},
isSelected () {
if (this.isSelected)
this.lastSchema = this.schema;
},
lastSchema () {
if (this.editor) {
this.editor.completers = this.baseCompleter.map(el => Object.assign({}, el));
this.setCustomCompleter();
}
}
},
created () {
this.id = this._uid;
this.lastSchema = this.schema;
},
mounted () {
this.editor = ace.edit(this.$refs.editor, {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
@@ -182,26 +200,10 @@ export default {
enableLiveAutocompletion: this.autoComplete
});
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
if (!this.baseCompleter.length)
this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el)));
this.baseCompleter = this.editor.completers;
this.setCustomCompleter();
this.editor.commands.on('afterExec', e => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
@@ -224,13 +226,13 @@ export default {
}).catch(console.log);
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
});
@@ -249,6 +251,33 @@ export default {
setTimeout(() => {
this.editor.resize();
}, 20);
},
methods: {
...mapActions({
setBaseCompleters: 'application/setBaseCompleter'
}),
setCustomCompleter () {
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
this.customCompleter = this.editor.completers;
}
}
};
</script>

View File

@@ -7,10 +7,34 @@
ref="tabWrap"
class="tab tab-block column col-12"
>
<li class="tab-item d-none">
<a class="tab-link workspace-tools-link">
<li class="tab-item dropdown tools-dropdown">
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
>
<i class="mdi mdi-24px mdi-tools" />
</a>
<ul class="menu text-left text-uppercase">
<li class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
</a>
</li>
<li class="menu-item" title="Coming...">
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
</a>
</li>
<li class="menu-item" title="Coming...">
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
</a>
</li>
</ul>
</li>
<li
v-if="schemaChild"
@@ -19,7 +43,7 @@
@click="selectTab({uid: workspace.uid, tab: 'prop'})"
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" />
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span>
</a>
</li>
@@ -109,11 +133,16 @@
<WorkspaceQueryTab
v-for="tab of queryTabs"
:key="tab.uid"
:tab-uid="tab.uid"
:tab="tab"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
</div>
<ModalProcessesList
v-if="isProcessesModal"
:connection="connection"
@close="hideProcessesModal"
/>
</div>
</template>
@@ -129,6 +158,7 @@ import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
export default {
name: 'Workspace',
@@ -141,14 +171,16 @@ export default {
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler
WorkspacePropsTabScheduler,
ModalProcessesList
},
props: {
connection: Object
},
data () {
return {
hasWheelEvent: false
hasWheelEvent: false,
isProcessesModal: false
};
},
computed: {
@@ -164,13 +196,20 @@ export default {
},
selectedTab () {
if (
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
) ||
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.selected_tab === 'data'
)
)
return this.queryTabs[0].uid;
@@ -214,7 +253,7 @@ export default {
removeTab: 'workspaces/removeTab'
}),
addTab () {
this.newTab(this.connection.uid);
this.newTab({ uid: this.connection.uid });
if (!this.hasWheelEvent) {
this.$refs.tabWrap.addEventListener('wheel', e => {
@@ -227,6 +266,12 @@ export default {
closeTab (tUid) {
if (this.queryTabs.length === 1) return;
this.removeTab({ uid: this.connection.uid, tab: tUid });
},
showProcessesModal () {
this.isProcessesModal = true;
},
hideProcessesModal () {
this.isProcessesModal = false;
}
}
};
@@ -291,6 +336,48 @@ export default {
opacity: 1;
}
&.tools-dropdown {
.tab-link:focus {
color: $primary-color;
opacity: 1;
outline: 0;
box-shadow: none;
}
.menu {
min-width: 100%;
.menu-item a {
border-radius: 0.1rem;
color: inherit;
display: block;
margin: 0 -0.4rem;
padding: 0.2rem 0.4rem;
text-decoration: none;
white-space: nowrap;
border: 0;
&:hover {
color: $primary-color;
background: $bg-color-gray;
}
.tool-icon {
line-height: 1;
display: inline-block;
font-size: 20px;
}
}
}
z-index: 9;
position: absolute;
}
&.tools-dropdown + .tab-item {
margin-left: 56px;
}
.workspace-tools-link {
padding-bottom: 0;
padding-top: 0.3rem;

View File

@@ -27,6 +27,17 @@
/>
</span>
</div>
<div class="workspace-explorebar-search">
<div v-if="workspace.connected" class="has-icon-right">
<input
v-model="searchTerm"
class="form-input input-sm"
type="text"
:placeholder="$t('message.searchForElements')"
>
<i class="form-icon mdi mdi-magnify mdi-18px" />
</div>
</div>
<WorkspaceConnectPanel
v-if="!workspace.connected"
class="workspace-explorebar-body"
@@ -117,7 +128,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash';// TODO: remove
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
@@ -172,6 +182,8 @@ export default {
isNewSchedulerModal: false,
localWidth: null,
explorebarWidthInterval: null,
searchTermInterval: null,
isDatabaseContext: false,
isTableContext: false,
isMiscContext: false,
@@ -182,7 +194,8 @@ export default {
selectedDatabase: '',
selectedTable: null,
selectedMisc: null
selectedMisc: null,
searchTerm: ''
};
},
computed: {
@@ -199,11 +212,22 @@ export default {
}
},
watch: {
localWidth: _.debounce(function (val) {
this.changeExplorebarSize(val);
}, 500),
localWidth (val) {
clearTimeout(this.explorebarWidthInterval);
this.explorebarWidthInterval = setTimeout(() => {
this.changeExplorebarSize(val);
}, 500);
},
isSelected (val) {
if (val) this.localWidth = this.explorebarSize;
},
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => {
this.setSearchTerm(this.searchTerm);
}, 200);
}
},
created () {
@@ -225,6 +249,7 @@ export default {
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab',
setSearchTerm: 'workspaces/setSearchTerm',
addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize'
}),
@@ -477,9 +502,40 @@ export default {
}
}
.workspace-explorebar-search {
width: 100%;
display: flex;
justify-content: space-between;
font-size: 0.6rem;
height: 28px;
.has-icon-right {
width: 100%;
padding: 0.1rem;
.form-icon {
opacity: 0.5;
transition: opacity 0.2s;
}
.form-input {
height: 1.2rem;
padding-left: 0.2rem;
&:focus + .form-icon {
opacity: 0.9;
}
&::placeholder {
opacity: 0.6;
}
}
}
}
.workspace-explorebar-body {
width: 100%;
height: calc((100vh - 30px) - #{$excluding-size});
height: calc((100vh - 58px) - #{$excluding-size});
overflow: overlay;
padding: 0 0.1rem;
}

View File

@@ -15,7 +15,7 @@
<div class="database-tables">
<ul class="menu menu-nav pt-0">
<li
v-for="table of database.tables"
v-for="table of filteredTables"
:key="table.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@@ -24,7 +24,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span>{{ table.name }}</span>
<span v-html="highlightWord(table.name)" />
</a>
<div
v-if="table.type === 'table'"
@@ -37,7 +37,7 @@
</ul>
</div>
<div v-if="database.triggers.length" class="database-misc">
<div v-if="filteredTriggers.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}">
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
@@ -47,7 +47,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="trigger of database.triggers"
v-for="trigger of filteredTriggers"
:key="trigger.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@@ -56,7 +56,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span>{{ trigger.name }}</span>
<span v-html="highlightWord(trigger.name)" />
</a>
</li>
</ul>
@@ -65,7 +65,7 @@
</details>
</div>
<div v-if="database.procedures.length" class="database-misc">
<div v-if="filteredProcedures.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}">
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
@@ -75,7 +75,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="procedure of database.procedures"
v-for="procedure of filteredProcedures"
:key="procedure.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@@ -84,7 +84,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span>{{ procedure.name }}</span>
<span v-html="highlightWord(procedure.name)" />
</a>
</li>
</ul>
@@ -93,7 +93,7 @@
</details>
</div>
<div v-if="database.functions.length" class="database-misc">
<div v-if="filteredFunctions.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}">
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
@@ -103,7 +103,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="func of database.functions"
v-for="func of filteredFunctions"
:key="func.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@@ -112,7 +112,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" />
<span>{{ func.name }}</span>
<span v-html="highlightWord(func.name)" />
</a>
</li>
</ul>
@@ -121,7 +121,7 @@
</details>
</div>
<div v-if="database.schedulers.length" class="database-misc">
<div v-if="filteredSchedulers.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}">
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
@@ -131,7 +131,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="scheduler of database.schedulers"
v-for="scheduler of filteredSchedulers"
:key="scheduler.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@@ -140,7 +140,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span>{{ scheduler.name }}</span>
<span v-html="highlightWord(scheduler.name)" />
</a>
</li>
</ul>
@@ -170,8 +170,27 @@ export default {
computed: {
...mapGetters({
getLoadedSchemas: 'workspaces/getLoadedSchemas',
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
getSearchTerm: 'workspaces/getSearchTerm'
}),
searchTerm () {
return this.getSearchTerm(this.connection.uid);
},
filteredTables () {
return this.database.tables.filter(table => table.name.search(this.searchTerm) >= 0);
},
filteredTriggers () {
return this.database.triggers.filter(trigger => trigger.name.search(this.searchTerm) >= 0);
},
filteredProcedures () {
return this.database.procedures.filter(procedure => procedure.name.search(this.searchTerm) >= 0);
},
filteredFunctions () {
return this.database.functions.filter(func => func.name.search(this.searchTerm) >= 0);
},
filteredSchedulers () {
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
},
breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs;
},
@@ -222,6 +241,14 @@ export default {
setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload);
},
highlightWord (string) {
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
}
}
};
@@ -229,6 +256,13 @@ export default {
<style lang="scss">
.workspace-explorebar-database {
.database-name {
position: sticky;
top: 0;
background: $bg-color-gray;
z-index: 2;
}
.database-name,
.misc-name,
a.table-name {
@@ -272,7 +306,7 @@ export default {
.misc-name {
&:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
background: $bg-color-light;
border-radius: 2px;
}
}

View File

@@ -5,8 +5,8 @@
>
<div
v-if="['procedure', 'function'].includes(selectedMisc.type)"
class="context-element disabled"
@click="showRunModal"
class="context-element"
@click="runElementCheck"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div>
@@ -29,6 +29,12 @@
</div>
</div>
</ConfirmModal>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localElement"
@confirm="runElement"
@close="hideAskParamsModal"
/>
</BaseContextMenu>
</template>
@@ -36,6 +42,7 @@
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions';
@@ -45,7 +52,8 @@ export default {
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu,
ConfirmModal
ConfirmModal,
ModalAskParameters
},
props: {
contextEvent: MouseEvent,
@@ -54,7 +62,9 @@ export default {
data () {
return {
isDeleteModal: false,
isRunModal: false
isRunModal: false,
isAskingParameters: false,
localElement: {}
};
},
computed: {
@@ -83,7 +93,8 @@ export default {
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
@@ -94,11 +105,12 @@ export default {
hideDeleteModal () {
this.isDeleteModal = false;
},
showRunModal () {
this.isRunModal = true;
showAskParamsModal () {
this.isAskingParameters = true;
},
hideRunModal () {
this.isRunModal = false;
hideAskParamsModal () {
this.isAskingParameters = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
@@ -148,6 +160,105 @@ export default {
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
runElementCheck () {
if (this.selectedMisc.type === 'procedure')
this.runRoutineCheck();
else if (this.selectedMisc.type === 'function')
this.runFunctionCheck();
},
runElement (params) {
if (this.selectedMisc.type === 'procedure')
this.runRoutine(params);
else if (this.selectedMisc.type === 'function')
this.runFunction(params);
},
async runRoutineCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
routine: this.workspace.breadcrumbs.procedure
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success')
this.localElement = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length)
this.showAskParamsModal();
else
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql;
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.closeContext();
},
async runFunctionCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
func: this.workspace.breadcrumbs.function
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success')
this.localElement = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.closeContext();
}
}
};

View File

@@ -2,7 +2,7 @@
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
@@ -34,10 +34,10 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param.name"
:key="param._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
:class="{'selected-param': selectedParam === param._id}"
@click="selectParameter($event, param._id)"
>
<div class="tile-icon">
<div>
@@ -54,7 +54,7 @@
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param.name)"
@click.prevent="removeParameter(param._id)"
>
<i class="mdi mdi-close" />
</button>
@@ -139,6 +139,7 @@
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
@@ -162,7 +163,7 @@ export default {
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam);
return this.parametersProxy.find(param => param._id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
@@ -182,12 +183,12 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
@@ -196,6 +197,7 @@ export default {
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
@@ -209,8 +211,8 @@ export default {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
@@ -223,7 +225,7 @@ export default {
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : '';
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
}
}
};

View File

@@ -2,7 +2,7 @@
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
@@ -34,10 +34,10 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param.name"
:key="param._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
:class="{'selected-param': selectedParam === param._id}"
@click="selectParameter($event, param._id)"
>
<div class="tile-icon">
<div>
@@ -54,7 +54,7 @@
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param.name)"
@click.prevent="removeParameter(param._id)"
>
<i class="mdi mdi-close" />
</button>
@@ -170,6 +170,7 @@
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
@@ -193,7 +194,7 @@ export default {
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam);
return this.parametersProxy.find(param => param._id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
@@ -213,12 +214,12 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
@@ -227,6 +228,7 @@ export default {
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
@@ -240,8 +242,8 @@ export default {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
@@ -254,7 +256,7 @@ export default {
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : '';
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
}
}
};

View File

@@ -138,7 +138,7 @@
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import { mask } from 'vue-the-mask';
import { VueMaskDirective } from 'v-mask';
import moment from 'moment';
export default {
@@ -147,7 +147,7 @@ export default {
ConfirmModal
},
directives: {
mask
mask: VueMaskDirective
},
props: {
localOptions: Object,

View File

@@ -25,9 +25,9 @@
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm disabled"
class="btn btn-dark btn-sm"
:disabled="isChanged"
@click="false"
@click="runFunctionCheck"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
@@ -70,15 +70,23 @@
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localFunction"
@confirm="runFunction"
@close="hideAskParamsModal"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsFunctionOptionsModal from '@/components/WorkspacePropsFunctionOptionsModal';
import WorkspacePropsFunctionParamsModal from '@/components/WorkspacePropsFunctionParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions';
export default {
@@ -87,7 +95,8 @@ export default {
BaseLoader,
QueryEditor,
WorkspacePropsFunctionOptionsModal,
WorkspacePropsFunctionParamsModal
WorkspacePropsFunctionParamsModal,
ModalAskParameters
},
props: {
connection: Object,
@@ -100,6 +109,7 @@ export default {
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
localFunction: { sql: '' },
lastFunction: null,
@@ -165,7 +175,8 @@ export default {
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
async getFunctionData () {
if (!this.function) return;
@@ -183,6 +194,12 @@ export default {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') {
this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.sqlProxy = this.localFunction.sql;
}
@@ -250,6 +267,31 @@ export default {
parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters };
},
runFunctionCheck () {
if (this.localFunction.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
@@ -261,6 +303,12 @@ export default {
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
}
}
};

View File

@@ -25,9 +25,9 @@
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm disabled"
class="btn btn-dark btn-sm"
:disabled="isChanged"
@click="false"
@click="runRoutineCheck"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
@@ -70,15 +70,23 @@
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localRoutine"
@confirm="runRoutine"
@close="hideAskParamsModal"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines';
export default {
@@ -87,7 +95,8 @@ export default {
QueryEditor,
BaseLoader,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal
WorkspacePropsRoutineParamsModal,
ModalAskParameters
},
props: {
connection: Object,
@@ -100,6 +109,7 @@ export default {
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalRoutine: null,
localRoutine: { sql: '' },
lastRoutine: null,
@@ -165,7 +175,8 @@ export default {
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
async getRoutineData () {
if (!this.routine) return;
@@ -182,6 +193,12 @@ export default {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') {
this.originalRoutine = response;
this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.sql;
}
@@ -249,6 +266,31 @@ export default {
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
runRoutineCheck () {
if (this.localRoutine.parameters.length)
this.showAskParamsModal();
else
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
@@ -260,6 +302,12 @@ export default {
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
}
}
};

View File

@@ -2,12 +2,13 @@
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:auto-focus="true"
:value.sync="query"
:workspace="workspace"
:schema="schema"
:is-selected="isSelected"
:height="editorHeight"
/>
<div ref="resizer" class="query-area-resizer" />
@@ -17,7 +18,7 @@
class="btn btn-primary btn-sm"
:class="{'loading':isQuering}"
:disabled="!query"
title="F9"
title="F5"
@click="runQuery(query)"
>
<span>{{ $t('word.run') }}</span>
@@ -25,6 +26,13 @@
</button>
</div>
<div class="workspace-query-info">
<div
v-if="results.length"
class="d-flex"
:title="$t('message.queryDuration')"
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
</div>
<div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div>
@@ -47,8 +55,9 @@
v-show="!isQuering"
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:tab-uid="tab.uid"
:conn-uid="connection.uid"
:is-selected="isSelected"
mode="query"
@update-field="updateField"
@delete-selected="deleteSelected"
@@ -73,7 +82,7 @@ export default {
mixins: [tableTabs],
props: {
connection: Object,
tabUid: String,
tab: Object,
isSelected: Boolean
},
data () {
@@ -83,19 +92,26 @@ export default {
isQuering: false,
results: [],
resultsCount: 0,
durationsCount: 0,
affectedCount: 0,
editorHeight: 200
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace;
}
},
created () {
this.query = this.tab.content;
window.addEventListener('keydown', this.onKey);
},
mounted () {
@@ -107,6 +123,9 @@ export default {
window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize);
});
if (this.tab.autorun)
this.runQuery(this.query);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
@@ -133,6 +152,7 @@ export default {
if (status === 'success') {
this.results = Array.isArray(response) ? response : [response];
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
this.durationsCount += this.results.reduce((acc, curr) => acc + curr.duration, 0);
this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0);
}
else
@@ -151,6 +171,7 @@ export default {
clearTabData () {
this.results = [];
this.resultsCount = 0;
this.durationsCount = 0;
this.affectedCount = 0;
},
resize (e) {
@@ -169,9 +190,9 @@ export default {
this.$refs.queryEditor.editor.resize();
},
onKey (e) {
if (this.isSelected) {
if (this.isSelected && this.isWorkspaceSelected) {
e.stopPropagation();
if (e.key === 'F9')
if (e.key === 'F5')
this.runQuery(this.query);
}
}

View File

@@ -9,6 +9,7 @@
:context-event="contextEvent"
:selected-rows="selectedRows"
@delete-selected="deleteSelected"
@set-null="setNull"
@close-context="isContext = false"
/>
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
@@ -41,7 +42,7 @@
/>
<span>{{ field.alias || field.name }}</span>
<i
v-if="currentSort === field.name || currentSort === `${field.table}.${field.name}`"
v-if="isSortable && currentSort === field.name || currentSort === `${field.table}.${field.name}`"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
@@ -64,9 +65,8 @@
v-for="row in items"
:key="row._id"
:row="row"
:fields="fields"
:fields="fieldsObj"
:key-usage="keyUsage"
class="tr"
:class="{'selected': selectedRows.includes(row._id)}"
@select-row="selectRow($event, row._id)"
@update-field="updateField($event, row)"
@@ -96,9 +96,9 @@ export default {
},
props: {
results: Array,
tabUid: [String, Number],
connUid: String,
mode: String
mode: String,
isSelected: Boolean
},
data () {
return {
@@ -124,6 +124,9 @@ export default {
primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
},
isSortable () {
return this.fields.every(field => field.name);
},
isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000;
},
@@ -150,6 +153,37 @@ export default {
},
keyUsage () {
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : [];
},
fieldsObj () {
if (this.sortedResults.length) {
const fieldsObj = {};
for (const key in this.sortedResults[0]) {
if (key === '_id') continue;
const fieldObj = this.fields.find(field => {
let fieldNames = [
field.name,
field.alias,
`${field.table}.${field.name}`,
`${field.table}.${field.alias}`,
`${field.tableAlias}.${field.name}`,
`${field.tableAlias}.${field.alias}`
];
if (field.table)
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
if (field.tableAlias)
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
return fieldNames.includes(key);
});
fieldsObj[key] = fieldObj;
}
return fieldsObj;
}
return {};
}
},
watch: {
@@ -240,7 +274,7 @@ export default {
: [];
},
resizeResults () {
if (this.$refs.resultTable) {
if (this.$refs.resultTable && this.isSelected) {
const el = this.$refs.tableWrapper;
if (el) {
@@ -281,6 +315,21 @@ export default {
};
this.$emit('delete-selected', params);
},
setNull () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id));
delete row._id;
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(row),
row,
field: this.selectedCell.field,
content: null
};
this.$emit('update-field', params);
},
applyUpdate (params) {
const { primary, id, field, table, content } = params;
@@ -321,6 +370,8 @@ export default {
this.selectedRows = [row];
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.selectedCell = cell;
if (!this.selectedRows.includes(cell.id))
this.selectedRows = [cell.id];
@@ -328,6 +379,8 @@ export default {
this.isContext = true;
},
sort (field) {
if (!this.isSortable) return;
if (this.mode === 'query')
field = `${this.getTable(this.resultsetIndex)}.${field}`;

View File

@@ -3,8 +3,19 @@
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedRows.length === 1"
class="context-element"
@click="setNull"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-null text-light pr-1" /> {{ $t('message.setNull') }}
</span>
</div>
<div class="context-element" @click="showConfirmModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}</span>
<span class="d-flex">
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</span>
</div>
<ConfirmModal
@@ -60,6 +71,10 @@ export default {
deleteRows () {
this.$emit('delete-selected');
this.closeContext();
},
setNull () {
this.$emit('set-null');
this.closeContext();
}
}
};

View File

@@ -6,15 +6,15 @@
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${getFieldType(cKey)}`"
:class="`${isNull(col)} type-${fields[cKey].type.toLowerCase()}`"
@dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span>
>{{ col | typeFormat(fields[cKey].type.toLowerCase(), fields[cKey].length) | cutText }}</span>
<ForeignKeySelect
v-else-if="isForeignKey(cKey)"
class="editable-field"
@@ -61,14 +61,52 @@
<div :slot="'body'">
<div class="mb-2">
<div>
<textarea
v-model="editingContent"
class="form-input textarea-editor"
<TextEditor
:value.sync="editingContent"
editor-class="textarea-editor"
:mode="editorMode"
/>
</div>
<div class="editor-field-info">
<div><b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
<div class="editor-field-info p-vcentered">
<div class="d-flex p-vcentered">
<label for="editorMode" class="form-label mr-2">
<b>{{ $t('word.content') }}</b>:
</label>
<select
id="editorMode"
v-model="editorMode"
class="form-select select-sm"
>
<option value="text">
TEXT
</option>
<option value="html">
HTML
</option>
<option value="xml">
XML
</option>
<option value="json">
JSON
</option>
<option value="svg">
SVG
</option>
<option value="yaml">
YAML
</option>
</select>
</div>
<div class="d-flex">
<div class="p-vcentered">
<div class="mr-4">
<b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}
</div>
<div>
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -136,18 +174,20 @@ import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'WorkspaceQueryTableRow',
components: {
ConfirmModal,
TextEditor,
ForeignKeySelect
},
directives: {
mask
mask: VueMaskDirective
},
filters: {
formatBytes,
@@ -190,10 +230,7 @@ export default {
},
props: {
row: Object,
fields: {
type: Array,
default: () => []
},
fields: Object,
keyUsage: Array
},
data () {
@@ -206,6 +243,7 @@ export default {
editingContent: null,
editingType: null,
editingField: null,
editorMode: 'text',
contentInfo: {
ext: '',
mime: '',
@@ -224,7 +262,7 @@ export default {
if (TIME.includes(this.editingType)) {
let timeMask = '##:##:##';
const precision = this.getFieldPrecision(this.editingField);
const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
@@ -237,7 +275,7 @@ export default {
if (DATETIME.includes(this.editingType)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.getFieldPrecision(this.editingField);
const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
@@ -261,15 +299,15 @@ export default {
},
isEditable () {
if (this.fields) {
const nElements = this.fields.reduce((acc, curr) => {
acc.add(curr.table);
acc.add(curr.schema);
const nElements = Object.keys(this.fields).reduce((acc, curr) => {
acc.add(this.fields[curr].table);
acc.add(this.fields[curr].schema);
return acc;
}, new Set([]));
if (nElements.size > 2) return false;
return !!(this.fields[0].schema && this.fields[0].table);
return !!(this.fields[Object.keys(this.fields)[0]].schema && this.fields[Object.keys(this.fields)[0]].table);
}
return false;
@@ -277,7 +315,7 @@ export default {
},
watch: {
fields () {
this.fields.forEach(field => {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
@@ -289,42 +327,6 @@ export default {
return this.foreignKeys.includes(key);
},
getFieldType (cKey) {
let type = 'unknown';
const field = this.getFieldObj(cKey);
if (field)
type = field.type;
return type.toLowerCase();
},
getFieldPrecision (cKey) {
let length = 0;
const field = this.getFieldObj(cKey);
if (field)
length = field.datePrecision;
return length;
},
getFieldObj (cKey) {
return this.fields.filter(field => {
let fieldNames = [
field.name,
field.alias,
`${field.table}.${field.name}`,
`${field.table}.${field.alias}`,
`${field.tableAlias}.${field.name}`,
`${field.tableAlias}.${field.alias}`
];
if (field.table)
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
if (field.tableAlias)
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
return fieldNames.includes(cKey);
})[0];
},
isNull (value) {
return value === null ? ' is-null' : '';
},
@@ -334,7 +336,9 @@ export default {
editON (event, content, field) {
if (!this.isEditable) return;
const type = this.getFieldType(field).toUpperCase(); ;
window.addEventListener('keydown', this.onKey);
const type = this.fields[field].type.toUpperCase(); ;
this.originalContent = content;
this.editingType = type;
this.editingField = field;
@@ -367,7 +371,7 @@ export default {
}
// Inline editable fields
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field));
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.fields[field].length);
this.$nextTick(() => { // Focus on input
event.target.blur();
@@ -378,6 +382,8 @@ export default {
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
},
editOFF () {
if (!this.editingField) return;
this.isInlineEditor[this.editingField] = false;
let content;
if (!BLOB.includes(this.editingType)) {
@@ -396,13 +402,14 @@ export default {
}
this.$emit('update-field', {
field: this.getFieldObj(this.editingField).name,
field: this.fields[this.editingField].name,
type: this.editingType,
content
});
this.editingType = null;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
},
hideEditorModal () {
this.isTextareaEditor = false;
@@ -434,9 +441,6 @@ export default {
};
this.willBeDeleted = true;
},
contextMenu (event, cell) {
this.$emit('update-field', event, cell);
},
selectRow (event, row) {
this.$emit('select-row', event, row);
},
@@ -444,6 +448,20 @@ export default {
if (keyName.includes('.'))
return this.keyUsage.find(key => key.field === keyName.split('.').pop());
return this.keyUsage.find(key => key.field === keyName);
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
}
}
};
@@ -473,7 +491,7 @@ export default {
}
.editor-field-info {
margin-top: 0.6rem;
margin-top: 0.4rem;
display: flex;
justify-content: space-between;
white-space: normal;

View File

@@ -36,6 +36,7 @@
<button
v-if="isTable"
class="btn btn-dark btn-sm"
:disabled="isQuering"
@click="showFakerModal"
>
<span>{{ $t('message.tableFiller') }}</span>
@@ -63,6 +64,13 @@
</div>
</div>
<div class="workspace-query-info">
<div
v-if="results.length"
class="d-flex"
:title="$t('message.queryDuration')"
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ results[0].duration / 1000 }}s</b>
</div>
<div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
</div>
@@ -82,6 +90,7 @@
:results="results"
:tab-uid="tabUid"
:conn-uid="connection.uid"
:is-selected="isSelected"
mode="table"
@update-field="updateField"
@delete-selected="deleteSelected"
@@ -142,13 +151,14 @@ export default {
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'data';
return this.workspace.selected_tab === 'data' && this.workspace.uid === this.selectedWorkspace;
},
isTable () {
return !!this.workspace.breadcrumbs.table;
@@ -242,6 +252,7 @@ export default {
this.isAddModal = false;
},
showFakerModal () {
if (this.isQuering) return;
this.isFakerModal = true;
},
hideFakerModal () {

View File

@@ -93,7 +93,14 @@ module.exports = {
ciphers: 'Ciphers',
upload: 'Upload',
browse: 'Browse',
faker: 'Faker'
faker: 'Faker',
content: 'Content',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
tools: 'Tools',
variables: 'Variables',
processes: 'Processes'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -184,7 +191,15 @@ module.exports = {
enableSsl: 'Enable SSL',
manualValue: 'Manual value',
tableFiller: 'Table Filler',
fakeDataLanguage: 'Fake data language'
fakeDataLanguage: 'Fake data language',
searchForElements: 'Search for elements',
selectAll: 'Select all',
queryDuration: 'Query duration',
includeBetaUpdates: 'Include beta updates',
setNull: 'Set NULL',
processesList: 'Processes list',
processInfo: 'Process info',
manageUsers: 'Manage users'
},
faker: {
address: 'Address',

View File

@@ -38,6 +38,10 @@ export default class {
return ipcRenderer.invoke('get-version', uid);
}
static getProcesses (uid) {
return ipcRenderer.invoke('get-processes', uid);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}

View File

@@ -30,6 +30,7 @@
"double": $number-color,
"decimal": $number-color,
"bigint": $number-color,
"newdecimal": $number-color,
"datetime": $date-color,
"date": $date-color,
"time": $date-color,
@@ -41,6 +42,7 @@
"blob": $blob-color,
"tinyblob": $blob-color,
"mediumblob": $blob-color,
"medium_blob": $blob-color,
"longblob": $blob-color,
"enum": $enum-color,
"set": $enum-color,

View File

@@ -96,6 +96,10 @@ body {
}
}
.process-row .td:last-child {
width: 100%;
}
// Scrollbars
::-webkit-scrollbar {
width: 10px;

View File

@@ -11,12 +11,14 @@ export default {
selected_setting_tab: 'general',
selected_conection: {},
update_status: 'noupdate', // noupdate, available, checking, nocheck, downloading, downloaded
download_progress: 0
download_progress: 0,
base_completer: [] // Needed to reset ace editor, due global-only ace completer
},
getters: {
isLoading: state => state.is_loading,
appName: state => state.app_name,
appVersion: state => state.app_version,
getBaseCompleter: state => state.base_completer,
getSelectedConnection: state => state.selected_conection,
isNewModal: state => state.is_new_modal,
isSettingModal: state => state.is_setting_modal,
@@ -28,6 +30,9 @@ export default {
SET_LOADING_STATUS (state, payload) {
state.is_loading = payload;
},
SET_BASE_COMPLETER (state, payload) {
state.base_completer = payload;
},
SHOW_NEW_CONNECTION_MODAL (state) {
state.is_new_modal = true;
},
@@ -52,6 +57,9 @@ export default {
setLoadingStatus ({ commit }, payload) {
commit('SET_LOADING_STATUS', payload);
},
setBaseCompleter ({ commit }, payload) {
commit('SET_BASE_COMPLETER', payload);
},
// Modals
showNewConnModal ({ commit }) {
commit('SHOW_NEW_CONNECTION_MODAL');

View File

@@ -12,7 +12,7 @@ export default {
namespaced: true,
strict: true,
state: {
connections: persistentStore.get('connections') || []
connections: persistentStore.get('connections', [])
},
getters: {
getConnections: state => state.connections,

View File

@@ -7,16 +7,18 @@ export default {
namespaced: true,
strict: true,
state: {
locale: persistentStore.get('locale') || 'en-US',
explorebar_size: persistentStore.get('explorebar_size') || null,
notifications_timeout: persistentStore.get('notifications_timeout') || 5,
auto_complete: persistentStore.get('auto_complete') || true,
line_wrap: persistentStore.get('line_wrap') || true,
application_theme: persistentStore.get('application_theme') || 'dark',
editor_theme: persistentStore.get('editor_theme') || 'twilight'
locale: persistentStore.get('locale', 'en-US'),
allow_prerelease: persistentStore.get('allow_prerelease', true),
explorebar_size: persistentStore.get('explorebar_size', null),
notifications_timeout: persistentStore.get('notifications_timeout', 5),
auto_complete: persistentStore.get('auto_complete', true),
line_wrap: persistentStore.get('line_wrap', true),
application_theme: persistentStore.get('application_theme', 'dark'),
editor_theme: persistentStore.get('editor_theme', 'twilight')
},
getters: {
getLocale: state => state.locale,
getAllowPrerelease: state => state.allow_prerelease,
getExplorebarSize: state => state.explorebar_size,
getNotificationsTimeout: state => state.notifications_timeout,
getAutoComplete: state => state.auto_complete,
@@ -30,6 +32,10 @@ export default {
i18n.locale = locale;
persistentStore.set('locale', state.locale);
},
SET_ALLOW_PRERELEASE (state, allow) {
state.allow_prerelease = allow;
persistentStore.set('allow_prerelease', state.allow_prerelease);
},
SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
state.notifications_timeout = timeout;
persistentStore.set('notifications_timeout', state.notifications_timeout);
@@ -54,6 +60,9 @@ export default {
changeLocale ({ commit }, locale) {
commit('SET_LOCALE', locale);
},
changeAllowPrerelease ({ commit }, allow) {
commit('SET_ALLOW_PRERELEASE', allow);
},
updateNotificationsTimeout ({ commit }, timeout) {
commit('SET_NOTIFICATIONS_TIMEOUT', timeout);
},

View File

@@ -43,6 +43,9 @@ export default {
getLoadedSchemas: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).loaded_schemas;
},
getSearchTerm: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).search_term;
},
isUnsavedDiscardModal: state => {
return state.is_unsaved_discard_modal;
}
@@ -141,7 +144,15 @@ export default {
}
: workspace);
},
NEW_TAB (state, { uid, tab }) {
SET_SEARCH_TERM (state, { uid, term }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
search_term: term
}
: workspace);
},
NEW_TAB (state, { uid, tab, content, autorun }) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
const newTab = {
uid: tab,
@@ -149,7 +160,9 @@ export default {
selected: false,
type: 'query',
fields: [],
keyUsage: []
keyUsage: [],
content: content || '',
autorun: !!autorun
};
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
@@ -368,6 +381,7 @@ export default {
uid,
connected: false,
selected_tab: 0,
search_term: '',
tabs: [],
structure: {},
variables: [],
@@ -380,7 +394,7 @@ export default {
commit('ADD_WORKSPACE', workspace);
if (getters.getWorkspace(uid).tabs.length < 3)
dispatch('newTab', uid);
dispatch('newTab', { uid });
dispatch('setUnsavedChanges', false);
},
@@ -415,10 +429,13 @@ export default {
if (payload.schema)
commit('ADD_LOADED_SCHEMA', { uid: getters.getSelected, schema: payload.schema });
},
newTab ({ commit }, uid) {
setSearchTerm ({ commit, getters }, term) {
commit('SET_SEARCH_TERM', { uid: getters.getSelected, term });
},
newTab ({ commit }, { uid, content, autorun }) {
const tab = uidGen('T');
commit('NEW_TAB', { uid, tab });
commit('NEW_TAB', { uid, tab, content, autorun });
commit('SELECT_TAB', { uid, tab });
},
removeTab ({ commit }, payload) {