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

Compare commits

...

35 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
dea5ec7513 chore(release): 0.1.2 2021-04-11 12:39:56 +02:00
be816e8588 perf(UI): improved setting modal rendering 2021-04-11 12:38:50 +02:00
1e938adc5d feat: in-app last release changelog 2021-04-11 12:35:16 +02:00
8735a0c5f9 feat(PostgreSQL): edit timezone in cell editor 2021-04-11 10:55:22 +02:00
3dde1c109e feat(PostgreSQL): procedures management 2021-04-10 20:38:46 +02:00
d0b3e1b1b8 feat(PostgreSQL): support of arrays in table settings 2021-04-09 19:31:41 +02:00
c20bff7bcb fix: deletion of rows from query results 2021-04-08 21:49:38 +02:00
9f5ec0276c fix: no foreign key select when cell value is NULL, closes #50 2021-04-08 18:02:16 +02:00
55932fe115 fix: cell edit doesn't properly use primary or unique index to update if both present, closes #51 2021-04-08 17:47:10 +02:00
d374372e20 fix: wrong datetime conversion when updating a row without an unique key 2021-04-07 15:05:11 +02:00
bb5f44681f fix(UI): white readonly inputs with dark theme 2021-04-07 09:20:11 +02:00
49a4e1cb7b fix(PostgreSQL): issue with selected schema different than public 2021-04-06 12:48:40 +02:00
57 changed files with 1184 additions and 543 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,7 +1,7 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [fabio286] github: [fabio286]
patreon: fabio286 patreon: #fabio286
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 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,74 @@
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)
### 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)
### Features
* in-app last release changelog ([1e938ad](https://github.com/Fabio286/antares/commit/1e938adc5d8eb5ad16ab16342375eecd88f68d20))
* **PostgreSQL:** edit timezone in cell editor ([8735a0c](https://github.com/Fabio286/antares/commit/8735a0c5f9e5b6b3bcaadf37ce158aa7beae2c48))
* **PostgreSQL:** procedures management ([3dde1c1](https://github.com/Fabio286/antares/commit/3dde1c109e23342d94362626ef7350dc123ea859))
* **PostgreSQL:** support of arrays in table settings ([d0b3e1b](https://github.com/Fabio286/antares/commit/d0b3e1b1b8be9d2c038d70e16d4478671315de8f))
### Bug Fixes
* cell edit doesn't properly use primary or unique index to update if both present, closes [#51](https://github.com/Fabio286/antares/issues/51) ([55932fe](https://github.com/Fabio286/antares/commit/55932fe11583bd5ff48f82b8408965adba4f5071))
* deletion of rows from query results ([c20bff7](https://github.com/Fabio286/antares/commit/c20bff7bcbe340ac99ebcacaba3359edd61c068a))
* no foreign key select when cell value is NULL, closes [#50](https://github.com/Fabio286/antares/issues/50) ([9f5ec02](https://github.com/Fabio286/antares/commit/9f5ec0276c92904975fdaea34b4c845c92bfe8d4))
* wrong datetime conversion when updating a row without an unique key ([d374372](https://github.com/Fabio286/antares/commit/d374372e208318d7e50b258a8041145bdf7992c5))
* **PostgreSQL:** issue with selected schema different than public ([49a4e1c](https://github.com/Fabio286/antares/commit/49a4e1cb7b24642641265d5830d3fee370cceeb4))
* **UI:** white readonly inputs with dark theme ([bb5f446](https://github.com/Fabio286/antares/commit/bb5f44681f87aacf2cd2f60a6d958c5732289790))
### Improvements
* **UI:** improved setting modal rendering ([be816e8](https://github.com/Fabio286/antares/commit/be816e85888b4f3d26cbb9caac0adbc4dde0ea94))
### [0.1.1](https://github.com/Fabio286/antares/compare/v0.1.0...v0.1.1) (2021-04-03) ### [0.1.1](https://github.com/Fabio286/antares/compare/v0.1.0...v0.1.1) (2021-04-03)

View File

@@ -4,7 +4,7 @@
# Antares SQL Client # 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. 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.

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.1.1", "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,7 @@
"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",
"moment": "^2.29.1", "moment": "^2.29.1",
"mssql": "^6.2.3", "mssql": "^6.2.3",
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
@@ -82,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",
@@ -90,22 +91,22 @@
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"electron": "^11.3.0", "electron": "^11.4.3",
"electron-builder": "^22.9.1", "electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0", "electron-webpack-vue": "^2.4.0",
"eslint": "^7.20.0", "eslint": "^7.24.0",
"eslint-config-standard": "^16.0.2", "eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1", "eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.6.0", "eslint-plugin-vue": "^7.9.0",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"sass-loader": "^10.1.1", "sass-loader": "^10.1.1",
"standard-version": "^9.1.0", "standard-version": "^9.2.0",
"stylelint": "^13.9.0", "stylelint": "^13.12.0",
"stylelint-config-standard": "^20.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",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",

View File

@@ -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,
@@ -44,9 +47,23 @@ module.exports = {
comment: false, comment: false,
collation: false, collation: false,
definer: false, definer: false,
arrays: false,
onUpdate: false, onUpdate: false,
tableArray: false,
viewAlgorithm: false, viewAlgorithm: false,
viewSqlSecurity: false, viewSqlSecurity: false,
viewUpdateOption: false viewUpdateOption: false,
procedureDeterministic: false,
procedureDataAccess: false,
procedureSql: 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

@@ -46,5 +46,13 @@ module.exports = {
onUpdate: true, onUpdate: true,
viewAlgorithm: true, viewAlgorithm: true,
viewSqlSecurity: true, viewSqlSecurity: true,
viewUpdateOption: true viewUpdateOption: true,
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

@@ -14,22 +14,29 @@ module.exports = {
tables: true, tables: true,
views: true, views: true,
triggers: false, triggers: false,
routines: false, routines: true,
functions: false, functions: true,
schedulers: false,
// Settings // Settings
tableAdd: true, tableAdd: true,
viewAdd: true, viewAdd: true,
triggerAdd: false,
routineAdd: true,
functionAdd: true,
databaseEdit: false, databaseEdit: false,
tableSettings: true, tableSettings: true,
viewSettings: true, viewSettings: true,
triggerSettings: false, triggerSettings: false,
routineSettings: false, routineSettings: true,
functionSettings: false, functionSettings: true,
schedulerSettings: false,
indexes: true, indexes: true,
foreigns: true, foreigns: true,
sortableFields: false,
nullable: true, nullable: true,
arrays: true tableArray: true,
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

@@ -54,6 +54,7 @@ export const TIME = [
'TIME', 'TIME',
'TIME WITH TIME ZONE' 'TIME WITH TIME ZONE'
]; ];
export const DATETIME = [ export const DATETIME = [
'DATETIME', 'DATETIME',
'TIMESTAMP', 'TIMESTAMP',
@@ -61,6 +62,12 @@ export const DATETIME = [
'TIMESTAMP WITH TIME ZONE' 'TIMESTAMP WITH TIME ZONE'
]; ];
// Used to check datetime fields only
export const HAS_TIMEZONE = [
'TIMESTAMP WITH TIME ZONE',
'TIME WITH TIME ZONE'
];
export const BLOB = [ export const BLOB = [
'BLOB', 'BLOB',
'TINYBLOB', 'TINYBLOB',

View File

@@ -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();
}); });
} }

View File

@@ -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;
}); });
}; };

View File

@@ -8,7 +8,8 @@ export default connections => {
host: conn.host, host: conn.host,
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password password: conn.password,
application_name: 'Antares SQL'
}; };
if (conn.database) if (conn.database)
@@ -50,7 +51,8 @@ export default connections => {
host: conn.host, host: conn.host,
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password password: conn.password,
application_name: 'Antares SQL'
}; };
if (conn.database) if (conn.database)

View File

@@ -157,9 +157,13 @@ export default (connections) => {
ipcMain.handle('delete-table-rows', async (event, params) => { ipcMain.handle('delete-table-rows', async (event, params) => {
if (params.primary) { if (params.primary) {
const idString = params.rows.map(row => typeof row[params.primary] === 'string' const idString = params.rows.map(row => {
? `"${row[params.primary]}"` const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
: row[params.primary]).join(',');
return typeof row[fieldName] === 'string'
? `"${row[fieldName]}"`
: row[fieldName];
}).join(',');
try { try {
const result = await connections[params.uid] const result = await connections[params.uid]

View File

@@ -7,6 +7,8 @@ export class MySQLClient extends AntaresCore {
constructor (args) { constructor (args) {
super(args); super(args);
this._schema = null;
this.types = { this.types = {
0: 'DECIMAL', 0: 'DECIMAL',
1: 'TINYINT', 1: 'TINYINT',
@@ -102,8 +104,18 @@ export class MySQLClient extends AntaresCore {
async connect () { async connect () {
if (!this._poolSize) if (!this._poolSize)
this._connection = mysql.createConnection(this._params); this._connection = mysql.createConnection(this._params);
else else {
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize }); this._connection = mysql.createPool({
...this._params,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
}
} }
/** /**
@@ -120,6 +132,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
use (schema) { use (schema) {
this._schema = schema;
return this.raw(`USE \`${schema}\``); return this.raw(`USE \`${schema}\``);
} }
@@ -578,7 +591,7 @@ export class MySQLClient extends AntaresCore {
const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``; const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``;
const results = await this.raw(sql); const results = await this.raw(sql);
return results.rows.map(row => { return results.rows.map(async row => {
if (!row['Create Procedure']) { if (!row['Create Procedure']) {
return { return {
definer: null, definer: null,
@@ -592,22 +605,23 @@ export class MySQLClient extends AntaresCore {
}; };
} }
const parameters = row['Create Procedure'] const sql = `SELECT *
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0] FROM information_schema.parameters
.replaceAll('\r', '') WHERE SPECIFIC_NAME = '${routine}'
.replaceAll('\t', '') AND SPECIFIC_SCHEMA = '${schema}'
.slice(1, -1) ORDER BY ORDINAL_POSITION
.split(',') `;
.map(el => {
const param = el.split(' '); const results = await this.raw(sql);
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return { const parameters = results.rows.map(row => {
name: param[1] ? param[1].replaceAll('`', '') : '', return {
type: type[0].replaceAll('\n', ''), name: row.PARAMETER_NAME,
length: +type[1] ? +type[1].replace(/\D/g, '') : '', type: row.DATA_TYPE.toUpperCase(),
context: param[0] ? param[0].replace('\n', '') : '' length: row.NUMERIC_PRECISION || row.DATETIME_PRECISION || row.CHARACTER_MAXIMUM_LENGTH || '',
}; context: row.PARAMETER_MODE
}).filter(el => el.name); };
});
let dataAccess = 'CONTAINS SQL'; let dataAccess = 'CONTAINS SQL';
if (row['Create Procedure'].includes('NO SQL')) if (row['Create Procedure'].includes('NO SQL'))
@@ -698,7 +712,7 @@ export class MySQLClient extends AntaresCore {
const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``; const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``;
const results = await this.raw(sql); const results = await this.raw(sql);
return results.rows.map(row => { return results.rows.map(async row => {
if (!row['Create Function']) { if (!row['Create Function']) {
return { return {
definer: null, definer: null,
@@ -714,22 +728,23 @@ export class MySQLClient extends AntaresCore {
}; };
} }
const parameters = row['Create Function'] const sql = `SELECT *
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0] FROM information_schema.parameters
.replaceAll('\r', '') WHERE SPECIFIC_NAME = '${func}'
.replaceAll('\t', '') AND SPECIFIC_SCHEMA = '${schema}'
.slice(1, -1) ORDER BY ORDINAL_POSITION
.split(',') `;
.map(el => {
const param = el.split(' ');
const type = param[1] ? param[1].replace(')', '').split('(') : ['', null];
return { const results = await this.raw(sql);
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0], const parameters = results.rows.filter(row => row.PARAMETER_MODE).map(row => {
length: +type[1] ? +type[1].replace(/\D/g, '') : '' return {
}; name: row.PARAMETER_NAME,
}).filter(el => el.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'; let dataAccess = 'CONTAINS SQL';
if (row['Create Function'].includes('NO SQL')) if (row['Create Function'].includes('NO SQL'))
@@ -801,13 +816,15 @@ export class MySQLClient extends AntaresCore {
return acc; return acc;
}, []).join(','); }, []).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 LANGUAGE SQL
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'} ${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess} ${func.dataAccess}
SQL SECURITY ${func.security} SQL SECURITY ${func.security}
COMMENT '${func.comment}' COMMENT '${func.comment}'
${func.sql}`; ${body}`;
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
@@ -1049,7 +1066,7 @@ export class MySQLClient extends AntaresCore {
options options
} = params; } = params;
let sql = `ALTER TABLE \`${table}\` `; let sql = `ALTER TABLE \`${this._schema}\`.\`${table}\` `;
const alterColumns = []; const alterColumns = [];
// OPTIONS // OPTIONS

View File

@@ -23,54 +23,16 @@ export class PostgreSQLClient extends AntaresCore {
this.types = {}; this.types = {};
for (const key in types.builtins) for (const key in types.builtins)
this.types[types.builtins[key]] = key; this.types[types.builtins[key]] = key;
}
_getType (field) { this._arrayTypes = {
let name = this.types[field.columnType]; _int2: 'SMALLINT',
let length = field.columnLength; _int4: 'INTEGER',
_int8: 'BIGINT',
if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name)) _float4: 'REAL',
length = field.decimals; _float8: 'DOUBLE PRECISION',
_char: '"CHAR"',
if (name === 'TIMESTAMP') _varchar: 'CHARACTER VARYING'
length = 0; };
if (field.charsetNr === 63) { // if binary
if (name === 'CHAR')
name = 'BINARY';
else if (name === 'VARCHAR')
name = 'VARBINARY';
}
if (name === 'BLOB') {
switch (length) {
case 765:
name = 'TYNITEXT';
break;
case 196605:
name = 'TEXT';
break;
case 50331645:
name = 'MEDIUMTEXT';
break;
case 4294967295:
name = field.charsetNr === 63 ? 'LONGBLOB' : 'LONGTEXT';
break;
case 255:
name = 'TINYBLOB';
break;
case 65535:
name = 'BLOB';
break;
case 16777215:
name = 'MEDIUMBLOB';
break;
default:
name = field.charsetNr === 63 ? 'BLOB' : 'TEXT';
}
}
return { name, length };
} }
_getTypeInfo (type) { _getTypeInfo (type) {
@@ -79,6 +41,12 @@ export class PostgreSQLClient extends AntaresCore {
.filter(_type => _type.name === type.toUpperCase())[0]; .filter(_type => _type.name === type.toUpperCase())[0];
} }
_getArrayType (type) {
if (Object.keys(this._arrayTypes).includes(type))
return this._arrayTypes[type];
return type.replace('_', '');
}
/** /**
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
@@ -109,7 +77,8 @@ export class PostgreSQLClient extends AntaresCore {
*/ */
use (schema) { use (schema) {
this._schema = schema; this._schema = schema;
return this.raw(`SET search_path TO '${schema}', '$user'`); if (schema)
return this.raw(`SET search_path TO ${schema}`);
} }
/** /**
@@ -189,41 +158,43 @@ export class PostgreSQLClient extends AntaresCore {
}); });
// PROCEDURES // PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.database).map(procedure => { const remappedProcedures = procedures.filter(procedure => procedure.routine_schema === db.database).map(procedure => {
return { return {
name: procedure.Name, name: procedure.routine_name,
type: procedure.Type, type: procedure.routine_type,
definer: procedure.Definer, security: procedure.security_type
created: procedure.Created,
updated: procedure.Modified,
comment: procedure.Comment,
charset: procedure.character_set_client,
security: procedure.Security_type
}; };
}); });
// FUNCTIONS // 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 { return {
name: func.routine_name, name: func.routine_name,
type: func.routine_type, 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 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 // 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: ''
}; };
}); });
@@ -233,6 +204,7 @@ export class PostgreSQLClient extends AntaresCore {
functions: remappedFunctions, functions: remappedFunctions,
procedures: remappedProcedures, procedures: remappedProcedures,
triggers: remappedTriggers, triggers: remappedTriggers,
triggerFunctions: remappedTriggerFunctions,
schedulers: [] schedulers: []
}; };
} }
@@ -256,7 +228,7 @@ export class PostgreSQLClient extends AntaresCore {
* @returns {Object} table scructure * @returns {Object} table scructure
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async getTableColumns ({ schema, table }) { async getTableColumns ({ schema, table }, arrayRemap = true) {
const { rows } = await this const { rows } = await this
.select('*') .select('*')
.schema('information_schema') .schema('information_schema')
@@ -266,10 +238,17 @@ export class PostgreSQLClient extends AntaresCore {
.run(); .run();
return rows.map(field => { return rows.map(field => {
let type = field.data_type;
const isArray = type === 'ARRAY';
if (isArray && arrayRemap)
type = this._getArrayType(field.udt_name);
return { return {
name: field.column_name, name: field.column_name,
key: null, key: null,
type: field.data_type.toUpperCase(), type: type.toUpperCase(),
isArray,
schema: field.table_schema, schema: field.table_schema,
table: field.table_name, table: field.table_name,
numPrecision: field.numeric_precision, numPrecision: field.numeric_precision,
@@ -297,6 +276,9 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async getTableIndexes ({ schema, table }) { async getTableIndexes ({ schema, table }) {
if (schema !== 'public')
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
FROM pg_index, pg_class FROM pg_index, pg_class
@@ -476,7 +458,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropView (params) { async dropView (params) {
const sql = `DROP VIEW ${params.view}`; const sql = `DROP VIEW ${this._schema}.${params.view}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -488,10 +470,10 @@ export class PostgreSQLClient extends AntaresCore {
*/ */
async alterView (params) { async alterView (params) {
const { view } = params; const { view } = params;
let sql = `CREATE OR REPLACE VIEW ${view.oldName} AS ${view.sql}`; let sql = `CREATE OR REPLACE VIEW ${this._schema}.${view.oldName} AS ${view.sql}`;
if (view.name !== view.oldName) if (view.name !== view.oldName)
sql += `; ALTER VIEW ${view.oldName} RENAME TO ${view.name}`; sql += `; ALTER VIEW ${this._schema}.${view.oldName} RENAME TO ${view.name}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -503,7 +485,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createView (view) { async createView (view) {
const sql = `CREATE VIEW ${view.name} AS ${view.sql}`; const sql = `CREATE VIEW ${this._schema}.${view.name} AS ${view.sql}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -536,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);
} }
@@ -580,16 +563,16 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async getRoutineInformations ({ schema, routine }) { async getRoutineInformations ({ schema, routine }) {
const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``; const sql = `SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${routine}'));`;
const results = await this.raw(sql); const results = await this.raw(sql);
return results.rows.map(row => { return results.rows.map(async row => {
if (!row['Create Procedure']) { if (!row.pg_get_functiondef) {
return { return {
definer: null, definer: null,
sql: '', sql: '',
parameters: [], parameters: [],
name: row.Procedure, name: routine,
comment: '', comment: '',
security: 'DEFINER', security: 'DEFINER',
deterministic: false, deterministic: false,
@@ -597,40 +580,48 @@ export class PostgreSQLClient extends AntaresCore {
}; };
} }
const parameters = row['Create Procedure'] const sql = `SELECT proc.specific_schema AS procedure_schema,
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0] proc.specific_name,
.replaceAll('\r', '') proc.routine_name AS procedure_name,
.replaceAll('\t', '') proc.external_language,
.slice(1, -1) args.parameter_name,
.split(',') args.parameter_mode,
.map(el => { args.data_type
const param = el.split(' '); FROM information_schema.routines proc
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null]; LEFT JOIN information_schema.parameters args
return { ON proc.specific_schema = args.specific_schema
name: param[1] ? param[1].replaceAll('`', '') : '', AND proc.specific_name = args.specific_name
type: type[0].replaceAll('\n', ''), WHERE proc.routine_schema not in ('pg_catalog', 'information_schema')
length: +type[1] ? +type[1].replace(/\D/g, '') : '', AND proc.routine_type = 'PROCEDURE'
context: param[0] ? param[0].replace('\n', '') : '' AND proc.routine_name = '${routine}'
}; AND proc.specific_schema = '${schema}'
}).filter(el => el.name); ORDER BY procedure_schema,
specific_name,
procedure_name,
args.ordinal_position
`;
let dataAccess = 'CONTAINS SQL'; const results = await this.raw(sql);
if (row['Create Procedure'].includes('NO SQL'))
dataAccess = 'NO SQL'; const parameters = results.rows.map(row => {
if (row['Create Procedure'].includes('READS SQL DATA')) return {
dataAccess = 'READS SQL DATA'; name: row.parameter_name,
if (row['Create Procedure'].includes('MODIFIES SQL DATA')) type: row.data_type.toUpperCase(),
dataAccess = 'MODIFIES SQL DATA'; length: '',
context: row.parameter_mode
};
});
return { return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], definer: '',
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0], sql: row.pg_get_functiondef.match(/(\$(.*)\$)(.*)(\$(.*)\$)/gs)[0],
parameters: parameters || [], parameters: parameters || [],
name: row.Procedure, name: routine,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '', comment: '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER', security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
deterministic: row['Create Procedure'].includes('DETERMINISTIC'), deterministic: null,
dataAccess dataAccess: null,
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0]
}; };
})[0]; })[0];
} }
@@ -642,7 +633,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropRoutine (params) { async dropRoutine (params) {
const sql = `DROP PROCEDURE \`${params.routine}\``; const sql = `DROP PROCEDURE ${this._schema}.${params.routine}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -677,18 +668,18 @@ export class PostgreSQLClient extends AntaresCore {
async createRoutine (routine) { async createRoutine (routine) {
const parameters = 'parameters' in routine const parameters = 'parameters' in routine
? routine.parameters.reduce((acc, curr) => { ? routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`); acc.push(`${curr.context} ${curr.name} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc; return acc;
}, []).join(',') }, []).join(',')
: ''; : '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters}) if (this._schema !== 'public')
LANGUAGE SQL await this.use(this._schema);
${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${routine.dataAccess} const sql = `CREATE PROCEDURE ${this._schema}.${routine.name}(${parameters})
SQL SECURITY ${routine.security} LANGUAGE ${routine.language}
COMMENT '${routine.comment}' SECURITY ${routine.security}
${routine.sql}`; AS ${routine.sql}`;
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
@@ -700,63 +691,66 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async getFunctionInformations ({ schema, func }) { 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); const results = await this.raw(sql);
return results.rows.map(row => { return results.rows.map(async row => {
if (!row['Create Function']) { if (!row.pg_get_functiondef) {
return { return {
definer: null, definer: null,
sql: '', sql: '',
parameters: [], parameters: [],
name: row.Procedure, name: func,
comment: '', comment: '',
security: 'DEFINER', security: 'DEFINER',
deterministic: false, deterministic: false,
dataAccess: 'CONTAINS SQL', dataAccess: 'CONTAINS SQL'
returns: 'INT',
returnsLength: null
}; };
} }
const parameters = row['Create Function'] const sql = `SELECT proc.specific_schema AS procedure_schema,
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0] proc.specific_name,
.replaceAll('\r', '') proc.routine_name AS procedure_name,
.replaceAll('\t', '') proc.external_language,
.slice(1, -1) args.parameter_name,
.split(',') args.parameter_mode,
.map(el => { args.data_type
const param = el.split(' '); FROM information_schema.routines proc
const type = param[1] ? param[1].replace(')', '').split('(') : ['', null]; 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 { const results = await this.raw(sql);
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0],
length: +type[1] ? +type[1].replace(/\D/g, '') : ''
};
}).filter(el => el.name);
let dataAccess = 'CONTAINS SQL'; const parameters = results.rows.filter(row => row.parameter_mode).map(row => {
if (row['Create Function'].includes('NO SQL')) return {
dataAccess = 'NO SQL'; name: row.parameter_name,
if (row['Create Function'].includes('READS SQL DATA')) type: row.data_type.toUpperCase(),
dataAccess = 'READS SQL DATA'; length: '',
if (row['Create Function'].includes('MODIFIES SQL DATA')) context: row.parameter_mode
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];
return { return {
definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], definer: '',
sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0], sql: row.pg_get_functiondef.match(/(\$(.*)\$)(.*)(\$(.*)\$)/gs)[0],
parameters: parameters || [], parameters: parameters || [],
name: row.Function, name: func,
comment: row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '', comment: '',
security: row['Create Function'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER', security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
deterministic: row['Create Function'].includes('DETERMINISTIC'), deterministic: null,
dataAccess, dataAccess: null,
returns: output[0].toUpperCase(), language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0],
returnsLength: +output[1] returns: row.pg_get_functiondef.match(/(?<=RETURNS )(.*)(?<=[\S+\n\r\s])/gm)[0].replace('SETOF ', '').toUpperCase()
}; };
})[0]; })[0];
} }
@@ -768,7 +762,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropFunction (params) { async dropFunction (params) {
const sql = `DROP FUNCTION \`${params.func}\``; const sql = `DROP FUNCTION ${this._schema}.${params.func}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -801,18 +795,23 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createFunction (func) { async createFunction (func) {
const parameters = func.parameters.reduce((acc, curr) => { const parameters = 'parameters' in func
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`); ? func.parameters.reduce((acc, curr) => {
return acc; acc.push(`${curr.context} ${curr.name} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
}, []).join(','); return acc;
}, []).join(',')
: '';
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns}${func.returnsLength ? `(${func.returnsLength})` : ''} if (this._schema !== 'public')
LANGUAGE SQL await this.use(this._schema);
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess} const body = func.returns ? func.sql : '$BODY$\n$BODY$';
SQL SECURITY ${func.security}
COMMENT '${func.comment}' const sql = `CREATE FUNCTION ${this._schema}.${func.name}(${parameters})
${func.sql}`; RETURNS ${func.returns || 'void'}
LANGUAGE ${func.language}
SECURITY ${func.security}
AS ${body}`;
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
@@ -1000,7 +999,7 @@ export class PostgreSQLClient extends AntaresCore {
name name
} = params; } = params;
const sql = `CREATE TABLE ${name} (${name}_id INTEGER NULL); ALTER TABLE ${name} DROP COLUMN ${name}_id`; const sql = `CREATE TABLE ${this._schema}.${name} (${name}_id INTEGER NULL); ALTER TABLE ${this._schema}.${name} DROP COLUMN ${name}_id`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -1022,6 +1021,9 @@ export class PostgreSQLClient extends AntaresCore {
options options
} = params; } = params;
if (this._schema !== 'public')
await this.use(this._schema);
let sql = ''; let sql = '';
const alterColumns = []; const alterColumns = [];
const renameColumns = []; const renameColumns = [];
@@ -1040,7 +1042,7 @@ export class PostgreSQLClient extends AntaresCore {
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false; const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN ${addition.name} alterColumns.push(`ADD COLUMN ${addition.name}
${addition.type.toUpperCase()}${length ? `(${length})` : ''} ${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''}
${addition.unsigned ? 'UNSIGNED' : ''} ${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''} ${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'} ${addition.nullable ? 'NULL' : 'NOT NULL'}
@@ -1061,7 +1063,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (type === 'UNIQUE') else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${addition.name} UNIQUE (${fields})`); alterColumns.push(`ADD CONSTRAINT ${addition.name} UNIQUE (${fields})`);
else else
manageIndexes.push(`CREATE INDEX ${addition.name} ON ${table}(${fields})`); manageIndexes.push(`CREATE INDEX ${addition.name} ON ${this._schema}.${table}(${fields})`);
}); });
// ADD FOREIGN KEYS // ADD FOREIGN KEYS
@@ -1089,24 +1091,22 @@ export class PostgreSQLClient extends AntaresCore {
localType = change.type.toLowerCase(); localType = change.type.toLowerCase();
} }
alterColumns.push(`ALTER COLUMN "${change.orgName}" TYPE ${localType}${length ? `(${length})` : ''} USING "${change.orgName}"::${localType}`); alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.orgName}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`); alterColumns.push(`ALTER COLUMN "${change.name}" ${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}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
if (['SERIAL', 'SMALLSERIAL', 'BIGSERIAL'].includes(change.type)) { if (['SERIAL', 'SMALLSERIAL', 'BIGSERIAL'].includes(change.type)) {
const sequenceName = `${table}_${change.name}_seq`.replace(' ', '_'); const sequenceName = `${table}_${change.name}_seq`.replace(' ', '_');
createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.orgName}"`); createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.name}"`);
alterColumns.push(`ALTER COLUMN "${change.orgName}" SET DEFAULT nextval('${sequenceName}')`); alterColumns.push(`ALTER COLUMN "${change.name}" SET DEFAULT nextval('${sequenceName}')`);
} }
if (change.orgName !== change.name) if (change.orgName !== change.name)
renameColumns.push(`ALTER TABLE "${table}" RENAME COLUMN "${change.orgName}" TO "${change.name}"`); renameColumns.push(`ALTER TABLE "${this._schema}"."${table}" RENAME COLUMN "${change.orgName}" TO "${change.name}"`);
}); });
// CHANGE INDEX // CHANGE INDEX
indexChanges.changes.forEach(change => { indexChanges.changes.forEach(change => {
if (change.oldType === 'PRIMARY') if (['PRIMARY', 'UNIQUE'].includes(change.oldType))
alterColumns.push('DROP PRIMARY KEY');
else if (change.oldType === 'UNIQUE')
alterColumns.push(`DROP CONSTRAINT ${change.oldName}`); alterColumns.push(`DROP CONSTRAINT ${change.oldName}`);
else else
manageIndexes.push(`DROP INDEX ${change.oldName}`); manageIndexes.push(`DROP INDEX ${change.oldName}`);
@@ -1119,7 +1119,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (type === 'UNIQUE') else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${change.name} UNIQUE (${fields})`); alterColumns.push(`ADD CONSTRAINT ${change.name} UNIQUE (${fields})`);
else else
manageIndexes.push(`CREATE INDEX ${change.name} ON ${table}(${fields})`); manageIndexes.push(`CREATE INDEX ${change.name} ON ${this._schema}.${table}(${fields})`);
}); });
// CHANGE FOREIGN KEYS // CHANGE FOREIGN KEYS
@@ -1146,13 +1146,13 @@ export class PostgreSQLClient extends AntaresCore {
alterColumns.push(`DROP CONSTRAINT ${deletion.constraintName}`); alterColumns.push(`DROP CONSTRAINT ${deletion.constraintName}`);
}); });
if (alterColumns.length) sql += `ALTER TABLE "${table}" ${alterColumns.join(', ')}; `; 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 (createSequences.length) sql = `${createSequences.join(';')}; ${sql}`;
if (manageIndexes.length) sql = `${manageIndexes.join(';')}; ${sql}`; if (manageIndexes.length) sql = `${manageIndexes.join(';')}; ${sql}`;
if (options.name) sql += `ALTER TABLE "${table}" RENAME TO "${options.name}"; `; 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); return await this.raw(sql);
} }
@@ -1164,7 +1164,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async truncateTable (params) { async truncateTable (params) {
const sql = `TRUNCATE TABLE ${params.table}`; const sql = `TRUNCATE TABLE ${this._schema}.${params.table}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -1175,7 +1175,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropTable (params) { async dropTable (params) {
const sql = `DROP TABLE ${params.table}`; const sql = `DROP TABLE ${this._schema}.${params.table}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -1249,6 +1249,10 @@ export class PostgreSQLClient extends AntaresCore {
split: true, split: true,
...args ...args
}; };
if (args.nest && this._schema !== 'public')
await this.use(this._schema);
const resultsArr = []; const resultsArr = [];
let paramsArr = []; let paramsArr = [];
const queries = args.split ? sql.split(';') : [sql]; const queries = args.split ? sql.split(';') : [sql];
@@ -1338,7 +1342,7 @@ export class PostgreSQLClient extends AntaresCore {
if (!paramObj.table || !paramObj.schema) continue; if (!paramObj.table || !paramObj.schema) continue;
try { // Column details try { // Column details
const columns = await this.getTableColumns(paramObj); const columns = await this.getTableColumns(paramObj, false);
const indexes = await this.getTableIndexes(paramObj); const indexes = await this.getTableIndexes(paramObj);
remappedFields = remappedFields.map(field => { remappedFields = remappedFields.map(field => {

View File

@@ -58,6 +58,7 @@ export default {
}, },
mounted () { mounted () {
ipcRenderer.send('check-for-updates'); ipcRenderer.send('check-for-updates');
this.checkVersionUpdate();
const Menu = remote.Menu; const Menu = remote.Menu;
@@ -100,7 +101,8 @@ export default {
}, },
methods: { methods: {
...mapActions({ ...mapActions({
showNewConnModal: 'application/showNewConnModal' showNewConnModal: 'application/showNewConnModal',
checkVersionUpdate: 'application/checkVersionUpdate'
}) })
} }
}; };

View File

@@ -69,7 +69,7 @@ export default {
this.editor = ace.edit(`editor-${this.id}`, { this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`, mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`, theme: `ace/theme/${this.editorTheme}`,
value: this.value, value: this.value || '',
fontSize: '14px', fontSize: '14px',
printMargin: false, printMargin: false,
readOnly: this.readOnly, readOnly: this.readOnly,

View File

@@ -51,7 +51,7 @@ export default {
}), }),
isValidDefault () { isValidDefault () {
if (!this.foreignList.length) return true; if (!this.foreignList.length) return true;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString()); return this.value === null || this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString());
} }
}, },
async created () { async created () {

View File

@@ -15,7 +15,7 @@
<div class="content"> <div class="content">
<form class="form-horizontal"> <form class="form-horizontal">
<div <div
v-for="(parameter, i) in localRoutine.parameters" v-for="(parameter, i) in inParameters"
:key="parameter._id" :key="parameter._id"
class="form-group" class="form-group"
> >
@@ -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"
> >
@@ -43,6 +43,7 @@
</template> </template>
<script> <script>
import { NUMBER, FLOAT } from 'common/fieldTypes';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
export default { export default {
@@ -57,13 +58,19 @@ export default {
} }
}, },
props: { props: {
localRoutine: Object localRoutine: Object,
client: String
}, },
data () { data () {
return { return {
values: {} values: {}
}; };
}, },
computed: {
inParameters () {
return this.localRoutine.parameters.filter(param => param.context === 'IN');
}
},
created () { created () {
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
@@ -81,8 +88,23 @@ 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) => {
const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr]; let qc;
switch (this.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
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); acc.push(value);
return acc; return acc;
}, []); }, []);

View File

@@ -7,7 +7,7 @@
> >
<template :slot="'header'"> <template :slot="'header'">
<div class="d-flex"> <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> </div>
</template> </template>
<div :slot="'body'"> <div :slot="'body'">
@@ -25,7 +25,19 @@
> >
</div> </div>
</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"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -53,42 +65,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" 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">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -111,7 +88,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -124,7 +101,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -152,11 +129,12 @@ export default {
return { return {
localFunction: { localFunction: {
definer: '', definer: '',
sql: 'BEGIN\r\n RETURN NULL;\r\nEND', sql: '',
parameters: [], parameters: [],
name: '', name: '',
comment: '', comment: '',
returns: 'INT', language: null,
returns: null,
returnsLength: 10, returnsLength: 10,
security: 'DEFINER', security: 'DEFINER',
deterministic: false, deterministic: false,
@@ -168,9 +146,17 @@ export default {
computed: { computed: {
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
if (this.customizations.languages)
this.localFunction.language = this.customizations.languages[0];
if (this.customizations.procedureSql)
this.localFunction.sql = this.customizations.procedureSql;
setTimeout(() => { setTimeout(() => {
this.$refs.firstInput.focus(); this.$refs.firstInput.focus();
}, 20); }, 20);

View File

@@ -25,7 +25,19 @@
> >
</div> </div>
</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="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"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -53,7 +65,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -76,7 +88,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -89,7 +101,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -117,10 +129,11 @@ export default {
return { return {
localRoutine: { localRoutine: {
definer: '', definer: '',
sql: 'BEGIN\r\n\r\nEND', sql: '',
parameters: [], parameters: [],
name: '', name: '',
comment: '', comment: '',
language: null,
security: 'DEFINER', security: 'DEFINER',
deterministic: false, deterministic: false,
dataAccess: 'CONTAINS SQL' dataAccess: 'CONTAINS SQL'
@@ -131,9 +144,17 @@ export default {
computed: { computed: {
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
if (this.customizations.languages)
this.localRoutine.language = this.customizations.languages[0];
if (this.customizations.procedureSql)
this.localRoutine.sql = this.customizations.procedureSql;
setTimeout(() => { setTimeout(() => {
this.$refs.firstInput.focus(); this.$refs.firstInput.focus();
}, 20); }, 20);

View File

@@ -37,6 +37,13 @@
> >
<a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a> <a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li> </li>
<li
class="tab-item"
:class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')"
>
<a class="c-hand">{{ $t('word.changelog') }}</a>
</li>
<li <li
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'about'}" :class="{'active': selectedTab === 'about'}"
@@ -46,7 +53,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="selectedTab === 'general'" class="panel-body py-4"> <div v-show="selectedTab === 'general'" class="panel-body py-4">
<div class="container"> <div class="container">
<form class="form-horizontal columns"> <form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1"> <div class="column col-12 h6 text-uppercase mb-1">
@@ -134,7 +141,7 @@
</div> </div>
</div> </div>
<div v-if="selectedTab === 'themes'" class="panel-body py-4"> <div v-show="selectedTab === 'themes'" class="panel-body py-4">
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="column col-12 h6 text-uppercase mb-2"> <div class="column col-12 h6 text-uppercase mb-2">
@@ -207,17 +214,20 @@
</div> </div>
</div> </div>
<div v-if="selectedTab === 'update'" class="panel-body py-4"> <div v-show="selectedTab === 'update'" class="panel-body py-4">
<ModalSettingsUpdate /> <ModalSettingsUpdate />
</div> </div>
<div v-show="selectedTab === 'changelog'" class="panel-body py-4">
<ModalSettingsChangelog />
</div>
<div v-if="selectedTab === 'about'" class="panel-body py-4"> <div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center"> <div class="text-center">
<img :src="require('@/images/logo.svg').default" width="128"> <img :src="require('@/images/logo.svg').default" width="128">
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p> <p>
{{ $t('word.version') }} {{ appVersion }}<br> {{ $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('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small> <small>{{ $t('message.madeWithJS') }}</small>
</p> </p>
@@ -233,6 +243,7 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import localesNames from '@/i18n/supported-locales'; import localesNames from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate'; import ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog';
import BaseTextEditor from '@/components/BaseTextEditor'; import BaseTextEditor from '@/components/BaseTextEditor';
const { shell } = require('electron'); const { shell } = require('electron');
@@ -240,6 +251,7 @@ export default {
name: 'ModalSettings', name: 'ModalSettings',
components: { components: {
ModalSettingsUpdate, ModalSettingsUpdate,
ModalSettingsChangelog,
BaseTextEditor BaseTextEditor
}, },
data () { data () {
@@ -394,53 +406,59 @@ ORDER BY
<style lang="scss"> <style lang="scss">
#settings { #settings {
.modal-body { .modal-container {
overflow: hidden; position: absolute;
top: 17.5vh;
.panel-body { .modal-body {
height: calc(70vh - 70px); overflow: hidden;
overflow: auto;
.theme-block { .panel-body {
position: relative; min-height: calc(25vh - 70px);
text-align: center; max-height: 65vh;
overflow: auto;
&.selected { .theme-block {
img { position: relative;
box-shadow: 0 0 0 3px $primary-color; text-align: center;
&.selected {
img {
box-shadow: 0 0 0 3px $primary-color;
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
} }
} }
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
}
} }
}
.badge::after { .badge::after {
background: #32b643; background: #32b643;
} }
.badge-update::after { .badge-update::after {
bottom: initial; bottom: initial;
background: $primary-color; background: $primary-color;
} }
.form-label { .form-label {
display: flex; display: flex;
align-items: center; align-items: center;
}
} }
} }
} }

View File

@@ -0,0 +1,82 @@
<template>
<div class="p-relative">
<BaseLoader v-if="isLoading" />
<div
id="changelog"
class="container"
v-html="changelog"
/>
<div v-if="isError" class="empty">
<div class="empty-icon">
<i class="mdi mdi-48px mdi-alert-outline" />
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import marked from 'marked';
import BaseLoader from '@/components/BaseLoader';
export default {
name: 'ModalSettingsChangelog',
components: {
BaseLoader
},
data () {
return {
changelog: '',
isLoading: true,
error: '',
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/tags/v${this.appVersion}`, {
method: 'GET'
});
const { body } = await apiRes.json();
const markdown = body.substr(0, body.indexOf('### Download'));
const renderer = {
link (href, title, text) {
return text;
},
listitem (text) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
}
};
marked.use({ renderer });
this.changelog = marked(markdown);
}
catch (err) {
this.error = err.message;
this.isError = true;
}
this.isLoading = false;
}
}
};
</script>
<style lang="scss">
#changelog {
h3 {
font-size: 1rem;
}
li {
margin-top: 0;
}
}
</style>

View File

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

View File

@@ -11,9 +11,9 @@
<div class="footer-right-elements"> <div class="footer-right-elements">
<ul class="footer-elements"> <ul class="footer-elements">
<li class="footer-element footer-link" @click="openOutside('https://github.com/sponsors/Fabio286')"> <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-coffee mr-1" /> <i class="mdi mdi-18px mdi-tree mr-1" />
<small>{{ $t('word.donate') }}</small> <small>{{ $t('message.plantATree') }}</small>
</li> </li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')"> <li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<i class="mdi mdi-18px mdi-bug" /> <i class="mdi mdi-18px mdi-bug" />

View File

@@ -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 '';
}
}
} }
} }
}; };

View File

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

View File

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

View File

@@ -32,6 +32,7 @@
<ModalAskParameters <ModalAskParameters
v-if="isAskingParameters" v-if="isAskingParameters"
:local-routine="localElement" :local-routine="localElement"
:client="workspace.client"
@confirm="runElement" @confirm="runElement"
@close="hideAskParamsModal" @close="hideAskParamsModal"
/> />
@@ -205,13 +206,13 @@ export default {
case 'maria': case 'maria':
case 'mysql': case 'mysql':
case 'pg': case 'pg':
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`; sql = `CALL ${this.localElement.name}(${params.join(',')})`;
break; break;
case 'mssql': case 'mssql':
sql = `EXEC ${this.localElement.name} ${params.join(',')}`; sql = `EXEC ${this.localElement.name} ${params.join(',')}`;
break; break;
default: default:
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`; sql = `CALL \`${this.localElement.name}\`(${params.join(',')})`;
} }
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true }); this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
@@ -247,9 +248,11 @@ export default {
switch (this.workspace.client) { // TODO: move in a better place switch (this.workspace.client) { // TODO: move in a better place
case 'maria': case 'maria':
case 'mysql': case 'mysql':
case 'pg':
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`; sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
break; break;
case 'pg':
sql = `SELECT ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql': case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`; sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break; break;

View File

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

View File

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

View File

@@ -49,7 +49,7 @@
<div class="tile-title"> <div class="tile-title">
{{ param.name }} {{ param.name }}
</div> </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>
<div class="tile-action"> <div class="tile-action">
<button <button
@@ -106,7 +106,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ $t('word.length') }}
</label> </label>
@@ -119,6 +119,37 @@
> >
</div> </div>
</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> </form>
<div v-if="!parametersProxy.length" class="empty"> <div v-if="!parametersProxy.length" class="empty">
<div class="empty-icon"> <div class="empty-icon">
@@ -168,6 +199,9 @@ export default {
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy); return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {

View File

@@ -26,7 +26,19 @@
> >
</div> </div>
</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"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -54,7 +66,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -77,7 +89,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDataAccess" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -90,7 +102,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -124,6 +136,9 @@ export default {
computed: { computed: {
isTableNameValid () { isTableNameValid () {
return this.optionsProxy.name !== ''; return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
} }
}, },
created () { created () {

View File

@@ -106,7 +106,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ $t('word.length') }}
</label> </label>
@@ -119,7 +119,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureContext" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.context') }} {{ $t('word.context') }}
</label> </label>
@@ -199,6 +199,9 @@ export default {
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy); return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
@@ -235,10 +238,10 @@ export default {
addParameter () { addParameter () {
this.parametersProxy = [...this.parametersProxy, { this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(), _id: uidGen(),
name: `Param${this.i++}`, name: `param${this.i++}`,
type: 'INT', type: this.workspace.dataTypes[0].types[0].name,
context: 'IN', context: 'IN',
length: 10 length: ''
}]; }];
if (this.parametersProxy.length === 1) if (this.parametersProxy.length === 1)

View File

@@ -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>
@@ -68,6 +69,7 @@
@remove-field="removeField" @remove-field="removeField"
@add-new-index="addNewIndex" @add-new-index="addNewIndex"
@add-to-index="addToIndex" @add-to-index="addToIndex"
@rename-field="renameField"
/> />
</div> </div>
<WorkspacePropsOptionsModal <WorkspacePropsOptionsModal
@@ -149,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 () {
@@ -162,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;
@@ -199,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',
@@ -457,6 +466,20 @@ export default {
scrollable.scrollTop = scrollable.scrollHeight + 30; scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20); }, 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) { removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid); this.localFields = this.localFields.filter(field => field._id !== uid);
}, },
@@ -504,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();
}
}
} }
} }
}; };

View File

@@ -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>
@@ -47,7 +48,7 @@
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label> <label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
:value.sync="localFunction.sql" :value.sync="localFunction.sql"
:workspace="workspace" :workspace="workspace"
@@ -73,6 +74,7 @@
<ModalAskParameters <ModalAskParameters
v-if="isAskingParameters" v-if="isAskingParameters"
:local-routine="localFunction" :local-routine="localFunction"
:client="workspace.client"
@confirm="runFunction" @confirm="runFunction"
@close="hideAskParamsModal" @close="hideAskParamsModal"
/> />
@@ -119,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;
@@ -170,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',
@@ -280,9 +289,11 @@ export default {
switch (this.connection.client) { // TODO: move in a better place switch (this.connection.client) { // TODO: move in a better place
case 'maria': case 'maria':
case 'mysql': case 'mysql':
case 'pg':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`; sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break; break;
case 'pg':
sql = `SELECT ${this.originalFunction.name}(${params.join(',')})`;
break;
case 'mssql': case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`; sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break; break;
@@ -309,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();
}
}
} }
} }
}; };

View File

@@ -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>
@@ -74,6 +75,7 @@
<ModalAskParameters <ModalAskParameters
v-if="isAskingParameters" v-if="isAskingParameters"
:local-routine="localRoutine" :local-routine="localRoutine"
:client="workspace.client"
@confirm="runRoutine" @confirm="runRoutine"
@close="hideAskParamsModal" @close="hideAskParamsModal"
/> />
@@ -120,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;
@@ -171,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',
@@ -281,13 +290,13 @@ export default {
case 'maria': case 'maria':
case 'mysql': case 'mysql':
case 'pg': case 'pg':
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`; sql = `CALL ${this.originalRoutine.name}(${params.join(',')})`;
break; break;
case 'mssql': case 'mssql':
sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`; sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`;
break; break;
default: default:
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`; sql = `CALL \`${this.originalRoutine.name}\`(${params.join(',')})`;
} }
this.newTab({ uid: this.connection.uid, content: sql, autorun: true }); this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
@@ -309,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();
}
}
} }
} }
}; };

View File

@@ -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();
}
}
} }
} }
}; };

View File

@@ -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();
}
}
} }
} }
}; };

View File

@@ -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();
}
}
} }
} }
}; };

View File

@@ -42,6 +42,13 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.tableArray" class="th">
<div class="column-resizable">
<div class="table-column-title">
{{ $t('word.array') }}
</div>
</div>
</div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
@@ -108,6 +115,7 @@
:data-types="dataTypes" :data-types="dataTypes"
:customizations="customizations" :customizations="customizations"
@contextmenu="contextMenu" @contextmenu="contextMenu"
@rename-field="$emit('rename-field', $event)"
/> />
</draggable> </draggable>
</div> </div>

View File

@@ -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 }}
@@ -76,6 +79,16 @@
</optgroup> </optgroup>
</select> </select>
</div> </div>
<div
v-if="customizations.tableArray"
class="td"
tabindex="0"
>
<label class="form-checkbox">
<input v-model="localRow.isArray" type="checkbox">
<i class="form-icon" />
</label>
</div>
<div class="td type-int" tabindex="0"> <div class="td type-int" tabindex="0">
<template v-if="fieldType.length"> <template v-if="fieldType.length">
<span <span
@@ -364,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: {
@@ -430,9 +453,6 @@ export default {
this.defaultValue.expression = this.localRow.default; this.defaultValue.expression = this.localRow.default;
} }
}, },
updateRow () {
this.$emit('input', this.localRow);
},
editON (event, content, field) { editON (event, content, field) {
if (field === 'length') { if (field === 'length') {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.editingField = 'numLength'; if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.editingField = 'numLength';
@@ -459,6 +479,9 @@ export default {
} }
}, },
editOFF () { editOFF () {
if (this.editingField === 'name')
this.$emit('rename-field', { old: this.localRow[this.editingField], new: this.editingContent });
this.localRow[this.editingField] = this.editingContent; this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'type' && this.editingContent !== this.originalContent) { if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
@@ -481,8 +504,7 @@ export default {
if (!this.fieldType.zerofill) if (!this.fieldType.zerofill)
this.localRow.zerofill = false; this.localRow.zerofill = false;
} }
else if (this.editingField === 'default') {
if (this.editingField === 'default') {
switch (this.defaultValue.type) { switch (this.defaultValue.type) {
case 'autoincrement': case 'autoincrement':
this.localRow.autoIncrement = true; this.localRow.autoIncrement = true;

View File

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

View File

@@ -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,14 +104,17 @@ 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';
export default { export default {
name: 'WorkspaceQueryTable', name: 'WorkspaceQueryTable',
components: { components: {
BaseVirtualScroll, BaseVirtualScroll,
WorkspaceQueryTableRow, WorkspaceQueryTableRow,
TableContext TableContext,
ConfirmModal
}, },
props: { props: {
results: Array, results: Array,
@@ -105,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: [],
@@ -122,12 +145,13 @@ export default {
return this.getWorkspace(this.connUid).breadcrumbs.schema; return this.getWorkspace(this.connUid).breadcrumbs.schema;
}, },
primaryField () { primaryField () {
const primaryFields = this.fields.filter(field => ['pri', 'uni'].includes(field.key)); const primaryFields = this.fields.filter(field => field.key === 'pri');
const uniqueFields = this.fields.filter(field => field.key === 'uni');
if (primaryFields.length > 1 || !primaryFields.length) if ((primaryFields.length > 1 || !primaryFields.length) && (uniqueFields.length > 1 || !uniqueFields.length))
return false; return false;
return primaryFields[0]; return primaryFields[0] || uniqueFields[0];
}, },
isSortable () { isSortable () {
return this.fields.every(field => field.name); return this.fields.every(field => field.name);
@@ -298,6 +322,17 @@ export default {
delete row._id; delete row._id;
delete orgRow._id; delete orgRow._id;
Object.keys(orgRow).forEach(key => { // remap the row
if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime
let datePrecision = '';
const precision = this.fields.find(field => field.name === key).datePrecision;
for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
orgRow[key] = moment(orgRow[key]).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
});
const params = { const params = {
primary: this.primaryField.name, primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex), schema: this.getSchema(this.resultsetIndex),
@@ -309,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;

View File

@@ -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();

View File

@@ -182,7 +182,7 @@ import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64'; import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT, HAS_TIMEZONE } from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask'; import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor'; import TextEditor from '@/components/BaseTextEditor';
@@ -286,6 +286,9 @@ export default {
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#'; timeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType))
timeMask += 'X##';
return { type: 'text', mask: timeMask }; return { type: 'text', mask: timeMask };
} }
@@ -299,6 +302,9 @@ export default {
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#'; datetimeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType))
datetimeMask += 'X##';
return { type: 'text', mask: datetimeMask }; return { type: 'text', mask: datetimeMask };
} }

View File

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

View File

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

View File

@@ -42,6 +42,7 @@ export default {
try { try {
const { status, response } = await Tables.deleteTableRows(params); const { status, response } = await Tables.deleteTableRows(params);
this.isQuering = false;
if (status === 'success') if (status === 'success')
this.reloadTable(); this.reloadTable();
@@ -50,9 +51,8 @@ export default {
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
this.isQuering = false;
} }
this.isQuering = false;
} }
} }
}; };

View File

@@ -30,6 +30,7 @@
"macaddr": $string-color, "macaddr": $string-color,
"macaddr8": $string-color, "macaddr8": $string-color,
"uuid": $string-color, "uuid": $string-color,
"regproc": $string-color,
"int": $number-color, "int": $number-color,
"tinyint": $number-color, "tinyint": $number-color,
"smallint": $number-color, "smallint": $number-color,
@@ -76,6 +77,7 @@
"tsvector": $array-color, "tsvector": $array-color,
"tsquery": $array-color, "tsquery": $array-color,
"pg_node_tree": $array-color, "pg_node_tree": $array-color,
"aclitem": $array-color,
"unknown": $unknown-color, "unknown": $unknown-color,
) )
); );

View File

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

View File

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

View File

@@ -106,6 +106,11 @@
border-color: $primary-color; border-color: $primary-color;
} }
.form-input[readonly] {
background-color: $bg-color-dark;
cursor: default;
}
.input-group .input-group-addon { .input-group .input-group-addon {
border-color: #3f3f3f; border-color: #3f3f3f;
background: $bg-color-dark; background: $bg-color-dark;
@@ -381,7 +386,6 @@
bottom: -10px; bottom: -10px;
right: 0; right: 0;
position: absolute; position: absolute;
background: $success-color;
} }
&.badge-update::after { &.badge-update::after {

View File

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

View File

@@ -1,10 +1,14 @@
'use strict'; 'use strict';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
export default { export default {
namespaced: true, namespaced: true,
strict: true, strict: true,
state: { state: {
app_name: 'Antares - SQL Client', app_name: 'Antares - SQL Client',
app_version: process.env.PACKAGE_VERSION || 0, app_version: process.env.PACKAGE_VERSION || 0,
cached_version: persistentStore.get('cached_version', 0),
is_loading: false, is_loading: false,
is_new_modal: false, is_new_modal: false,
is_setting_modal: false, is_setting_modal: false,
@@ -19,6 +23,7 @@ export default {
isLoading: state => state.is_loading, isLoading: state => state.is_loading,
appName: state => state.app_name, appName: state => state.app_name,
appVersion: state => state.app_version, appVersion: state => state.app_version,
cachedVersion: state => state.cached_version,
getBaseCompleter: state => state.base_completer, getBaseCompleter: state => state.base_completer,
getSelectedConnection: state => state.selected_conection, getSelectedConnection: state => state.selected_conection,
isNewModal: state => state.is_new_modal, isNewModal: state => state.is_new_modal,
@@ -54,6 +59,10 @@ export default {
HIDE_SCRATCHPAD (state) { HIDE_SCRATCHPAD (state) {
state.is_scratchpad = false; state.is_scratchpad = false;
}, },
CHANGE_CACHED_VERSION (state) {
state.cached_version = state.app_version;
persistentStore.set('cached_version', state.cached_version);
},
CHANGE_UPDATE_STATUS (state, status) { CHANGE_UPDATE_STATUS (state, status) {
state.update_status = status; state.update_status = status;
}, },
@@ -62,6 +71,12 @@ export default {
} }
}, },
actions: { actions: {
checkVersionUpdate ({ getters, commit, dispatch }) {
if (getters.appVersion !== getters.cachedVersion) {
dispatch('showSettingModal', 'changelog');
commit('CHANGE_CACHED_VERSION');
}
},
setLoadingStatus ({ commit }, payload) { setLoadingStatus ({ commit }, payload) {
commit('SET_LOADING_STATUS', payload); commit('SET_LOADING_STATUS', payload);
}, },

View File

@@ -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'));

View File

@@ -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: [],