1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-02-12 01:30:46 +01:00

feat: in-app debug console, closes #824

This commit is contained in:
Fabio Di Stasio 2024-07-01 18:16:18 +02:00
parent 4a38656b7e
commit 3e223b475e
15 changed files with 1444 additions and 1227 deletions

View File

@ -10,9 +10,7 @@
:key="connection.uid"
:connection="connection"
/>
<div class="connection-panel-wrapper p-relative">
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
<TheFooter />
<TheNotificationsBoard />
@ -48,6 +46,8 @@ import { useSchemaExportStore } from '@/stores/schemaExport';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import { useConsoleStore } from './stores/console';
const { t } = useI18n();
const TheTitleBar = defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar.vue'));
@ -80,6 +80,8 @@ const schemaExportStore = useSchemaExportStore();
const { hideExportModal } = schemaExportStore;
const { isExportModal: isExportSchemaModal } = storeToRefs(schemaExportStore);
const consoleStore = useConsoleStore();
const isAllConnectionsModal: Ref<boolean> = ref(false);
document.addEventListener('DOMContentLoaded', () => {
@ -152,6 +154,60 @@ onMounted(() => {
}
});
});
// Console messages
const oldLog = console.log;
const oldWarn = console.warn;
const oldInfo = console.info;
const oldError = console.error;
console.log = function (...args) {
consoleStore.putLog('debug', {
level: 'log',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldLog.apply(this, args);
};
console.info = function (...args) {
consoleStore.putLog('debug', {
level: 'info',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldInfo.apply(this, args);
};
console.warn = function (...args) {
consoleStore.putLog('debug', {
level: 'warn',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldWarn.apply(this, args);
};
console.error = function (...args) {
consoleStore.putLog('debug', {
level: 'error',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldError.apply(this, args);
};
window.addEventListener('unhandledrejection', (event) => {
console.error(event.reason);
});
window.addEventListener('error', (event) => {
console.error(event.error, '| File name:', event.filename.split('/').pop().split('?')[0]);
});
</script>
<style lang="scss">

View File

@ -365,7 +365,11 @@ export default defineComponent({
};
const handleWheelEvent = (e) => {
if (!e.target.className.includes('select__')) deactivate();
try {
if (!e.target.className.includes('select__')) deactivate();
}
catch (_) {
}
};
onMounted(() => {

View File

@ -0,0 +1,285 @@
<template>
<div
ref="wrapper"
class="console-wrapper"
@mouseenter="isHover = true"
@mouseleave="isHover = false"
>
<div ref="resizer" class="console-resizer" />
<div
id="console"
ref="queryConsole"
class="console column col-12"
:style="{height: localHeight ? localHeight+'px' : ''}"
>
<div class="console-header">
<ul class="tab tab-block">
<li class="tab-item" :class="{'active': selectedTab === 'query'}">
<a class="tab-link" @click="selectedTab = 'query'">{{ t('application.executedQueries') }}</a>
</li>
<li class="tab-item" :class="{'active': selectedTab === 'debug'}">
<a class="tab-link" @click="selectedTab = 'debug'">{{ t('application.debugConsole') }}</a>
</li>
</ul>
<button class="btn btn-clear mr-1" @click="resizeConsole(0)" />
</div>
<div
v-show="selectedTab === 'query'"
ref="queryConsoleBody"
class="console-body"
>
<div
v-for="(wLog, i) in workspaceQueryLogs"
:key="i"
class="console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)"
>
<span class="console-log-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="console-log-sql" v-html="highlight(wLog.sql, {html: true})" />
</div>
</div>
<div
v-show="selectedTab === 'debug'"
ref="logConsoleBody"
class="console-body"
>
<div
v-for="(log, i) in debugLogs"
:key="i"
class="console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, log)"
>
<span class="console-log-datetime">{{ moment(log.date).format('HH:mm:ss') }}</span> <small>[{{ log.process.substring(0, 1).toUpperCase() }}]</small>: <span class="console-log-message" :class="`console-log-level-${log.level}`">{{ log.message }}</span>
</div>
</div>
</div>
</div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="copyLog">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copy') }}</span>
</div>
</BaseContextMenu>
</template>
<script setup lang="ts">
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import { copyText } from '@/libs/copyText';
import { useConsoleStore } from '@/stores/console';
const { t } = useI18n();
const consoleStore = useConsoleStore();
const { resizeConsole, getLogsByWorkspace } = consoleStore;
const {
isConsoleOpen,
consoleHeight,
selectedTab,
debugLogs
} = storeToRefs(consoleStore);
const props = defineProps({
uid: {
type: String,
default: null,
required: false
}
});
const wrapper: Ref<HTMLInputElement> = ref(null);
const queryConsole: Ref<HTMLInputElement> = ref(null);
const queryConsoleBody: Ref<HTMLInputElement> = ref(null);
const logConsoleBody: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
const localHeight = ref(consoleHeight.value);
const isHover = ref(false);
const isContext = ref(false);
const contextContent: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const resize = (e: MouseEvent) => {
const el = queryConsole.value;
let elementHeight = el.getBoundingClientRect().bottom - e.pageY;
if (elementHeight > 400) elementHeight = 400;
localHeight.value = elementHeight;
};
const workspaceQueryLogs = computed(() => {
return getLogsByWorkspace(props.uid);
});
const stopResize = () => {
if (localHeight.value < 0) localHeight.value = 0;
resizeConsole(localHeight.value);
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResize);
};
const contextMenu = (event: MouseEvent, wLog: {date: Date; sql?: string; message?: string}) => {
contextEvent.value = event;
contextContent.value = wLog.sql || wLog.message;
isContext.value = true;
};
const copyLog = () => {
copyText(contextContent.value);
isContext.value = false;
};
watch(workspaceQueryLogs, async () => {
if (!isHover.value) {
await nextTick();
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
}
});
watch(() => debugLogs.value.length, async () => {
if (!isHover.value) {
await nextTick();
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
}
});
watch(isConsoleOpen, async () => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
});
watch(selectedTab, async () => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
});
watch(consoleHeight, async (val) => {
await nextTick();
localHeight.value = val;
});
onMounted(() => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
});
</script>
<style lang="scss" scoped>
.console-wrapper {
width: -webkit-fill-available;
z-index: 9;
margin-top: auto;
position: absolute;
bottom: 0;
.console-resizer {
height: 4px;
top: -1px;
width: 100%;
cursor: ns-resize;
position: absolute;
z-index: 99;
transition: background 0.2s;
&:hover {
background: var(--primary-color-dark);
}
}
.console {
padding: 0;
padding-bottom: $footer-height;
.console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px;
.tab-block {
margin-top: 0;
margin-bottom: 0;
}
.tab-block,
.tab-item {
background-color: transparent;
}
.tab-link {
padding: 0.2rem 0.6rem;
cursor: pointer;
white-space: nowrap;
}
}
.console-body {
overflow: auto;
display: flex;
flex-direction: column;
max-height: 100%;
padding: 0 6px 3px;
.console-log {
padding: 1px 3px;
margin: 1px 0;
border-radius: $border-radius;
user-select: text;
&-datetime {
opacity: .6;
font-size: 90%;
}
&-sql {
font-size: 95%;
opacity: 0.8;
font-weight: 700;
&:hover {
user-select: text;
}
}
&-message {
font-size: 95%;
}
&-level {
// &-log,
// &-info {}
&-warn {
color: orange;
}
&-error {
color: red;
}
}
small {
opacity: .6;
}
}
}
}
}
</style>

View File

@ -282,7 +282,7 @@
import { ClientCode, SchemaInfos } from 'common/interfaces/antares';
import { Customizations } from 'common/interfaces/customizations';
import { ExportOptions, ExportState } from 'common/interfaces/exporter';
import { ipcRenderer } from 'electron';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
@ -293,6 +293,7 @@ import BaseSelect from '@/components/BaseSelect.vue';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useSchemaExportStore } from '@/stores/schemaExport';
import { useWorkspacesStore } from '@/stores/workspaces';
@ -384,16 +385,28 @@ const startExport = async () => {
else {
progressStatus.value = response;
addNotification({ status: 'error', message: response });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: response,
date: new Date()
});
}
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: err.stack,
date: new Date()
});
}
isExporting.value = false;
};
const updateProgress = (event: Event, state: ExportState) => {
const updateProgress = (event: IpcRendererEvent, state: ExportState) => {
progressPercentage.value = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':

View File

@ -55,7 +55,7 @@
<script setup lang="ts">
import { ImportState } from 'common/interfaces/importer';
import { ipcRenderer } from 'electron';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
@ -63,6 +63,7 @@ import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import Schema from '@/ipc-api/Schema';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
@ -118,23 +119,35 @@ const startImport = async (file: string) => {
else {
progressStatus.value = response;
addNotification({ status: 'error', message: response });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: response,
date: new Date()
});
}
refreshSchema({ uid, schema: props.selectedSchema });
completed.value = true;
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: err.stack,
date: new Date()
});
}
isImporting.value = false;
};
const updateProgress = (event: Event, state: ImportState) => {
const updateProgress = (event: IpcRendererEvent, state: ImportState) => {
progressPercentage.value = parseFloat(Number(state.percentage).toFixed(1));
queryCount.value = Number(state.queryCount);
};
const handleQueryError = (event: Event, err: { time: string; message: string }) => {
const handleQueryError = (event: IpcRendererEvent, err: { time: string; message: string }) => {
queryErrors.value.push(err);
};

View File

@ -43,11 +43,7 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li
v-if="workspace?.connectionStatus === 'connected'"
class="footer-element footer-link"
@click="toggleConsole()"
>
<li class="footer-element footer-link" @click="toggleConsole()">
<BaseIcon
icon-name="mdiConsoleLine"
class="mr-1"

File diff suppressed because it is too large Load Diff

View File

@ -1,440 +1,446 @@
<template>
<div class="connection-panel">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ t('application.general') }}</a>
</li>
<li
v-if="clientCustomizations.sslConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="tab-link">{{ t('connection.ssl') }}</a>
</li>
<li
v-if="clientCustomizations.sshConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="tab-link">{{ t('connection.sshTunnel') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionName') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.client') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseSelect
v-model="connection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select"
/>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionString') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="pgString"
v-model="connection.pgConnString"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.databasePath"
:message="t('general.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
:placeholder="clientCustomizations.defaultDatabase"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.schema') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.schema"
class="form-input"
type="text"
:placeholder="t('general.all')"
>
</div>
</div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label>
</div>
</div>
<div v-if="clientCustomizations.singleConnectionMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
<div class="connection-panel-wrapper p-relative">
<div class="connection-panel">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ t('application.general') }}</a>
</li>
<li
v-if="clientCustomizations.sslConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="tab-link">{{ t('connection.ssl') }}</a>
</li>
<li
v-if="clientCustomizations.sshConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="tab-link">{{ t('connection.sshTunnel') }}</a>
</li>
</ul>
</div>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsl') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.key"
:message="t('general.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.certificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.cert"
:message="t('general.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.caCertificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.ca"
:message="t('general.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.ciphers') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('connection.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsh') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.sshKey"
:message="t('general.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.passphrase') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPassphrase"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.keepAliveInterval') }}</label>
</div>
<div class="column col-7 col-sm-12">
<div class="input-group">
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionName') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshKeepAliveInterval"
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.client') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseSelect
v-model="connection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select"
/>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionString') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="pgString"
v-model="connection.pgConnString"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.databasePath"
:message="t('general.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
<span class="input-group-addon">{{ t('general.seconds') }}</span>
</div>
</div>
<div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
:placeholder="clientCustomizations.defaultDatabase"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.schema') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.schema"
class="form-input"
type="text"
:placeholder="t('general.all')"
>
</div>
</div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label>
</div>
</div>
<div v-if="clientCustomizations.singleConnectionMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsl') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
</fieldset>
</form>
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.key"
:message="t('general.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.certificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.cert"
:message="t('general.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.caCertificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.ca"
:message="t('general.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.ciphers') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('connection.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsh') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.sshKey"
:message="t('general.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.passphrase') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPassphrase"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.keepAliveInterval') }}</label>
</div>
<div class="column col-7 col-sm-12">
<div class="input-group">
<input
v-model="connection.sshKeepAliveInterval"
class="form-input"
type="number"
min="1"
>
<span class="input-group-addon">{{ t('general.seconds') }}</span>
</div>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="panel-footer">
<button
id="connection-test"
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
>
<BaseIcon
icon-name="mdiLightningBolt"
:size="24"
class="mr-1"
/>
{{ t('connection.testConnection') }}
</button>
<button
id="connection-save"
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy"
@click="saveConnection"
>
<BaseIcon
icon-name="mdiContentSave"
:size="24"
class="mr-1"
/>
{{ t('general.save') }}
</button>
</div>
</div>
<div class="panel-footer">
<button
id="connection-test"
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
>
<BaseIcon
icon-name="mdiLightningBolt"
:size="24"
class="mr-1"
/>
{{ t('connection.testConnection') }}
</button>
<button
id="connection-save"
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy"
@click="saveConnection"
>
<BaseIcon
icon-name="mdiContentSave"
:size="24"
class="mr-1"
/>
{{ t('general.save') }}
</button>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
<DebugConsole v-if="isConsoleOpen" />
</template>
<script setup lang="ts">
import customizations from 'common/customizations';
import { ConnectionParams } from 'common/interfaces/antares';
import { uidGen } from 'common/libs/uidGen';
import { storeToRefs } from 'pinia';
import { computed, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import BaseUploadInput from '@/components/BaseUploadInput.vue';
import DebugConsole from '@/components/DebugConsole.vue';
import ModalAskCredentials from '@/components/ModalAskCredentials.vue';
import Connection from '@/ipc-api/Connection';
import { useConnectionsStore } from '@/stores/connections';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
@ -443,6 +449,7 @@ const { t } = useI18n();
const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { isConsoleOpen } = storeToRefs(useConsoleStore());
const { connectWorkspace, selectWorkspace } = workspacesStore;

View File

@ -598,6 +598,22 @@ localConnection.value = JSON.parse(JSON.stringify(props.connection));
min-width: 450px;
border-radius: $border-radius;
.panel-nav {
.tab-block {
background: transparent;
margin: 0.2rem 0 0.15rem 0;
.tab-item {
background: transparent;
flex: 1 0 0;
> a {
padding: 8px 4px 6px 4px
}
}
}
}
.panel-body {
flex: initial;
}

View File

@ -1,189 +0,0 @@
<template>
<div
ref="wrapper"
class="query-console-wrapper"
@mouseenter="isHover = true"
@mouseleave="isHover = false"
>
<div ref="resizer" class="query-console-resizer" />
<div
id="query-console"
ref="queryConsole"
class="query-console column col-12"
:style="{height: localHeight ? localHeight+'px' : ''}"
>
<div class="query-console-header">
<div>{{ t('application.console') }}</div>
<button class="btn btn-clear mr-1" @click="resizeConsole(0)" />
</div>
<div ref="queryConsoleBody" class="query-console-body">
<div
v-for="(wLog, i) in workspaceLogs"
:key="i"
class="query-console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)"
>
<span class="type-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="query-console-log-sql" v-html="highlight(wLog.sql, {html: true})" />
</div>
</div>
</div>
</div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="copyQuery">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copy') }}</span>
</div>
</BaseContextMenu>
</template>
<script setup lang="ts">
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import { copyText } from '@/libs/copyText';
import { useConsoleStore } from '@/stores/console';
const { t } = useI18n();
const consoleStore = useConsoleStore();
const { resizeConsole, getLogsByWorkspace } = consoleStore;
const { consoleHeight } = storeToRefs(consoleStore);
const props = defineProps({
uid: String
});
const wrapper: Ref<HTMLInputElement> = ref(null);
const queryConsole: Ref<HTMLInputElement> = ref(null);
const queryConsoleBody: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
const localHeight = ref(250);
const isHover = ref(false);
const isContext = ref(false);
const contextQuery: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const resize = (e: MouseEvent) => {
const el = queryConsole.value;
let consoleHeight = el.getBoundingClientRect().bottom - e.pageY;
if (consoleHeight > 400) consoleHeight = 400;
localHeight.value = consoleHeight;
};
const workspaceLogs = computed(() => {
return getLogsByWorkspace(props.uid);
});
const stopResize = () => {
if (localHeight.value < 0) localHeight.value = 0;
resizeConsole(localHeight.value);
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResize);
};
const contextMenu = (event: MouseEvent, wLog: {date: Date; sql: string}) => {
contextEvent.value = event;
contextQuery.value = wLog.sql;
isContext.value = true;
};
const copyQuery = () => {
copyText(contextQuery.value);
isContext.value = false;
};
watch(workspaceLogs, async () => {
if (!isHover.value) {
await nextTick();
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
}
});
onMounted(() => {
localHeight.value = consoleHeight.value;
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
});
onMounted(() => {
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
});
</script>
<style lang="scss" scoped>
.query-console-wrapper {
width: 100%;
z-index: 9;
margin-top: auto;
position: absolute;
bottom: 0;
.query-console-resizer {
height: 4px;
top: -1px;
width: 100%;
cursor: ns-resize;
position: absolute;
z-index: 99;
transition: background 0.2s;
&:hover {
background: var(--primary-color-dark);
}
}
.query-console {
padding: 0;
padding-bottom: $footer-height;
.query-console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px;
font-weight: 700;
}
.query-console-body {
overflow: auto;
display: flex;
flex-direction: column;
max-height: 100%;
padding: 0 6px 3px;
.query-console-log {
padding: 1px 3px;
margin: 1px 0;
border-radius: $border-radius;
.query-console-log-sql {
font-size: 95%;
opacity: 0.8;
font-weight: 700;
&:hover {
user-select: text;
}
}
}
}
}
}
</style>

View File

@ -407,8 +407,9 @@ export const enUS = {
saveFile: 'Save file',
saveFileAs: 'Save file as',
openFile: 'Open file',
openNotes: 'Open notes'
openNotes: 'Open notes',
debugConsole: 'Debug console', // <- console tab name
executedQueries: 'Executed queries' // <- console tab name
},
faker: { // Faker.js methods, used in random generated content
address: 'Address',

View File

@ -12,7 +12,7 @@ import { createApp } from 'vue';
import App from '@/App.vue';
import { i18n } from '@/i18n';
import { useApplicationStore } from '@/stores/application';
import { useConsoleStore } from '@/stores/console';
import { QueryLog, useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings';
@ -37,11 +37,17 @@ i18n.global.locale = locale;
// IPC exceptions
ipcRenderer.on('unhandled-exception', (event, error) => {
useNotificationsStore().addNotification({ status: 'error', message: error.message });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'main',
message: error.message,
date: new Date()
});
});
// IPC query logs
ipcRenderer.on('query-log', (event, logRecord) => {
useConsoleStore().putLog(logRecord);
ipcRenderer.on('query-log', (event, logRecord: QueryLog) => {
useConsoleStore().putLog('query', logRecord);
});
ipcRenderer.on('toggle-console', () => {

View File

@ -349,11 +349,11 @@
}
}
.query-console {
.console {
border-top: 1px solid #444;
background-color: $bg-color-dark;
.query-console-log {
.console-log {
&:hover,
&:focus {
background: $bg-color-gray;

View File

@ -173,11 +173,11 @@
}
}
.query-console {
.console {
border-top: 1px solid darken($bg-color-light-gray, 15%);
background-color: $bg-color-light;
.query-console-log {
.console-log {
&:hover,
&:focus {
background: $bg-color-light-gray;

View File

@ -1,56 +1,63 @@
import { defineStore } from 'pinia';
import { useWorkspacesStore } from './workspaces';
const logsSize = 1000;
export interface ConsoleRecord {
export type LogType = 'query' | 'debug'
export interface QueryLog {
cUid: string;
sql: string;
date: Date;
}
export interface DebugLog {
level: 'log' | 'info' | 'warn' | 'error';
process: 'renderer' | 'main' | 'worker';
message: string;
date: Date;
}
export const useConsoleStore = defineStore('console', {
state: () => ({
records: [] as ConsoleRecord[],
consolesHeight: new Map<string, number>(),
consolesOpened: new Set([])
isConsoleOpen: false,
queryLogs: [] as QueryLog[],
debugLogs: [] as DebugLog[],
selectedTab: 'query' as LogType,
consoleHeight: 0
}),
getters: {
getLogsByWorkspace: state => (uid: string) => state.records.filter(r => r.cUid === uid),
isConsoleOpen: state => (uid: string) => state.consolesOpened.has(uid),
consoleHeight: state => {
const uid = useWorkspacesStore().getSelected;
return state.consolesHeight.get(uid) || 0;
}
getLogsByWorkspace: state => (uid: string) => state.queryLogs.filter(r => r.cUid === uid)
},
actions: {
putLog (record: ConsoleRecord) {
this.records.push(record);
putLog (type: LogType, record: QueryLog | DebugLog) {
if (type === 'query') {
this.queryLogs.push(record);
if (this.records.length > logsSize)
this.records = this.records.slice(0, logsSize);
if (this.queryLogs.length > logsSize)
this.queryLogs = this.queryLogs.slice(0, logsSize);
}
else if (type === 'debug') {
this.debugLogs.push(record);
if (this.debugLogs.length > logsSize)
this.debugLogs = this.debugLogs.slice(0, logsSize);
}
},
openConsole () {
const uid = useWorkspacesStore().getSelected;
this.consolesOpened.add(uid);
this.consolesHeight.set(uid, 250);
this.isConsoleOpen = true;
this.consoleHeight = 250;
},
closeConsole () {
const uid = useWorkspacesStore().getSelected;
this.consolesOpened.delete(uid);
this.consolesHeight.set(uid, 0);
this.isConsoleOpen = false;
this.consoleHeight = 0;
},
resizeConsole (height: number) {
const uid = useWorkspacesStore().getSelected;
if (height < 30)
this.closeConsole();
else
this.consolesHeight.set(uid, height);
this.consoleHeight = height;
},
toggleConsole () {
const uid = useWorkspacesStore().getSelected;
if (this.consolesOpened.has(uid))
if (this.isConsoleOpen)
this.closeConsole();
else
this.openConsole();