1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-24 06:28:42 +01:00

feat: adds a side panel for complex logs that enables reordering and enabling fields (#3237)

This commit is contained in:
Amir Raminfar
2024-08-28 17:37:04 -07:00
committed by GitHub
parent 62a2ef9eb4
commit 85271a595a
31 changed files with 345 additions and 174 deletions

View File

@@ -50,6 +50,7 @@ declare global {
const effectScope: typeof import('vue')['effectScope'] const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef'] const extendRef: typeof import('@vueuse/core')['extendRef']
const flattenJSON: typeof import('./utils/index')['flattenJSON'] const flattenJSON: typeof import('./utils/index')['flattenJSON']
const flattenJSONToMap: typeof import('./utils/index')['flattenJSONToMap']
const formatBytes: typeof import('./utils/index')['formatBytes'] const formatBytes: typeof import('./utils/index')['formatBytes']
const getActivePinia: typeof import('pinia')['getActivePinia'] const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
@@ -104,6 +105,7 @@ declare global {
const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer'] const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer']
const pinnedContainers: typeof import('./composable/storage')['pinnedContainers'] const pinnedContainers: typeof import('./composable/storage')['pinnedContainers']
const provide: typeof import('vue')['provide'] const provide: typeof import('vue')['provide']
const provideDetails: typeof import('./composable/showDetails')['provideDetails']
const provideLocal: typeof import('@vueuse/core')['provideLocal'] const provideLocal: typeof import('@vueuse/core')['provideLocal']
const provideLogDetails: typeof import('./composable/showLogDetails')['provideLogDetails'] const provideLogDetails: typeof import('./composable/showLogDetails')['provideLogDetails']
const provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext'] const provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext']
@@ -205,6 +207,7 @@ declare global {
const useDebounce: typeof import('@vueuse/core')['useDebounce'] const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDetails: typeof import('./composable/showDetails')['useDetails']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
@@ -413,6 +416,7 @@ declare module 'vue' {
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']> readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']> readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly flattenJSONToMap: UnwrapRef<typeof import('./utils/index')['flattenJSONToMap']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']> readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']> readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
@@ -468,6 +472,7 @@ declare module 'vue' {
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']> readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']> readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']> readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['provideLogDetails']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']> readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']> readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
@@ -497,6 +502,7 @@ declare module 'vue' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']> readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['showLogDetails']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']> readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']> readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']> readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
@@ -607,6 +613,7 @@ declare module 'vue' {
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']> readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']> readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']> readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']> readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']> readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']> readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
@@ -766,6 +773,7 @@ declare module '@vue/runtime-core' {
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']> readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']> readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly flattenJSONToMap: UnwrapRef<typeof import('./utils/index')['flattenJSONToMap']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']> readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']> readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
@@ -821,6 +829,7 @@ declare module '@vue/runtime-core' {
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']> readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']> readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']> readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['provideLogDetails']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']> readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']> readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
@@ -850,6 +859,7 @@ declare module '@vue/runtime-core' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']> readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['showLogDetails']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']> readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']> readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']> readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
@@ -960,6 +970,7 @@ declare module '@vue/runtime-core' {
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']> readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']> readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']> readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']> readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']> readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']> readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>

View File

@@ -49,6 +49,7 @@ declare module 'vue' {
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default'] LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
Links: typeof import('./components/Links.vue')['default'] Links: typeof import('./components/Links.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default'] LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default'] LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
LogList: typeof import('./components/LogViewer/LogList.vue')['default'] LogList: typeof import('./components/LogViewer/LogList.vue')['default']
LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default'] LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default']
@@ -69,6 +70,7 @@ declare module 'vue' {
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default'] 'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default'] 'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default'] 'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
'Mdi:mdi:close': typeof import('~icons/mdi/mdi')['default']
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default'] 'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default'] MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default'] MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
@@ -97,6 +99,7 @@ declare module 'vue' {
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default'] ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
Search: typeof import('./components/Search.vue')['default'] Search: typeof import('./components/Search.vue')['default']
ServiceLog: typeof import('./components/ServiceViewer/ServiceLog.vue')['default'] ServiceLog: typeof import('./components/ServiceViewer/ServiceLog.vue')['default']
SideDrawer: typeof import('./components/common/SideDrawer.vue')['default']
SideMenu: typeof import('./components/SideMenu.vue')['default'] SideMenu: typeof import('./components/SideMenu.vue')['default']
SidePanel: typeof import('./components/SidePanel.vue')['default'] SidePanel: typeof import('./components/SidePanel.vue')['default']
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default'] SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']

View File

@@ -13,7 +13,12 @@
v-model="query" v-model="query"
:placeholder="$t('placeholder.search-containers')" :placeholder="$t('placeholder.search-containers')"
/> />
<mdi:keyboard-esc class="flex" /> <form method="dialog">
<button class="swap swap-rotate hover:swap-active">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
</div> </div>
<div <div
class="dropdown-content !relative mt-2 max-h-[calc(100dvh-20rem)] w-full overflow-y-scroll rounded-md border-y-8 border-transparent bg-base-lighter px-2" class="dropdown-content !relative mt-2 max-h-[calc(100dvh-20rem)] w-full overflow-y-scroll rounded-md border-y-8 border-transparent bg-base-lighter px-2"

View File

@@ -16,7 +16,7 @@
ref="viewer" ref="viewer"
:stream-source="useGroupedStream" :stream-source="useGroupedStream"
:entity="group" :entity="group"
:visible-keys="visibleKeys" :visible-keys="new Map<string[], boolean>()"
:show-container-name="true" :show-container-name="true"
/> />
</template> </template>
@@ -43,6 +43,5 @@ const { customGroups } = storeToRefs(swarmStore);
const group = computed(() => customGroups.value.find((g) => g.name === name) ?? new GroupedContainers("", [])); const group = computed(() => customGroups.value.find((g) => g.name === name) ?? new GroupedContainers("", []));
const visibleKeys = ref<string[][]>([]);
provideLoggingContext(toRef(() => group.value.containers)); provideLoggingContext(toRef(() => group.value.containers));
</script> </script>

View File

@@ -1,10 +1,5 @@
<template> <template>
<div class="group/item relative flex w-full gap-x-2" @click="expandToggle()"> <div class="group/item relative flex w-full gap-x-2 hover:bg-secondary/10" @click="showLogDetails(logEntry)">
<label class="swap swap-rotate invisible absolute -left-4 top-0.5 size-4 group-hover/item:visible">
<input type="checkbox" v-model="expanded" @click="expandToggle()" />
<material-symbols:expand-all-rounded class="swap-off text-secondary" />
<material-symbols:collapse-all-rounded class="swap-on text-secondary" />
</label>
<div v-if="showContainerName"> <div v-if="showContainerName">
<ContainerName :id="logEntry.containerID" /> <ContainerName :id="logEntry.containerID" />
</div> </div>
@@ -18,7 +13,7 @@
<LogLevel :level="logEntry.level" /> <LogLevel :level="logEntry.level" />
</div> </div>
<div> <div>
<ul class="fields cursor-pointer space-x-4" :class="{ expanded }"> <ul class="fields cursor-pointer space-x-4">
<li v-for="(value, name) in validValues" :key="name"> <li v-for="(value, name) in validValues" :key="name">
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span> <span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)"> <template v-else-if="Array.isArray(value)">
@@ -28,7 +23,6 @@
</li> </li>
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li> <li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul> </ul>
<FieldList :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys" />
</div> </div>
<LogMessageActions <LogMessageActions
class="duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100" class="duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100"
@@ -44,15 +38,14 @@ const { markSearch } = useSearchFilter();
const { logEntry, showContainerName = false } = defineProps<{ const { logEntry, showContainerName = false } = defineProps<{
logEntry: ComplexLogEntry; logEntry: ComplexLogEntry;
visibleKeys: string[][];
showContainerName?: boolean; showContainerName?: boolean;
}>(); }>();
const [expanded, expandToggle] = useToggle();
const validValues = computed(() => { const validValues = computed(() => {
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined)); return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
}); });
const showLogDetails = useLogDetails();
</script> </script>
<style lang="postcss" scoped> <style lang="postcss" scoped>

View File

@@ -1,74 +0,0 @@
<template>
<ul v-if="expanded" ref="root" class="ml-8">
<li v-for="(value, name) in fields" :key="name">
<template v-if="isObject(value)">
<span class="text-light">{{ name }}=</span>
<FieldList :fields="value" :parent-key="parentKey.concat(name)" :visible-keys="visibleKeys" expanded />
</template>
<template v-else-if="Array.isArray(value)">
<a @click.stop="toggleField(name)" class="link-primary mr-2 cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}
</a>
<span class="text-light">{{ name }}=</span>[
<span class="font-bold" v-for="(item, index) in value">
<span v-html="JSON.stringify(item)"></span><span v-if="index !== value.length - 1">,</span>
</span>
]
</template>
<template v-else>
<a @click.stop="toggleField(name)" class="link-primary mr-2 cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}
</a>
<span class="text-light">{{ name }}=</span><span class="font-bold" v-html="value"></span>
</template>
</li>
</ul>
</template>
<script lang="ts" setup>
import { arrayEquals, isObject } from "@/utils";
const {
fields,
expanded = false,
parentKey = [],
visibleKeys = [],
} = defineProps<{
fields: Record<string, any>;
expanded?: boolean;
parentKey?: string[];
visibleKeys?: string[][];
}>();
const root = ref<HTMLElement>();
async function toggleField(field: string) {
const index = fieldIndex(field);
if (index > -1) {
visibleKeys.splice(index, 1);
} else {
visibleKeys.push(parentKey.concat(field));
}
await nextTick();
root.value?.scrollIntoView({
block: "center",
});
}
function hasField(field: string) {
return fieldIndex(field) > -1;
}
function fieldIndex(field: string) {
const path = parentKey.concat(field);
return visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
}
</script>
<style scoped lang="postcss">
.text-light {
@apply text-base-content/70;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<header class="flex items-center gap-4">
<Tag :data-level="entry.level" class="uppercase text-white">{{ entry.level }}</Tag>
<h1 class="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-4">
<section class="grid grid-cols-3 gap-2">
<div>
<div class="font-thin">Container Name</div>
<div class="text-lg font-bold">{{ container.name }}</div>
</div>
<div>
<div class="font-thin">Host</div>
<div class="text-lg font-bold">
{{ hosts[container.host].name }}
</div>
</div>
<div>
<div class="font-thin">Image</div>
<div class="text-lg font-bold">{{ container.image }}</div>
</div>
</section>
<table class="table" v-if="entry instanceof ComplexLogEntry">
<thead class="text-lg">
<tr>
<th>Field</th>
<th>Value</th>
<th>Show</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">
{{ key.join(".") }}
</td>
<td>
<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 DistanceTime from "../common/DistanceTime.vue";
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);
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: false });
}
}
}
return fieldsWithValue;
},
set(value) {
const map = new Map<string[], boolean>();
for (const { key, enabled } of value) {
map.set(key, enabled);
}
visibleKeys.value = map;
},
});
useSortable(list, fields);
</script>
<style lang="postcss" scoped>
.font-mono {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
}
</style>

View File

@@ -32,25 +32,26 @@ defineProps<{
margin-top: -0.4em; margin-top: -0.4em;
align-self: flex-start; align-self: flex-start;
} }
</style>
<style lang="postcss">
[data-level="debug"], [data-level="debug"],
[data-level="trace"] { [data-level="trace"] {
@apply bg-purple; @apply !bg-purple;
} }
[data-level="info"] { [data-level="info"] {
@apply bg-green; @apply !bg-green;
} }
[data-level="error"], [data-level="error"],
[data-level="severe"], [data-level="severe"],
[data-level="critical"], [data-level="critical"],
[data-level="fatal"] { [data-level="fatal"] {
@apply bg-red; @apply !bg-red;
} }
[data-level="warn"], [data-level="warn"],
[data-level="warning"] { [data-level="warning"] {
@apply bg-orange; @apply !bg-orange;
} }
</style> </style>

View File

@@ -13,12 +13,7 @@
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }" :class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
class="group/entry" class="group/entry"
> >
<component <component :is="item.getComponent()" :log-entry="item" :show-container-name="showContainerName" />
:is="item.getComponent()"
:log-entry="item"
:visible-keys="visibleKeys"
:show-container-name="showContainerName"
/>
</li> </li>
</ul> </ul>
</template> </template>
@@ -30,7 +25,6 @@ const { loading, progress, currentDate } = useScrollContext();
const { messages } = defineProps<{ const { messages } = defineProps<{
messages: LogEntry<string | JSONObject>[]; messages: LogEntry<string | JSONObject>[];
visibleKeys: string[][];
lastSelectedItem: LogEntry<string | JSONObject> | undefined; lastSelectedItem: LogEntry<string | JSONObject> | undefined;
showContainerName: boolean; showContainerName: boolean;
}>(); }>();

View File

@@ -7,7 +7,7 @@
> >
<span <span
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700" class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
@click="copyLogMessageToClipBoard()" @click.prevent="copyLogMessageToClipBoard()"
> >
<carbon:copy-file /> <carbon:copy-file />
</span> </span>
@@ -19,7 +19,7 @@
> >
<a <a
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700" class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
@click="handleJumpLineSelected($event, logEntry)" @click.prevent="handleJumpLineSelected($event, logEntry)"
:href="`#${logEntry.id}`" :href="`#${logEntry.id}`"
> >
<carbon:search-locate /> <carbon:search-locate />

View File

@@ -1,20 +1,18 @@
<template> <template>
<LogList <SideDrawer ref="drawer">
:messages="filtered" <LogDetails :entry="entry" v-if="entry && entry instanceof ComplexLogEntry" />
:last-selected-item="lastSelectedItem" </SideDrawer>
:visible-keys="visibleKeys" <LogList :messages="filtered" :last-selected-item="lastSelectedItem" :show-container-name="showContainerName" />
:show-container-name="showContainerName"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useRouteHash } from "@vueuse/router"; import { useRouteHash } from "@vueuse/router";
import SideDrawer from "@/components/common/SideDrawer.vue";
import { type JSONObject, LogEntry } from "@/models/LogEntry"; import { ComplexLogEntry, type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{ const props = defineProps<{
messages: LogEntry<string | JSONObject>[]; messages: LogEntry<string | JSONObject>[];
visibleKeys: string[][]; visibleKeys: Map<string[], boolean>;
showContainerName: boolean; showContainerName: boolean;
}>(); }>();
@@ -23,6 +21,10 @@ const { messages, visibleKeys } = toRefs(props);
const { filteredPayload } = useVisibleFilter(visibleKeys); const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages } = useSearchFilter(); const { filteredMessages } = useSearchFilter();
const drawer = ref<InstanceType<typeof SideDrawer>>() as Ref<InstanceType<typeof SideDrawer>>;
const { entry } = provideLogDetails(drawer);
const visible = filteredPayload(messages); const visible = filteredPayload(messages);
const filtered = filteredMessages(visible); const filtered = filteredMessages(visible);

View File

@@ -10,7 +10,7 @@ import { LogStreamSource } from "@/composable/eventStreams";
const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{ const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource; streamSource: (t: Ref<T>) => LogStreamSource;
visibleKeys: string[][]; visibleKeys: Map<string[], boolean>;
showContainerName: boolean; showContainerName: boolean;
entity: T; entity: T;
}>(); }>();

View File

@@ -3,7 +3,7 @@
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = ` exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium"> "<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry"> <li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys=""> <div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small"> <div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
@@ -23,7 +23,7 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = ` exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium"> "<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry"> <li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys=""> <div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small"> <div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
@@ -43,7 +43,7 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = ` exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium"> "<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry"> <li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys=""> <div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small"> <div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
@@ -63,7 +63,7 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
exports[`<ContainerEventSource /> > render html correctly > should render messages with filter 1`] = ` exports[`<ContainerEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium"> "<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="2" data-time="1560336942459" class="group/entry"> <li data-v-cf9ff940="" data-key="2" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys=""> <div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small"> <div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
@@ -85,7 +85,7 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
exports[`<ContainerEventSource /> > render html correctly > should render messages with html entities 1`] = ` exports[`<ContainerEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium"> "<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry"> <li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys=""> <div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small"> <div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
@@ -104,6 +104,16 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
exports[`<ContainerEventSource /> > renders loading correctly 1`] = ` exports[`<ContainerEventSource /> > renders loading correctly 1`] = `
"<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div> "<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div>
<dialog data-v-43493931="" class="modal-right modal items-start outline-none backdrop:bg-none">
<div data-v-43493931="" class="modal-box">
<form data-v-43493931="" method="dialog"><button data-v-43493931="" class="swap swap-rotate absolute right-4 top-4 hover:swap-active"><svg data-v-43493931="" viewBox="0 0 24 24" width="1.2em" height="1.2em" class="swap-off">
<path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"></path>
</svg><svg data-v-43493931="" viewBox="0 0 24 24" width="1.2em" height="1.2em" class="swap-on">
<path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"></path>
</svg></button></form>
</div>
<form data-v-43493931="" method="dialog" class="modal-backdrop"><button data-v-43493931="">close</button></form>
</dialog>
<!--v-if-->" <!--v-if-->"
`; `;

View File

@@ -15,7 +15,7 @@
ref="viewer" ref="viewer"
:stream-source="useMergedStream" :stream-source="useMergedStream"
:entity="containers" :entity="containers"
:visible-keys="visibleKeys" :visible-keys="new Map<string[], boolean>()"
:show-container-name="true" :show-container-name="true"
/> />
</template> </template>
@@ -35,8 +35,5 @@ const containerStore = useContainerStore();
const { allContainersById, ready } = storeToRefs(containerStore); const { allContainersById, ready } = storeToRefs(containerStore);
const containers = computed(() => ids.map((id) => allContainersById.value[id])); const containers = computed(() => ids.map((id) => allContainersById.value[id]));
const visibleKeys = ref<string[][]>([]);
provideLoggingContext(containers); provideLoggingContext(containers);
</script> </script>

View File

@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div <div
class="animate-background h-1 bg-gradient-to-br from-primary via-transparent to-primary" class="animate-background h-1 bg-gradient-to-br from-primary via-primary/20 to-primary"
v-show="!scrollContext.paused && !scrollContext.loading" v-show="!scrollContext.paused && !scrollContext.loading"
></div> ></div>
<div ref="scrollObserver" class="h-px"></div> <div ref="scrollObserver" class="h-px"></div>
@@ -96,7 +96,7 @@ function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
.animate-background { .animate-background {
background-size: 400% 400%; background-size: 400% 400%;
animation: gradient-animation 4s ease infinite; animation: gradient-animation 5s ease infinite;
} }
@keyframes gradient-animation { @keyframes gradient-animation {

View File

@@ -19,7 +19,7 @@
ref="viewer" ref="viewer"
:stream-source="useServiceStream" :stream-source="useServiceStream"
:entity="service" :entity="service"
:visible-keys="visibleKeys" :visible-keys="new Map<string[], boolean>()"
:show-container-name="true" :show-container-name="true"
/> />
</template> </template>
@@ -36,7 +36,6 @@ const { name, scrollable = false } = defineProps<{
name: string; name: string;
}>(); }>();
const visibleKeys = ref<string[][]>([]);
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>(); const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const store = useSwarmStore(); const store = useSwarmStore();
const { services } = storeToRefs(store) as unknown as { services: Ref<Service[]> }; const { services } = storeToRefs(store) as unknown as { services: Ref<Service[]> };

View File

@@ -22,7 +22,7 @@
ref="viewer" ref="viewer"
:stream-source="useStackStream" :stream-source="useStackStream"
:entity="stack" :entity="stack"
:visible-keys="visibleKeys" :visible-keys="new Map<string[], boolean>()"
:show-container-name="true" :show-container-name="true"
/> />
</template> </template>
@@ -38,7 +38,6 @@ const { name, scrollable = false } = defineProps<{
name: string; name: string;
}>(); }>();
const visibleKeys = ref<string[][]>([]);
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>(); const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const store = useSwarmStore(); const store = useSwarmStore();
const { stacks } = storeToRefs(store) as unknown as { stacks: Ref<Stack[]> }; const { stacks } = storeToRefs(store) as unknown as { stacks: Ref<Stack[]> };

View File

@@ -0,0 +1,34 @@
<template>
<dialog ref="panel" class="modal-right modal items-start outline-none backdrop:bg-none">
<div class="modal-box">
<form method="dialog">
<button class="swap swap-rotate absolute right-4 top-4 hover:swap-active">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
<slot></slot>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</template>
<script setup lang="ts">
const panel = ref<HTMLDialogElement>();
defineExpose({
open: () => {
panel.value?.showModal();
},
});
</script>
<style scoped lang="postcss">
.modal-right :where(.modal-box) {
@apply fixed right-0 h-lvh max-h-screen max-w-3xl translate-x-24 scale-100 rounded-none bg-base-lighter shadow-none;
}
.modal-right[open] .modal-box {
@apply translate-x-0;
}
</style>

View File

@@ -1,14 +1,34 @@
import { Profile } from "@/stores/config"; import { Profile } from "@/stores/config";
export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue: NonNullable<Profile[K]>) { interface SerializerTransformer<T, U> {
from: (raw: U) => T;
to: (value: T) => U;
}
export function useProfileStorage<K extends keyof Profile>(
key: K,
defaultValue: NonNullable<Profile[K]>,
transformer?: SerializerTransformer<NonNullable<Profile[K]>, any>,
) {
const storageKey = "DOZZLE_" + key.toUpperCase(); const storageKey = "DOZZLE_" + key.toUpperCase();
const storage = useStorage<NonNullable<Profile[K]>>(storageKey, defaultValue, undefined, { const storage = useStorage<NonNullable<Profile[K]>>(storageKey, defaultValue, undefined, {
writeDefaults: false, writeDefaults: false,
mergeDefaults: true, mergeDefaults: true,
serializer: transformer
? {
read: (raw) => transformer.from(JSON.parse(raw)),
write: (value) => JSON.stringify(transformer.to(value)),
}
: undefined,
onError: (e) => {
console.error(`Failed to read ${storageKey} from storage`, e);
},
}); });
if (config.profile?.[key]) { if (config.profile?.[key]) {
if (storage.value instanceof Set && config.profile[key] instanceof Array) { if (transformer) {
storage.value = transformer.from(config.profile[key]);
} else if (storage.value instanceof Set && config.profile[key] instanceof Array) {
storage.value = new Set([...(config.profile[key] as Iterable<any>)]) as unknown as NonNullable<Profile[K]>; storage.value = new Set([...(config.profile[key] as Iterable<any>)]) as unknown as NonNullable<Profile[K]>;
} else if (config.profile[key] instanceof Array) { } else if (config.profile[key] instanceof Array) {
storage.value = config.profile[key] as NonNullable<Profile[K]>; storage.value = config.profile[key] as NonNullable<Profile[K]>;
@@ -23,9 +43,19 @@ export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue:
watch( watch(
storage, storage,
(value) => { (value) => {
const transformedValue = transformer ? transformer.to(value) : value;
fetch(withBase("/api/profile"), { fetch(withBase("/api/profile"), {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ [key]: value }, (_, value) => (value instanceof Set ? [...value] : value)), body: JSON.stringify({ [key]: transformedValue }, (_, value) => {
if (value instanceof Set) {
return Array.from(value);
} else if (value instanceof Map) {
return Array.from(value.entries());
} else {
return value;
}
}),
}); });
}, },
{ deep: true }, { deep: true },

View File

@@ -0,0 +1,22 @@
import { JSONObject, LogEntry } from "@/models/LogEntry";
import SideDrawer from "@/components/common/SideDrawer.vue";
export const showLogDetails = Symbol("showLogDetails") as InjectionKey<
(logEntry: LogEntry<string | JSONObject>) => void
>;
export const provideLogDetails = (drawer: Ref<InstanceType<typeof SideDrawer>>) => {
const entry = ref<LogEntry<string | JSONObject>>();
provide(showLogDetails, (logEntry: LogEntry<string | JSONObject>) => {
entry.value = logEntry;
drawer.value?.open();
});
return { entry };
};
export const useLogDetails = () => {
const showDetails = inject(showLogDetails, () => {});
return showDetails;
};

View File

@@ -7,17 +7,21 @@ if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id; sessionHost.value = config.hosts[0].id;
} }
export function persistentVisibleKeysForContainer(container: Ref<Container>): Ref<string[][]> { const storage = useProfileStorage("visibleKeys", new Map<string, Map<string[], boolean>>(), {
const storage = useProfileStorage("visibleKeys", {}); from(transformed: [string, [string[], boolean][]][]) {
return computed(() => { return new Map(transformed.map(([key, value]) => [key, new Map(value)]));
if (!(container.value.storageKey in storage.value)) { },
// Returning a temporary ref here to avoid writing an empty array to storage to(value: Map<string, Map<string[], boolean>>) {
const visibleKeys = ref<string[][]>([]); const outer = Array.from(value.entries());
watchOnce(visibleKeys, () => (storage.value[container.value.storageKey] = visibleKeys.value), { deep: true }); const inner = outer.map(([key, value]) => [key, Array.from(value.entries())]);
return visibleKeys.value; return inner;
} },
});
return storage.value[container.value.storageKey]; export function persistentVisibleKeysForContainer(container: Ref<Container>): Ref<Map<string[], boolean>> {
// Computed property to only store to storage when the value changes
return computed({
get: () => storage.value.get(container.value.storageKey) || new Map<string[], boolean>(),
set: (value: Map<string[], boolean>) => storage.value.set(container.value.storageKey, value),
}); });
} }

View File

@@ -1,7 +1,7 @@
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry"; import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
import type { Ref } from "vue"; import type { Ref } from "vue";
export function useVisibleFilter(visibleKeys: Ref<string[][]>) { export function useVisibleFilter(visibleKeys: Ref<Map<string[], boolean>>) {
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) { function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => { return computed(() => {
return messages.value.map((d) => { return messages.value.map((d) => {

View File

@@ -67,15 +67,18 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
date: Date, date: Date,
public readonly level: Level, public readonly level: Level,
public readonly std: Std, public readonly std: Std,
visibleKeys?: Ref<string[][]>, visibleKeys?: Ref<Map<string[], boolean>>,
) { ) {
super(message, containerID, id, date, std, level); super(message, containerID, id, date, std, level);
if (visibleKeys) { if (visibleKeys) {
this.filteredMessage = computed(() => { this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) { if (visibleKeys.value.size === 0) {
return flattenJSON(message); return flattenJSON(message);
} else { } else {
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {}); const keys = Array.from(visibleKeys.value.entries())
.filter(([, value]) => value)
.map(([key]) => key);
return keys.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
} }
}); });
} else { } else {
@@ -87,14 +90,14 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
} }
public get message(): JSONObject { public get message(): JSONObject {
return this.filteredMessage.value; return unref(this.filteredMessage);
} }
public get unfilteredMessage(): JSONObject { public get unfilteredMessage(): JSONObject {
return this._message; return this._message;
} }
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry { static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<Map<string[], boolean>>): ComplexLogEntry {
return new ComplexLogEntry( return new ComplexLogEntry(
event._message, event._message,
event.containerID, event.containerID,
@@ -165,7 +168,7 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
event.id, event.id,
new Date(event.ts), new Date(event.ts),
event.l, event.l,
event.s === "unknown" ? "stderr" : event.s ?? "stderr", event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
); );
} else { } else {
return new SimpleLogEntry( return new SimpleLogEntry(
@@ -175,7 +178,7 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
new Date(event.ts), new Date(event.ts),
event.l, event.l,
event.p, event.p,
event.s === "unknown" ? "stderr" : event.s ?? "stderr", event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
); );
} }
} }

View File

@@ -108,7 +108,6 @@
</div> </div>
<LogList <LogList
:messages="fakeMessages" :messages="fakeMessages"
:visible-keys="keys"
:last-selected-item="undefined" :last-selected-item="undefined"
:show-container-name="false" :show-container-name="false"
class="hidden overflow-hidden rounded-lg border border-base-content/50 shadow @3xl:block" class="hidden overflow-hidden rounded-lg border border-base-content/50 shadow @3xl:block"
@@ -163,7 +162,6 @@ const { t } = useI18n();
setTitle(t("title.settings")); setTitle(t("title.settings"));
const { latest, hasUpdate } = useReleases(); const { latest, hasUpdate } = useReleases();
const keys = ref<string[][]>([]);
const now = new Date(); const now = new Date();
const hoursAgo = (hours: number) => { const hoursAgo = (hours: number) => {
const date = new Date(now); const date = new Date(now);
@@ -208,7 +206,6 @@ const fakeMessages = computedWithControl(
new Date(), new Date(),
"info", "info",
"stdout", "stdout",
keys,
), ),
new SimpleLogEntry(t("settings.log.simple"), "123", 7, new Date(), "debug", undefined, "stderr"), new SimpleLogEntry(t("settings.log.simple"), "123", 7, new Date(), "debug", undefined, "stderr"),
], ],

View File

@@ -22,7 +22,7 @@ export interface Config {
export interface Profile { export interface Profile {
settings?: Settings; settings?: Settings;
pinned?: Set<string>; pinned?: Set<string>;
visibleKeys?: { [key: string]: string[][] }; visibleKeys?: Map<string, Map<string[], boolean>>;
releaseSeen?: string; releaseSeen?: string;
collapsedGroups?: Set<string>; collapsedGroups?: Set<string>;
} }

View File

@@ -16,16 +16,28 @@ export function isObject(value: any): value is Record<string, any> {
} }
export function flattenJSON(obj: Record<string, any>, path: string[] = []) { export function flattenJSON(obj: Record<string, any>, path: string[] = []) {
const result: Record<string, any> = {}; const map = flattenJSONToMap(obj);
Object.keys(obj).forEach((key) => { const result = {} as Record<string, any>;
for (const [key, value] of map) {
result[key.join(".")] = value;
}
return result;
}
export function flattenJSONToMap(obj: Record<string, any>, path: string[] = []): Map<string[], any> {
const result = new Map<string[], any>();
for (const key of Object.keys(obj)) {
const value = obj[key]; const value = obj[key];
const newPath = path.concat(key); const newPath = path.concat(key);
if (isObject(value)) { if (isObject(value)) {
Object.assign(result, flattenJSON(value, newPath)); for (const [k, v] of flattenJSONToMap(value, newPath)) {
} else { result.set(k, v);
result[newPath.join(".")] = value;
} }
}); } else {
result.set(newPath, value);
}
}
return result; return result;
} }

View File

@@ -40,7 +40,7 @@ type Settings struct {
type Profile struct { type Profile struct {
Settings *Settings `json:"settings,omitempty"` Settings *Settings `json:"settings,omitempty"`
Pinned []string `json:"pinned"` Pinned []string `json:"pinned"`
VisibleKeys map[string][][]string `json:"visibleKeys,omitempty"` VisibleKeys []interface{} `json:"visibleKeys,omitempty"`
ReleaseSeen string `json:"releaseSeen,omitempty"` ReleaseSeen string `json:"releaseSeen,omitempty"`
CollapsedGroups []string `json:"collapsedGroups"` CollapsedGroups []string `json:"collapsedGroups"`
} }
@@ -68,7 +68,7 @@ func UpdateFromReader(user auth.User, reader io.Reader) error {
defer mux.Unlock() defer mux.Unlock()
existingProfile, err := Load(user) existingProfile, err := Load(user)
if err != nil && err != errMissingProfileErr { if err != nil && err != errMissingProfileErr {
return err log.Error().Err(err).Msg("Unable to load profile. Overwriting it.")
} }
if err := json.NewDecoder(reader).Decode(&existingProfile); err != nil { if err := json.NewDecoder(reader).Decode(&existingProfile); err != nil {

View File

@@ -27,6 +27,7 @@ func Test_createRoutes_index(t *testing.T) {
abide.AssertHTTPResponse(t, t.Name(), rr.Result()) abide.AssertHTTPResponse(t, t.Name(), rr.Result())
} }
func Test_createRoutes_redirect(t *testing.T) { func Test_createRoutes_redirect(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")

View File

@@ -40,7 +40,7 @@ error:
events-stream: events-stream:
title: Unexpected Error title: Unexpected Error
message: >- message: >-
Dozzle UI wasn't able to connect API. Please check your network settings. Dozzle UI wasn't able to connect to API. Please check your network settings.
If you are using a reverse proxy, please make sure it is configured If you are using a reverse proxy, please make sure it is configured
properly. properly.
events-timeout: events-timeout:

View File

@@ -59,6 +59,7 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"postcss": "^8.4.41", "postcss": "^8.4.41",
"sortablejs": "^1.15.2",
"splitpanes": "^3.1.5", "splitpanes": "^3.1.5",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
@@ -85,7 +86,7 @@
"@types/d3-shape": "^3.1.6", "@types/d3-shape": "^3.1.6",
"@types/d3-transition": "^3.0.8", "@types/d3-transition": "^3.0.8",
"@types/lodash.debounce": "^4.0.9", "@types/lodash.debounce": "^4.0.9",
"@types/node": "^22.5.1", "@types/node": "^22.5.0",
"@vitejs/plugin-vue": "5.1.2", "@vitejs/plugin-vue": "5.1.2",
"@vue/compiler-sfc": "^3.4.38", "@vue/compiler-sfc": "^3.4.38",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",

21
pnpm-lock.yaml generated
View File

@@ -49,7 +49,7 @@ importers:
version: 11.0.3(vue@3.4.38(typescript@5.5.4)) version: 11.0.3(vue@3.4.38(typescript@5.5.4))
'@vueuse/integrations': '@vueuse/integrations':
specifier: ^11.0.3 specifier: ^11.0.3
version: 11.0.3(focus-trap@7.5.4)(fuse.js@7.0.0)(vue@3.4.38(typescript@5.5.4)) version: 11.0.3(focus-trap@7.5.4)(fuse.js@7.0.0)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))
'@vueuse/router': '@vueuse/router':
specifier: ^11.0.3 specifier: ^11.0.3
version: 11.0.3(vue-router@4.4.3(vue@3.4.38(typescript@5.5.4)))(vue@3.4.38(typescript@5.5.4)) version: 11.0.3(vue-router@4.4.3(vue@3.4.38(typescript@5.5.4)))(vue@3.4.38(typescript@5.5.4))
@@ -98,6 +98,9 @@ importers:
postcss: postcss:
specifier: ^8.4.41 specifier: ^8.4.41
version: 8.4.41 version: 8.4.41
sortablejs:
specifier: ^1.15.2
version: 1.15.2
splitpanes: splitpanes:
specifier: ^3.1.5 specifier: ^3.1.5
version: 3.1.5 version: 3.1.5
@@ -133,7 +136,7 @@ importers:
version: 0.11.0(vite@5.4.2(@types/node@22.5.1))(vue-router@4.4.3(vue@3.4.38(typescript@5.5.4)))(vue@3.4.38(typescript@5.5.4)) version: 0.11.0(vite@5.4.2(@types/node@22.5.1))(vue-router@4.4.3(vue@3.4.38(typescript@5.5.4)))(vue@3.4.38(typescript@5.5.4))
vitepress: vitepress:
specifier: 1.3.4 specifier: 1.3.4
version: 1.3.4(@algolia/client-search@5.0.0)(@types/node@22.5.1)(fuse.js@7.0.0)(postcss@8.4.41)(search-insights@2.16.3)(typescript@5.5.4) version: 1.3.4(@algolia/client-search@5.0.0)(@types/node@22.5.1)(fuse.js@7.0.0)(postcss@8.4.41)(search-insights@2.16.3)(sortablejs@1.15.2)(typescript@5.5.4)
vue: vue:
specifier: ^3.4.38 specifier: ^3.4.38
version: 3.4.38(typescript@5.5.4) version: 3.4.38(typescript@5.5.4)
@@ -172,7 +175,7 @@ importers:
specifier: ^4.0.9 specifier: ^4.0.9
version: 4.0.9 version: 4.0.9
'@types/node': '@types/node':
specifier: ^22.5.1 specifier: ^22.5.0
version: 22.5.1 version: 22.5.1
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: 5.1.2 specifier: 5.1.2
@@ -2661,6 +2664,9 @@ packages:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'} engines: {node: '>=18'}
sortablejs@1.15.2:
resolution: {integrity: sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==}
source-map-js@1.2.0: source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -4305,7 +4311,7 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@vueuse/integrations@11.0.3(focus-trap@7.5.4)(fuse.js@7.0.0)(vue@3.4.38(typescript@5.5.4))': '@vueuse/integrations@11.0.3(focus-trap@7.5.4)(fuse.js@7.0.0)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))':
dependencies: dependencies:
'@vueuse/core': 11.0.3(vue@3.4.38(typescript@5.5.4)) '@vueuse/core': 11.0.3(vue@3.4.38(typescript@5.5.4))
'@vueuse/shared': 11.0.3(vue@3.4.38(typescript@5.5.4)) '@vueuse/shared': 11.0.3(vue@3.4.38(typescript@5.5.4))
@@ -4313,6 +4319,7 @@ snapshots:
optionalDependencies: optionalDependencies:
focus-trap: 7.5.4 focus-trap: 7.5.4
fuse.js: 7.0.0 fuse.js: 7.0.0
sortablejs: 1.15.2
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
@@ -5638,6 +5645,8 @@ snapshots:
ansi-styles: 6.2.1 ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0 is-fullwidth-code-point: 5.0.0
sortablejs@1.15.2: {}
source-map-js@1.2.0: {} source-map-js@1.2.0: {}
source-map@0.6.1: source-map@0.6.1:
@@ -6098,7 +6107,7 @@ snapshots:
'@types/node': 22.5.1 '@types/node': 22.5.1
fsevents: 2.3.3 fsevents: 2.3.3
vitepress@1.3.4(@algolia/client-search@5.0.0)(@types/node@22.5.1)(fuse.js@7.0.0)(postcss@8.4.41)(search-insights@2.16.3)(typescript@5.5.4): vitepress@1.3.4(@algolia/client-search@5.0.0)(@types/node@22.5.1)(fuse.js@7.0.0)(postcss@8.4.41)(search-insights@2.16.3)(sortablejs@1.15.2)(typescript@5.5.4):
dependencies: dependencies:
'@docsearch/css': 3.6.1 '@docsearch/css': 3.6.1
'@docsearch/js': 3.6.1(@algolia/client-search@5.0.0)(search-insights@2.16.3) '@docsearch/js': 3.6.1(@algolia/client-search@5.0.0)(search-insights@2.16.3)
@@ -6109,7 +6118,7 @@ snapshots:
'@vue/devtools-api': 7.3.8 '@vue/devtools-api': 7.3.8
'@vue/shared': 3.4.38 '@vue/shared': 3.4.38
'@vueuse/core': 11.0.3(vue@3.4.38(typescript@5.5.4)) '@vueuse/core': 11.0.3(vue@3.4.38(typescript@5.5.4))
'@vueuse/integrations': 11.0.3(focus-trap@7.5.4)(fuse.js@7.0.0)(vue@3.4.38(typescript@5.5.4)) '@vueuse/integrations': 11.0.3(focus-trap@7.5.4)(fuse.js@7.0.0)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))
focus-trap: 7.5.4 focus-trap: 7.5.4
mark.js: 8.11.1 mark.js: 8.11.1
minisearch: 7.1.0 minisearch: 7.1.0