2025-01-25 17:09:53 +01:00
|
|
|
<script setup>
|
|
|
|
|
|
|
|
|
|
import { ref, reactive, onMounted, watch } from 'vue';
|
2025-03-23 17:16:50 +01:00
|
|
|
import { Button, Spinner, TextInput, MultiSelect } from 'pankow';
|
2025-02-21 16:30:59 +01:00
|
|
|
import { useDebouncedRef, prettyDate, prettyLongDate } from 'pankow/utils';
|
2025-03-25 19:11:40 +01:00
|
|
|
import Section from '../components/Section.vue';
|
2025-01-25 17:09:53 +01:00
|
|
|
import AppsModel from '../models/AppsModel.js';
|
|
|
|
|
import EventlogsModel from '../models/EventlogsModel.js';
|
2025-02-21 16:30:59 +01:00
|
|
|
import { eventlogDetails, eventlogSource } from '../utils.js';
|
2025-01-25 17:09:53 +01:00
|
|
|
|
2025-01-31 21:02:48 +01:00
|
|
|
const appsModel = AppsModel.create();
|
|
|
|
|
const eventlogsModel = EventlogsModel.create();
|
2025-01-25 17:09:53 +01:00
|
|
|
|
|
|
|
|
function getApp(id) {
|
|
|
|
|
return apps.value.find(a => a.id === id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const availableActions = [
|
|
|
|
|
{ id: 'app.backup' },
|
|
|
|
|
{ id: 'app.backup.finish' },
|
|
|
|
|
{ id: 'app.configure' },
|
|
|
|
|
{ id: 'app.install' },
|
|
|
|
|
{ id: 'app.restore' },
|
|
|
|
|
{ id: 'app.uninstall' },
|
|
|
|
|
{ id: 'app.update' },
|
|
|
|
|
{ id: 'app.update.finish' },
|
|
|
|
|
{ id: 'app.login' },
|
|
|
|
|
{ id: 'app.oom' },
|
|
|
|
|
{ id: 'app.down' },
|
|
|
|
|
{ id: 'app.up' },
|
|
|
|
|
{ id: 'app.start' },
|
|
|
|
|
{ id: 'app.stop' },
|
|
|
|
|
{ id: 'app.restart' },
|
|
|
|
|
{ id: 'backup.cleanup' },
|
|
|
|
|
{ id: 'backup.cleanup.finish' },
|
|
|
|
|
{ id: 'backup.finish' },
|
|
|
|
|
{ id: 'backup.start' },
|
|
|
|
|
{ id: 'branding.avatar' },
|
|
|
|
|
{ id: 'branding.footer' },
|
|
|
|
|
{ id: 'branding.name' },
|
|
|
|
|
{ id: 'certificate.new' },
|
|
|
|
|
{ id: 'certificate.renew' },
|
|
|
|
|
{ id: 'certificate.cleanup' },
|
|
|
|
|
{ id: 'cloudron.activate' },
|
|
|
|
|
{ id: 'cloudron.provision' },
|
|
|
|
|
{ id: 'cloudron.restore' },
|
|
|
|
|
{ id: 'cloudron.start' },
|
|
|
|
|
{ id: 'cloudron.update' },
|
|
|
|
|
{ id: 'cloudron.update.finish' },
|
|
|
|
|
{ id: 'dashboard.domain.update' },
|
|
|
|
|
{ id: 'directoryserver.configure' },
|
|
|
|
|
{ id: 'dyndns.update' },
|
|
|
|
|
{ id: 'domain.add' },
|
|
|
|
|
{ id: 'domain.update' },
|
|
|
|
|
{ id: 'domain.remove' },
|
|
|
|
|
{ id: 'externalldap.configure' },
|
|
|
|
|
{ id: 'group.add' },
|
|
|
|
|
{ id: 'group.update' },
|
|
|
|
|
{ id: 'group.remove' },
|
|
|
|
|
{ id: 'mail.location' },
|
|
|
|
|
{ id: 'mail.enabled' },
|
|
|
|
|
{ id: 'mail.box.add' },
|
|
|
|
|
{ id: 'mail.box.update' },
|
|
|
|
|
{ id: 'mail.box.remove' },
|
|
|
|
|
{ id: 'mail.list.add' },
|
|
|
|
|
{ id: 'mail.list.update' },
|
|
|
|
|
{ id: 'mail.list.remove' },
|
|
|
|
|
{ id: 'service.configure' },
|
|
|
|
|
{ id: 'service.rebuild' },
|
|
|
|
|
{ id: 'service.restart' },
|
|
|
|
|
{ id: 'support.ticket' },
|
|
|
|
|
{ id: 'support.ssh' },
|
|
|
|
|
{ id: 'user.add' },
|
|
|
|
|
{ id: 'user.login' },
|
|
|
|
|
{ id: 'user.login.ghost' },
|
|
|
|
|
{ id: 'user.logout' },
|
|
|
|
|
{ id: 'user.remove' },
|
|
|
|
|
{ id: 'user.transfer' },
|
|
|
|
|
{ id: 'user.update' },
|
|
|
|
|
{ id: 'userdirectory.profileconfig.update' },
|
|
|
|
|
{ id: 'volume.add' },
|
|
|
|
|
{ id: 'volume.update' },
|
|
|
|
|
{ id: 'volume.remove' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const busy = ref(false);
|
|
|
|
|
const refreshBusy = ref(false);
|
|
|
|
|
const apps = ref([]);
|
|
|
|
|
const eventlogs = ref([]);
|
|
|
|
|
const activeId = ref(null);
|
|
|
|
|
const search = useDebouncedRef('');
|
|
|
|
|
const page = ref(1);
|
2025-03-23 17:16:50 +01:00
|
|
|
const perPage = ref(40);
|
2025-01-25 17:09:53 +01:00
|
|
|
const actions = reactive([]);
|
|
|
|
|
|
|
|
|
|
async function onRefresh() {
|
|
|
|
|
refreshBusy.value = true;
|
2025-03-10 16:16:04 +01:00
|
|
|
const [error, result] = await eventlogsModel.search(actions.join(','), search.value, page.value, perPage.value);
|
2025-01-25 17:09:53 +01:00
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
eventlogs.value = result.map(e => {
|
|
|
|
|
return {
|
|
|
|
|
id: Symbol(),
|
|
|
|
|
raw: e,
|
2025-02-21 16:30:59 +01:00
|
|
|
details: eventlogDetails(e, e.appId ? getApp(e.appId) : null),
|
|
|
|
|
source: eventlogSource(e, e.appId ? getApp(e.appId) : null)
|
2025-01-25 17:09:53 +01:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
refreshBusy.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onEventLogDetails(id) {
|
|
|
|
|
if (activeId.value === id) activeId.value = null;
|
|
|
|
|
else activeId.value = id;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-23 17:16:50 +01:00
|
|
|
async function onScroll(event) {
|
|
|
|
|
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) {
|
|
|
|
|
page.value++;
|
|
|
|
|
const [error, result] = await eventlogsModel.search(actions.join(','), search.value, page.value, perPage.value);
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
eventlogs.value = eventlogs.value.concat(result.map(e => {
|
|
|
|
|
return {
|
|
|
|
|
id: Symbol(),
|
|
|
|
|
raw: e,
|
|
|
|
|
details: eventlogDetails(e, e.appId ? getApp(e.appId) : null),
|
|
|
|
|
source: eventlogSource(e, e.appId ? getApp(e.appId) : null)
|
|
|
|
|
};
|
|
|
|
|
}));
|
|
|
|
|
}
|
2025-01-25 17:09:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
<template>
|
2025-03-23 17:16:50 +01:00
|
|
|
<div class="content" style="overflow: hidden; display: flex; flex-direction: column;">
|
2025-03-25 19:11:40 +01:00
|
|
|
<Section :title="$t('eventlog.title')">
|
|
|
|
|
<template #header-buttons>
|
2025-03-23 17:16:50 +01:00
|
|
|
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="search"/>
|
|
|
|
|
<MultiSelect v-model="actions" :options="availableActions" option-label="id" :selected-label="actions.length ? $t('main.multiselect.selected', { n: actions.length }) : $t('eventlog.filterAllEvents')"/>
|
2025-03-10 21:06:33 +01:00
|
|
|
<Button tool secondary @click="onRefresh()" :loading="refreshBusy" icon="fa-solid fa-sync-alt" />
|
2025-03-25 19:11:40 +01:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<center v-show="busy"><Spinner class="pankow-spinner-large" /></center>
|
|
|
|
|
<div style="overflow: auto; position: relative; margin-bottom: 10px;" @scroll="onScroll">
|
|
|
|
|
<table v-show="!busy" class="eventlog-table">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>{{ $t('eventlog.time') }}</th>
|
|
|
|
|
<th>{{ $t('eventlog.source') }}</th>
|
|
|
|
|
<th>{{ $t('eventlog.details') }}</th>
|
2025-03-23 17:16:50 +01:00
|
|
|
</tr>
|
2025-03-25 19:11:40 +01:00
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<template v-for="eventlog in eventlogs" :key="eventlog.id">
|
|
|
|
|
<tr @click="onEventLogDetails(eventlog.id)" :class="{ 'active': activeId === eventlog.id }" >
|
|
|
|
|
<td style="white-space: nowrap;"><span v-tooltip="prettyLongDate(eventlog.raw.creationTime)" class="arrow">{{ prettyDate(eventlog.raw.creationTime) }}</span></td>
|
|
|
|
|
<td>{{ eventlog.source }}</td>
|
|
|
|
|
<td v-html="eventlog.details"></td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr v-show="activeId === eventlog.id">
|
|
|
|
|
<td colspan="4" class="eventlog-details">
|
|
|
|
|
<p v-show="eventlog.raw.source.ip">Source IP: <code>{{ eventlog.raw.source.ip }}</code></p>
|
|
|
|
|
<pre>{{ JSON.stringify(eventlog.raw.data, null, 4) }}</pre>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</Section>
|
2025-01-25 17:09:53 +01:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
2025-03-23 17:16:50 +01:00
|
|
|
.eventlog-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 200px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-25 17:09:53 +01:00
|
|
|
.eventlog-filter {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.eventlog-details {
|
2025-03-23 17:16:50 +01:00
|
|
|
background-color: var(--pankow-color-background-hover);
|
|
|
|
|
cursor: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.eventlog-details pre {
|
2025-01-25 17:09:53 +01:00
|
|
|
white-space: pre-wrap;
|
|
|
|
|
color: var(--pankow-text-color);
|
2025-03-23 17:16:50 +01:00
|
|
|
font-size: 13px;
|
|
|
|
|
padding: 6px;
|
|
|
|
|
margin: 0;
|
2025-01-25 17:09:53 +01:00
|
|
|
border: none;
|
|
|
|
|
border-radius: var(--pankow-border-radius);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|