feat(MySQL): ability to cancel queries

This commit is contained in:
Fabio Di Stasio 2021-12-19 11:59:09 +01:00
parent e7a1858091
commit a59f77f618
10 changed files with 149 additions and 19 deletions

View File

@ -1,6 +1,7 @@
{ {
"extends": [ "extends": [
"stylelint-config-standard" "stylelint-config-standard",
"stylelint-config-standard-scss"
], ],
"fix": true, "fix": true,
"formatter": "verbose", "formatter": "verbose",

View File

@ -11,6 +11,7 @@ module.exports = {
sslConnection: false, sslConnection: false,
sshConnection: false, sshConnection: false,
fileConnection: false, fileConnection: false,
cancelQueries: false,
// Tools // Tools
processesList: false, processesList: false,
usersManagement: false, usersManagement: false,

View File

@ -12,6 +12,7 @@ module.exports = {
engines: true, engines: true,
sslConnection: true, sslConnection: true,
sshConnection: true, sshConnection: true,
cancelQueries: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure

View File

@ -135,7 +135,7 @@ export default connections => {
} }
}); });
ipcMain.handle('raw-query', async (event, { uid, query, schema }) => { ipcMain.handle('raw-query', async (event, { uid, query, schema, tabUid }) => {
if (!query) return; if (!query) return;
try { try {
@ -143,6 +143,7 @@ export default connections => {
nest: true, nest: true,
details: true, details: true,
schema, schema,
tabUid,
comments: false comments: false
}); });
@ -152,4 +153,16 @@ export default connections => {
return { status: 'error', response: err.toString() }; return { status: 'error', response: err.toString() };
} }
}); });
ipcMain.handle('kill-tab-query', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].killTabQuery(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
}; };

View File

@ -9,6 +9,7 @@ export class MySQLClient extends AntaresCore {
super(args); super(args);
this._schema = null; this._schema = null;
this._runningConnections = new Map();
this.types = { this.types = {
0: 'DECIMAL', 0: 'DECIMAL',
@ -1210,10 +1211,26 @@ export class MySQLClient extends AntaresCore {
}); });
} }
/**
*
* @param {number} id
* @returns {Promise<null>}
*/
async killProcess (id) { async killProcess (id) {
return await this.raw(`KILL ${id}`); return await this.raw(`KILL ${id}`);
} }
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async killTabQuery (tabUid) {
const id = this._runningConnections.get(tabUid);
if (id)
return await this.killProcess(id);
}
/** /**
* CREATE TABLE * CREATE TABLE
* *
@ -1535,6 +1552,9 @@ export class MySQLClient extends AntaresCore {
const isPool = typeof this._connection.getConnection === 'function'; const isPool = typeof this._connection.getConnection === 'function';
const connection = isPool ? await this._connection.getConnection() : this._connection; const connection = isPool ? await this._connection.getConnection() : this._connection;
if (args.tabUid && isPool)
this._runningConnections.set(args.tabUid, connection.connection.connectionId);
if (args.schema) if (args.schema)
await connection.query(`USE \`${args.schema}\``); await connection.query(`USE \`${args.schema}\``);
@ -1595,7 +1615,10 @@ export class MySQLClient extends AntaresCore {
}); });
} }
catch (err) { catch (err) {
if (isPool) connection.release(); if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err); reject(err);
} }
@ -1604,7 +1627,10 @@ export class MySQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response; keysArr = keysArr ? [...keysArr, ...response] : response;
} }
catch (err) { catch (err) {
if (isPool) connection.release(); if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err); reject(err);
} }
} }
@ -1619,7 +1645,10 @@ export class MySQLClient extends AntaresCore {
keys: keysArr keys: keysArr
}); });
}).catch((err) => { }).catch((err) => {
if (isPool) connection.release(); if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err); reject(err);
}); });
}); });
@ -1627,7 +1656,10 @@ export class MySQLClient extends AntaresCore {
resultsArr.push({ rows, report, fields, keys, duration }); resultsArr.push({ rows, report, fields, keys, duration });
} }
if (isPool) connection.release(); if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr; return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
} }

View File

@ -4,6 +4,7 @@
class="workspace-query-tab column col-12 columns col-gapless no-outline p-0" class="workspace-query-tab column col-12 columns col-gapless no-outline p-0"
tabindex="0" tabindex="0"
@keydown.116="runQuery(query)" @keydown.116="runQuery(query)"
@keydown.75="killTabQuery"
@keydown.ctrl.alt.87="clear" @keydown.ctrl.alt.87="clear"
@keydown.ctrl.66="beautify" @keydown.ctrl.66="beautify"
@keydown.ctrl.71="openHistoryModal" @keydown.ctrl.71="openHistoryModal"
@ -22,16 +23,29 @@
<div ref="resizer" class="query-area-resizer" /> <div ref="resizer" class="query-area-resizer" />
<div class="workspace-query-runner-footer"> <div class="workspace-query-runner-footer">
<div class="workspace-query-buttons"> <div class="workspace-query-buttons">
<button <div @mouseenter="setCancelButtonVisibility(true)" @mouseleave="setCancelButtonVisibility(false)">
class="btn btn-primary btn-sm" <button
:class="{'loading':isQuering}" v-if="showCancel && isQuering"
:disabled="!query" class="btn btn-primary btn-sm cancellable"
title="F5" :disabled="!query"
@click="runQuery(query)" :title="$t('word.cancel')"
> @click="killTabQuery()"
<i class="mdi mdi-24px mdi-play pr-1" /> >
<span>{{ $t('word.run') }}</span> <i class="mdi mdi-24px mdi-window-close" />
</button> <span class="d-invisible pr-1">{{ $t('word.run') }}</span>
</button>
<button
v-else
class="btn btn-primary btn-sm"
:class="{'loading':isQuering}"
:disabled="!query"
title="F5"
@click="runQuery(query)"
>
<i class="mdi mdi-24px mdi-play pr-1" />
<span>{{ $t('word.run') }}</span>
</button>
</div>
<button <button
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:disabled="!query || isQuering" :disabled="!query || isQuering"
@ -110,7 +124,7 @@
</div> </div>
</div> </div>
</div> </div>
<WorkspaceTabQueryEmptyState v-if="!results.length && !isQuering" /> <WorkspaceTabQueryEmptyState v-if="!results.length && !isQuering" :customizations="workspace.customizations" />
<div class="workspace-query-results p-relative column col-12"> <div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" /> <BaseLoader v-if="isQuering" />
<WorkspaceTabQueryTable <WorkspaceTabQueryTable
@ -166,6 +180,8 @@ export default {
query: '', query: '',
lastQuery: '', lastQuery: '',
isQuering: false, isQuering: false,
isCancelling: false,
showCancel: false,
results: [], results: [],
selectedSchema: null, selectedSchema: null,
resultsCount: 0, resultsCount: 0,
@ -248,6 +264,7 @@ export default {
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.selectedSchema, schema: this.selectedSchema,
tabUid: this.tab.uid,
query query
}; };
@ -283,6 +300,29 @@ export default {
this.isQuering = false; this.isQuering = false;
this.lastQuery = query; this.lastQuery = query;
}, },
async killTabQuery () {
if (this.isCancelling) return;
this.isCancelling = true;
try {
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
await Schema.killTabQuery(params);
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isCancelling = false;
},
setCancelButtonVisibility (val) {
if (this.workspace.customizations.cancelQueries)
this.showCancel = val;
},
reloadTable () { reloadTable () {
this.runQuery(this.lastQuery); this.runQuery(this.lastQuery);
}, },

View File

@ -5,6 +5,9 @@
<div class="mb-4"> <div class="mb-4">
{{ $t('message.runQuery') }} {{ $t('message.runQuery') }}
</div> </div>
<div v-if="customizations.cancelQueries" class="mb-4">
{{ $t('message.killQuery') }}
</div>
<div class="mb-4"> <div class="mb-4">
{{ $t('word.format') }} {{ $t('word.format') }}
</div> </div>
@ -25,6 +28,9 @@
<div class="mb-4"> <div class="mb-4">
<code>F5</code> <code>F5</code>
</div> </div>
<div v-if="customizations.cancelQueries" class="mb-4">
<code>CTRL</code> + <code>K</code>
</div>
<div class="mb-4"> <div class="mb-4">
<code>CTRL</code> + <code>B</code> <code>CTRL</code> + <code>B</code>
</div> </div>
@ -47,7 +53,10 @@
<script> <script>
export default { export default {
name: 'WorkspaceTabQueryEmptyState' name: 'WorkspaceTabQueryEmptyState',
props: {
customizations: Object
}
}; };
</script> </script>

View File

@ -251,7 +251,8 @@ module.exports = {
killProcess: 'Kill process', killProcess: 'Kill process',
closeTab: 'Close tab', closeTab: 'Close tab',
goToDownloadPage: 'Go to download page', goToDownloadPage: 'Go to download page',
readOnlyMode: 'Read-only mode' readOnlyMode: 'Read-only mode',
killQuery: 'Kill query'
}, },
faker: { faker: {
address: 'Address', address: 'Address',

View File

@ -46,6 +46,10 @@ export default class {
return ipcRenderer.invoke('kill-process', params); return ipcRenderer.invoke('kill-process', params);
} }
static killTabQuery (params) {
return ipcRenderer.invoke('kill-tab-query', params);
}
static useSchema (params) { static useSchema (params) {
return ipcRenderer.invoke('use-schema', params); return ipcRenderer.invoke('use-schema', params);
} }

View File

@ -59,6 +59,34 @@ option:checked {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.cancellable {
color: transparent !important;
min-height: 0.8rem;
position: relative;
> .mdi,
> .span {
visibility: hidden;
}
&::after {
content: "\2715";
color: $light-color;
font-weight: 700;
top: 36%;
display: block;
height: 0.8rem;
left: 50%;
margin-left: -0.4rem;
margin-top: -0.4rem;
opacity: 1;
padding: 0;
position: absolute;
width: 0.8rem;
z-index: 1;
}
}
.workspace-tabs { .workspace-tabs {
align-content: baseline; align-content: baseline;