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

feat: workspace query history

This commit is contained in:
2021-09-17 18:32:28 +02:00
parent abd46aa322
commit 3959333662
8 changed files with 419 additions and 22 deletions

View File

@ -4,7 +4,7 @@ import { PostgreSQLClient } from './clients/PostgreSQLClient';
const queryLogger = sql => { const queryLogger = sql => {
// Remove comments, newlines and multiple spaces // Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' '); const escapedSql = sql.replace(/(\/\*(.|[\r\n|\n|\r])*?\*\/)|(--(.*|[\r\n|\n|\r]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql); console.log(escapedSql);
}; };

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

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

View File

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

View File

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

View File

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

View File

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

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);
}
}
};