Files
cloudron-box/dashboard/src/views/EventlogView.vue
2025-09-26 11:13:32 +02:00

207 lines
6.6 KiB
Vue

<script setup>
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
import moment from 'moment';
import { Button, TextInput, MultiSelect } from '@cloudron/pankow';
import { useDebouncedRef, copyToClipboard } from '@cloudron/pankow/utils';
import AppsModel from '../models/AppsModel.js';
import EventlogsModel from '../models/EventlogsModel.js';
import { eventlogDetails, eventlogSource } from '../utils.js';
const appsModel = AppsModel.create();
const eventlogsModel = EventlogsModel.create();
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 refreshBusy = ref(false);
const apps = ref([]);
const eventlogs = ref([]);
const search = useDebouncedRef('');
const page = ref(1);
const perPage = ref(40);
const actions = reactive([]);
async function onRefresh() {
refreshBusy.value = true;
page.value = 1;
const [error, result] = await eventlogsModel.search(actions.join(','), search.value, page.value, perPage.value);
if (error) return console.error(error);
eventlogs.value = 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)
};
});
refreshBusy.value = false;
}
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)
};
}));
}
}
function onCopySource(eventlog) {
copyToClipboard(eventlog.raw.source.ip);
window.pankow.notify({ type: 'success', text: 'Copied' });
}
watch(actions, onRefresh);
watch(search, onRefresh);
async function onHashChange() {
const params = new URLSearchParams(window.location.hash.slice(window.location.hash.indexOf('?')));
search.value = params.get('search') || '';
}
onMounted(async () => {
const [error, result] = await appsModel.list();
if (error) console.error(error);
else apps.value = result;
window.addEventListener('hashchange', onHashChange);
onHashChange();
if (!search.value) onRefresh();
});
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange);
});
</script>
<template>
<div class="content-large" 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;">
<h1 class="section-header">
{{ $t('eventlog.title') }}
<div>
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="search"/>
<MultiSelect :search-threshold="10" v-model="actions" :options="availableActions" option-label="id" option-key="id" :selected-label="actions.length ? $t('main.multiselect.selected', { n: actions.length }) : $t('eventlog.filterAllEvents')"/>
<Button tool secondary @click="onRefresh()" :loading="refreshBusy" icon="fa-solid fa-sync-alt" />
</div>
</h1>
<div class="section-body" style="overflow: auto; margin-top: 10px; padding-top: 0px" @scroll="onScroll">
<table class="eventlog-table">
<thead>
<tr>
<th>{{ $t('eventlog.time') }}</th>
<th></th>
<th>{{ $t('eventlog.source') }}</th>
<th>{{ $t('eventlog.details') }}</th>
</tr>
</thead>
<tbody>
<template v-for="eventlog in eventlogs" :key="eventlog.id">
<tr @click="eventlog.isOpen = !eventlog.isOpen" :class="{ 'active': eventlog.isOpen }" >
<td style="white-space: nowrap;">{{ moment(eventlog.raw.creationTime).format('DD.MM.YYYY') }}</td>
<td style="white-space: nowrap;">{{ moment(eventlog.raw.creationTime).format('HH:mm:SS') }}</td>
<td>{{ eventlog.source }}</td>
<td v-html="eventlog.details"></td>
</tr>
<tr v-show="eventlog.isOpen">
<td colspan="4" class="eventlog-details">
<div v-if="eventlog.raw.source.ip" class="eventlog-source">Source IP: <span @click="onCopySource(eventlog)">{{ eventlog.raw.source.ip }}</span></div>
<pre>{{ JSON.stringify(eventlog.raw.data, null, 4) }}</pre>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</template>
<style scoped>
</style>