mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
7d0c929fb8 | |||
25d72e3952 | |||
b232a3bb5f | |||
e9a26c1bc0 | |||
76c5c0c680 | |||
ddfb713124 | |||
0081a4167c | |||
239cb4488f | |||
fb5adbe676 | |||
9cd51c8d8b | |||
8dfaa3b7be | |||
5d7efa75b7 | |||
4862d51fba | |||
07f60c3917 | |||
049143d143 | |||
db4430609e | |||
71b4310117 | |||
201fad9265 | |||
45351faeae | |||
b4ead6992c | |||
b1ea32b680 | |||
39ca1974bc | |||
777b73fa6f |
41
CHANGELOG.md
41
CHANGELOG.md
@@ -2,6 +2,47 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.0.20](https://github.com/Fabio286/antares/compare/v0.0.19...v0.0.20) (2021-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **UI:** loader layers on query and data tabs ([b232a3b](https://github.com/Fabio286/antares/commit/b232a3bb5ff7e38c83aa33e8b96ec7202bc4881e))
|
||||
* **UI:** row markers in sql editors ([ddfb713](https://github.com/Fabio286/antares/commit/ddfb7131248a47fa2055ccfb72e223986a17f986))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **UI:** avoid unnecessary updates when cell content not change ([e9a26c1](https://github.com/Fabio286/antares/commit/e9a26c1bc0641b7087d8143cc948405850d7552f))
|
||||
* **UI:** row mark not applied on first click ([25d72e3](https://github.com/Fabio286/antares/commit/25d72e39529884c09cf1286ff64ba00c8a5c7b24))
|
||||
* **UI:** table rows lose internal id after an update ([76c5c0c](https://github.com/Fabio286/antares/commit/76c5c0c680521b4a20de1f12bab25314ac084d5c))
|
||||
|
||||
### [0.0.19](https://github.com/Fabio286/antares/compare/v0.0.18...v0.0.19) (2021-03-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **UI:** modal that shows process query ([07f60c3](https://github.com/Fabio286/antares/commit/07f60c39173e9db452909d74573c3aecf4b6466b))
|
||||
* processes list tool ([049143d](https://github.com/Fabio286/antares/commit/049143d143d8187bfcb6377f2bf374c471c28046))
|
||||
* **MySQL:** support to new mysql8 authentication, closes [#45](https://github.com/Fabio286/antares/issues/45) ([db44306](https://github.com/Fabio286/antares/commit/db4430609e22816f4a3a8ecdbf61e9b51cde2579))
|
||||
* context menu shortcut to set NULL a table cell ([71b4310](https://github.com/Fabio286/antares/commit/71b43101172c27d810d533c936534bcf089dbca2))
|
||||
* **UI:** esc key to cancel cell edit ([45351fa](https://github.com/Fabio286/antares/commit/45351faeaea6ad4af8280da6c0ec1b4ded0f86fe))
|
||||
* setting to enable beta updates (future use) ([b1ea32b](https://github.com/Fabio286/antares/commit/b1ea32b68024593481120e2ef67642e127554888))
|
||||
* **UI:** query duration calc ([777b73f](https://github.com/Fabio286/antares/commit/777b73fa6f9d6f7806e0d3d07589dfaee0c40786))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MySQL:** wrong TIMESTAMP fields length ([201fad9](https://github.com/Fabio286/antares/commit/201fad9265e01e19ae4c8dc16bff84ee9dc9c894))
|
||||
* **UI:** modal processes list does not regain size on window resize ([5d7efa7](https://github.com/Fabio286/antares/commit/5d7efa75b76f59b85654603b4df8035a36af0576))
|
||||
* **UI:** wrong height in scrolling tables in some cases ([4862d51](https://github.com/Fabio286/antares/commit/4862d51fba863e055ca6735586b0abe4894037eb))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* **UI:** big performance improvement in tables rendering ([39ca197](https://github.com/Fabio286/antares/commit/39ca1974bcea84c9047779893ed3f458300474e3))
|
||||
* **UI:** improvements of date time inputs ([b4ead69](https://github.com/Fabio286/antares/commit/b4ead6992c8ddc172380a97e7aa125e0dd078f1e))
|
||||
|
||||
### [0.0.18](https://github.com/Fabio286/antares/compare/v0.0.17...v0.0.18) (2021-02-25)
|
||||
|
||||
|
||||
|
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "antares",
|
||||
"productName": "Antares",
|
||||
"version": "0.0.18",
|
||||
"version": "0.0.20",
|
||||
"description": "A cross-platform easy to use SQL client.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Fabio286/antares.git",
|
||||
@@ -11,6 +11,7 @@
|
||||
"build": "cross-env NODE_ENV=production npm run compile && electron-builder",
|
||||
"release": "standard-version",
|
||||
"release:pre": "npm run release -- --prerelease alpha",
|
||||
"release:snap": "npm run build -- --linux snap && snapcraft upload --release=edge ./dist/Antares-${npm_package_version}-linux_amd64.snap",
|
||||
"test": "npm run lint",
|
||||
"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"
|
||||
@@ -19,6 +20,12 @@
|
||||
"build": {
|
||||
"appId": "com.fabio286.antares",
|
||||
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
"portable"
|
||||
]
|
||||
},
|
||||
"dmg": {
|
||||
"contents": [
|
||||
{
|
||||
@@ -43,6 +50,9 @@
|
||||
"appImage": {
|
||||
"license": "./LICENSE",
|
||||
"category": "Development"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "${productName}-${version}-portable.exe"
|
||||
}
|
||||
},
|
||||
"electronWebpack": {
|
||||
@@ -60,12 +70,12 @@
|
||||
"keytar": "^7.3.0",
|
||||
"moment": "^2.29.1",
|
||||
"mssql": "^6.2.3",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^2.2.5",
|
||||
"pg": "^8.5.1",
|
||||
"source-map-support": "^0.5.16",
|
||||
"spectre.css": "^0.5.9",
|
||||
"v-mask": "^2.2.4",
|
||||
"vue-i18n": "^8.22.4",
|
||||
"vue-the-mask": "^0.11.1",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.0"
|
||||
},
|
||||
|
96
snap/snapcraft.yaml
Normal file
96
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
name: antares
|
||||
adopt-info: antares
|
||||
summary: Open source SQL client made to be simple and complete.
|
||||
description: |
|
||||
Antares is an SQL client that aims to become an useful and complete tool, especially for developers.
|
||||
The 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 and supports only MySQL and x86 architecture.
|
||||
Most of its current features might be enough for MySQL management, so give it a chance and send us your feedback, we would really appreciate it.
|
||||
base: core18
|
||||
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
||||
architectures:
|
||||
- build-on: amd64
|
||||
compression: lzo
|
||||
|
||||
parts:
|
||||
antares:
|
||||
plugin: dump
|
||||
source: .
|
||||
override-build: |
|
||||
snapcraftctl build
|
||||
ARCHITECTURE=$(dpkg --print-architecture)
|
||||
if [ "${ARCHITECTURE}" = "amd64" ]; then
|
||||
FILTER="amd64.deb"
|
||||
else
|
||||
echo "ERROR! Antares only produces debs for amd64. Failing the build here."
|
||||
exit 1
|
||||
fi
|
||||
# Get the latest releases json
|
||||
echo "Get GitHub releases..."
|
||||
wget --quiet https://api.github.com/repos/fabio286/antares/releases/latest -O releases.json
|
||||
# Get the version from the tag_name and the download URL.
|
||||
VERSION=$(jq . releases.json | grep tag_name | cut -d'"' -f4 | sed s'/release-//')
|
||||
DEB_URL=$(cat releases.json | jq -r ".assets[] | select(.name | test(\"${FILTER}\")) | .browser_download_url")
|
||||
DEB=$(basename "${DEB_URL}")
|
||||
echo "Downloading ${DEB_URL}..."
|
||||
wget --quiet "${DEB_URL}" -O "${SNAPCRAFT_PART_INSTALL}/${DEB}"
|
||||
echo "Unpacking ${DEB}..."
|
||||
dpkg -x "${SNAPCRAFT_PART_INSTALL}/${DEB}" ${SNAPCRAFT_PART_INSTALL}
|
||||
rm -f releases.json 2>/dev/null
|
||||
rm -f "${SNAPCRAFT_PART_INSTALL}/${DEB}" 2>/dev/null
|
||||
echo $VERSION > $SNAPCRAFT_STAGE/version
|
||||
# Correct path to icon.
|
||||
sed -i 's|Icon=antares|Icon=/usr/share/icons/hicolor/256x256/apps/antares\.png|g' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/antares.desktop
|
||||
# Delete usr/bin/antares, it's a broken symlink pointing outside the snap.
|
||||
rm -f ${SNAPCRAFT_PART_INSTALL}/usr/bin/antares
|
||||
chmod -s ${SNAPCRAFT_PART_INSTALL}/opt/Antares/chrome-sandbox
|
||||
snapcraftctl set-version "$(echo $VERSION)"
|
||||
build-packages:
|
||||
- dpkg
|
||||
- jq
|
||||
- sed
|
||||
- wget
|
||||
stage-packages:
|
||||
- fcitx-frontend-gtk3
|
||||
- libappindicator3-1
|
||||
- libasound2
|
||||
- libgconf-2-4
|
||||
- libgtk-3-0
|
||||
- libnotify4
|
||||
- libnspr4
|
||||
- libnss3
|
||||
- libpcre3
|
||||
- libpulse0
|
||||
- libxss1
|
||||
- libsecret-1-0
|
||||
- libxtst6
|
||||
- libxkbfile1
|
||||
cleanup:
|
||||
after: [antares]
|
||||
plugin: nil
|
||||
build-snaps: [ gnome-3-28-1804 ]
|
||||
override-prime: |
|
||||
set -eux
|
||||
cd /snap/gnome-3-28-1804/current
|
||||
find . -type f,l -exec rm -f $SNAPCRAFT_PRIME/{} \;
|
||||
|
||||
apps:
|
||||
antares:
|
||||
command: opt/Antares/antares --no-sandbox
|
||||
desktop: usr/share/applications/antares.desktop
|
||||
extensions: [gnome-3-28]
|
||||
environment:
|
||||
# Fallback to XWayland if running in a Wayland session.
|
||||
DISABLE_WAYLAND: 1
|
||||
plugs:
|
||||
- browser-support
|
||||
- cups-control
|
||||
- home
|
||||
- network
|
||||
- opengl
|
||||
- pulseaudio
|
||||
- removable-media
|
||||
- unity7
|
@@ -96,11 +96,16 @@ else {
|
||||
|
||||
// create main BrowserWindow when electron is ready
|
||||
app.on('ready', async () => {
|
||||
let key = await keytar.getPassword('antares', 'user');
|
||||
try {
|
||||
let key = await keytar.getPassword('antares', 'user');
|
||||
|
||||
if (!key) {
|
||||
key = crypto.randomBytes(16).toString('hex');
|
||||
keytar.setPassword('antares', 'user', key);
|
||||
if (!key) {
|
||||
key = crypto.randomBytes(16).toString('hex');
|
||||
keytar.setPassword('antares', 'user', key);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
mainWindow = createMainWindow();
|
||||
|
@@ -7,7 +7,14 @@ export default () => {
|
||||
});
|
||||
|
||||
ipcMain.on('get-key', async event => {
|
||||
const key = await keytar.getPassword('antares', 'user');
|
||||
let key = false;
|
||||
|
||||
try {
|
||||
key = await keytar.getPassword('antares', 'user');
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
event.returnValue = key;
|
||||
});
|
||||
};
|
||||
|
@@ -105,6 +105,17 @@ export default connections => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-processes', async (event, uid) => {
|
||||
try {
|
||||
const result = await connections[uid].getProcesses();
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('use-schema', async (event, { uid, schema }) => {
|
||||
if (!schema) return;
|
||||
|
||||
|
@@ -81,6 +81,8 @@ export default (connections) => {
|
||||
escapedParam = `b'${sqlEscaper(params.content)}'`;
|
||||
reload = true;
|
||||
}
|
||||
else if (params.content === null)
|
||||
escapedParam = 'NULL';
|
||||
else
|
||||
escapedParam = `"${sqlEscaper(params.content)}"`;
|
||||
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import Store from 'electron-store';
|
||||
const persistentStore = new Store({ name: 'settings' });
|
||||
|
||||
let mainWindow;
|
||||
autoUpdater.allowPrerelease = true;
|
||||
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
|
||||
|
||||
export default () => {
|
||||
ipcMain.on('check-for-updates', event => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
import mysql from 'mysql';
|
||||
import mysql from 'mysql2';
|
||||
import { AntaresCore } from '../AntaresCore';
|
||||
import dataTypes from 'common/data-types/mysql';
|
||||
|
||||
@@ -43,17 +43,21 @@ export class MySQLClient extends AntaresCore {
|
||||
}
|
||||
|
||||
_getType (field) {
|
||||
let name = this.types[field.type];
|
||||
let length = field.length;
|
||||
let name = this.types[field.columnType];
|
||||
let length = field.columnLength;
|
||||
|
||||
if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name))
|
||||
length = field.decimals;
|
||||
|
||||
if (name === 'CHAR' && field.charsetNr === 63)// if binary
|
||||
name = 'BINARY';
|
||||
if (name === 'TIMESTAMP')
|
||||
length = 0;
|
||||
|
||||
if (name === 'VARCHAR' && field.charsetNr === 63)// if binary
|
||||
name = 'VARBINARY';
|
||||
if (field.charsetNr === 63) { // if binary
|
||||
if (name === 'CHAR')
|
||||
name = 'BINARY';
|
||||
else if (name === 'VARCHAR')
|
||||
name = 'VARBINARY';
|
||||
}
|
||||
|
||||
if (name === 'BLOB') {
|
||||
switch (length) {
|
||||
@@ -952,6 +956,25 @@ export class MySQLClient extends AntaresCore {
|
||||
}, {});
|
||||
}
|
||||
|
||||
async getProcesses () {
|
||||
const sql = 'SELECT `ID`, `USER`, `HOST`, `DB`, `COMMAND`, `TIME`, `STATE`, LEFT(`INFO`, 51200) AS `INFO` FROM `information_schema`.`PROCESSLIST`';
|
||||
|
||||
const { rows } = await this.raw(sql);
|
||||
|
||||
return rows.map(row => {
|
||||
return {
|
||||
id: row.ID,
|
||||
user: row.USER,
|
||||
host: row.HOST,
|
||||
db: row.DB,
|
||||
command: row.COMMAND,
|
||||
time: row.TIME,
|
||||
state: row.STATE,
|
||||
info: row.INFO
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE
|
||||
*
|
||||
@@ -1206,10 +1229,13 @@ export class MySQLClient extends AntaresCore {
|
||||
|
||||
for (const query of queries) {
|
||||
if (!query) continue;
|
||||
const timeStart = new Date();
|
||||
let timeStop;
|
||||
let keysArr = [];
|
||||
|
||||
const { rows, report, fields, keys } = await new Promise((resolve, reject) => {
|
||||
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
|
||||
this._connection.query({ sql: query, nestTables }, async (err, response, fields) => {
|
||||
timeStop = new Date();
|
||||
const queryResult = response;
|
||||
|
||||
if (err)
|
||||
@@ -1226,10 +1252,9 @@ export class MySQLClient extends AntaresCore {
|
||||
name: field.orgName,
|
||||
alias: field.name,
|
||||
orgName: field.orgName,
|
||||
schema: field.db,
|
||||
schema: field.schema,
|
||||
table: field.table,
|
||||
tableAlias: field.table,
|
||||
zerofill: field.zerofill,
|
||||
orgTable: field.orgTable,
|
||||
type: type.name,
|
||||
length: type.length
|
||||
@@ -1277,6 +1302,7 @@ export class MySQLClient extends AntaresCore {
|
||||
}
|
||||
|
||||
resolve({
|
||||
duration: timeStop - timeStart,
|
||||
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
|
||||
report: !Array.isArray(queryResult) ? queryResult : false,
|
||||
fields: remappedFields,
|
||||
@@ -1286,7 +1312,7 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
});
|
||||
|
||||
resultsArr.push({ rows, report, fields, keys });
|
||||
resultsArr.push({ rows, report, fields, keys, duration });
|
||||
}
|
||||
|
||||
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
|
||||
|
@@ -24,7 +24,7 @@
|
||||
<slot name="body" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer pt-0">
|
||||
<div v-if="!hideFooter" class="modal-footer">
|
||||
<button
|
||||
class="btn btn-primary mr-2"
|
||||
@click.stop="confirmModal"
|
||||
@@ -51,6 +51,10 @@ export default {
|
||||
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
|
||||
default: 'small'
|
||||
},
|
||||
hideFooter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
confirmText: String,
|
||||
cancelText: String
|
||||
},
|
||||
@@ -74,6 +78,12 @@ export default {
|
||||
else return '';
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
confirmModal () {
|
||||
this.$emit('confirm');
|
||||
@@ -82,6 +92,11 @@ export default {
|
||||
|
||||
hideModal () {
|
||||
this.$emit('hide');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.hideModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -41,14 +41,16 @@ export default {
|
||||
localScrollElement: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
scrollElement () {
|
||||
this.setScrollElement();
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this._checkScrollPosition = this.checkScrollPosition.bind(this);
|
||||
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
|
||||
this.updateWindow();
|
||||
this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
|
||||
this.setScrollElement();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition);
|
||||
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
|
||||
},
|
||||
methods: {
|
||||
checkScrollPosition (e) {
|
||||
@@ -58,7 +60,7 @@ export default {
|
||||
this.updateWindow(e);
|
||||
}, 200);
|
||||
},
|
||||
updateWindow (e) {
|
||||
updateWindow () {
|
||||
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
|
||||
const totalScrollHeight = this.items.length * this.itemHeight;
|
||||
const offset = 50;
|
||||
@@ -74,6 +76,14 @@ export default {
|
||||
|
||||
this.topHeight = firstCutIndex * this.itemHeight;
|
||||
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
|
||||
},
|
||||
setScrollElement () {
|
||||
if (this.localScrollElement)
|
||||
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
|
||||
|
||||
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
|
||||
this.updateWindow();
|
||||
this.localScrollElement.addEventListener('scroll', this.checkScrollPosition);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -90,7 +90,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mask } from 'vue-the-mask';
|
||||
import { VueMaskDirective } from 'v-mask';
|
||||
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
|
||||
import BaseUploadInput from '@/components/BaseUploadInput';
|
||||
import ForeignKeySelect from '@/components/ForeignKeySelect';
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
BaseUploadInput
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
mask: VueMaskDirective
|
||||
},
|
||||
props: {
|
||||
type: String,
|
||||
|
@@ -185,7 +185,6 @@
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
|
||||
import { mask } from 'vue-the-mask';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import FakerSelect from '@/components/FakerSelect';
|
||||
@@ -195,9 +194,6 @@ export default {
|
||||
components: {
|
||||
FakerSelect
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
},
|
||||
filters: {
|
||||
wrapNumber (num) {
|
||||
if (!num) return '';
|
||||
|
@@ -118,7 +118,7 @@
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
|
||||
import { mask } from 'vue-the-mask';
|
||||
import { VueMaskDirective } from 'v-mask';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import ForeignKeySelect from '@/components/ForeignKeySelect';
|
||||
@@ -129,7 +129,7 @@ export default {
|
||||
ForeignKeySelect
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
mask: VueMaskDirective
|
||||
},
|
||||
filters: {
|
||||
wrapNumber (num) {
|
||||
|
310
src/renderer/components/ModalProcessesList.vue
Normal file
310
src/renderer/components/ModalProcessesList.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div class="modal-container p-0 pb-4">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-memory mr-1" /> {{ $t('message.processesList') }}: {{ connectionName }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="processes-toolbar py-2 px-4">
|
||||
<div class="dropdown">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
|
||||
:class="{'loading':isQuering}"
|
||||
title="F5"
|
||||
@click="getProcessesList"
|
||||
>
|
||||
<span>{{ $t('word.refresh') }}</span>
|
||||
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
|
||||
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
|
||||
</button>
|
||||
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
|
||||
<i class="mdi mdi-24px mdi-menu-down" />
|
||||
</div>
|
||||
<div class="menu px-3">
|
||||
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
|
||||
<input
|
||||
v-model="autorefreshTimer"
|
||||
class="slider no-border"
|
||||
type="range"
|
||||
min="0"
|
||||
max="15"
|
||||
step="0.5"
|
||||
@change="setRefreshInterval"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-query-info">
|
||||
<div v-if="sortedResults.length">
|
||||
{{ $t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body py-0 workspace-query-results">
|
||||
<div
|
||||
ref="tableWrapper"
|
||||
class="vscroll"
|
||||
:style="{'height': resultsSize+'px'}"
|
||||
>
|
||||
<div ref="table" class="table table-hover">
|
||||
<div class="thead">
|
||||
<div class="tr">
|
||||
<div
|
||||
v-for="(field, index) in fields"
|
||||
:key="index"
|
||||
class="th c-hand"
|
||||
>
|
||||
<div ref="columnResize" class="column-resizable">
|
||||
<div class="table-column-title" @click="sort(field)">
|
||||
<span>{{ field.toUpperCase() }}</span>
|
||||
<i
|
||||
v-if="currentSort === field"
|
||||
class="mdi sort-icon"
|
||||
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BaseVirtualScroll
|
||||
ref="resultTable"
|
||||
:items="sortedResults"
|
||||
:item-height="22"
|
||||
class="tbody"
|
||||
:visible-height="resultsSize"
|
||||
:scroll-element="scrollElement"
|
||||
>
|
||||
<template slot-scope="{ items }">
|
||||
<ProcessesListRow
|
||||
v-for="row in items"
|
||||
:key="row._id"
|
||||
class="process-row"
|
||||
:row="row"
|
||||
@contextmenu="contextMenu"
|
||||
@stop-refresh="stopRefresh"
|
||||
/>
|
||||
</template>
|
||||
</BaseVirtualScroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Database from '@/ipc-api/Database';
|
||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
|
||||
import ProcessesListRow from '@/components/ProcessesListRow';
|
||||
|
||||
export default {
|
||||
name: 'ModalProcessesList',
|
||||
components: {
|
||||
BaseVirtualScroll,
|
||||
ProcessesListRow
|
||||
},
|
||||
props: {
|
||||
connection: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resultsSize: 1000,
|
||||
isQuering: false,
|
||||
autorefreshTimer: 0,
|
||||
refreshInterval: null,
|
||||
results: [],
|
||||
fields: [],
|
||||
currentSort: '',
|
||||
currentSortDir: 'asc',
|
||||
scrollElement: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getConnectionName: 'connections/getConnectionName'
|
||||
}),
|
||||
connectionName () {
|
||||
return this.getConnectionName(this.connection.uid);
|
||||
},
|
||||
sortedResults () {
|
||||
if (this.currentSort) {
|
||||
return [...this.results].sort((a, b) => {
|
||||
let modifier = 1;
|
||||
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
|
||||
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
|
||||
if (this.currentSortDir === 'desc') modifier = -1;
|
||||
if (valA < valB) return -1 * modifier;
|
||||
if (valA > valB) return 1 * modifier;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
else
|
||||
return this.results;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.onKey, { capture: true });
|
||||
},
|
||||
updated () {
|
||||
if (this.$refs.table)
|
||||
this.refreshScroller();
|
||||
|
||||
if (this.$refs.tableWrapper)
|
||||
this.scrollElement = this.$refs.tableWrapper;
|
||||
},
|
||||
mounted () {
|
||||
this.getProcessesList();
|
||||
window.addEventListener('resize', this.resizeResults);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey, { capture: true });
|
||||
window.removeEventListener('resize', this.resizeResults);
|
||||
clearInterval(this.refreshInterval);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
async getProcessesList () {
|
||||
this.isQuering = true;
|
||||
|
||||
// if table changes clear cached values
|
||||
if (this.lastTable !== this.table)
|
||||
this.results = [];
|
||||
|
||||
try { // Table data
|
||||
const { status, response } = await Database.getProcesses(this.connection.uid);
|
||||
|
||||
if (status === 'success') {
|
||||
this.results = response;
|
||||
this.fields = response.length ? Object.keys(response[0]) : [];
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isQuering = false;
|
||||
},
|
||||
setRefreshInterval () {
|
||||
this.clearRefresh();
|
||||
|
||||
if (+this.autorefreshTimer) {
|
||||
this.refreshInterval = setInterval(() => {
|
||||
if (!this.isQuering)
|
||||
this.getProcessesList();
|
||||
}, this.autorefreshTimer * 1000);
|
||||
}
|
||||
},
|
||||
clearRefresh () {
|
||||
if (this.refreshInterval)
|
||||
clearInterval(this.refreshInterval);
|
||||
},
|
||||
resizeResults () {
|
||||
if (this.$refs.resultTable) {
|
||||
const el = this.$refs.tableWrapper.parentElement;
|
||||
|
||||
if (el) {
|
||||
const size = el.offsetHeight;
|
||||
this.resultsSize = size;
|
||||
}
|
||||
this.$refs.resultTable.updateWindow();
|
||||
}
|
||||
},
|
||||
refreshScroller () {
|
||||
this.resizeResults();
|
||||
},
|
||||
sort (field) {
|
||||
if (field === this.currentSort) {
|
||||
if (this.currentSortDir === 'asc')
|
||||
this.currentSortDir = 'desc';
|
||||
else
|
||||
this.resetSort();
|
||||
}
|
||||
else {
|
||||
this.currentSortDir = 'asc';
|
||||
this.currentSort = field;
|
||||
}
|
||||
},
|
||||
resetSort () {
|
||||
this.currentSort = '';
|
||||
this.currentSortDir = 'asc';
|
||||
},
|
||||
stopRefresh () {
|
||||
this.autorefreshTimer = 0;
|
||||
this.clearRefresh();
|
||||
},
|
||||
contextMenu () {},
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
if (e.key === 'F5')
|
||||
this.getProcessesList();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vscroll {
|
||||
height: 1000px;
|
||||
overflow: auto;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
.column-resizable {
|
||||
&:hover,
|
||||
&:active {
|
||||
resize: horizontal;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table-column-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
|
||||
.result-tabs {
|
||||
background: transparent !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
align-items: flex-start;
|
||||
|
||||
.modal-container {
|
||||
max-width: 75vw;
|
||||
margin-top: 10vh;
|
||||
|
||||
.modal-body {
|
||||
height: 80vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.processes-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
@@ -36,11 +36,17 @@
|
||||
{{ $t('message.restartToInstall') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
|
||||
<input type="checkbox" :checked="allowPrerelease">
|
||||
<i class="form-icon" /> {{ $t('message.includeBetaUpdates') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export default {
|
||||
@@ -48,7 +54,8 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
updateStatus: 'application/getUpdateStatus',
|
||||
downloadPercentage: 'application/getDownloadProgress'
|
||||
downloadPercentage: 'application/getDownloadProgress',
|
||||
allowPrerelease: 'settings/getAllowPrerelease'
|
||||
}),
|
||||
updateMessage () {
|
||||
switch (this.updateStatus) {
|
||||
@@ -70,11 +77,17 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
changeAllowPrerelease: 'settings/changeAllowPrerelease'
|
||||
}),
|
||||
checkForUpdates () {
|
||||
ipcRenderer.send('check-for-updates');
|
||||
},
|
||||
restartToUpdate () {
|
||||
ipcRenderer.send('restart-to-update');
|
||||
},
|
||||
toggleAllowPrerelease () {
|
||||
this.changeAllowPrerelease(!this.allowPrerelease);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
155
src/renderer/components/ProcessesListRow.vue
Normal file
155
src/renderer/components/ProcessesListRow.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="tr" @click="selectRow($event, row._id)">
|
||||
<div
|
||||
v-for="(col, cKey) in row"
|
||||
v-show="cKey !== '_id'"
|
||||
:key="cKey"
|
||||
class="td p-0"
|
||||
tabindex="0"
|
||||
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
|
||||
>
|
||||
<template v-if="cKey !== '_id'">
|
||||
<span
|
||||
v-if="!isInlineEditor[cKey]"
|
||||
class="cell-content px-2"
|
||||
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
|
||||
@dblclick="dblClick(cKey)"
|
||||
>{{ col | cutText }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
v-if="isInfoModal"
|
||||
:confirm-text="$t('word.update')"
|
||||
:cancel-text="$t('word.close')"
|
||||
size="medium"
|
||||
:hide-footer="true"
|
||||
@hide="hideInfoModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<div>
|
||||
<div>
|
||||
<TextEditor
|
||||
:value="row.info || ''"
|
||||
editor-class="textarea-editor"
|
||||
:mode="editorMode"
|
||||
:read-only="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import TextEditor from '@/components/BaseTextEditor';
|
||||
|
||||
export default {
|
||||
name: 'ProcessesListRow',
|
||||
components: {
|
||||
ConfirmModal,
|
||||
TextEditor
|
||||
},
|
||||
filters: {
|
||||
cutText (val) {
|
||||
if (typeof val !== 'string') return val;
|
||||
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
row: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isInlineEditor: {},
|
||||
isInfoModal: false,
|
||||
editorMode: 'sql'
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
watch: {
|
||||
fields () {
|
||||
Object.keys(this.fields).forEach(field => {
|
||||
this.isInlineEditor[field.name] = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isNull (value) {
|
||||
return value === null ? ' is-null' : '';
|
||||
},
|
||||
selectRow (event, row) {
|
||||
this.$emit('select-row', event, row);
|
||||
},
|
||||
openContext (event, payload) {
|
||||
if (this.isEditable) {
|
||||
payload.field = this.fields[payload.field].name;// Ensures field name only
|
||||
this.$emit('contextmenu', event, payload);
|
||||
}
|
||||
},
|
||||
hideInfoModal () {
|
||||
this.isInfoModal = false;
|
||||
},
|
||||
dblClick (col) {
|
||||
if (col !== 'info') return;
|
||||
this.$emit('stop-refresh');
|
||||
this.isInfoModal = true;
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape') {
|
||||
this.isInlineEditor[this.editingField] = false;
|
||||
this.editingField = null;
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.editable-field {
|
||||
margin: 0;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.cell-content {
|
||||
display: block;
|
||||
min-height: 0.8rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.textarea-editor {
|
||||
height: 50vh !important;
|
||||
}
|
||||
|
||||
.editor-field-info {
|
||||
margin-top: 0.4rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.editor-buttons {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -241,6 +241,22 @@ export default {
|
||||
this.$emit('update:value', content);
|
||||
});
|
||||
|
||||
this.editor.on('guttermousedown', e => {
|
||||
const target = e.domEvent.target;
|
||||
if (target.className.indexOf('ace_gutter-cell') === -1)
|
||||
return;
|
||||
if (e.clientX > 25 + target.getBoundingClientRect().left)
|
||||
return;
|
||||
|
||||
const row = e.getDocumentPosition().row;
|
||||
const breakpoints = e.editor.session.getBreakpoints(row, 0);
|
||||
if (typeof breakpoints[row] === typeof undefined)
|
||||
e.editor.session.setBreakpoint(row);
|
||||
else
|
||||
e.editor.session.clearBreakpoint(row);
|
||||
e.stop();
|
||||
});
|
||||
|
||||
if (this.autoFocus) {
|
||||
setTimeout(() => {
|
||||
this.editor.focus();
|
||||
@@ -308,4 +324,21 @@ export default {
|
||||
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight {
|
||||
color: #e0d00c;
|
||||
}
|
||||
|
||||
.ace_gutter-cell.ace_breakpoint {
|
||||
&::before {
|
||||
content: '\F0403';
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 2px;
|
||||
color: $primary-color;
|
||||
display: inline-block;
|
||||
font: normal normal normal 24px/1 "Material Design Icons", sans-serif;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
line-height: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -7,10 +7,34 @@
|
||||
ref="tabWrap"
|
||||
class="tab tab-block column col-12"
|
||||
>
|
||||
<li class="tab-item d-none">
|
||||
<a class="tab-link workspace-tools-link">
|
||||
<li class="tab-item dropdown tools-dropdown">
|
||||
<a
|
||||
class="tab-link workspace-tools-link dropdown-toggle"
|
||||
tabindex="0"
|
||||
:title="$t('word.tools')"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-tools" />
|
||||
</a>
|
||||
<ul 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 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 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>
|
||||
<li
|
||||
v-if="schemaChild"
|
||||
@@ -19,7 +43,7 @@
|
||||
@click="selectTab({uid: workspace.uid, tab: 'prop'})"
|
||||
>
|
||||
<a class="tab-link">
|
||||
<i class="mdi mdi-18px mdi-tune mr-1" />
|
||||
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
|
||||
<span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -114,6 +138,11 @@
|
||||
:connection="connection"
|
||||
/>
|
||||
</div>
|
||||
<ModalProcessesList
|
||||
v-if="isProcessesModal"
|
||||
:connection="connection"
|
||||
@close="hideProcessesModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -129,6 +158,7 @@ import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
|
||||
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
|
||||
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
|
||||
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
|
||||
import ModalProcessesList from '@/components/ModalProcessesList';
|
||||
|
||||
export default {
|
||||
name: 'Workspace',
|
||||
@@ -141,14 +171,16 @@ export default {
|
||||
WorkspacePropsTabTrigger,
|
||||
WorkspacePropsTabRoutine,
|
||||
WorkspacePropsTabFunction,
|
||||
WorkspacePropsTabScheduler
|
||||
WorkspacePropsTabScheduler,
|
||||
ModalProcessesList
|
||||
},
|
||||
props: {
|
||||
connection: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hasWheelEvent: false
|
||||
hasWheelEvent: false,
|
||||
isProcessesModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -234,6 +266,12 @@ export default {
|
||||
closeTab (tUid) {
|
||||
if (this.queryTabs.length === 1) return;
|
||||
this.removeTab({ uid: this.connection.uid, tab: tUid });
|
||||
},
|
||||
showProcessesModal () {
|
||||
this.isProcessesModal = true;
|
||||
},
|
||||
hideProcessesModal () {
|
||||
this.isProcessesModal = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -298,6 +336,48 @@ export default {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.tools-dropdown {
|
||||
.tab-link:focus {
|
||||
color: $primary-color;
|
||||
opacity: 1;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.menu {
|
||||
min-width: 100%;
|
||||
|
||||
.menu-item a {
|
||||
border-radius: 0.1rem;
|
||||
color: inherit;
|
||||
display: block;
|
||||
margin: 0 -0.4rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
background: $bg-color-gray;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
z-index: 9;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&.tools-dropdown + .tab-item {
|
||||
margin-left: 56px;
|
||||
}
|
||||
|
||||
.workspace-tools-link {
|
||||
padding-bottom: 0;
|
||||
padding-top: 0.3rem;
|
||||
|
@@ -138,7 +138,7 @@
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import { mask } from 'vue-the-mask';
|
||||
import { VueMaskDirective } from 'v-mask';
|
||||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
@@ -147,7 +147,7 @@ export default {
|
||||
ConfirmModal
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
mask: VueMaskDirective
|
||||
},
|
||||
props: {
|
||||
localOptions: Object,
|
||||
|
@@ -47,7 +47,8 @@
|
||||
<BaseLoader v-if="isLoading" />
|
||||
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
|
||||
<QueryEditor
|
||||
v-if="isSelected"
|
||||
v-show="isSelected"
|
||||
:key="`${routine}-${_uid}`"
|
||||
ref="queryEditor"
|
||||
:value.sync="localRoutine.sql"
|
||||
:workspace="workspace"
|
||||
|
@@ -118,7 +118,7 @@
|
||||
<BaseLoader v-if="isLoading" />
|
||||
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label>
|
||||
<QueryEditor
|
||||
v-if="isSelected"
|
||||
v-show="isSelected"
|
||||
ref="queryEditor"
|
||||
:value.sync="localScheduler.sql"
|
||||
:workspace="workspace"
|
||||
|
@@ -99,7 +99,7 @@
|
||||
<BaseLoader v-if="isLoading" />
|
||||
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
|
||||
<QueryEditor
|
||||
v-if="isSelected"
|
||||
v-show="isSelected"
|
||||
ref="queryEditor"
|
||||
:value.sync="localTrigger.sql"
|
||||
:workspace="workspace"
|
||||
|
@@ -26,6 +26,13 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="workspace-query-info">
|
||||
<div
|
||||
v-if="results.length"
|
||||
class="d-flex"
|
||||
:title="$t('message.queryDuration')"
|
||||
>
|
||||
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
|
||||
</div>
|
||||
<div v-if="resultsCount">
|
||||
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
|
||||
</div>
|
||||
@@ -42,7 +49,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-query-results column col-12">
|
||||
<div class="workspace-query-results p-relative column col-12">
|
||||
<BaseLoader v-if="isQuering" />
|
||||
<WorkspaceQueryTable
|
||||
v-if="results"
|
||||
v-show="!isQuering"
|
||||
@@ -50,6 +58,7 @@
|
||||
:results="results"
|
||||
:tab-uid="tab.uid"
|
||||
:conn-uid="connection.uid"
|
||||
:is-selected="isSelected"
|
||||
mode="query"
|
||||
@update-field="updateField"
|
||||
@delete-selected="deleteSelected"
|
||||
@@ -61,6 +70,7 @@
|
||||
<script>
|
||||
import Database from '@/ipc-api/Database';
|
||||
import QueryEditor from '@/components/QueryEditor';
|
||||
import BaseLoader from '@/components/BaseLoader';
|
||||
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import tableTabs from '@/mixins/tableTabs';
|
||||
@@ -68,6 +78,7 @@ import tableTabs from '@/mixins/tableTabs';
|
||||
export default {
|
||||
name: 'WorkspaceQueryTab',
|
||||
components: {
|
||||
BaseLoader,
|
||||
QueryEditor,
|
||||
WorkspaceQueryTable
|
||||
},
|
||||
@@ -84,6 +95,7 @@ export default {
|
||||
isQuering: false,
|
||||
results: [],
|
||||
resultsCount: 0,
|
||||
durationsCount: 0,
|
||||
affectedCount: 0,
|
||||
editorHeight: 200
|
||||
};
|
||||
@@ -143,6 +155,7 @@ export default {
|
||||
if (status === 'success') {
|
||||
this.results = Array.isArray(response) ? response : [response];
|
||||
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
|
||||
this.durationsCount += this.results.reduce((acc, curr) => acc + curr.duration, 0);
|
||||
this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0);
|
||||
}
|
||||
else
|
||||
@@ -161,6 +174,7 @@ export default {
|
||||
clearTabData () {
|
||||
this.results = [];
|
||||
this.resultsCount = 0;
|
||||
this.durationsCount = 0;
|
||||
this.affectedCount = 0;
|
||||
},
|
||||
resize (e) {
|
||||
@@ -232,6 +246,10 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-results {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -9,6 +9,7 @@
|
||||
:context-event="contextEvent"
|
||||
:selected-rows="selectedRows"
|
||||
@delete-selected="deleteSelected"
|
||||
@set-null="setNull"
|
||||
@close-context="isContext = false"
|
||||
/>
|
||||
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
|
||||
@@ -41,7 +42,7 @@
|
||||
/>
|
||||
<span>{{ field.alias || field.name }}</span>
|
||||
<i
|
||||
v-if="idSortable && currentSort === field.name || currentSort === `${field.table}.${field.name}`"
|
||||
v-if="isSortable && currentSort === field.name || currentSort === `${field.table}.${field.name}`"
|
||||
class="mdi sort-icon"
|
||||
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
|
||||
/>
|
||||
@@ -64,9 +65,8 @@
|
||||
v-for="row in items"
|
||||
:key="row._id"
|
||||
:row="row"
|
||||
:fields="fields"
|
||||
:fields="fieldsObj"
|
||||
:key-usage="keyUsage"
|
||||
class="tr"
|
||||
:class="{'selected': selectedRows.includes(row._id)}"
|
||||
@select-row="selectRow($event, row._id)"
|
||||
@update-field="updateField($event, row)"
|
||||
@@ -96,9 +96,9 @@ export default {
|
||||
},
|
||||
props: {
|
||||
results: Array,
|
||||
tabUid: [String, Number],
|
||||
connUid: String,
|
||||
mode: String
|
||||
mode: String,
|
||||
isSelected: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -124,7 +124,7 @@ export default {
|
||||
primaryField () {
|
||||
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
|
||||
},
|
||||
idSortable () {
|
||||
isSortable () {
|
||||
return this.fields.every(field => field.name);
|
||||
},
|
||||
isHardSort () {
|
||||
@@ -153,6 +153,37 @@ export default {
|
||||
},
|
||||
keyUsage () {
|
||||
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : [];
|
||||
},
|
||||
fieldsObj () {
|
||||
if (this.sortedResults.length) {
|
||||
const fieldsObj = {};
|
||||
for (const key in this.sortedResults[0]) {
|
||||
if (key === '_id') continue;
|
||||
|
||||
const fieldObj = this.fields.find(field => {
|
||||
let fieldNames = [
|
||||
field.name,
|
||||
field.alias,
|
||||
`${field.table}.${field.name}`,
|
||||
`${field.table}.${field.alias}`,
|
||||
`${field.tableAlias}.${field.name}`,
|
||||
`${field.tableAlias}.${field.alias}`
|
||||
];
|
||||
|
||||
if (field.table)
|
||||
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
|
||||
|
||||
if (field.tableAlias)
|
||||
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
|
||||
|
||||
return fieldNames.includes(key);
|
||||
});
|
||||
|
||||
fieldsObj[key] = fieldObj;
|
||||
}
|
||||
return fieldsObj;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -243,7 +274,7 @@ export default {
|
||||
: [];
|
||||
},
|
||||
resizeResults () {
|
||||
if (this.$refs.resultTable) {
|
||||
if (this.$refs.resultTable && this.isSelected) {
|
||||
const el = this.$refs.tableWrapper;
|
||||
|
||||
if (el) {
|
||||
@@ -258,14 +289,15 @@ export default {
|
||||
this.resizeResults();
|
||||
},
|
||||
updateField (payload, row) {
|
||||
delete row._id;
|
||||
const localRow = Object.assign({}, row);
|
||||
delete localRow._id;
|
||||
|
||||
const params = {
|
||||
primary: this.primaryField.name,
|
||||
schema: this.getSchema(this.resultsetIndex),
|
||||
table: this.getTable(this.resultsetIndex),
|
||||
id: this.getPrimaryValue(row),
|
||||
row,
|
||||
id: this.getPrimaryValue(localRow),
|
||||
localRow,
|
||||
...payload
|
||||
};
|
||||
this.$emit('update-field', params);
|
||||
@@ -284,6 +316,21 @@ export default {
|
||||
};
|
||||
this.$emit('delete-selected', params);
|
||||
},
|
||||
setNull () {
|
||||
const row = this.localResults.find(row => this.selectedRows.includes(row._id));
|
||||
delete row._id;
|
||||
|
||||
const params = {
|
||||
primary: this.primaryField.name,
|
||||
schema: this.getSchema(this.resultsetIndex),
|
||||
table: this.getTable(this.resultsetIndex),
|
||||
id: this.getPrimaryValue(row),
|
||||
row,
|
||||
field: this.selectedCell.field,
|
||||
content: null
|
||||
};
|
||||
this.$emit('update-field', params);
|
||||
},
|
||||
applyUpdate (params) {
|
||||
const { primary, id, field, table, content } = params;
|
||||
|
||||
@@ -333,7 +380,7 @@ export default {
|
||||
this.isContext = true;
|
||||
},
|
||||
sort (field) {
|
||||
if (!this.idSortable) return;
|
||||
if (!this.isSortable) return;
|
||||
|
||||
if (this.mode === 'query')
|
||||
field = `${this.getTable(this.resultsetIndex)}.${field}`;
|
||||
|
@@ -3,8 +3,19 @@
|
||||
:context-event="contextEvent"
|
||||
@close-context="closeContext"
|
||||
>
|
||||
<div
|
||||
v-if="selectedRows.length === 1"
|
||||
class="context-element"
|
||||
@click="setNull"
|
||||
>
|
||||
<span class="d-flex">
|
||||
<i class="mdi mdi-18px mdi-null text-light pr-1" /> {{ $t('message.setNull') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="context-element" @click="showConfirmModal">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}</span>
|
||||
<span class="d-flex">
|
||||
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
@@ -60,6 +71,10 @@ export default {
|
||||
deleteRows () {
|
||||
this.$emit('delete-selected');
|
||||
this.closeContext();
|
||||
},
|
||||
setNull () {
|
||||
this.$emit('set-null');
|
||||
this.closeContext();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -6,15 +6,15 @@
|
||||
:key="cKey"
|
||||
class="td p-0"
|
||||
tabindex="0"
|
||||
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
|
||||
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
|
||||
>
|
||||
<template v-if="cKey !== '_id'">
|
||||
<span
|
||||
v-if="!isInlineEditor[cKey]"
|
||||
class="cell-content px-2"
|
||||
:class="`${isNull(col)} type-${getFieldType(cKey)}`"
|
||||
:class="`${isNull(col)} type-${fields[cKey].type.toLowerCase()}`"
|
||||
@dblclick="editON($event, col, cKey)"
|
||||
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span>
|
||||
>{{ col | typeFormat(fields[cKey].type.toLowerCase(), fields[cKey].length) | cutText }}</span>
|
||||
<ForeignKeySelect
|
||||
v-else-if="isForeignKey(cKey)"
|
||||
class="editable-field"
|
||||
@@ -174,7 +174,7 @@ import { formatBytes } from 'common/libs/formatBytes';
|
||||
import { bufferToBase64 } from 'common/libs/bufferToBase64';
|
||||
import hexToBinary from 'common/libs/hexToBinary';
|
||||
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
|
||||
import { mask } from 'vue-the-mask';
|
||||
import { VueMaskDirective } from 'v-mask';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import TextEditor from '@/components/BaseTextEditor';
|
||||
import ForeignKeySelect from '@/components/ForeignKeySelect';
|
||||
@@ -187,7 +187,7 @@ export default {
|
||||
ForeignKeySelect
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
mask: VueMaskDirective
|
||||
},
|
||||
filters: {
|
||||
formatBytes,
|
||||
@@ -230,10 +230,7 @@ export default {
|
||||
},
|
||||
props: {
|
||||
row: Object,
|
||||
fields: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
fields: Object,
|
||||
keyUsage: Array
|
||||
},
|
||||
data () {
|
||||
@@ -246,6 +243,7 @@ export default {
|
||||
editingContent: null,
|
||||
editingType: null,
|
||||
editingField: null,
|
||||
editingLength: null,
|
||||
editorMode: 'text',
|
||||
contentInfo: {
|
||||
ext: '',
|
||||
@@ -265,7 +263,7 @@ export default {
|
||||
|
||||
if (TIME.includes(this.editingType)) {
|
||||
let timeMask = '##:##:##';
|
||||
const precision = this.getFieldPrecision(this.editingField);
|
||||
const precision = this.fields[this.editingField].length;
|
||||
|
||||
for (let i = 0; i < precision; i++)
|
||||
timeMask += i === 0 ? '.#' : '#';
|
||||
@@ -278,7 +276,7 @@ export default {
|
||||
|
||||
if (DATETIME.includes(this.editingType)) {
|
||||
let datetimeMask = '####-##-## ##:##:##';
|
||||
const precision = this.getFieldPrecision(this.editingField);
|
||||
const precision = this.fields[this.editingField].length;
|
||||
|
||||
for (let i = 0; i < precision; i++)
|
||||
datetimeMask += i === 0 ? '.#' : '#';
|
||||
@@ -302,15 +300,15 @@ export default {
|
||||
},
|
||||
isEditable () {
|
||||
if (this.fields) {
|
||||
const nElements = this.fields.reduce((acc, curr) => {
|
||||
acc.add(curr.table);
|
||||
acc.add(curr.schema);
|
||||
const nElements = Object.keys(this.fields).reduce((acc, curr) => {
|
||||
acc.add(this.fields[curr].table);
|
||||
acc.add(this.fields[curr].schema);
|
||||
return acc;
|
||||
}, new Set([]));
|
||||
|
||||
if (nElements.size > 2) return false;
|
||||
|
||||
return !!(this.fields[0].schema && this.fields[0].table);
|
||||
return !!(this.fields[Object.keys(this.fields)[0]].schema && this.fields[Object.keys(this.fields)[0]].table);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -318,7 +316,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
fields () {
|
||||
this.fields.forEach(field => {
|
||||
Object.keys(this.fields).forEach(field => {
|
||||
this.isInlineEditor[field.name] = false;
|
||||
});
|
||||
}
|
||||
@@ -330,42 +328,6 @@ export default {
|
||||
|
||||
return this.foreignKeys.includes(key);
|
||||
},
|
||||
getFieldType (cKey) {
|
||||
let type = 'unknown';
|
||||
const field = this.getFieldObj(cKey);
|
||||
if (field)
|
||||
type = field.type;
|
||||
|
||||
return type.toLowerCase();
|
||||
},
|
||||
getFieldPrecision (cKey) {
|
||||
let length = 0;
|
||||
const field = this.getFieldObj(cKey);
|
||||
if (field)
|
||||
length = field.datePrecision;
|
||||
|
||||
return length;
|
||||
},
|
||||
getFieldObj (cKey) {
|
||||
return this.fields.filter(field => {
|
||||
let fieldNames = [
|
||||
field.name,
|
||||
field.alias,
|
||||
`${field.table}.${field.name}`,
|
||||
`${field.table}.${field.alias}`,
|
||||
`${field.tableAlias}.${field.name}`,
|
||||
`${field.tableAlias}.${field.alias}`
|
||||
];
|
||||
|
||||
if (field.table)
|
||||
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
|
||||
|
||||
if (field.tableAlias)
|
||||
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
|
||||
|
||||
return fieldNames.includes(cKey);
|
||||
})[0];
|
||||
},
|
||||
isNull (value) {
|
||||
return value === null ? ' is-null' : '';
|
||||
},
|
||||
@@ -375,24 +337,27 @@ export default {
|
||||
editON (event, content, field) {
|
||||
if (!this.isEditable) return;
|
||||
|
||||
const type = this.getFieldType(field).toUpperCase(); ;
|
||||
this.originalContent = content;
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
|
||||
const type = this.fields[field].type.toUpperCase(); ;
|
||||
this.originalContent = this.$options.filters.typeFormat(content, type, this.fields[field].length);
|
||||
this.editingType = type;
|
||||
this.editingField = field;
|
||||
this.editingLength = this.fields[field].length;
|
||||
|
||||
if (LONG_TEXT.includes(type)) {
|
||||
this.isTextareaEditor = true;
|
||||
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type);
|
||||
this.editingContent = this.$options.filters.typeFormat(content, type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BLOB.includes(type)) {
|
||||
this.isBlobEditor = true;
|
||||
this.editingContent = this.originalContent || '';
|
||||
this.editingContent = content || '';
|
||||
this.fileToUpload = null;
|
||||
this.willBeDeleted = false;
|
||||
|
||||
if (this.originalContent !== null) {
|
||||
if (content !== null) {
|
||||
const buff = Buffer.from(this.editingContent);
|
||||
if (buff.length) {
|
||||
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
|
||||
@@ -408,7 +373,7 @@ export default {
|
||||
}
|
||||
|
||||
// Inline editable fields
|
||||
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field));
|
||||
this.editingContent = this.originalContent;
|
||||
this.$nextTick(() => { // Focus on input
|
||||
event.target.blur();
|
||||
|
||||
@@ -419,10 +384,12 @@ export default {
|
||||
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
|
||||
},
|
||||
editOFF () {
|
||||
if (!this.editingField) return;
|
||||
|
||||
this.isInlineEditor[this.editingField] = false;
|
||||
let content;
|
||||
if (!BLOB.includes(this.editingType)) {
|
||||
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType)) return;// If not changed
|
||||
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType, this.editingLength)) return;// If not changed
|
||||
content = this.editingContent;
|
||||
}
|
||||
else { // Handle file upload
|
||||
@@ -437,13 +404,14 @@ export default {
|
||||
}
|
||||
|
||||
this.$emit('update-field', {
|
||||
field: this.getFieldObj(this.editingField).name,
|
||||
field: this.fields[this.editingField].name,
|
||||
type: this.editingType,
|
||||
content
|
||||
});
|
||||
|
||||
this.editingType = null;
|
||||
this.editingField = null;
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
hideEditorModal () {
|
||||
this.isTextareaEditor = false;
|
||||
@@ -475,9 +443,6 @@ export default {
|
||||
};
|
||||
this.willBeDeleted = true;
|
||||
},
|
||||
contextMenu (event, cell) {
|
||||
this.$emit('update-field', event, cell);
|
||||
},
|
||||
selectRow (event, row) {
|
||||
this.$emit('select-row', event, row);
|
||||
},
|
||||
@@ -485,6 +450,20 @@ export default {
|
||||
if (keyName.includes('.'))
|
||||
return this.keyUsage.find(key => key.field === keyName.split('.').pop());
|
||||
return this.keyUsage.find(key => key.field === keyName);
|
||||
},
|
||||
openContext (event, payload) {
|
||||
if (this.isEditable) {
|
||||
payload.field = this.fields[payload.field].name;// Ensures field name only
|
||||
this.$emit('contextmenu', event, payload);
|
||||
}
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape') {
|
||||
this.isInlineEditor[this.editingField] = false;
|
||||
this.editingField = null;
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -64,6 +64,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-query-info">
|
||||
<div
|
||||
v-if="results.length"
|
||||
class="d-flex"
|
||||
:title="$t('message.queryDuration')"
|
||||
>
|
||||
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ results[0].duration / 1000 }}s</b>
|
||||
</div>
|
||||
<div v-if="results.length && results[0].rows">
|
||||
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
|
||||
</div>
|
||||
@@ -76,13 +83,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-query-results column col-12">
|
||||
<div class="workspace-query-results p-relative column col-12">
|
||||
<BaseLoader v-if="isQuering" />
|
||||
<WorkspaceQueryTable
|
||||
v-if="results"
|
||||
ref="queryTable"
|
||||
:results="results"
|
||||
:tab-uid="tabUid"
|
||||
:conn-uid="connection.uid"
|
||||
:is-selected="isSelected"
|
||||
mode="table"
|
||||
@update-field="updateField"
|
||||
@delete-selected="deleteSelected"
|
||||
@@ -110,6 +119,7 @@
|
||||
|
||||
<script>
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import BaseLoader from '@/components/BaseLoader';
|
||||
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
|
||||
import ModalNewTableRow from '@/components/ModalNewTableRow';
|
||||
import ModalFakerRows from '@/components/ModalFakerRows';
|
||||
@@ -119,6 +129,7 @@ import tableTabs from '@/mixins/tableTabs';
|
||||
export default {
|
||||
name: 'WorkspaceTableTab',
|
||||
components: {
|
||||
BaseLoader,
|
||||
WorkspaceQueryTable,
|
||||
ModalNewTableRow,
|
||||
ModalFakerRows
|
||||
|
@@ -97,7 +97,10 @@ module.exports = {
|
||||
content: 'Content',
|
||||
cut: 'Cut',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste'
|
||||
paste: 'Paste',
|
||||
tools: 'Tools',
|
||||
variables: 'Variables',
|
||||
processes: 'Processes'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Welcome to Antares SQL Client!',
|
||||
@@ -190,7 +193,13 @@ module.exports = {
|
||||
tableFiller: 'Table Filler',
|
||||
fakeDataLanguage: 'Fake data language',
|
||||
searchForElements: 'Search for elements',
|
||||
selectAll: 'Select all'
|
||||
selectAll: 'Select all',
|
||||
queryDuration: 'Query duration',
|
||||
includeBetaUpdates: 'Include beta updates',
|
||||
setNull: 'Set NULL',
|
||||
processesList: 'Processes list',
|
||||
processInfo: 'Process info',
|
||||
manageUsers: 'Manage users'
|
||||
},
|
||||
faker: {
|
||||
address: 'Address',
|
||||
|
@@ -38,6 +38,10 @@ export default class {
|
||||
return ipcRenderer.invoke('get-version', uid);
|
||||
}
|
||||
|
||||
static getProcesses (uid) {
|
||||
return ipcRenderer.invoke('get-processes', uid);
|
||||
}
|
||||
|
||||
static useSchema (params) {
|
||||
return ipcRenderer.invoke('use-schema', params);
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@
|
||||
"double": $number-color,
|
||||
"decimal": $number-color,
|
||||
"bigint": $number-color,
|
||||
"newdecimal": $number-color,
|
||||
"datetime": $date-color,
|
||||
"date": $date-color,
|
||||
"time": $date-color,
|
||||
@@ -41,6 +42,7 @@
|
||||
"blob": $blob-color,
|
||||
"tinyblob": $blob-color,
|
||||
"mediumblob": $blob-color,
|
||||
"medium_blob": $blob-color,
|
||||
"longblob": $blob-color,
|
||||
"enum": $enum-color,
|
||||
"set": $enum-color,
|
||||
|
@@ -96,6 +96,10 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.process-row .td:last-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Scrollbars
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
|
@@ -1,7 +1,13 @@
|
||||
'use strict';
|
||||
import Store from 'electron-store';
|
||||
import crypto from 'crypto';
|
||||
import Application from '../../ipc-api/Application';
|
||||
const key = Application.getKey();
|
||||
const key = Application.getKey() || localStorage.getItem('key');
|
||||
|
||||
if (!key)
|
||||
localStorage.setItem('key', crypto.randomBytes(16).toString('hex'));
|
||||
else
|
||||
localStorage.setItem('key', key);
|
||||
|
||||
const persistentStore = new Store({
|
||||
name: 'connections',
|
||||
@@ -12,7 +18,7 @@ export default {
|
||||
namespaced: true,
|
||||
strict: true,
|
||||
state: {
|
||||
connections: persistentStore.get('connections') || []
|
||||
connections: persistentStore.get('connections', [])
|
||||
},
|
||||
getters: {
|
||||
getConnections: state => state.connections,
|
||||
|
@@ -7,16 +7,18 @@ export default {
|
||||
namespaced: true,
|
||||
strict: true,
|
||||
state: {
|
||||
locale: persistentStore.get('locale') || 'en-US',
|
||||
explorebar_size: persistentStore.get('explorebar_size') || null,
|
||||
notifications_timeout: persistentStore.get('notifications_timeout') || 5,
|
||||
auto_complete: persistentStore.get('auto_complete') || true,
|
||||
line_wrap: persistentStore.get('line_wrap') || true,
|
||||
application_theme: persistentStore.get('application_theme') || 'dark',
|
||||
editor_theme: persistentStore.get('editor_theme') || 'twilight'
|
||||
locale: persistentStore.get('locale', 'en-US'),
|
||||
allow_prerelease: persistentStore.get('allow_prerelease', true),
|
||||
explorebar_size: persistentStore.get('explorebar_size', null),
|
||||
notifications_timeout: persistentStore.get('notifications_timeout', 5),
|
||||
auto_complete: persistentStore.get('auto_complete', true),
|
||||
line_wrap: persistentStore.get('line_wrap', true),
|
||||
application_theme: persistentStore.get('application_theme', 'dark'),
|
||||
editor_theme: persistentStore.get('editor_theme', 'twilight')
|
||||
},
|
||||
getters: {
|
||||
getLocale: state => state.locale,
|
||||
getAllowPrerelease: state => state.allow_prerelease,
|
||||
getExplorebarSize: state => state.explorebar_size,
|
||||
getNotificationsTimeout: state => state.notifications_timeout,
|
||||
getAutoComplete: state => state.auto_complete,
|
||||
@@ -30,6 +32,10 @@ export default {
|
||||
i18n.locale = locale;
|
||||
persistentStore.set('locale', state.locale);
|
||||
},
|
||||
SET_ALLOW_PRERELEASE (state, allow) {
|
||||
state.allow_prerelease = allow;
|
||||
persistentStore.set('allow_prerelease', state.allow_prerelease);
|
||||
},
|
||||
SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
|
||||
state.notifications_timeout = timeout;
|
||||
persistentStore.set('notifications_timeout', state.notifications_timeout);
|
||||
@@ -54,6 +60,9 @@ export default {
|
||||
changeLocale ({ commit }, locale) {
|
||||
commit('SET_LOCALE', locale);
|
||||
},
|
||||
changeAllowPrerelease ({ commit }, allow) {
|
||||
commit('SET_ALLOW_PRERELEASE', allow);
|
||||
},
|
||||
updateNotificationsTimeout ({ commit }, timeout) {
|
||||
commit('SET_NOTIFICATIONS_TIMEOUT', timeout);
|
||||
},
|
||||
|
Reference in New Issue
Block a user