mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-25 10:56:00 +00:00
528 lines
16 KiB
Svelte
528 lines
16 KiB
Svelte
<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';
|
|
import DropDownButton from '../buttons/DropDownButton.svelte';
|
|
import { showModal } from '../modals/modalTools';
|
|
import ValueLookupModal from '../modals/ValueLookupModal.svelte';
|
|
import { createLogCompoudCondition } from 'dbgate-sqltree';
|
|
import { exportQuickExportFile } from '../utility/exportFileTools';
|
|
import ToolStripExportButton, {
|
|
createQuickExportHandlerRef,
|
|
registerQuickExportHandler,
|
|
} from '../buttons/ToolStripExportButton.svelte';
|
|
import { _t } from '../translations';
|
|
|
|
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;
|
|
let jslid;
|
|
|
|
const quickExportHandlerRef = createQuickExportHandlerRef();
|
|
|
|
function formatPossibleUuid(value) {
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
if (_.isString(value) && value.match(uuidRegex)) {
|
|
return value.slice(0, 8);
|
|
}
|
|
if (value == null) {
|
|
return 'N/A';
|
|
}
|
|
return value;
|
|
}
|
|
|
|
async function loadNextRows() {
|
|
const pageSize = getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000);
|
|
const rows = await apiCall('jsldata/get-rows', {
|
|
jslid,
|
|
offset: loadedRows.length,
|
|
limit: pageSize,
|
|
filters: createLogCompoudCondition(
|
|
filters,
|
|
'time',
|
|
startOfDay(dateFilter[0]).getTime(),
|
|
endOfDay(dateFilter[1]).getTime()
|
|
),
|
|
});
|
|
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(createNewJslId = true) {
|
|
switch (mode) {
|
|
case 'recent':
|
|
loadedRows = await apiCall('files/get-recent-app-log', { limit: 100 });
|
|
await tick();
|
|
scrollToRecent();
|
|
break;
|
|
case 'date':
|
|
if (createNewJslId) {
|
|
const resp = await apiCall('files/fill-app-logs', {
|
|
dateFrom: startOfDay(dateFilter[0]).getTime(),
|
|
dateTo: endOfDay(dateFilter[1]).getTime(),
|
|
});
|
|
jslid = resp.jslid;
|
|
}
|
|
loadedRows = [];
|
|
loadedAll = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
function doSetFilter(field, values) {
|
|
filters = {
|
|
...filters,
|
|
[field]: values,
|
|
};
|
|
reloadData(false);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
function filterBy(field) {
|
|
showModal(ValueLookupModal, {
|
|
jslid,
|
|
field,
|
|
multiselect: true,
|
|
onConfirm: values => {
|
|
doSetFilter(field, values);
|
|
},
|
|
});
|
|
}
|
|
|
|
onMount(() => {
|
|
apiOn('applog-event', handleLogMessage);
|
|
reloadData();
|
|
});
|
|
onDestroy(() => {
|
|
apiOff('applog-event', handleLogMessage);
|
|
});
|
|
|
|
const quickExportHandler = fmt => async () => {
|
|
const resp =
|
|
mode == 'recent'
|
|
? await apiCall('files/fill-app-logs', {
|
|
dateFrom: startOfDay(new Date()).getTime(),
|
|
dateTo: endOfDay(new Date()).getTime(),
|
|
prepareForExport: true,
|
|
})
|
|
: await apiCall('files/fill-app-logs', {
|
|
dateFrom: startOfDay(dateFilter[0]).getTime(),
|
|
dateTo: endOfDay(dateFilter[1]).getTime(),
|
|
prepareForExport: true,
|
|
});
|
|
|
|
exportQuickExportFile(
|
|
'Log',
|
|
{
|
|
functionName: 'jslDataReader',
|
|
props: {
|
|
jslid: resp.jslid,
|
|
},
|
|
},
|
|
fmt
|
|
);
|
|
};
|
|
registerQuickExportHandler(quickExportHandler);
|
|
</script>
|
|
|
|
<ToolStripContainer>
|
|
<div class="wrapper classicform">
|
|
<div class="filters">
|
|
<div class="filter-label">Mode:</div>
|
|
<SelectField
|
|
isNative
|
|
options={[
|
|
{ label: _t('logs.recentLogs', { defaultMessage: 'Recent logs' }), value: 'recent' },
|
|
{ label: _t('logs.chooseDate', { defaultMessage: 'Choose date' }), value: 'date' },
|
|
]}
|
|
value={mode}
|
|
on:change={e => {
|
|
mode = e.detail;
|
|
reloadData();
|
|
}}
|
|
/>
|
|
|
|
{#if mode === 'recent'}
|
|
<div class="filter-label ml-2">{_t('logs.autoScroll', { defaultMessage: 'Auto-scroll' })}</div>
|
|
<input
|
|
type="checkbox"
|
|
checked={autoScroll}
|
|
on:change={e => {
|
|
autoScroll = e.target['checked'];
|
|
}}
|
|
/>
|
|
{/if}
|
|
|
|
{#if mode === 'date'}
|
|
<div class="filter-label">{_t('logs.date', { defaultMessage: 'Date:' })}</div>
|
|
<DateRangeSelector
|
|
onChange={value => {
|
|
dateFilter = value;
|
|
reloadData();
|
|
}}
|
|
/>
|
|
<div class="ml-2">
|
|
<DropDownButton
|
|
data-testid="AdminAuditLogTab_addFilter"
|
|
icon="icon filter"
|
|
menu={[
|
|
{ text: _t('logs.connectionId', { defaultMessage: 'Connection ID' }), onClick: () => filterBy('conid') },
|
|
{ text: _t('logs.database', { defaultMessage: 'Database' }), onClick: () => filterBy('database') },
|
|
{ text: _t('logs.engine', { defaultMessage: 'Engine' }), onClick: () => filterBy('engine') },
|
|
{ text: _t('logs.messageCode', { defaultMessage: 'Message code' }), onClick: () => filterBy('msgcode') },
|
|
{ text: _t('logs.caller', { defaultMessage: 'Caller' }), onClick: () => filterBy('caller') },
|
|
{ text: _t('logs.name', { defaultMessage: 'Name' }), onClick: () => filterBy('name') },
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
{#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(false);
|
|
}}
|
|
>
|
|
{formatPossibleUuid(value)}
|
|
</Chip>
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
<div class="tablewrap" bind:this={domTable}>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:80px">{_t('logs.dateTab', { defaultMessage: 'Date' })}</th>
|
|
<th>{_t('logs.timeTab', { defaultMessage: 'Time' })}</th>
|
|
<th>{_t('logs.codeTab', { defaultMessage: 'Code' })}</th>
|
|
<th>{_t('logs.messageTab', { defaultMessage: 'Message' })}</th>
|
|
<th>{_t('logs.connectionTab', { defaultMessage: 'Connection' })}</th>
|
|
<th>{_t('logs.databaseTab', { defaultMessage: 'Database' })}</th>
|
|
<th>{_t('logs.engineTab', { defaultMessage: 'Engine' })}</th>
|
|
<th>{_t('logs.callerTab', { defaultMessage: 'Caller' })}</th>
|
|
<th>{_t('logs.nameTab', { defaultMessage: 'Name' })}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each loadedRows as row, index}
|
|
<tr
|
|
class="clickable"
|
|
on:click={() => {
|
|
if (selectedLogIndex === index) {
|
|
selectedLogIndex = null;
|
|
} else {
|
|
selectedLogIndex = index;
|
|
}
|
|
}}
|
|
>
|
|
<td>{row.time ? format(new Date(parseInt(row.time)), 'yyyy-MM-dd') : ''}</td>
|
|
<td>{row.time ? format(new Date(parseInt(row.time)), 'HH:mm:ss') : ''}</td>
|
|
<td>{row.msgcode || ''}</td>
|
|
<td>{row.msg}</td>
|
|
<td>{formatPossibleUuid(row.conid) || ''}</td>
|
|
<td>{row.database || ''}</td>
|
|
<td>{row.engine?.includes('@') ? row.engine.split('@')[0] : row.engine || ''}</td>
|
|
<td>{row.caller || ''}</td>
|
|
<td>{row.name || ''}</td>
|
|
</tr>
|
|
|
|
{#if index === selectedLogIndex}
|
|
<tr>
|
|
<td colspan="9">
|
|
<TabControl
|
|
isInline
|
|
tabs={_.compact([
|
|
{ label: _t('logs.details', { defaultMessage: 'Details' }), slot: 1 },
|
|
{ label: 'JSON', slot: 2 },
|
|
])}
|
|
>
|
|
<svelte:fragment slot="1">
|
|
<div class="details-wrap">
|
|
<div class="row">
|
|
<div>{_t('logs.messageCode', { defaultMessage: '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>{_t('logs.message', { defaultMessage: 'Message:' })}</div>
|
|
{row.msg}
|
|
</div>
|
|
<div class="row">
|
|
<div>{_t('logs.time', { defaultMessage: 'Time:' })}</div>
|
|
<b>{row.time ? format(new Date(parseInt(row.time)), 'yyyy-MM-dd HH:mm:ss') : ''}</b>
|
|
</div>
|
|
<div class="row">
|
|
<div>{_t('logs.caller', { defaultMessage: '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>{_t('logs.name', { defaultMessage: 'Name:' })}</div>
|
|
{#if mode == 'date'}
|
|
<Link onClick={() => doSetFilter('name', [row.name])}>{row.name || 'N/A'}</Link>
|
|
{:else}
|
|
{row.name || 'N/A'}
|
|
{/if}
|
|
</div>
|
|
{#if row.conid}
|
|
<div class="row">
|
|
<div>{_t('logs.connectionId', { defaultMessage: 'Connection ID:' })}</div>
|
|
{#if mode == 'date'}
|
|
<Link onClick={() => doSetFilter('conid', [row.conid])}
|
|
>{formatPossibleUuid(row.conid)}</Link
|
|
>
|
|
{:else}
|
|
{formatPossibleUuid(row.conid)}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{#if row.database}
|
|
<div class="row">
|
|
<div>{_t('logs.database', { defaultMessage: 'Database:' })}</div>
|
|
{#if mode == 'date'}
|
|
<Link onClick={() => doSetFilter('database', [row.database])}>{row.database}</Link>
|
|
{:else}
|
|
{row.database}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{#if row.engine}
|
|
<div class="row">
|
|
<div>{_t('logs.engine', { defaultMessage: 'Engine:' })}</div>
|
|
{#if mode == 'date'}
|
|
<Link onClick={() => doSetFilter('engine', [row.engine])}>{row.engine}</Link>
|
|
{:else}
|
|
{row.engine}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</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">{_t('logs.noDataForSelectedDate', { defaultMessage: "No data for selected date" })}</td>
|
|
</tr>
|
|
{/if}
|
|
{#if !loadedAll && mode === 'date'}
|
|
{#key loadedRows}
|
|
<tr>
|
|
<td colspan="6" bind:this={domLoadNext}>{_t('logs.loadingNextRows', { defaultMessage: "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();
|
|
}}>{_t('logs.refresh', { defaultMessage: 'Refresh' })}</ToolStripButton
|
|
>
|
|
<ToolStripExportButton {quickExportHandlerRef} />
|
|
</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>
|