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

Compare commits

..

39 Commits

Author SHA1 Message Date
27fd9ec203 chore(release): 0.0.13 2021-01-06 12:00:30 +01:00
0ec2710872 fix: wrong new stored routine modal icon 2021-01-06 12:00:09 +01:00
3bcd02fc4e feat: stored routines creation 2021-01-06 11:57:49 +01:00
aa33850286 feat: stored routines delete 2021-01-06 11:07:55 +01:00
82fdc0bcd7 feat: stored routines edit 2021-01-05 17:25:18 +01:00
d695c9f8d2 feat: triggers creation 2021-01-02 15:27:02 +01:00
b32132ad84 feat: triggers delete 2021-01-02 14:46:27 +01:00
3126625461 feat: triggers edit 2020-12-31 19:55:02 +01:00
ab307f82b1 feat: select definer in view creation/edit 2020-12-29 10:35:46 +01:00
0df2b836b1 fix: wrong or duplicate fields in some queries 2020-12-28 17:46:23 +01:00
6611aad840 perf: improved performance getting database structure 2020-12-28 13:05:30 +01:00
8c4aaec167 feat: views creation 2020-12-27 16:16:48 +01:00
b7053bdf80 fix: unable to rename views 2020-12-27 13:14:41 +01:00
b6b7be098a fix: breadcrumb not change after table rename 2020-12-27 13:08:13 +01:00
56f2a27f00 feat: views edit 2020-12-26 15:37:34 +01:00
dcf469ebed feat: views deletion 2020-12-26 14:47:15 +01:00
d94b49febf feat: option to toggle line wrap mode 2020-12-24 15:33:51 +01:00
3b4f1475df chore(release): 0.0.12 2020-12-24 10:58:03 +01:00
155154b43d feat: option to toggle editor auto completion 2020-12-24 10:40:22 +01:00
a95b8d188c feat: option to change editor theme 2020-12-23 18:07:50 +01:00
cb1fce6f99 feat: query editor auto-completer for tables and columns 2020-12-22 22:31:31 +01:00
0014f48079 refactor: migrated to ace from monaco-editor 2020-12-21 11:06:41 +01:00
fc35f271d7 feat: better security connections credentials storage 2020-12-18 18:44:32 +01:00
65ad0e954d ci: update travis configuration 2020-12-18 17:38:06 +01:00
8cafade8b1 chore(release): 0.0.11 2020-12-15 17:24:04 +01:00
d13b708377 chore: update REDME.md 2020-12-15 17:23:24 +01:00
206597e5b8 feat: foreign keys management 2020-12-15 17:08:36 +01:00
c5458159d1 fix: unable to switch tabs when no table selected 2020-12-11 18:22:07 +01:00
1476e899d1 feat: auto focus on first input in modals 2020-12-11 18:09:17 +01:00
797ab70e7c chore: update links 2020-12-11 16:05:32 +01:00
f81312aeb0 feat: query tabs auto focus 2020-12-11 15:55:18 +01:00
3ed5ea023e fix: some properties do not reset after fields changes 2020-12-11 12:57:24 +01:00
9291a7a7b4 fix: file field editor not show 2020-12-10 15:15:32 +01:00
5cfdc9b92d fix: wrong field type detection 2020-12-09 18:22:46 +01:00
15b08d7ea8 fix: data tab sort not maintained at refresh 2020-12-08 18:41:08 +01:00
acebe435ff fix: improved changes dedection in props tab 2020-12-07 19:11:29 +01:00
5712b80022 feat: improved data table sorts 2020-12-07 17:51:48 +01:00
d38583262e fix: deletion of rows with non-numeric ID 2020-12-07 15:07:59 +01:00
e0e2131981 chore: update README.md 2020-12-04 11:43:27 +01:00
73 changed files with 6644 additions and 6486 deletions

5
.gitattributes vendored
View File

@@ -1 +1,6 @@
* text eol=lf * text eol=lf
*.jpg binary
*.png binary
*.gif binary
*.ico binary
*.icns binary

View File

@@ -31,11 +31,6 @@ jobs:
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
fetch-depth: 2 fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1

View File

@@ -1,9 +1,6 @@
language: node_js language: node_js
node_js: 12 node_js: 12
before_install:
- npm install
cache: cache:
directories: directories:
- node_modules - node_modules
@@ -20,6 +17,9 @@ env:
jobs: jobs:
include: include:
- stage: Test - stage: Test
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script: script:
- npm test - npm test
@@ -27,6 +27,9 @@ jobs:
if: tag IS present if: tag IS present
os: linux os: linux
services: docker services: docker
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script: 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" - 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: before_cache:
@@ -35,6 +38,8 @@ jobs:
- stage: Deploy Mac - stage: Deploy Mac
if: tag IS present if: tag IS present
os: osx os: osx
before_install:
- npm install
osx_image: xcode10.2 osx_image: xcode10.2
script: script:
- npm run build -- -p always - npm run build -- -p always

View File

@@ -2,6 +2,62 @@
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.0.13](https://github.com/Fabio286/antares/compare/v0.0.12...v0.0.13) (2021-01-06)
### Features
* option to toggle line wrap mode ([d94b49f](https://github.com/Fabio286/antares/commit/d94b49febf54b0200127859f2a8ed7ef591e56ab))
* select definer in view creation/edit ([ab307f8](https://github.com/Fabio286/antares/commit/ab307f82b1d78c5f9571233090b6678a964bd674))
* stored routines creation ([3bcd02f](https://github.com/Fabio286/antares/commit/3bcd02fc4ea9a4b780305212f906d6d78c7a8dae))
* stored routines delete ([aa33850](https://github.com/Fabio286/antares/commit/aa3385028685417860b3ce985cc7a74f9da377ad))
* stored routines edit ([82fdc0b](https://github.com/Fabio286/antares/commit/82fdc0bcd7514b321c1c9852a773adacf81baf87))
* triggers creation ([d695c9f](https://github.com/Fabio286/antares/commit/d695c9f8d2418a6a4523a7a242fa1a8cba80e035))
* triggers delete ([b32132a](https://github.com/Fabio286/antares/commit/b32132ad84d5798555b80eec3c624b681c37c339))
* triggers edit ([3126625](https://github.com/Fabio286/antares/commit/3126625461f4b6d68d641b6b0eda8fcd390bb636))
* views creation ([8c4aaec](https://github.com/Fabio286/antares/commit/8c4aaec167f58333a343b52927205b68137ad408))
* views deletion ([dcf469e](https://github.com/Fabio286/antares/commit/dcf469ebed6252b4a496800206e0c34cd83b1f5e))
* views edit ([56f2a27](https://github.com/Fabio286/antares/commit/56f2a27f0059cc10316204210db078a97408973c))
### Bug Fixes
* breadcrumb not change after table rename ([b6b7be0](https://github.com/Fabio286/antares/commit/b6b7be098ad5ab4d55bfe05a7f862f045c1f54da))
* unable to rename views ([b7053bd](https://github.com/Fabio286/antares/commit/b7053bdf8036d027e1685d6b5080d6b927a80e08))
* wrong new stored routine modal icon ([0ec2710](https://github.com/Fabio286/antares/commit/0ec2710872c692b3feac076a3250d3b760af4009))
* wrong or duplicate fields in some queries ([0df2b83](https://github.com/Fabio286/antares/commit/0df2b836b15436a2397d6a2202bd049b5cd53de4))
### [0.0.12](https://github.com/Fabio286/antares/compare/v0.0.11...v0.0.12) (2020-12-24)
### Features
* better security connections credentials storage ([fc35f27](https://github.com/Fabio286/antares/commit/fc35f271d7fe384cd786ce33547c0ef17135ddd8))
* option to change editor theme ([a95b8d1](https://github.com/Fabio286/antares/commit/a95b8d188cfcc8f563ad73b4f0b676d068775d36))
* option to toggle editor auto completion ([155154b](https://github.com/Fabio286/antares/commit/155154b43d0cd02ae875ded3ce865a37a999da5c))
* query editor auto-completer for tables and columns ([cb1fce6](https://github.com/Fabio286/antares/commit/cb1fce6f998ea7332886820910e245ab19416a9d))
### [0.0.11](https://github.com/Fabio286/antares/compare/v0.0.10...v0.0.11) (2020-12-15)
### Features
* auto focus on first input in modals ([1476e89](https://github.com/Fabio286/antares/commit/1476e899d164562f12342ced8c76903b9bdcfa55))
* foreign keys management ([206597e](https://github.com/Fabio286/antares/commit/206597e5b891e13e6f7635075bd11599355ab778))
* improved data table sorts ([5712b80](https://github.com/Fabio286/antares/commit/5712b8002203b32027f0e820f98a61e2ec965e79))
* query tabs auto focus ([f81312a](https://github.com/Fabio286/antares/commit/f81312aeb02ef55affd2ae9e81a9b4cb4c2e6da2))
### Bug Fixes
* data tab sort not maintained at refresh ([15b08d7](https://github.com/Fabio286/antares/commit/15b08d7ea858cb28111c7b548af9a13b1bf0da91))
* deletion of rows with non-numeric ID ([d385832](https://github.com/Fabio286/antares/commit/d38583262e672a2b47c5ad0aca0f13c129830a7b))
* file field editor not show ([9291a7a](https://github.com/Fabio286/antares/commit/9291a7a7b41e7aeb9b65c7f32e496f523e482272))
* improved changes dedection in props tab ([acebe43](https://github.com/Fabio286/antares/commit/acebe435ff6fa1581692fbf7ee1bc23b334e3947))
* some properties do not reset after fields changes ([3ed5ea0](https://github.com/Fabio286/antares/commit/3ed5ea023e1852d724b2b59ab156f8722876f85b))
* unable to switch tabs when no table selected ([c545815](https://github.com/Fabio286/antares/commit/c5458159d1e30cecf6615086c074d98b4b599637))
* wrong field type detection ([5cfdc9b](https://github.com/Fabio286/antares/commit/5cfdc9b92d4b778a7863b02fd64e6445236c89bc))
### [0.0.10](https://github.com/Fabio286/antares/compare/v0.0.9...v0.0.10) (2020-12-04) ### [0.0.10](https://github.com/Fabio286/antares/compare/v0.0.9...v0.0.10) (2020-12-04)

View File

@@ -9,7 +9,9 @@
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.
**At the moment this application is an alpha, it lacks many features, and isn't ready as a main SQL client**. However i'm actively working on it (yes, i'm a lone dev), hoping to provide all essential features as soon as possible. **At the moment this application is an alpha, it lacks many features** and supports only MySQL.
Most of its current features might be enough for basic MySQL use, so give it a chance and send me your feedback, I would really appreciate it.
I'm actively working on it (yes, i'm a lone dev), hoping to provide cool features and fixes as soon as possible.
🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases). 🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases).
👁 To stay tuned for new releases watch this repo on **Release only** channel. 👁 To stay tuned for new releases watch this repo on **Release only** channel.
@@ -29,33 +31,30 @@ An application created with minimalism and semplicity in mind, with features in
- Multiple database connections at same time. - Multiple database connections at same time.
- Database management (add/edit/delete). - Database management (add/edit/delete).
- Tables fields management (add/edit/delete). - Full tables management, including indexes and foreign keys.
- Tables content management (add/edit/delete).
- Run queries on multiple tabs. - Run queries on multiple tabs.
- Query suggestions. - Query suggestions and auto complete.
- Native dark theme. - Native dark theme.
- Multi language. - Multi language.
- Secure password storage.
- Auto updates. - Auto updates.
## Coming soon ## Coming soon
This is a roadmap with major features will come in near future. This is a roadmap with major features will come in near future.
- Tables management (add/edit/delete).
- Users management (add/edit/delete).
- Stored procedures, views, schedulers and triggers support. - Stored procedures, views, schedulers and triggers support.
- More secure password storage. - Users management (add/edit/delete).
- Database tools (variables, process list...). - Database tools (variables, process list...).
- SSL and SSH tunnel support.
- Support for other databases. - Support for other databases.
- Improvements of query editor area. - UI/UX improvements.
- Improvements of query suggestions.
- Query history. - Query history.
- More context menu shortcuts. - More context menu shortcuts.
- More keyboard shortcuts. - More keyboard shortcuts.
- Query logs console. - Query logs console.
- Fake data filler. - Fake data filler.
- Import/export and migration. - Import/export and migration.
- SSL and SSH tunnel support.
- Themes. - Themes.
## Currently supported ## Currently supported

5
jsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"include": [
"./src/renderer/**/*"
]
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.0.10", "version": "0.0.13",
"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",
@@ -48,11 +48,13 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^5.8.55", "@mdi/font": "^5.8.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.0", "electron-log": "^4.3.0",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5", "electron-updater": "^4.3.5",
"keytar": "^7.3.0",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"moment": "^2.29.1", "moment": "^2.29.1",
"monaco-editor": "^0.20.0",
"mssql": "^6.2.3", "mssql": "^6.2.3",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"pg": "^8.5.1", "pg": "^8.5.1",
@@ -78,7 +80,6 @@
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-vue": "^7.1.0", "eslint-plugin-vue": "^7.1.0",
"monaco-editor-webpack-plugin": "^1.9.1",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"sass-loader": "^10.1.0", "sass-loader": "^10.1.0",
"standard-version": "^9.0.0", "standard-version": "^9.0.0",

View File

@@ -206,7 +206,7 @@ module.exports = [
}, },
{ {
name: 'TIMESTAMP', name: 'TIMESTAMP',
length: true, length: false,
collation: false, collation: false,
unsigned: false, unsigned: false,
zerofill: false zerofill: false

View File

@@ -1,12 +1,12 @@
export const TEXT = ['char', 'varchar']; export const TEXT = ['CHAR', 'VARCHAR'];
export const LONG_TEXT = ['text', 'mediumtext', 'longtext']; export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'LONGTEXT'];
export const NUMBER = ['int', 'tinyint', 'smallint', 'mediumint', 'bigint', 'float', 'double', 'decimal', 'bool']; export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BOOL'];
export const DATE = ['date']; export const DATE = ['DATE'];
export const TIME = ['time']; export const TIME = ['TIME'];
export const DATETIME = ['datetime', 'timestamp']; export const DATETIME = ['DATETIME', 'TIMESTAMP'];
export const BLOB = ['blob', 'mediumblob', 'longblob']; export const BLOB = ['BLOB', 'MEDIUMBLOB', 'LONGBLOB'];
export const BIT = ['bit']; export const BIT = ['BIT'];

View File

@@ -2,7 +2,9 @@
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 ipcHandlers from './ipc-handlers'; import ipcHandlers from './ipc-handlers';
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -89,7 +91,14 @@ else {
}); });
// create main BrowserWindow when electron is ready // create main BrowserWindow when electron is ready
app.on('ready', () => { app.on('ready', async () => {
let key = await keytar.getPassword('antares', 'user');
if (!key) {
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
}
mainWindow = createMainWindow(); mainWindow = createMainWindow();
}); });
} }

View File

@@ -1,7 +1,13 @@
import keytar from 'keytar';
import { app, ipcMain } from 'electron'; import { app, ipcMain } from 'electron';
export default () => { export default () => {
ipcMain.on('close-app', () => { ipcMain.on('close-app', () => {
app.exit(); app.exit();
}); });
ipcMain.on('get-key', async event => {
const key = await keytar.getPassword('antares', 'user');
event.returnValue = key;
});
}; };

View File

@@ -1,15 +1,23 @@
import connection from './connection'; import connection from './connection';
import tables from './tables'; import tables from './tables';
import views from './views';
import triggers from './triggers';
import routines from './routines';
import updates from './updates'; import updates from './updates';
import application from './application'; import application from './application';
import database from './database'; import database from './database';
import users from './users';
const connections = {}; const connections = {};
export default () => { export default () => {
connection(connections); connection(connections);
tables(connections); tables(connections);
views(connections);
triggers(connections);
routines(connections);
database(connections); database(connections);
users(connections);
updates(); updates();
application(); application();
}; };

View File

@@ -0,0 +1,43 @@
import { ipcMain } from 'electron';
export default (connections) => {
ipcMain.handle('get-routine-informations', async (event, params) => {
try {
const result = await connections[params.uid].getRoutineInformations(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('drop-routine', async (event, params) => {
try {
await connections[params.uid].dropRoutine(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('alter-routine', async (event, params) => {
try {
await connections[params.uid].alterRoutine(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-routine', async (event, params) => {
try {
await connections[params.uid].createRoutine(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -14,14 +14,18 @@ export default (connections) => {
} }
}); });
ipcMain.handle('get-table-data', async (event, { uid, schema, table }) => { ipcMain.handle('get-table-data', async (event, { uid, schema, table, sortParams }) => {
try { try {
const result = await connections[uid] const query = connections[uid]
.select('*') .select('*')
.schema(schema) .schema(schema)
.from(table) .from(table)
.limit(1000) .limit(1000);
.run({ details: true });
if (sortParams && sortParams.field && sortParams.dir)
query.orderBy({ [sortParams.field]: sortParams.dir.toUpperCase() });
const result = await query.run({ details: true });
return { status: 'success', response: result }; return { status: 'success', response: result };
} }
@@ -89,11 +93,18 @@ export default (connections) => {
}); });
ipcMain.handle('delete-table-rows', async (event, params) => { ipcMain.handle('delete-table-rows', async (event, params) => {
let idString;
if (typeof params.rows[0] === 'string')
idString = params.rows.map(row => `"${row}"`).join(',');
else
idString = params.rows.join(',');
try { try {
const result = await connections[params.uid] const result = await connections[params.uid]
.schema(params.schema) .schema(params.schema)
.delete(params.table) .delete(params.table)
.where({ [params.primary]: `IN (${params.rows.join(',')})` }) .where({ [params.primary]: `IN (${idString})` })
.run(); .run();
return { status: 'success', response: result }; return { status: 'success', response: result };

View File

@@ -0,0 +1,43 @@
import { ipcMain } from 'electron';
export default (connections) => {
ipcMain.handle('get-trigger-informations', async (event, params) => {
try {
const result = await connections[params.uid].getTriggerInformations(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('drop-trigger', async (event, params) => {
try {
await connections[params.uid].dropTrigger(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('alter-trigger', async (event, params) => {
try {
await connections[params.uid].alterTrigger(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-trigger', async (event, params) => {
try {
await connections[params.uid].createTrigger(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -0,0 +1,15 @@
import { ipcMain } from 'electron';
export default (connections) => {
ipcMain.handle('get-users', async (event, uid) => {
try {
const result = await connections[uid].getUsers();
return { status: 'success', response: result };
}
catch (err) {
if (err.code === 'ER_TABLEACCESS_DENIED_ERROR')
return { status: 'success', response: [] };
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -0,0 +1,43 @@
import { ipcMain } from 'electron';
export default (connections) => {
ipcMain.handle('get-view-informations', async (event, params) => {
try {
const result = await connections[params.uid].getViewInformations(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('drop-view', async (event, params) => {
try {
await connections[params.uid].dropView(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('alter-view', async (event, params) => {
try {
await connections[params.uid].alterView(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-view', async (event, params) => {
try {
await connections[params.uid].createView(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -36,21 +36,23 @@ export class MySQLClient extends AntaresCore {
*/ */
async getStructure () { async getStructure () {
const { rows: databases } = await this.raw('SHOW DATABASES'); const { rows: databases } = await this.raw('SHOW DATABASES');
// TODO: SHOW TABLE STATUS FROM `{DATABASE_NAME}`;
const { rows: tables } = await this
.select('*')
.schema('information_schema')
.from('TABLES')
.orderBy({ TABLE_SCHEMA: 'ASC', TABLE_NAME: 'ASC' })
.run();
const { rows: functions } = await this.raw('SHOW FUNCTION STATUS'); const { rows: functions } = await this.raw('SHOW FUNCTION STATUS');
const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS'); const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS');
const { rows: schedulers } = await this.raw('SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM information_schema.`EVENTS`'); const { rows: schedulers } = await this.raw('SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM information_schema.`EVENTS`');
const tablesArr = [];
const triggersArr = []; const triggersArr = [];
for (const db of databases) { for (const db of databases) {
let { rows: tables } = await this.raw(`SHOW TABLE STATUS FROM \`${db.Database}\``);
if (tables.length) {
tables = tables.map(table => {
table.Db = db.Database;
return table;
});
tablesArr.push(...tables);
}
let { rows: triggers } = await this.raw(`SHOW TRIGGERS FROM \`${db.Database}\``); let { rows: triggers } = await this.raw(`SHOW TRIGGERS FROM \`${db.Database}\``);
if (triggers.length) { if (triggers.length) {
triggers = triggers.map(trigger => { triggers = triggers.map(trigger => {
@@ -63,9 +65,9 @@ export class MySQLClient extends AntaresCore {
return databases.map(db => { return databases.map(db => {
// TABLES // TABLES
const remappedTables = tables.filter(table => table.TABLE_SCHEMA === db.Database).map(table => { const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => {
let tableType; let tableType;
switch (table.TABLE_TYPE) { switch (table.Comment) {
case 'VIEW': case 'VIEW':
tableType = 'view'; tableType = 'view';
break; break;
@@ -75,16 +77,16 @@ export class MySQLClient extends AntaresCore {
} }
return { return {
name: table.TABLE_NAME, name: table.Name,
type: tableType, type: tableType,
rows: table.TABLE_ROWS, rows: table.Rows,
created: table.CREATE_TIME, created: table.Create_time,
updated: table.UPDATE_TIME, updated: table.Update_time,
engine: table.ENGINE, engine: table.Engine,
comment: table.TABLE_COMMENT, comment: table.Comment,
size: table.DATA_LENGTH + table.INDEX_LENGTH, size: table.Data_length + table.Index_length,
autoIncrement: table.AUTO_INCREMENT, autoIncrement: table.Auto_increment,
collation: table.TABLE_COLLATION collation: table.Collation
}; };
}); });
@@ -102,6 +104,20 @@ export class MySQLClient extends AntaresCore {
}; };
}); });
// FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => {
return {
name: func.Name,
type: func.Type,
definer: func.Definer,
created: func.Created,
updated: func.Modified,
comment: func.Comment,
charset: func.character_set_client,
security: func.Security_type
};
});
// SCHEDULERS // SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => { const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return { return {
@@ -146,7 +162,7 @@ export class MySQLClient extends AntaresCore {
return { return {
name: db.Database, name: db.Database,
tables: remappedTables, tables: remappedTables,
functions: functions.filter(func => func.Db === db.Database), // TODO: remap functions functions: remappedFunctions,
procedures: remappedProcedures, procedures: remappedProcedures,
triggers: remappedTriggers, triggers: remappedTriggers,
schedulers: remappedSchedulers schedulers: remappedSchedulers
@@ -177,7 +193,7 @@ export class MySQLClient extends AntaresCore {
return { return {
name: field.COLUMN_NAME, name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(), key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE, type: field.DATA_TYPE.toUpperCase(),
schema: field.TABLE_SCHEMA, schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME, table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION, numPrecision: field.NUMERIC_PRECISION,
@@ -237,21 +253,280 @@ export class MySQLClient extends AntaresCore {
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' }) .where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run(); .run();
const { rows: extras } = await this
.select('*')
.schema('information_schema')
.from('REFERENTIAL_CONSTRAINTS')
.where({ CONSTRAINT_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
return rows.map(field => { return rows.map(field => {
const extra = extras.find(x => x.CONSTRAINT_NAME === field.CONSTRAINT_NAME);
return { return {
schema: field.TABLE_SCHEMA, schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME, table: field.TABLE_NAME,
column: field.COLUMN_NAME, field: field.COLUMN_NAME,
position: field.ORDINAL_POSITION, position: field.ORDINAL_POSITION,
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT, constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
constraintName: field.CONSTRAINT_NAME, constraintName: field.CONSTRAINT_NAME,
refSchema: field.REFERENCED_TABLE_SCHEMA, refSchema: field.REFERENCED_TABLE_SCHEMA,
refTable: field.REFERENCED_TABLE_NAME, refTable: field.REFERENCED_TABLE_NAME,
refColumn: field.REFERENCED_COLUMN_NAME refField: field.REFERENCED_COLUMN_NAME,
onUpdate: extra.UPDATE_RULE,
onDelete: extra.DELETE_RULE
}; };
}); });
} }
/**
* SELECT `user`, `host`, IF(LENGTH(password)>0, password, authentication_string) AS `password` FROM `mysql`.`user`
*
* @returns {Array.<Object>} users list
* @memberof MySQLClient
*/
async getUsers () {
const { rows } = await this.raw('SELECT `user`, `host`, IF(LENGTH(password)>0, password, authentication_string) AS `password` FROM `mysql`.`user`');
return rows.map(row => {
return {
name: row.user,
host: row.host,
password: row.password
};
});
}
/**
* SHOW CREATE VIEW
*
* @returns {Array.<Object>} view informations
* @memberof MySQLClient
*/
async getViewInformations ({ schema, view }) {
const sql = `SHOW CREATE VIEW \`${schema}\`.\`${view}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
algorithm: row['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0],
definer: row['Create View'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
security: row['Create View'].match(/(?<=SQL SECURITY ).*?(?=\s)/gs)[0],
updateOption: row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs) ? row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs)[0] : '',
sql: row['Create View'].match(/(?<=AS ).*?$/gs)[0],
name: row.View
};
})[0];
}
/**
* DROP VIEW
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropView (params) {
const sql = `DROP VIEW \`${params.view}\``;
return await this.raw(sql);
}
/**
* ALTER VIEW
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterView (params) {
const { view } = params;
let sql = `ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} SQL SECURITY ${view.security} VIEW \`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
if (view.name !== view.oldName)
sql += `; RENAME TABLE \`${view.oldName}\` TO \`${view.name}\``;
return await this.raw(sql);
}
/**
* CREATE VIEW
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createView (view) {
const sql = `CREATE ALGORITHM = ${view.algorithm} ${view.definer ? `DEFINER=${view.definer} ` : ''}SQL SECURITY ${view.security} VIEW \`${view.name}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
return await this.raw(sql);
}
/**
* SHOW CREATE TRIGGER
*
* @returns {Array.<Object>} view informations
* @memberof MySQLClient
*/
async getTriggerInformations ({ schema, trigger }) {
const sql = `SHOW CREATE TRIGGER \`${schema}\`.\`${trigger}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
definer: row['SQL Original Statement'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['SQL Original Statement'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: row.Trigger,
table: row['SQL Original Statement'].match(/(?<=ON `).*?(?=`)/gs)[0],
event1: row['SQL Original Statement'].match(/(BEFORE|AFTER)/gs)[0],
event2: row['SQL Original Statement'].match(/(INSERT|UPDATE|DELETE)/gs)[0]
};
})[0];
}
/**
* DROP TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.trigger}\``;
return await this.raw(sql);
}
/**
* ALTER TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterTrigger (params) {
const { trigger } = params;
const tempTrigger = Object.assign({}, trigger);
tempTrigger.name = `Antares_${tempTrigger.name}_tmp`;
try {
await this.createTrigger(tempTrigger);
await this.dropTrigger({ trigger: tempTrigger.name });
await this.dropTrigger({ trigger: trigger.oldName });
await this.createTrigger(trigger);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createTrigger (trigger) {
const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${trigger.name}\` ${trigger.event1} ${trigger.event2} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW CREATE PROCEDURE
*
* @returns {Array.<Object>} view informations
* @memberof MySQLClient
*/
async getRoutineInformations ({ schema, routine }) {
const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
const parameters = row['Create Procedure']
.match(/(?<=\().*?(?=\))/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return {
name: param[1] ? param[1].replaceAll('`', '') : '',
type: type[0],
length: +type[1],
context: param[0] ? param[0].replace('\n', '') : ''
};
}).filter(el => el.name);
let dataAccess = 'CONTAINS SQL';
if (row['Create Procedure'].includes('NO SQL'))
dataAccess = 'NO SQL';
if (row['Create Procedure'].includes('READS SQL DATA'))
dataAccess = 'READS SQL DATA';
if (row['Create Procedure'].includes('MODIFIES SQL DATA'))
dataAccess = 'MODIFIES SQL DATA';
return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters,
name: row.Procedure,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
deterministic: row['Create Procedure'].includes('DETERMINISTIC'),
dataAccess
};
})[0];
}
/**
* DROP PROCEDURE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropRoutine (params) {
const sql = `DROP PROCEDURE \`${params.routine}\``;
return await this.raw(sql);
}
/**
* ALTER PROCEDURE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterRoutine (params) {
const { routine } = params;
const tempProcedure = Object.assign({}, routine);
tempProcedure.name = `Antares_${tempProcedure.name}_tmp`;
try {
await this.createRoutine(tempProcedure);
await this.dropRoutine({ routine: tempProcedure.name });
await this.dropRoutine({ routine: routine.oldName });
await this.createRoutine(routine);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE PROCEDURE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createRoutine (routine) {
const parameters = routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters})
LANGUAGE SQL
${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${routine.dataAccess}
SQL SECURITY ${routine.security}
COMMENT '${routine.comment}'
${routine.sql}`;
return await this.raw(sql, { split: false });
}
/** /**
* SHOW COLLATION * SHOW COLLATION
* *
@@ -346,6 +621,7 @@ export class MySQLClient extends AntaresCore {
deletions, deletions,
changes, changes,
indexChanges, indexChanges,
foreignChanges,
options options
} = params; } = params;
@@ -390,6 +666,11 @@ export class MySQLClient extends AntaresCore {
} }
}); });
// ADD FOREIGN KEYS
foreignChanges.additions.forEach(addition => {
alterColumns.push(`ADD CONSTRAINT \`${addition.constraintName}\` FOREIGN KEY (\`${addition.field}\`) REFERENCES \`${addition.refTable}\` (\`${addition.refField}\`) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
});
// CHANGE FIELDS // CHANGE FIELDS
changes.forEach(change => { changes.forEach(change => {
const length = change.numLength || change.charLength || change.datePrecision; const length = change.numLength || change.charLength || change.datePrecision;
@@ -427,6 +708,12 @@ export class MySQLClient extends AntaresCore {
} }
}); });
// CHANGE FOREIGN KEYS
foreignChanges.changes.forEach(change => {
alterColumns.push(`DROP FOREIGN KEY \`${change.oldName}\``);
alterColumns.push(`ADD CONSTRAINT \`${change.constraintName}\` FOREIGN KEY (\`${change.field}\`) REFERENCES \`${change.refTable}\` (\`${change.refField}\`) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
});
// DROP FIELDS // DROP FIELDS
deletions.forEach(deletion => { deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN \`${deletion.name}\``); alterColumns.push(`DROP COLUMN \`${deletion.name}\``);
@@ -440,6 +727,11 @@ export class MySQLClient extends AntaresCore {
alterColumns.push(`DROP INDEX \`${deletion.name}\``); alterColumns.push(`DROP INDEX \`${deletion.name}\``);
}); });
// DROP FOREIGN KEYS
foreignChanges.deletions.forEach(deletion => {
alterColumns.push(`DROP FOREIGN KEY \`${deletion.constraintName}\``);
});
sql += alterColumns.join(', '); sql += alterColumns.join(', ');
// RENAME // RENAME
@@ -536,6 +828,7 @@ export class MySQLClient extends AntaresCore {
* @param {object} args * @param {object} args
* @param {boolean} args.nest * @param {boolean} args.nest
* @param {boolean} args.details * @param {boolean} args.details
* @param {boolean} args.split
* @returns {Promise} * @returns {Promise}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
@@ -543,13 +836,14 @@ export class MySQLClient extends AntaresCore {
args = { args = {
nest: false, nest: false,
details: false, details: false,
split: true,
...args ...args
}; };
const nestTables = args.nest ? '.' : false; const nestTables = args.nest ? '.' : false;
const resultsArr = []; const resultsArr = [];
let paramsArr = []; let paramsArr = [];
let selectedFields = []; let selectedFields = [];
const queries = sql.split(';'); const queries = args.split ? sql.split(';') : [sql];
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
@@ -573,7 +867,7 @@ export class MySQLClient extends AntaresCore {
schema: field.db, schema: field.db,
table: field.table, table: field.table,
orgTable: field.orgTable, orgTable: field.orgTable,
type: 'varchar' type: 'VARCHAR'
}; };
}) })
: []; : [];
@@ -603,13 +897,13 @@ export class MySQLClient extends AntaresCore {
let detailedFields = response.length let detailedFields = response.length
? selectedFields.map(selField => { ? selectedFields.map(selField => {
return response.find(field => field.name === selField.name && field.table === selField.table); return response.find(field => field.name.toLowerCase() === selField.name.toLowerCase() && field.table === selField.table);
}).filter(el => !!el) }).filter(el => !!el)
: []; : [];
if (selectedFields.length) { if (selectedFields.length) {
detailedFields = detailedFields.map(field => { detailedFields = detailedFields.map(field => {
const aliasObj = remappedFields.find(resField => resField.orgName === field.name); const aliasObj = remappedFields.find(resField => resField.orgName === field.name && resField.orgTable === field.table);
return { return {
...field, ...field,
alias: aliasObj.name || field.name, alias: aliasObj.name || field.name,
@@ -620,12 +914,18 @@ export class MySQLClient extends AntaresCore {
if (!detailedFields.length) { if (!detailedFields.length) {
detailedFields = remappedFields.map(field => { detailedFields = remappedFields.map(field => {
const isInFields = fieldsArr.some(f => field.name.toLowerCase() === f.name.toLowerCase() && field.table === f.table);
if (!isInFields) {
return { return {
...field, ...field,
alias: field.name, alias: field.name,
tableAlias: field.table tableAlias: field.table
}; };
}); }
else
return false;
}).filter(Boolean);
} }
fieldsArr = fieldsArr ? [...fieldsArr, ...detailedFields] : detailedFields; fieldsArr = fieldsArr ? [...fieldsArr, ...detailedFields] : detailedFields;

View File

@@ -140,4 +140,10 @@ export default {
} }
} }
.disabled {
pointer-events: none;
filter: grayscale(100%);
opacity: 0.5;
}
</style> </style>

View File

@@ -1,10 +1,14 @@
<template> <template>
<select <select
ref="editField" ref="editField"
class="px-1" class="form-select pl-1 pr-4"
:class="{'small-select': size === 'small'}"
@change="onChange" @change="onChange"
@blur="$emit('blur')" @blur="$emit('blur')"
> >
<option v-if="!isValidDefault" :value="value">
{{ value }} - {{ $t('message.invalidDefault') }}
</option>
<option <option
v-for="row in foreignList" v-for="row in foreignList"
:key="row.foreignColumn" :key="row.foreignColumn"
@@ -30,7 +34,11 @@ export default {
}, },
props: { props: {
value: [String, Number], value: [String, Number],
keyUsage: Object keyUsage: Object,
size: {
type: String,
default: ''
}
}, },
data () { data () {
return { return {
@@ -40,7 +48,10 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
selectedWorkspace: 'workspaces/getSelected' selectedWorkspace: 'workspaces/getSelected'
}) }),
isValidDefault () {
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString());
}
}, },
async created () { async created () {
let firstTextField; let firstTextField;
@@ -64,7 +75,7 @@ export default {
try { // Foregn list try { // Foregn list
const { status, response } = await Tables.getForeignList({ const { status, response } = await Tables.getForeignList({
...params, ...params,
column: this.keyUsage.refColumn, column: this.keyUsage.refField,
description: firstTextField description: firstTextField
}); });

View File

@@ -19,6 +19,7 @@
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
ref="firstInput"
v-model="credentials.user" v-model="credentials.user"
class="form-input" class="form-input"
type="text" type="text"
@@ -63,6 +64,11 @@ export default {
} }
}; };
}, },
created () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: { methods: {
closeModal () { closeModal () {
this.$emit('close-asking'); this.$emit('close-asking');

View File

@@ -20,6 +20,7 @@
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
ref="firstInput"
v-model="localConnection.name" v-model="localConnection.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -172,6 +173,10 @@ export default {
created () { created () {
this.localConnection = Object.assign({}, this.connection); this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);

View File

@@ -33,7 +33,11 @@
<label class="form-label">{{ $t('word.collation') }}:</label> <label class="form-label">{{ $t('word.collation') }}:</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select v-model="database.collation" class="form-select"> <select
ref="firstInput"
v-model="database.collation"
class="form-select"
>
<option <option
v-for="collation in collations" v-for="collation in collations"
:key="collation.id" :key="collation.id"
@@ -114,6 +118,10 @@ export default {
}; };
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);

View File

@@ -20,6 +20,7 @@
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
ref="firstInput"
v-model="connection.name" v-model="connection.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -182,6 +183,10 @@ export default {
}, },
created () { created () {
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);

View File

@@ -19,6 +19,7 @@
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
ref="firstInput"
v-model="database.name" v-model="database.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -89,6 +90,9 @@ export default {
created () { created () {
this.database = { ...this.database, collation: this.defaultCollation }; this.database = { ...this.database, collation: this.defaultCollation };
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);

View File

@@ -0,0 +1,147 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewRoutine"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewRoutine') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localRoutine.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localRoutine.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localRoutine.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="localRoutine.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="localRoutine.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewRoutine',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localRoutine: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
parameters: [],
name: '',
comment: '',
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
}
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewRoutine () {
this.$emit('open-create-routine-editor', this.localRoutine);
}
}
};
</script>

View File

@@ -18,6 +18,7 @@
</label> </label>
<div class="column"> <div class="column">
<input <input
ref="firstInput"
v-model="localOptions.name" v-model="localOptions.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -83,7 +84,6 @@ export default {
ConfirmModal ConfirmModal
}, },
props: { props: {
table: String,
workspace: Object workspace: Object
}, },
data () { data () {
@@ -112,6 +112,10 @@ export default {
mounted () { mounted () {
this.localOptions.collation = this.defaultCollation; this.localOptions.collation = this.defaultCollation;
this.localOptions.engine = this.defaultEngine; this.localOptions.engine = this.defaultEngine;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
methods: { methods: {
confirmOptionsChange () { confirmOptionsChange () {

View File

@@ -25,6 +25,7 @@
<div class="input-group col-8 col-sm-12"> <div class="input-group col-8 col-sm-12">
<ForeignKeySelect <ForeignKeySelect
v-if="foreignKeys.includes(field.name)" v-if="foreignKeys.includes(field.name)"
ref="formInput"
class="form-select" class="form-select"
:value.sync="localRow[field.name]" :value.sync="localRow[field.name]"
:key-usage="getKeyUsage(field.name)" :key-usage="getKeyUsage(field.name)"
@@ -32,6 +33,7 @@
/> />
<input <input
v-else-if="inputProps(field).mask" v-else-if="inputProps(field).mask"
ref="formInput"
v-model="localRow[field.name]" v-model="localRow[field.name]"
v-mask="inputProps(field).mask" v-mask="inputProps(field).mask"
class="form-input" class="form-input"
@@ -41,6 +43,7 @@
> >
<input <input
v-else-if="inputProps(field).type === 'file'" v-else-if="inputProps(field).type === 'file'"
ref="formInput"
class="form-input" class="form-input"
type="file" type="file"
:disabled="fieldsToExclude.includes(field.name)" :disabled="fieldsToExclude.includes(field.name)"
@@ -49,13 +52,14 @@
> >
<input <input
v-else v-else
ref="formInput"
v-model="localRow[field.name]" v-model="localRow[field.name]"
class="form-input" class="form-input"
:type="inputProps(field).type" :type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)" :disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1" :tabindex="key+1"
> >
<span class="input-group-addon" :class="`type-${field.type}`"> <span class="input-group-addon" :class="`type-${field.type.toLowerCase()}`">
{{ field.type }} {{ fieldLength(field) | wrapNumber }} {{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span> </span>
<label class="form-checkbox ml-3" :title="$t('word.insert')"> <label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -146,7 +150,7 @@ export default {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
foreignKeys () { foreignKeys () {
return this.keyUsage.map(key => key.column); return this.keyUsage.map(key => key.field);
} }
}, },
watch: { watch: {
@@ -192,6 +196,12 @@ export default {
} }
this.localRow = { ...rowObj }; this.localRow = { ...rowObj };
// Auto focus
setTimeout(() => {
const firstSelectableInput = this.$refs.formInput.find(input => !input.disabled);
firstSelectableInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
@@ -296,7 +306,7 @@ export default {
this.localRow[field] = files[0].path; this.localRow[field] = files[0].path;
}, },
getKeyUsage (keyName) { getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName); return this.keyUsage.find(key => key.field === keyName);
}, },
onKey (e) { onKey (e) {
e.stopPropagation(); e.stopPropagation();

View File

@@ -0,0 +1,140 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewTrigger"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewTrigger') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localTrigger.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localTrigger.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.table') }}
</label>
<div class="column">
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.event') }}
</label>
<div class="column">
<div class="input-group">
<select v-model="localTrigger.event1" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select v-model="localTrigger.event2" class="form-select">
<option>INSERT</option>
<option>UPDATE</option>
<option>DELETE</option>
</select>
</div>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTrigger',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localTrigger: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
table: '',
event1: 'BEFORE',
event2: 'INSERT'
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
mounted () {
this.localTrigger.table = this.schemaTables.length ? this.schemaTables[0].name : '';
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewTrigger () {
this.$emit('open-create-trigger-editor', this.localTrigger);
}
}
};
</script>

View File

@@ -0,0 +1,192 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmOptionsChange"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-eye-plus mr-1" /> {{ $t('message.createNewView') }}
</div>
</template>
<div :slot="'body'">
<div class="container">
<div class="columns mb-4">
<div class="column col-6">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-6">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select v-model="localView.definer" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
</div>
</div>
</div>
<div class="columns">
<div class="column col-4">
<div class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="DEFINER"
>
<i class="form-icon" /> DEFINER
</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="INVOKER"
>
<i class="form-icon" /> INVOKER
</label>
</div>
</div>
<div class="column col-4">
<div class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
name="algorithm"
value="UNDEFINED"
>
<i class="form-icon" /> UNDEFINED
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="MERGE"
name="algorithm"
>
<i class="form-icon" /> MERGE
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="TEMPTABLE"
name="algorithm"
>
<i class="form-icon" /> TEMPTABLE
</label>
</div>
</div>
<div class="column col-4">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value=""
>
<i class="form-icon" /> None
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="CASCADED"
>
<i class="form-icon" /> CASCADED
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="LOCAL"
>
<i class="form-icon" /> LOCAL
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
<QueryEditor
ref="queryEditor"
:value.sync="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import QueryEditor from '@/components/QueryEditor';
export default {
name: 'ModalNewView',
components: {
ConfirmModal,
QueryEditor
},
props: {
workspace: Object
},
data () {
return {
localView: {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
},
isOptionsChanging: false,
editorHeight: 300
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
}
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
this.$emit('open-create-view-editor', this.localView);
}
}
};
</script>

View File

@@ -45,10 +45,13 @@
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="selectedTab === 'general'" class="panel-body py-4"> <div v-if="selectedTab === 'general'" class="panel-body py-4">
<form class="form-horizontal"> <div class="container">
<div class="col-8 col-sm-12"> <form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1">
{{ $t('word.application') }}
</div>
<div class="column col-8 col-sm-12 mb-2">
<div class="form-group mb-4"> <div class="form-group mb-4">
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
<label class="form-label"> <label class="form-label">
@@ -92,12 +95,104 @@
</div> </div>
</div> </div>
</div> </div>
<div class="column col-12 h6 mt-4 text-uppercase mb-1">
{{ $t('word.editor') }}
</div>
<div class="column col-8 col-sm-12">
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('word.autoCompletion') }}:
</label>
</div>
<div class="col-6 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleAutoComplete">
<input type="checkbox" :checked="selectedAutoComplete">
<i class="form-icon" />
</label>
</div>
</div>
</div>
<div class="column col-8 col-sm-12">
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('message.wrapLongLines') }}:
</label>
</div>
<div class="col-6 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleLineWrap">
<input type="checkbox" :checked="selectedLineWrap">
<i class="form-icon" />
</label>
</div>
</div>
</div>
</form> </form>
</div> </div>
</div>
<div v-if="selectedTab === 'themes'" class="panel-body py-4"> <div v-if="selectedTab === 'themes'" class="panel-body py-4">
<div class="text-center"> <div class="container">
<p>In future releases</p> <div class="columns">
<div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }}
</div>
<div class="column col-6 c-hand theme-block" :class="{'selected': applicationTheme === 'dark'}">
<img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name">
<i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.dark') }}
</div>
</div>
</div>
<div class="column col-6 theme-block disabled" :class="{'selected': applicationTheme === 'light'}">
<div class="theme-name">
<i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.light') }} (Coming)
</div>
</div>
</div>
</div>
<div class="columns mt-4">
<div class="column col-12 h6 text-uppercase mb-2 mt-4">
{{ $t('message.editorTheme') }}
</div>
<div class="column col-6 h5 mb-4">
<select
v-model="localEditorTheme"
class="form-select"
@change="changeEditorTheme(localEditorTheme)"
>
<optgroup
v-for="group in editorThemes"
:key="group.group"
:label="group.group"
>
<option
v-for="theme in group.themes"
:key="theme.name"
:value="theme.code"
:selected="editorTheme === theme.code"
>
{{ theme.name }}
</option>
</optgroup>
</select>
</div>
<div class="column col-12">
<QueryEditor
:value="exampleQuery"
:workspace="workspace"
:read-only="true"
:height="270"
/>
</div>
</div>
</div> </div>
</div> </div>
@@ -111,7 +206,8 @@
<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/EStarium/antares')">GitHub</a><br> <a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a><br>
<small>{{ $t('word.author') }}: <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small> <small>{{ $t('message.madeWithJS') }}</small>
</p> </p>
</div> </div>
@@ -126,18 +222,70 @@
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 QueryEditor from '@/components/QueryEditor';
const { shell } = require('electron'); const { shell } = require('electron');
export default { export default {
name: 'ModalSettings', name: 'ModalSettings',
components: { components: {
ModalSettingsUpdate ModalSettingsUpdate,
QueryEditor
}, },
data () { data () {
return { return {
localLocale: null, localLocale: null,
localTimeout: null, localTimeout: null,
selectedTab: 'general' localEditorTheme: null,
selectedTab: 'general',
editorThemes: [
{
group: this.$t('word.light'),
themes: [
{ code: 'chrome', name: 'Chrome' },
{ code: 'clouds', name: 'Clouds' },
{ code: 'crimson_editor', name: 'Crimson Editor' },
{ code: 'dawn', name: 'Dawn' },
{ code: 'dreamweaver', name: 'Dreamweaver' },
{ code: 'eclupse', name: 'Eclipse' },
{ code: 'github', name: 'GitHub' },
{ code: 'iplastic', name: 'IPlastic' },
{ code: 'solarized_light', name: 'Solarized Light' },
{ code: 'textmate', name: 'TextMate' },
{ code: 'tomorrow', name: 'Tomorrow' },
{ code: 'xcode', name: 'Xcode' },
{ code: 'kuroir', name: 'Kuroir' },
{ code: 'katzenmilch', name: 'KatzenMilch' },
{ code: 'sqlserver', name: 'SQL Server' }
]
},
{
group: this.$t('word.dark'),
themes: [
{ code: 'ambiance', name: 'Ambiance' },
{ code: 'chaos', name: 'Chaos' },
{ code: 'clouds_midnight', name: 'Clouds Midnight' },
{ code: 'dracula', name: 'Dracula' },
{ code: 'cobalt', name: 'Cobalt' },
{ code: 'gruvbox', name: 'Gruvbox' },
{ code: 'gob', name: 'Green on Black' },
{ code: 'idle_fingers', name: 'Idle Fingers' },
{ code: 'kr_theme', name: 'krTheme' },
{ code: 'merbivore', name: 'Merbivore' },
{ code: 'mono_industrial', name: 'Mono Industrial' },
{ code: 'monokai', name: 'Monokai' },
{ code: 'nord_dark', name: 'Nord Dark' },
{ code: 'pastel_on_dark', name: 'Pastel on Dark' },
{ code: 'solarized_dark', name: 'Solarized Dark' },
{ code: 'terminal', name: 'Terminal' },
{ code: 'tomorrow_night', name: 'Tomorrow Night' },
{ code: 'tomorrow_night_blue', name: 'Tomorrow Night Blue' },
{ code: 'tomorrow_night_bright', name: 'Tomorrow Night Bright' },
{ code: 'tomorrow_night_eighties', name: 'Tomorrow Night 80s' },
{ code: 'twilight', name: 'Twilight' },
{ code: 'vibrant_ink', name: 'Vibrant Ink' }
]
}
]
}; };
}, },
computed: { computed: {
@@ -146,8 +294,14 @@ export default {
appVersion: 'application/appVersion', appVersion: 'application/appVersion',
selectedSettingTab: 'application/selectedSettingTab', selectedSettingTab: 'application/selectedSettingTab',
selectedLocale: 'settings/getLocale', selectedLocale: 'settings/getLocale',
selectedAutoComplete: 'settings/getAutoComplete',
selectedLineWrap: 'settings/getLineWrap',
notificationsTimeout: 'settings/getNotificationsTimeout', notificationsTimeout: 'settings/getNotificationsTimeout',
updateStatus: 'application/getUpdateStatus' applicationTheme: 'settings/getApplicationTheme',
editorTheme: 'settings/getEditorTheme',
updateStatus: 'application/getUpdateStatus',
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}), }),
locales () { locales () {
const locales = []; const locales = [];
@@ -158,11 +312,32 @@ export default {
}, },
hasUpdates () { hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus); return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
},
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
exampleQuery () {
return `-- This is an example
SELECT
employee.id,
employee.first_name,
employee.last_name,
SUM(DATEDIFF("SECOND", call.start, call.end)) AS call_duration
FROM call
INNER JOIN employee ON call.employee_id = employee.id
GROUP BY
employee.id,
employee.first_name,
employee.last_name
ORDER BY
employee.id ASC;
`;
} }
}, },
created () { created () {
this.localLocale = this.selectedLocale; this.localLocale = this.selectedLocale;
this.localTimeout = this.notificationsTimeout; this.localTimeout = this.notificationsTimeout;
this.localEditorTheme = this.editorTheme;
this.selectedTab = this.selectedSettingTab; this.selectedTab = this.selectedSettingTab;
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
}, },
@@ -173,6 +348,9 @@ export default {
...mapActions({ ...mapActions({
closeModal: 'application/hideSettingModal', closeModal: 'application/hideSettingModal',
changeLocale: 'settings/changeLocale', changeLocale: 'settings/changeLocale',
changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap',
changeEditorTheme: 'settings/changeEditorTheme',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout' updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}), }),
selectTab (tab) { selectTab (tab) {
@@ -191,6 +369,12 @@ export default {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')
this.closeModal(); this.closeModal();
},
toggleAutoComplete () {
this.changeAutoComplete(!this.selectedAutoComplete);
},
toggleLineWrap () {
this.changeLineWrap(!this.selectedLineWrap);
} }
} }
}; };
@@ -204,6 +388,34 @@ export default {
.panel-body { .panel-body {
height: calc(70vh - 70px); height: calc(70vh - 70px);
overflow: auto; overflow: auto;
.theme-block {
position: relative;
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%;
text-shadow: 0 0 8px #000;
}
}
} }
.badge::after { .badge::after {

View File

@@ -1,48 +1,194 @@
<template> <template>
<div class="editor-wrapper"> <div class="editor-wrapper">
<div ref="editor" class="editor" /> <div
ref="editor"
class="editor"
:style="{height: `${height}px`}"
/>
</div> </div>
</template> </template>
<script> <script>
import * as ace from 'ace-builds';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import 'ace-builds/webpack-resolver';
import { completionItemProvider } from '@/suggestions/sql'; import '../libs/ext-language_tools';
import { mapGetters } from 'vuex';
monaco.languages.registerCompletionItemProvider('sql', completionItemProvider(monaco)); import Tables from '@/ipc-api/Tables';
export default { export default {
name: 'QueryEditor', name: 'QueryEditor',
props: { props: {
value: String value: String,
workspace: Object,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 }
}, },
data () { data () {
return { return {
editor: null editor: null,
fields: [],
baseCompleter: []
}; };
}, },
mounted () { computed: {
this.editor = monaco.editor.create(this.$refs.editor, { ...mapGetters({
value: this.value, editorTheme: 'settings/getEditorTheme',
language: 'sql', autoComplete: 'settings/getAutoComplete',
theme: 'vs-dark', lineWrap: 'settings/getLineWrap'
autoIndent: true, }),
minimap: { tables () {
enabled: false return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.tables);
return acc;
}, []).map(table => {
return {
name: table.name,
comment: table.comment,
type: table.type,
fields: []
};
})
: [];
}, },
contextmenu: false, mode () {
wordBasedSuggestions: true, switch (this.workspace.client) {
acceptSuggestionOnEnter: 'smart', case 'mysql':
quickSuggestions: true case 'maria':
return 'mysql';
case 'mssql':
return 'sqlserver';
case 'pg':
return 'pgsql';
default:
return 'sql';
}
},
lastWord () {
const words = this.value.split(' ');
return words[words.length - 1];
},
isLastWordATable () {
return /\w+\.\w*/gm.test(this.lastWord);
},
fieldsCompleter () {
return {
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
this.fields.forEach(field => {
completions.push({
value: field,
meta: 'column',
score: 1000
});
});
callback(null, completions);
}
};
}
},
watch: {
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
autoComplete () {
if (this.editor) {
this.editor.setOptions({
enableLiveAutocompletion: this.autoComplete
});
}
},
lineWrap () {
if (this.editor) {
this.editor.setOptions({
wrap: this.lineWrap
});
}
}
},
mounted () {
this.editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly
}); });
this.editor.onDidChangeModelContent(e => { this.editor.setOptions({
enableBasicAutocompletion: true,
wrap: this.lineWrap,
enableSnippets: true,
enableLiveAutocompletion: this.autoComplete
});
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
this.tables.forEach(table => {
completions.push({
value: table.name,
meta: table.type,
caption: table.comment
});
});
callback(null, completions);
}
});
this.baseCompleter = this.editor.completers;
this.editor.commands.on('afterExec', e => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
if (this.isLastWordATable || e.args === '.') {
if (e.args !== ' ') {
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop());
if (table) {
const params = {
uid: this.workspace.uid,
schema: this.schema,
table: table.name
};
Tables.getTableColumns(params).then(res => {
if (res.response.length)
this.fields = res.response.map(field => field.name);
this.editor.completers = [this.fieldsCompleter];
this.editor.execCommand('startAutocomplete');
}).catch(console.log);
}
else
this.editor.completers = this.baseCompleter;
}
else
this.editor.completers = this.baseCompleter;
}
else
this.editor.completers = this.baseCompleter;
}
});
this.editor.session.on('change', () => {
const content = this.editor.getValue(); const content = this.editor.getValue();
this.$emit('update:value', content); this.$emit('update:value', content);
}); });
},
beforeDestroy () { if (this.autoFocus) {
this.editor && this.editor.dispose(); setTimeout(() => {
this.editor.focus();
this.editor.resize();
}, 20);
}
setTimeout(() => {
this.editor.resize();
}, 20);
} }
}; };
</script> </script>
@@ -52,19 +198,25 @@ export default {
border-bottom: 1px solid #444; border-bottom: 1px solid #444;
.editor { .editor {
height: 200px;
width: 100%; width: 100%;
} }
} }
.CodeMirror { .ace_.mdi {
.CodeMirror-scroll { display: inline-block;
max-width: 100%; width: 17px;
} }
.CodeMirror-line { .ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
word-break: break-word !important; background-color: #c9561a99;
white-space: pre-wrap !important;
} }
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover {
background-color: #c9571a33;
border: none;
}
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight {
color: #e0d00c;
} }
</style> </style>

View File

@@ -15,7 +15,7 @@
<i class="mdi mdi-18px mdi-coffee mr-1" /> <i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small> <small>{{ $t('word.donate') }}</small>
</li> </li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/EStarium/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" />
</li> </li>
<li class="footer-element footer-link" @click="showSettingModal('about')"> <li class="footer-element footer-link" @click="showSettingModal('about')">

View File

@@ -13,25 +13,25 @@
</a> </a>
</li> </li>
<li <li
v-if="workspace.breadcrumbs.table" v-if="schemaChild"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'prop'}" :class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 'prop'})" @click="selectTab({uid: workspace.uid, tab: 'prop'})"
> >
<a class="tab-link"> <a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" /> <i class="mdi mdi-18px mdi-tune mr-1" />
<span :title="workspace.breadcrumbs.table">{{ $t('word.properties').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span> <span :title="schemaChild">{{ $t('word.properties').toUpperCase() }}: {{ schemaChild }}</span>
</a> </a>
</li> </li>
<li <li
v-if="workspace.breadcrumbs.table" v-if="workspace.breadcrumbs.table || workspace.breadcrumbs.view"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'data'}" :class="{'active': selectedTab === 'data'}"
@click="selectTab({uid: workspace.uid, tab: 'data'})" @click="selectTab({uid: workspace.uid, tab: 'data'})"
> >
<a class="tab-link"> <a class="tab-link">
<i class="mdi mdi-18px mdi-table mr-1" /> <i class="mdi mdi-18px mr-1" :class="workspace.breadcrumbs.table ? 'mdi-table' : 'mdi-table-eye'" />
<span :title="workspace.breadcrumbs.table">{{ $t('word.data').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span> <span :title="schemaChild">{{ $t('word.data').toUpperCase() }}: {{ schemaChild }}</span>
</a> </a>
</li> </li>
<li <li
@@ -66,15 +66,33 @@
</li> </li>
</ul> </ul>
<WorkspacePropsTab <WorkspacePropsTab
v-show="selectedTab === 'prop'" v-show="selectedTab === 'prop' && workspace.breadcrumbs.table"
:is-selected="selectedTab === 'prop'" :is-selected="selectedTab === 'prop'"
:connection="connection" :connection="connection"
:table="workspace.breadcrumbs.table" :table="workspace.breadcrumbs.table"
/> />
<WorkspacePropsTabView
v-show="selectedTab === 'prop' && workspace.breadcrumbs.view"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:view="workspace.breadcrumbs.view"
/>
<WorkspacePropsTabTrigger
v-show="selectedTab === 'prop' && workspace.breadcrumbs.trigger"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:trigger="workspace.breadcrumbs.trigger"
/>
<WorkspacePropsTabRoutine
v-show="selectedTab === 'prop' && workspace.breadcrumbs.procedure"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:routine="workspace.breadcrumbs.procedure"
/>
<WorkspaceTableTab <WorkspaceTableTab
v-show="selectedTab === 'data'" v-show="selectedTab === 'data'"
:connection="connection" :connection="connection"
:table="workspace.breadcrumbs.table" :table="workspace.breadcrumbs.table || workspace.breadcrumbs.view"
/> />
<WorkspaceQueryTab <WorkspaceQueryTab
v-for="tab of queryTabs" v-for="tab of queryTabs"
@@ -94,6 +112,9 @@ import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab'; import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
import WorkspaceTableTab from '@/components/WorkspaceTableTab'; import WorkspaceTableTab from '@/components/WorkspaceTableTab';
import WorkspacePropsTab from '@/components/WorkspacePropsTab'; import WorkspacePropsTab from '@/components/WorkspacePropsTab';
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
export default { export default {
name: 'Workspace', name: 'Workspace',
@@ -101,7 +122,10 @@ export default {
WorkspaceExploreBar, WorkspaceExploreBar,
WorkspaceQueryTab, WorkspaceQueryTab,
WorkspaceTableTab, WorkspaceTableTab,
WorkspacePropsTab WorkspacePropsTab,
WorkspacePropsTabView,
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine
}, },
props: { props: {
connection: Object connection: Object
@@ -123,7 +147,15 @@ export default {
return this.selectedWorkspace === this.connection.uid; return this.selectedWorkspace === this.connection.uid;
}, },
selectedTab () { selectedTab () {
if (this.workspace.breadcrumbs.table === null) if (
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
)
return this.queryTabs[0].uid; return this.queryTabs[0].uid;
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) || return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) ||
@@ -133,6 +165,13 @@ export default {
}, },
queryTabs () { queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query'); return this.workspace.tabs.filter(tab => tab.type === 'query');
},
schemaChild () {
for (const key in this.workspace.breadcrumbs) {
if (key === 'schema') continue;
if (this.workspace.breadcrumbs[key]) return this.workspace.breadcrumbs[key];
}
return false;
} }
}, },
async created () { async created () {
@@ -193,6 +232,7 @@ export default {
align-items: flex-start; align-items: flex-start;
flex-wrap: nowrap; flex-wrap: nowrap;
overflow: auto; overflow: auto;
margin-bottom: 0;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 2px; width: 2px;

View File

@@ -40,6 +40,7 @@
:connection="connection" :connection="connection"
@show-database-context="openDatabaseContext" @show-database-context="openDatabaseContext"
@show-table-context="openTableContext" @show-table-context="openTableContext"
@show-misc-context="openMiscContext"
/> />
</div> </div>
</div> </div>
@@ -54,12 +55,33 @@
@close="hideCreateTableModal" @close="hideCreateTableModal"
@open-create-table-editor="openCreateTableEditor" @open-create-table-editor="openCreateTableEditor"
/> />
<ModalNewView
v-if="isNewViewModal"
:workspace="workspace"
@close="hideCreateViewModal"
@open-create-view-editor="openCreateViewEditor"
/>
<ModalNewTrigger
v-if="isNewTriggerModal"
:workspace="workspace"
@close="hideCreateTriggerModal"
@open-create-trigger-editor="openCreateTriggerEditor"
/>
<ModalNewRoutine
v-if="isNewRoutineModal"
:workspace="workspace"
@close="hideCreateRoutineModal"
@open-create-routine-editor="openCreateRoutineEditor"
/>
<DatabaseContext <DatabaseContext
v-if="isDatabaseContext" v-if="isDatabaseContext"
:selected-database="selectedDatabase" :selected-database="selectedDatabase"
:context-event="databaseContextEvent" :context-event="databaseContextEvent"
@close-context="closeDatabaseContext" @close-context="closeDatabaseContext"
@show-create-table-modal="showCreateTableModal" @show-create-table-modal="showCreateTableModal"
@show-create-view-modal="showCreateViewModal"
@show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal"
@reload="refresh" @reload="refresh"
/> />
<TableContext <TableContext
@@ -69,19 +91,35 @@
@close-context="closeTableContext" @close-context="closeTableContext"
@reload="refresh" @reload="refresh"
/> />
<MiscContext
v-if="isMiscContext"
:selected-misc="selectedMisc"
:context-event="miscContextEvent"
@close-context="closeMiscContext"
@reload="refresh"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash'; import _ from 'lodash';// TODO: remove
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines';
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel'; import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase'; import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase';
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext'; import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext'; import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import ModalNewDatabase from '@/components/ModalNewDatabase'; import ModalNewDatabase from '@/components/ModalNewDatabase';
import ModalNewTable from '@/components/ModalNewTable'; import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger';
import ModalNewRoutine from '@/components/ModalNewRoutine';
export default { export default {
name: 'WorkspaceExploreBar', name: 'WorkspaceExploreBar',
@@ -90,8 +128,12 @@ export default {
WorkspaceExploreBarDatabase, WorkspaceExploreBarDatabase,
DatabaseContext, DatabaseContext,
TableContext, TableContext,
MiscContext,
ModalNewDatabase, ModalNewDatabase,
ModalNewTable ModalNewTable,
ModalNewView,
ModalNewTrigger,
ModalNewRoutine
}, },
props: { props: {
connection: Object, connection: Object,
@@ -100,15 +142,25 @@ export default {
data () { data () {
return { return {
isRefreshing: false, isRefreshing: false,
isNewDBModal: false, isNewDBModal: false,
isNewTableModal: false, isNewTableModal: false,
isNewViewModal: false,
isNewTriggerModal: false,
isNewRoutineModal: false,
localWidth: null, localWidth: null,
isDatabaseContext: false, isDatabaseContext: false,
isTableContext: false, isTableContext: false,
isMiscContext: false,
databaseContextEvent: null, databaseContextEvent: null,
tableContextEvent: null, tableContextEvent: null,
miscContextEvent: null,
selectedDatabase: '', selectedDatabase: '',
selectedTable: '' selectedTable: null,
selectedMisc: null
}; };
}, },
computed: { computed: {
@@ -215,6 +267,83 @@ export default {
}, },
closeTableContext () { closeTableContext () {
this.isTableContext = false; this.isTableContext = false;
},
openMiscContext (payload) {
this.selectedMisc = payload.misc;
this.miscContextEvent = payload.event;
this.isMiscContext = true;
},
closeMiscContext () {
this.isMiscContext = false;
},
showCreateViewModal () {
this.closeDatabaseContext();
this.isNewViewModal = true;
},
hideCreateViewModal () {
this.isNewViewModal = false;
},
async openCreateViewEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Views.createView(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, view: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateTriggerModal () {
this.closeDatabaseContext();
this.isNewTriggerModal = true;
},
hideCreateTriggerModal () {
this.isNewTriggerModal = false;
},
async openCreateTriggerEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Triggers.createTrigger(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, trigger: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateRoutineModal () {
this.closeDatabaseContext();
this.isNewRoutineModal = true;
},
hideCreateRoutineModal () {
this.isNewRoutineModal = false;
},
async openCreateRoutineEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Routines.createRoutine(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, procedure: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
} }
} }
}; };

View File

@@ -17,20 +17,136 @@
v-for="table of database.tables" v-for="table of database.tables"
:key="table.name" :key="table.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.name}" :class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@click="setBreadcrumbs({schema: database.name, table: table.name})" @click="setBreadcrumbs({schema: database.name, [table.type]: table.name})"
@contextmenu.prevent="showTableContext($event, table.name)" @contextmenu.prevent="showTableContext($event, table)"
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" /> <i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span>{{ table.name }}</span> <span>{{ table.name }}</span>
</a> </a>
<div class="table-size tooltip tooltip-left mr-1" :data-tooltip="formatBytes(table.size)"> <div
v-if="table.type === 'table'"
class="table-size tooltip tooltip-left mr-1"
:data-tooltip="formatBytes(table.size)"
>
<div class="pie" :style="piePercentage(table.size)" /> <div class="pie" :style="piePercentage(table.size)" />
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="database.triggers.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}">
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
{{ $tc('word.trigger', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="trigger of database.triggers"
:key="trigger.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@click="setBreadcrumbs({schema: database.name, trigger: trigger.name})"
@contextmenu.prevent="showMiscContext($event, {...trigger, type: 'trigger'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span>{{ trigger.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="database.procedures.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}">
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
{{ $tc('word.storedRoutine', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="procedure of database.procedures"
:key="procedure.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@click="setBreadcrumbs({schema: database.name, procedure: procedure.name})"
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span>{{ procedure.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="database.functions.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}">
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
{{ $tc('word.function', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="func of database.functions"
:key="func.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@click="setBreadcrumbs({schema: database.name, function: func.name})"
@contextmenu.prevent="showMiscContext($event, {...func, type: 'function'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" />
<span>{{ func.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="database.schedulers.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}">
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
{{ $tc('word.scheduler', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="scheduler of database.schedulers"
:key="scheduler.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@click="setBreadcrumbs({schema: database.name, scheduler: scheduler.name})"
@contextmenu.prevent="showMiscContext($event, {...scheduler, type: 'scheduler'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span>{{ scheduler.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
</div> </div>
</details> </details>
</template> </template>
@@ -68,11 +184,17 @@ export default {
}), }),
formatBytes, formatBytes,
showDatabaseContext (event, database) { showDatabaseContext (event, database) {
this.changeBreadcrumbs({ schema: database, table: null });
this.$emit('show-database-context', { event, database }); this.$emit('show-database-context', { event, database });
}, },
showTableContext (event, table) { showTableContext (event, table) {
this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name });
this.$emit('show-table-context', { event, table }); this.$emit('show-table-context', { event, table });
}, },
showMiscContext (event, misc) {
this.setBreadcrumbs({ schema: this.database.name, [misc.type]: misc.name });
this.$emit('show-misc-context', { event, misc });
},
piePercentage (val) { piePercentage (val) {
const perc = val / this.maxSize * 100; const perc = val / this.maxSize * 100;
return { background: `conic-gradient(lime ${perc}%, white 0)` }; return { background: `conic-gradient(lime ${perc}%, white 0)` };
@@ -88,6 +210,7 @@ export default {
<style lang="scss"> <style lang="scss">
.workspace-explorebar-database { .workspace-explorebar-database {
.database-name, .database-name,
.misc-name,
a.table-name { a.table-name {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -103,12 +226,20 @@ export default {
} }
.database-icon, .database-icon,
.table-icon { .table-icon,
.misc-icon {
opacity: 0.7; opacity: 0.7;
} }
} }
.database-name { .misc-name {
line-height: 1;
padding: 0.1rem 1rem 0.1rem 0.1rem;
position: relative;
}
.database-name,
.misc-name {
&:hover { &:hover {
color: $body-font-color; color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05); background: rgba($color: #fff, $alpha: 0.05);
@@ -138,6 +269,18 @@ export default {
margin-left: 1.2rem; margin-left: 1.2rem;
} }
.database-misc {
margin-left: 1.6rem;
.accordion[open] .accordion-header > .misc-icon:first-child::before {
content: "\F0770";
}
.accordion-body {
margin-bottom: 0.2rem;
}
}
.table-size { .table-size {
position: absolute; position: absolute;
right: 0; right: 0;

View File

@@ -10,6 +10,21 @@
<div class="context-element" @click="showCreateTableModal"> <div class="context-element" @click="showCreateTableModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div> </div>
<div class="context-element" @click="showCreateViewModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div>
<div class="context-element" @click="showCreateTriggerModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div>
<div class="context-element" @click="showCreateRoutineModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div>
<div class="context-element disabled" @click="false">
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div>
<div class="context-element disabled" @click="false">
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div>
</div> </div>
</div> </div>
<div class="context-element" @click="showEditModal"> <div class="context-element" @click="showEditModal">
@@ -84,6 +99,15 @@ export default {
showCreateTableModal () { showCreateTableModal () {
this.$emit('show-create-table-modal'); this.$emit('show-create-table-modal');
}, },
showCreateViewModal () {
this.$emit('show-create-view-modal');
},
showCreateTriggerModal () {
this.$emit('show-create-trigger-modal');
},
showCreateRoutineModal () {
this.$emit('show-create-routine-modal');
},
showDeleteModal () { showDeleteModal () {
this.isDeleteModal = true; this.isDeleteModal = true;
}, },
@@ -124,3 +148,8 @@ export default {
} }
}; };
</script> </script>
<style lang="scss" scoped>
.context-submenu {
min-width: 150px !important;
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedMisc.type === 'procedure'"
class="context-element disabled"
@click="showRunModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div>
<div class="context-element" @click="showDeleteModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div>
<ConfirmModal
v-if="isDeleteModal"
@confirm="deleteMisc"
@hide="hideDeleteModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ deleteMessage }}
</div>
</template>
<div slot="body">
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"?
</div>
</div>
</ConfirmModal>
</BaseContextMenu>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu,
ConfirmModal
},
props: {
contextEvent: MouseEvent,
selectedMisc: Object
},
data () {
return {
isDeleteModal: false,
isRunModal: false
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
deleteMessage () {
switch (this.selectedMisc.type) {
case 'trigger':
return this.$t('message.deleteTrigger');
case 'procedure':
return this.$t('message.deleteRoutine');
default:
return '';
}
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showRunModal () {
this.isRunModal = true;
},
hideRunModal () {
this.isRunModal = false;
},
closeContext () {
this.$emit('close-context');
},
async deleteMisc () {
try {
let res;
switch (this.selectedMisc.type) {
case 'trigger':
res = await Triggers.dropTrigger({
uid: this.selectedWorkspace,
trigger: this.selectedMisc.name
});
break;
case 'procedure':
res = await Routines.dropRoutine({
uid: this.selectedWorkspace,
routine: this.selectedMisc.name
});
break;
// case 'schedules':
// res = await Tables.dropScheduler({
// uid: this.selectedWorkspace,
// scheduler: this.selectedMisc.name
// });
// break;
}
const { status, response } = res;
if (status === 'success') {
this.changeBreadcrumbs({ [this.selectedMisc.type]: null });
this.closeContext();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
}
};
</script>

View File

@@ -3,7 +3,11 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div class="context-element" @click="showEmptyModal"> <div
v-if="selectedTable.type === 'table'"
class="context-element"
@click="showEmptyModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-off text-light pr-1" /> {{ $t('message.emptyTable') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table-off text-light pr-1" /> {{ $t('message.emptyTable') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
@@ -22,7 +26,7 @@
</template> </template>
<div slot="body"> <div slot="body">
<div class="mb-2"> <div class="mb-2">
{{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable }}</b>"? {{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
@@ -33,12 +37,12 @@
> >
<template slot="header"> <template slot="header">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-table-remove mr-1" /> {{ $t('message.deleteTable') }} <i class="mdi mdi-24px mdi-table-remove mr-1" /> {{ selectedTable.type === 'table' ? $t('message.deleteTable') : $t('message.deleteView') }}
</div> </div>
</template> </template>
<div slot="body"> <div slot="body">
<div class="mb-2"> <div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable }}</b>"? {{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
@@ -50,6 +54,7 @@ import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
export default { export default {
name: 'WorkspaceExploreBarTableContext', name: 'WorkspaceExploreBarTableContext',
@@ -59,7 +64,7 @@ export default {
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
selectedTable: String selectedTable: Object
}, },
data () { data () {
return { return {
@@ -103,11 +108,11 @@ export default {
try { try {
const { status, response } = await Tables.truncateTable({ const { status, response } = await Tables.truncateTable({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
table: this.selectedTable table: this.selectedTable.name
}); });
if (status === 'success') { if (status === 'success') {
if (this.selectedTable === this.workspace.breadcrumbs.table) if (this.selectedTable.name === this.workspace.breadcrumbs.table)
this.changeBreadcrumbs({ table: null }); this.changeBreadcrumbs({ table: null });
this.closeContext(); this.closeContext();
@@ -122,14 +127,26 @@ export default {
}, },
async deleteTable () { async deleteTable () {
try { try {
const { status, response } = await Tables.dropTable({ let res;
if (this.selectedTable.type === 'table') {
res = await Tables.dropTable({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
table: this.selectedTable table: this.selectedTable.name
}); });
}
else if (this.selectedTable.type === 'view') {
res = await Views.dropView({
uid: this.selectedWorkspace,
view: this.selectedTable.name
});
}
const { status, response } = res;
if (status === 'success') { if (status === 'success') {
if (this.selectedTable === this.workspace.breadcrumbs.table) if (this.selectedTable.name === this.workspace.breadcrumbs.table || this.selectedTable.name === this.workspace.breadcrumbs.view)
this.changeBreadcrumbs({ table: null }); this.changeBreadcrumbs({ table: null, view: null });
this.closeContext(); this.closeContext();
this.$emit('reload'); this.$emit('reload');

View File

@@ -0,0 +1,416 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmForeignsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-key-link mr-1" /> {{ $t('word.foreignKeys') }} "{{ table }}"
</div>
</template>
<div :slot="'body'">
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addForeign">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-link-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')"
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
<div ref="indexesPanel" class="panel-body p-0 pr-1">
<div
v-for="foreign in foreignProxy"
:key="foreign._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-foreign': selectedForeignID === foreign._id}"
@click="selectForeign($event, foreign._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-key-link mdi-24px" />
</div>
</div>
<div class="tile-content">
<div class="tile-title">
{{ foreign.constraintName }}
</div>
<small class="tile-subtitle text-gray d-flex">
<i class="mdi mdi-link-variant mr-1" />
<div class="fk-details-wrapper">
<span v-if="foreign.table !== ''" class="fk-details">
<i class="mdi mdi-table mr-1" />
<span>{{ foreign.table }}.{{ foreign.field }}</span>
</span>
<span v-if="foreign.refTable !== ''" class="fk-details">
<i class="mdi mdi-table mr-1" />
<span>{{ foreign.refTable }}.{{ foreign.refField }}</span>
</span>
</div>
</small>
</div>
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeIndex(foreign._id)"
>
<i class="mdi mdi-close" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="column col-7 pl-2 editor-col">
<form
v-if="selectedForeignObj"
:style="{ height: modalInnerHeight + 'px'}"
class="form-horizontal"
>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.name') }}
</label>
<div class="column">
<input
v-model="selectedForeignObj.constraintName"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group mb-4">
<label class="form-label col-3">
{{ $tc('word.field', 1) }}
</label>
<div class="fields-list column pt-1">
<label
v-for="(field, i) in fields"
:key="`${field.name}-${i}`"
class="form-checkbox m-0"
@click.prevent="toggleField(field.name)"
>
<input type="checkbox" :checked="selectedForeignObj.field === field.name">
<i class="form-icon" /> {{ field.name }}
</label>
</div>
</div>
<div class="form-group">
<label class="form-label col-3 pt-0">
{{ $t('message.referenceTable') }}
</label>
<div class="column">
<select
v-model="selectedForeignObj.refTable"
class="form-select"
@change="reloadRefFields"
>
<option
v-for="schemaTable in schemaTables"
:key="schemaTable.name"
:value="schemaTable.name"
>
{{ schemaTable.name }}
</option>
</select>
</div>
</div>
<div class="form-group mb-4">
<label class="form-label col-3">
{{ $t('message.referenceField') }}
</label>
<div class="fields-list column pt-1">
<label
v-for="(field, i) in refFields[selectedForeignID]"
:key="`${field.name}-${i}`"
class="form-checkbox m-0"
@click.prevent="toggleRefField(field.name)"
>
<input type="checkbox" :checked="selectedForeignObj.refField === field.name && selectedForeignObj.refTable === field.table">
<i class="form-icon" /> {{ field.name }}
</label>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('message.onUpdate') }}
</label>
<div class="column">
<select v-model="selectedForeignObj.onUpdate" class="form-select">
<option
v-for="action in foreignActions"
:key="action"
:value="action"
>
{{ action }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('message.onDelete') }}
</label>
<div class="column">
<select v-model="selectedForeignObj.onDelete" class="form-select">
<option
v-for="action in foreignActions"
:key="action"
:value="action"
>
{{ action }}
</option>
</select>
</div>
</div>
</form>
<div v-if="!foreignProxy.length" class="empty">
<div class="empty-icon">
<i class="mdi mdi-key-link mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoForeign') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addForeign">
{{ $t('message.createNewForeign') }}
</button>
</div>
</div>
</div>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsForeignModal',
components: {
ConfirmModal
},
props: {
localKeyUsage: Array,
connection: Object,
table: String,
schema: String,
schemaTables: Array,
fields: Array,
workspace: Object
},
data () {
return {
foreignProxy: [],
isOptionsChanging: false,
selectedForeignID: '',
modalInnerHeight: 400,
refFields: {},
foreignActions: [
'RESTRICT',
'CASCADE',
'SET NULL',
'NO ACTION'
]
};
},
computed: {
selectedForeignObj () {
return this.foreignProxy.find(foreign => foreign._id === this.selectedForeignID);
},
isChanged () {
return JSON.stringify(this.localKeyUsage) !== JSON.stringify(this.foreignProxy);
},
hasPrimary () {
return this.foreignProxy.some(foreign => foreign.type === 'PRIMARY');
}
},
mounted () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (this.foreignProxy.length)
this.resetSelectedID();
if (this.selectedForeignObj)
this.getRefFields();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
destroyed () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
confirmForeignsChange () {
this.$emit('foreigns-update', this.foreignProxy);
},
selectForeign (event, id) {
if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field'))
this.selectedForeignID = id;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addForeign () {
this.foreignProxy = [...this.foreignProxy, {
_id: uidGen(),
constraintName: `FK_${this.foreignProxy.length + 1}`,
refSchema: this.schema,
table: this.table,
refTable: '',
field: '',
refField: '',
onUpdate: this.foreignActions[0],
onDelete: this.foreignActions[0]
}];
if (this.foreignProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.indexesPanel.scrollTop = this.$refs.indexesPanel.scrollHeight + 60;
}, 20);
},
removeIndex (id) {
this.foreignProxy = this.foreignProxy.filter(foreign => foreign._id !== id);
if (this.selectedForeignID === id && this.foreignProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (!this.foreignProxy.some(foreign => foreign._id === this.selectedForeignID))
this.resetSelectedID();
},
toggleField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID)
foreign.field = field;
return foreign;
});
},
toggleRefField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID)
foreign.refField = field;
return foreign;
});
},
resetSelectedID () {
this.selectedForeignID = this.foreignProxy.length ? this.foreignProxy[0]._id : '';
},
async getRefFields () {
const params = {
uid: this.connection.uid,
schema: this.selectedForeignObj.refSchema,
table: this.selectedForeignObj.refTable
};
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
this.refFields = {
...this.refFields,
[this.selectedForeignID]: response
};
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
reloadRefFields () {
this.selectedForeignObj.refField = '';
this.getRefFields();
}
}
};
</script>
<style lang="scss" scoped>
.tile {
border-radius: 2px;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;
.tile-action {
opacity: 0;
transition: opacity 0.2s;
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-foreign {
background: $bg-color-light;
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 80px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
.fk-details-wrapper {
max-width: calc(100% - 1rem);
.fk-details {
display: flex;
line-height: 1;
align-items: baseline;
> span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
padding-bottom: 2px;
}
}
}
</style>

View File

@@ -65,21 +65,28 @@
</div> </div>
<div class="column col-7 pl-2 editor-col"> <div class="column col-7 pl-2 editor-col">
<form v-if="selectedIndexObj" :style="{ height: modalInnerHeight + 'px'}"> <form
v-if="selectedIndexObj"
:style="{ height: modalInnerHeight + 'px'}"
class="form-horizontal"
>
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label col-3">
{{ $t('word.name') }} {{ $t('word.name') }}
</label> </label>
<div class="column">
<input <input
v-model="selectedIndexObj.name" v-model="selectedIndexObj.name"
class="form-input" class="form-input"
type="text" type="text"
> >
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label col-3">
{{ $t('word.type') }} {{ $t('word.type') }}
</label> </label>
<div class="column">
<select v-model="selectedIndexObj.type" class="form-select"> <select v-model="selectedIndexObj.type" class="form-select">
<option <option
v-for="index in indexTypes" v-for="index in indexTypes"
@@ -91,11 +98,12 @@
</option> </option>
</select> </select>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label col-3">
{{ $tc('word.field', fields.length) }} {{ $tc('word.field', fields.length) }}
</label> </label>
<div class="fields-list"> <div class="fields-list column pt-1">
<label <label
v-for="(field, i) in fields" v-for="(field, i) in fields"
:key="`${field.name}-${i}`" :key="`${field.name}-${i}`"
@@ -108,6 +116,19 @@
</div> </div>
</div> </div>
</form> </form>
<div v-if="!indexesProxy.length" class="empty">
<div class="empty-icon">
<i class="mdi mdi-key-outline mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoIndexes') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addIndex">
{{ $t('message.createNewIndex') }}
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -216,7 +237,7 @@ export default {
}); });
}, },
resetSelectedID () { resetSelectedID () {
this.selectedIndexID = this.indexesProxy[0]._id; this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._id : '';
} }
} }
}; };
@@ -253,7 +274,7 @@ export default {
} }
.fields-list { .fields-list {
max-height: 200px; max-height: 300px;
overflow: auto; overflow: auto;
} }

View File

@@ -18,6 +18,7 @@
</label> </label>
<div class="column"> <div class="column">
<input <input
ref="firstInput"
v-model="optionsProxy.name" v-model="optionsProxy.name"
class="form-input" class="form-input"
:class="{'is-error': !isTableNameValid}" :class="{'is-error': !isTableNameValid}"
@@ -112,6 +113,10 @@ export default {
}, },
created () { created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions)); this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
methods: { methods: {
confirmOptionsChange () { confirmOptionsChange () {

View File

@@ -0,0 +1,145 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" /> {{ $t('word.options') }} "{{ localOptions.name }}"
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="optionsProxy.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="optionsProxy.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="optionsProxy.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="optionsProxy.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="optionsProxy.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
}
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.isTableNameValid)
this.optionsProxy.name = this.localOptions.name;
this.$emit('options-update', this.optionsProxy);
}
}
};
</script>

View File

@@ -0,0 +1,300 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> {{ $t('word.parameters') }} "{{ routine }}"
</div>
</template>
<div :slot="'body'">
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')"
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param.name"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" />
</div>
</div>
<div class="tile-content">
<div class="tile-title">
{{ param.name }}
</div>
<small class="tile-subtitle text-gray">{{ param.type }}{{ param.length ? `(${param.length})` : '' }} · {{ param.context }}</small>
</div>
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param.name)"
>
<i class="mdi mdi-close" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="column col-7 pl-2 editor-col">
<form
v-if="selectedParamObj"
:style="{ height: modalInnerHeight + 'px'}"
class="form-horizontal"
>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.name') }}
</label>
<div class="column">
<input
v-model="selectedParamObj.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.type') }}
</label>
<div class="column">
<select v-model="selectedParamObj.type" class="form-select text-uppercase">
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="selectedParamObj.type.toUpperCase() === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.length') }}
</label>
<div class="column">
<input
v-model="selectedParamObj.length"
class="form-input"
type="number"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
</label>
<div class="column">
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
name="context"
value="IN"
> <i class="form-icon" /> IN
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="OUT"
name="context"
> <i class="form-icon" /> OUT
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="INOUT"
name="context"
> <i class="form-icon" /> INOUT
</label>
</div>
</div>
</form>
<div v-if="!parametersProxy.length" class="empty">
<div class="empty-icon">
<i class="mdi mdi-dots-horizontal mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }}
</button>
</div>
</div>
</div>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineParamsModal',
components: {
ConfirmModal
},
props: {
localParameters: Array,
routine: String,
workspace: Object
},
data () {
return {
parametersProxy: [],
isOptionsChanging: false,
selectedParam: '',
modalInnerHeight: 400,
i: 1
};
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
}
},
mounted () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (this.parametersProxy.length)
this.resetSelectedID();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
destroyed () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
length: 10
}];
if (this.parametersProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (!this.parametersProxy.some(param => param.name === this.selectedParam))
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : '';
}
}
};
</script>
<style lang="scss" scoped>
.tile {
border-radius: 2px;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;
.tile-action {
opacity: 0;
transition: opacity 0.2s;
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-param {
background: $bg-color-light;
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 300px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
</style>

View File

@@ -40,7 +40,7 @@
<span>{{ $t('word.indexes') }}</span> <span>{{ $t('word.indexes') }}</span>
<i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" /> <i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" />
</button> </button>
<button class="btn btn-dark btn-sm d-none"> <button class="btn btn-dark btn-sm" @click="showForeignModal">
<span>{{ $t('word.foreignKeys') }}</span> <span>{{ $t('word.foreignKeys') }}</span>
<i class="mdi mdi-24px mdi-key-link ml-1" /> <i class="mdi mdi-24px mdi-key-link ml-1" />
</button> </button>
@@ -57,6 +57,7 @@
ref="indexTable" ref="indexTable"
:fields="localFields" :fields="localFields"
:indexes="localIndexes" :indexes="localIndexes"
:foreigns="localKeyUsage"
:tab-uid="tabUid" :tab-uid="tabUid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:index-types="workspace.indexTypes" :index-types="workspace.indexTypes"
@@ -86,6 +87,18 @@
@hide="hideIndexesModal" @hide="hideIndexesModal"
@indexes-update="indexesUpdate" @indexes-update="indexesUpdate"
/> />
<WorkspacePropsForeignModal
v-if="isForeignModal"
:local-key-usage="localKeyUsage"
:connection="connection"
:table="table"
:schema="schema"
:schema-tables="schemaTables"
:fields="localFields"
:workspace="workspace"
@hide="hideForeignModal"
@foreigns-update="foreignsUpdate"
/>
</div> </div>
</template> </template>
@@ -96,13 +109,15 @@ import Tables from '@/ipc-api/Tables';
import WorkspacePropsTable from '@/components/WorkspacePropsTable'; import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal'; import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal'; import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal';
export default { export default {
name: 'WorkspacePropsTab', name: 'WorkspacePropsTab',
components: { components: {
WorkspacePropsTable, WorkspacePropsTable,
WorkspacePropsOptionsModal, WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal WorkspacePropsIndexesModal,
WorkspacePropsForeignModal
}, },
props: { props: {
connection: Object, connection: Object,
@@ -115,6 +130,7 @@ export default {
isSaving: false, isSaving: false,
isOptionsModal: false, isOptionsModal: false,
isIndexesModal: false, isIndexesModal: false,
isForeignModal: false,
isOptionsChanging: false, isOptionsChanging: false,
originalFields: [], originalFields: [],
localFields: [], localFields: [],
@@ -148,6 +164,13 @@ export default {
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
}, },
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
},
isChanged () { isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) || return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) || JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
@@ -177,7 +200,8 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure', refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges' setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}), }),
async getFieldsData () { async getFieldsData () {
if (!this.table) return; if (!this.table) return;
@@ -242,8 +266,13 @@ export default {
const { status, response } = await Tables.getKeyUsage(params); const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success') { if (status === 'success') {
this.originalKeyUsage = response; this.originalKeyUsage = response.map(foreign => {
this.localKeyUsage = JSON.parse(JSON.stringify(response)); return {
_id: uidGen(),
...foreign
};
});
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
} }
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
@@ -321,6 +350,35 @@ export default {
// Index Deletions // Index Deletions
indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._id)); indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._id));
// FOREIGN KEYS
const foreignChanges = {
additions: [],
changes: [],
deletions: []
};
const originalForeignIDs = this.originalKeyUsage.reduce((acc, curr) => [...acc, curr._id], []);
const localForeignIDs = this.localKeyUsage.reduce((acc, curr) => [...acc, curr._id], []);
// Foreigns Additions
foreignChanges.additions = this.localKeyUsage.filter(foreign => !originalForeignIDs.includes(foreign._id));
// Foreigns Changes
this.originalKeyUsage.forEach(originalForeign => {
const lI = this.localKeyUsage.findIndex(localForeign => localForeign._id === originalForeign._id);
if (JSON.stringify(originalForeign) !== JSON.stringify(this.localKeyUsage[lI])) {
if (this.localKeyUsage[lI]) {
foreignChanges.changes.push({
...this.localKeyUsage[lI],
oldName: originalForeign.constraintName
});
}
}
});
// Foreigns Deletions
foreignChanges.deletions = this.originalKeyUsage.filter(foreign => !localForeignIDs.includes(foreign._id));
// ALTER
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.schema, schema: this.schema,
@@ -329,14 +387,23 @@ export default {
changes, changes,
deletions, deletions,
indexChanges, indexChanges,
foreignChanges,
options options
}; };
try { // Key usage (foreign keys) try {
const { status, response } = await Tables.alterTable(params); const { status, response } = await Tables.alterTable(params);
if (status === 'success') { if (status === 'success') {
const oldName = this.tableOptions.name;
await this.refreshStructure(this.connection.uid); await this.refreshStructure(this.connection.uid);
if (oldName !== this.localOptions.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, table: this.localOptions.name });
}
this.getFieldsData(); this.getFieldsData();
} }
else else
@@ -423,6 +490,15 @@ export default {
}, },
indexesUpdate (indexes) { indexesUpdate (indexes) {
this.localIndexes = indexes; this.localIndexes = indexes;
},
showForeignModal () {
this.isForeignModal = true;
},
hideForeignModal () {
this.isForeignModal = false;
},
foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns;
} }
} }
}; };

View File

@@ -0,0 +1,262 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm disabled"
:disabled="isChanged"
@click="false"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<span>{{ $t('word.parameters') }}</span>
<i class="mdi mdi-24px mdi-dots-horizontal ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<span>{{ $t('word.options') }}</span>
<i class="mdi mdi-24px mdi-cogs ml-1" />
</button>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor
v-if="isSelected"
ref="queryEditor"
:value.sync="localRoutine.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspacePropsRoutineOptionsModal
v-if="isOptionsModal"
:local-options="localRoutine"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsRoutineParamsModal
v-if="isParamsModal"
:local-parameters="localRoutine.parameters"
:workspace="workspace"
:routine="localRoutine.name"
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspacePropsTabRoutine',
components: {
QueryEditor,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal
},
props: {
connection: Object,
routine: String
},
data () {
return {
tabUid: 'prop',
isQuering: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
originalRoutine: null,
localRoutine: { sql: '' },
lastRoutine: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine);
},
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
async routine () {
if (this.isSelected) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
async isSelected (val) {
if (val && this.lastRoutine !== this.routine) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
isChanged (val) {
if (this.isSelected && this.lastRoutine === this.routine && this.routine !== null)
this.setUnsavedChanges(val);
}
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async getRoutineData () {
if (!this.routine) return;
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
routine: this.workspace.breadcrumbs.procedure
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') {
this.originalRoutine = response;
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isQuering = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
routine: {
...this.localRoutine,
oldName: this.originalRoutine.name
}
};
try {
const { status, response } = await Routines.alterRoutine(params);
if (status === 'success') {
const oldName = this.originalRoutine.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localRoutine.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, procedure: this.localRoutine.name });
}
this.getRoutineData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
optionsUpdate (options) {
this.localRoutine = options;
},
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
}
}
};
</script>

View File

@@ -0,0 +1,275 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
v-model="localTrigger.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localTrigger.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalTrigger.definer">
{{ originalTrigger.definer.replaceAll('`', '') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
</div>
<div class="columns">
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group">
<select v-model="localTrigger.event1" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select v-model="localTrigger.event2" class="form-select">
<option>INSERT</option>
<option>UPDATE</option>
<option>DELETE</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor
v-if="isSelected"
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import Triggers from '@/ipc-api/Triggers';
export default {
name: 'WorkspacePropsTabTrigger',
components: {
QueryEditor
},
props: {
connection: Object,
trigger: String
},
data () {
return {
tabUid: 'prop',
isQuering: false,
isSaving: false,
originalTrigger: null,
localTrigger: { sql: '' },
lastTrigger: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalTrigger) !== JSON.stringify(this.localTrigger);
},
isDefinerInUsers () {
return this.originalTrigger ? this.workspace.users.some(user => this.originalTrigger.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
async trigger () {
if (this.isSelected) {
await this.getTriggerData();
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
this.lastTrigger = this.trigger;
}
},
async isSelected (val) {
if (val && this.lastTrigger !== this.trigger) {
await this.getTriggerData();
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
this.lastTrigger = this.trigger;
}
},
isChanged (val) {
if (this.isSelected && this.lastTrigger === this.trigger && this.trigger !== null)
this.setUnsavedChanges(val);
}
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async getTriggerData () {
if (!this.trigger) return;
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
trigger: this.workspace.breadcrumbs.trigger
};
try {
const { status, response } = await Triggers.getTriggerInformations(params);
if (status === 'success') {
this.originalTrigger = response;
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.sqlProxy = this.localTrigger.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isQuering = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
trigger: {
...this.localTrigger,
oldName: this.originalTrigger.name
}
};
try {
const { status, response } = await Triggers.alterTrigger(params);
if (status === 'success') {
const oldName = this.originalTrigger.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localTrigger.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, trigger: this.localTrigger.name });
}
this.getTriggerData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
}
}
};
</script>

View File

@@ -0,0 +1,329 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localView.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalView.definer">
{{ originalView.definer.replaceAll('`', '') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
</div>
<div class="columns">
<div class="column col-2">
<div class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="DEFINER"
>
<i class="form-icon" /> DEFINER
</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="INVOKER"
>
<i class="form-icon" /> INVOKER
</label>
</div>
</div>
<div class="column col-2">
<div class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
name="algorithm"
value="UNDEFINED"
>
<i class="form-icon" /> UNDEFINED
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="MERGE"
name="algorithm"
>
<i class="form-icon" /> MERGE
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="TEMPTABLE"
name="algorithm"
>
<i class="form-icon" /> TEMPTABLE
</label>
</div>
</div>
<div class="column col-2">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value=""
>
<i class="form-icon" /> None
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="CASCADED"
>
<i class="form-icon" /> CASCADED
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="LOCAL"
>
<i class="form-icon" /> LOCAL
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
<QueryEditor
v-if="isSelected"
ref="queryEditor"
:value.sync="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import Views from '@/ipc-api/Views';
export default {
name: 'WorkspacePropsTabView',
components: {
QueryEditor
},
props: {
connection: Object,
view: String
},
data () {
return {
tabUid: 'prop',
isQuering: false,
isSaving: false,
originalView: null,
localView: { sql: '' },
lastView: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalView) !== JSON.stringify(this.localView);
},
isDefinerInUsers () {
return this.originalView ? this.workspace.users.some(user => this.originalView.definer === `\`${user.name}\`@\`${user.host}\``) : true;
}
},
watch: {
async view () {
if (this.isSelected) {
await this.getViewData();
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
this.lastView = this.view;
}
},
async isSelected (val) {
if (val && this.lastView !== this.view) {
await this.getViewData();
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
this.lastView = this.view;
}
},
isChanged (val) {
if (this.isSelected && this.lastView === this.view && this.view !== null)
this.setUnsavedChanges(val);
}
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async getViewData () {
if (!this.view) return;
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
view: this.workspace.breadcrumbs.view
};
try {
const { status, response } = await Views.getViewInformations(params);
if (status === 'success') {
this.originalView = response;
this.localView = JSON.parse(JSON.stringify(this.originalView));
this.sqlProxy = this.localView.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isQuering = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
view: {
...this.localView,
oldName: this.originalView.name
}
};
try {
const { status, response } = await Views.alterView(params);
if (status === 'success') {
const oldName = this.originalView.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localView.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, view: this.localView.name });
}
this.getViewData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localView = JSON.parse(JSON.stringify(this.originalView));
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
}
}
};
</script>

View File

@@ -29,14 +29,14 @@
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.name') }} {{ $t('word.name') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.type') }} {{ $t('word.type') }}
</div> </div>
@@ -85,7 +85,7 @@
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.collation') }} {{ $t('word.collation') }}
</div> </div>
@@ -104,6 +104,7 @@
:key="row._id" :key="row._id"
:row="row" :row="row"
:indexes="getIndexes(row.name)" :indexes="getIndexes(row.name)"
:foreigns="getForeigns(row.name)"
:data-types="dataTypes" :data-types="dataTypes"
@contextmenu="contextMenu" @contextmenu="contextMenu"
/> />
@@ -128,6 +129,7 @@ export default {
props: { props: {
fields: Array, fields: Array,
indexes: Array, indexes: Array,
foreigns: Array,
indexTypes: Array, indexTypes: Array,
tabUid: [String, Number], tabUid: [String, Number],
connUid: String, connUid: String,
@@ -214,6 +216,13 @@ export default {
acc.push(...curr.fields.map(f => ({ name: f, type: curr.type }))); acc.push(...curr.fields.map(f => ({ name: f, type: curr.type })));
return acc; return acc;
}, []).filter(f => f.name === field); }, []).filter(f => f.name === field);
},
getForeigns (field) {
return this.foreigns.reduce((acc, curr) => {
if (curr.field === field)
acc.push(`${curr.refTable}.${curr.refField}`);
return acc;
}, []);
} }
} }
}; };
@@ -231,4 +240,8 @@ export default {
.vscroll { .vscroll {
overflow: auto; overflow: auto;
} }
.min-100 {
min-width: 100px !important;
}
</style> </style>

View File

@@ -77,11 +77,3 @@ export default {
} }
}; };
</script> </script>
<style lang="scss" scoped>
.disabled {
pointer-events: none;
filter: grayscale(100%);
opacity: 0.5;
}
</style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)"> <div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)">
<div class="td"> <div class="td" tabindex="0">
<div class="row-draggable"> <div class="row-draggable">
<i class="mdi mdi-drag-horizontal row-draggable-icon" /> <i class="mdi mdi-drag-horizontal row-draggable-icon" />
{{ localRow.order }} {{ localRow.order }}
@@ -15,9 +15,15 @@
class="d-inline-block mdi mdi-key column-key c-help" class="d-inline-block mdi mdi-key column-key c-help"
:class="`key-${index.type}`" :class="`key-${index.type}`"
/> />
<i
v-for="foreign in foreigns"
:key="foreign"
:title="foreign"
class="d-inline-block mdi mdi-key-link c-help"
/>
</div> </div>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<span <span
v-if="!isInlineEditor.name" v-if="!isInlineEditor.name"
class="cell-content" class="cell-content"
@@ -35,11 +41,15 @@
@blur="editOFF" @blur="editOFF"
> >
</div> </div>
<div class="td text-uppercase text-left" :class="`type-${lowerCase(localRow.type)}`"> <div
class="td text-uppercase"
tabindex="0"
>
<span <span
v-if="!isInlineEditor.type" v-if="!isInlineEditor.type"
class="cell-content" class="cell-content text-left"
@dblclick="editON($event, localRow.type.toUpperCase(), 'type')" :class="`type-${lowerCase(localRow.type)}`"
@click="editON($event, localRow.type.toUpperCase(), 'type')"
> >
{{ localRow.type }} {{ localRow.type }}
</span> </span>
@@ -47,7 +57,7 @@
v-else v-else
ref="editField" ref="editField"
v-model="editingContent" v-model="editingContent"
class="editable-field px-1 text-uppercase" class="form-select editable-field small-select text-uppercase"
@blur="editOFF" @blur="editOFF"
> >
<optgroup <optgroup
@@ -66,7 +76,7 @@
</optgroup> </optgroup>
</select> </select>
</div> </div>
<div class="td type-int"> <div class="td type-int" tabindex="0">
<template v-if="fieldType.length"> <template v-if="fieldType.length">
<span <span
v-if="!isInlineEditor.length" v-if="!isInlineEditor.length"
@@ -86,7 +96,7 @@
> >
</template> </template>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.unsigned" v-model="localRow.unsigned"
@@ -96,17 +106,17 @@
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.nullable" v-model="localRow.nullable"
type="checkbox" type="checkbox"
:disabled="localRow.key === 'pri'" :disabled="!isNullable"
> >
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.zerofill" v-model="localRow.zerofill"
@@ -116,12 +126,12 @@
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<span class="cell-content" @dblclick="editON($event, localRow.default, 'default')"> <span class="cell-content" @dblclick="editON($event, localRow.default, 'default')">
{{ fieldDefault }} {{ fieldDefault }}
</span> </span>
</div> </div>
<div class="td type-varchar"> <div class="td type-varchar" tabindex="0">
<span <span
v-if="!isInlineEditor.comment" v-if="!isInlineEditor.comment"
class="cell-content" class="cell-content"
@@ -139,12 +149,12 @@
@blur="editOFF" @blur="editOFF"
> >
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<template v-if="fieldType.collation"> <template v-if="fieldType.collation">
<span <span
v-if="!isInlineEditor.collation" v-if="!isInlineEditor.collation"
class="cell-content" class="cell-content"
@dblclick="editON($event, localRow.collation, 'collation')" @click="editON($event, localRow.collation, 'collation')"
> >
{{ localRow.collation }} {{ localRow.collation }}
</span> </span>
@@ -152,7 +162,7 @@
v-else v-else
ref="editField" ref="editField"
v-model="editingContent" v-model="editingContent"
class="editable-field px-1" class="form-select small-select editable-field"
@blur="editOFF" @blur="editOFF"
> >
<option <option
@@ -224,7 +234,7 @@
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="defaultValue.type" v-model="defaultValue.type"
:disabled="localRow.key !== 'pri'" :disabled="!canAutoincrement"
type="radio" type="radio"
name="default" name="default"
value="autoincrement" value="autoincrement"
@@ -283,7 +293,8 @@ export default {
props: { props: {
row: Object, row: Object,
dataTypes: Array, dataTypes: Array,
indexes: Array indexes: Array,
foreigns: Array
}, },
data () { data () {
return { return {
@@ -297,6 +308,7 @@ export default {
onUpdate: '' onUpdate: ''
}, },
editingContent: null, editingContent: null,
originalContent: null,
editingField: null editingField: null
}; };
}, },
@@ -325,6 +337,12 @@ export default {
}, },
collations () { collations () {
return this.getWorkspace(this.selectedWorkspace).collations; return this.getWorkspace(this.selectedWorkspace).collations;
},
canAutoincrement () {
return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type));
},
isNullable () {
return !this.indexes.some(index => ['PRIMARY'].includes(index.type));
} }
}, },
watch: { watch: {
@@ -333,6 +351,13 @@ export default {
}, },
row () { row () {
this.localRow = this.row; this.localRow = this.row;
},
indexes () {
if (!this.canAutoincrement)
this.localRow.autoIncrement = false;
if (!this.isNullable)
this.localRow.nullable = false;
} }
}, },
mounted () { mounted () {
@@ -397,6 +422,7 @@ export default {
this.editingField = field; this.editingField = field;
this.editingContent = content; this.editingContent = content;
this.originalContent = content;
const obj = { [field]: true }; const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj }; this.isInlineEditor = { ...this.isInlineEditor, ...obj };
@@ -414,10 +440,10 @@ export default {
editOFF () { editOFF () {
this.localRow[this.editingField] = this.editingContent; this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'type') { if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
this.localRow.numLength = false; this.localRow.numLength = null;
this.localRow.charLength = false; this.localRow.charLength = null;
this.localRow.datePrecision = false; this.localRow.datePrecision = null;
if (this.fieldType.length) { if (this.fieldType.length) {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11; if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11;
@@ -427,6 +453,12 @@ export default {
if (!this.fieldType.collation) if (!this.fieldType.collation)
this.localRow.collation = null; this.localRow.collation = null;
if (!this.fieldType.unsigned)
this.localRow.unsigned = false;
if (!this.fieldType.zerofill)
this.localRow.zerofill = false;
} }
if (this.editingField === 'default') { if (this.editingField === 'default') {
@@ -460,6 +492,7 @@ export default {
}); });
this.editingContent = null; this.editingContent = null;
this.originalContent = null;
this.editingField = null; this.editingField = null;
}, },
hideDefaultModal () { hideDefaultModal () {

View File

@@ -1,7 +1,13 @@
<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">
<div class="workspace-query-runner column col-12"> <div class="workspace-query-runner column col-12">
<QueryEditor v-if="isSelected" :value.sync="query" /> <QueryEditor
v-if="isSelected"
:auto-focus="true"
:value.sync="query"
:workspace="workspace"
:schema="schema"
/>
<div class="workspace-query-runner-footer"> <div class="workspace-query-runner-footer">
<div class="workspace-query-buttons"> <div class="workspace-query-buttons">
<button <button
@@ -99,6 +105,7 @@ export default {
if (!query || this.isQuering) return; if (!query || this.isQuering) return;
this.isQuering = true; this.isQuering = true;
this.clearTabData(); this.clearTabData();
this.$refs.queryTable.resetSort();
try { // Query Data try { // Query Data
const params = { const params = {

View File

@@ -123,8 +123,11 @@ export default {
primaryField () { primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false; return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
}, },
isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000;
},
sortedResults () { sortedResults () {
if (this.currentSort) { if (this.currentSort && !this.isHardSort) {
return [...this.localResults].sort((a, b) => { return [...this.localResults].sort((a, b) => {
let modifier = 1; let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort]; const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
@@ -228,7 +231,6 @@ export default {
return row[primaryFieldName]; return row[primaryFieldName];
}, },
setLocalResults () { setLocalResults () {
this.resetSort();
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows
? this.resultsWithRows[this.resultsetIndex].rows.map(item => { ? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
return { ...item, _id: uidGen() }; return { ...item, _id: uidGen() };
@@ -342,6 +344,9 @@ export default {
this.currentSortDir = 'asc'; this.currentSortDir = 'asc';
this.currentSort = field; this.currentSort = field;
} }
if (this.isHardSort)
this.$emit('hard-sort', { field: this.currentSort, dir: this.currentSortDir });
}, },
resetSort () { resetSort () {
this.currentSort = ''; this.currentSort = '';

View File

@@ -20,6 +20,7 @@
class="editable-field" class="editable-field"
:value.sync="editingContent" :value.sync="editingContent"
:key-usage="getKeyUsage(cKey)" :key-usage="getKeyUsage(cKey)"
size="small"
@blur="editOFF" @blur="editOFF"
/> />
<template v-else> <template v-else>
@@ -157,6 +158,8 @@ export default {
typeFormat (val, type, precision) { typeFormat (val, type, precision) {
if (!val) return val; if (!val) return val;
type = type.toUpperCase();
if (DATE.includes(type)) if (DATE.includes(type))
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val; return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
@@ -254,7 +257,7 @@ export default {
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext); return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
}, },
foreignKeys () { foreignKeys () {
return this.keyUsage.map(key => key.column); return this.keyUsage.map(key => key.field);
}, },
isEditable () { isEditable () {
return this.fields ? !!(this.fields[0].schema && this.fields[0].table) : false; return this.fields ? !!(this.fields[0].schema && this.fields[0].table) : false;
@@ -274,7 +277,7 @@ export default {
if (field) if (field)
type = field.type; type = field.type;
return type; return type.toLowerCase();
}, },
getFieldPrecision (cKey) { getFieldPrecision (cKey) {
let length = 0; let length = 0;
@@ -313,7 +316,7 @@ export default {
editON (event, content, field) { editON (event, content, field) {
if (!this.isEditable) return; if (!this.isEditable) return;
const type = this.getFieldType(field); const type = this.getFieldType(field).toUpperCase(); ;
this.originalContent = content; this.originalContent = content;
this.editingType = type; this.editingType = type;
this.editingField = field; this.editingField = field;
@@ -420,7 +423,7 @@ export default {
this.$emit('select-row', event, row); this.$emit('select-row', event, row);
}, },
getKeyUsage (keyName) { getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName); return this.keyUsage.find(key => key.field === keyName);
} }
} }
}; };

View File

@@ -63,6 +63,7 @@
mode="table" mode="table"
@update-field="updateField" @update-field="updateField"
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
@hard-sort="hardSort"
/> />
</div> </div>
<ModalNewTableRow <ModalNewTableRow
@@ -102,7 +103,8 @@ export default {
lastTable: null, lastTable: null,
isAddModal: false, isAddModal: false,
autorefreshTimer: 0, autorefreshTimer: 0,
refreshInterval: null refreshInterval: null,
sortParams: {}
}; };
}, },
computed: { computed: {
@@ -133,8 +135,10 @@ export default {
watch: { watch: {
table () { table () {
if (this.isSelected) { if (this.isSelected) {
this.sortParams = {};
this.getTableData(); this.getTableData();
this.lastTable = this.table; this.lastTable = this.table;
this.$refs.queryTable.resetSort();
} }
}, },
isSelected (val) { isSelected (val) {
@@ -156,7 +160,7 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
async getTableData () { async getTableData (sortParams) {
if (!this.table) return; if (!this.table) return;
this.isQuering = true; this.isQuering = true;
@@ -167,7 +171,8 @@ export default {
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.schema, schema: this.schema,
table: this.workspace.breadcrumbs.table table: this.workspace.breadcrumbs.table || this.workspace.breadcrumbs.view,
sortParams
}; };
try { // Table data try { // Table data
@@ -188,7 +193,11 @@ export default {
return this.table; return this.table;
}, },
reloadTable () { reloadTable () {
this.getTableData(); this.getTableData(this.sortParams);
},
hardSort (sortParams) {
this.sortParams = sortParams;
this.getTableData(sortParams);
}, },
showAddModal () { showAddModal () {
this.isAddModal = true; this.isAddModal = true;

View File

@@ -61,7 +61,24 @@ module.exports = {
total: 'Total', total: 'Total',
table: 'Table', table: 'Table',
discard: 'Discard', discard: 'Discard',
stay: 'Stay' stay: 'Stay',
author: 'Author',
light: 'Light',
dark: 'Dark',
autoCompletion: 'Auto Completion',
application: 'Application',
editor: 'Editor',
view: 'View',
definer: 'Definer',
algorithm: 'Algorithm',
trigger: 'Trigger | Triggers',
storedRoutine: 'Stored routine | Stored routines',
scheduler: 'Scheduler | Schedulers',
event: 'Event',
parameters: 'Parameters',
function: 'Function | Functions',
deterministic: 'Deterministic',
context: 'Context'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -115,7 +132,33 @@ module.exports = {
deleteTable: 'Delete table', deleteTable: 'Delete table',
emptyCorfirm: 'Do you confirm to empty', emptyCorfirm: 'Do you confirm to empty',
unsavedChanges: 'Unsaved changes', unsavedChanges: 'Unsaved changes',
discardUnsavedChanges: 'You have some unsaved changes. By leaving this tab these changes will be discarded.' discardUnsavedChanges: 'You have some unsaved changes. By leaving this tab these changes will be discarded.',
thereAreNoIndexes: 'There are no indexes',
thereAreNoForeign: 'There are no foreign keys',
createNewForeign: 'Create new foreign key',
referenceTable: 'Ref. table',
referenceField: 'Ref. field',
foreignFields: 'Foreign fields',
invalidDefault: 'Invalid default',
onDelete: 'On delete',
applicationTheme: 'Application Theme',
editorTheme: 'Editor Theme',
wrapLongLines: 'Wrap long lines',
selectStatement: 'Select statement',
triggerStatement: 'Trigger statement',
sqlSecurity: 'SQL security',
updateOption: 'Update option',
deleteView: 'Delete view',
createNewView: 'Create new view',
deleteTrigger: 'Delete trigger',
createNewTrigger: 'Create new trigger',
currentUser: 'Current user',
routineBody: 'Routine body',
dataAccess: 'Data access',
thereAreNoParameters: 'There are no parameters',
createNewParameter: 'Create new parameter',
createNewRoutine: 'Create new stored routine',
deleteRoutine: 'Delete stored routine'
}, },
// Date and Time // Date and Time
short: { short: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,8 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getKey (params) {
return ipcRenderer.sendSync('get-key', params);
}
}

View File

@@ -0,0 +1,20 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getRoutineInformations (params) {
return ipcRenderer.invoke('get-routine-informations', params);
}
static dropRoutine (params) {
return ipcRenderer.invoke('drop-routine', params);
}
static alterRoutine (params) {
return ipcRenderer.invoke('alter-routine', params);
}
static createRoutine (params) {
return ipcRenderer.invoke('create-routine', params);
}
}

View File

@@ -0,0 +1,20 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getTriggerInformations (params) {
return ipcRenderer.invoke('get-trigger-informations', params);
}
static dropTrigger (params) {
return ipcRenderer.invoke('drop-trigger', params);
}
static alterTrigger (params) {
return ipcRenderer.invoke('alter-trigger', params);
}
static createTrigger (params) {
return ipcRenderer.invoke('create-trigger', params);
}
}

View File

@@ -0,0 +1,8 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getUsers (params) {
return ipcRenderer.invoke('get-users', params);
}
}

View File

@@ -0,0 +1,20 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getViewInformations (params) {
return ipcRenderer.invoke('get-view-informations', params);
}
static dropView (params) {
return ipcRenderer.invoke('drop-view', params);
}
static alterView (params) {
return ipcRenderer.invoke('alter-view', params);
}
static createView (params) {
return ipcRenderer.invoke('create-view', params);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -180,6 +180,12 @@ body {
.form-select { .form-select {
cursor: pointer; cursor: pointer;
&.small-select {
height: 1rem;
font-size: 0.7rem;
padding: 1px 0.4rem 0;
}
} }
.form-select, .form-select,
@@ -206,6 +212,7 @@ body {
.input-group .input-group-addon { .input-group .input-group-addon {
border-color: #3f3f3f; border-color: #3f3f3f;
z-index: 1;
} }
.menu { .menu {
@@ -219,7 +226,7 @@ body {
} }
.accordion-body { .accordion-body {
max-height: 500rem !important; max-height: 5000rem !important;
} }
.btn.loading { .btn.loading {
@@ -228,3 +235,7 @@ body {
visibility: hidden; visibility: hidden;
} }
} }
.empty {
color: $body-font-color;
}

View File

@@ -1,10 +1,18 @@
'use strict'; 'use strict';
import Store from 'electron-store';
import Application from '../../ipc-api/Application';
const key = Application.getKey();
const persistentStore = new Store({
name: 'connections',
encryptionKey: key
});
export default { export default {
namespaced: true, namespaced: true,
strict: true, strict: true,
state: { state: {
connections: [] connections: persistentStore.get('connections') || []
}, },
getters: { getters: {
getConnections: state => state.connections, getConnections: state => state.connections,
@@ -20,9 +28,11 @@ export default {
mutations: { mutations: {
ADD_CONNECTION (state, connection) { ADD_CONNECTION (state, connection) {
state.connections.push(connection); state.connections.push(connection);
persistentStore.set('connections', state.connections);
}, },
DELETE_CONNECTION (state, connection) { DELETE_CONNECTION (state, connection) {
state.connections = state.connections.filter(el => el.uid !== connection.uid); state.connections = state.connections.filter(el => el.uid !== connection.uid);
persistentStore.set('connections', state.connections);
}, },
EDIT_CONNECTION (state, connection) { EDIT_CONNECTION (state, connection) {
const editedConnections = state.connections.map(conn => { const editedConnections = state.connections.map(conn => {
@@ -31,9 +41,11 @@ export default {
}); });
state.connections = editedConnections; state.connections = editedConnections;
state.selected_conection = {}; state.selected_conection = {};
persistentStore.set('connections', state.connections);
}, },
UPDATE_CONNECTIONS (state, connections) { UPDATE_CONNECTIONS (state, connections) {
state.connections = connections; state.connections = connections;
persistentStore.set('connections', state.connections);
} }
}, },
actions: { actions: {

View File

@@ -1,29 +1,53 @@
'use strict'; 'use strict';
import i18n from '@/i18n'; import i18n from '@/i18n';
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: {
locale: 'en-US', locale: persistentStore.get('locale') || 'en-US',
explorebar_size: null, explorebar_size: persistentStore.get('explorebar_size') || null,
notifications_timeout: 5 notifications_timeout: persistentStore.get('notifications_timeout') || 5,
auto_complete: persistentStore.get('auto_complete') || true,
line_wrap: persistentStore.get('line_wrap') || true,
application_theme: persistentStore.get('application_theme') || 'dark',
editor_theme: persistentStore.get('editor_theme') || 'twilight'
}, },
getters: { getters: {
getLocale: state => state.locale, getLocale: state => state.locale,
getExplorebarSize: state => state.explorebar_size, getExplorebarSize: state => state.explorebar_size,
getNotificationsTimeout: state => state.notifications_timeout getNotificationsTimeout: state => state.notifications_timeout,
getAutoComplete: state => state.auto_complete,
getLineWrap: state => state.line_wrap,
getApplicationTheme: state => state.application_theme,
getEditorTheme: state => state.editor_theme
}, },
mutations: { mutations: {
SET_LOCALE (state, locale) { SET_LOCALE (state, locale) {
state.locale = locale; state.locale = locale;
i18n.locale = locale; i18n.locale = locale;
persistentStore.set('locale', state.locale);
}, },
SET_NOTIFICATIONS_TIMEOUT (state, timeout) { SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
state.notifications_timeout = timeout; state.notifications_timeout = timeout;
persistentStore.set('notifications_timeout', state.notifications_timeout);
},
SET_AUTO_COMPLETE (state, val) {
state.auto_complete = val;
persistentStore.set('auto_complete', state.auto_complete);
},
SET_LINE_WRAP (state, val) {
state.line_wrap = val;
persistentStore.set('line_wrap', state.line_wrap);
}, },
SET_EXPLOREBAR_SIZE (state, size) { SET_EXPLOREBAR_SIZE (state, size) {
state.explorebar_size = size; state.explorebar_size = size;
persistentStore.set('explorebar_size', state.explorebar_size);
},
SET_EDITOR_THEME (state, theme) {
state.editor_theme = theme;
} }
}, },
actions: { actions: {
@@ -35,6 +59,15 @@ export default {
}, },
changeExplorebarSize ({ commit }, size) { changeExplorebarSize ({ commit }, size) {
commit('SET_EXPLOREBAR_SIZE', size); commit('SET_EXPLOREBAR_SIZE', size);
},
changeAutoComplete ({ commit }, val) {
commit('SET_AUTO_COMPLETE', val);
},
changeLineWrap ({ commit }, val) {
commit('SET_LINE_WRAP', val);
},
changeEditorTheme ({ commit }, theme) {
commit('SET_EDITOR_THEME', theme);
} }
} }
}; };

View File

@@ -1,6 +1,7 @@
'use strict'; 'use strict';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database'; import Database from '@/ipc-api/Database';
import Users from '@/ipc-api/Users';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
const tabIndex = []; const tabIndex = [];
let lastBreadcrumbs = {}; let lastBreadcrumbs = {};
@@ -100,6 +101,14 @@ export default {
} }
: workspace); : workspace);
}, },
REFRESH_USERS (state, { uid, users }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
users
}
: workspace);
},
ADD_WORKSPACE (state, workspace) { ADD_WORKSPACE (state, workspace) {
state.workspaces.push(workspace); state.workspaces.push(workspace);
}, },
@@ -111,11 +120,10 @@ export default {
} }
: workspace); : workspace);
}, },
NEW_TAB (state, uid) { NEW_TAB (state, { uid, tab }) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1; tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
const newTab = { const newTab = {
uid: uidGen('T'), uid: tab,
index: tabIndex[uid], index: tabIndex[uid],
selected: false, selected: false,
type: 'query', type: 'query',
@@ -222,6 +230,7 @@ export default {
dispatch('refreshCollations', connection.uid); dispatch('refreshCollations', connection.uid);
dispatch('refreshVariables', connection.uid); dispatch('refreshVariables', connection.uid);
dispatch('refreshEngines', connection.uid); dispatch('refreshEngines', connection.uid);
dispatch('refreshUsers', connection.uid);
} }
} }
catch (err) { catch (err) {
@@ -276,6 +285,18 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
} }
}, },
async refreshUsers ({ dispatch, commit }, uid) {
try {
const { status, response } = await Users.getUsers(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_USERS', { uid, users: response });
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
removeConnected ({ commit }, uid) { removeConnected ({ commit }, uid) {
Connection.disconnect(uid); Connection.disconnect(uid);
commit('REMOVE_CONNECTED', uid); commit('REMOVE_CONNECTED', uid);
@@ -301,6 +322,7 @@ export default {
structure: {}, structure: {},
variables: [], variables: [],
collations: [], collations: [],
users: [],
breadcrumbs: {} breadcrumbs: {}
}; };
@@ -323,7 +345,9 @@ export default {
table: null, table: null,
trigger: null, trigger: null,
procedure: null, procedure: null,
scheduler: null function: null,
scheduler: null,
view: null
}; };
const hasLastChildren = Object.keys(lastBreadcrumbs).filter(b => b !== 'schema').some(b => lastBreadcrumbs[b]); const hasLastChildren = Object.keys(lastBreadcrumbs).filter(b => b !== 'schema').some(b => lastBreadcrumbs[b]);
@@ -338,7 +362,10 @@ export default {
lastBreadcrumbs = { ...breadcrumbsObj, ...payload }; lastBreadcrumbs = { ...breadcrumbsObj, ...payload };
}, },
newTab ({ commit }, uid) { newTab ({ commit }, uid) {
commit('NEW_TAB', uid); const tab = uidGen('T');
commit('NEW_TAB', { uid, tab });
commit('SELECT_TAB', { uid, tab });
}, },
removeTab ({ commit }, payload) { removeTab ({ commit }, payload) {
commit('REMOVE_TAB', payload); commit('REMOVE_TAB', payload);

View File

@@ -1,12 +0,0 @@
import { functions } from '@/suggestions/sql/sql-functions';
import { keywords } from '@/suggestions/sql/sql-keywords';
import { operators } from '@/suggestions/sql/sql-operators';
import { variables } from '@/suggestions/sql/sql-variables';
export const completionItemProvider = (monaco) => {
return {
provideCompletionItems () {
return { suggestions: [...functions(monaco), ...keywords(monaco), ...operators(monaco), ...variables(monaco)] };
}
};
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,142 +0,0 @@
export const operators = (monaco) => {
return [{
label: 'ALL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'ALL'
},
{
label: 'AND',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'AND'
},
{
label: 'ANY',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'ANY'
},
{
label: 'BETWEEN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'BETWEEN'
},
{
label: 'EXISTS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'EXISTS'
},
{
label: 'IN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'IN'
},
{
label: 'LIKE',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'LIKE'
},
{
label: 'NOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'NOT'
},
{
label: 'OR',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'OR'
},
{
label: 'SOME',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'SOME'
},
{
label: 'EXCEPT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'EXCEPT'
},
{
label: 'INTERSECT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'INTERSECT'
},
{
label: 'UNION',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'UNION'
},
{
label: 'APPLY',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'APPLY'
},
{
label: 'CROSS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'CROSS'
},
{
label: 'FULL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'FULL'
},
{
label: 'INNER',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'INNER'
},
{
label: 'JOIN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'JOIN'
},
{
label: 'LEFT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'LEFT'
},
{
label: 'OUTER',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'OUTER'
},
{
label: 'RIGHT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'RIGHT'
},
{
label: 'CONTAINS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'CONTAINS'
},
{
label: 'FREETEXT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'FREETEXT'
},
{
label: 'IS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'IS'
},
{
label: 'NULL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'NULL'
},
{
label: 'PIVOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'PIVOT'
},
{
label: 'UNPIVOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'UNPIVOT'
},
{
label: 'MATCHED',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'MATCHED'
}];
};

View File

@@ -1,172 +0,0 @@
export const variables = (monaco) => {
return [{
label: '@@DATEFIRST',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DATEFIRST'
},
{
label: '@@DBTS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DBTS'
},
{
label: '@@LANGID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LANGID'
},
{
label: '@@LANGUAGE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LANGUAGE'
},
{
label: '@@LOCK_TIMEOUT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LOCK_TIMEOUT'
},
{
label: '@@MAX_CONNECTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@MAX_CONNECTIONS'
},
{
label: '@@MAX_PRECISION',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@MAX_PRECISION'
},
{
label: '@@NESTLEVEL',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@NESTLEVEL'
},
{
label: '@@OPTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@OPTIONS'
},
{
label: '@@REMSERVER',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@REMSERVER'
},
{
label: '@@SERVERNAME',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SERVERNAME'
},
{
label: '@@SERVICENAME',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SERVICENAME'
},
{
label: '@@SPID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SPID'
},
{
label: '@@TEXTSIZE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TEXTSIZE'
},
{
label: '@@VERSION',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@VERSION'
},
{
label: '@@CURSOR_ROWS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CURSOR_ROWS'
},
{
label: '@@FETCH_STATUS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@FETCH_STATUS'
},
{
label: '@@DATEFIRST',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DATEFIRST'
},
{
label: '@@PROCID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PROCID'
},
{
label: '@@ERROR',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@ERROR'
},
{
label: '@@IDENTITY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IDENTITY'
},
{
label: '@@ROWCOUNT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@ROWCOUNT'
},
{
label: '@@TRANCOUNT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TRANCOUNT'
},
{
label: '@@CONNECTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CONNECTIONS'
},
{
label: '@@CPU_BUSY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CPU_BUSY'
},
{
label: '@@IDLE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IDLE'
},
{
label: '@@IO_BUSY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IO_BUSY'
},
{
label: '@@PACKET_ERRORS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACKET_ERRORS'
},
{
label: '@@PACK_RECEIVED',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACK_RECEIVED'
},
{
label: '@@PACK_SENT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACK_SENT'
},
{
label: '@@TIMETICKS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TIMETICKS'
},
{
label: '@@TOTAL_ERRORS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_ERRORS'
},
{
label: '@@TOTAL_READ',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_READ'
},
{
label: '@@TOTAL_WRITE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_WRITE'
}];
};

View File

@@ -1,12 +1,8 @@
const webpack = require('webpack'); const webpack = require('webpack');
const MonacoEditorPlugin = require('monaco-editor-webpack-plugin');
module.exports = { module.exports = {
stats: 'errors-warnings', stats: 'errors-warnings',
plugins: [ plugins: [
new MonacoEditorPlugin({
languages: ['sql']
}),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {
PACKAGE_VERSION: JSON.stringify(require('./package.json').version) PACKAGE_VERSION: JSON.stringify(require('./package.json').version)