Files
dozzle/assets/components/LogViewer/LogDetails.vue
T

184 lines
5.7 KiB
Vue

<template>
<header class="flex items-center gap-4">
<Tag :data-level="entry.level" class="show-unknown text-white uppercase" v-if="entry.level">{{ entry.level }}</Tag>
<h1 class="text-lg max-md:hidden">
<DateTime :date="entry.date" />
</h1>
<h2 class="text-sm"><RelativeTime :date="entry.date" /> on {{ entry.std }}</h2>
</header>
<div class="mt-8 flex flex-col gap-10">
<section class="grid grid-cols-3 gap-2">
<div>
<div class="font-thin">Container Name</div>
<div class="truncate text-lg font-bold">{{ container.name }}</div>
</div>
<div>
<div class="font-thin">Host</div>
<div class="truncate text-lg font-bold">
{{ hosts[container.host].name }}
</div>
</div>
<div>
<div class="font-thin">Image</div>
<div class="truncate text-lg font-bold">{{ container.image }}</div>
</div>
</section>
<section class="flex flex-col gap-2">
<div class="flex gap-2">
Raw JSON
<UseClipboard v-slot="{ copy, copied }" :source="entry.rawMessage">
<button class="icon-btn swap outline-hidden" @click="copy()" :class="{ 'hover:swap-active': copied }">
<mdi:check class="swap-on" />
<material-symbols:content-copy class="swap-off" />
</button>
</UseClipboard>
<button class="icon-btn outline-hidden" @click="downloadJSON()" title="Download JSON">
<material-symbols:download />
</button>
</div>
<div class="bg-base-200 max-h-125 overflow-scroll rounded-sm border border-white/20 p-2">
<JsonFormatted :value="entry.rawMessage" class="text-sm" />
</div>
</section>
<table class="table-pin-rows table table-fixed" v-if="entry instanceof ComplexLogEntry">
<caption class="caption-bottom">
Fields are sortable by dragging and dropping.
</caption>
<thead class="text-lg">
<tr>
<th class="w-60">Field</th>
<th class="max-md:hidden">Value</th>
<th class="w-20">
<input type="checkbox" class="toggle toggle-primary" v-model="toggleAllFields" title="Toggle all" />
</th>
</tr>
</thead>
<tbody ref="list">
<tr v-for="{ key, value, enabled } in fields" :key="key.join('.')" class="hover">
<td class="cursor-move font-mono break-all">
{{ key.join(".") }}
</td>
<td class="truncate max-md:hidden">
<code>{{ JSON.stringify(value) }}</code>
</td>
<td>
<input type="checkbox" class="toggle toggle-primary" :checked="enabled" @change="toggleField(key)" />
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { ComplexLogEntry } from "@/models/LogEntry";
import { UseClipboard } from "@vueuse/components";
const { entry } = defineProps<{ entry: ComplexLogEntry }>();
const { currentContainer } = useContainerStore();
const list = ref<HTMLElement>();
const container = currentContainer(toRef(() => entry.containerID));
const visibleKeys = persistentVisibleKeysForContainer(container);
const { hosts } = useHosts();
const { useSortable } = await import("@vueuse/integrations/useSortable");
function downloadJSON() {
let content = entry.rawMessage;
try {
content = JSON.stringify(JSON.parse(content), null, 2);
} catch {
// not valid JSON, download as is
}
const name = (container.value?.name ?? "log").replace(/[^\w.-]+/g, "-");
const timestamp = entry.date.toISOString().replace(/[:.]/g, "-");
const url = URL.createObjectURL(new Blob([content], { type: "application/json" }));
const link = document.createElement("a");
link.href = url;
link.download = `${name}-${timestamp}.json`;
link.click();
URL.revokeObjectURL(url);
}
function toggleField(key: string[]) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
const enabled = visibleKeys.value.get(key) ?? true;
visibleKeys.value.set(key, !enabled);
}
const fields = computed({
get() {
const fieldsWithValue: { key: string[]; value: any; enabled: boolean }[] = [];
const rawFields = JSON.parse(entry.rawMessage);
const allFields = flattenJSONToMap(rawFields);
if (visibleKeys.value.size === 0) {
for (const [key, value] of allFields) {
fieldsWithValue.push({ key, value, enabled: true });
}
} else {
for (const [key, enabled] of visibleKeys.value) {
const value = getDeep(rawFields, key);
fieldsWithValue.push({ key, value, enabled });
}
for (const [key, value] of allFields) {
if ([...visibleKeys.value.keys()].findIndex((k) => arrayEquals(k, key)) === -1) {
fieldsWithValue.push({ key, value, enabled: true });
}
}
}
return fieldsWithValue;
},
set(value) {
const map = new Map<string[], boolean>();
for (const { key, enabled } of value) {
map.set(key, enabled);
}
visibleKeys.value = map;
},
});
const toggleAllFields = computed({
get: () => fields.value.every(({ enabled }) => enabled),
set(value) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
for (const key of visibleKeys.value.keys()) {
visibleKeys.value.set(key, value);
}
for (const field of fields.value) {
visibleKeys.value.set(field.key, value);
}
},
});
useSortable(list, fields);
</script>
<style scoped>
@reference "@/main.css";
.font-mono {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
}
</style>