Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
020ce36312 | |||
b4545b178f | |||
d9a3eab015 | |||
2ab49c218d | |||
|
8f9385d508 | ||
0c002918eb | |||
3d56ec7b49 | |||
48c3e6afc4 | |||
|
50a5c8fe1e | ||
b0d1f115c9 | |||
a59f77f618 | |||
e7a1858091 | |||
648a51efe8 | |||
38962c4807 | |||
|
43e4cdd4cf | ||
|
91046c1ac1 | ||
|
63f8b9b6a1 | ||
|
ed476d9b5c | ||
|
f41d8c0480 | ||
e3b54a8be1 | |||
|
c2c0394624 | ||
8a6f5eac59 | |||
a5fdcc1a85 | |||
1df21da47c | |||
8da0224876 | |||
359e14a9eb | |||
813aa320d9 | |||
aaa5549609 | |||
35cb7e1dc4 | |||
992a033cb2 | |||
5267b37eaf | |||
8fe30d8b6d | |||
37ccb6b00d | |||
e8af2d24a8 | |||
d7f1aa97af | |||
c46224635a |
@@ -120,6 +120,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wenj91",
|
||||
"name": "文杰",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12549338?v=4",
|
||||
"profile": "https://github.com/wenj91",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
8
.github/workflows/build-linux.yml
vendored
@@ -12,12 +12,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Build/release Electron app
|
||||
uses: samuelmeuli/action-electron-builder@v1
|
||||
|
8
.github/workflows/build-mac.yml
vendored
@@ -12,12 +12,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Build/release Electron app
|
||||
uses: samuelmeuli/action-electron-builder@v1
|
||||
|
8
.github/workflows/build-win.yml
vendored
@@ -12,13 +12,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Build/release Electron app
|
||||
uses: samuelmeuli/action-electron-builder@v1
|
||||
with:
|
||||
|
46
CHANGELOG.md
@@ -2,7 +2,51 @@
|
||||
|
||||
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.3.10](https://github.com/Fabio286/antares/compare/v0.3.9...v0.3.10) (2021-11-24)
|
||||
### [0.4.2](https://github.com/Fabio286/antares/compare/v0.4.1...v0.4.2) (2022-01-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **MySQL:** ability to cancel queries ([a59f77f](https://github.com/Fabio286/antares/commit/a59f77f618aea6156fc80fb832d3efcb9848411f))
|
||||
* **PostgreSQL:** ability to cancel queries ([0c00291](https://github.com/Fabio286/antares/commit/0c002918eb0226f6b3f21ed62117495f86396fb1))
|
||||
* save window state ([8f9385d](https://github.com/Fabio286/antares/commit/8f9385d50815635d091758ecd5d00884e3297ca0))
|
||||
* **UI:** textarea autofocus selecting a query tab, closes [#166](https://github.com/Fabio286/antares/issues/166) ([b4545b1](https://github.com/Fabio286/antares/commit/b4545b178f795712c781a3f4fc35eec31b5ad902))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **SQLite:** exception with some fields ([e7a1858](https://github.com/Fabio286/antares/commit/e7a18580915e7739bfa97948c6a0c4fc90a7e78a))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* hash for foreign key default names ([48c3e6a](https://github.com/Fabio286/antares/commit/48c3e6afc43c51f70a16703f1a71194f43da7a3e))
|
||||
* **MySQL:** support to ANSI_QUOTES sql_mode, closes [#158](https://github.com/Fabio286/antares/issues/158) ([d9a3eab](https://github.com/Fabio286/antares/commit/d9a3eab015302e9f23112f659658073ab3242191))
|
||||
|
||||
### [0.4.1](https://github.com/Fabio286/antares/compare/v0.4.0...v0.4.1) (2021-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* language format detection for text fields ([a5fdcc1](https://github.com/Fabio286/antares/commit/a5fdcc1a85aa188ff1b9a15b1a768aced026f360))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cell disappear on edit in one column tables ([aaa5549](https://github.com/Fabio286/antares/commit/aaa5549609664665bd4513632d621cb249b379c1))
|
||||
* false positive with Windows Defender ([992a033](https://github.com/Fabio286/antares/commit/992a033cb2bede3d1eb52e19482d810f6692de1e))
|
||||
* **MySQL:** wrong datetime fields default in table filler in some cases ([8da0224](https://github.com/Fabio286/antares/commit/8da022487650039b7f34a9c86a7bd9045eba65e2))
|
||||
* **MySQL:** wrong value for fields "on update" in some conditions ([359e14a](https://github.com/Fabio286/antares/commit/359e14a9ebd48f86069ba7762fe00a7056f52d47))
|
||||
* select all rows with ctrl+a when editing a cell ([35cb7e1](https://github.com/Fabio286/antares/commit/35cb7e1dc48d3a74e9d106cb1a37f454c1b4a4d1))
|
||||
* **SQLite:** update rows with a text primary key ([d7f1aa9](https://github.com/Fabio286/antares/commit/d7f1aa97af32a4c51fc7022498bd47e15fa08430))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* **UI:** avoid columns size change when editing cells or scrolling results ([813aa32](https://github.com/Fabio286/antares/commit/813aa320d9ab799efea38a7110b7c0bdf7549123))
|
||||
* **UI:** disable save button in table creation when no fields are added ([e8af2d2](https://github.com/Fabio286/antares/commit/e8af2d24a869f7667c069936648808952d2062ab))
|
||||
|
||||
## [0.4.0](https://github.com/Fabio286/antares/compare/v0.3.9...v0.4.0) (2021-11-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
@@ -130,6 +130,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="http://rsacchetto.nexxontech.it/"><img src="https://avatars.githubusercontent.com/u/18429412?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Riccardo Sacchetto</b></sub></a><br /><a href="#platform-Occhioverde" title="Packaging/porting to new platform">📦</a></td>
|
||||
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=kilianstallz" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=wenj91" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.4 KiB |
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "antares",
|
||||
"productName": "Antares",
|
||||
"version": "0.3.10",
|
||||
"version": "0.4.2",
|
||||
"description": "A modern, fast and productivity driven SQL client with a focus in UX.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Fabio286/antares.git",
|
||||
@@ -18,7 +18,7 @@
|
||||
"release": "standard-version",
|
||||
"release:pre": "npm run release -- --prerelease alpha",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"test": "npm run lint",
|
||||
"test": "npm run compile && node tests/app.spec.js",
|
||||
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
|
||||
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
|
||||
"contributors:add": "all-contributors add",
|
||||
@@ -82,6 +82,7 @@
|
||||
"appx": {
|
||||
"displayName": "Antares SQL",
|
||||
"backgroundColor": "transparent",
|
||||
"showNameOnTiles": true,
|
||||
"identityName": "62514FabioDiStasio.AntaresSQLClient",
|
||||
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
|
||||
"applicationId": "FabioDiStasio.AntaresSQLClient"
|
||||
@@ -104,11 +105,13 @@
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.1",
|
||||
"@mdi/font": "^6.1.95",
|
||||
"@vscode/vscode-languagedetection": "^1.0.21",
|
||||
"ace-builds": "^1.4.13",
|
||||
"better-sqlite3": "^7.4.4",
|
||||
"electron-log": "^4.4.1",
|
||||
"electron-store": "^8.0.1",
|
||||
"electron-updater": "^4.3.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"faker": "^5.5.3",
|
||||
"marked": "^4.0.0",
|
||||
"moment": "^2.29.1",
|
||||
@@ -134,18 +137,19 @@
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^6.5.0",
|
||||
"electron": "^16.0.1",
|
||||
"electron-builder": "^22.13.1",
|
||||
"electron-builder": "^22.14.11",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"mini-css-extract-plugin": "^2.4.3",
|
||||
"node-loader": "^2.0.0",
|
||||
"playwright": "^1.16.3",
|
||||
"progress-webpack-plugin": "^1.0.12",
|
||||
"sass": "^1.42.1",
|
||||
"sass-loader": "^12.3.0",
|
||||
|
@@ -11,6 +11,7 @@ module.exports = {
|
||||
sslConnection: false,
|
||||
sshConnection: false,
|
||||
fileConnection: false,
|
||||
cancelQueries: false,
|
||||
// Tools
|
||||
processesList: false,
|
||||
usersManagement: false,
|
||||
|
@@ -12,6 +12,7 @@ module.exports = {
|
||||
engines: true,
|
||||
sslConnection: true,
|
||||
sshConnection: true,
|
||||
cancelQueries: true,
|
||||
// Tools
|
||||
processesList: true,
|
||||
// Structure
|
||||
|
@@ -10,6 +10,7 @@ module.exports = {
|
||||
database: true,
|
||||
sslConnection: true,
|
||||
sshConnection: true,
|
||||
cancelQueries: true,
|
||||
// Tools
|
||||
processesList: true,
|
||||
// Structure
|
||||
|
@@ -7,8 +7,8 @@ module.exports = {
|
||||
views: true,
|
||||
triggers: true,
|
||||
// Settings
|
||||
elementsWrapper: '',
|
||||
stringsWrapper: '"',
|
||||
elementsWrapper: '"',
|
||||
stringsWrapper: '\'',
|
||||
tableAdd: true,
|
||||
viewAdd: true,
|
||||
triggerAdd: true,
|
||||
|
@@ -52,7 +52,7 @@ export default connections => {
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err };
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -135,7 +135,7 @@ export default connections => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('raw-query', async (event, { uid, query, schema }) => {
|
||||
ipcMain.handle('raw-query', async (event, { uid, query, schema, tabUid }) => {
|
||||
if (!query) return;
|
||||
|
||||
try {
|
||||
@@ -143,6 +143,7 @@ export default connections => {
|
||||
nest: true,
|
||||
details: true,
|
||||
schema,
|
||||
tabUid,
|
||||
comments: false
|
||||
});
|
||||
|
||||
@@ -152,4 +153,16 @@ export default connections => {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('kill-tab-query', async (event, { uid, tabUid }) => {
|
||||
if (!tabUid) return;
|
||||
|
||||
try {
|
||||
await connections[uid].killTabQuery(tabUid);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -3,6 +3,7 @@ import faker from 'faker';
|
||||
import moment from 'moment';
|
||||
import { sqlEscaper } from 'common/libs/sqlEscaper';
|
||||
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
|
||||
import * as customizations from 'common/customizations';
|
||||
import fs from 'fs';
|
||||
|
||||
export default (connections) => {
|
||||
@@ -85,11 +86,12 @@ export default (connections) => {
|
||||
|
||||
ipcMain.handle('update-table-cell', async (event, params) => {
|
||||
delete params.row._antares_id;
|
||||
const { stringsWrapper: sw } = customizations[connections[params.uid]._client];
|
||||
|
||||
try { // TODO: move to client classes
|
||||
let escapedParam;
|
||||
let reload = false;
|
||||
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
|
||||
const id = typeof params.id === 'number' ? params.id : `${sw}${params.id}${sw}`;
|
||||
|
||||
if ([...NUMBER, ...FLOAT].includes(params.type))
|
||||
escapedParam = params.content;
|
||||
|
@@ -9,6 +9,7 @@ export class MySQLClient extends AntaresCore {
|
||||
super(args);
|
||||
|
||||
this._schema = null;
|
||||
this._runningConnections = new Map();
|
||||
|
||||
this.types = {
|
||||
0: 'DECIMAL',
|
||||
@@ -136,8 +137,16 @@ export class MySQLClient extends AntaresCore {
|
||||
if (!this._poolSize) {
|
||||
this._connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
// ANSI_QUOTES check
|
||||
const res = await this.getVariable('sql_mode', 'global');
|
||||
const sqlMode = res?.value.split(',');
|
||||
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
|
||||
|
||||
if (this._params.readonly)
|
||||
await this.raw('SET SESSION TRANSACTION READ ONLY');
|
||||
|
||||
if (hasAnsiQuotes)
|
||||
await this.raw(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
|
||||
}
|
||||
else {
|
||||
this._connection = mysql.createPool({
|
||||
@@ -151,11 +160,21 @@ export class MySQLClient extends AntaresCore {
|
||||
}
|
||||
});
|
||||
|
||||
if (this._params.readonly) {
|
||||
this._connection.on('connection', connection => {
|
||||
// ANSI_QUOTES check
|
||||
const res = await this.getVariable('sql_mode', 'global');
|
||||
const sqlMode = res?.value.split(',');
|
||||
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
|
||||
|
||||
if (hasAnsiQuotes)
|
||||
await this._connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
|
||||
|
||||
this._connection.on('connection', connection => {
|
||||
if (this._params.readonly)
|
||||
connection.query('SET SESSION TRANSACTION READ ONLY');
|
||||
});
|
||||
}
|
||||
|
||||
if (hasAnsiQuotes)
|
||||
connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +443,7 @@ export class MySQLClient extends AntaresCore {
|
||||
return {
|
||||
name: field.COLUMN_NAME,
|
||||
key: field.COLUMN_KEY.toLowerCase(),
|
||||
type: remappedFields ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
|
||||
type: (remappedFields && remappedFields[field.COLUMN_NAME]) ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
|
||||
schema: field.TABLE_SCHEMA,
|
||||
table: field.TABLE_NAME,
|
||||
numPrecision: field.NUMERIC_PRECISION,
|
||||
@@ -436,11 +455,13 @@ export class MySQLClient extends AntaresCore {
|
||||
unsigned: field.COLUMN_TYPE.includes('unsigned'),
|
||||
zerofill: field.COLUMN_TYPE.includes('zerofill'),
|
||||
order: field.ORDINAL_POSITION,
|
||||
default: remappedFields ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT,
|
||||
default: (remappedFields && remappedFields[field.COLUMN_NAME]) ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT,
|
||||
charset: field.CHARACTER_SET_NAME,
|
||||
collation: field.COLLATION_NAME,
|
||||
autoIncrement: field.EXTRA.includes('auto_increment'),
|
||||
onUpdate: field.EXTRA.toLowerCase().includes('on update') ? field.EXTRA.replace('on update', '') : '',
|
||||
onUpdate: field.EXTRA.toLowerCase().includes('on update')
|
||||
? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim()
|
||||
: '',
|
||||
comment: field.COLUMN_COMMENT
|
||||
};
|
||||
});
|
||||
@@ -1137,6 +1158,26 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW VARIABLES LIKE %variable%
|
||||
*
|
||||
* @param {String} variable
|
||||
* @param {'global'|'session'|null} level
|
||||
* @returns {Object} variable
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getVariable (variable, level) {
|
||||
const sql = `SHOW${level ? ' ' + level.toUpperCase() : ''} VARIABLES LIKE '%${variable}%'`;
|
||||
const results = await this.raw(sql);
|
||||
|
||||
if (results.rows.length) {
|
||||
return {
|
||||
name: results.rows[0].Variable_name,
|
||||
value: results.rows[0].Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW ENGINES
|
||||
*
|
||||
@@ -1208,10 +1249,26 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async killProcess (id) {
|
||||
return await this.raw(`KILL ${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async killTabQuery (tabUid) {
|
||||
const id = this._runningConnections.get(tabUid);
|
||||
if (id)
|
||||
return await this.killProcess(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE
|
||||
*
|
||||
@@ -1533,6 +1590,9 @@ export class MySQLClient extends AntaresCore {
|
||||
const isPool = typeof this._connection.getConnection === 'function';
|
||||
const connection = isPool ? await this._connection.getConnection() : this._connection;
|
||||
|
||||
if (args.tabUid && isPool)
|
||||
this._runningConnections.set(args.tabUid, connection.connection.connectionId);
|
||||
|
||||
if (args.schema)
|
||||
await connection.query(`USE \`${args.schema}\``);
|
||||
|
||||
@@ -1593,7 +1653,10 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
if (isPool) connection.release();
|
||||
if (isPool) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
|
||||
@@ -1602,7 +1665,10 @@ export class MySQLClient extends AntaresCore {
|
||||
keysArr = keysArr ? [...keysArr, ...response] : response;
|
||||
}
|
||||
catch (err) {
|
||||
if (isPool) connection.release();
|
||||
if (isPool) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
@@ -1617,7 +1683,10 @@ export class MySQLClient extends AntaresCore {
|
||||
keys: keysArr
|
||||
});
|
||||
}).catch((err) => {
|
||||
if (isPool) connection.release();
|
||||
if (isPool) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
@@ -1625,7 +1694,10 @@ export class MySQLClient extends AntaresCore {
|
||||
resultsArr.push({ rows, report, fields, keys, duration });
|
||||
}
|
||||
|
||||
if (isPool) connection.release();
|
||||
if (isPool) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
|
||||
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
super(args);
|
||||
|
||||
this._schema = null;
|
||||
this._runningConnections = new Map();
|
||||
|
||||
this.types = {};
|
||||
for (const key in types.builtins)
|
||||
@@ -1088,10 +1089,26 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async killProcess (id) {
|
||||
return await this.raw(`SELECT pg_terminate_backend(${id})`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async killTabQuery (tabUid) {
|
||||
const id = this._runningConnections.get(tabUid);
|
||||
if (id)
|
||||
return await this.raw(`SELECT pg_cancel_backend(${id})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE
|
||||
*
|
||||
@@ -1396,6 +1413,8 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async raw (sql, args) {
|
||||
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
|
||||
|
||||
args = {
|
||||
nest: false,
|
||||
details: false,
|
||||
@@ -1404,9 +1423,6 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
...args
|
||||
};
|
||||
|
||||
if (args.schema && args.schema !== 'public')
|
||||
await this.use(args.schema);
|
||||
|
||||
if (!args.comments)
|
||||
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
|
||||
|
||||
@@ -1418,7 +1434,14 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
.map(q => q.trim())
|
||||
: [sql];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
|
||||
const isPool = this._connection instanceof Pool;
|
||||
const connection = isPool ? await this._connection.connect() : this._connection;
|
||||
|
||||
if (args.tabUid && isPool)
|
||||
this._runningConnections.set(args.tabUid, connection.processID);
|
||||
|
||||
if (args.schema && args.schema !== 'public')
|
||||
await this.use(args.schema);
|
||||
|
||||
for (const query of queries) {
|
||||
if (!query) continue;
|
||||
@@ -1428,15 +1451,12 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
let keysArr = [];
|
||||
|
||||
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
|
||||
this._connection.query({
|
||||
rowMode: args.nest ? 'array' : null,
|
||||
text: query
|
||||
}, async (err, res) => {
|
||||
timeStop = new Date();
|
||||
(async () => {
|
||||
try {
|
||||
const res = await connection.query({ rowMode: args.nest ? 'array' : null, text: query });
|
||||
|
||||
timeStop = new Date();
|
||||
|
||||
if (err)
|
||||
reject(err);
|
||||
else {
|
||||
let ast;
|
||||
|
||||
try {
|
||||
@@ -1525,6 +1545,10 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
if (isPool) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
|
||||
@@ -1533,6 +1557,10 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
keysArr = keysArr ? [...keysArr, ...response] : response;
|
||||
}
|
||||
catch (err) {
|
||||
if (isPool) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
@@ -1547,12 +1575,24 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
keys: keysArr
|
||||
});
|
||||
}
|
||||
});
|
||||
catch (err) {
|
||||
if (isPool) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
resultsArr.push({ rows, report, fields, keys, duration });
|
||||
}
|
||||
|
||||
if (isPool) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
|
||||
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
import sqlite from 'better-sqlite3';
|
||||
import { AntaresCore } from '../AntaresCore';
|
||||
import dataTypes from 'common/data-types/mysql';
|
||||
import dataTypes from 'common/data-types/sqlite';
|
||||
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
|
||||
|
||||
export class SQLiteClient extends AntaresCore {
|
||||
@@ -732,7 +732,7 @@ export class SQLiteClient extends AntaresCore {
|
||||
|
||||
if ([...TIME, ...DATETIME].includes(parsedType)) {
|
||||
const firstNotNull = queryResult.find(res => res[field.name] !== null);
|
||||
if (firstNotNull[field.name].includes('.'))
|
||||
if (firstNotNull && firstNotNull[field.name].includes('.'))
|
||||
length = firstNotNull[field.name].split('.').pop().length;
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@
|
||||
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
|
||||
import * as path from 'path';
|
||||
import Store from 'electron-store';
|
||||
import * as windowStateKeeper from 'electron-window-state';
|
||||
import * as remoteMain from '@electron/remote/main';
|
||||
|
||||
import ipcHandlers from './ipc-handlers';
|
||||
@@ -18,12 +19,15 @@ process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
|
||||
|
||||
// global reference to mainWindow (necessary to prevent window from being garbage collected)
|
||||
let mainWindow;
|
||||
let mainWindowState;
|
||||
|
||||
async function createMainWindow () {
|
||||
const icon = require('../renderer/images/logo-32.png');
|
||||
const window = new BrowserWindow({
|
||||
width: 1024,
|
||||
height: 800,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
minWidth: 900,
|
||||
minHeight: 550,
|
||||
title: 'Antares SQL',
|
||||
@@ -41,6 +45,9 @@ async function createMainWindow () {
|
||||
backgroundColor: '#1d1d1d'
|
||||
});
|
||||
|
||||
mainWindowState.manage(window);
|
||||
window.on('moved', saveWindowState);
|
||||
|
||||
remoteMain.enable(window.webContents);
|
||||
|
||||
try {
|
||||
@@ -70,6 +77,7 @@ async function createMainWindow () {
|
||||
}
|
||||
|
||||
window.on('closed', () => {
|
||||
window.removeListener('moved', saveWindowState);
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
@@ -104,6 +112,11 @@ else {
|
||||
|
||||
// create main BrowserWindow when electron is ready
|
||||
app.on('ready', async () => {
|
||||
mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1024,
|
||||
defaultHeight: 800
|
||||
});
|
||||
|
||||
mainWindow = await createMainWindow();
|
||||
createAppMenu();
|
||||
|
||||
@@ -160,3 +173,7 @@ function createAppMenu () {
|
||||
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
function saveWindowState () {
|
||||
mainWindowState.saveState(mainWindow);
|
||||
}
|
||||
|
@@ -6,13 +6,13 @@
|
||||
@confirm="runRoutine"
|
||||
@hide="closeModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-play mr-1" />
|
||||
<span class="cut-text">{{ $t('word.parameters') }}: {{ localRoutine.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<template #body>
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<div
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
@@ -5,16 +5,16 @@
|
||||
@confirm="$emit('confirm')"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ $t('message.unsavedChanges') }}
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<template #body>
|
||||
<div>
|
||||
{{ $t('message.discardUnsavedChanges') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
@@ -41,7 +41,7 @@
|
||||
<label class="form-checkbox ml-3" :title="$t('word.insert')">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!field.autoIncrement"
|
||||
:checked="!fieldsToExclude.includes(field.name)"
|
||||
@change.prevent="toggleFields($event, field)"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
@@ -264,7 +264,7 @@ export default {
|
||||
else if (BIT.includes(field.type))
|
||||
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
|
||||
else if (DATETIME.includes(field.type)) {
|
||||
if (field.default && ['current_timestamp', 'now()'].includes(field.default.toLowerCase())) {
|
||||
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
|
||||
let datePrecision = '';
|
||||
for (let i = 0; i < field.datePrecision; i++)
|
||||
datePrecision += i === 0 ? '.S' : 'S';
|
||||
@@ -281,7 +281,7 @@ export default {
|
||||
|
||||
rowObj[field.name] = { value: fieldDefault };
|
||||
|
||||
if (field.autoIncrement)// Disable by default auto increment fields
|
||||
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
|
||||
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
|
||||
}
|
||||
|
||||
|
@@ -22,12 +22,12 @@
|
||||
:hide-footer="true"
|
||||
@hide="hideInfoModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div>
|
||||
<div>
|
||||
<TextEditor
|
||||
@@ -38,7 +38,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -15,16 +15,16 @@
|
||||
@confirm="confirmDeleteConnection"
|
||||
@hide="hideConfirmModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ $t('message.deleteConnection') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
{{ $t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
@@ -6,12 +6,12 @@
|
||||
:hide-footer="true"
|
||||
@hide="hideScratchpad"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ $t('word.scratchpad') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div>
|
||||
<div>
|
||||
<TextEditor
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<small class="text-gray">{{ $t('message.markdownSupported') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
@@ -200,7 +200,7 @@ export default {
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
height: 0;
|
||||
width: 3px;
|
||||
transition: height 0.2s;
|
||||
|
@@ -18,7 +18,6 @@
|
||||
<li
|
||||
v-for="(tab, i) of draggableTabs"
|
||||
:key="i"
|
||||
:ref="selectedTab === tab.uid ? 'tab-selected' : ''"
|
||||
class="tab-item tab-draggable"
|
||||
draggable="true"
|
||||
:class="{'active': selectedTab === tab.uid}"
|
||||
@@ -256,56 +255,59 @@
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.customizations.processesList"
|
||||
slot="header"
|
||||
class="tab-item dropdown tools-dropdown"
|
||||
>
|
||||
<a
|
||||
class="tab-link workspace-tools-link dropdown-toggle"
|
||||
tabindex="0"
|
||||
:title="$t('word.tools')"
|
||||
<template #header>
|
||||
<li
|
||||
v-if="workspace.customizations.processesList"
|
||||
class="tab-item dropdown tools-dropdown"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-tools" />
|
||||
</a>
|
||||
<ul v-if="hasTools" class="menu text-left text-uppercase">
|
||||
<li class="menu-item">
|
||||
<a class="c-hand p-vcentered" @click="showProcessesModal">
|
||||
<i class="mdi mdi-memory mr-1 tool-icon" />
|
||||
<span>{{ $t('message.processesList') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.customizations.variables"
|
||||
class="menu-item"
|
||||
title="Coming..."
|
||||
<a
|
||||
class="tab-link workspace-tools-link dropdown-toggle"
|
||||
tabindex="0"
|
||||
:title="$t('word.tools')"
|
||||
>
|
||||
<a class="c-hand p-vcentered disabled">
|
||||
<i class="mdi mdi-shape mr-1 tool-icon" />
|
||||
<span>{{ $t('word.variables') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.customizations.usersManagement"
|
||||
class="menu-item"
|
||||
title="Coming..."
|
||||
<i class="mdi mdi-24px mdi-tools" />
|
||||
</a>
|
||||
<ul v-if="hasTools" class="menu text-left text-uppercase">
|
||||
<li class="menu-item">
|
||||
<a class="c-hand p-vcentered" @click="showProcessesModal">
|
||||
<i class="mdi mdi-memory mr-1 tool-icon" />
|
||||
<span>{{ $t('message.processesList') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.customizations.variables"
|
||||
class="menu-item"
|
||||
title="Coming..."
|
||||
>
|
||||
<a class="c-hand p-vcentered disabled">
|
||||
<i class="mdi mdi-shape mr-1 tool-icon" />
|
||||
<span>{{ $t('word.variables') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.customizations.usersManagement"
|
||||
class="menu-item"
|
||||
title="Coming..."
|
||||
>
|
||||
<a class="c-hand p-vcentered disabled">
|
||||
<i class="mdi mdi-account-group mr-1 tool-icon" />
|
||||
<span>{{ $t('message.manageUsers') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
<template #footer>
|
||||
<li class="tab-item">
|
||||
<a
|
||||
class="tab-add"
|
||||
:title="$t('message.openNewTab')"
|
||||
@click="addQueryTab"
|
||||
>
|
||||
<a class="c-hand p-vcentered disabled">
|
||||
<i class="mdi mdi-account-group mr-1 tool-icon" />
|
||||
<span>{{ $t('message.manageUsers') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li slot="footer" class="tab-item">
|
||||
<a
|
||||
class="tab-add"
|
||||
:title="$t('message.openNewTab')"
|
||||
@click="addQueryTab"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-plus" />
|
||||
</a>
|
||||
</li>
|
||||
<i class="mdi mdi-24px mdi-plus" />
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</Draggable>
|
||||
<WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" />
|
||||
<template v-for="tab of workspace.tabs">
|
||||
@@ -565,7 +567,7 @@ export default {
|
||||
return this.workspace ? this.workspace.selectedTab : null;
|
||||
},
|
||||
queryTabs () {
|
||||
return this.workspace.tabs.filter(tab => tab.type === 'query');
|
||||
return this.workspace ? this.workspace.tabs.filter(tab => tab.type === 'query') : [];
|
||||
},
|
||||
schemaChild () {
|
||||
for (const key in this.workspace.breadcrumbs) {
|
||||
@@ -581,16 +583,12 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedTab (newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
queryTabs: function (newVal, oldVal) {
|
||||
if (newVal.length > oldVal.length) {
|
||||
setTimeout(() => {
|
||||
const element = this.$refs['tab-selected'] ? this.$refs['tab-selected'][0] : null;
|
||||
if (element) {
|
||||
element.setAttribute('tabindex', '-1');
|
||||
element.focus();
|
||||
element.removeAttribute('tabindex');
|
||||
}
|
||||
}, 50);
|
||||
const scroller = this.$refs.tabWrap;
|
||||
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -42,17 +42,17 @@
|
||||
@confirm="deleteMisc"
|
||||
@hide="hideDeleteModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-delete mr-1" />
|
||||
<span class="cut-text">{{ deleteMessage }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"?
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
<ModalAskParameters
|
||||
v-if="isAskingParameters"
|
||||
|
@@ -452,9 +452,9 @@ export default {
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
|
||||
.schema-size{
|
||||
visibility: hidden;
|
||||
width: 22.5px;
|
||||
.schema-size {
|
||||
visibility: hidden;
|
||||
width: 22.5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,8 +502,8 @@ export default {
|
||||
&:hover {
|
||||
border-radius: $border-radius;
|
||||
|
||||
.schema-size{
|
||||
visibility: visible;
|
||||
.schema-size {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -78,17 +78,17 @@
|
||||
@confirm="deleteSchema"
|
||||
@hide="hideDeleteModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-database-remove mr-1" />
|
||||
<span class="cut-text">{{ $t('message.deleteSchema') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedSchema }}</b>"?
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
<ModalEditSchema
|
||||
v-if="isEditModal"
|
||||
|
@@ -40,33 +40,33 @@
|
||||
@confirm="emptyTable"
|
||||
@hide="hideEmptyModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-table-off mr-1" /> <span class="cut-text">{{ $t('message.emptyTable') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
{{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
<ConfirmModal
|
||||
v-if="isDeleteModal"
|
||||
@confirm="deleteTable"
|
||||
@hide="hideDeleteModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-table-remove mr-1" />
|
||||
<span class="cut-text">{{ selectedTable.type === 'table' ? $t('message.deleteTable') : $t('message.deleteView') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<div class="workspace-query-buttons">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
:disabled="!isChanged"
|
||||
:disabled="!isChanged || !isValid"
|
||||
:class="{'loading':isSaving}"
|
||||
title="CTRL+S"
|
||||
@click="saveChanges"
|
||||
@@ -242,6 +242,9 @@ export default {
|
||||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
|
||||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
|
||||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
|
||||
},
|
||||
isValid () {
|
||||
return !!this.localFields.length && !!this.localOptions.name.trim().length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -287,7 +290,7 @@ export default {
|
||||
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
|
||||
}),
|
||||
async saveChanges () {
|
||||
if (this.isSaving) return;
|
||||
if (this.isSaving || !this.isValid) return;
|
||||
this.isSaving = true;
|
||||
|
||||
const params = {
|
||||
|
@@ -6,13 +6,13 @@
|
||||
@confirm="confirmParametersChange"
|
||||
@hide="$emit('hide')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
|
||||
<span class="cut-text">{{ $t('word.parameters') }} "{{ func }}"</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="columns col-gapless">
|
||||
<div class="column col-5">
|
||||
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
|
||||
@@ -167,7 +167,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
@@ -6,13 +6,13 @@
|
||||
@confirm="confirmParametersChange"
|
||||
@hide="$emit('hide')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
|
||||
<span class="cut-text">{{ $t('word.parameters') }} "{{ routine }}"</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="columns col-gapless">
|
||||
<div class="column col-5">
|
||||
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
|
||||
@@ -167,7 +167,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
@@ -5,13 +5,13 @@
|
||||
@confirm="confirmOptionsChange"
|
||||
@hide="$emit('hide')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-timer mr-1" />
|
||||
<span class="cut-text">{{ $t('word.timing') }} "{{ localOptions.name }}"</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
@@ -133,7 +133,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
@@ -6,13 +6,13 @@
|
||||
@confirm="confirmForeignsChange"
|
||||
@hide="$emit('hide')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-key-link mr-1" />
|
||||
<span class="cut-text">{{ $t('word.foreignKeys') }} "{{ table }}"</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="columns col-gapless">
|
||||
<div class="column col-5">
|
||||
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
|
||||
@@ -197,7 +197,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
@@ -289,7 +289,7 @@ export default {
|
||||
addForeign () {
|
||||
this.foreignProxy = [...this.foreignProxy, {
|
||||
_antares_id: uidGen(),
|
||||
constraintName: `FK_${this.foreignProxy.length + 1}`,
|
||||
constraintName: `FK_${uidGen()}`,
|
||||
refSchema: this.schema,
|
||||
table: this.table,
|
||||
refTable: '',
|
||||
|
@@ -6,13 +6,13 @@
|
||||
@confirm="confirmIndexesChange"
|
||||
@hide="$emit('hide')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
|
||||
<span class="cut-text">{{ $t('word.indexes') }} "{{ table }}"</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="columns col-gapless">
|
||||
<div class="column col-5">
|
||||
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
|
||||
@@ -133,7 +133,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
@@ -230,13 +230,13 @@
|
||||
@confirm="editOFF"
|
||||
@hide="hideDefaultModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-playlist-edit mr-1" />
|
||||
<span class="cut-text">{{ $t('word.default') }} "{{ row.name }}"</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<form class="form-horizontal">
|
||||
<div class="mb-2">
|
||||
<label class="form-radio form-inline">
|
||||
@@ -324,7 +324,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -4,6 +4,7 @@
|
||||
class="workspace-query-tab column col-12 columns col-gapless no-outline p-0"
|
||||
tabindex="0"
|
||||
@keydown.116="runQuery(query)"
|
||||
@keydown.75="killTabQuery"
|
||||
@keydown.ctrl.alt.87="clear"
|
||||
@keydown.ctrl.66="beautify"
|
||||
@keydown.ctrl.71="openHistoryModal"
|
||||
@@ -22,16 +23,29 @@
|
||||
<div ref="resizer" class="query-area-resizer" />
|
||||
<div class="workspace-query-runner-footer">
|
||||
<div class="workspace-query-buttons">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
:class="{'loading':isQuering}"
|
||||
:disabled="!query"
|
||||
title="F5"
|
||||
@click="runQuery(query)"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-play pr-1" />
|
||||
<span>{{ $t('word.run') }}</span>
|
||||
</button>
|
||||
<div @mouseenter="setCancelButtonVisibility(true)" @mouseleave="setCancelButtonVisibility(false)">
|
||||
<button
|
||||
v-if="showCancel && isQuering"
|
||||
class="btn btn-primary btn-sm cancellable"
|
||||
:disabled="!query"
|
||||
:title="$t('word.cancel')"
|
||||
@click="killTabQuery()"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-window-close" />
|
||||
<span class="d-invisible pr-1">{{ $t('word.run') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-primary btn-sm"
|
||||
:class="{'loading':isQuering}"
|
||||
:disabled="!query"
|
||||
title="F5"
|
||||
@click="runQuery(query)"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-play pr-1" />
|
||||
<span>{{ $t('word.run') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-link btn-sm mr-0"
|
||||
:disabled="!query || isQuering"
|
||||
@@ -110,7 +124,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceTabQueryEmptyState v-if="!results.length && !isQuering" />
|
||||
<WorkspaceTabQueryEmptyState v-if="!results.length && !isQuering" :customizations="workspace.customizations" />
|
||||
<div class="workspace-query-results p-relative column col-12">
|
||||
<BaseLoader v-if="isQuering" />
|
||||
<WorkspaceTabQueryTable
|
||||
@@ -166,6 +180,8 @@ export default {
|
||||
query: '',
|
||||
lastQuery: '',
|
||||
isQuering: false,
|
||||
isCancelling: false,
|
||||
showCancel: false,
|
||||
results: [],
|
||||
selectedSchema: null,
|
||||
resultsCount: 0,
|
||||
@@ -202,8 +218,13 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
isSelected (val) {
|
||||
if (val)
|
||||
if (val) {
|
||||
this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
|
||||
setTimeout(() => {
|
||||
if (this.$refs.queryEditor)
|
||||
this.$refs.queryEditor.editor.focus();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
selectedSchema () {
|
||||
this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
|
||||
@@ -248,6 +269,7 @@ export default {
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
schema: this.selectedSchema,
|
||||
tabUid: this.tab.uid,
|
||||
query
|
||||
};
|
||||
|
||||
@@ -283,6 +305,29 @@ export default {
|
||||
this.isQuering = false;
|
||||
this.lastQuery = query;
|
||||
},
|
||||
async killTabQuery () {
|
||||
if (this.isCancelling) return;
|
||||
|
||||
this.isCancelling = true;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
tabUid: this.tab.uid
|
||||
};
|
||||
|
||||
await Schema.killTabQuery(params);
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isCancelling = false;
|
||||
},
|
||||
setCancelButtonVisibility (val) {
|
||||
if (this.workspace.customizations.cancelQueries)
|
||||
this.showCancel = val;
|
||||
},
|
||||
reloadTable () {
|
||||
this.runQuery(this.lastQuery);
|
||||
},
|
||||
|
@@ -5,6 +5,9 @@
|
||||
<div class="mb-4">
|
||||
{{ $t('message.runQuery') }}
|
||||
</div>
|
||||
<div v-if="customizations.cancelQueries" class="mb-4">
|
||||
{{ $t('message.killQuery') }}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
{{ $t('word.format') }}
|
||||
</div>
|
||||
@@ -25,6 +28,9 @@
|
||||
<div class="mb-4">
|
||||
<code>F5</code>
|
||||
</div>
|
||||
<div v-if="customizations.cancelQueries" class="mb-4">
|
||||
<code>CTRL</code> + <code>K</code>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<code>CTRL</code> + <code>B</code>
|
||||
</div>
|
||||
@@ -47,7 +53,10 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WorkspaceTabQueryEmptyState'
|
||||
name: 'WorkspaceTabQueryEmptyState',
|
||||
props: {
|
||||
customizations: Object
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
tabindex="0"
|
||||
:style="{'height': resultsSize+'px'}"
|
||||
@keyup.46="showDeleteConfirmModal"
|
||||
@keydown.ctrl.65="selectAllRows"
|
||||
@keydown.ctrl.65="selectAllRows($event)"
|
||||
@keydown.esc="deselectRows"
|
||||
>
|
||||
<TableContext
|
||||
@@ -53,6 +53,7 @@
|
||||
class="mdi sort-icon"
|
||||
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
|
||||
/>
|
||||
<i v-else class="mdi sort-icon mdi-minus d-invisible" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,7 +63,7 @@
|
||||
v-if="resultsWithRows[resultsetIndex] && resultsWithRows[resultsetIndex].rows"
|
||||
ref="resultTable"
|
||||
:items="sortedResults"
|
||||
:item-height="23"
|
||||
:item-height="rowHeight"
|
||||
class="tbody"
|
||||
:visible-height="resultsSize"
|
||||
:scroll-element="scrollElement"
|
||||
@@ -71,6 +72,7 @@
|
||||
<WorkspaceTabQueryTableRow
|
||||
v-for="row in items"
|
||||
:key="row._antares_id"
|
||||
:item-height="rowHeight"
|
||||
:row="row"
|
||||
:fields="fieldsObj"
|
||||
:key-usage="keyUsage"
|
||||
@@ -89,17 +91,17 @@
|
||||
@confirm="deleteSelected"
|
||||
@hide="hideDeleteConfirmModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-delete mr-1" />
|
||||
<span class="cut-text">{{ $tc('message.deleteRows', selectedRows.length) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -142,7 +144,8 @@ export default {
|
||||
currentSort: '',
|
||||
currentSortDir: 'asc',
|
||||
resultsetIndex: 0,
|
||||
scrollElement: null
|
||||
scrollElement: null,
|
||||
rowHeight: 23
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -242,6 +245,11 @@ export default {
|
||||
|
||||
if (this.$refs.tableWrapper)
|
||||
this.scrollElement = this.$refs.tableWrapper;
|
||||
|
||||
document.querySelectorAll('.column-resizable').forEach(element => {
|
||||
if (element.clientWidth !== 0)
|
||||
element.style.width = element.clientWidth + 'px';
|
||||
});
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('resize', this.resizeResults);
|
||||
@@ -450,7 +458,9 @@ export default {
|
||||
else
|
||||
this.selectedRows = [row];
|
||||
},
|
||||
selectAllRows () {
|
||||
selectAllRows (e) {
|
||||
if (e.target.classList.contains('editable-field')) return;
|
||||
|
||||
this.selectedRows = this.localResults.reduce((acc, curr) => {
|
||||
acc.push(curr._antares_id);
|
||||
return acc;
|
||||
|
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="tr" @click="selectRow($event, row._antares_id)">
|
||||
<div
|
||||
class="tr"
|
||||
:style="{height: itemHeight+'px'}"
|
||||
@click="selectRow($event, row._antares_id)"
|
||||
>
|
||||
<div
|
||||
v-for="(col, cKey) in row"
|
||||
v-show="cKey !== '_antares_id'"
|
||||
@@ -72,12 +76,12 @@
|
||||
@confirm="editOFF"
|
||||
@hide="hideEditorModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<TextEditor
|
||||
@@ -96,23 +100,12 @@
|
||||
v-model="editorMode"
|
||||
class="form-select select-sm"
|
||||
>
|
||||
<option value="text">
|
||||
TEXT
|
||||
</option>
|
||||
<option value="html">
|
||||
HTML
|
||||
</option>
|
||||
<option value="xml">
|
||||
XML
|
||||
</option>
|
||||
<option value="json">
|
||||
JSON
|
||||
</option>
|
||||
<option value="svg">
|
||||
SVG
|
||||
</option>
|
||||
<option value="yaml">
|
||||
YAML
|
||||
<option
|
||||
v-for="language in availableLanguages"
|
||||
:key="language.slug"
|
||||
:value="language.slug"
|
||||
>
|
||||
{{ language.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -128,7 +121,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
<ConfirmModal
|
||||
v-if="isBlobEditor"
|
||||
@@ -136,13 +129,13 @@
|
||||
@confirm="editOFF"
|
||||
@hide="hideEditorModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-playlist-edit mr-1" />
|
||||
<span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
<transition name="jump-down">
|
||||
<div v-if="contentInfo.size">
|
||||
@@ -182,13 +175,14 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { ModelOperations } from '@vscode/vscode-languagedetection';
|
||||
import { mimeFromHex } from 'common/libs/mimeFromHex';
|
||||
import { formatBytes } from 'common/libs/formatBytes';
|
||||
import { bufferToBase64 } from 'common/libs/bufferToBase64';
|
||||
@@ -261,6 +255,7 @@ export default {
|
||||
row: Object,
|
||||
fields: Object,
|
||||
keyUsage: Array,
|
||||
itemHeight: Number,
|
||||
elementType: { type: String, default: 'table' }
|
||||
},
|
||||
data () {
|
||||
@@ -280,7 +275,17 @@ export default {
|
||||
mime: '',
|
||||
size: null
|
||||
},
|
||||
fileToUpload: null
|
||||
fileToUpload: null,
|
||||
availableLanguages: [
|
||||
{ name: 'TEXT', slug: 'text', id: 'text' },
|
||||
{ name: 'HTML', slug: 'html', id: 'html' },
|
||||
{ name: 'XML', slug: 'xml', id: 'xml' },
|
||||
{ name: 'JSON', slug: 'json', id: 'json' },
|
||||
{ name: 'SVG', slug: 'svg', id: 'svg' },
|
||||
{ name: 'INI', slug: 'ini', id: 'ini' },
|
||||
{ name: 'MARKDOWN', slug: 'markdown', id: 'md' },
|
||||
{ name: 'YAML', slug: 'yaml', id: 'yaml' }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -362,6 +367,21 @@ export default {
|
||||
Object.keys(this.fields).forEach(field => {
|
||||
this.isInlineEditor[field.name] = false;
|
||||
});
|
||||
},
|
||||
isTextareaEditor (val) {
|
||||
if (val) {
|
||||
const modelOperations = new ModelOperations();
|
||||
(async () => {
|
||||
const detected = await modelOperations.runModel(this.editingContent);
|
||||
const filteredLanguages = detected.filter(dLang =>
|
||||
this.availableLanguages.some(aLang => aLang.id === dLang.languageId) &&
|
||||
dLang.confidence > 0.1
|
||||
);
|
||||
|
||||
if (filteredLanguages.length)
|
||||
this.editorMode = this.availableLanguages.find(lang => lang.id === filteredLanguages[0].languageId).slug;
|
||||
})();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@@ -251,7 +251,8 @@ module.exports = {
|
||||
killProcess: 'Kill process',
|
||||
closeTab: 'Close tab',
|
||||
goToDownloadPage: 'Go to download page',
|
||||
readOnlyMode: 'Read-only mode'
|
||||
readOnlyMode: 'Read-only mode',
|
||||
killQuery: 'Kill query'
|
||||
},
|
||||
faker: {
|
||||
address: 'Address',
|
||||
|
@@ -120,7 +120,11 @@ module.exports = {
|
||||
new: 'Mới',
|
||||
history: 'Lịch sử',
|
||||
select: 'Chọn',
|
||||
passphrase: 'Cụm mật khẩu'
|
||||
passphrase: 'Cụm mật khẩu',
|
||||
filter: 'Bộ lọc',
|
||||
disabled: 'Đã tắt',
|
||||
enable: 'Bật',
|
||||
disable: 'Tắt'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Chào bạn đến với Antares SQL Client!',
|
||||
@@ -244,7 +248,10 @@ module.exports = {
|
||||
newTriggerFunction: 'Chức năng kích hoạt mới',
|
||||
thereIsNoQueriesYet: 'Không có truy vấn nào',
|
||||
searchForQueries: 'Tìm kiếm truy vấn',
|
||||
killProcess: 'Huỷ quá trình'
|
||||
killProcess: 'Huỷ quá trình',
|
||||
closeTab: 'Đóng tab',
|
||||
goToDownloadPage: 'Tới trang tải về',
|
||||
readOnlyMode: 'Chế độ chỉ đọc'
|
||||
},
|
||||
faker: {
|
||||
address: 'Địa chỉ',
|
||||
|
@@ -46,6 +46,10 @@ export default class {
|
||||
return ipcRenderer.invoke('kill-process', params);
|
||||
}
|
||||
|
||||
static killTabQuery (params) {
|
||||
return ipcRenderer.invoke('kill-tab-query', params);
|
||||
}
|
||||
|
||||
static useSchema (params) {
|
||||
return ipcRenderer.invoke('use-schema', params);
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@
|
||||
background-image: url("../images/svg/pg.svg");
|
||||
}
|
||||
|
||||
&.dbi-sqlite {
|
||||
&.dbi-sqlite {
|
||||
background-image: url("../images/svg/sqlite.svg");
|
||||
}
|
||||
|
||||
|
@@ -59,6 +59,34 @@ option:checked {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cancellable {
|
||||
color: transparent !important;
|
||||
min-height: 0.8rem;
|
||||
position: relative;
|
||||
|
||||
> .mdi,
|
||||
> .span {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "\2715";
|
||||
color: $light-color;
|
||||
font-weight: 700;
|
||||
top: 36%;
|
||||
display: block;
|
||||
height: 0.8rem;
|
||||
left: 50%;
|
||||
margin-left: -0.4rem;
|
||||
margin-top: -0.4rem;
|
||||
opacity: 1;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 0.8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-tabs {
|
||||
align-content: baseline;
|
||||
|
||||
|
50
tests/app.spec.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const { _electron: electron } = require('playwright');
|
||||
const { strict: assert } = require('assert');
|
||||
|
||||
(async () => {
|
||||
console.log('Starting tests');
|
||||
// Launch Electron app.
|
||||
const electronApp = await electron.launch({ args: ['dist/main.js'] });
|
||||
|
||||
/**
|
||||
* App main window state
|
||||
* @type {{isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean}}
|
||||
*/
|
||||
const windowState = await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
const getState = () => ({
|
||||
isVisible: mainWindow.isVisible(),
|
||||
isDevToolsOpened: mainWindow.webContents.isDevToolsOpened()
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (mainWindow.isVisible())
|
||||
resolve(getState());
|
||||
else
|
||||
mainWindow.once('ready-to-show', () => setTimeout(() => resolve(getState()), 0));
|
||||
});
|
||||
});
|
||||
|
||||
// Check main window state
|
||||
assert.ok(windowState.isVisible, 'Main window not visible');
|
||||
assert.ok(!windowState.isDevToolsOpened, 'DevTools opened');
|
||||
assert.ok(!windowState.isCrashed, 'Window crashed');
|
||||
|
||||
/**
|
||||
* Rendered Main window web-page
|
||||
* @type {Page}
|
||||
*/
|
||||
const page = await electronApp.firstWindow();
|
||||
console.log(await page.title());
|
||||
|
||||
// Check web-page content
|
||||
const element = await page.$('#wrapper', { strict: true });
|
||||
|
||||
assert.notStrictEqual(element, null, 'Can\'t find root element');
|
||||
assert.notStrictEqual((await element.innerHTML()).trim(), '', 'Window content is empty');
|
||||
|
||||
// Close app
|
||||
await electronApp.close();
|
||||
console.log('Tests finished');
|
||||
})();
|