mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 21:33:18 +01:00
194 lines
5.7 KiB
Vue
194 lines
5.7 KiB
Vue
<template>
|
|
<header class="flex items-center gap-4">
|
|
<Tag :data-level="entry.level" class="uppercase text-white" v-if="entry.level">{{ entry.level }}</Tag>
|
|
<h1 class="mobile-hidden text-lg">
|
|
<DateTime :date="entry.date" />
|
|
</h1>
|
|
<h2 class="text-sm"><DistanceTime :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 font-thin">
|
|
Raw JSON
|
|
|
|
<UseClipboard v-slot="{ copy, copied }" :source="JSON.stringify(entry.unfilteredMessage)">
|
|
<button class="swap swap-flip outline-none" @click="copy()" :class="{ 'hover:swap-active': copied }">
|
|
<mdi:check class="swap-on" />
|
|
<mdi:content-copy class="swap-off" />
|
|
</button>
|
|
</UseClipboard>
|
|
</div>
|
|
<div class="max-h-48 overflow-scroll rounded border border-base-lighter bg-base-darker p-2">
|
|
<pre v-html="syntaxHighlight(entry.unfilteredMessage)"></pre>
|
|
</div>
|
|
</section>
|
|
<table class="table table-pin-rows 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="mobile-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 break-all font-mono">
|
|
{{ key.join(".") }}
|
|
</td>
|
|
<td class="mobile-hidden truncate">
|
|
<code v-html="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 { useSortable } from "@vueuse/integrations/useSortable";
|
|
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();
|
|
|
|
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 allFields = flattenJSONToMap(entry.unfilteredMessage);
|
|
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(entry.unfilteredMessage, 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);
|
|
}
|
|
},
|
|
});
|
|
|
|
function syntaxHighlight(json: any) {
|
|
json = JSON.stringify(json, null, 2);
|
|
return json.replace(
|
|
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g,
|
|
function (match: string) {
|
|
var cls = "json-number";
|
|
if (/^"/.test(match)) {
|
|
if (/:$/.test(match)) {
|
|
cls = "json-key";
|
|
} else {
|
|
cls = "json-string";
|
|
}
|
|
} else if (/true|false/.test(match)) {
|
|
cls = "json-boolean";
|
|
} else if (/null/.test(match)) {
|
|
cls = "json-null";
|
|
}
|
|
return `<span class="${cls}">${match}</span>`;
|
|
},
|
|
);
|
|
}
|
|
|
|
useSortable(list, fields);
|
|
</script>
|
|
<style lang="postcss" scoped>
|
|
.font-mono {
|
|
font-family:
|
|
ui-monospace,
|
|
SFMono-Regular,
|
|
SF Mono,
|
|
Consolas,
|
|
Liberation Mono,
|
|
monaco,
|
|
Menlo,
|
|
monospace;
|
|
}
|
|
|
|
pre {
|
|
:deep(.json-key) {
|
|
@apply text-blue;
|
|
}
|
|
:deep(.json-string) {
|
|
@apply text-green;
|
|
}
|
|
:deep(.json-number) {
|
|
@apply text-orange;
|
|
}
|
|
:deep(.json-boolean) {
|
|
@apply text-purple;
|
|
}
|
|
:deep(.json-null) {
|
|
@apply text-red;
|
|
}
|
|
}
|
|
</style>
|