mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
feat: in-app debug console, closes #824
This commit is contained in:
@ -10,10 +10,8 @@
|
||||
:key="connection.uid"
|
||||
:connection="connection"
|
||||
/>
|
||||
<div class="connection-panel-wrapper p-relative">
|
||||
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
|
||||
</div>
|
||||
</div>
|
||||
<TheFooter />
|
||||
<TheNotificationsBoard />
|
||||
<TheScratchpad v-if="isScratchpad" />
|
||||
@ -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">
|
||||
|
@ -365,7 +365,11 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const handleWheelEvent = (e) => {
|
||||
try {
|
||||
if (!e.target.className.includes('select__')) deactivate();
|
||||
}
|
||||
catch (_) {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
285
src/renderer/components/DebugConsole.vue
Normal file
285
src/renderer/components/DebugConsole.vue
Normal 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>
|
@ -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':
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
|
@ -15,7 +15,8 @@
|
||||
:connection="connection"
|
||||
:is-selected="isSelected"
|
||||
/>
|
||||
<div v-if="workspace?.connectionStatus === 'connected'" class="workspace-tabs column columns col-gapless">
|
||||
<div class="workspace-tabs column">
|
||||
<div v-if="workspace?.connectionStatus === 'connected'" class="columns col-gapless">
|
||||
<Draggable
|
||||
ref="tabWrap"
|
||||
v-model="draggableTabs"
|
||||
@ -598,11 +599,12 @@
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
</template>
|
||||
<WorkspaceQueryConsole v-if="isConsoleOpen(workspace.uid)" :uid="workspace.uid" />
|
||||
</div>
|
||||
<div v-else class="connection-panel-wrapper p-relative">
|
||||
<WorkspaceEditConnectionPanel :connection="connection" />
|
||||
</div>
|
||||
<DebugConsole v-if="isConsoleOpen" :uid="workspace.uid" />
|
||||
</div>
|
||||
<ModalProcessesList
|
||||
v-if="isProcessesModal"
|
||||
:connection="connection"
|
||||
@ -626,12 +628,12 @@ import { useI18n } from 'vue-i18n';
|
||||
import * as Draggable from 'vuedraggable';
|
||||
|
||||
import BaseIcon from '@/components/BaseIcon.vue';
|
||||
import DebugConsole from '@/components/DebugConsole.vue';
|
||||
import ModalDiscardChanges from '@/components/ModalDiscardChanges.vue';
|
||||
import ModalProcessesList from '@/components/ModalProcessesList.vue';
|
||||
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel.vue';
|
||||
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState.vue';
|
||||
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar.vue';
|
||||
import WorkspaceQueryConsole from '@/components/WorkspaceQueryConsole.vue';
|
||||
import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction.vue';
|
||||
import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine.vue';
|
||||
import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler.vue';
|
||||
|
@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<div class="connection-panel-wrapper p-relative">
|
||||
<div class="connection-panel">
|
||||
<div class="panel">
|
||||
<div class="panel-nav">
|
||||
@ -420,21 +421,26 @@
|
||||
@credentials="continueTest"
|
||||
/>
|
||||
</div>
|
||||
</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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
@ -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',
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
Reference in New Issue
Block a user