mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-20 14:46:01 +00:00
SYNC: Merge pull request #7 from dbgate/feature/applog
This commit is contained in:
@@ -311,6 +311,7 @@
|
||||
'img sort-asc': 'mdi mdi-sort-alphabetical-ascending color-icon-green',
|
||||
'img sort-desc': 'mdi mdi-sort-alphabetical-descending color-icon-green',
|
||||
'img map': 'mdi mdi-map color-icon-blue',
|
||||
'img applog': 'mdi mdi-desktop-classic color-icon-green',
|
||||
|
||||
'img reference': 'mdi mdi-link-box',
|
||||
'img link': 'mdi mdi-link',
|
||||
|
||||
407
packages/web/src/tabs/AppLogTab.svelte
Normal file
407
packages/web/src/tabs/AppLogTab.svelte
Normal file
@@ -0,0 +1,407 @@
|
||||
<script lang="ts" context="module">
|
||||
export const matchingProps = [];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import ToolStripButton from '../buttons/ToolStripButton.svelte';
|
||||
import { apiCall, apiOff, apiOn } from '../utility/api';
|
||||
import { format, startOfDay, endOfDay } from 'date-fns';
|
||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
||||
import DateRangeSelector from '../elements/DateRangeSelector.svelte';
|
||||
import Chip from '../elements/Chip.svelte';
|
||||
import TabControl from '../elements/TabControl.svelte';
|
||||
import Link from '../elements/Link.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
|
||||
let loadedRows = [];
|
||||
let loadedAll = false;
|
||||
let domLoadNext;
|
||||
let observer;
|
||||
let dateFilter = [new Date(), new Date()];
|
||||
let selectedLogIndex = null;
|
||||
let filters = {};
|
||||
let mode = 'recent';
|
||||
let autoScroll = true;
|
||||
let domTable;
|
||||
|
||||
function formatValue(value) {
|
||||
if (value == null) {
|
||||
return 'N/A';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function loadNextRows() {
|
||||
const pageSize = getIntSettingsValue('dataGrid.pageSize', 100, 5, 1000);
|
||||
const rows = await apiCall('files/get-app-log', {
|
||||
offset: loadedRows.length,
|
||||
limit: pageSize,
|
||||
dateFrom: startOfDay(dateFilter[0]).getTime(),
|
||||
dateTo: endOfDay(dateFilter[1]).getTime(),
|
||||
filters,
|
||||
});
|
||||
loadedRows = [...loadedRows, ...rows];
|
||||
if (rows.length < 10) {
|
||||
loadedAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
function startObserver(dom) {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
if (dom) {
|
||||
observer = new IntersectionObserver(entries => {
|
||||
if (entries.find(x => x.isIntersecting)) {
|
||||
loadNextRows();
|
||||
}
|
||||
});
|
||||
observer.observe(dom);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (mode == 'date') {
|
||||
startObserver(domLoadNext);
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
switch (mode) {
|
||||
case 'recent':
|
||||
loadedRows = await apiCall('files/get-recent-app-log', { limit: 100 });
|
||||
await tick();
|
||||
scrollToRecent();
|
||||
break;
|
||||
case 'date':
|
||||
loadedRows = [];
|
||||
loadedAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function doSetFilter(field, values) {
|
||||
filters = {
|
||||
...filters,
|
||||
[field]: values,
|
||||
};
|
||||
reloadData();
|
||||
}
|
||||
|
||||
const ColumnNamesMap = {
|
||||
msgcode: 'Code',
|
||||
};
|
||||
|
||||
function handleLogMessage(msg) {
|
||||
// console.log('AppLogTab: handleLogMessage', msg);
|
||||
if (mode !== 'recent') return;
|
||||
if (loadedRows.find(x => x.counter == msg.counter)) {
|
||||
return; // Already loaded
|
||||
}
|
||||
loadedRows = [...loadedRows, msg];
|
||||
scrollToRecent();
|
||||
}
|
||||
|
||||
function scrollToRecent() {
|
||||
if (autoScroll && domTable) {
|
||||
domTable.scrollTop = domTable.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
apiOn('applog-event', handleLogMessage);
|
||||
reloadData();
|
||||
});
|
||||
onDestroy(() => {
|
||||
apiOff('applog-event', handleLogMessage);
|
||||
});
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
<div class="wrapper classicform">
|
||||
<div class="filters">
|
||||
<div class="filter-label">Mode:</div>
|
||||
<SelectField
|
||||
isNative
|
||||
options={[
|
||||
{ label: 'Recent logs', value: 'recent' },
|
||||
{ label: 'Choose date', value: 'date' },
|
||||
]}
|
||||
value={mode}
|
||||
on:change={e => {
|
||||
mode = e.detail;
|
||||
reloadData();
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if mode === 'recent'}
|
||||
<div class="filter-label ml-2">Auto-scroll</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoScroll}
|
||||
on:change={e => {
|
||||
autoScroll = e.target['checked'];
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if mode === 'date'}
|
||||
<div class="filter-label">Date:</div>
|
||||
<DateRangeSelector
|
||||
onChange={value => {
|
||||
dateFilter = value;
|
||||
reloadData();
|
||||
}}
|
||||
/>
|
||||
{#each Object.keys(filters) as filterKey}
|
||||
<div class="ml-2">
|
||||
<span class="filter-label">{ColumnNamesMap[filterKey] || filterKey}:</span>
|
||||
{#each filters[filterKey] as value}
|
||||
<Chip
|
||||
onClose={() => {
|
||||
filters = { ...filters, [filterKey]: filters[filterKey].filter(x => x !== value) };
|
||||
if (!filters[filterKey].length) {
|
||||
filters = _.omit(filters, filterKey);
|
||||
}
|
||||
reloadData();
|
||||
}}
|
||||
>
|
||||
{formatValue(value)}
|
||||
</Chip>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="tablewrap" bind:this={domTable}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:80px">Date</th>
|
||||
<th>Time</th>
|
||||
<th>Code</th>
|
||||
<th>Message</th>
|
||||
<th>Caller</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each loadedRows as row, index}
|
||||
<tr
|
||||
class="clickable"
|
||||
on:click={() => {
|
||||
if (selectedLogIndex === index) {
|
||||
selectedLogIndex = null;
|
||||
} else {
|
||||
selectedLogIndex = index;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td>{format(new Date(parseInt(row.time)), 'yyyy-MM-dd')}</td>
|
||||
<td>{format(new Date(parseInt(row.time)), 'HH:mm:ss')}</td>
|
||||
<td>{row.msgcode || ''}</td>
|
||||
<td>{row.msg}</td>
|
||||
<td>{row.caller || ''}</td>
|
||||
<td>{row.name || ''}</td>
|
||||
</tr>
|
||||
|
||||
{#if index === selectedLogIndex}
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<TabControl
|
||||
isInline
|
||||
tabs={_.compact([
|
||||
{ label: 'Details', slot: 1 },
|
||||
{ label: 'JSON', slot: 2 },
|
||||
])}
|
||||
>
|
||||
<svelte:fragment slot="1">
|
||||
<div class="details-wrap">
|
||||
<div class="row">
|
||||
<div>Message code:</div>
|
||||
{#if mode == 'date'}
|
||||
<Link onClick={() => doSetFilter('msgcode', [row.msgcode])}>{row.msgcode || 'N/A'}</Link>
|
||||
{:else}
|
||||
{row.msgcode || 'N/A'}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>Message:</div>
|
||||
{row.msg}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>Time:</div>
|
||||
<b>{format(new Date(parseInt(row.time)), 'yyyy-MM-dd HH:mm:ss')}</b>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>Caller:</div>
|
||||
{#if mode == 'date'}
|
||||
<Link onClick={() => doSetFilter('caller', [row.caller])}>{row.caller || 'N/A'}</Link>
|
||||
{:else}
|
||||
{row.caller || 'N/A'}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>Name:</div>
|
||||
{#if mode == 'date'}
|
||||
<Link onClick={() => doSetFilter('name', [row.name])}>{row.name || 'N/A'}</Link>
|
||||
{:else}
|
||||
{row.name || 'N/A'}
|
||||
{/if}
|
||||
</div>
|
||||
</div></svelte:fragment
|
||||
>
|
||||
<svelte:fragment slot="2">
|
||||
<pre>{JSON.stringify(row, null, 2)}</pre>
|
||||
</svelte:fragment>
|
||||
</TabControl>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if !loadedRows?.length && mode === 'date'}
|
||||
<tr>
|
||||
<td colspan="6">No data for selected date</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if !loadedAll && mode === 'date'}
|
||||
{#key loadedRows}
|
||||
<tr>
|
||||
<td colspan="6" bind:this={domLoadNext}>Loading next rows... </td>
|
||||
</tr>
|
||||
{/key}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripButton
|
||||
icon="icon refresh"
|
||||
data-testid="AdminAuditLogTab_refreshButton"
|
||||
on:click={() => {
|
||||
reloadData();
|
||||
}}>Refresh</ToolStripButton
|
||||
>
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
|
||||
<style>
|
||||
.editor-wrap {
|
||||
height: 200px;
|
||||
}
|
||||
.tablewrap {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
table.disableFocusOutline:focus {
|
||||
outline: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
table.selectable {
|
||||
user-select: none;
|
||||
}
|
||||
tbody tr {
|
||||
background: var(--theme-bg-0);
|
||||
}
|
||||
tbody tr.selected {
|
||||
background: var(--theme-bg-3);
|
||||
}
|
||||
table:focus tbody tr.selected {
|
||||
background: var(--theme-bg-selected);
|
||||
}
|
||||
tbody tr.clickable:hover {
|
||||
background: var(--theme-bg-hover);
|
||||
}
|
||||
|
||||
thead th {
|
||||
border: 1px solid var(--theme-border);
|
||||
background-color: var(--theme-bg-1);
|
||||
padding: 5px;
|
||||
}
|
||||
tbody td {
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
tbody td {
|
||||
padding: 5px;
|
||||
}
|
||||
td.isHighlighted {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
td.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
border-top: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
table th {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
thead :global(tr:first-child) :global(th) {
|
||||
border-top: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
table td {
|
||||
border: 0px;
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
border-right: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
border-left: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
.empty-cell {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
margin-right: 5px;
|
||||
color: var(--theme-font-2);
|
||||
}
|
||||
|
||||
.details-wrap {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.details-wrap .row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.details-wrap .row div:first-child {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
max-width: 50vw;
|
||||
}
|
||||
</style>
|
||||
@@ -24,6 +24,7 @@ import * as MapTab from './MapTab.svelte';
|
||||
import * as ServerSummaryTab from './ServerSummaryTab.svelte';
|
||||
import * as ImportExportTab from './ImportExportTab.svelte';
|
||||
import * as SqlObjectTab from './SqlObjectTab.svelte';
|
||||
import * as AppLogTab from './AppLogTab.svelte';
|
||||
|
||||
import protabs from './index-pro';
|
||||
|
||||
@@ -54,5 +55,6 @@ export default {
|
||||
ServerSummaryTab,
|
||||
ImportExportTab,
|
||||
SqlObjectTab,
|
||||
AppLogTab,
|
||||
...protabs,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import NewObjectModal from '../modals/NewObjectModal.svelte';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
|
||||
let domSettings;
|
||||
let domCloudAccount;
|
||||
@@ -123,6 +124,16 @@
|
||||
$visibleWidgetSideBar = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'View application logs',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Application log',
|
||||
icon: 'img applog',
|
||||
tabComponent: 'AppLogTab',
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
currentDropDownMenu.set({ left, top, items });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user