237 lines
9.4 KiB
Vue
237 lines
9.4 KiB
Vue
<script setup>
|
|
|
|
import { ref, reactive, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';
|
|
import { Button, TextInput, MultiSelect } from '@cloudron/pankow';
|
|
import { useDebouncedRef, copyToClipboard, prettyLongDate, prettyShortDate } 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 = [
|
|
{ separator: true, label: 'App' },
|
|
{ id: 'app.backup', label: 'Backup started' },
|
|
{ id: 'app.backup.finish', label: 'Backup finished' },
|
|
{ id: 'app.configure', label: 'Reconfigured' },
|
|
{ id: 'app.install', label: 'Installed' },
|
|
{ id: 'app.restore', label: 'Restored' },
|
|
{ id: 'app.uninstall', label: 'Uninstalled' },
|
|
{ id: 'app.update', label: 'Update started' },
|
|
{ id: 'app.update.finish', label: 'Update finished' },
|
|
{ id: 'app.login', label: 'Log in' },
|
|
{ id: 'app.oom', label: 'Out of memory' },
|
|
{ id: 'app.down', label: 'Down' },
|
|
{ id: 'app.up', label: 'Up' },
|
|
{ id: 'app.start', label: 'Started' },
|
|
{ id: 'app.stop', label: 'Stopped' },
|
|
{ id: 'app.restart', label: 'Restarted' },
|
|
{ separator: true, label: 'Platform backup' },
|
|
{ id: 'backup.cleanup', label: 'Cleanup started' },
|
|
{ id: 'backup.cleanup.finish', label: 'Cleanup finished' },
|
|
{ id: 'backup.start', label: 'Started' },
|
|
{ id: 'backup.finish', label: 'Finished' },
|
|
{ id: 'backuptarget.add', label: 'Site added' },
|
|
{ separator: true, label: 'Certificates' },
|
|
{ id: 'certificate.new', label: 'Obtained' },
|
|
{ id: 'certificate.renew', label: 'Renewed' },
|
|
{ id: 'certificate.cleanup', label: 'Cleaned up' },
|
|
{ separator: true, label: 'Domains' },
|
|
{ id: 'domain.add', label: 'Added' },
|
|
{ id: 'domain.update', label: 'Updated' },
|
|
{ id: 'domain.remove', label: 'Removed' },
|
|
{ separator: true, label: 'Email' },
|
|
{ id: 'mail.location', label: 'Location changed' },
|
|
{ id: 'mail.enabled', label: 'Enabled/Disabled' },
|
|
{ id: 'mail.box.add', label: 'Mailbox added' },
|
|
{ id: 'mail.box.update', label: 'Mailbox updated' },
|
|
{ id: 'mail.box.remove', label: 'Mailbox removed' },
|
|
{ id: 'mail.list.add', label: 'Mailinglist added' },
|
|
{ id: 'mail.list.update', label: 'Mailinglist updated' },
|
|
{ id: 'mail.list.remove', label: 'Mailinglist removed' },
|
|
{ separator: true, label: 'Services' },
|
|
{ id: 'service.configure', label: 'Configured' },
|
|
{ id: 'service.rebuild', label: 'Rebuilt' },
|
|
{ id: 'service.restart', label: 'Restarted' },
|
|
{ separator: true, label: 'Users' },
|
|
{ id: 'user.add', label: 'Added' },
|
|
{ id: 'user.update', label: 'Updated' },
|
|
{ id: 'user.remove', label: 'Removed' },
|
|
{ id: 'user.login', label: 'Logged in' },
|
|
{ id: 'user.login.ghost', label: 'Ghost logged in' },
|
|
{ id: 'user.logout', label: 'Logged out' },
|
|
// { id: 'user.transfer', label: 'Transferred' },
|
|
{ separator: true, label: 'Groups' },
|
|
{ id: 'group.add', label: 'Added' },
|
|
{ id: 'group.update', label: 'Updated' },
|
|
{ id: 'group.remove', label: 'Removed' },
|
|
{ separator: true, label: 'Volumes' },
|
|
{ id: 'volume.add', label: 'Added' },
|
|
{ id: 'volume.update', label: 'Updated' },
|
|
{ id: 'volume.remove', label: 'Removed' },
|
|
{ separator: true, label: 'Branding' },
|
|
{ id: 'branding.avatar', label: 'Avatar changed' },
|
|
{ id: 'branding.footer', label: 'Footer changed' },
|
|
{ id: 'branding.name', label: 'Name started' },
|
|
{ separator: true, label: 'Cloudron' },
|
|
{ id: 'cloudron.activate', label: 'Activated' },
|
|
{ id: 'cloudron.provision', label: 'Provisioned' },
|
|
{ id: 'cloudron.restore', label: 'Restored' },
|
|
{ id: 'cloudron.start', label: 'Started' },
|
|
{ id: 'cloudron.update', label: 'Update started' },
|
|
{ id: 'cloudron.update.finish', label: 'Update finished' },
|
|
{ id: 'dashboard.domain.update', label: 'Dashboard domain updated' },
|
|
{ id: 'dyndns.update', label: 'DynDNS changed' },
|
|
{ id: 'directoryserver.configure', label: 'LDAP configured ' },
|
|
{ id: 'externalldap.configure', label: 'External LDAP configured' },
|
|
{ id: 'userdirectory.profileconfig.update', label: 'Profile config changed' },
|
|
// { id: 'support.ssh', label: '' },
|
|
// { id: 'support.ticket', label: '' },
|
|
];
|
|
|
|
const refreshBusy = ref(false);
|
|
const apps = ref([]);
|
|
const eventlogs = ref([]);
|
|
const search = useDebouncedRef('');
|
|
const page = ref(1);
|
|
const perPage = ref(40);
|
|
const actions = reactive([]);
|
|
const eventlogContainer = useTemplateRef('eventlogContainer');
|
|
|
|
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.data?.appId ? getApp(e.data.appId) : null),
|
|
source: eventlogSource(e, e.data?.appId ? getApp(e.data.appId) : null)
|
|
};
|
|
});
|
|
|
|
refreshBusy.value = false;
|
|
}
|
|
|
|
async function fetchMore() {
|
|
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)
|
|
};
|
|
}));
|
|
}
|
|
|
|
async function onScroll(event) {
|
|
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) await fetchMore();
|
|
}
|
|
|
|
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();
|
|
|
|
while (eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) {
|
|
await fetchMore();
|
|
}
|
|
}
|
|
});
|
|
|
|
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="label" 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" ref="eventlogContainer" style="overflow: auto; margin-top: 10px; padding-top: 0px" @scroll="onScroll">
|
|
<div class="eventlog-item" v-for="eventlog in eventlogs" :key="eventlog.id" :class="{ 'active': eventlog.isOpen }" @click="eventlog.isOpen = !eventlog.isOpen">
|
|
<div class="eventlog-summary">
|
|
<div style="width: 160px; flex-shrink: 0;" class="pankow-no-mobile">{{ prettyLongDate(eventlog.raw.creationTime) }}</div>
|
|
<div style="width: 80px; flex-shrink: 0;" class="pankow-no-desktop">{{ prettyShortDate(eventlog.raw.creationTime) }}</div>
|
|
<div style="width: 160px; flex-shrink: 0; font-weight: bold; overflow: hidden; text-overflow: ellipsis;" class="pankow-no-mobile">{{ eventlog.source }}</div>
|
|
<div style="flex-grow: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" v-html="eventlog.details"></div>
|
|
<!-- <div style="width: 160px; flex-shrink: 0; cursor: copy; overflow: hidden; text-overflow: ellipsis;" v-if="eventlog.raw.source.ip" @click="onCopySource(eventlog)">{{ eventlog.raw.source.ip }}</div> -->
|
|
<div style="width: 40px; flex-shrink: 0;"><Button v-if="eventlog.raw.data.taskId" @click.stop plain small tool :href="`/logs.html?taskId=${eventlog.raw.data.taskId}`" target="_blank">Logs</Button></div>
|
|
</div>
|
|
<div v-show="eventlog.isOpen" class="eventlog-details" @click.stop>
|
|
<pre>{{ JSON.stringify(eventlog.raw.data, null, 4) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
|
|
.eventlog-item {
|
|
border-radius: var(--pankow-border-radius);
|
|
cursor: pointer;
|
|
/*padding: 5px 10px;*/
|
|
}
|
|
|
|
.eventlog-item.active,
|
|
.eventlog-item:hover {
|
|
background-color: var(--pankow-color-background-hover);
|
|
}
|
|
|
|
.eventlog-summary {
|
|
display: flex;
|
|
padding: 5px 10px;
|
|
}
|
|
|
|
.eventlog-details {
|
|
background-color: color-mix(in oklab, var(--pankow-color-background-hover), black 5%);
|
|
cursor: auto;
|
|
position: relative;
|
|
border-radius: var(--pankow-border-radius);
|
|
padding: 5px 0;
|
|
}
|
|
|
|
</style>
|