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

Compare commits

..

23 Commits

Author SHA1 Message Date
1df57dc705 chore(release): 0.1.4 2021-04-22 15:18:07 +02:00
86240fb53c refactor(PostgreSQL): preparing code to support triggers 2021-04-22 15:15:08 +02:00
1d363f755e feat: query results export 2021-04-22 15:08:22 +02:00
0d77aee3eb refactor: Improved pulse animation code 2021-04-22 14:24:34 +02:00
a41cf1ab56 fix: wrong changelog in some cases 2021-04-22 11:35:59 +02:00
5ceddb8e00 perf(UI): improved connection status indicator 2021-04-21 16:41:42 +02:00
16e17b39b6 feat(UI): ctrl+s shortcut to save changes 2021-04-20 17:39:15 +02:00
20cba6ee9b feat(UI): canc press to delete selected rows 2021-04-20 16:30:10 +02:00
9ffd443a66 feat(UI): format and clear queries 2021-04-19 19:15:06 +02:00
f82dbd24dc fix: launch from shortcut of procedures or functions with parameters without name dont works 2021-04-19 15:40:25 +02:00
6eb2977568 fix(UI): data type not listed in selection if not present in global types 2021-04-19 11:07:29 +02:00
cafb65560a chore: update README.md 2021-04-17 12:28:55 +02:00
532d963019 chore: update README.md 2021-04-17 11:28:02 +02:00
1b0a63ff31 chore(release): 0.1.3 2021-04-17 10:36:07 +02:00
c22187c305 perf(UI): improved table fields suggestion in query editor 2021-04-17 10:33:15 +02:00
dcccb544f9 fix(MySQL): invalid JavaScript datetime values not shown 2021-04-16 18:48:56 +02:00
7d2ace9456 fix: field apparently loses index or foreign key on rename in table editor 2021-04-16 17:42:16 +02:00
2584c9b9c2 chore: replaced link for donations with Treedom 2021-04-15 14:55:37 +02:00
a6b75ad0dc fix: approximate row count shown for results less than 1000 2021-04-15 10:13:55 +02:00
90fd9db917 perf(MySQL): improved the way to get routine and functions parameters 2021-04-14 18:06:20 +02:00
c0f54b9514 build: update dependencies 2021-04-14 10:42:00 +02:00
cd31413256 feat(PostgreSQL): functions management 2021-04-13 18:05:03 +02:00
b33199ea59 feat(PostgreSQL): procedure language select 2021-04-12 18:46:35 +02:00
47 changed files with 790 additions and 377 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,7 +1,7 @@
# These are supported funding model platforms
github: [fabio286]
patreon: fabio286
patreon: #fabio286
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel

View File

@@ -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

View File

@@ -2,6 +2,49 @@
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)
### Features
* **PostgreSQL:** functions management ([cd31413](https://github.com/Fabio286/antares/commit/cd3141325681ea572c06b8998dd7bd334ceb3236))
* **PostgreSQL:** procedure language select ([b33199e](https://github.com/Fabio286/antares/commit/b33199ea59c60b467601f333857494aa40adf4e8))
### Bug Fixes
* **MySQL:** invalid JavaScript datetime values not shown ([dcccb54](https://github.com/Fabio286/antares/commit/dcccb544f9ec24ad693c9e81fb4bcfbdbb7cc4e1))
* approximate row count shown for results less than 1000 ([a6b75ad](https://github.com/Fabio286/antares/commit/a6b75ad0dc0d884332464c277e8542b2698630b9))
* field apparently loses index or foreign key on rename in table editor ([7d2ace9](https://github.com/Fabio286/antares/commit/7d2ace94562f8da307b15b83c89d919727d800c8))
### Improvements
* **MySQL:** improved the way to get routine and functions parameters ([90fd9db](https://github.com/Fabio286/antares/commit/90fd9db917c40262f2bc2501ab86f5feba3d8db4))
* **UI:** improved table fields suggestion in query editor ([c22187c](https://github.com/Fabio286/antares/commit/c22187c3053aef368a351cc35e2f1d407ecde209))
### [0.1.2](https://github.com/Fabio286/antares/compare/v0.1.1...v0.1.2) (2021-04-11)

View File

@@ -4,7 +4,7 @@
# Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares)
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](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.
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.1.2",
"version": "0.1.4",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -73,7 +73,6 @@
"electron-store": "^7.0.0",
"electron-updater": "^4.3.5",
"faker": "^5.3.1",
"keytar": "^7.3.0",
"marked": "^2.0.2",
"moment": "^2.29.1",
"mssql": "^6.2.3",
@@ -83,6 +82,7 @@
"pgsql-ast-parser": "^7.0.2",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"sql-formatter": "^4.0.2",
"v-mask": "^2.2.4",
"vue-i18n": "^8.22.4",
"vuedraggable": "^2.24.3",
@@ -91,22 +91,22 @@
"devDependencies": {
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.2",
"electron": "^11.3.0",
"electron": "^11.4.3",
"electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.20.0",
"eslint": "^7.24.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-vue": "^7.6.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.9.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",
"standard-version": "^9.2.0",
"stylelint": "^13.12.0",
"stylelint-config-standard": "^21.0.0",
"stylelint-scss": "^3.19.0",
"vue": "^2.6.12",
"vue-template-compiler": "^2.6.12",

View File

@@ -16,6 +16,7 @@ module.exports = {
tables: false,
views: false,
triggers: false,
triggerFunctions: false,
routines: false,
functions: false,
schedulers: false,
@@ -23,6 +24,7 @@ module.exports = {
tableAdd: false,
viewAdd: false,
triggerAdd: false,
triggerFunctionAdd: false,
routineAdd: false,
functionAdd: false,
schedulerAdd: false,
@@ -31,6 +33,7 @@ module.exports = {
tableSettings: false,
viewSettings: false,
triggerSettings: false,
triggerFunctionSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
@@ -52,5 +55,15 @@ module.exports = {
procedureDeterministic: false,
procedureDataAccess: false,
procedureSql: false,
parametersLength: false
procedureContext: false,
procedureLanguage: false,
functionDeterministic: false,
functionDataAccess: false,
functionSql: false,
functionContext: false,
functionLanguage: false,
triggerMiltipleEvents: false,
triggerUpdateColumns: false,
parametersLength: false,
languages: false
};

View File

@@ -50,5 +50,9 @@ module.exports = {
procedureDeterministic: true,
procedureDataAccess: true,
procedureSql: 'BEGIN\r\n\r\nEND',
procedureContext: true,
functionDeterministic: true,
functionDataAccess: true,
functionSql: 'BEGIN\r\n\r\nEND',
parametersLength: true
};

View File

@@ -15,25 +15,28 @@ module.exports = {
views: true,
triggers: false,
routines: true,
functions: false,
schedulers: false,
functions: true,
// Settings
tableAdd: true,
viewAdd: true,
triggerAdd: false,
routineAdd: true,
functionAdd: false,
functionAdd: true,
databaseEdit: false,
tableSettings: true,
viewSettings: true,
triggerSettings: false,
routineSettings: true,
functionSettings: false,
schedulerSettings: false,
functionSettings: true,
indexes: true,
foreigns: true,
sortableFields: false,
nullable: true,
tableArray: true,
procedureSql: '$BODY$\r\n\r\n$BODY$'
procedureSql: '$BODY$\r\n\r\n$BODY$',
procedureContext: true,
procedureLanguage: true,
functionSql: '$BODY$\r\n\r\n$BODY$',
functionContext: true,
functionLanguage: true,
languages: ['sql', 'plpgsql', 'c', 'internal']
};

View File

@@ -2,9 +2,7 @@
import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path';
import crypto from 'crypto';
import { format as formatUrl } from 'url';
import keytar from 'keytar';
import Store from 'electron-store';
import ipcHandlers from './ipc-handlers';
@@ -96,18 +94,6 @@ else {
// create main BrowserWindow when electron is ready
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();
});
}

View File

@@ -1,4 +1,3 @@
import keytar from 'keytar';
import { app, ipcMain } from 'electron';
export default () => {
@@ -7,14 +6,7 @@ export default () => {
});
ipcMain.on('get-key', async event => {
let key = false;
try {
key = await keytar.getPassword('antares', 'user');
}
catch (err) {
console.log(err);
}
const key = false;
event.returnValue = key;
});
};

View File

@@ -104,8 +104,18 @@ export class MySQLClient extends AntaresCore {
async connect () {
if (!this._poolSize)
this._connection = mysql.createConnection(this._params);
else
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize });
else {
this._connection = mysql.createPool({
...this._params,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
}
}
/**
@@ -581,7 +591,7 @@ export class MySQLClient extends AntaresCore {
const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
return results.rows.map(async row => {
if (!row['Create Procedure']) {
return {
definer: null,
@@ -595,22 +605,23 @@ export class MySQLClient extends AntaresCore {
};
}
const parameters = row['Create Procedure']
.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].replaceAll('\n', ''),
length: +type[1] ? +type[1].replace(/\D/g, '') : '',
context: param[0] ? param[0].replace('\n', '') : ''
};
}).filter(el => el.name);
const sql = `SELECT *
FROM information_schema.parameters
WHERE SPECIFIC_NAME = '${routine}'
AND SPECIFIC_SCHEMA = '${schema}'
ORDER BY ORDINAL_POSITION
`;
const results = await this.raw(sql);
const parameters = results.rows.map(row => {
return {
name: row.PARAMETER_NAME,
type: row.DATA_TYPE.toUpperCase(),
length: row.NUMERIC_PRECISION || row.DATETIME_PRECISION || row.CHARACTER_MAXIMUM_LENGTH || '',
context: row.PARAMETER_MODE
};
});
let dataAccess = 'CONTAINS SQL';
if (row['Create Procedure'].includes('NO SQL'))
@@ -701,7 +712,7 @@ export class MySQLClient extends AntaresCore {
const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
return results.rows.map(async row => {
if (!row['Create Function']) {
return {
definer: null,
@@ -717,22 +728,23 @@ export class MySQLClient extends AntaresCore {
};
}
const parameters = row['Create Function']
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[1] ? param[1].replace(')', '').split('(') : ['', null];
const sql = `SELECT *
FROM information_schema.parameters
WHERE SPECIFIC_NAME = '${func}'
AND SPECIFIC_SCHEMA = '${schema}'
ORDER BY ORDINAL_POSITION
`;
return {
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0],
length: +type[1] ? +type[1].replace(/\D/g, '') : ''
};
}).filter(el => el.name);
const results = await this.raw(sql);
const parameters = results.rows.filter(row => row.PARAMETER_MODE).map(row => {
return {
name: row.PARAMETER_NAME,
type: row.DATA_TYPE.toUpperCase(),
length: row.NUMERIC_PRECISION || row.DATETIME_PRECISION || row.CHARACTER_MAXIMUM_LENGTH || '',
context: row.PARAMETER_MODE
};
});
let dataAccess = 'CONTAINS SQL';
if (row['Create Function'].includes('NO SQL'))
@@ -804,13 +816,15 @@ export class MySQLClient extends AntaresCore {
return acc;
}, []).join(',');
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns}${func.returnsLength ? `(${func.returnsLength})` : ''}
const body = func.returns ? func.sql : 'BEGIN\n RETURN 0;\nEND';
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns || 'SMALLINT'}${func.returnsLength ? `(${func.returnsLength})` : ''}
LANGUAGE SQL
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess}
SQL SECURITY ${func.security}
COMMENT '${func.comment}'
${func.sql}`;
${body}`;
return await this.raw(sql, { split: false });
}

View File

@@ -77,7 +77,8 @@ export class PostgreSQLClient extends AntaresCore {
*/
use (schema) {
this._schema = schema;
return this.raw(`SET search_path TO ${schema}`);
if (schema)
return this.raw(`SET search_path TO ${schema}`);
}
/**
@@ -166,27 +167,34 @@ export class PostgreSQLClient extends AntaresCore {
});
// FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.database).map(func => {
const remappedFunctions = functions.filter(func => func.routine_schema === db.database && func.data_type !== 'trigger').map(func => {
return {
name: func.routine_name,
type: func.routine_type,
definer: null, // func.Definer,
created: null, // func.Created,
updated: null, // func.Modified,
comment: null, // func.Comment,
charset: null, // func.character_set_client,
security: func.security_type
};
});
// TRIGGER FUNCTIONS
const remappedTriggerFunctions = functions.filter(func => func.routine_schema === db.database && func.data_type === 'trigger').map(func => {
return {
name: func.routine_name,
type: func.routine_type,
security: func.security_type
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.database).map(trigger => {
return {
name: trigger.trigger_name,
name: `${trigger.table_name}.${trigger.trigger_name}`,
orgName: trigger.trigger_name,
timing: trigger.activation,
definer: trigger.definition, // ???
definer: '',
definition: trigger.definition,
event: trigger.event,
table: trigger.table_trigger,
sqlMode: trigger.sql_mode
table: trigger.table_name,
sqlMode: ''
};
});
@@ -196,6 +204,7 @@ export class PostgreSQLClient extends AntaresCore {
functions: remappedFunctions,
procedures: remappedProcedures,
triggers: remappedTriggers,
triggerFunctions: remappedTriggerFunctions,
schedulers: []
};
}
@@ -268,7 +277,7 @@ export class PostgreSQLClient extends AntaresCore {
*/
async getTableIndexes ({ schema, table }) {
if (schema !== 'public')
this.use(schema);
await this.use(schema);
const { rows } = await this.raw(`WITH ndx_list AS (
SELECT pg_index.indexrelid, pg_class.oid
@@ -509,7 +518,8 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
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);
}
@@ -571,25 +581,25 @@ export class PostgreSQLClient extends AntaresCore {
}
const sql = `SELECT proc.specific_schema AS procedure_schema,
proc.specific_name,
proc.routine_name AS procedure_name,
proc.external_language,
args.parameter_name,
args.parameter_mode,
args.data_type
FROM information_schema.routines proc
LEFT JOIN information_schema.parameters args
ON proc.specific_schema = args.specific_schema
AND proc.specific_name = args.specific_name
WHERE proc.routine_schema not in ('pg_catalog', 'information_schema')
AND proc.routine_type = 'PROCEDURE'
AND proc.routine_name = '${routine}'
AND proc.specific_schema = '${schema}'
ORDER BY procedure_schema,
specific_name,
procedure_name,
args.ordinal_position
`;
proc.specific_name,
proc.routine_name AS procedure_name,
proc.external_language,
args.parameter_name,
args.parameter_mode,
args.data_type
FROM information_schema.routines proc
LEFT JOIN information_schema.parameters args
ON proc.specific_schema = args.specific_schema
AND proc.specific_name = args.specific_name
WHERE proc.routine_schema not in ('pg_catalog', 'information_schema')
AND proc.routine_type = 'PROCEDURE'
AND proc.routine_name = '${routine}'
AND proc.specific_schema = '${schema}'
ORDER BY procedure_schema,
specific_name,
procedure_name,
args.ordinal_position
`;
const results = await this.raw(sql);
@@ -664,10 +674,10 @@ export class PostgreSQLClient extends AntaresCore {
: '';
if (this._schema !== 'public')
this.use(this._schema);
await this.use(this._schema);
const sql = `CREATE PROCEDURE ${this._schema}.${routine.name}(${parameters})
LANGUAGE SQL
LANGUAGE ${routine.language}
SECURITY ${routine.security}
AS ${routine.sql}`;
@@ -681,63 +691,66 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async getFunctionInformations ({ schema, func }) {
const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``;
const sql = `SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${func}'));`;
const results = await this.raw(sql);
return results.rows.map(row => {
if (!row['Create Function']) {
return results.rows.map(async row => {
if (!row.pg_get_functiondef) {
return {
definer: null,
sql: '',
parameters: [],
name: row.Procedure,
name: func,
comment: '',
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL',
returns: 'INT',
returnsLength: null
dataAccess: 'CONTAINS SQL'
};
}
const parameters = row['Create Function']
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[1] ? param[1].replace(')', '').split('(') : ['', null];
const sql = `SELECT proc.specific_schema AS procedure_schema,
proc.specific_name,
proc.routine_name AS procedure_name,
proc.external_language,
args.parameter_name,
args.parameter_mode,
args.data_type
FROM information_schema.routines proc
LEFT JOIN information_schema.parameters args
ON proc.specific_schema = args.specific_schema
AND proc.specific_name = args.specific_name
WHERE proc.routine_schema not in ('pg_catalog', 'information_schema')
AND proc.routine_type = 'FUNCTION'
AND proc.routine_name = '${func}'
AND proc.specific_schema = '${schema}'
ORDER BY procedure_schema,
specific_name,
procedure_name,
args.ordinal_position
`;
return {
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0],
length: +type[1] ? +type[1].replace(/\D/g, '') : ''
};
}).filter(el => el.name);
const results = await this.raw(sql);
let dataAccess = 'CONTAINS SQL';
if (row['Create Function'].includes('NO SQL'))
dataAccess = 'NO SQL';
if (row['Create Function'].includes('READS SQL DATA'))
dataAccess = 'READS SQL DATA';
if (row['Create Function'].includes('MODIFIES SQL DATA'))
dataAccess = 'MODIFIES SQL DATA';
const output = row['Create Function'].match(/(?<=RETURNS ).*?(?=\s)/gs).length ? row['Create Function'].match(/(?<=RETURNS ).*?(?=\s)/gs)[0].replace(')', '').split('(') : ['', null];
const parameters = results.rows.filter(row => row.parameter_mode).map(row => {
return {
name: row.parameter_name,
type: row.data_type.toUpperCase(),
length: '',
context: row.parameter_mode
};
});
return {
definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
definer: '',
sql: row.pg_get_functiondef.match(/(\$(.*)\$)(.*)(\$(.*)\$)/gs)[0],
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',
deterministic: row['Create Function'].includes('DETERMINISTIC'),
dataAccess,
returns: output[0].toUpperCase(),
returnsLength: +output[1]
name: func,
comment: '',
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
deterministic: null,
dataAccess: null,
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0],
returns: row.pg_get_functiondef.match(/(?<=RETURNS )(.*)(?<=[\S+\n\r\s])/gm)[0].replace('SETOF ', '').toUpperCase()
};
})[0];
}
@@ -749,7 +762,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropFunction (params) {
const sql = `DROP FUNCTION \`${params.func}\``;
const sql = `DROP FUNCTION ${this._schema}.${params.func}`;
return await this.raw(sql);
}
@@ -782,18 +795,23 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async createFunction (func) {
const parameters = func.parameters.reduce((acc, curr) => {
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
const parameters = 'parameters' in func
? func.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} ${curr.name} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns}${func.returnsLength ? `(${func.returnsLength})` : ''}
LANGUAGE SQL
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess}
SQL SECURITY ${func.security}
COMMENT '${func.comment}'
${func.sql}`;
if (this._schema !== 'public')
await this.use(this._schema);
const body = func.returns ? func.sql : '$BODY$\n$BODY$';
const sql = `CREATE FUNCTION ${this._schema}.${func.name}(${parameters})
RETURNS ${func.returns || 'void'}
LANGUAGE ${func.language}
SECURITY ${func.security}
AS ${body}`;
return await this.raw(sql, { split: false });
}
@@ -1003,6 +1021,9 @@ export class PostgreSQLClient extends AntaresCore {
options
} = params;
if (this._schema !== 'public')
await this.use(this._schema);
let sql = '';
const alterColumns = [];
const renameColumns = [];
@@ -1042,7 +1063,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${addition.name} UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX ${addition.name} ON ${table}(${fields})`);
manageIndexes.push(`CREATE INDEX ${addition.name} ON ${this._schema}.${table}(${fields})`);
});
// ADD FOREIGN KEYS
@@ -1070,13 +1091,13 @@ export class PostgreSQLClient extends AntaresCore {
localType = change.type.toLowerCase();
}
alterColumns.push(`ALTER COLUMN "${change.orgName}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.orgName}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.orgName}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.orgName}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
if (['SERIAL', 'SMALLSERIAL', 'BIGSERIAL'].includes(change.type)) {
const sequenceName = `${table}_${change.name}_seq`.replace(' ', '_');
createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.orgName}"`);
alterColumns.push(`ALTER COLUMN "${change.orgName}" SET DEFAULT nextval('${sequenceName}')`);
createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.name}"`);
alterColumns.push(`ALTER COLUMN "${change.name}" SET DEFAULT nextval('${sequenceName}')`);
}
if (change.orgName !== change.name)
@@ -1085,9 +1106,7 @@ export class PostgreSQLClient extends AntaresCore {
// CHANGE INDEX
indexChanges.changes.forEach(change => {
if (change.oldType === 'PRIMARY')
alterColumns.push('DROP PRIMARY KEY');
else if (change.oldType === 'UNIQUE')
if (['PRIMARY', 'UNIQUE'].includes(change.oldType))
alterColumns.push(`DROP CONSTRAINT ${change.oldName}`);
else
manageIndexes.push(`DROP INDEX ${change.oldName}`);
@@ -1100,7 +1119,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${change.name} UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX ${change.name} ON ${table}(${fields})`);
manageIndexes.push(`CREATE INDEX ${change.name} ON ${this._schema}.${table}(${fields})`);
});
// CHANGE FOREIGN KEYS
@@ -1128,13 +1147,13 @@ export class PostgreSQLClient extends AntaresCore {
});
if (alterColumns.length) sql += `ALTER TABLE "${this._schema}"."${table}" ${alterColumns.join(', ')}; `;
// RENAME
if (renameColumns.length) sql += `${renameColumns.join(';')}; `;
if (createSequences.length) sql = `${createSequences.join(';')}; ${sql}`;
if (manageIndexes.length) sql = `${manageIndexes.join(';')}; ${sql}`;
if (options.name) sql += `ALTER TABLE "${this._schema}"."${table}" RENAME TO "${options.name}"; `;
// RENAME
if (renameColumns.length) sql = `${renameColumns.join(';')}; ${sql}`;
return await this.raw(sql);
}
@@ -1232,7 +1251,7 @@ export class PostgreSQLClient extends AntaresCore {
};
if (args.nest && this._schema !== 'public')
this.use(this._schema);
await this.use(this._schema);
const resultsArr = [];
let paramsArr = [];

View File

@@ -15,7 +15,7 @@
<div class="content">
<form class="form-horizontal">
<div
v-for="(parameter, i) in localRoutine.parameters"
v-for="(parameter, i) in inParameters"
:key="parameter._id"
class="form-group"
>
@@ -26,7 +26,7 @@
<div class="input-group">
<input
:ref="i === 0 ? 'firstInput' : ''"
v-model="values[parameter.name]"
v-model="values[`${i}-${parameter.name}`]"
class="form-input"
type="text"
>
@@ -66,6 +66,11 @@ export default {
values: {}
};
},
computed: {
inParameters () {
return this.localRoutine.parameters.filter(param => param.context === 'IN');
}
},
created () {
window.addEventListener('keydown', this.onKey);
@@ -83,7 +88,7 @@ export default {
return '';
},
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr) => {
const valArr = Object.keys(this.values).reduce((acc, curr, i) => {
let qc;
switch (this.client) {
case 'maria':
@@ -97,7 +102,7 @@ export default {
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}`;
acc.push(value);

View File

@@ -7,7 +7,7 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewRoutine') }}
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewFunction') }}
</div>
</template>
<div :slot="'body'">
@@ -25,7 +25,19 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
@@ -53,42 +65,7 @@
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.returns') }}
</label>
<div class="column">
<div class="input-group">
<select
v-model="localFunction.returns"
class="form-select text-uppercase"
style="width: 0;"
>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="localFunction.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-model="localFunction.returnsLength"
class="form-input"
type="number"
min="0"
>
</div>
</div>
</div>
<div class="form-group">
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
@@ -111,7 +88,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
@@ -124,7 +101,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
@@ -152,11 +129,12 @@ export default {
return {
localFunction: {
definer: '',
sql: 'BEGIN\r\n RETURN NULL;\r\nEND',
sql: '',
parameters: [],
name: '',
comment: '',
returns: 'INT',
language: null,
returns: null,
returnsLength: 10,
security: 'DEFINER',
deterministic: false,
@@ -168,9 +146,17 @@ export default {
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
if (this.customizations.languages)
this.localFunction.language = this.customizations.languages[0];
if (this.customizations.procedureSql)
this.localFunction.sql = this.customizations.procedureSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);

View File

@@ -25,6 +25,18 @@
>
</div>
</div>
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localRoutine.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
@@ -121,6 +133,7 @@ export default {
parameters: [],
name: '',
comment: '',
language: null,
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
@@ -137,6 +150,9 @@ export default {
}
},
mounted () {
if (this.customizations.languages)
this.localRoutine.language = this.customizations.languages[0];
if (this.customizations.procedureSql)
this.localRoutine.sql = this.customizations.procedureSql;
setTimeout(() => {

View File

@@ -38,7 +38,6 @@
<a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li>
<li
v-if="updateStatus !== 'disabled'"
class="tab-item"
:class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')"
@@ -228,7 +227,7 @@
<h4>{{ appName }}</h4>
<p>
{{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a> | <a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares/blob/master/CHANGELOG.md')">CHANGELOG</a><br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a> | <a class="c-hand" @click="openOutside('https://antares-sql.app/')">Website</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small>
</p>
@@ -416,6 +415,7 @@ ORDER BY
.panel-body {
min-height: calc(25vh - 70px);
max-height: 65vh;
overflow: auto;
.theme-block {

View File

@@ -15,6 +15,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import marked from 'marked';
import BaseLoader from '@/components/BaseLoader';
@@ -31,13 +32,16 @@ export default {
isError: false
};
},
computed: {
...mapGetters({ appVersion: 'application/appVersion' })
},
created () {
this.getChangelog();
},
methods: {
async getChangelog () {
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'
});

View File

@@ -126,9 +126,13 @@ export default {
return 'sql';
}
},
cursorPosition () {
return this.editor.session.doc.positionToIndex(this.editor.getCursorPosition());
},
lastWord () {
const words = this.value.split(' ');
return words[words.length - 1];
const charsBefore = this.value.slice(0, this.cursorPosition);
const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean);
return words.pop();
},
isLastWordATable () {
return /\w+\.\w*/gm.test(this.lastWord);
@@ -209,7 +213,7 @@ export default {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
if (this.isLastWordATable || e.args === '.') {
if (e.args !== ' ') {
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop());
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop().trim());
if (table) {
const params = {

View File

@@ -11,9 +11,9 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li class="footer-element footer-link" @click="openOutside('https://github.com/sponsors/Fabio286')">
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
<li class="footer-element footer-link" @click="openOutside('https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet')">
<i class="mdi mdi-18px mdi-tree mr-1" />
<small>{{ $t('message.plantATree') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<i class="mdi mdi-18px mdi-bug" />

View File

@@ -19,7 +19,7 @@
@contextmenu.prevent="contextMenu($event, connection)"
@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>
</li>
</draggable>
@@ -73,7 +73,7 @@ export default {
...mapGetters({
getConnections: 'connections/getConnections',
getConnectionName: 'connections/getConnectionName',
connected: 'workspaces/getConnected',
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected',
updateStatus: 'application/getUpdateStatus'
}),
@@ -109,6 +109,22 @@ export default {
const el = e.target;
const fromTop = window.pageYOffset + el.getBoundingClientRect().top - (el.offsetHeight / 4);
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 '';
}
}
}
}
};

View File

@@ -1,7 +1,7 @@
<template>
<div v-show="isSelected" class="workspace column columns col-gapless">
<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
id="tabWrap"
ref="tabWrap"

View File

@@ -8,7 +8,7 @@
>
<div class="workspace-explorebar-header">
<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
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')"
@@ -28,7 +28,7 @@
</span>
</div>
<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
v-model="searchTerm"
class="form-input input-sm"
@@ -39,7 +39,7 @@
</div>
</div>
<WorkspaceConnectPanel
v-if="!workspace.connected"
v-if="workspace.connection_status !== 'connected'"
class="workspace-explorebar-body"
:connection="connection"
/>

View File

@@ -248,9 +248,11 @@ export default {
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 'pg':
sql = `SELECT ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break;

View File

@@ -75,8 +75,8 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="procedure of filteredProcedures"
:key="procedure.name"
v-for="(procedure, i) of filteredProcedures"
:key="`${procedure.name}-${i}`"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@click="setBreadcrumbs({schema: database.name, procedure: procedure.name})"
@@ -103,8 +103,8 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="func of filteredFunctions"
:key="func.name"
v-for="(func, i) of filteredFunctions"
:key="`${func.name}-${i}`"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@click="setBreadcrumbs({schema: database.name, function: func.name})"

View File

@@ -26,7 +26,19 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
@@ -65,6 +77,12 @@
class="form-select text-uppercase"
style="width: 0;"
>
<option v-if="localOptions.returns === 'VOID'">
VOID
</option>
<option v-if="!isInDataTypes">
{{ localOptions.returns }}
</option>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
@@ -81,6 +99,7 @@
</optgroup>
</select>
<input
v-if="customizations.parametersLength"
v-model="optionsProxy.returnsLength"
class="form-input"
type="number"
@@ -89,7 +108,7 @@
</div>
</div>
</div>
<div class="form-group">
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
@@ -112,7 +131,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
@@ -125,7 +144,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
@@ -159,6 +178,19 @@ export default {
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
},
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 () {

View File

@@ -49,7 +49,7 @@
<div class="tile-title">
{{ param.name }}
</div>
<small class="tile-subtitle text-gray">{{ param.type }}{{ param.length ? `(${param.length})` : '' }}</small>
<small class="tile-subtitle text-gray">{{ param.type }}{{ param.length ? `(${param.length})` : '' }} · {{ param.context }}</small>
</div>
<div class="tile-action">
<button
@@ -106,7 +106,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3">
{{ $t('word.length') }}
</label>
@@ -119,6 +119,37 @@
>
</div>
</div>
<div v-if="customizations.functionContext" class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
</label>
<div class="column">
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
name="context"
value="IN"
> <i class="form-icon" /> IN
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="OUT"
name="context"
> <i class="form-icon" /> OUT
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="INOUT"
name="context"
> <i class="form-icon" /> INOUT
</label>
</div>
</div>
</form>
<div v-if="!parametersProxy.length" class="empty">
<div class="empty-icon">
@@ -168,6 +199,9 @@ export default {
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {

View File

@@ -26,6 +26,18 @@
>
</div>
</div>
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}

View File

@@ -119,7 +119,7 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.procedureContext" class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
</label>

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
@@ -68,6 +69,7 @@
@remove-field="removeField"
@add-new-index="addNewIndex"
@add-to-index="addToIndex"
@rename-field="renameField"
/>
</div>
<WorkspacePropsOptionsModal
@@ -149,6 +151,7 @@ export default {
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
workspace () {
@@ -162,7 +165,7 @@ export default {
return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || '';
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.table;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -199,6 +202,12 @@ export default {
this.setUnsavedChanges(val);
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -457,6 +466,20 @@ export default {
scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20);
},
renameField (payload) {
this.localIndexes = this.localIndexes.map(index => {
const fi = index.fields.findIndex(field => field === payload.old);
if (fi !== -1)
index.fields[fi] = payload.new;
return index;
});
this.localKeyUsage = this.localKeyUsage.map(key => {
if (key.field === payload.old)
key.field = payload.new;
return key;
});
},
removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid);
},
@@ -504,6 +527,15 @@ export default {
},
foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
@@ -47,7 +48,7 @@
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:value.sync="localFunction.sql"
:workspace="workspace"
@@ -120,13 +121,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.function;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -171,6 +173,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -281,9 +289,11 @@ export default {
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 'pg':
sql = `SELECT ${this.originalFunction.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
@@ -310,6 +320,15 @@ export default {
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
@@ -121,13 +122,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.routine;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -172,6 +174,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -310,6 +318,15 @@ export default {
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
@@ -169,13 +170,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.scheduler;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -220,6 +222,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -310,6 +318,15 @@ export default {
},
timingUpdate (options) {
this.localScheduler = options;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
@@ -140,13 +141,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.trigger;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -191,6 +193,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -274,6 +282,15 @@ export default {
this.editorHeight = size;
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();
}
}
}
}
};

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
@@ -201,13 +202,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.view;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -245,6 +247,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -327,6 +335,15 @@ export default {
this.editorHeight = size;
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();
}
}
}
}
};

View File

@@ -115,6 +115,7 @@
:data-types="dataTypes"
:customizations="customizations"
@contextmenu="contextMenu"
@rename-field="$emit('rename-field', $event)"
/>
</draggable>
</div>

View File

@@ -60,6 +60,9 @@
class="form-select editable-field small-select text-uppercase"
@blur="editOFF"
>
<option v-if="!isInDataTypes">
{{ row.type }}
</option>
<optgroup
v-for="group in dataTypes"
:key="group.group"
@@ -68,7 +71,7 @@
<option
v-for="type in group.types"
:key="type.name"
:selected="localRow.type.toUpperCase() === type.name"
:selected="localRow.type === type.name"
:value="type.name"
>
{{ type.name }}
@@ -374,6 +377,16 @@ export default {
},
isNullable () {
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: {
@@ -440,9 +453,6 @@ export default {
this.defaultValue.expression = this.localRow.default;
}
},
updateRow () {
this.$emit('input', this.localRow);
},
editON (event, content, field) {
if (field === 'length') {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.editingField = 'numLength';
@@ -469,6 +479,9 @@ export default {
}
},
editOFF () {
if (this.editingField === 'name')
this.$emit('rename-field', { old: this.localRow[this.editingField], new: this.editingContent });
this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
@@ -491,8 +504,7 @@ export default {
if (!this.fieldType.zerofill)
this.localRow.zerofill = false;
}
if (this.editingField === 'default') {
else if (this.editingField === 'default') {
switch (this.defaultValue.type) {
case 'autoincrement':
this.localRow.autoIncrement = true;

View File

@@ -1,5 +1,12 @@
<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">
<QueryEditor
v-show="isSelected"
@@ -24,6 +31,43 @@
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play" />
</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 class="workspace-query-info">
<div
@@ -68,6 +112,7 @@
</template>
<script>
import { format } from 'sql-formatter';
import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
@@ -192,12 +237,36 @@ export default {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.resize();
},
onKey (e) {
if (this.isSelected && this.isWorkspaceSelected) {
e.stopPropagation();
if (e.key === 'F5')
this.runQuery(this.query);
beautify () {
if (this.$refs.queryEditor) {
let language = 'sql';
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>

View File

@@ -1,16 +1,18 @@
<template>
<div
ref="tableWrapper"
class="vscroll"
class="vscroll no-outline"
tabindex="0"
:style="{'height': resultsSize+'px'}"
@keyup.46="showDeleteConfirmModal"
>
<TableContext
v-if="isContext"
:context-event="contextEvent"
:selected-rows="selectedRows"
@delete-selected="deleteSelected"
@show-delete-modal="showDeleteConfirmModal"
@set-null="setNull"
@close-context="isContext = false"
@close-context="closeContext"
/>
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
<li
@@ -75,6 +77,23 @@
</template>
</BaseVirtualScroll>
</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>
</template>
@@ -85,6 +104,7 @@ import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext';
import ConfirmModal from '@/components/BaseConfirmModal';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
@@ -93,7 +113,8 @@ export default {
components: {
BaseVirtualScroll,
WorkspaceQueryTableRow,
TableContext
TableContext,
ConfirmModal
},
props: {
results: Array,
@@ -106,6 +127,7 @@ export default {
resultsSize: 1000,
localResults: [],
isContext: false,
isDeleteConfirmModal: false,
contextEvent: null,
selectedCell: null,
selectedRows: [],
@@ -322,7 +344,17 @@ export default {
};
this.$emit('update-field', params);
},
closeContext () {
this.isContext = false;
},
showDeleteConfirmModal () {
this.isDeleteConfirmModal = true;
},
hideDeleteConfirmModal () {
this.isDeleteConfirmModal = false;
},
deleteSelected () {
this.closeContext();
const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => {
delete row._id;
return row;

View File

@@ -17,61 +17,30 @@
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</span>
</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>
</template>
<script>
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspaceQueryTableContext',
components: {
BaseContextMenu,
ConfirmModal
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedRows: Array
},
data () {
return {
isConfirmModal: false
};
},
computed: {
},
methods: {
showConfirmModal () {
this.isConfirmModal = true;
},
hideConfirmModal () {
this.isConfirmModal = false;
this.$emit('show-delete-modal');
},
closeContext () {
this.$emit('close-context');
},
deleteRows () {
this.$emit('delete-selected');
this.closeContext();
},
setNull () {
this.$emit('set-null');
this.closeContext();

View File

@@ -74,7 +74,7 @@
<div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
</div>
<div v-if="results.length && results[0].rows && tableInfo && results[0].rows.length < tableInfo.rows">
<div v-if="hasApproximately">
{{ $t('word.total') }}: <b>{{ tableInfo.rows.toLocaleString() }}</b> <small>({{ $t('word.approximately') }})</small>
</div>
<div v-if="workspace.breadcrumbs.database">
@@ -179,6 +179,13 @@ export default {
catch (err) {
return { rows: 0 };
}
},
hasApproximately () {
return this.results.length &&
this.results[0].rows &&
this.tableInfo &&
this.results[0].rows.length === 1000 &&
this.results[0].rows.length < this.tableInfo.rows;
}
},
watch: {

View File

@@ -104,7 +104,8 @@ module.exports = {
database: 'Database',
scratchpad: 'Scratchpad',
array: 'Array',
changelog: 'Changelog'
changelog: 'Changelog',
format: 'Format'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -208,7 +209,8 @@ module.exports = {
schemaName: 'Schema name',
editSchema: 'Edit schema',
deleteSchema: 'Delete schema',
markdownSupported: 'Markdown supported'
markdownSupported: 'Markdown supported',
plantATree: 'Plant a Tree'
},
faker: {
address: 'Address',

View File

@@ -30,6 +30,12 @@
animation: jump-down-in 0.2s reverse;
}
.pulse {
animation-name: pulse;
animation-duration: 2s;
animation-iteration-count: infinite;
}
@keyframes jump-down-in {
0% {
transform: scale(0);
@@ -39,3 +45,17 @@
transform: scale(1);
}
}
@keyframes pulse {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@@ -30,6 +30,10 @@ body {
cursor: help;
}
.no-outline {
outline: none !important;
}
.no-border {
outline: none !important;
border: none !important;
@@ -99,6 +103,7 @@ body {
.modal-container,
.modal-sm .modal-container {
padding: 0;
border-radius: 3px;
.modal-header {
padding: 0.4rem 0.8rem;
@@ -106,6 +111,7 @@ body {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 3px 3px 0 0;
}
}
}
@@ -130,6 +136,21 @@ body {
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 {

View File

@@ -386,7 +386,6 @@
bottom: -10px;
right: 0;
position: absolute;
background: $success-color;
}
&.badge-update::after {

View File

@@ -168,7 +168,6 @@
bottom: -10px;
right: 0;
position: absolute;
background: $success-color;
}
&.badge-update::after {

View File

@@ -1,8 +1,7 @@
'use strict';
import Store from 'electron-store';
import crypto from 'crypto';
import Application from '../../ipc-api/Application';
const key = Application.getKey() || localStorage.getItem('key');
const key = localStorage.getItem('key');
if (!key)
localStorage.setItem('key', crypto.randomBytes(16).toString('hex'));

View File

@@ -37,7 +37,7 @@ export default {
},
getConnected: state => {
return state.workspaces
.filter(workspace => workspace.connected)
.filter(workspace => workspace.connection_status === 'connected')
.map(workspace => workspace.uid);
},
getLoadedSchemas: state => uid => {
@@ -54,7 +54,7 @@ export default {
SELECT_WORKSPACE (state, uid) {
state.selected_workspace = uid;
},
ADD_CONNECTED (state, payload) {
SET_CONNECTED (state, payload) {
const { uid, client, dataTypes, indexTypes, customizations, structure, version } = payload;
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
@@ -65,19 +65,41 @@ export default {
indexTypes,
customizations,
structure,
connected: true,
connection_status: 'connected',
version
}
: workspace);
},
REMOVE_CONNECTED (state, uid) {
SET_CONNECTING (state, uid) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
structure: {},
breadcrumbs: {},
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);
},
@@ -247,10 +269,14 @@ export default {
commit('SELECT_WORKSPACE', uid);
},
async connectWorkspace ({ dispatch, commit }, connection) {
commit('SET_CONNECTING', connection.uid);
try {
const { status, response } = await Connection.connect(connection);
if (status === 'error')
if (status === 'error') {
dispatch('notifications/addNotification', { status, message: response }, { root: true });
commit('SET_FAILED', connection.uid);
}
else {
let dataTypes = [];
let indexTypes = [];
@@ -288,7 +314,7 @@ export default {
dispatch('connections/editConnection', connProxy, { root: true });
}
commit('ADD_CONNECTED', {
commit('SET_CONNECTED', {
uid: connection.uid,
client: connection.client,
dataTypes,
@@ -382,13 +408,13 @@ export default {
},
removeConnected ({ commit }, uid) {
Connection.disconnect(uid);
commit('REMOVE_CONNECTED', uid);
commit('SET_DISCONNECTED', uid);
commit('SELECT_TAB', { uid, tab: 0 });
},
addWorkspace ({ commit, dispatch, getters }, uid) {
const workspace = {
uid,
connected: false,
connection_status: 'disconnected',
selected_tab: 0,
search_term: '',
tabs: [],