mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
1df57dc705 | |||
86240fb53c | |||
1d363f755e | |||
0d77aee3eb | |||
a41cf1ab56 | |||
5ceddb8e00 | |||
16e17b39b6 | |||
20cba6ee9b | |||
9ffd443a66 | |||
f82dbd24dc | |||
6eb2977568 | |||
cafb65560a | |||
532d963019 |
52
.travis.yml
52
.travis.yml
@@ -1,52 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
node_js: 12
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- node_modules
|
|
||||||
- app/node_modules
|
|
||||||
- $HOME/.cache/electron
|
|
||||||
- $HOME/.cache/electron-builder
|
|
||||||
- $HOME/.npm/_prebuilds
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- ELECTRON_CACHE=$HOME/.cache/electron
|
|
||||||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: Test
|
|
||||||
before_install:
|
|
||||||
- sudo apt-get install libsecret-1-dev
|
|
||||||
- npm install
|
|
||||||
script:
|
|
||||||
- npm test
|
|
||||||
|
|
||||||
- stage: Deploy Linux & Windows
|
|
||||||
if: tag IS present
|
|
||||||
os: linux
|
|
||||||
services: docker
|
|
||||||
before_install:
|
|
||||||
- sudo apt-get install libsecret-1-dev
|
|
||||||
- npm install
|
|
||||||
script:
|
|
||||||
- docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always"
|
|
||||||
before_cache:
|
|
||||||
- rm -rf $HOME/.cache/electron-builder/wine
|
|
||||||
|
|
||||||
- stage: Deploy Mac
|
|
||||||
if: tag IS present
|
|
||||||
os: osx
|
|
||||||
before_install:
|
|
||||||
- npm install
|
|
||||||
osx_image: xcode10.2
|
|
||||||
script:
|
|
||||||
- npm run build -- -p always
|
|
||||||
|
|
||||||
# - stage: Deploy ARM Linux
|
|
||||||
# if: tag IS present
|
|
||||||
# os: linux
|
|
||||||
# arch: arm64
|
|
||||||
# script:
|
|
||||||
# - npm run build -- --linux AppImage -p always
|
|
22
CHANGELOG.md
22
CHANGELOG.md
@@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
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.
|
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.1.4](https://github.com/Fabio286/antares/compare/v0.1.3...v0.1.4) (2021-04-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* query results export ([1d363f7](https://github.com/Fabio286/antares/commit/1d363f755e025d0fc6fec61cbd47ff87a8f25728))
|
||||||
|
* **UI:** canc press to delete selected rows ([20cba6e](https://github.com/Fabio286/antares/commit/20cba6ee9bc1daa902b04d6e2ddcb31d04fbf805))
|
||||||
|
* **UI:** ctrl+s shortcut to save changes ([16e17b3](https://github.com/Fabio286/antares/commit/16e17b39b6c8b561cc018d02afee2276190ce304))
|
||||||
|
* **UI:** format and clear queries ([9ffd443](https://github.com/Fabio286/antares/commit/9ffd443a66303f88fc4529896f6d1d7917454f7a))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* launch from shortcut of procedures or functions with parameters without name dont works ([f82dbd2](https://github.com/Fabio286/antares/commit/f82dbd24dcef7b4d8d127a604e256b3f79a6c617))
|
||||||
|
* wrong changelog in some cases ([a41cf1a](https://github.com/Fabio286/antares/commit/a41cf1ab5662f5f5fdedff4a9e1c626c23071377))
|
||||||
|
* **UI:** data type not listed in selection if not present in global types ([6eb2977](https://github.com/Fabio286/antares/commit/6eb2977568987b9440b62ae7dbd7183338bfcc9b))
|
||||||
|
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
* **UI:** improved connection status indicator ([5ceddb8](https://github.com/Fabio286/antares/commit/5ceddb8e00f3bc1984b8e47de270dc39b367903f))
|
||||||
|
|
||||||
### [0.1.3](https://github.com/Fabio286/antares/compare/v0.1.2...v0.1.3) (2021-04-17)
|
### [0.1.3](https://github.com/Fabio286/antares/compare/v0.1.2...v0.1.3) (2021-04-17)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
# Antares SQL Client
|
# Antares SQL Client
|
||||||
|
|
||||||
 [](https://travis-ci.com/Fabio286/antares)   [](https://snapcraft.io/antares) [](https://snapcraft.io/antares)
|
   [](https://snapcraft.io/antares) [](https://snapcraft.io/antares) [](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
|
||||||
|
|
||||||
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
|
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
|
||||||
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
|
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "antares",
|
"name": "antares",
|
||||||
"productName": "Antares",
|
"productName": "Antares",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"description": "A cross-platform easy to use SQL client.",
|
"description": "A cross-platform easy to use SQL client.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "https://github.com/Fabio286/antares.git",
|
"repository": "https://github.com/Fabio286/antares.git",
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
"electron-store": "^7.0.0",
|
"electron-store": "^7.0.0",
|
||||||
"electron-updater": "^4.3.5",
|
"electron-updater": "^4.3.5",
|
||||||
"faker": "^5.3.1",
|
"faker": "^5.3.1",
|
||||||
"keytar": "^7.3.0",
|
|
||||||
"marked": "^2.0.2",
|
"marked": "^2.0.2",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"mssql": "^6.2.3",
|
"mssql": "^6.2.3",
|
||||||
@@ -83,6 +82,7 @@
|
|||||||
"pgsql-ast-parser": "^7.0.2",
|
"pgsql-ast-parser": "^7.0.2",
|
||||||
"source-map-support": "^0.5.16",
|
"source-map-support": "^0.5.16",
|
||||||
"spectre.css": "^0.5.9",
|
"spectre.css": "^0.5.9",
|
||||||
|
"sql-formatter": "^4.0.2",
|
||||||
"v-mask": "^2.2.4",
|
"v-mask": "^2.2.4",
|
||||||
"vue-i18n": "^8.22.4",
|
"vue-i18n": "^8.22.4",
|
||||||
"vuedraggable": "^2.24.3",
|
"vuedraggable": "^2.24.3",
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
"node-sass": "^5.0.0",
|
"node-sass": "^5.0.0",
|
||||||
"sass-loader": "^10.1.1",
|
"sass-loader": "^10.1.1",
|
||||||
"standard-version": "^9.2.0",
|
"standard-version": "^9.2.0",
|
||||||
"stylelint": "^13.9.0",
|
"stylelint": "^13.12.0",
|
||||||
"stylelint-config-standard": "^21.0.0",
|
"stylelint-config-standard": "^21.0.0",
|
||||||
"stylelint-scss": "^3.19.0",
|
"stylelint-scss": "^3.19.0",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
|
@@ -16,6 +16,7 @@ module.exports = {
|
|||||||
tables: false,
|
tables: false,
|
||||||
views: false,
|
views: false,
|
||||||
triggers: false,
|
triggers: false,
|
||||||
|
triggerFunctions: false,
|
||||||
routines: false,
|
routines: false,
|
||||||
functions: false,
|
functions: false,
|
||||||
schedulers: false,
|
schedulers: false,
|
||||||
@@ -23,6 +24,7 @@ module.exports = {
|
|||||||
tableAdd: false,
|
tableAdd: false,
|
||||||
viewAdd: false,
|
viewAdd: false,
|
||||||
triggerAdd: false,
|
triggerAdd: false,
|
||||||
|
triggerFunctionAdd: false,
|
||||||
routineAdd: false,
|
routineAdd: false,
|
||||||
functionAdd: false,
|
functionAdd: false,
|
||||||
schedulerAdd: false,
|
schedulerAdd: false,
|
||||||
@@ -31,6 +33,7 @@ module.exports = {
|
|||||||
tableSettings: false,
|
tableSettings: false,
|
||||||
viewSettings: false,
|
viewSettings: false,
|
||||||
triggerSettings: false,
|
triggerSettings: false,
|
||||||
|
triggerFunctionSettings: false,
|
||||||
routineSettings: false,
|
routineSettings: false,
|
||||||
functionSettings: false,
|
functionSettings: false,
|
||||||
schedulerSettings: false,
|
schedulerSettings: false,
|
||||||
@@ -59,6 +62,8 @@ module.exports = {
|
|||||||
functionSql: false,
|
functionSql: false,
|
||||||
functionContext: false,
|
functionContext: false,
|
||||||
functionLanguage: false,
|
functionLanguage: false,
|
||||||
|
triggerMiltipleEvents: false,
|
||||||
|
triggerUpdateColumns: false,
|
||||||
parametersLength: false,
|
parametersLength: false,
|
||||||
languages: false
|
languages: false
|
||||||
};
|
};
|
||||||
|
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import { app, BrowserWindow, nativeImage } from 'electron';
|
import { app, BrowserWindow, nativeImage } from 'electron';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { format as formatUrl } from 'url';
|
import { format as formatUrl } from 'url';
|
||||||
import keytar from 'keytar';
|
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
|
||||||
import ipcHandlers from './ipc-handlers';
|
import ipcHandlers from './ipc-handlers';
|
||||||
@@ -96,18 +94,6 @@ else {
|
|||||||
|
|
||||||
// create main BrowserWindow when electron is ready
|
// create main BrowserWindow when electron is ready
|
||||||
app.on('ready', async () => {
|
app.on('ready', async () => {
|
||||||
try {
|
|
||||||
let key = await keytar.getPassword('antares', 'user');
|
|
||||||
|
|
||||||
if (!key) {
|
|
||||||
key = crypto.randomBytes(16).toString('hex');
|
|
||||||
keytar.setPassword('antares', 'user', key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow = createMainWindow();
|
mainWindow = createMainWindow();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import keytar from 'keytar';
|
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
@@ -7,14 +6,7 @@ export default () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('get-key', async event => {
|
ipcMain.on('get-key', async event => {
|
||||||
let key = false;
|
const key = false;
|
||||||
|
|
||||||
try {
|
|
||||||
key = await keytar.getPassword('antares', 'user');
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
event.returnValue = key;
|
event.returnValue = key;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -77,6 +77,7 @@ export class PostgreSQLClient extends AntaresCore {
|
|||||||
*/
|
*/
|
||||||
use (schema) {
|
use (schema) {
|
||||||
this._schema = schema;
|
this._schema = schema;
|
||||||
|
if (schema)
|
||||||
return this.raw(`SET search_path TO ${schema}`);
|
return this.raw(`SET search_path TO ${schema}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,12 +187,14 @@ export class PostgreSQLClient extends AntaresCore {
|
|||||||
// TRIGGERS
|
// TRIGGERS
|
||||||
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.database).map(trigger => {
|
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.database).map(trigger => {
|
||||||
return {
|
return {
|
||||||
name: trigger.trigger_name,
|
name: `${trigger.table_name}.${trigger.trigger_name}`,
|
||||||
|
orgName: trigger.trigger_name,
|
||||||
timing: trigger.activation,
|
timing: trigger.activation,
|
||||||
definer: trigger.definition, // ???
|
definer: '',
|
||||||
|
definition: trigger.definition,
|
||||||
event: trigger.event,
|
event: trigger.event,
|
||||||
table: trigger.table_trigger,
|
table: trigger.table_name,
|
||||||
sqlMode: trigger.sql_mode
|
sqlMode: ''
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -274,7 +277,7 @@ export class PostgreSQLClient extends AntaresCore {
|
|||||||
*/
|
*/
|
||||||
async getTableIndexes ({ schema, table }) {
|
async getTableIndexes ({ schema, table }) {
|
||||||
if (schema !== 'public')
|
if (schema !== 'public')
|
||||||
this.use(schema);
|
await this.use(schema);
|
||||||
|
|
||||||
const { rows } = await this.raw(`WITH ndx_list AS (
|
const { rows } = await this.raw(`WITH ndx_list AS (
|
||||||
SELECT pg_index.indexrelid, pg_class.oid
|
SELECT pg_index.indexrelid, pg_class.oid
|
||||||
@@ -515,7 +518,8 @@ export class PostgreSQLClient extends AntaresCore {
|
|||||||
* @memberof PostgreSQLClient
|
* @memberof PostgreSQLClient
|
||||||
*/
|
*/
|
||||||
async dropTrigger (params) {
|
async dropTrigger (params) {
|
||||||
const sql = `DROP TRIGGER \`${params.trigger}\``;
|
const triggerParts = params.trigger.split('.');
|
||||||
|
const sql = `DROP TRIGGER ${triggerParts[1]} ON ${triggerParts[0]}`;
|
||||||
return await this.raw(sql);
|
return await this.raw(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,7 +674,7 @@ export class PostgreSQLClient extends AntaresCore {
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (this._schema !== 'public')
|
if (this._schema !== 'public')
|
||||||
this.use(this._schema);
|
await this.use(this._schema);
|
||||||
|
|
||||||
const sql = `CREATE PROCEDURE ${this._schema}.${routine.name}(${parameters})
|
const sql = `CREATE PROCEDURE ${this._schema}.${routine.name}(${parameters})
|
||||||
LANGUAGE ${routine.language}
|
LANGUAGE ${routine.language}
|
||||||
@@ -799,7 +803,7 @@ export class PostgreSQLClient extends AntaresCore {
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (this._schema !== 'public')
|
if (this._schema !== 'public')
|
||||||
this.use(this._schema);
|
await this.use(this._schema);
|
||||||
|
|
||||||
const body = func.returns ? func.sql : '$BODY$\n$BODY$';
|
const body = func.returns ? func.sql : '$BODY$\n$BODY$';
|
||||||
|
|
||||||
@@ -1018,7 +1022,7 @@ export class PostgreSQLClient extends AntaresCore {
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (this._schema !== 'public')
|
if (this._schema !== 'public')
|
||||||
this.use(this._schema);
|
await this.use(this._schema);
|
||||||
|
|
||||||
let sql = '';
|
let sql = '';
|
||||||
const alterColumns = [];
|
const alterColumns = [];
|
||||||
@@ -1247,7 +1251,7 @@ export class PostgreSQLClient extends AntaresCore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (args.nest && this._schema !== 'public')
|
if (args.nest && this._schema !== 'public')
|
||||||
this.use(this._schema);
|
await this.use(this._schema);
|
||||||
|
|
||||||
const resultsArr = [];
|
const resultsArr = [];
|
||||||
let paramsArr = [];
|
let paramsArr = [];
|
||||||
|
@@ -26,7 +26,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
:ref="i === 0 ? 'firstInput' : ''"
|
:ref="i === 0 ? 'firstInput' : ''"
|
||||||
v-model="values[parameter.name]"
|
v-model="values[`${i}-${parameter.name}`]"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
>
|
||||||
@@ -88,7 +88,7 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
runRoutine () {
|
runRoutine () {
|
||||||
const valArr = Object.keys(this.values).reduce((acc, curr) => {
|
const valArr = Object.keys(this.values).reduce((acc, curr, i) => {
|
||||||
let qc;
|
let qc;
|
||||||
switch (this.client) {
|
switch (this.client) {
|
||||||
case 'maria':
|
case 'maria':
|
||||||
@@ -102,7 +102,7 @@ export default {
|
|||||||
qc = '"';
|
qc = '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
const param = this.localRoutine.parameters.find(param => param.name === curr);
|
const param = this.localRoutine.parameters.find(param => `${i}-${param.name}` === curr);
|
||||||
|
|
||||||
const value = [...NUMBER, ...FLOAT].includes(param.type) ? this.values[curr] : `${qc}${this.values[curr]}${qc}`;
|
const value = [...NUMBER, ...FLOAT].includes(param.type) ? this.values[curr] : `${qc}${this.values[curr]}${qc}`;
|
||||||
acc.push(value);
|
acc.push(value);
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import marked from 'marked';
|
import marked from 'marked';
|
||||||
import BaseLoader from '@/components/BaseLoader';
|
import BaseLoader from '@/components/BaseLoader';
|
||||||
|
|
||||||
@@ -31,13 +32,16 @@ export default {
|
|||||||
isError: false
|
isError: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ appVersion: 'application/appVersion' })
|
||||||
|
},
|
||||||
created () {
|
created () {
|
||||||
this.getChangelog();
|
this.getChangelog();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async getChangelog () {
|
async getChangelog () {
|
||||||
try {
|
try {
|
||||||
const apiRes = await fetch('https://api.github.com/repos/Fabio286/antares/releases/latest', {
|
const apiRes = await fetch(`https://api.github.com/repos/Fabio286/antares/releases/tags/v${this.appVersion}`, {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
@contextmenu.prevent="contextMenu($event, connection)"
|
@contextmenu.prevent="contextMenu($event, connection)"
|
||||||
@mouseover.self="tooltipPosition"
|
@mouseover.self="tooltipPosition"
|
||||||
>
|
>
|
||||||
<i class="settingbar-element-icon dbi" :class="`dbi-${connection.client} ${connected.includes(connection.uid) ? 'badge' : ''}`" />
|
<i class="settingbar-element-icon dbi" :class="`dbi-${connection.client} ${getStatusBadge(connection.uid)}`" />
|
||||||
<span class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span>
|
<span class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span>
|
||||||
</li>
|
</li>
|
||||||
</draggable>
|
</draggable>
|
||||||
@@ -73,7 +73,7 @@ export default {
|
|||||||
...mapGetters({
|
...mapGetters({
|
||||||
getConnections: 'connections/getConnections',
|
getConnections: 'connections/getConnections',
|
||||||
getConnectionName: 'connections/getConnectionName',
|
getConnectionName: 'connections/getConnectionName',
|
||||||
connected: 'workspaces/getConnected',
|
getWorkspace: 'workspaces/getWorkspace',
|
||||||
selectedWorkspace: 'workspaces/getSelected',
|
selectedWorkspace: 'workspaces/getSelected',
|
||||||
updateStatus: 'application/getUpdateStatus'
|
updateStatus: 'application/getUpdateStatus'
|
||||||
}),
|
}),
|
||||||
@@ -109,6 +109,22 @@ export default {
|
|||||||
const el = e.target;
|
const el = e.target;
|
||||||
const fromTop = window.pageYOffset + el.getBoundingClientRect().top - (el.offsetHeight / 4);
|
const fromTop = window.pageYOffset + el.getBoundingClientRect().top - (el.offsetHeight / 4);
|
||||||
el.querySelector('.ex-tooltip-content').style.top = `${fromTop}px`;
|
el.querySelector('.ex-tooltip-content').style.top = `${fromTop}px`;
|
||||||
|
},
|
||||||
|
getStatusBadge (uid) {
|
||||||
|
if (this.getWorkspace(uid)) {
|
||||||
|
const status = this.getWorkspace(uid).connection_status;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
return 'badge badge-connected';
|
||||||
|
case 'connecting':
|
||||||
|
return 'badge badge-connecting';
|
||||||
|
case 'failed':
|
||||||
|
return 'badge badge-failed';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="isSelected" class="workspace column columns col-gapless">
|
<div v-show="isSelected" class="workspace column columns col-gapless">
|
||||||
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
|
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
|
||||||
<div v-if="workspace.connected" class="workspace-tabs column columns col-gapless">
|
<div v-if="workspace.connection_status === 'connected'" class="workspace-tabs column columns col-gapless">
|
||||||
<ul
|
<ul
|
||||||
id="tabWrap"
|
id="tabWrap"
|
||||||
ref="tabWrap"
|
ref="tabWrap"
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<div class="workspace-explorebar-header">
|
<div class="workspace-explorebar-header">
|
||||||
<span class="workspace-explorebar-title">{{ connectionName }}</span>
|
<span class="workspace-explorebar-title">{{ connectionName }}</span>
|
||||||
<span v-if="workspace.connected" class="workspace-explorebar-tools">
|
<span v-if="workspace.connection_status === 'connected'" class="workspace-explorebar-tools">
|
||||||
<i
|
<i
|
||||||
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
|
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
|
||||||
:title="$t('message.createNewSchema')"
|
:title="$t('message.createNewSchema')"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workspace-explorebar-search">
|
<div class="workspace-explorebar-search">
|
||||||
<div v-if="workspace.connected" class="has-icon-right">
|
<div v-if="workspace.connection_status === 'connected'" class="has-icon-right">
|
||||||
<input
|
<input
|
||||||
v-model="searchTerm"
|
v-model="searchTerm"
|
||||||
class="form-input input-sm"
|
class="form-input input-sm"
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WorkspaceConnectPanel
|
<WorkspaceConnectPanel
|
||||||
v-if="!workspace.connected"
|
v-if="workspace.connection_status !== 'connected'"
|
||||||
class="workspace-explorebar-body"
|
class="workspace-explorebar-body"
|
||||||
:connection="connection"
|
:connection="connection"
|
||||||
/>
|
/>
|
||||||
|
@@ -80,6 +80,9 @@
|
|||||||
<option v-if="localOptions.returns === 'VOID'">
|
<option v-if="localOptions.returns === 'VOID'">
|
||||||
VOID
|
VOID
|
||||||
</option>
|
</option>
|
||||||
|
<option v-if="!isInDataTypes">
|
||||||
|
{{ localOptions.returns }}
|
||||||
|
</option>
|
||||||
<optgroup
|
<optgroup
|
||||||
v-for="group in workspace.dataTypes"
|
v-for="group in workspace.dataTypes"
|
||||||
:key="group.group"
|
:key="group.group"
|
||||||
@@ -178,6 +181,16 @@ export default {
|
|||||||
},
|
},
|
||||||
customizations () {
|
customizations () {
|
||||||
return this.workspace.customizations;
|
return this.workspace.customizations;
|
||||||
|
},
|
||||||
|
isInDataTypes () {
|
||||||
|
let typeNames = [];
|
||||||
|
for (const group of this.workspace.dataTypes) {
|
||||||
|
typeNames = group.types.reduce((acc, curr) => {
|
||||||
|
acc.push(curr.name);
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
return typeNames.includes(this.localOptions.returns);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
:disabled="!isChanged"
|
:disabled="!isChanged"
|
||||||
:class="{'loading':isSaving}"
|
:class="{'loading':isSaving}"
|
||||||
|
title="CTRL+S"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
>
|
>
|
||||||
<span>{{ $t('word.save') }}</span>
|
<span>{{ $t('word.save') }}</span>
|
||||||
@@ -150,6 +151,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
getWorkspace: 'workspaces/getWorkspace',
|
getWorkspace: 'workspaces/getWorkspace',
|
||||||
|
selectedWorkspace: 'workspaces/getSelected',
|
||||||
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||||
}),
|
}),
|
||||||
workspace () {
|
workspace () {
|
||||||
@@ -163,7 +165,7 @@ export default {
|
|||||||
return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || '';
|
return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || '';
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return this.workspace.selected_tab === 'prop';
|
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.table;
|
||||||
},
|
},
|
||||||
schema () {
|
schema () {
|
||||||
return this.workspace.breadcrumbs.schema;
|
return this.workspace.breadcrumbs.schema;
|
||||||
@@ -200,6 +202,12 @@ export default {
|
|||||||
this.setUnsavedChanges(val);
|
this.setUnsavedChanges(val);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
addNotification: 'notifications/addNotification',
|
addNotification: 'notifications/addNotification',
|
||||||
@@ -519,6 +527,15 @@ export default {
|
|||||||
},
|
},
|
||||||
foreignsUpdate (foreigns) {
|
foreignsUpdate (foreigns) {
|
||||||
this.localKeyUsage = foreigns;
|
this.localKeyUsage = foreigns;
|
||||||
|
},
|
||||||
|
onKey (e) {
|
||||||
|
if (this.isSelected) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
|
||||||
|
if (this.isChanged)
|
||||||
|
this.saveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
:disabled="!isChanged"
|
:disabled="!isChanged"
|
||||||
:class="{'loading':isSaving}"
|
:class="{'loading':isSaving}"
|
||||||
|
title="CTRL+S"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
>
|
>
|
||||||
<span>{{ $t('word.save') }}</span>
|
<span>{{ $t('word.save') }}</span>
|
||||||
@@ -120,13 +121,14 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
selectedWorkspace: 'workspaces/getSelected',
|
||||||
getWorkspace: 'workspaces/getWorkspace'
|
getWorkspace: 'workspaces/getWorkspace'
|
||||||
}),
|
}),
|
||||||
workspace () {
|
workspace () {
|
||||||
return this.getWorkspace(this.connection.uid);
|
return this.getWorkspace(this.connection.uid);
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return this.workspace.selected_tab === 'prop';
|
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.function;
|
||||||
},
|
},
|
||||||
schema () {
|
schema () {
|
||||||
return this.workspace.breadcrumbs.schema;
|
return this.workspace.breadcrumbs.schema;
|
||||||
@@ -171,6 +173,12 @@ export default {
|
|||||||
destroyed () {
|
destroyed () {
|
||||||
window.removeEventListener('resize', this.resizeQueryEditor);
|
window.removeEventListener('resize', this.resizeQueryEditor);
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
addNotification: 'notifications/addNotification',
|
addNotification: 'notifications/addNotification',
|
||||||
@@ -312,6 +320,15 @@ export default {
|
|||||||
},
|
},
|
||||||
hideAskParamsModal () {
|
hideAskParamsModal () {
|
||||||
this.isAskingParameters = false;
|
this.isAskingParameters = false;
|
||||||
|
},
|
||||||
|
onKey (e) {
|
||||||
|
if (this.isSelected) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
|
||||||
|
if (this.isChanged)
|
||||||
|
this.saveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
:disabled="!isChanged"
|
:disabled="!isChanged"
|
||||||
:class="{'loading':isSaving}"
|
:class="{'loading':isSaving}"
|
||||||
|
title="CTRL+S"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
>
|
>
|
||||||
<span>{{ $t('word.save') }}</span>
|
<span>{{ $t('word.save') }}</span>
|
||||||
@@ -121,13 +122,14 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
selectedWorkspace: 'workspaces/getSelected',
|
||||||
getWorkspace: 'workspaces/getWorkspace'
|
getWorkspace: 'workspaces/getWorkspace'
|
||||||
}),
|
}),
|
||||||
workspace () {
|
workspace () {
|
||||||
return this.getWorkspace(this.connection.uid);
|
return this.getWorkspace(this.connection.uid);
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return this.workspace.selected_tab === 'prop';
|
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.routine;
|
||||||
},
|
},
|
||||||
schema () {
|
schema () {
|
||||||
return this.workspace.breadcrumbs.schema;
|
return this.workspace.breadcrumbs.schema;
|
||||||
@@ -172,6 +174,12 @@ export default {
|
|||||||
destroyed () {
|
destroyed () {
|
||||||
window.removeEventListener('resize', this.resizeQueryEditor);
|
window.removeEventListener('resize', this.resizeQueryEditor);
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
addNotification: 'notifications/addNotification',
|
addNotification: 'notifications/addNotification',
|
||||||
@@ -310,6 +318,15 @@ export default {
|
|||||||
},
|
},
|
||||||
hideAskParamsModal () {
|
hideAskParamsModal () {
|
||||||
this.isAskingParameters = false;
|
this.isAskingParameters = false;
|
||||||
|
},
|
||||||
|
onKey (e) {
|
||||||
|
if (this.isSelected) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
|
||||||
|
if (this.isChanged)
|
||||||
|
this.saveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
:disabled="!isChanged"
|
:disabled="!isChanged"
|
||||||
:class="{'loading':isSaving}"
|
:class="{'loading':isSaving}"
|
||||||
|
title="CTRL+S"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
>
|
>
|
||||||
<span>{{ $t('word.save') }}</span>
|
<span>{{ $t('word.save') }}</span>
|
||||||
@@ -169,13 +170,14 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
selectedWorkspace: 'workspaces/getSelected',
|
||||||
getWorkspace: 'workspaces/getWorkspace'
|
getWorkspace: 'workspaces/getWorkspace'
|
||||||
}),
|
}),
|
||||||
workspace () {
|
workspace () {
|
||||||
return this.getWorkspace(this.connection.uid);
|
return this.getWorkspace(this.connection.uid);
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return this.workspace.selected_tab === 'prop';
|
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.scheduler;
|
||||||
},
|
},
|
||||||
schema () {
|
schema () {
|
||||||
return this.workspace.breadcrumbs.schema;
|
return this.workspace.breadcrumbs.schema;
|
||||||
@@ -220,6 +222,12 @@ export default {
|
|||||||
destroyed () {
|
destroyed () {
|
||||||
window.removeEventListener('resize', this.resizeQueryEditor);
|
window.removeEventListener('resize', this.resizeQueryEditor);
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
addNotification: 'notifications/addNotification',
|
addNotification: 'notifications/addNotification',
|
||||||
@@ -310,6 +318,15 @@ export default {
|
|||||||
},
|
},
|
||||||
timingUpdate (options) {
|
timingUpdate (options) {
|
||||||
this.localScheduler = options;
|
this.localScheduler = options;
|
||||||
|
},
|
||||||
|
onKey (e) {
|
||||||
|
if (this.isSelected) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
|
||||||
|
if (this.isChanged)
|
||||||
|
this.saveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
:disabled="!isChanged"
|
:disabled="!isChanged"
|
||||||
:class="{'loading':isSaving}"
|
:class="{'loading':isSaving}"
|
||||||
|
title="CTRL+S"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
>
|
>
|
||||||
<span>{{ $t('word.save') }}</span>
|
<span>{{ $t('word.save') }}</span>
|
||||||
@@ -140,13 +141,14 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
selectedWorkspace: 'workspaces/getSelected',
|
||||||
getWorkspace: 'workspaces/getWorkspace'
|
getWorkspace: 'workspaces/getWorkspace'
|
||||||
}),
|
}),
|
||||||
workspace () {
|
workspace () {
|
||||||
return this.getWorkspace(this.connection.uid);
|
return this.getWorkspace(this.connection.uid);
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return this.workspace.selected_tab === 'prop';
|
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.trigger;
|
||||||
},
|
},
|
||||||
schema () {
|
schema () {
|
||||||
return this.workspace.breadcrumbs.schema;
|
return this.workspace.breadcrumbs.schema;
|
||||||
@@ -191,6 +193,12 @@ export default {
|
|||||||
destroyed () {
|
destroyed () {
|
||||||
window.removeEventListener('resize', this.resizeQueryEditor);
|
window.removeEventListener('resize', this.resizeQueryEditor);
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
addNotification: 'notifications/addNotification',
|
addNotification: 'notifications/addNotification',
|
||||||
@@ -274,6 +282,15 @@ export default {
|
|||||||
this.editorHeight = size;
|
this.editorHeight = size;
|
||||||
this.$refs.queryEditor.editor.resize();
|
this.$refs.queryEditor.editor.resize();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onKey (e) {
|
||||||
|
if (this.isSelected) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
|
||||||
|
if (this.isChanged)
|
||||||
|
this.saveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
:disabled="!isChanged"
|
:disabled="!isChanged"
|
||||||
:class="{'loading':isSaving}"
|
:class="{'loading':isSaving}"
|
||||||
|
title="CTRL+S"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
>
|
>
|
||||||
<span>{{ $t('word.save') }}</span>
|
<span>{{ $t('word.save') }}</span>
|
||||||
@@ -201,13 +202,14 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
selectedWorkspace: 'workspaces/getSelected',
|
||||||
getWorkspace: 'workspaces/getWorkspace'
|
getWorkspace: 'workspaces/getWorkspace'
|
||||||
}),
|
}),
|
||||||
workspace () {
|
workspace () {
|
||||||
return this.getWorkspace(this.connection.uid);
|
return this.getWorkspace(this.connection.uid);
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return this.workspace.selected_tab === 'prop';
|
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.view;
|
||||||
},
|
},
|
||||||
schema () {
|
schema () {
|
||||||
return this.workspace.breadcrumbs.schema;
|
return this.workspace.breadcrumbs.schema;
|
||||||
@@ -245,6 +247,12 @@ export default {
|
|||||||
destroyed () {
|
destroyed () {
|
||||||
window.removeEventListener('resize', this.resizeQueryEditor);
|
window.removeEventListener('resize', this.resizeQueryEditor);
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('keydown', this.onKey);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
addNotification: 'notifications/addNotification',
|
addNotification: 'notifications/addNotification',
|
||||||
@@ -327,6 +335,15 @@ export default {
|
|||||||
this.editorHeight = size;
|
this.editorHeight = size;
|
||||||
this.$refs.queryEditor.editor.resize();
|
this.$refs.queryEditor.editor.resize();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onKey (e) {
|
||||||
|
if (this.isSelected) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
|
||||||
|
if (this.isChanged)
|
||||||
|
this.saveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -60,6 +60,9 @@
|
|||||||
class="form-select editable-field small-select text-uppercase"
|
class="form-select editable-field small-select text-uppercase"
|
||||||
@blur="editOFF"
|
@blur="editOFF"
|
||||||
>
|
>
|
||||||
|
<option v-if="!isInDataTypes">
|
||||||
|
{{ row.type }}
|
||||||
|
</option>
|
||||||
<optgroup
|
<optgroup
|
||||||
v-for="group in dataTypes"
|
v-for="group in dataTypes"
|
||||||
:key="group.group"
|
:key="group.group"
|
||||||
@@ -68,7 +71,7 @@
|
|||||||
<option
|
<option
|
||||||
v-for="type in group.types"
|
v-for="type in group.types"
|
||||||
:key="type.name"
|
:key="type.name"
|
||||||
:selected="localRow.type.toUpperCase() === type.name"
|
:selected="localRow.type === type.name"
|
||||||
:value="type.name"
|
:value="type.name"
|
||||||
>
|
>
|
||||||
{{ type.name }}
|
{{ type.name }}
|
||||||
@@ -374,6 +377,16 @@ export default {
|
|||||||
},
|
},
|
||||||
isNullable () {
|
isNullable () {
|
||||||
return !this.indexes.some(index => ['PRIMARY'].includes(index.type));
|
return !this.indexes.some(index => ['PRIMARY'].includes(index.type));
|
||||||
|
},
|
||||||
|
isInDataTypes () {
|
||||||
|
let typeNames = [];
|
||||||
|
for (const group of this.dataTypes) {
|
||||||
|
typeNames = group.types.reduce((acc, curr) => {
|
||||||
|
acc.push(curr.name);
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
return typeNames.includes(this.row.type);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
|
<div
|
||||||
|
v-show="isSelected"
|
||||||
|
class="workspace-query-tab column col-12 columns col-gapless no-outline"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown.116="runQuery(query)"
|
||||||
|
@keydown.ctrl.87="clear"
|
||||||
|
@keydown.ctrl.119="beautify"
|
||||||
|
>
|
||||||
<div class="workspace-query-runner column col-12">
|
<div class="workspace-query-runner column col-12">
|
||||||
<QueryEditor
|
<QueryEditor
|
||||||
v-show="isSelected"
|
v-show="isSelected"
|
||||||
@@ -24,6 +31,43 @@
|
|||||||
<span>{{ $t('word.run') }}</span>
|
<span>{{ $t('word.run') }}</span>
|
||||||
<i class="mdi mdi-24px mdi-play" />
|
<i class="mdi mdi-24px mdi-play" />
|
||||||
</button>
|
</button>
|
||||||
|
<div class="dropdown export-dropdown pr-2">
|
||||||
|
<button
|
||||||
|
:disabled="!results.length || isQuering"
|
||||||
|
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span>{{ $t('word.export') }}</span>
|
||||||
|
<i class="mdi mdi-24px mdi-file-export ml-1" />
|
||||||
|
<i class="mdi mdi-24px mdi-menu-down" />
|
||||||
|
</button>
|
||||||
|
<ul class="menu text-left">
|
||||||
|
<li class="menu-item">
|
||||||
|
<a class="c-hand" @click="downloadTable('json')">JSON</a>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item">
|
||||||
|
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-dark btn-sm"
|
||||||
|
:disabled="!query || isQuering"
|
||||||
|
title="CTRL+F8"
|
||||||
|
@click="beautify()"
|
||||||
|
>
|
||||||
|
<span>{{ $t('word.format') }}</span>
|
||||||
|
<i class="mdi mdi-24px mdi-brush pl-1" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-link btn-sm"
|
||||||
|
:disabled="!query || isQuering"
|
||||||
|
title="CTRL+W"
|
||||||
|
@click="clear()"
|
||||||
|
>
|
||||||
|
<span>{{ $t('word.clear') }}</span>
|
||||||
|
<i class="mdi mdi-24px mdi-delete-sweep pl-1" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="workspace-query-info">
|
<div class="workspace-query-info">
|
||||||
<div
|
<div
|
||||||
@@ -68,6 +112,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { format } from 'sql-formatter';
|
||||||
import Schema from '@/ipc-api/Schema';
|
import Schema from '@/ipc-api/Schema';
|
||||||
import QueryEditor from '@/components/QueryEditor';
|
import QueryEditor from '@/components/QueryEditor';
|
||||||
import BaseLoader from '@/components/BaseLoader';
|
import BaseLoader from '@/components/BaseLoader';
|
||||||
@@ -192,12 +237,36 @@ export default {
|
|||||||
if (this.$refs.queryEditor)
|
if (this.$refs.queryEditor)
|
||||||
this.$refs.queryEditor.editor.resize();
|
this.$refs.queryEditor.editor.resize();
|
||||||
},
|
},
|
||||||
onKey (e) {
|
beautify () {
|
||||||
if (this.isSelected && this.isWorkspaceSelected) {
|
if (this.$refs.queryEditor) {
|
||||||
e.stopPropagation();
|
let language = 'sql';
|
||||||
if (e.key === 'F5')
|
|
||||||
this.runQuery(this.query);
|
switch (this.workspace.client) {
|
||||||
|
case 'mysql':
|
||||||
|
language = 'mysql';
|
||||||
|
break;
|
||||||
|
case 'maria':
|
||||||
|
language = 'mariadb';
|
||||||
|
break;
|
||||||
|
case 'pg':
|
||||||
|
language = 'postgresql';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formattedQuery = format(this.query, {
|
||||||
|
language,
|
||||||
|
uppercase: true
|
||||||
|
});
|
||||||
|
this.$refs.queryEditor.editor.session.setValue(formattedQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear () {
|
||||||
|
if (this.$refs.queryEditor)
|
||||||
|
this.$refs.queryEditor.editor.session.setValue('');
|
||||||
|
this.clearTabData();
|
||||||
|
},
|
||||||
|
downloadTable (format) {
|
||||||
|
this.$refs.queryTable.downloadTable(format, `${this.tab.type}-${this.tab.index}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -251,4 +320,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-dropdown {
|
||||||
|
.menu {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="tableWrapper"
|
ref="tableWrapper"
|
||||||
class="vscroll"
|
class="vscroll no-outline"
|
||||||
|
tabindex="0"
|
||||||
:style="{'height': resultsSize+'px'}"
|
:style="{'height': resultsSize+'px'}"
|
||||||
|
@keyup.46="showDeleteConfirmModal"
|
||||||
>
|
>
|
||||||
<TableContext
|
<TableContext
|
||||||
v-if="isContext"
|
v-if="isContext"
|
||||||
:context-event="contextEvent"
|
:context-event="contextEvent"
|
||||||
:selected-rows="selectedRows"
|
:selected-rows="selectedRows"
|
||||||
@delete-selected="deleteSelected"
|
@show-delete-modal="showDeleteConfirmModal"
|
||||||
@set-null="setNull"
|
@set-null="setNull"
|
||||||
@close-context="isContext = false"
|
@close-context="closeContext"
|
||||||
/>
|
/>
|
||||||
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
|
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
|
||||||
<li
|
<li
|
||||||
@@ -75,6 +77,23 @@
|
|||||||
</template>
|
</template>
|
||||||
</BaseVirtualScroll>
|
</BaseVirtualScroll>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
v-if="isDeleteConfirmModal"
|
||||||
|
@confirm="deleteSelected"
|
||||||
|
@hide="hideDeleteConfirmModal"
|
||||||
|
>
|
||||||
|
<template :slot="'header'">
|
||||||
|
<div class="d-flex">
|
||||||
|
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div :slot="'body'">
|
||||||
|
<div class="mb-2">
|
||||||
|
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ConfirmModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -85,6 +104,7 @@ import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
|
|||||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
|
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
|
||||||
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
|
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
|
||||||
import TableContext from '@/components/WorkspaceQueryTableContext';
|
import TableContext from '@/components/WorkspaceQueryTableContext';
|
||||||
|
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
@@ -93,7 +113,8 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
BaseVirtualScroll,
|
BaseVirtualScroll,
|
||||||
WorkspaceQueryTableRow,
|
WorkspaceQueryTableRow,
|
||||||
TableContext
|
TableContext,
|
||||||
|
ConfirmModal
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
results: Array,
|
results: Array,
|
||||||
@@ -106,6 +127,7 @@ export default {
|
|||||||
resultsSize: 1000,
|
resultsSize: 1000,
|
||||||
localResults: [],
|
localResults: [],
|
||||||
isContext: false,
|
isContext: false,
|
||||||
|
isDeleteConfirmModal: false,
|
||||||
contextEvent: null,
|
contextEvent: null,
|
||||||
selectedCell: null,
|
selectedCell: null,
|
||||||
selectedRows: [],
|
selectedRows: [],
|
||||||
@@ -322,7 +344,17 @@ export default {
|
|||||||
};
|
};
|
||||||
this.$emit('update-field', params);
|
this.$emit('update-field', params);
|
||||||
},
|
},
|
||||||
|
closeContext () {
|
||||||
|
this.isContext = false;
|
||||||
|
},
|
||||||
|
showDeleteConfirmModal () {
|
||||||
|
this.isDeleteConfirmModal = true;
|
||||||
|
},
|
||||||
|
hideDeleteConfirmModal () {
|
||||||
|
this.isDeleteConfirmModal = false;
|
||||||
|
},
|
||||||
deleteSelected () {
|
deleteSelected () {
|
||||||
|
this.closeContext();
|
||||||
const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => {
|
const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => {
|
||||||
delete row._id;
|
delete row._id;
|
||||||
return row;
|
return row;
|
||||||
|
@@ -17,61 +17,30 @@
|
|||||||
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
|
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
v-if="isConfirmModal"
|
|
||||||
@confirm="deleteRows"
|
|
||||||
@hide="hideConfirmModal"
|
|
||||||
>
|
|
||||||
<template :slot="'header'">
|
|
||||||
<div class="d-flex">
|
|
||||||
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div :slot="'body'">
|
|
||||||
<div class="mb-2">
|
|
||||||
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ConfirmModal>
|
|
||||||
</BaseContextMenu>
|
</BaseContextMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WorkspaceQueryTableContext',
|
name: 'WorkspaceQueryTableContext',
|
||||||
components: {
|
components: {
|
||||||
BaseContextMenu,
|
BaseContextMenu
|
||||||
ConfirmModal
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
contextEvent: MouseEvent,
|
contextEvent: MouseEvent,
|
||||||
selectedRows: Array
|
selectedRows: Array
|
||||||
},
|
},
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isConfirmModal: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showConfirmModal () {
|
showConfirmModal () {
|
||||||
this.isConfirmModal = true;
|
this.$emit('show-delete-modal');
|
||||||
},
|
|
||||||
hideConfirmModal () {
|
|
||||||
this.isConfirmModal = false;
|
|
||||||
},
|
},
|
||||||
closeContext () {
|
closeContext () {
|
||||||
this.$emit('close-context');
|
this.$emit('close-context');
|
||||||
},
|
},
|
||||||
deleteRows () {
|
|
||||||
this.$emit('delete-selected');
|
|
||||||
this.closeContext();
|
|
||||||
},
|
|
||||||
setNull () {
|
setNull () {
|
||||||
this.$emit('set-null');
|
this.$emit('set-null');
|
||||||
this.closeContext();
|
this.closeContext();
|
||||||
|
@@ -104,7 +104,8 @@ module.exports = {
|
|||||||
database: 'Database',
|
database: 'Database',
|
||||||
scratchpad: 'Scratchpad',
|
scratchpad: 'Scratchpad',
|
||||||
array: 'Array',
|
array: 'Array',
|
||||||
changelog: 'Changelog'
|
changelog: 'Changelog',
|
||||||
|
format: 'Format'
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
appWelcome: 'Welcome to Antares SQL Client!',
|
appWelcome: 'Welcome to Antares SQL Client!',
|
||||||
|
@@ -30,6 +30,12 @@
|
|||||||
animation: jump-down-in 0.2s reverse;
|
animation: jump-down-in 0.2s reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation-name: pulse;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes jump-down-in {
|
@keyframes jump-down-in {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
@@ -39,3 +45,17 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -30,6 +30,10 @@ body {
|
|||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-outline {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.no-border {
|
.no-border {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
@@ -132,6 +136,21 @@ body {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.badge-connected::after {
|
||||||
|
background: $success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-connecting::after {
|
||||||
|
background: $warning-color;
|
||||||
|
animation-name: pulse;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-failed::after {
|
||||||
|
background: $error-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-select {
|
.form-select {
|
||||||
|
@@ -386,7 +386,6 @@
|
|||||||
bottom: -10px;
|
bottom: -10px;
|
||||||
right: 0;
|
right: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: $success-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.badge-update::after {
|
&.badge-update::after {
|
||||||
|
@@ -168,7 +168,6 @@
|
|||||||
bottom: -10px;
|
bottom: -10px;
|
||||||
right: 0;
|
right: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: $success-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.badge-update::after {
|
&.badge-update::after {
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import Application from '../../ipc-api/Application';
|
const key = localStorage.getItem('key');
|
||||||
const key = Application.getKey() || localStorage.getItem('key');
|
|
||||||
|
|
||||||
if (!key)
|
if (!key)
|
||||||
localStorage.setItem('key', crypto.randomBytes(16).toString('hex'));
|
localStorage.setItem('key', crypto.randomBytes(16).toString('hex'));
|
||||||
|
@@ -37,7 +37,7 @@ export default {
|
|||||||
},
|
},
|
||||||
getConnected: state => {
|
getConnected: state => {
|
||||||
return state.workspaces
|
return state.workspaces
|
||||||
.filter(workspace => workspace.connected)
|
.filter(workspace => workspace.connection_status === 'connected')
|
||||||
.map(workspace => workspace.uid);
|
.map(workspace => workspace.uid);
|
||||||
},
|
},
|
||||||
getLoadedSchemas: state => uid => {
|
getLoadedSchemas: state => uid => {
|
||||||
@@ -54,7 +54,7 @@ export default {
|
|||||||
SELECT_WORKSPACE (state, uid) {
|
SELECT_WORKSPACE (state, uid) {
|
||||||
state.selected_workspace = uid;
|
state.selected_workspace = uid;
|
||||||
},
|
},
|
||||||
ADD_CONNECTED (state, payload) {
|
SET_CONNECTED (state, payload) {
|
||||||
const { uid, client, dataTypes, indexTypes, customizations, structure, version } = payload;
|
const { uid, client, dataTypes, indexTypes, customizations, structure, version } = payload;
|
||||||
|
|
||||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||||
@@ -65,19 +65,41 @@ export default {
|
|||||||
indexTypes,
|
indexTypes,
|
||||||
customizations,
|
customizations,
|
||||||
structure,
|
structure,
|
||||||
connected: true,
|
connection_status: 'connected',
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
: workspace);
|
: workspace);
|
||||||
},
|
},
|
||||||
REMOVE_CONNECTED (state, uid) {
|
SET_CONNECTING (state, uid) {
|
||||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||||
? {
|
? {
|
||||||
...workspace,
|
...workspace,
|
||||||
structure: {},
|
structure: {},
|
||||||
breadcrumbs: {},
|
breadcrumbs: {},
|
||||||
loaded_schemas: new Set(),
|
loaded_schemas: new Set(),
|
||||||
connected: false
|
connection_status: 'connecting'
|
||||||
|
}
|
||||||
|
: workspace);
|
||||||
|
},
|
||||||
|
SET_FAILED (state, uid) {
|
||||||
|
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||||
|
? {
|
||||||
|
...workspace,
|
||||||
|
structure: {},
|
||||||
|
breadcrumbs: {},
|
||||||
|
loaded_schemas: new Set(),
|
||||||
|
connection_status: 'failed'
|
||||||
|
}
|
||||||
|
: workspace);
|
||||||
|
},
|
||||||
|
SET_DISCONNECTED (state, uid) {
|
||||||
|
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||||
|
? {
|
||||||
|
...workspace,
|
||||||
|
structure: {},
|
||||||
|
breadcrumbs: {},
|
||||||
|
loaded_schemas: new Set(),
|
||||||
|
connection_status: 'disconnected'
|
||||||
}
|
}
|
||||||
: workspace);
|
: workspace);
|
||||||
},
|
},
|
||||||
@@ -247,10 +269,14 @@ export default {
|
|||||||
commit('SELECT_WORKSPACE', uid);
|
commit('SELECT_WORKSPACE', uid);
|
||||||
},
|
},
|
||||||
async connectWorkspace ({ dispatch, commit }, connection) {
|
async connectWorkspace ({ dispatch, commit }, connection) {
|
||||||
|
commit('SET_CONNECTING', connection.uid);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { status, response } = await Connection.connect(connection);
|
const { status, response } = await Connection.connect(connection);
|
||||||
if (status === 'error')
|
if (status === 'error') {
|
||||||
dispatch('notifications/addNotification', { status, message: response }, { root: true });
|
dispatch('notifications/addNotification', { status, message: response }, { root: true });
|
||||||
|
commit('SET_FAILED', connection.uid);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
let dataTypes = [];
|
let dataTypes = [];
|
||||||
let indexTypes = [];
|
let indexTypes = [];
|
||||||
@@ -288,7 +314,7 @@ export default {
|
|||||||
dispatch('connections/editConnection', connProxy, { root: true });
|
dispatch('connections/editConnection', connProxy, { root: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
commit('ADD_CONNECTED', {
|
commit('SET_CONNECTED', {
|
||||||
uid: connection.uid,
|
uid: connection.uid,
|
||||||
client: connection.client,
|
client: connection.client,
|
||||||
dataTypes,
|
dataTypes,
|
||||||
@@ -382,13 +408,13 @@ export default {
|
|||||||
},
|
},
|
||||||
removeConnected ({ commit }, uid) {
|
removeConnected ({ commit }, uid) {
|
||||||
Connection.disconnect(uid);
|
Connection.disconnect(uid);
|
||||||
commit('REMOVE_CONNECTED', uid);
|
commit('SET_DISCONNECTED', uid);
|
||||||
commit('SELECT_TAB', { uid, tab: 0 });
|
commit('SELECT_TAB', { uid, tab: 0 });
|
||||||
},
|
},
|
||||||
addWorkspace ({ commit, dispatch, getters }, uid) {
|
addWorkspace ({ commit, dispatch, getters }, uid) {
|
||||||
const workspace = {
|
const workspace = {
|
||||||
uid,
|
uid,
|
||||||
connected: false,
|
connection_status: 'disconnected',
|
||||||
selected_tab: 0,
|
selected_tab: 0,
|
||||||
search_term: '',
|
search_term: '',
|
||||||
tabs: [],
|
tabs: [],
|
||||||
|
Reference in New Issue
Block a user