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

Compare commits

..

12 Commits

Author SHA1 Message Date
94a53fec6c chore(release): 0.3.6 2021-09-26 11:39:19 +02:00
13aa47cd44 feat: processes list exportation 2021-09-26 11:36:42 +02:00
85f625daf7 feat: copy cell/row or kill connections on context menu from processes list 2021-09-26 11:19:48 +02:00
7de3bb9346 refactor(core): compatibility with electron/remote 2.x 2021-09-21 11:16:07 +02:00
e43a0ba0b4 Merge pull request #102 from Fabio286/dependabot/npm_and_yarn/electron/remote-2.0.1
build(deps): bump @electron/remote from 1.2.2 to 2.0.1
2021-09-21 10:03:35 +02:00
dependabot[bot]
638cd4bfb7 build(deps): bump @electron/remote from 1.2.2 to 2.0.1
Bumps [@electron/remote](https://github.com/electron/remote) from 1.2.2 to 2.0.1.
- [Release notes](https://github.com/electron/remote/releases)
- [Changelog](https://github.com/electron/remote/blob/main/.releaserc.json)
- [Commits](https://github.com/electron/remote/compare/v1.2.2...v2.0.1)

---
updated-dependencies:
- dependency-name: "@electron/remote"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-20 19:06:45 +00:00
3959333662 feat: workspace query history 2021-09-17 18:32:28 +02:00
abd46aa322 perf(core): better communication of internal exceptions 2021-09-15 15:31:57 +02:00
d4888ad8fb fix: adding a connection default values not change when switching clients, closes #101 2021-09-15 11:08:00 +02:00
6bfc229b77 chore(release): 0.3.5 2021-09-13 15:03:02 +02:00
d31b051f4b chore: update dependencies 2021-09-13 15:02:28 +02:00
95b60df8fc fix(MySQL): connections stuck at startup if 5 or more tabs are restored 2021-09-13 10:03:27 +02:00
21 changed files with 707 additions and 84 deletions

View File

@@ -2,6 +2,32 @@
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.6](https://github.com/Fabio286/antares/compare/v0.3.5...v0.3.6) (2021-09-26)
### Features
* copy cell/row or kill connections on context menu from processes list ([85f625d](https://github.com/Fabio286/antares/commit/85f625daf7026815dac6223a29c5a6479830edbb))
* processes list exportation ([13aa47c](https://github.com/Fabio286/antares/commit/13aa47cd4441aa47c93038dbd91d6a0e54f6a60c))
* workspace query history ([3959333](https://github.com/Fabio286/antares/commit/39593336626e6d9f3d3b65d2a4081388900e37d6))
### Bug Fixes
* adding a connection default values not change when switching clients, closes [#101](https://github.com/Fabio286/antares/issues/101) ([d4888ad](https://github.com/Fabio286/antares/commit/d4888ad8fba3c8e8ec2d6b6d9a78bb212d83eeed))
### Improvements
* **core:** better communication of internal exceptions ([abd46aa](https://github.com/Fabio286/antares/commit/abd46aa32256f822e52eaac2fc698da378b8163f))
### [0.3.5](https://github.com/Fabio286/antares/compare/v0.3.4...v0.3.5) (2021-09-13)
### Bug Fixes
* **MySQL:** connections stuck at startup if 5 or more tabs are restored ([95b60df](https://github.com/Fabio286/antares/commit/95b60df8fc634b96a4c2c5c48dc6b10848888978))
### [0.3.4](https://github.com/Fabio286/antares/compare/v0.3.3...v0.3.4) (2021-09-12)

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.3.4",
"version": "0.3.6",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -87,31 +87,31 @@
}
},
"dependencies": {
"@electron/remote": "^1.2.1",
"@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95",
"ace-builds": "^1.4.12",
"electron-log": "^4.4.1",
"electron-store": "^8.0.0",
"electron-updater": "^4.3.9",
"faker": "^5.5.3",
"marked": "^3.0.0",
"marked": "^3.0.3",
"moment": "^2.29.1",
"mysql2": "^2.3.0",
"pg": "^8.7.1",
"pgsql-ast-parser": "^7.2.1",
"source-map-support": "^0.5.16",
"source-map-support": "^0.5.20",
"spectre.css": "^0.5.9",
"sql-formatter": "^4.0.2",
"ssh2-promise": "^0.2.0",
"v-mask": "^2.2.4",
"vue-i18n": "^8.24.4",
"vue-i18n": "^8.25.1",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.4",
"cross-env": "^7.0.2",
"electron": "^14.0.0",
"electron": "^14.0.1",
"electron-builder": "^22.11.7",
"electron-devtools-installer": "^3.2.0",
"electron-webpack": "^2.8.2",
@@ -122,12 +122,12 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.17.0",
"sass": "^1.39.0",
"sass": "^1.40.0",
"sass-loader": "^10.2.0",
"standard-version": "^9.3.1",
"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.20.1",
"stylelint-scss": "^3.21.0",
"vue": "^2.6.14",
"vue-template-compiler": "^2.6.14",
"webpack": "^4.46.0"

View File

@@ -3,9 +3,11 @@
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path';
import Store from 'electron-store';
import * as remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers';
// remoteMain.initialize();
Store.initRenderer();
const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -30,13 +32,14 @@ async function createMainWindow () {
nodeIntegration: true,
contextIsolation: false,
'web-security': false,
enableRemoteModule: true,
spellcheck: false
},
frame: false,
backgroundColor: '#1d1d1d'
});
remoteMain.enable(window.webContents);
try {
if (isDevelopment) { //
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
@@ -97,7 +100,16 @@ else {
app.on('ready', async () => {
mainWindow = await createMainWindow();
Menu.setApplicationMenu(null);
if (isDevelopment)
mainWindow.webContents.openDevTools();
process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error);
});
process.on('unhandledRejection', error => {
mainWindow.webContents.send('unhandled-exception', error);
});
});
}

View File

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

View File

@@ -1176,6 +1176,10 @@ export class MySQLClient extends AntaresCore {
});
}
async killProcess (id) {
return await this.raw(`KILL ${id}`);
}
/**
* CREATE TABLE
*

View File

@@ -1029,6 +1029,10 @@ export class PostgreSQLClient extends AntaresCore {
});
}
async killProcess (id) {
return await this.raw(`SELECT pg_terminate_backend(${id})`);
}
/**
* CREATE TABLE
*

View File

@@ -0,0 +1,282 @@
<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-history mr-1" />
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body p-0 workspace-query-results">
<div
v-if="history.length"
ref="searchForm"
class="form-group has-icon-right p-2 m-0"
>
<input
v-model="searchTerm"
class="form-input"
type="text"
:placeholder="$t('message.searchForQueries')"
>
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
<i
v-else
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-4"
@click="searchTerm = ''"
/>
</div>
<div
v-if="history.length"
ref="tableWrapper"
class="vscroll px-1 "
:style="{'height': resultsSize+'px'}"
>
<div ref="table">
<BaseVirtualScroll
ref="resultTable"
:items="filteredHistory"
:item-height="66"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<div
v-for="query in items"
:key="query.uid"
class="tile my-2"
tabindex="0"
>
<div class="tile-icon">
<i class="mdi mdi-code-tags pr-1" />
</div>
<div class="tile-content">
<div class="tile-title">
<code
class="cut-text"
:title="query.sql"
v-html="highlightWord(query.sql)"
/>
</div>
<div class="tile-bottom-content">
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
<div class="tile-history-buttons">
<button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)">
<i class="mdi mdi-open-in-app pr-1" /> {{ $t('word.select') }}
</button>
<button class="btn btn-link pl-1" @click="copyQuery(query.sql)">
<i class="mdi mdi-content-copy pr-1" /> {{ $t('word.copy') }}
</button>
<button class="btn btn-link pl-1" @click="deleteQuery(query)">
<i class="mdi mdi-delete-forever pr-1" /> {{ $t('word.delete') }}
</button>
</div>
</div>
</div>
</div>
</template>
</BaseVirtualScroll>
</div>
</div>
<div v-else class="empty">
<div class="empty-icon">
<i class="mdi mdi-history mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }}
</p>
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import { mapGetters, mapActions } from 'vuex';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
export default {
name: 'ModalHistory',
components: {
BaseVirtualScroll
},
props: {
connection: Object
},
data () {
return {
resultsSize: 1000,
isQuering: false,
scrollElement: null,
searchTermInterval: null,
searchTerm: '',
localSearchTerm: ''
};
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName',
getHistoryByWorkspace: 'history/getHistoryByWorkspace'
}),
connectionName () {
return this.getConnectionName(this.connection.uid);
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
},
filteredHistory () {
return this.history.filter(q => q.sql.toLowerCase().search(this.searchTerm.toLowerCase()) >= 0);
}
},
watch: {
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => {
this.localSearchTerm = this.searchTerm;
}, 200);
}
},
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.resizeResults();
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',
deleteQueryFromHistory: 'history/deleteQueryFromHistory'
}),
copyQuery (sql) {
navigator.clipboard.writeText(sql);
},
deleteQuery (query) {
this.deleteQueryFromHistory({
workspace: this.connection.uid,
...query
});
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el)
this.resultsSize = el.offsetHeight - this.$refs.searchForm.offsetHeight;
this.$refs.resultTable.updateWindow();
}
},
formatDate (date) {
return moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
},
refreshScroller () {
this.resizeResults();
},
closeModal () {
this.$emit('close');
},
highlightWord (string) {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.tile {
border-radius: $border-radius;
display: flex;
align-items: center;
&:hover,
&:focus {
.tile-content {
.tile-bottom-content {
.tile-history-buttons {
opacity: 1;
}
}
}
}
.tile-icon {
font-size: 1.2rem;
margin-left: 0.3rem;
width: 28px;
}
.tile-content {
padding: 0.3rem;
padding-left: 0.1rem;
max-width: calc(100% - 30px);
code {
max-width: 100%;
display: inline-block;
font-size: 100%;
// color: $primary-color;
opacity: 0.8;
}
.tile-subtitle {
opacity: 0.8;
}
.tile-bottom-content {
display: flex;
justify-content: space-between;
.tile-history-buttons {
opacity: 0;
transition: opacity 0.2s;
button {
font-size: 0.7rem;
height: 1rem;
line-height: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
}
}
}
</style>

View File

@@ -1,5 +1,15 @@
<template>
<div class="modal active">
<ModalProcessesListContext
v-if="isContext"
:context-event="contextEvent"
:selected-row="selectedRow"
:selected-cell="selectedCell"
@copy-cell="copyCell"
@copy-row="copyRow"
@kill-process="killProcess"
@close-context="closeContext"
/>
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
@@ -12,34 +22,55 @@
<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"
>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
<span>{{ $t('word.refresh') }}</span>
</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 class="workspace-query-buttons">
<div class="dropdown pr-1">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
title="F5"
@click="getProcessesList"
>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
<span>{{ $t('word.refresh') }}</span>
</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="dropdown table-dropdown">
<button
:disabled="isQuering"
class="btn btn-dark btn-sm dropdown-toggle d-flex mr-0 pr-0"
tabindex="0"
>
<i class="mdi mdi-24px mdi-file-export mr-1" />
<span>{{ $t('word.export') }}</span>
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
<li class="menu-item">
<a class="c-hand" @click="downloadTable('json')">JSON</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
</ul>
</div>
</div>
<div class="workspace-query-info">
<div v-if="sortedResults.length">
@@ -85,9 +116,10 @@
<template slot-scope="{ items }">
<ModalProcessesListRow
v-for="row in items"
:key="row._id"
:key="row.id"
class="process-row"
:row="row"
@select-row="selectRow(row.id)"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
@@ -102,15 +134,18 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import arrayToFile from '../libs/arrayToFile';
import Schema from '@/ipc-api/Schema';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ModalProcessesListRow from '@/components/ModalProcessesListRow';
import ModalProcessesListContext from '@/components/ModalProcessesListContext';
export default {
name: 'ModalProcessesList',
components: {
BaseVirtualScroll,
ModalProcessesListRow
ModalProcessesListRow,
ModalProcessesListContext
},
props: {
connection: Object
@@ -119,8 +154,12 @@ export default {
return {
resultsSize: 1000,
isQuering: false,
isContext: false,
autorefreshTimer: 0,
refreshInterval: null,
contextEvent: null,
selectedCell: null,
selectedRow: null,
results: [],
fields: [],
currentSort: '',
@@ -245,10 +284,55 @@ export default {
this.autorefreshTimer = 0;
this.clearRefresh();
},
contextMenu () {},
selectRow (row) {
this.selectedRow = row;
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.stopRefresh();
this.selectedCell = cell;
this.selectedRow = cell.id;
this.contextEvent = event;
this.isContext = true;
},
async killProcess () {
try { // Table data
const { status, response } = await Schema.killProcess({ uid: this.connection.uid, pid: this.selectedRow });
if (status === 'success')
this.getProcessesList();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
closeContext () {
this.isContext = false;
},
copyCell () {
const row = this.results.find(row => row.id === this.selectedRow);
const valueToCopy = row[this.selectedCell.field];
navigator.clipboard.writeText(valueToCopy);
},
copyRow () {
const row = this.results.find(row => row.id === this.selectedRow);
const rowToCopy = JSON.parse(JSON.stringify(row));
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
},
closeModal () {
this.$emit('close');
},
downloadTable (format) {
if (!this.sortedResults) return;
arrayToFile({
type: format,
content: this.sortedResults,
filename: 'processes'
});
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')

View File

@@ -0,0 +1,75 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div v-if="selectedRow" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
v-if="selectedRow"
class="context-element"
@click="copyCell"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ $tc('word.cell', 1) }}
</span>
</div>
<div
v-if="selectedRow"
class="context-element"
@click="copyRow"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ $tc('word.row', 1) }}
</span>
</div>
</div>
</div>
<div
v-if="selectedRow"
class="context-element"
@click="killProcess"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-close-circle-outline text-light pr-1" /> {{ $t('message.killProcess') }}
</span>
</div>
</BaseContextMenu>
</template>
<script>
import BaseContextMenu from '@/components/BaseContextMenu';
export default {
name: 'ModalProcessesListContext',
components: {
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedRow: Number,
selectedCell: Object
},
computed: {
},
methods: {
closeContext () {
this.$emit('close-context');
},
copyCell () {
this.$emit('copy-cell');
this.closeContext();
},
copyRow () {
this.$emit('copy-row');
this.closeContext();
},
killProcess () {
this.$emit('kill-process');
this.closeContext();
}
}
};
</script>

View File

@@ -1,21 +1,18 @@
<template>
<div class="tr" @click="selectRow($event, row._id)">
<div class="tr" @click="selectRow()">
<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 })"
@contextmenu.prevent="openContext($event, { id: row.id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</template>
<span
v-if="!isInlineEditor[cKey]"
class="cell-content"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</div>
<ConfirmModal
v-if="isInfoModal"
@@ -73,25 +70,15 @@ export default {
};
},
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);
selectRow () {
this.$emit('select-row');
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
this.$emit('contextmenu', event, payload);
},
hideInfoModal () {
this.isInfoModal = false;

View File

@@ -393,6 +393,13 @@ export default {
return this.isConnecting || this.isTesting;
}
},
watch: {
'connection.client' () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
}
},
created () {
this.setDefaults();

View File

@@ -5,7 +5,8 @@
tabindex="0"
@keydown.116="runQuery(query)"
@keydown.ctrl.87="clear"
@keydown.ctrl.119="beautify"
@keydown.ctrl.66="beautify"
@keydown.ctrl.71="openHistoryModal"
>
<div class="workspace-query-runner column col-12">
<QueryEditor
@@ -32,16 +33,7 @@
<span>{{ $t('word.run') }}</span>
</button>
<button
class="btn btn-dark btn-sm"
:disabled="!query || isQuering"
title="CTRL+F8"
@click="beautify()"
>
<i class="mdi mdi-24px mdi-brush pr-1" />
<span>{{ $t('word.format') }}</span>
</button>
<button
class="btn btn-link btn-sm"
class="btn btn-link btn-sm mr-0"
:disabled="!query || isQuering"
title="CTRL+W"
@click="clear()"
@@ -52,6 +44,24 @@
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm"
:disabled="!query || isQuering"
title="CTRL+B"
@click="beautify()"
>
<i class="mdi mdi-24px mdi-brush pr-1" />
<span>{{ $t('word.format') }}</span>
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isQuering"
title="CTRL+G"
@click="openHistoryModal()"
>
<i class="mdi mdi-24px mdi-history pr-1" />
<span>{{ $t('word.history') }}</span>
</button>
<div class="dropdown table-dropdown pr-2">
<button
:disabled="!results.length || isQuering"
@@ -116,17 +126,24 @@
@delete-selected="deleteSelected"
/>
</div>
<ModalHistory
v-if="isHistoryOpen"
:connection="connection"
@select-query="selectQuery"
@close="isHistoryOpen = false"
/>
</div>
</template>
<script>
import { format } from 'sql-formatter';
import { mapGetters, mapActions } from 'vuex';
import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable';
import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyState';
import { mapGetters, mapActions } from 'vuex';
import ModalHistory from '@/components/ModalHistory';
import tableTabs from '@/mixins/tableTabs';
export default {
@@ -135,7 +152,8 @@ export default {
BaseLoader,
QueryEditor,
WorkspaceTabQueryTable,
WorkspaceTabQueryEmptyState
WorkspaceTabQueryEmptyState,
ModalHistory
},
mixins: [tableTabs],
props: {
@@ -153,13 +171,15 @@ export default {
resultsCount: 0,
durationsCount: 0,
affectedCount: 0,
editorHeight: 200
editorHeight: 200,
isHistoryOpen: false
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
selectedWorkspace: 'workspaces/getSelected',
getHistoryByWorkspace: 'history/getHistoryByWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
@@ -175,6 +195,9 @@ export default {
},
isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace;
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
}
},
watch: {
@@ -189,7 +212,6 @@ export default {
created () {
this.query = this.tab.content;
this.selectedSchema = this.tab.schema || this.breadcrumbsSchema;
// this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
window.addEventListener('keydown', this.onKey);
},
@@ -213,7 +235,8 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
updateTabContent: 'workspaces/updateTabContent'
updateTabContent: 'workspaces/updateTabContent',
saveHistory: 'history/saveHistory'
}),
async runQuery (query) {
if (!query || this.isQuering) return;
@@ -236,7 +259,14 @@ export default {
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);
this.updateTabContent({ uid: this.connection.uid, tab: this.tab.uid, type: 'query', schema: this.selectedSchema, content: query });
this.updateTabContent({
uid: this.connection.uid,
tab: this.tab.uid,
type: 'query',
schema: this.selectedSchema,
content: query
});
this.saveHistory(params);
}
else
this.addNotification({ status: 'error', message: response });
@@ -295,6 +325,15 @@ export default {
this.$refs.queryEditor.editor.session.setValue(formattedQuery);
}
},
openHistoryModal () {
this.isHistoryOpen = true;
},
selectQuery (sql) {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue(sql);
this.isHistoryOpen = false;
},
clear () {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue('');

View File

@@ -11,17 +11,23 @@
<div class="mb-4">
{{ $t('word.clear') }}
</div>
<div class="mb-4">
{{ $t('word.history') }}
</div>
</div>
<div class="column col-16">
<div class="mb-4">
<code>F5</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>F8</code>
<code>CTRL</code> + <code>B</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>W</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>G</code>
</div>
</div>
</div>
</div>

View File

@@ -287,7 +287,7 @@ export default {
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async getTableData () {
if (!this.table) return;
if (!this.table || !this.isSelected) return;
this.isQuering = true;
// if table changes clear cached values

View File

@@ -117,7 +117,9 @@ module.exports = {
all: 'All',
duplicate: 'Duplicate',
routine: 'Routine',
new: 'New'
new: 'New',
history: 'History',
select: 'Select'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -238,7 +240,10 @@ module.exports = {
newRoutine: 'New routine',
newFunction: 'New function',
newScheduler: 'New scheduler',
newTriggerFunction: 'New trigger function'
newTriggerFunction: 'New trigger function',
thereIsNoQueriesYet: 'There is no queries yet',
searchForQueries: 'Search for queries',
killProcess: 'Kill process'
},
faker: {
address: 'Address',

View File

@@ -42,6 +42,10 @@ export default class {
return ipcRenderer.invoke('get-processes', uid);
}
static killProcess (params) {
return ipcRenderer.invoke('kill-process', params);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}

View File

@@ -283,6 +283,12 @@
}
.tile {
transition: background 0.2s;
&:focus {
background: rgba($bg-color-light-dark, 60%);
}
&:hover {
background: $bg-color-light-dark;
}

View File

@@ -79,6 +79,12 @@
}
.tile {
transition: background 0.2s;
&:focus {
background: rgba($bg-color-light-gray, 70%);
}
&:hover {
background: $bg-color-light-gray;
}

View File

@@ -5,12 +5,14 @@ import Vuex from 'vuex';
import application from './modules/application.store';
import settings from './modules/settings.store';
import history from './modules/history.store';
import scratchpad from './modules/scratchpad.store';
import connections from './modules/connections.store';
import workspaces from './modules/workspaces.store';
import notifications from './modules/notifications.store';
import ipcUpdates from './plugins/ipcUpdates';
import ipcExceptions from './plugins/ipcExceptions';
Vue.use(Vuex);
@@ -19,12 +21,14 @@ export default new Vuex.Store({
modules: {
application,
settings,
history,
scratchpad,
connections,
workspaces,
notifications
},
plugins: [
ipcUpdates
ipcUpdates,
ipcExceptions
]
});

View File

@@ -0,0 +1,54 @@
'use strict';
import Store from 'electron-store';
import { uidGen } from 'common/libs/uidGen';
const persistentStore = new Store({ name: 'history' });
const historySize = 1000;
export default {
namespaced: true,
strict: true,
state: {
history: persistentStore.get('history', {}),
favorites: persistentStore.get('favorites', {})
},
getters: {
getHistoryByWorkspace: state => uid => state.history[uid]
},
mutations: {
SET_HISTORY (state, args) {
if (!(args.uid in state.history))
state.history[args.uid] = [];
state.history[args.uid] = [
{
uid: uidGen('H'),
sql: args.query,
date: new Date(),
schema: args.schema
},
...state.history[args.uid]
];
if (state.history[args.uid].length > historySize)
state.history[args.uid] = state.history[args.uid].slice(0, historySize);
persistentStore.set('history', state.history);
},
DELETE_QUERY_FROM_HISTORY (state, query) {
state.history[query.workspace] = state.history[query.workspace].filter(q => q.uid !== query.uid);
persistentStore.set('history', state.history);
}
},
actions: {
saveHistory ({ commit, getters }, args) {
if (getters.getHistoryByWorkspace(args.uid) &&
getters.getHistoryByWorkspace(args.uid).length &&
getters.getHistoryByWorkspace(args.uid)[0].sql === args.query
) return;
commit('SET_HISTORY', args);
},
deleteQueryFromHistory ({ commit }, query) {
commit('DELETE_QUERY_FROM_HISTORY', query);
}
}
};

View File

@@ -0,0 +1,7 @@
import { ipcRenderer } from 'electron';
export default store => {
ipcRenderer.on('unhandled-exception', (event, error) => {
store.dispatch('notifications/addNotification', { status: 'error', message: error.message });
});
};