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

Compare commits

..

34 Commits

Author SHA1 Message Date
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
7470bddd70 chore(release): 0.0.10 2020-12-04 11:20:51 +01:00
33d1fa2290 feat: unsaved changes reminder 2020-12-04 11:19:16 +01:00
a4122b4eaa feat: drop and truncate tables 2020-12-03 16:15:10 +01:00
e6602d1bfa feat: create new tables 2020-12-03 13:00:54 +01:00
f8cf90a89e fix: index deletion issue 2020-12-01 17:29:16 +01:00
41505bde65 feat: index management 2020-12-01 16:48:20 +01:00
8ebc3bce92 chore: remove deprecated eslint-plugin-standard 2020-11-30 18:24:12 +01:00
45e9cdc591 Merge pull request #40 from Fabio286/dependabot/npm_and_yarn/eslint-plugin-standard-5.0.0
build(deps-dev): bump eslint-plugin-standard from 4.1.0 to 5.0.0
2020-11-30 18:20:47 +01:00
3cbfc0e148 chore: update README.md 2020-11-30 10:04:31 +01:00
a47e9e1b1f Merge pull request #41 from EStarium/dependabot/npm_and_yarn/electron-11.0.2
build(deps-dev): bump electron from 10.1.6 to 11.0.2
2020-11-28 09:16:34 +01:00
e95d29c7c3 feat: approximate totals in table tata tab 2020-11-25 11:47:35 +01:00
e954f04828 refactor: improved structure for table options modal 2020-11-23 12:25:44 +01:00
dependabot[bot]
85c800f85b build(deps-dev): bump electron from 10.1.6 to 11.0.2
Bumps [electron](https://github.com/electron/electron) from 10.1.6 to 11.0.2.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v10.1.6...v11.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-23 05:58:15 +00:00
dependabot[bot]
e0482244d7 build(deps-dev): bump eslint-plugin-standard from 4.1.0 to 5.0.0
Bumps [eslint-plugin-standard](https://github.com/standard/eslint-plugin-standard) from 4.1.0 to 5.0.0.
- [Release notes](https://github.com/standard/eslint-plugin-standard/releases)
- [Commits](https://github.com/standard/eslint-plugin-standard/compare/v4.1.0...v5.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-23 05:47:45 +00:00
27769f204f feat: display all keys in properties tab 2020-11-20 17:24:02 +01:00
dfb24c65f3 fix: sqlEscaper function wrong quotes conversion 2020-11-20 09:16:18 +01:00
0fe71572a5 fix: some problems with properties and data tabs when changing database from sidebar 2020-11-18 18:21:15 +01:00
db577bfef0 ci: temporary removed Linux ARM build 2020-11-16 17:17:33 +01:00
0805b96a75 feat: tables options edit 2020-11-16 17:16:39 +01:00
49 changed files with 2304 additions and 177 deletions

View File

@@ -39,9 +39,9 @@ jobs:
script: script:
- npm run build -- -p always - npm run build -- -p always
- stage: Deploy ARM Linux # - stage: Deploy ARM Linux
if: tag IS present # if: tag IS present
os: linux # os: linux
arch: arm64 # arch: arm64
script: # script:
- npm run build -- --linux AppImage -p always # - npm run build -- --linux AppImage -p always

View File

@@ -2,6 +2,47 @@
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.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)
### Features
* approximate totals in table tata tab ([e95d29c](https://github.com/Fabio286/antares/commit/e95d29c7c37e24e7cc14b466f9b539fa667042c2))
* create new tables ([e6602d1](https://github.com/Fabio286/antares/commit/e6602d1bfa9ca10c6bb078ee80ddc94fb338763d))
* display all keys in properties tab ([27769f2](https://github.com/Fabio286/antares/commit/27769f204f731d20c7ba2f838c02b7c2f28fa0c3))
* drop and truncate tables ([a4122b4](https://github.com/Fabio286/antares/commit/a4122b4eaaa5b30d97ba5a93df8c9d21c30bc40b))
* index management ([41505bd](https://github.com/Fabio286/antares/commit/41505bde6547c0af3c3413248ad8a0d182838bb1))
* tables options edit ([0805b96](https://github.com/Fabio286/antares/commit/0805b96a75e439a7d65e8341ecc86fa938679a9f))
* unsaved changes reminder ([33d1fa2](https://github.com/Fabio286/antares/commit/33d1fa22905f477924292135b0dcfefe168ee641))
### Bug Fixes
* index deletion issue ([f8cf90a](https://github.com/Fabio286/antares/commit/f8cf90a89e7367c95e164b7dc669506df392b700))
* some problems with properties and data tabs when changing database from sidebar ([0fe7157](https://github.com/Fabio286/antares/commit/0fe71572a5e74c17a5c66237351bb0b02c33e824))
* sqlEscaper function wrong quotes conversion ([dfb24c6](https://github.com/Fabio286/antares/commit/dfb24c65f3c395d78d27a2f29e9aa8eeb427cff7))
### [0.0.9](https://github.com/EStarium/antares/compare/v0.0.8...v0.0.9) (2020-11-13) ### [0.0.9](https://github.com/EStarium/antares/compare/v0.0.8...v0.0.9) (2020-11-13)

View File

@@ -4,14 +4,16 @@
# Antares SQL Client # Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/estarium/antares) [![Build Status](https://travis-ci.com/EStarium/antares.svg?branch=master)](https://travis-ci.com/EStarium/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/estarium/antares/total) ![GitHub](https://img.shields.io/github/license/estarium/antares) ![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares)
Antares 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/EStarium/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.
🌟 Don't forget to **leave a star** if you appreciate this project. 🌟 Don't forget to **leave a star** if you appreciate this project.
@@ -23,14 +25,13 @@ An application created with minimalism and semplicity in mind, with features in
## How to contribute ## How to contribute
- [Translate Antares](https://github.com/EStarium/antares/wiki/Translate-Antares) - [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
## Current main features ## Current main features
- 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.
- Native dark theme. - Native dark theme.
@@ -41,12 +42,13 @@ An application created with minimalism and semplicity in mind, with features in
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.
- Users management (add/edit/delete).
- More secure password storage. - More secure password storage.
- Database tools (variables, process list...). - Database tools (variables, process list...).
- SSL and SSH tunnel support.
- Support for other databases. - Support for other databases.
- UI/UX improvements.
- Improvements of query editor area. - Improvements of query editor area.
- Improvements of query suggestions. - Improvements of query suggestions.
- Query history. - Query history.
@@ -55,7 +57,6 @@ This is a roadmap with major features will come in near future.
- 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
@@ -80,11 +81,11 @@ This is a roadmap with major features will come in near future.
#### • ARM #### • ARM
- [ ] Windows - [ ] Windows
- [x] Linux - [ ] Linux
- [ ] MacOS - [ ] MacOS
## Translations ## Translations
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/EStarium/antares/pull/20) [Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/Fabio286/antares/pull/20)
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/EStarium/antares/pull/29) [Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/Fabio286/antares/pull/29)
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/EStarium/antares/pull/32) [hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/Fabio286/antares/pull/32)

5
jsconfig.json Normal file
View File

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

View File

@@ -1,10 +1,10 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.0.9", "version": "0.0.11",
"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/EStarium/antares.git", "repository": "https://github.com/Fabio286/antares.git",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development electron-webpack dev", "dev": "cross-env NODE_ENV=development electron-webpack dev",
"compile": "electron-webpack", "compile": "electron-webpack",
@@ -17,7 +17,7 @@
}, },
"author": "Fabio Di Stasio <fabio286@gmail.com>", "author": "Fabio Di Stasio <fabio286@gmail.com>",
"build": { "build": {
"appId": "com.estarium.antares", "appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"dmg": { "dmg": {
"contents": [ "contents": [
@@ -58,32 +58,31 @@
"pg": "^8.5.1", "pg": "^8.5.1",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"spectre.css": "^0.5.9", "spectre.css": "^0.5.9",
"vue-i18n": "^8.22.1", "vue-i18n": "^8.22.2",
"vue-the-mask": "^0.11.1", "vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.5.1", "vuex": "^3.6.0",
"vuex-persist": "^3.1.3" "vuex-persist": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"electron": "^10.1.5", "electron": "^11.0.2",
"electron-builder": "^22.9.1", "electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0", "electron-webpack-vue": "^2.4.0",
"eslint": "^7.13.0", "eslint": "^7.14.0",
"eslint-config-standard": "^16.0.1", "eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^7.1.0", "eslint-plugin-vue": "^7.1.0",
"monaco-editor-webpack-plugin": "^1.9.1", "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",
"stylelint": "^13.7.2", "stylelint": "^13.8.0",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0", "stylelint-scss": "^3.18.0",
"vue": "^2.6.12", "vue": "^2.6.12",

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

@@ -0,0 +1,6 @@
module.exports = [
'PRIMARY',
'INDEX',
'UNIQUE',
'FULLTEXT'
];

View File

@@ -11,8 +11,8 @@ const regex = new RegExp(pattern);
*/ */
function sqlEscaper (string) { function sqlEscaper (string) {
return string.replace(regex, char => { return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '"', '\\', '\\\\', '%']; const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\'\'', '""', '\\\\', '\\\\\\\\', '\\%']; const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\\%'];
return r[m.indexOf(char)] || char; return r[m.indexOf(char)] || char;
}); });
} }

View File

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

View File

@@ -14,14 +14,29 @@ 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 };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-indexes', async (event, params) => {
try {
const result = await connections[params.uid].getTableIndexes(params);
return { status: 'success', response: result }; return { status: 'success', response: result };
} }
@@ -78,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 };
@@ -154,6 +176,16 @@ export default (connections) => {
} }
}); });
ipcMain.handle('create-table', async (event, params) => {
try {
await connections[params.uid].createTable(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('alter-table', async (event, params) => { ipcMain.handle('alter-table', async (event, params) => {
try { try {
await connections[params.uid].alterTable(params); await connections[params.uid].alterTable(params);
@@ -163,4 +195,24 @@ export default (connections) => {
return { status: 'error', response: err.toString() }; return { status: 'error', response: err.toString() };
} }
}); });
ipcMain.handle('truncate-table', async (event, params) => {
try {
await connections[params.uid].truncateTable(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('drop-table', async (event, params) => {
try {
await connections[params.uid].dropTable(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
}; };

View File

@@ -177,7 +177,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,
@@ -198,6 +198,30 @@ export class MySQLClient extends AntaresCore {
}); });
} }
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table indexes
* @memberof MySQLClient
*/
async getTableIndexes ({ schema, table }) {
const { rows } = await this.raw(`SHOW INDEXES FROM \`${table}\` FROM \`${schema}\``);
return rows.map(row => {
return {
unique: !row.Non_unique,
name: row.Key_name,
column: row.Column_name,
indexType: row.Index_type,
type: row.Key_name === 'PRIMARY' ? 'PRIMARY' : !row.Non_unique ? 'UNIQUE' : row.Index_type === 'FULLTEXT' ? 'FULLTEXT' : 'INDEX',
cardinality: row.Cardinality,
comment: row.Comment,
indexComment: row.Index_comment
};
});
}
/** /**
* @param {Object} params * @param {Object} params
* @param {String} params.schema * @param {String} params.schema
@@ -213,17 +237,27 @@ 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
}; };
}); });
} }
@@ -267,6 +301,48 @@ export class MySQLClient extends AntaresCore {
}); });
} }
/**
* SHOW ENGINES
*
* @returns {Array.<Object>} engines list
* @memberof MySQLClient
*/
async getEngines () {
const sql = 'SHOW ENGINES';
const results = await this.raw(sql);
return results.rows.map(row => {
return {
name: row.Engine,
support: row.Support,
comment: row.Comment,
transactions: row.Transactions,
xa: row.XA,
savepoints: row.Savepoints,
isDefault: row.Support.includes('DEFAULT')
};
});
}
/**
* CREATE TABLE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createTable (params) {
const {
name,
collation,
comment,
engine
} = params;
const sql = `CREATE TABLE \`${name}\` (\`${name}_ID\` INT NULL) COMMENT='${comment}', COLLATE='${collation}', ENGINE=${engine}`;
return await this.raw(sql);
}
/** /**
* ALTER TABLE * ALTER TABLE
* *
@@ -278,13 +354,22 @@ export class MySQLClient extends AntaresCore {
table, table,
additions, additions,
deletions, deletions,
changes changes,
indexChanges,
foreignChanges,
options
} = params; } = params;
let sql = `ALTER TABLE \`${table}\` `; let sql = `ALTER TABLE \`${table}\` `;
const alterColumns = []; const alterColumns = [];
// ADD // OPTIONS
if ('comment' in options) alterColumns.push(`COMMENT='${options.comment}'`);
if ('engine' in options) alterColumns.push(`ENGINE=${options.engine}`);
if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`);
if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`);
// ADD FIELDS
additions.forEach(addition => { additions.forEach(addition => {
const length = addition.numLength || addition.charLength || addition.datePrecision; const length = addition.numLength || addition.charLength || addition.datePrecision;
@@ -301,7 +386,27 @@ export class MySQLClient extends AntaresCore {
${addition.after ? `AFTER \`${addition.after}\`` : 'FIRST'}`); ${addition.after ? `AFTER \`${addition.after}\`` : 'FIRST'}`);
}); });
// CHANGE // ADD INDEX
indexChanges.additions.forEach(addition => {
const fields = addition.fields.map(field => `\`${field}\``).join(',');
let type = addition.type;
if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else {
if (type === 'UNIQUE')
type = 'UNIQUE INDEX';
alterColumns.push(`ADD ${type} \`${addition.name}\` (${fields})`);
}
});
// 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
changes.forEach(change => { changes.forEach(change => {
const length = change.numLength || change.charLength || change.datePrecision; const length = change.numLength || change.charLength || change.datePrecision;
@@ -318,13 +423,77 @@ export class MySQLClient extends AntaresCore {
${change.after ? `AFTER \`${change.after}\`` : 'FIRST'}`); ${change.after ? `AFTER \`${change.after}\`` : 'FIRST'}`);
}); });
// DROP // CHANGE INDEX
indexChanges.changes.forEach(change => {
if (change.oldType === 'PRIMARY')
alterColumns.push('DROP PRIMARY KEY');
else
alterColumns.push(`DROP INDEX \`${change.oldName}\``);
const fields = change.fields.map(field => `\`${field}\``).join(',');
let type = change.type;
if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else {
if (type === 'UNIQUE')
type = 'UNIQUE INDEX';
alterColumns.push(`ADD ${type} \`${change.name}\` (${fields})`);
}
});
// 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
deletions.forEach(deletion => { deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN \`${deletion.name}\``); alterColumns.push(`DROP COLUMN \`${deletion.name}\``);
}); });
// DROP INDEX
indexChanges.deletions.forEach(deletion => {
if (deletion.type === 'PRIMARY')
alterColumns.push('DROP PRIMARY KEY');
else
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
if (options.name) sql += `; RENAME TABLE \`${table}\` TO \`${options.name}\``;
return await this.raw(sql);
}
/**
* TRUNCATE TABLE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async truncateTable (params) {
const sql = `TRUNCATE TABLE \`${params.table}\``;
return await this.raw(sql);
}
/**
* DROP TABLE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropTable (params) {
const sql = `DROP TABLE \`${params.table}\``;
return await this.raw(sql); return await this.raw(sql);
} }

View File

@@ -17,6 +17,7 @@
<TheNotificationsBoard /> <TheNotificationsBoard />
<ModalNewConnection v-if="isNewConnModal" /> <ModalNewConnection v-if="isNewConnModal" />
<ModalSettings v-if="isSettingModal" /> <ModalSettings v-if="isSettingModal" />
<ModalDiscardChanges v-if="isUnsavedDiscardModal" />
</div> </div>
</div> </div>
</template> </template>
@@ -35,7 +36,8 @@ export default {
TheAppWelcome: () => import(/* webpackChunkName: "TheAppWelcome" */'@/components/TheAppWelcome'), TheAppWelcome: () => import(/* webpackChunkName: "TheAppWelcome" */'@/components/TheAppWelcome'),
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'), Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'),
ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'), ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'),
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings') ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'),
ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges')
}, },
data () { data () {
return {}; return {};
@@ -46,7 +48,8 @@ export default {
isNewConnModal: 'application/isNewModal', isNewConnModal: 'application/isNewModal',
isEditModal: 'application/isEditModal', isEditModal: 'application/isEditModal',
isSettingModal: 'application/isSettingModal', isSettingModal: 'application/isSettingModal',
connections: 'connections/getConnections' connections: 'connections/getConnections',
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
}) })
}, },
mounted () { mounted () {

View File

@@ -86,9 +86,8 @@ export default {
.context-container { .context-container {
min-width: 100px; min-width: 100px;
max-width: 150px;
z-index: 10; z-index: 10;
box-shadow: 0 0 1px 0 #000; box-shadow: 0 0 2px 0 #000;
padding: 0; padding: 0;
background: #1d1d1d; background: #1d1d1d;
border-radius: 0.1rem; border-radius: 0.1rem;
@@ -103,9 +102,28 @@ export default {
padding: 0.1rem 0.3rem; padding: 0.1rem 0.3rem;
cursor: pointer; cursor: pointer;
justify-content: space-between; justify-content: space-between;
position: relative;
.context-submenu {
opacity: 0;
visibility: hidden;
transition: opacity 0.2s;
position: absolute;
left: 100%;
top: 0;
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
min-width: 100px;
}
&:hover { &:hover {
background: $primary-color; background: $primary-color;
.context-submenu {
display: block;
visibility: visible;
opacity: 1;
}
} }
} }
} }

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

@@ -0,0 +1,57 @@
<template>
<ConfirmModal
:confirm-text="$t('word.discard')"
:cancel-text="$t('word.stay')"
@confirm="discardUnsavedChanges"
@hide="closeUnsavedChangesModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ $t('message.unsavedChanges') }}
</div>
</template>
<div slot="body">
<div>
{{ $t('message.discardUnsavedChanges') }}
</div>
</div>
</ConfirmModal>
</template>
<script>
import { mapActions } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalDiscardChanges',
components: {
ConfirmModal
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
discardUnsavedChanges: 'workspaces/discardUnsavedChanges',
closeUnsavedChangesModal: 'workspaces/closeUnsavedChangesModal'
}),
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style scoped>
.modal-container {
max-width: 360px;
}
</style>

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,127 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-plus mr-1" /> {{ $t('message.createNewTable') }}
</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="localOptions.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localOptions.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.collation') }}
</label>
<div class="column">
<select v-model="localOptions.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.engine') }}
</label>
<div class="column">
<select v-model="localOptions.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import { mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTable',
components: {
ConfirmModal
},
props: {
table: String,
workspace: Object
},
data () {
return {
localOptions: {
name: '',
comment: '',
collation: '',
engine: ''
},
isOptionsChanging: false
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
},
defaultEngine () {
return this.workspace.engines.find(engine => engine.isDefault).name;
}
},
mounted () {
this.localOptions.collation = this.defaultCollation;
this.localOptions.engine = this.defaultEngine;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
this.$emit('open-create-table-editor', this.localOptions);
}
}
};
</script>

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

@@ -111,7 +111,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>

View File

@@ -14,7 +14,8 @@ monaco.languages.registerCompletionItemProvider('sql', completionItemProvider(mo
export default { export default {
name: 'QueryEditor', name: 'QueryEditor',
props: { props: {
value: String value: String,
autoFocus: { type: Boolean, default: false }
}, },
data () { data () {
return { return {
@@ -40,6 +41,12 @@ export default {
const content = this.editor.getValue(); const content = this.editor.getValue();
this.$emit('update:value', content); this.$emit('update:value', content);
}); });
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
}, 20);
}
}, },
beforeDestroy () { beforeDestroy () {
this.editor && this.editor.dispose(); this.editor && this.editor.dispose();

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

@@ -67,6 +67,7 @@
</ul> </ul>
<WorkspacePropsTab <WorkspacePropsTab
v-show="selectedTab === 'prop'" v-show="selectedTab === 'prop'"
:is-selected="selectedTab === 'prop'"
:connection="connection" :connection="connection"
:table="workspace.breadcrumbs.table" :table="workspace.breadcrumbs.table"
/> />
@@ -122,7 +123,13 @@ export default {
return this.selectedWorkspace === this.connection.uid; return this.selectedWorkspace === this.connection.uid;
}, },
selectedTab () { selectedTab () {
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) || ['data', 'prop'].includes(this.workspace.selected_tab) ? this.workspace.selected_tab : this.queryTabs[0].uid; if (this.workspace.breadcrumbs.table === null && ['data', 'prop'].includes(this.workspace.selected_tab))
return this.queryTabs[0].uid;
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) ||
['data', 'prop'].includes(this.workspace.selected_tab)
? this.workspace.selected_tab
: this.queryTabs[0].uid;
}, },
queryTabs () { queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query'); return this.workspace.tabs.filter(tab => tab.type === 'query');

View File

@@ -39,6 +39,7 @@
:database="db" :database="db"
:connection="connection" :connection="connection"
@show-database-context="openDatabaseContext" @show-database-context="openDatabaseContext"
@show-table-context="openTableContext"
/> />
</div> </div>
</div> </div>
@@ -47,11 +48,25 @@
@close="hideNewDBModal" @close="hideNewDBModal"
@reload="refresh" @reload="refresh"
/> />
<ModalNewTable
v-if="isNewTableModal"
:workspace="workspace"
@close="hideCreateTableModal"
@open-create-table-editor="openCreateTableEditor"
/>
<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"
@reload="refresh"
/>
<TableContext
v-if="isTableContext"
:selected-table="selectedTable"
:context-event="tableContextEvent"
@close-context="closeTableContext"
@reload="refresh" @reload="refresh"
/> />
</div> </div>
@@ -60,10 +75,13 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash'; import _ from 'lodash';
import Tables from '@/ipc-api/Tables';
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 ModalNewDatabase from '@/components/ModalNewDatabase'; import ModalNewDatabase from '@/components/ModalNewDatabase';
import ModalNewTable from '@/components/ModalNewTable';
export default { export default {
name: 'WorkspaceExploreBar', name: 'WorkspaceExploreBar',
@@ -71,7 +89,9 @@ export default {
WorkspaceConnectPanel, WorkspaceConnectPanel,
WorkspaceExploreBarDatabase, WorkspaceExploreBarDatabase,
DatabaseContext, DatabaseContext,
ModalNewDatabase TableContext,
ModalNewDatabase,
ModalNewTable
}, },
props: { props: {
connection: Object, connection: Object,
@@ -81,6 +101,7 @@ export default {
return { return {
isRefreshing: false, isRefreshing: false,
isNewDBModal: false, isNewDBModal: false,
isNewTableModal: false,
localWidth: null, localWidth: null,
isDatabaseContext: false, isDatabaseContext: false,
isTableContext: false, isTableContext: false,
@@ -128,6 +149,9 @@ export default {
...mapActions({ ...mapActions({
disconnectWorkspace: 'workspaces/removeConnected', disconnectWorkspace: 'workspaces/removeConnected',
refreshStructure: 'workspaces/refreshStructure', refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab',
addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize' changeExplorebarSize: 'settings/changeExplorebarSize'
}), }),
async refresh () { async refresh () {
@@ -153,15 +177,44 @@ export default {
hideNewDBModal () { hideNewDBModal () {
this.isNewDBModal = false; this.isNewDBModal = false;
}, },
showCreateTableModal () {
this.closeDatabaseContext();
this.isNewTableModal = true;
},
hideCreateTableModal () {
this.isNewTableModal = false;
},
async openCreateTableEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Tables.createTable(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, table: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
openDatabaseContext (payload) { openDatabaseContext (payload) {
this.isTableContext = false;
this.selectedDatabase = payload.database; this.selectedDatabase = payload.database;
this.databaseContextEvent = payload.event; this.databaseContextEvent = payload.event;
this.isDatabaseContext = true; this.isDatabaseContext = true;
}, },
closeDatabaseContext () { closeDatabaseContext () {
this.isDatabaseContext = false; this.isDatabaseContext = false;
this.selectedDatabase = ''; },
openTableContext (payload) {
this.selectedTable = payload.table;
this.tableContextEvent = payload.event;
this.isTableContext = true;
},
closeTableContext () {
this.isTableContext = false;
} }
} }
}; };

View File

@@ -18,7 +18,7 @@
: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 === table.name}"
@click="changeBreadcrumbs({schema: database.name, table: table.name})" @click="setBreadcrumbs({schema: database.name, table: table.name})"
@contextmenu.prevent="showTableContext($event, table.name)" @contextmenu.prevent="showTableContext($event, table.name)"
> >
<a class="table-name"> <a class="table-name">
@@ -70,12 +70,16 @@ export default {
showDatabaseContext (event, database) { showDatabaseContext (event, database) {
this.$emit('show-database-context', { event, database }); this.$emit('show-database-context', { event, database });
}, },
showTableContext (table) { showTableContext (event, table) {
this.$emit('show-table-context', table); this.$emit('show-table-context', { event, table });
}, },
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)` };
},
setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload);
} }
} }
}; };

View File

@@ -3,15 +3,20 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<!-- <div class="context-element"> <div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
</div> --> <div class="context-submenu">
<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>
</div>
</div>
</div>
<div class="context-element" @click="showEditModal"> <div class="context-element" @click="showEditModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div> </div>
<ConfirmModal <ConfirmModal
@@ -64,15 +69,21 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
selectedWorkspace: 'workspaces/getSelected' selectedWorkspace: 'workspaces/getSelected',
}) getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
}, },
methods: { methods: {
...mapActions({ ...mapActions({
deleteConnection: 'connections/deleteConnection', addNotification: 'notifications/addNotification',
showEditModal: 'application/showEditConnModal', changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
addNotification: 'notifications/addNotification'
}), }),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () { showDeleteModal () {
this.isDeleteModal = true; this.isDeleteModal = true;
}, },
@@ -97,6 +108,9 @@ export default {
}); });
if (status === 'success') { if (status === 'success') {
if (this.selectedDatabase === this.workspace.breadcrumbs.schema)
this.changeBreadcrumbs({ schema: null });
this.closeContext(); this.closeContext();
this.$emit('reload'); this.$emit('reload');
} }

View File

@@ -0,0 +1,146 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div 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>
</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="isEmptyModal"
@confirm="emptyTable"
@hide="hideEmptyModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-off mr-1" /> {{ $t('message.emptyTable') }}
</div>
</template>
<div slot="body">
<div class="mb-2">
{{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable }}</b>"?
</div>
</div>
</ConfirmModal>
<ConfirmModal
v-if="isDeleteModal"
@confirm="deleteTable"
@hide="hideDeleteModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-remove mr-1" /> {{ $t('message.deleteTable') }}
</div>
</template>
<div slot="body">
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable }}</b>"?
</div>
</div>
</ConfirmModal>
</BaseContextMenu>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import Tables from '@/ipc-api/Tables';
export default {
name: 'WorkspaceExploreBarTableContext',
components: {
BaseContextMenu,
ConfirmModal
},
props: {
contextEvent: MouseEvent,
selectedTable: String
},
data () {
return {
isDeleteModal: false,
isEmptyModal: false
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showEmptyModal () {
this.isEmptyModal = true;
},
hideEmptyModal () {
this.isEmptyModal = false;
},
closeContext () {
this.$emit('close-context');
},
async emptyTable () {
try {
const { status, response } = await Tables.truncateTable({
uid: this.selectedWorkspace,
table: this.selectedTable
});
if (status === 'success') {
if (this.selectedTable === this.workspace.breadcrumbs.table)
this.changeBreadcrumbs({ table: null });
this.closeContext();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
async deleteTable () {
try {
const { status, response } = await Tables.dropTable({
uid: this.selectedWorkspace,
table: this.selectedTable
});
if (status === 'success') {
if (this.selectedTable === this.workspace.breadcrumbs.table)
this.changeBreadcrumbs({ table: 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

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

@@ -0,0 +1,284 @@
<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-key mdi-rotate-45 mr-1" /> {{ $t('word.indexes') }} "{{ 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="addIndex">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-key-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="index in indexesProxy"
:key="index._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-index': selectedIndexID === index._id}"
@click="selectIndex($event, index._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-key mdi-24px column-key" :class="`key-${index.type}`" />
</div>
</div>
<div class="tile-content">
<div class="tile-title">
{{ index.name }}
</div>
<small class="tile-subtitle text-gray">{{ index.type }} · {{ index.fields.length }} {{ $tc('word.field', index.fields.length) }}</small>
</div>
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeIndex(index._id)"
>
<i class="mdi mdi-close" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="column col-7 pl-2 editor-col">
<form
v-if="selectedIndexObj"
: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="selectedIndexObj.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="selectedIndexObj.type" class="form-select">
<option
v-for="index in indexTypes"
:key="index"
:value="index"
:disabled="index === 'PRIMARY' && hasPrimary"
>
{{ index }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $tc('word.field', fields.length) }}
</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="selectedIndexObj.fields.some(f => f === field.name)">
<i class="form-icon" /> {{ field.name }}
</label>
</div>
</div>
</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>
</ConfirmModal>
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsIndexesModal',
components: {
ConfirmModal
},
props: {
localIndexes: Array,
table: String,
fields: Array,
workspace: Object,
indexTypes: Array
},
data () {
return {
indexesProxy: [],
isOptionsChanging: false,
selectedIndexID: '',
modalInnerHeight: 400
};
},
computed: {
selectedIndexObj () {
return this.indexesProxy.find(index => index._id === this.selectedIndexID);
},
isChanged () {
return JSON.stringify(this.localIndexes) !== JSON.stringify(this.indexesProxy);
},
hasPrimary () {
return this.indexesProxy.some(index => index.type === 'PRIMARY');
}
},
mounted () {
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
if (this.indexesProxy.length)
this.resetSelectedID();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
destroyed () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
this.$emit('indexes-update', this.indexesProxy);
},
selectIndex (event, id) {
if (this.selectedIndexID !== id && !event.target.classList.contains('remove-field'))
this.selectedIndexID = id;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addIndex () {
this.indexesProxy = [...this.indexesProxy, {
_id: uidGen(),
name: 'NEW_INDEX',
fields: [],
type: 'INDEX',
comment: '',
indexType: 'BTREE',
indexComment: '',
cardinality: 0
}];
if (this.indexesProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.indexesPanel.scrollTop = this.$refs.indexesPanel.scrollHeight + 60;
}, 20);
},
removeIndex (id) {
this.indexesProxy = this.indexesProxy.filter(index => index._id !== id);
if (this.selectedIndexID === id && this.indexesProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
if (!this.indexesProxy.some(index => index._id === this.selectedIndexID))
this.resetSelectedID();
},
toggleField (field) {
this.indexesProxy = this.indexesProxy.map(index => {
if (index._id === this.selectedIndexID) {
if (index.fields.includes(field))
index.fields = index.fields.filter(f => f !== field);
else
index.fields.push(field);
}
return index;
});
},
resetSelectedID () {
this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._id : '';
}
}
};
</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-index {
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

@@ -0,0 +1,130 @@
<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') }} "{{ table }}"
</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.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('word.autoIncrement') }}
</label>
<div class="column">
<input
v-model="optionsProxy.autoIncrement"
class="form-input"
type="number"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.collation') }}
</label>
<div class="column">
<select v-model="optionsProxy.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.engine') }}
</label>
<div class="column">
<select v-model="optionsProxy.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
table: String,
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

@@ -32,15 +32,19 @@
<span>{{ $t('word.add') }}</span> <span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" /> <i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button> </button>
<button class="btn btn-dark btn-sm d-none"> <button
class="btn btn-dark btn-sm"
:title="$t('message.manageIndexes')"
@click="showIntdexesModal"
>
<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>
<button class="btn btn-dark btn-sm d-none"> <button class="btn btn-dark btn-sm" @click="showOptionsModal">
<span>{{ $t('word.options') }}</span> <span>{{ $t('word.options') }}</span>
<i class="mdi mdi-24px mdi-cogs ml-1" /> <i class="mdi mdi-24px mdi-cogs ml-1" />
</button> </button>
@@ -50,16 +54,51 @@
<div class="workspace-query-results column col-12"> <div class="workspace-query-results column col-12">
<WorkspacePropsTable <WorkspacePropsTable
v-if="localFields" v-if="localFields"
ref="queryTable" ref="indexTable"
:fields="localFields" :fields="localFields"
:indexes="localIndexes"
:foreigns="localKeyUsage"
:tab-uid="tabUid" :tab-uid="tabUid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:index-types="workspace.indexTypes"
:table="table" :table="table"
:schema="schema" :schema="schema"
mode="table" mode="table"
@remove-field="removeField" @remove-field="removeField"
@add-new-index="addNewIndex"
@add-to-index="addToIndex"
/> />
</div> </div>
<WorkspacePropsOptionsModal
v-if="isOptionsModal"
:local-options="localOptions"
:table="table"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsIndexesModal
v-if="isIndexesModal"
:local-indexes="localIndexes"
:table="table"
:fields="localFields"
:index-types="workspace.indexTypes"
:workspace="workspace"
@hide="hideIndexesModal"
@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>
@@ -68,11 +107,17 @@ import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import WorkspacePropsTable from '@/components/WorkspacePropsTable'; import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal';
export default { export default {
name: 'WorkspacePropsTab', name: 'WorkspacePropsTab',
components: { components: {
WorkspacePropsTable WorkspacePropsTable,
WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal,
WorkspacePropsForeignModal
}, },
props: { props: {
connection: Object, connection: Object,
@@ -83,29 +128,54 @@ export default {
tabUid: 'prop', tabUid: 'prop',
isQuering: false, isQuering: false,
isSaving: false, isSaving: false,
isOptionsModal: false,
isIndexesModal: false,
isForeignModal: false,
isOptionsChanging: false,
originalFields: [], originalFields: [],
localFields: [], localFields: [],
originalKeyUsage: [], originalKeyUsage: [],
localKeyUsage: [], localKeyUsage: [],
originalIndexes: [],
localIndexes: [],
localOptions: {},
lastTable: null, lastTable: null,
isAddModal: false newFieldsCounter: 0
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
tableOptions () {
const db = this.workspace.structure.find(db => db.name === this.schema);
return db && this.table ? db.tables.find(table => table.name === this.table) : {};
},
defaultEngine () {
return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || '';
},
isSelected () { isSelected () {
return this.workspace.selected_tab === 'prop'; return this.workspace.selected_tab === 'prop';
}, },
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) || JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage); return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
} }
}, },
watch: { watch: {
@@ -120,15 +190,23 @@ export default {
this.getFieldsData(); this.getFieldsData();
this.lastTable = this.table; this.lastTable = this.table;
} }
},
isChanged (val) {
if (this.isSelected && this.lastTable === this.table && this.table !== null)
this.setUnsavedChanges(val);
} }
}, },
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges'
}), }),
async getFieldsData () { async getFieldsData () {
if (!this.table) return; if (!this.table) return;
this.newFieldsCounter = 0;
this.isQuering = true; this.isQuering = true;
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
@@ -151,12 +229,49 @@ export default {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
} }
try { // Indexes
const { status, response } = await Tables.getTableIndexes(params);
if (status === 'success') {
const indexesObj = response.reduce((acc, curr) => {
acc[curr.name] = acc[curr.name] || [];
acc[curr.name].push(curr);
return acc;
}, {});
this.originalIndexes = Object.keys(indexesObj).map(index => {
return {
_id: uidGen(),
name: index,
fields: indexesObj[index].map(field => field.column),
type: indexesObj[index][0].type,
comment: indexesObj[index][0].comment,
indexType: indexesObj[index][0].indexType,
indexComment: indexesObj[index][0].indexComment,
cardinality: indexesObj[index][0].cardinality
};
});
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes));
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Key usage (foreign keys) try { // Key usage (foreign keys)
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 });
@@ -171,20 +286,21 @@ export default {
if (this.isSaving) return; if (this.isSaving) return;
this.isSaving = true; this.isSaving = true;
// FIELDS
const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._id], []); const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._id], []);
const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._id], []); const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._id], []);
// Additions // Fields Additions
const additions = this.localFields.filter((field, i) => !originalIDs.includes(field._id)).map(field => { const additions = this.localFields.filter((field, i) => !originalIDs.includes(field._id)).map(field => {
const lI = this.localFields.findIndex(localField => localField._id === field._id); const lI = this.localFields.findIndex(localField => localField._id === field._id);
const after = lI > 0 ? this.localFields[lI - 1].name : false; const after = lI > 0 ? this.localFields[lI - 1].name : false;
return { ...field, after }; return { ...field, after };
}); });
// Deletions // Fields Deletions
const deletions = this.originalFields.filter(field => !localIDs.includes(field._id)); const deletions = this.originalFields.filter(field => !localIDs.includes(field._id));
// Changes // Fields Changes
const changes = []; const changes = [];
this.originalFields.forEach((originalField, oI) => { this.originalFields.forEach((originalField, oI) => {
const lI = this.localFields.findIndex(localField => localField._id === originalField._id); const lI = this.localFields.findIndex(localField => localField._id === originalField._id);
@@ -197,20 +313,90 @@ export default {
if (this.localFields[lI]) changes.push({ ...this.localFields[lI], after, orgName }); if (this.localFields[lI]) changes.push({ ...this.localFields[lI], after, orgName });
}); });
// OPTIONS
const options = Object.keys(this.localOptions).reduce((acc, option) => {
if (this.localOptions[option] !== this.tableOptions[option])
acc[option] = this.localOptions[option];
return acc;
}, {});
// INDEXES
const indexChanges = {
additions: [],
changes: [],
deletions: []
};
const originalIndexIDs = this.originalIndexes.reduce((acc, curr) => [...acc, curr._id], []);
const localIndexIDs = this.localIndexes.reduce((acc, curr) => [...acc, curr._id], []);
// Index Additions
indexChanges.additions = this.localIndexes.filter(index => !originalIndexIDs.includes(index._id));
// Index Changes
this.originalIndexes.forEach(originalIndex => {
const lI = this.localIndexes.findIndex(localIndex => localIndex._id === originalIndex._id);
if (JSON.stringify(originalIndex) !== JSON.stringify(this.localIndexes[lI])) {
if (this.localIndexes[lI]) {
indexChanges.changes.push({
...this.localIndexes[lI],
oldName: originalIndex.name,
oldType: originalIndex.type
});
}
}
});
// Index Deletions
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,
table: this.workspace.breadcrumbs.table, table: this.workspace.breadcrumbs.table,
additions, additions,
changes, changes,
deletions deletions,
indexChanges,
foreignChanges,
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') {
await this.refreshStructure(this.connection.uid);
this.getFieldsData(); this.getFieldsData();
}
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
} }
@@ -219,15 +405,19 @@ export default {
} }
this.isSaving = false; this.isSaving = false;
this.newFieldsCounter = 0;
}, },
clearChanges () { clearChanges () {
this.localFields = JSON.parse(JSON.stringify(this.originalFields)); this.localFields = JSON.parse(JSON.stringify(this.originalFields));
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes));
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage)); this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
this.newFieldsCounter = 0;
}, },
addField () { addField () {
this.localFields.push({ this.localFields.push({
_id: uidGen(), _id: uidGen(),
name: '', name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '', key: '',
type: 'int', type: 'int',
schema: this.schema, schema: this.schema,
@@ -247,15 +437,59 @@ export default {
onUpdate: '', onUpdate: '',
comment: '' comment: ''
}); });
setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20);
}, },
removeField (uid) { removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid); this.localFields = this.localFields.filter(field => field._id !== uid);
}, },
showAddModal () { addNewIndex (payload) {
this.isAddModal = true; this.localIndexes = [...this.localIndexes, {
_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
fields: [payload.field],
type: payload.index,
comment: '',
indexType: 'BTREE',
indexComment: '',
cardinality: 0
}];
}, },
hideAddModal () { addToIndex (payload) {
this.isAddModal = false; this.localIndexes = this.localIndexes.map(index => {
if (index._id === payload.index) index.fields.push(payload.field);
return index;
});
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
optionsUpdate (options) {
this.localOptions = options;
},
showIntdexesModal () {
this.isIndexesModal = true;
},
hideIndexesModal () {
this.isIndexesModal = false;
},
indexesUpdate (indexes) {
this.localIndexes = indexes;
},
showForeignModal () {
this.isForeignModal = true;
},
hideForeignModal () {
this.isForeignModal = false;
},
foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns;
} }
} }
}; };

View File

@@ -8,8 +8,12 @@
v-if="isContext" v-if="isContext"
:context-event="contextEvent" :context-event="contextEvent"
:selected-field="selectedField" :selected-field="selectedField"
:index-types="indexTypes"
:indexes="indexes"
@delete-selected="removeField" @delete-selected="removeField"
@close-context="isContext = false" @close-context="isContext = false"
@add-new-index="$emit('add-new-index', $event)"
@add-to-index="$emit('add-to-index', $event)"
/> />
<div ref="propTable" class="table table-hover"> <div ref="propTable" class="table table-hover">
<div class="thead"> <div class="thead">
@@ -25,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>
@@ -81,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>
@@ -99,6 +103,8 @@
v-for="row in fields" v-for="row in fields"
:key="row._id" :key="row._id"
:row="row" :row="row"
:indexes="getIndexes(row.name)"
:foreigns="getForeigns(row.name)"
:data-types="dataTypes" :data-types="dataTypes"
@contextmenu="contextMenu" @contextmenu="contextMenu"
/> />
@@ -122,6 +128,9 @@ export default {
}, },
props: { props: {
fields: Array, fields: Array,
indexes: Array,
foreigns: Array,
indexTypes: Array,
tabUid: [String, Number], tabUid: [String, Number],
connUid: String, connUid: String,
table: String, table: String,
@@ -131,7 +140,6 @@ export default {
data () { data () {
return { return {
resultsSize: 1000, resultsSize: 1000,
localResults: [],
isContext: false, isContext: false,
contextEvent: null, contextEvent: null,
selectedField: null, selectedField: null,
@@ -154,6 +162,14 @@ export default {
}, },
tabProperties () { tabProperties () {
return this.getWorkspaceTab(this.tabUid); return this.getWorkspaceTab(this.tabUid);
},
fieldsLength () {
return this.fields.length;
}
},
watch: {
fieldsLength () {
this.refreshScroller();
} }
}, },
updated () { updated () {
@@ -182,19 +198,31 @@ export default {
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
this.resultsSize = size; this.resultsSize = size;
} }
// this.$refs.resultTable.updateWindow();
} }
}, },
refreshScroller () { refreshScroller () {
this.resizeResults(); this.resizeResults();
}, },
contextMenu (event, uid) { contextMenu (event, uid) {
this.selectedField = uid; this.selectedField = this.fields.find(field => field._id === uid);
this.contextEvent = event; this.contextEvent = event;
this.isContext = true; this.isContext = true;
}, },
removeField () { removeField () {
this.$emit('remove-field', this.selectedField); this.$emit('remove-field', this.selectedField._id);
},
getIndexes (field) {
return this.indexes.reduce((acc, curr) => {
acc.push(...curr.fields.map(f => ({ name: f, type: curr.type })));
return acc;
}, []).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;
}, []);
} }
} }
}; };
@@ -208,4 +236,12 @@ export default {
overflow: hidden; overflow: hidden;
} }
} }
.vscroll {
overflow: auto;
}
.min-100 {
min-width: 100px !important;
}
</style> </style>

View File

@@ -3,6 +3,36 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-key-plus text-light pr-1" /> {{ $t('message.createNewIndex') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
v-for="index in indexTypes"
:key="index"
class="context-element"
:class="{'disabled': index === 'PRIMARY' && hasPrimary}"
@click="addNewIndex(index)"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-key column-key pr-1" :class="`key-${index}`" /> {{ index }}</span>
</div>
</div>
</div>
<div v-if="indexes.length" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-key-arrow-right text-light pr-1" /> {{ $t('message.addToIndex') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
v-for="index in indexes"
:key="index.name"
class="context-element"
:class="{'disabled': index.fields.includes(selectedField.name)}"
@click="addToIndex(index._id)"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-key column-key pr-1" :class="`key-${index.type}`" /> {{ index.name }}</span>
</div>
</div>
</div>
<div class="context-element" @click="deleteField"> <div class="context-element" @click="deleteField">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span>
</div> </div>
@@ -19,14 +49,14 @@ export default {
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
selectedField: String indexes: Array,
}, indexTypes: Array,
data () { selectedField: Object
return {
isConfirmModal: false
};
}, },
computed: { computed: {
hasPrimary () {
return this.indexes.some(index => index.type === 'PRIMARY');
}
}, },
methods: { methods: {
closeContext () { closeContext () {
@@ -35,7 +65,23 @@ export default {
deleteField () { deleteField () {
this.$emit('delete-selected'); this.$emit('delete-selected');
this.closeContext(); this.closeContext();
},
addNewIndex (index) {
this.$emit('add-new-index', { field: this.selectedField.name, index });
this.closeContext();
},
addToIndex (index) {
this.$emit('add-to-index', { field: this.selectedField.name, index });
this.closeContext();
} }
} }
}; };
</script> </script>
<style lang="scss" scoped>
.disabled {
pointer-events: none;
filter: grayscale(100%);
opacity: 0.5;
}
</style>

View File

@@ -1,20 +1,29 @@
<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 }}
</div> </div>
</div> </div>
<div class="td" tabindex="0"> <div class="td" tabindex="0">
<i <div class="text-center">
v-if="localRow.key" <i
:title="keyName(localRow.key)" v-for="(index, i) in indexes"
class="mdi mdi-key column-key c-help pl-1" :key="`${index.name}-${i}`"
:class="`key-${localRow.key}`" :title="index.type"
/> class="d-inline-block mdi mdi-key column-key c-help"
: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 class="td"> <div class="td" tabindex="0">
<span <span
v-if="!isInlineEditor.name" v-if="!isInlineEditor.name"
class="cell-content" class="cell-content"
@@ -32,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>
@@ -44,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
@@ -63,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"
@@ -83,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"
@@ -93,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"
@@ -113,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"
@@ -136,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>
@@ -149,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
@@ -221,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"
@@ -279,7 +292,9 @@ export default {
}, },
props: { props: {
row: Object, row: Object,
dataTypes: Array dataTypes: Array,
indexes: Array,
foreigns: Array
}, },
data () { data () {
return { return {
@@ -293,6 +308,7 @@ export default {
onUpdate: '' onUpdate: ''
}, },
editingContent: null, editingContent: null,
originalContent: null,
editingField: null editingField: null
}; };
}, },
@@ -321,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: {
@@ -329,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 () {
@@ -393,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 };
@@ -410,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;
@@ -423,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') {
@@ -456,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,11 @@
<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"
/>
<div class="workspace-query-runner-footer"> <div class="workspace-query-runner-footer">
<div class="workspace-query-buttons"> <div class="workspace-query-buttons">
<button <button
@@ -17,7 +21,7 @@
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div v-if="resultsCount"> <div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount }}</b> {{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div> </div>
<div v-if="affectedCount"> <div v-if="affectedCount">
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b> {{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
@@ -99,6 +103,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

@@ -42,7 +42,10 @@
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div v-if="results.length && results[0].rows"> <div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length }}</b> {{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
</div>
<div v-if="results.length && results[0].rows && tableInfo && results[0].rows.length < tableInfo.rows">
{{ $t('word.total') }}: <b>{{ tableInfo.rows.toLocaleString() }}</b> <small>({{ $t('word.approximately') }})</small>
</div> </div>
<div v-if="workspace.breadcrumbs.database"> <div v-if="workspace.breadcrumbs.database">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b> {{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b>
@@ -60,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
@@ -99,7 +103,8 @@ export default {
lastTable: null, lastTable: null,
isAddModal: false, isAddModal: false,
autorefreshTimer: 0, autorefreshTimer: 0,
refreshInterval: null refreshInterval: null,
sortParams: {}
}; };
}, },
computed: { computed: {
@@ -117,13 +122,23 @@ export default {
}, },
keyUsage () { keyUsage () {
return this.results.length ? this.results[0].keys : []; return this.results.length ? this.results[0].keys : [];
},
tableInfo () {
try {
return this.workspace.structure.find(db => db.name === this.schema).tables.find(table => table.name === this.table);
}
catch (err) {
return { rows: 0 };
}
} }
}, },
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) {
@@ -145,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;
@@ -156,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,
sortParams
}; };
try { // Table data try { // Table data
@@ -177,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

@@ -53,7 +53,16 @@ module.exports = {
comment: 'Comment', comment: 'Comment',
key: 'Key | Keys', key: 'Key | Keys',
order: 'Order', order: 'Order',
expression: 'Expression' expression: 'Expression',
autoIncrement: 'Auto Increment',
engine: 'Engine',
field: 'Field | Fields',
approximately: 'Approximately',
total: 'Total',
table: 'Table',
discard: 'Discard',
stay: 'Stay',
author: 'Author'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -99,7 +108,23 @@ module.exports = {
zeroFill: 'Zero fill', zeroFill: 'Zero fill',
customValue: 'Custom value', customValue: 'Custom value',
onUpdate: 'On update', onUpdate: 'On update',
deleteField: 'Delete field' deleteField: 'Delete field',
createNewIndex: 'Create new index',
addToIndex: 'Add to index',
createNewTable: 'Create new table',
emptyTable: 'Empty table',
deleteTable: 'Delete table',
emptyCorfirm: 'Do you confirm to empty',
unsavedChanges: 'Unsaved changes',
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'
}, },
// Date and Time // Date and Time
short: { short: {

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 34 32"><path fill="#444" d="M21.576 3.59c-1.115-.994-2.465-.595-3.798.588a9.407 9.407 0 00-.591.579c-2.279 2.418-4.395 6.897-5.053 10.318.256.519.456 1.182.588 1.688.034.13.064.252.089.355.058.245.089.405.089.405s-.02-.077-.104-.321l-.055-.158a1.44 1.44 0 00-.035-.087c-.149-.346-.56-1.075-.741-1.393-.155.457-.292.884-.406 1.271.523.956.841 2.595.841 2.595s-.028-.106-.159-.477c-.117-.328-.697-1.345-.835-1.583-.235.869-.329 1.455-.244 1.598.164.277.32.754.457 1.282.309 1.189.524 2.637.524 2.637l.019.244c-.043.999-.017 2.034.06 2.97.103 1.239.295 2.303.541 2.873l.167-.091c-.361-1.122-.508-2.593-.444-4.289.097-2.593.694-5.719 1.796-8.978 1.863-4.919 4.447-8.866 6.811-10.751-2.155 1.947-5.073 8.248-5.946 10.581-.978 2.613-1.671 5.065-2.088 7.414.721-2.202 3.05-3.149 3.05-3.149s1.143-1.409 2.478-3.422c-.8.182-2.113.495-2.553.68-.649.272-.824.365-.824.365s2.102-1.28 3.905-1.86c2.48-3.906 5.182-9.456 2.461-11.884z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 34 32"><path fill="#fff" d="M21.576 3.59c-1.115-.994-2.465-.595-3.798.588a9.407 9.407 0 00-.591.579c-2.279 2.418-4.395 6.897-5.053 10.318.256.519.456 1.182.588 1.688.034.13.064.252.089.355.058.245.089.405.089.405s-.02-.077-.104-.321l-.055-.158a1.44 1.44 0 00-.035-.087c-.149-.346-.56-1.075-.741-1.393-.155.457-.292.884-.406 1.271.523.956.841 2.595.841 2.595s-.028-.106-.159-.477c-.117-.328-.697-1.345-.835-1.583-.235.869-.329 1.455-.244 1.598.164.277.32.754.457 1.282.309 1.189.524 2.637.524 2.637l.019.244c-.043.999-.017 2.034.06 2.97.103 1.239.295 2.303.541 2.873l.167-.091c-.361-1.122-.508-2.593-.444-4.289.097-2.593.694-5.719 1.796-8.978 1.863-4.919 4.447-8.866 6.811-10.751-2.155 1.947-5.073 8.248-5.946 10.581-.978 2.613-1.671 5.065-2.088 7.414.721-2.202 3.05-3.149 3.05-3.149s1.143-1.409 2.478-3.422c-.8.182-2.113.495-2.553.68-.649.272-.824.365-.824.365s2.102-1.28 3.905-1.86c2.48-3.906 5.182-9.456 2.461-11.884z"/></svg>

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

View File

@@ -30,6 +30,10 @@ export default class {
return ipcRenderer.invoke('get-variables', uid); return ipcRenderer.invoke('get-variables', uid);
} }
static getEngines (uid) {
return ipcRenderer.invoke('get-engines', uid);
}
static useSchema (params) { static useSchema (params) {
return ipcRenderer.invoke('use-schema', params); return ipcRenderer.invoke('use-schema', params);
} }

View File

@@ -10,6 +10,10 @@ export default class {
return ipcRenderer.invoke('get-table-data', params); return ipcRenderer.invoke('get-table-data', params);
} }
static getTableIndexes (params) {
return ipcRenderer.invoke('get-table-indexes', params);
}
static getKeyUsage (params) { static getKeyUsage (params) {
return ipcRenderer.invoke('get-key-usage', params); return ipcRenderer.invoke('get-key-usage', params);
} }
@@ -30,7 +34,19 @@ export default class {
return ipcRenderer.invoke('get-foreign-list', params); return ipcRenderer.invoke('get-foreign-list', params);
} }
static createTable (params) {
return ipcRenderer.invoke('create-table', params);
}
static alterTable (params) { static alterTable (params) {
return ipcRenderer.invoke('alter-table', params); return ipcRenderer.invoke('alter-table', params);
} }
static truncateTable (params) {
return ipcRenderer.invoke('truncate-table', params);
}
static dropTable (params) {
return ipcRenderer.invoke('drop-table', params);
}
} }

View File

@@ -4,15 +4,22 @@
line-height: 1.5; line-height: 1.5;
margin-right: 0.2rem; margin-right: 0.2rem;
&.key-pri { &.key-pri,
&.key-PRIMARY {
color: goldenrod; color: goldenrod;
} }
&.key-uni { &.key-uni,
&.key-UNIQUE {
color: deepskyblue; color: deepskyblue;
} }
&.key-mul { &.key-mul,
&.key-INDEX {
color: palegreen; color: palegreen;
} }
&.key-FULLTEXT {
color: mediumvioletred;
}
} }

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,
@@ -191,6 +197,11 @@ body {
background-color: $bg-color-gray; background-color: $bg-color-gray;
} }
.form-input.is-error,
.form-select.is-error {
background-color: $bg-color-gray;
}
.form-input:not(:placeholder-shown):invalid:focus { .form-input:not(:placeholder-shown):invalid:focus {
background: $bg-color-gray; background: $bg-color-gray;
} }
@@ -223,3 +234,7 @@ body {
visibility: hidden; visibility: hidden;
} }
} }
.empty {
color: $body-font-color;
}

View File

@@ -3,14 +3,17 @@ import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database'; import Database from '@/ipc-api/Database';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
const tabIndex = []; const tabIndex = [];
let lastSchema = ''; let lastBreadcrumbs = {};
export default { export default {
namespaced: true, namespaced: true,
strict: true, strict: true,
state: { state: {
workspaces: [], workspaces: [],
selected_workspace: null selected_workspace: null,
has_unsaved_changes: false,
is_unsaved_discard_modal: false,
pending_breadcrumbs: {}
}, },
getters: { getters: {
getSelected: state => { getSelected: state => {
@@ -35,18 +38,22 @@ export default {
return state.workspaces return state.workspaces
.filter(workspace => workspace.connected) .filter(workspace => workspace.connected)
.map(workspace => workspace.uid); .map(workspace => workspace.uid);
},
isUnsavedDiscardModal: state => {
return state.is_unsaved_discard_modal;
} }
}, },
mutations: { mutations: {
SELECT_WORKSPACE (state, uid) { SELECT_WORKSPACE (state, uid) {
state.selected_workspace = uid; state.selected_workspace = uid;
}, },
ADD_CONNECTED (state, { uid, client, dataTypes, structure }) { ADD_CONNECTED (state, { uid, client, dataTypes, indexTypes, structure }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? { ? {
...workspace, ...workspace,
client, client,
dataTypes, dataTypes,
indexTypes,
structure, structure,
connected: true connected: true
} }
@@ -85,6 +92,14 @@ export default {
} }
: workspace); : workspace);
}, },
REFRESH_ENGINES (state, { uid, engines }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
engines
}
: workspace);
},
ADD_WORKSPACE (state, workspace) { ADD_WORKSPACE (state, workspace) {
state.workspaces.push(workspace); state.workspaces.push(workspace);
}, },
@@ -96,11 +111,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',
@@ -166,6 +180,15 @@ export default {
else else
return workspace; return workspace;
}); });
},
SET_UNSAVED_CHANGES (state, val) {
state.has_unsaved_changes = !!val;
},
SET_UNSAVED_DISCARD_MODAL (state, val) {
state.is_unsaved_discard_modal = !!val;
},
SET_PENDING_BREADCRUMBS (state, payload) {
state.pending_breadcrumbs = payload;
} }
}, },
actions: { actions: {
@@ -179,21 +202,25 @@ export default {
dispatch('notifications/addNotification', { status, message: response }, { root: true }); dispatch('notifications/addNotification', { status, message: response }, { root: true });
else { else {
let dataTypes = []; let dataTypes = [];
let indexTypes = [];
switch (connection.client) { switch (connection.client) {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
dataTypes = require('common/data-types/mysql'); dataTypes = require('common/data-types/mysql');
indexTypes = require('common/index-types/mysql');
break; break;
} }
commit('ADD_CONNECTED', { commit('ADD_CONNECTED', {
uid: connection.uid, uid: connection.uid,
client: connection.client, client: connection.client,
dataTypes, dataTypes,
indexTypes,
structure: response structure: response
}); });
dispatch('refreshCollations', connection.uid); dispatch('refreshCollations', connection.uid);
dispatch('refreshVariables', connection.uid); dispatch('refreshVariables', connection.uid);
dispatch('refreshEngines', connection.uid);
} }
} }
catch (err) { catch (err) {
@@ -236,6 +263,18 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
} }
}, },
async refreshEngines ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getEngines(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_ENGINES', { uid, engines: 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);
@@ -268,17 +307,40 @@ export default {
if (getters.getWorkspace(uid).tabs.length < 3) if (getters.getWorkspace(uid).tabs.length < 3)
dispatch('newTab', uid); dispatch('newTab', uid);
dispatch('setUnsavedChanges', false);
}, },
changeBreadcrumbs ({ commit, getters }, payload) { changeBreadcrumbs ({ state, commit, getters }, payload) {
if (lastSchema !== payload.schema) { if (state.has_unsaved_changes) {
Database.useSchema({ uid: getters.getSelected, schema: payload.schema }); commit('SET_UNSAVED_DISCARD_MODAL', true);
lastSchema = payload.schema; commit('SET_PENDING_BREADCRUMBS', payload);
return;
} }
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: payload }); const breadcrumbsObj = {
schema: null,
table: null,
trigger: null,
procedure: null,
scheduler: null
};
const hasLastChildren = Object.keys(lastBreadcrumbs).filter(b => b !== 'schema').some(b => lastBreadcrumbs[b]);
const hasChildren = Object.keys(payload).filter(b => b !== 'schema').some(b => payload[b]);
if (lastBreadcrumbs.schema === payload.schema && hasLastChildren && !hasChildren) return;
if (lastBreadcrumbs.schema !== payload.schema)
Database.useSchema({ uid: getters.getSelected, schema: payload.schema });
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...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);
@@ -291,6 +353,18 @@ export default {
}, },
setTabKeyUsage ({ commit }, payload) { setTabKeyUsage ({ commit }, payload) {
commit('SET_TAB_KEY_USAGE', payload); commit('SET_TAB_KEY_USAGE', payload);
},
setUnsavedChanges ({ commit }, val) {
commit('SET_UNSAVED_CHANGES', val);
},
discardUnsavedChanges ({ state, commit, dispatch }) {
dispatch('setUnsavedChanges', false);
dispatch('changeBreadcrumbs', state.pending_breadcrumbs);
commit('SET_UNSAVED_DISCARD_MODAL', false);
commit('SET_PENDING_BREADCRUMBS', {});
},
closeUnsavedChangesModal ({ commit }) {
commit('SET_UNSAVED_DISCARD_MODAL', false);
} }
} }
}; };