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

Compare commits

...

22 Commits

Author SHA1 Message Date
3b4f1475df chore(release): 0.0.12 2020-12-24 10:58:03 +01:00
155154b43d feat: option to toggle editor auto completion 2020-12-24 10:40:22 +01:00
a95b8d188c feat: option to change editor theme 2020-12-23 18:07:50 +01:00
cb1fce6f99 feat: query editor auto-completer for tables and columns 2020-12-22 22:31:31 +01:00
0014f48079 refactor: migrated to ace from monaco-editor 2020-12-21 11:06:41 +01:00
fc35f271d7 feat: better security connections credentials storage 2020-12-18 18:44:32 +01:00
65ad0e954d ci: update travis configuration 2020-12-18 17:38:06 +01:00
8cafade8b1 chore(release): 0.0.11 2020-12-15 17:24:04 +01:00
d13b708377 chore: update REDME.md 2020-12-15 17:23:24 +01:00
206597e5b8 feat: foreign keys management 2020-12-15 17:08:36 +01:00
c5458159d1 fix: unable to switch tabs when no table selected 2020-12-11 18:22:07 +01:00
1476e899d1 feat: auto focus on first input in modals 2020-12-11 18:09:17 +01:00
797ab70e7c chore: update links 2020-12-11 16:05:32 +01:00
f81312aeb0 feat: query tabs auto focus 2020-12-11 15:55:18 +01:00
3ed5ea023e fix: some properties do not reset after fields changes 2020-12-11 12:57:24 +01:00
9291a7a7b4 fix: file field editor not show 2020-12-10 15:15:32 +01:00
5cfdc9b92d fix: wrong field type detection 2020-12-09 18:22:46 +01:00
15b08d7ea8 fix: data tab sort not maintained at refresh 2020-12-08 18:41:08 +01:00
acebe435ff fix: improved changes dedection in props tab 2020-12-07 19:11:29 +01:00
5712b80022 feat: improved data table sorts 2020-12-07 17:51:48 +01:00
d38583262e fix: deletion of rows with non-numeric ID 2020-12-07 15:07:59 +01:00
e0e2131981 chore: update README.md 2020-12-04 11:43:27 +01:00
49 changed files with 3602 additions and 6353 deletions

7
.gitattributes vendored
View File

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

View File

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

View File

@@ -1,9 +1,6 @@
language: node_js language: node_js
node_js: 12 node_js: 12
before_install:
- npm install
cache: cache:
directories: directories:
- node_modules - node_modules
@@ -20,6 +17,9 @@ env:
jobs: jobs:
include: include:
- stage: Test - stage: Test
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script: script:
- npm test - npm test
@@ -27,6 +27,9 @@ jobs:
if: tag IS present if: tag IS present
os: linux os: linux
services: docker services: docker
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script: script:
- docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always" - docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always"
before_cache: before_cache:
@@ -35,6 +38,8 @@ jobs:
- stage: Deploy Mac - stage: Deploy Mac
if: tag IS present if: tag IS present
os: osx os: osx
before_install:
- npm install
osx_image: xcode10.2 osx_image: xcode10.2
script: script:
- npm run build -- -p always - npm run build -- -p always

View File

@@ -2,6 +2,37 @@
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.12](https://github.com/Fabio286/antares/compare/v0.0.11...v0.0.12) (2020-12-24)
### Features
* better security connections credentials storage ([fc35f27](https://github.com/Fabio286/antares/commit/fc35f271d7fe384cd786ce33547c0ef17135ddd8))
* option to change editor theme ([a95b8d1](https://github.com/Fabio286/antares/commit/a95b8d188cfcc8f563ad73b4f0b676d068775d36))
* option to toggle editor auto completion ([155154b](https://github.com/Fabio286/antares/commit/155154b43d0cd02ae875ded3ce865a37a999da5c))
* query editor auto-completer for tables and columns ([cb1fce6](https://github.com/Fabio286/antares/commit/cb1fce6f998ea7332886820910e245ab19416a9d))
### [0.0.11](https://github.com/Fabio286/antares/compare/v0.0.10...v0.0.11) (2020-12-15)
### Features
* auto focus on first input in modals ([1476e89](https://github.com/Fabio286/antares/commit/1476e899d164562f12342ced8c76903b9bdcfa55))
* foreign keys management ([206597e](https://github.com/Fabio286/antares/commit/206597e5b891e13e6f7635075bd11599355ab778))
* improved data table sorts ([5712b80](https://github.com/Fabio286/antares/commit/5712b8002203b32027f0e820f98a61e2ec965e79))
* query tabs auto focus ([f81312a](https://github.com/Fabio286/antares/commit/f81312aeb02ef55affd2ae9e81a9b4cb4c2e6da2))
### Bug Fixes
* data tab sort not maintained at refresh ([15b08d7](https://github.com/Fabio286/antares/commit/15b08d7ea858cb28111c7b548af9a13b1bf0da91))
* deletion of rows with non-numeric ID ([d385832](https://github.com/Fabio286/antares/commit/d38583262e672a2b47c5ad0aca0f13c129830a7b))
* file field editor not show ([9291a7a](https://github.com/Fabio286/antares/commit/9291a7a7b41e7aeb9b65c7f32e496f523e482272))
* improved changes dedection in props tab ([acebe43](https://github.com/Fabio286/antares/commit/acebe435ff6fa1581692fbf7ee1bc23b334e3947))
* some properties do not reset after fields changes ([3ed5ea0](https://github.com/Fabio286/antares/commit/3ed5ea023e1852d724b2b59ab156f8722876f85b))
* unable to switch tabs when no table selected ([c545815](https://github.com/Fabio286/antares/commit/c5458159d1e30cecf6615086c074d98b4b599637))
* wrong field type detection ([5cfdc9b](https://github.com/Fabio286/antares/commit/5cfdc9b92d4b778a7863b02fd64e6445236c89bc))
### [0.0.10](https://github.com/Fabio286/antares/compare/v0.0.9...v0.0.10) (2020-12-04) ### [0.0.10](https://github.com/Fabio286/antares/compare/v0.0.9...v0.0.10) (2020-12-04)

View File

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

5
jsconfig.json Normal file
View File

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

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.0.10", "version": "0.0.12",
"description": "A cross-platform easy to use SQL client.", "description": "A cross-platform easy to use SQL client.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/Fabio286/antares.git", "repository": "https://github.com/Fabio286/antares.git",
@@ -48,11 +48,13 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^5.8.55", "@mdi/font": "^5.8.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.0", "electron-log": "^4.3.0",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5", "electron-updater": "^4.3.5",
"keytar": "^7.3.0",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"moment": "^2.29.1", "moment": "^2.29.1",
"monaco-editor": "^0.20.0",
"mssql": "^6.2.3", "mssql": "^6.2.3",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"pg": "^8.5.1", "pg": "^8.5.1",
@@ -78,7 +80,6 @@
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-vue": "^7.1.0", "eslint-plugin-vue": "^7.1.0",
"monaco-editor-webpack-plugin": "^1.9.1",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"sass-loader": "^10.1.0", "sass-loader": "^10.1.0",
"standard-version": "^9.0.0", "standard-version": "^9.0.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
@@ -237,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
}; };
}); });
} }
@@ -346,6 +356,7 @@ export class MySQLClient extends AntaresCore {
deletions, deletions,
changes, changes,
indexChanges, indexChanges,
foreignChanges,
options options
} = params; } = params;
@@ -390,6 +401,11 @@ export class MySQLClient extends AntaresCore {
} }
}); });
// ADD FOREIGN KEYS
foreignChanges.additions.forEach(addition => {
alterColumns.push(`ADD CONSTRAINT \`${addition.constraintName}\` FOREIGN KEY (\`${addition.field}\`) REFERENCES \`${addition.refTable}\` (\`${addition.refField}\`) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
});
// CHANGE FIELDS // CHANGE FIELDS
changes.forEach(change => { changes.forEach(change => {
const length = change.numLength || change.charLength || change.datePrecision; const length = change.numLength || change.charLength || change.datePrecision;
@@ -427,6 +443,12 @@ export class MySQLClient extends AntaresCore {
} }
}); });
// CHANGE FOREIGN KEYS
foreignChanges.changes.forEach(change => {
alterColumns.push(`DROP FOREIGN KEY \`${change.oldName}\``);
alterColumns.push(`ADD CONSTRAINT \`${change.constraintName}\` FOREIGN KEY (\`${change.field}\`) REFERENCES \`${change.refTable}\` (\`${change.refField}\`) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
});
// DROP FIELDS // DROP FIELDS
deletions.forEach(deletion => { deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN \`${deletion.name}\``); alterColumns.push(`DROP COLUMN \`${deletion.name}\``);
@@ -440,6 +462,11 @@ export class MySQLClient extends AntaresCore {
alterColumns.push(`DROP INDEX \`${deletion.name}\``); alterColumns.push(`DROP INDEX \`${deletion.name}\``);
}); });
// DROP FOREIGN KEYS
foreignChanges.deletions.forEach(deletion => {
alterColumns.push(`DROP FOREIGN KEY \`${deletion.constraintName}\``);
});
sql += alterColumns.join(', '); sql += alterColumns.join(', ');
// RENAME // RENAME

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -123,7 +123,7 @@ export default {
return this.selectedWorkspace === this.connection.uid; return this.selectedWorkspace === this.connection.uid;
}, },
selectedTab () { selectedTab () {
if (this.workspace.breadcrumbs.table === null) if (this.workspace.breadcrumbs.table === null && ['data', 'prop'].includes(this.workspace.selected_tab))
return this.queryTabs[0].uid; return this.queryTabs[0].uid;
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) || return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) ||
@@ -193,6 +193,7 @@ export default {
align-items: flex-start; align-items: flex-start;
flex-wrap: nowrap; flex-wrap: nowrap;
overflow: auto; overflow: auto;
margin-bottom: 0;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 2px; width: 2px;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,13 @@ module.exports = {
total: 'Total', total: 'Total',
table: 'Table', table: 'Table',
discard: 'Discard', discard: 'Discard',
stay: 'Stay' stay: 'Stay',
author: 'Author',
light: 'Light',
dark: 'Dark',
autoCompletion: 'Auto Completion',
application: 'Application',
editor: 'Editor'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -115,7 +121,17 @@ module.exports = {
deleteTable: 'Delete table', deleteTable: 'Delete table',
emptyCorfirm: 'Do you confirm to empty', emptyCorfirm: 'Do you confirm to empty',
unsavedChanges: 'Unsaved changes', unsavedChanges: 'Unsaved changes',
discardUnsavedChanges: 'You have some unsaved changes. By leaving this tab these changes will be discarded.' discardUnsavedChanges: 'You have some unsaved changes. By leaving this tab these changes will be discarded.',
thereAreNoIndexes: 'There are no indexes',
thereAreNoForeign: 'There are no foreign keys',
createNewForeign: 'Create new foreign key',
referenceTable: 'Ref. table',
referenceField: 'Ref. field',
foreignFields: 'Foreign fields',
invalidDefault: 'Invalid default',
onDelete: 'On delete',
applicationTheme: 'Application Theme',
editorTheme: 'Editor Theme'
}, },
// Date and Time // Date and Time
short: { short: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -180,6 +180,12 @@ body {
.form-select { .form-select {
cursor: pointer; cursor: pointer;
&.small-select {
height: 1rem;
font-size: 0.7rem;
padding: 1px 0.4rem 0;
}
} }
.form-select, .form-select,
@@ -228,3 +234,7 @@ body {
visibility: hidden; visibility: hidden;
} }
} }
.empty {
color: $body-font-color;
}

View File

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

View File

@@ -1,29 +1,47 @@
'use strict'; 'use strict';
import i18n from '@/i18n'; import i18n from '@/i18n';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
export default { export default {
namespaced: true, namespaced: true,
strict: true, strict: true,
state: { state: {
locale: 'en-US', locale: persistentStore.get('locale') || 'en-US',
explorebar_size: null, explorebar_size: persistentStore.get('explorebar_size') || null,
notifications_timeout: 5 notifications_timeout: persistentStore.get('notifications_timeout') || 5,
auto_complete: persistentStore.get('auto_complete') || true,
application_theme: persistentStore.get('application_theme') || 'dark',
editor_theme: persistentStore.get('editor_theme') || 'twilight'
}, },
getters: { getters: {
getLocale: state => state.locale, getLocale: state => state.locale,
getExplorebarSize: state => state.explorebar_size, getExplorebarSize: state => state.explorebar_size,
getNotificationsTimeout: state => state.notifications_timeout getNotificationsTimeout: state => state.notifications_timeout,
getAutoComplete: state => state.auto_complete,
getApplicationTheme: state => state.application_theme,
getEditorTheme: state => state.editor_theme
}, },
mutations: { mutations: {
SET_LOCALE (state, locale) { SET_LOCALE (state, locale) {
state.locale = locale; state.locale = locale;
i18n.locale = locale; i18n.locale = locale;
persistentStore.set('locale', state.locale);
}, },
SET_NOTIFICATIONS_TIMEOUT (state, timeout) { SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
state.notifications_timeout = timeout; state.notifications_timeout = timeout;
persistentStore.set('notifications_timeout', state.notifications_timeout);
},
SET_AUTO_COMPLETE (state, val) {
state.auto_complete = val;
persistentStore.set('auto_complete', state.auto_complete);
}, },
SET_EXPLOREBAR_SIZE (state, size) { SET_EXPLOREBAR_SIZE (state, size) {
state.explorebar_size = size; state.explorebar_size = size;
persistentStore.set('explorebar_size', state.explorebar_size);
},
SET_EDITOR_THEME (state, theme) {
state.editor_theme = theme;
} }
}, },
actions: { actions: {
@@ -35,6 +53,12 @@ export default {
}, },
changeExplorebarSize ({ commit }, size) { changeExplorebarSize ({ commit }, size) {
commit('SET_EXPLOREBAR_SIZE', size); commit('SET_EXPLOREBAR_SIZE', size);
},
changeAutoComplete ({ commit }, val) {
commit('SET_AUTO_COMPLETE', val);
},
changeEditorTheme ({ commit }, theme) {
commit('SET_EDITOR_THEME', theme);
} }
} }
}; };

View File

@@ -111,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',
@@ -338,7 +337,10 @@ export default {
lastBreadcrumbs = { ...breadcrumbsObj, ...payload }; lastBreadcrumbs = { ...breadcrumbsObj, ...payload };
}, },
newTab ({ commit }, uid) { newTab ({ commit }, uid) {
commit('NEW_TAB', uid); const tab = uidGen('T');
commit('NEW_TAB', { uid, tab });
commit('SELECT_TAB', { uid, tab });
}, },
removeTab ({ commit }, payload) { removeTab ({ commit }, payload) {
commit('REMOVE_TAB', payload); commit('REMOVE_TAB', payload);

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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