Fix up email event log view to use fetch on scroll instead of pagination
This commit is contained in:
@@ -290,6 +290,12 @@ function create() {
|
||||
}
|
||||
|
||||
if (result.status !== 200) return [result];
|
||||
|
||||
// some eventlogs miss uuid so lets make a unique id `uuid-ts`
|
||||
result.body.eventlogs.forEach(e => {
|
||||
e._id = `${e.uuid}-${e.ts}`;
|
||||
});
|
||||
|
||||
return [null, result.body.eventlogs];
|
||||
},
|
||||
async setMailRelay(domain, data) {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<script setup>
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { Button, ButtonGroup, SingleSelect, Spinner, TextInput, MultiSelect } from 'pankow';
|
||||
import { Button, TextInput, MultiSelect } from 'pankow';
|
||||
import { useDebouncedRef, prettyDate, prettyLongDate, prettyEmailAddresses } from 'pankow/utils';
|
||||
import Section from '../components/Section.vue';
|
||||
import MailModel from '../models/MailModel.js';
|
||||
|
||||
const mailModel = MailModel.create();
|
||||
@@ -23,13 +18,6 @@ const availableTypes = [
|
||||
{ id: 'spam', name: 'Spam' },
|
||||
];
|
||||
|
||||
const perPageOptions = [
|
||||
{ name: t('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
||||
{ name: t('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
||||
{ name: t('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
||||
];
|
||||
|
||||
const busy = ref(false);
|
||||
const refreshBusy = ref(false);
|
||||
const eventlogs = ref([]);
|
||||
const activeId = ref('');
|
||||
@@ -54,15 +42,15 @@ function onEventLogDetails(id) {
|
||||
else activeId.value = id;
|
||||
}
|
||||
|
||||
async function onPrevPage() {
|
||||
if (page.value > 1) page.value--;
|
||||
else page.value = 1;
|
||||
await onRefresh();
|
||||
}
|
||||
async function onScroll(event) {
|
||||
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) {
|
||||
page.value++;
|
||||
|
||||
async function onNextPage() {
|
||||
page.value++;
|
||||
await onRefresh();
|
||||
const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
eventlogs.value = result;
|
||||
}
|
||||
}
|
||||
|
||||
watch(perPage, onRefresh);
|
||||
@@ -70,96 +58,120 @@ watch(types, onRefresh);
|
||||
watch(search, onRefresh);
|
||||
|
||||
onMounted(async () => {
|
||||
busy.value = true;
|
||||
|
||||
await onRefresh();
|
||||
|
||||
busy.value = false;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<Section :title="$t('emails.eventlog.title')">
|
||||
<template #header-buttons>
|
||||
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="search"/>
|
||||
<MultiSelect v-model="types" :options="availableTypes" option-key="id" option-label="name" :selected-label="types.length ? $t('main.multiselect.selected', { n: types.length }) : $t('emails.typeFilterHeader')"/>
|
||||
<Button href="/logs.html?id=mail" target="_blank">{{ $t('main.action.logs') }}</Button>
|
||||
</template>
|
||||
|
||||
<!-- TODO replace with fetch more on scroll -->
|
||||
<div class="eventlog-filter">
|
||||
|
||||
<SingleSelect v-model="perPage" :options="perPageOptions" option-key="value" option-label="name"/>
|
||||
|
||||
<ButtonGroup>
|
||||
<Button tool secondary @click="onPrevPage()" :disabled="busy || page <= 1" icon="fa-solid fa-angle-double-left" v-tooltip="$t('main.pagination.prev')" />
|
||||
<div class="content" style="height: 100%; overflow: hidden; display: flex; flex-direction: column;">
|
||||
<!-- cant use Section component as we need control over vertical scrolling -->
|
||||
<div class="section" style="overflow: hidden; display: flex; flex-direction: column;">
|
||||
<h2 class="section-header">
|
||||
{{ $t('emails.eventlog.title') }}
|
||||
<div>
|
||||
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="search"/>
|
||||
<MultiSelect v-model="types" :options="availableTypes" option-key="id" option-label="name" :selected-label="types.length ? $t('main.multiselect.selected', { n: types.length }) : $t('emails.typeFilterHeader')"/>
|
||||
<Button tool secondary @click="onRefresh()" :loading="refreshBusy" icon="fa-solid fa-sync-alt" />
|
||||
<Button tool secondary @click="onNextPage()" :disabled="busy || perPage > eventlogs.length" icon="fa-solid fa-angle-double-right" v-tooltip="$t('main.pagination.next')" />
|
||||
</ButtonGroup>
|
||||
<Button href="/logs.html?id=mail" target="_blank">{{ $t('main.action.logs') }}</Button>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="section-body" style="overflow: auto; padding-top: 0" @scroll="onScroll">
|
||||
<table class="eventlog-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"><!-- Icon --></th>
|
||||
<th style="width: 15%">{{ $t('emails.eventlog.time') }}</th>
|
||||
<th style="width: 25%">{{ $t('emails.eventlog.mailFrom') }}</th>
|
||||
<th style="width: 25%">{{ $t('emails.eventlog.rcptTo') }}</th>
|
||||
<th style="width: 30%">{{ $t('emails.eventlog.details') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="eventlog in eventlogs" :key="eventlog._id">
|
||||
<tr @click="onEventLogDetails(eventlog._id)" :class="{ 'active': activeId === eventlog._id }" >
|
||||
<td class="no-wrap">
|
||||
<i class="fas fa-arrow-circle-left" v-if="eventlog.type === 'delivered'" v-tooltip="$t('emails.eventlog.type.outgoing')"></i>
|
||||
<i class="fas fa-history" v-if="eventlog.type === 'deferred'" v-tooltip="$t('emails.eventlog.type.deferred')"></i>
|
||||
<i class="fas fa-arrow-circle-right" v-if="eventlog.type === 'received'" v-tooltip="$t('emails.eventlog.type.incoming')"></i>
|
||||
<i class="fas fa-align-justify" v-if="eventlog.type === 'queued' && eventlog.spamStatus && eventlog.spamStatus.indexOf('Yes,') !== 0" v-tooltip="$t('emails.eventlog.type.queued')"></i>
|
||||
<i class="fas fa-trash" v-if="eventlog.type === 'queued' && eventlog.spamStatus && eventlog.spamStatus.indexOf('Yes,') === 0" v-tooltip="$t('emails.eventlog.type.queued')"></i>
|
||||
<i class="fas fa-minus-circle" v-if="eventlog.type === 'denied'" v-tooltip="$t('emails.eventlog.type.denied')"></i>
|
||||
<i class="fas fa-hand-paper" v-if="eventlog.type === 'bounce'" v-tooltip="$t('emails.eventlog.type.bounce')"></i>
|
||||
<i class="fas fa-filter" v-if="eventlog.type === 'spam-learn'" v-tooltip="$t('emails.eventlog.type.spamFilterTrained')"></i>
|
||||
<i class="fas fa-fill-drip" v-if="eventlog.type === 'quota'" v-tooltip="$t('emails.eventlog.type.quota')"></i>
|
||||
</td>
|
||||
<td class="no-wrap"><span v-tooltip="prettyLongDate(eventlog.ts)" class="arrow">{{ prettyDate(eventlog.ts) }}</span></td>
|
||||
<td class="elide-table-cell">{{ prettyEmailAddresses(eventlog.mailFrom) || '-' }}</td>
|
||||
<td class="elide-table-cell">{{ prettyEmailAddresses(eventlog.rcptTo) || eventlog.mailbox || '-' }}</td>
|
||||
<td>
|
||||
<span v-if="eventlog.type === 'bounce'">{{ $t('emails.eventlog.type.bounceInfo') }}. {{ eventlog.message || eventlog.reason }}</span>
|
||||
<span v-if="eventlog.type === 'deferred'">{{ $t('emails.eventlog.type.deferredInfo', { delay:eventlog.delay }) }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span v-if="eventlog.type === 'queued'">
|
||||
<span v-if="eventlog.direction === 'inbound'">{{ $t('emails.eventlog.type.inboundInfo') }}</span>
|
||||
<span v-if="eventlog.direction === 'outbound'">{{ $t('emails.eventlog.type.outboundInfo') }}</span>
|
||||
</span>
|
||||
<span v-if="eventlog.type === 'received'">{{ $t('emails.eventlog.type.receivedInfo') }}</span>
|
||||
<span v-if="eventlog.type === 'delivered'">{{ $t('emails.eventlog.type.deliveredInfo') }}</span>
|
||||
<span v-if="eventlog.type === 'denied'">{{ $t('emails.eventlog.type.deniedInfo') }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span v-if="eventlog.type === 'spam-learn'">{{ $t('emails.eventlog.type.spamFilterTrainedInfo') }}</span>
|
||||
<span v-if="eventlog.type === 'quota'">
|
||||
<span v-if="eventlog.quotaPercent > 0">{{ $t('emails.eventlog.type.overQuotaInfo', { mailbox: eventlog.mailbox, quotaPercent: eventlog.quotaPercent }) }}</span>
|
||||
<span v-if="eventlog.quotaPercent < 0">{{ $t('emails.eventlog.type.underQuotaInfo', { mailbox: eventlog.mailbox, quotaPercent: -eventlog.quotaPercent }) }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="activeId === eventlog._id">
|
||||
<td colspan="6" class="eventlog-details">
|
||||
<pre>{{ JSON.stringify(eventlog, null, 4) }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- TODO style table -->
|
||||
<center v-show="busy"><Spinner class="pankow-spinner-large" /></center>
|
||||
<table v-show="!busy" class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"><!-- Icon --></th>
|
||||
<th style="width: 15%">{{ $t('emails.eventlog.time') }}</th>
|
||||
<th style="width: 25%">{{ $t('emails.eventlog.mailFrom') }}</th>
|
||||
<th style="width: 25%">{{ $t('emails.eventlog.rcptTo') }}</th>
|
||||
<th style="width: 30%">{{ $t('emails.eventlog.details') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="eventlog in eventlogs" :key="eventlog.uuid">
|
||||
<tr @click="onEventLogDetails(eventlog.uuid)" class="hand" >
|
||||
<td class="no-wrap">
|
||||
<i class="fas fa-arrow-circle-left" v-if="eventlog.type === 'delivered'" v-tooltip="$t('emails.eventlog.type.outgoing')"></i>
|
||||
<i class="fas fa-history" v-if="eventlog.type === 'deferred'" v-tooltip="$t('emails.eventlog.type.deferred')"></i>
|
||||
<i class="fas fa-arrow-circle-right" v-if="eventlog.type === 'received'" v-tooltip="$t('emails.eventlog.type.incoming')"></i>
|
||||
<i class="fas fa-align-justify" v-if="eventlog.type === 'queued' && eventlog.spamStatus && eventlog.spamStatus.indexOf('Yes,') !== 0" v-tooltip="$t('emails.eventlog.type.queued')"></i>
|
||||
<i class="fas fa-trash" v-if="eventlog.type === 'queued' && eventlog.spamStatus && eventlog.spamStatus.indexOf('Yes,') === 0" v-tooltip="$t('emails.eventlog.type.queued')"></i>
|
||||
<i class="fas fa-minus-circle" v-if="eventlog.type === 'denied'" v-tooltip="$t('emails.eventlog.type.denied')"></i>
|
||||
<i class="fas fa-hand-paper" v-if="eventlog.type === 'bounce'" v-tooltip="$t('emails.eventlog.type.bounce')"></i>
|
||||
<i class="fas fa-filter" v-if="eventlog.type === 'spam-learn'" v-tooltip="$t('emails.eventlog.type.spamFilterTrained')"></i>
|
||||
<i class="fas fa-fill-drip" v-if="eventlog.type === 'quota'" v-tooltip="$t('emails.eventlog.type.quota')"></i>
|
||||
</td>
|
||||
<td class="no-wrap"><span v-tooltip="prettyLongDate(eventlog.ts)" class="arrow">{{ prettyDate(eventlog.ts) }}</span></td>
|
||||
<td class="elide-table-cell">{{ prettyEmailAddresses(eventlog.mailFrom) || '-' }}</td>
|
||||
<td class="elide-table-cell">{{ prettyEmailAddresses(eventlog.rcptTo) || eventlog.mailbox || '-' }}</td>
|
||||
<td>
|
||||
<span v-if="eventlog.type === 'bounce'">{{ $t('emails.eventlog.type.bounceInfo') }}. {{ eventlog.message || eventlog.reason }}</span>
|
||||
<span v-if="eventlog.type === 'deferred'">{{ $t('emails.eventlog.type.deferredInfo', { delay:eventlog.delay }) }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span v-if="eventlog.type === 'queued'">
|
||||
<span v-if="eventlog.direction === 'inbound'">{{ $t('emails.eventlog.type.inboundInfo') }}</span>
|
||||
<span v-if="eventlog.direction === 'outbound'">{{ $t('emails.eventlog.type.outboundInfo') }}</span>
|
||||
</span>
|
||||
<span v-if="eventlog.type === 'received'">{{ $t('emails.eventlog.type.receivedInfo') }}</span>
|
||||
<span v-if="eventlog.type === 'delivered'">{{ $t('emails.eventlog.type.deliveredInfo') }}</span>
|
||||
<span v-if="eventlog.type === 'denied'">{{ $t('emails.eventlog.type.deniedInfo') }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span v-if="eventlog.type === 'spam-learn'">{{ $t('emails.eventlog.type.spamFilterTrainedInfo') }}</span>
|
||||
<span v-if="eventlog.type === 'quota'">
|
||||
<span v-if="eventlog.quotaPercent > 0">{{ $t('emails.eventlog.type.overQuotaInfo', { mailbox: eventlog.mailbox, quotaPercent: eventlog.quotaPercent }) }}</span>
|
||||
<span v-if="eventlog.quotaPercent < 0">{{ $t('emails.eventlog.type.underQuotaInfo', { mailbox: eventlog.mailbox, quotaPercent: -eventlog.quotaPercent }) }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="activeId === eventlog.uuid">
|
||||
<td colspan="6">
|
||||
<pre class="eventlog-details">{{ JSON.stringify(eventlog, null, 4) }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.eventlog-table {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
border-spacing: 0px;
|
||||
}
|
||||
|
||||
.eventlog-table thead {
|
||||
background-color: var(--pankow-body-background-color);
|
||||
top: 0;
|
||||
position: sticky;
|
||||
z-index: 1; /* avoids see-through table headers if items in the table have opacity set */
|
||||
}
|
||||
|
||||
.eventlog-table th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.eventlog-table tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.eventlog-table tbody tr.active,
|
||||
.eventlog-table tbody tr:hover {
|
||||
background-color: var(--pankow-color-background-hover);
|
||||
}
|
||||
|
||||
.eventlog-table tbody tr.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.eventlog-table th,
|
||||
.eventlog-table td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.eventlog-filter {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
@@ -168,9 +180,16 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.eventlog-details {
|
||||
background-color: var(--pankow-color-background-hover);
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.eventlog-details pre {
|
||||
white-space: pre-wrap;
|
||||
color: var(--pankow-text-color);
|
||||
background-color: var(--pankow-input-background-color);
|
||||
font-size: 13px;
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: var(--pankow-border-radius);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ const availableActions = [
|
||||
{ id: 'volume.remove' },
|
||||
];
|
||||
|
||||
const busy = ref(false);
|
||||
const refreshBusy = ref(false);
|
||||
const apps = ref([]);
|
||||
const eventlogs = ref([]);
|
||||
@@ -136,14 +135,11 @@ watch(actions, onRefresh);
|
||||
watch(search, onRefresh);
|
||||
|
||||
onMounted(async () => {
|
||||
busy.value = true;
|
||||
|
||||
const [error, result] = await appsModel.list();
|
||||
if (error) console.error(error);
|
||||
else apps.value = result;
|
||||
|
||||
await onRefresh();
|
||||
busy.value = false;
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -161,8 +157,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</h2>
|
||||
<div class="section-body" style="overflow: auto; padding-top: 0" @scroll="onScroll">
|
||||
<center v-show="busy"><Spinner class="pankow-spinner-large" /></center>
|
||||
<table v-show="!busy" class="eventlog-table">
|
||||
<table class="eventlog-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('eventlog.time') }}</th>
|
||||
|
||||
Reference in New Issue
Block a user