mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
feat: adds a side panel for complex logs that enables reordering and enabling fields (#3237)
This commit is contained in:
11
assets/auto-imports.d.ts
vendored
11
assets/auto-imports.d.ts
vendored
@@ -50,6 +50,7 @@ declare global {
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const flattenJSON: typeof import('./utils/index')['flattenJSON']
|
||||
const flattenJSONToMap: typeof import('./utils/index')['flattenJSONToMap']
|
||||
const formatBytes: typeof import('./utils/index')['formatBytes']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
@@ -104,6 +105,7 @@ declare global {
|
||||
const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer']
|
||||
const pinnedContainers: typeof import('./composable/storage')['pinnedContainers']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const provideDetails: typeof import('./composable/showDetails')['provideDetails']
|
||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||
const provideLogDetails: typeof import('./composable/showLogDetails')['provideLogDetails']
|
||||
const provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext']
|
||||
@@ -205,6 +207,7 @@ declare global {
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDetails: typeof import('./composable/showDetails')['useDetails']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
@@ -413,6 +416,7 @@ declare module 'vue' {
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||
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 getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
@@ -468,6 +472,7 @@ declare module 'vue' {
|
||||
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
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 provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
@@ -497,6 +502,7 @@ declare module 'vue' {
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
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 showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
|
||||
@@ -607,6 +613,7 @@ declare module 'vue' {
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
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 useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
|
||||
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 extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||
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 getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
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 provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
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 provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
|
||||
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 shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
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 showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
|
||||
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 useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
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 useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
|
||||
3
assets/components.d.ts
vendored
3
assets/components.d.ts
vendored
@@ -49,6 +49,7 @@ declare module 'vue' {
|
||||
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
|
||||
Links: typeof import('./components/Links.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']
|
||||
LogList: typeof import('./components/LogViewer/LogList.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:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['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']
|
||||
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
|
||||
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
|
||||
@@ -97,6 +99,7 @@ declare module 'vue' {
|
||||
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
|
||||
Search: typeof import('./components/Search.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']
|
||||
SidePanel: typeof import('./components/SidePanel.vue')['default']
|
||||
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
v-model="query"
|
||||
: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
|
||||
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"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
ref="viewer"
|
||||
:stream-source="useGroupedStream"
|
||||
:entity="group"
|
||||
:visible-keys="visibleKeys"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
:show-container-name="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -43,6 +43,5 @@ const { customGroups } = storeToRefs(swarmStore);
|
||||
|
||||
const group = computed(() => customGroups.value.find((g) => g.name === name) ?? new GroupedContainers("", []));
|
||||
|
||||
const visibleKeys = ref<string[][]>([]);
|
||||
provideLoggingContext(toRef(() => group.value.containers));
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<div class="group/item relative flex w-full gap-x-2" @click="expandToggle()">
|
||||
<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 class="group/item relative flex w-full gap-x-2 hover:bg-secondary/10" @click="showLogDetails(logEntry)">
|
||||
<div v-if="showContainerName">
|
||||
<ContainerName :id="logEntry.containerID" />
|
||||
</div>
|
||||
@@ -18,7 +13,7 @@
|
||||
<LogLevel :level="logEntry.level" />
|
||||
</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">
|
||||
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null"><null></span>
|
||||
<template v-else-if="Array.isArray(value)">
|
||||
@@ -28,7 +23,6 @@
|
||||
</li>
|
||||
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
|
||||
</ul>
|
||||
<FieldList :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys" />
|
||||
</div>
|
||||
<LogMessageActions
|
||||
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<{
|
||||
logEntry: ComplexLogEntry;
|
||||
visibleKeys: string[][];
|
||||
showContainerName?: boolean;
|
||||
}>();
|
||||
|
||||
const [expanded, expandToggle] = useToggle();
|
||||
|
||||
const validValues = computed(() => {
|
||||
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
|
||||
});
|
||||
|
||||
const showLogDetails = useLogDetails();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -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>
|
||||
119
assets/components/LogViewer/LogDetails.vue
Normal file
119
assets/components/LogViewer/LogDetails.vue
Normal 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>
|
||||
@@ -32,25 +32,26 @@ defineProps<{
|
||||
margin-top: -0.4em;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
</style>
|
||||
<style lang="postcss">
|
||||
[data-level="debug"],
|
||||
[data-level="trace"] {
|
||||
@apply bg-purple;
|
||||
@apply !bg-purple;
|
||||
}
|
||||
|
||||
[data-level="info"] {
|
||||
@apply bg-green;
|
||||
@apply !bg-green;
|
||||
}
|
||||
|
||||
[data-level="error"],
|
||||
[data-level="severe"],
|
||||
[data-level="critical"],
|
||||
[data-level="fatal"] {
|
||||
@apply bg-red;
|
||||
@apply !bg-red;
|
||||
}
|
||||
|
||||
[data-level="warn"],
|
||||
[data-level="warning"] {
|
||||
@apply bg-orange;
|
||||
@apply !bg-orange;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,12 +13,7 @@
|
||||
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
|
||||
class="group/entry"
|
||||
>
|
||||
<component
|
||||
:is="item.getComponent()"
|
||||
:log-entry="item"
|
||||
:visible-keys="visibleKeys"
|
||||
:show-container-name="showContainerName"
|
||||
/>
|
||||
<component :is="item.getComponent()" :log-entry="item" :show-container-name="showContainerName" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -30,7 +25,6 @@ const { loading, progress, currentDate } = useScrollContext();
|
||||
|
||||
const { messages } = defineProps<{
|
||||
messages: LogEntry<string | JSONObject>[];
|
||||
visibleKeys: string[][];
|
||||
lastSelectedItem: LogEntry<string | JSONObject> | undefined;
|
||||
showContainerName: boolean;
|
||||
}>();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<span
|
||||
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 />
|
||||
</span>
|
||||
@@ -19,7 +19,7 @@
|
||||
>
|
||||
<a
|
||||
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}`"
|
||||
>
|
||||
<carbon:search-locate />
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<template>
|
||||
<LogList
|
||||
:messages="filtered"
|
||||
:last-selected-item="lastSelectedItem"
|
||||
:visible-keys="visibleKeys"
|
||||
:show-container-name="showContainerName"
|
||||
/>
|
||||
<SideDrawer ref="drawer">
|
||||
<LogDetails :entry="entry" v-if="entry && entry instanceof ComplexLogEntry" />
|
||||
</SideDrawer>
|
||||
<LogList :messages="filtered" :last-selected-item="lastSelectedItem" :show-container-name="showContainerName" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouteHash } from "@vueuse/router";
|
||||
|
||||
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||
import SideDrawer from "@/components/common/SideDrawer.vue";
|
||||
import { ComplexLogEntry, type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||
|
||||
const props = defineProps<{
|
||||
messages: LogEntry<string | JSONObject>[];
|
||||
visibleKeys: string[][];
|
||||
visibleKeys: Map<string[], boolean>;
|
||||
showContainerName: boolean;
|
||||
}>();
|
||||
|
||||
@@ -23,6 +21,10 @@ const { messages, visibleKeys } = toRefs(props);
|
||||
const { filteredPayload } = useVisibleFilter(visibleKeys);
|
||||
const { filteredMessages } = useSearchFilter();
|
||||
|
||||
const drawer = ref<InstanceType<typeof SideDrawer>>() as Ref<InstanceType<typeof SideDrawer>>;
|
||||
|
||||
const { entry } = provideLogDetails(drawer);
|
||||
|
||||
const visible = filteredPayload(messages);
|
||||
const filtered = filteredMessages(visible);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LogStreamSource } from "@/composable/eventStreams";
|
||||
|
||||
const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{
|
||||
streamSource: (t: Ref<T>) => LogStreamSource;
|
||||
visibleKeys: string[][];
|
||||
visibleKeys: Map<string[], boolean>;
|
||||
showContainerName: boolean;
|
||||
entity: T;
|
||||
}>();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
|
||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
||||
<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-->
|
||||
<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`] = `
|
||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
||||
<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-->
|
||||
<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`] = `
|
||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
||||
<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-->
|
||||
<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`] = `
|
||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
||||
<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-->
|
||||
<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`] = `
|
||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
||||
<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-->
|
||||
<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`] = `
|
||||
"<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-->"
|
||||
`;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
ref="viewer"
|
||||
:stream-source="useMergedStream"
|
||||
:entity="containers"
|
||||
:visible-keys="visibleKeys"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
:show-container-name="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -35,8 +35,5 @@ const containerStore = useContainerStore();
|
||||
const { allContainersById, ready } = storeToRefs(containerStore);
|
||||
|
||||
const containers = computed(() => ids.map((id) => allContainersById.value[id]));
|
||||
|
||||
const visibleKeys = ref<string[][]>([]);
|
||||
|
||||
provideLoggingContext(containers);
|
||||
</script>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</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"
|
||||
></div>
|
||||
<div ref="scrollObserver" class="h-px"></div>
|
||||
@@ -96,7 +96,7 @@ function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
|
||||
|
||||
.animate-background {
|
||||
background-size: 400% 400%;
|
||||
animation: gradient-animation 4s ease infinite;
|
||||
animation: gradient-animation 5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-animation {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
ref="viewer"
|
||||
:stream-source="useServiceStream"
|
||||
:entity="service"
|
||||
:visible-keys="visibleKeys"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
:show-container-name="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -36,7 +36,6 @@ const { name, scrollable = false } = defineProps<{
|
||||
name: string;
|
||||
}>();
|
||||
|
||||
const visibleKeys = ref<string[][]>([]);
|
||||
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
|
||||
const store = useSwarmStore();
|
||||
const { services } = storeToRefs(store) as unknown as { services: Ref<Service[]> };
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
ref="viewer"
|
||||
:stream-source="useStackStream"
|
||||
:entity="stack"
|
||||
:visible-keys="visibleKeys"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
:show-container-name="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -38,7 +38,6 @@ const { name, scrollable = false } = defineProps<{
|
||||
name: string;
|
||||
}>();
|
||||
|
||||
const visibleKeys = ref<string[][]>([]);
|
||||
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
|
||||
const store = useSwarmStore();
|
||||
const { stacks } = storeToRefs(store) as unknown as { stacks: Ref<Stack[]> };
|
||||
|
||||
34
assets/components/common/SideDrawer.vue
Normal file
34
assets/components/common/SideDrawer.vue
Normal 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>
|
||||
@@ -1,14 +1,34 @@
|
||||
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 storage = useStorage<NonNullable<Profile[K]>>(storageKey, defaultValue, undefined, {
|
||||
writeDefaults: false,
|
||||
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 (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]>;
|
||||
} else if (config.profile[key] instanceof Array) {
|
||||
storage.value = config.profile[key] as NonNullable<Profile[K]>;
|
||||
@@ -23,9 +43,19 @@ export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue:
|
||||
watch(
|
||||
storage,
|
||||
(value) => {
|
||||
const transformedValue = transformer ? transformer.to(value) : value;
|
||||
|
||||
fetch(withBase("/api/profile"), {
|
||||
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 },
|
||||
|
||||
22
assets/composable/showLogDetails.ts
Normal file
22
assets/composable/showLogDetails.ts
Normal 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;
|
||||
};
|
||||
@@ -7,17 +7,21 @@ if (config.hosts.length === 1 && !sessionHost.value) {
|
||||
sessionHost.value = config.hosts[0].id;
|
||||
}
|
||||
|
||||
export function persistentVisibleKeysForContainer(container: Ref<Container>): Ref<string[][]> {
|
||||
const storage = useProfileStorage("visibleKeys", {});
|
||||
return computed(() => {
|
||||
if (!(container.value.storageKey in storage.value)) {
|
||||
// Returning a temporary ref here to avoid writing an empty array to storage
|
||||
const visibleKeys = ref<string[][]>([]);
|
||||
watchOnce(visibleKeys, () => (storage.value[container.value.storageKey] = visibleKeys.value), { deep: true });
|
||||
return visibleKeys.value;
|
||||
}
|
||||
|
||||
return storage.value[container.value.storageKey];
|
||||
const storage = useProfileStorage("visibleKeys", new Map<string, Map<string[], boolean>>(), {
|
||||
from(transformed: [string, [string[], boolean][]][]) {
|
||||
return new Map(transformed.map(([key, value]) => [key, new Map(value)]));
|
||||
},
|
||||
to(value: Map<string, Map<string[], boolean>>) {
|
||||
const outer = Array.from(value.entries());
|
||||
const inner = outer.map(([key, value]) => [key, Array.from(value.entries())]);
|
||||
return inner;
|
||||
},
|
||||
});
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
|
||||
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>[]>) {
|
||||
return computed(() => {
|
||||
return messages.value.map((d) => {
|
||||
|
||||
@@ -67,15 +67,18 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
|
||||
date: Date,
|
||||
public readonly level: Level,
|
||||
public readonly std: Std,
|
||||
visibleKeys?: Ref<string[][]>,
|
||||
visibleKeys?: Ref<Map<string[], boolean>>,
|
||||
) {
|
||||
super(message, containerID, id, date, std, level);
|
||||
if (visibleKeys) {
|
||||
this.filteredMessage = computed(() => {
|
||||
if (!visibleKeys.value.length) {
|
||||
if (visibleKeys.value.size === 0) {
|
||||
return flattenJSON(message);
|
||||
} 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 {
|
||||
@@ -87,14 +90,14 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
|
||||
}
|
||||
|
||||
public get message(): JSONObject {
|
||||
return this.filteredMessage.value;
|
||||
return unref(this.filteredMessage);
|
||||
}
|
||||
|
||||
public get unfilteredMessage(): JSONObject {
|
||||
return this._message;
|
||||
}
|
||||
|
||||
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
|
||||
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<Map<string[], boolean>>): ComplexLogEntry {
|
||||
return new ComplexLogEntry(
|
||||
event._message,
|
||||
event.containerID,
|
||||
@@ -165,7 +168,7 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
|
||||
event.id,
|
||||
new Date(event.ts),
|
||||
event.l,
|
||||
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
|
||||
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
|
||||
);
|
||||
} else {
|
||||
return new SimpleLogEntry(
|
||||
@@ -175,7 +178,7 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
|
||||
new Date(event.ts),
|
||||
event.l,
|
||||
event.p,
|
||||
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
|
||||
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
</div>
|
||||
<LogList
|
||||
:messages="fakeMessages"
|
||||
:visible-keys="keys"
|
||||
:last-selected-item="undefined"
|
||||
:show-container-name="false"
|
||||
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"));
|
||||
const { latest, hasUpdate } = useReleases();
|
||||
|
||||
const keys = ref<string[][]>([]);
|
||||
const now = new Date();
|
||||
const hoursAgo = (hours: number) => {
|
||||
const date = new Date(now);
|
||||
@@ -208,7 +206,6 @@ const fakeMessages = computedWithControl(
|
||||
new Date(),
|
||||
"info",
|
||||
"stdout",
|
||||
keys,
|
||||
),
|
||||
new SimpleLogEntry(t("settings.log.simple"), "123", 7, new Date(), "debug", undefined, "stderr"),
|
||||
],
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface Config {
|
||||
export interface Profile {
|
||||
settings?: Settings;
|
||||
pinned?: Set<string>;
|
||||
visibleKeys?: { [key: string]: string[][] };
|
||||
visibleKeys?: Map<string, Map<string[], boolean>>;
|
||||
releaseSeen?: string;
|
||||
collapsedGroups?: Set<string>;
|
||||
}
|
||||
|
||||
@@ -16,16 +16,28 @@ export function isObject(value: any): value is Record<string, any> {
|
||||
}
|
||||
|
||||
export function flattenJSON(obj: Record<string, any>, path: string[] = []) {
|
||||
const result: Record<string, any> = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const map = flattenJSONToMap(obj);
|
||||
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 newPath = path.concat(key);
|
||||
if (isObject(value)) {
|
||||
Object.assign(result, flattenJSON(value, newPath));
|
||||
for (const [k, v] of flattenJSONToMap(value, newPath)) {
|
||||
result.set(k, v);
|
||||
}
|
||||
} else {
|
||||
result[newPath.join(".")] = value;
|
||||
result.set(newPath, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ type Settings struct {
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
Settings *Settings `json:"settings,omitempty"`
|
||||
Pinned []string `json:"pinned"`
|
||||
VisibleKeys map[string][][]string `json:"visibleKeys,omitempty"`
|
||||
ReleaseSeen string `json:"releaseSeen,omitempty"`
|
||||
CollapsedGroups []string `json:"collapsedGroups"`
|
||||
Settings *Settings `json:"settings,omitempty"`
|
||||
Pinned []string `json:"pinned"`
|
||||
VisibleKeys []interface{} `json:"visibleKeys,omitempty"`
|
||||
ReleaseSeen string `json:"releaseSeen,omitempty"`
|
||||
CollapsedGroups []string `json:"collapsedGroups"`
|
||||
}
|
||||
|
||||
var dataPath string
|
||||
@@ -68,7 +68,7 @@ func UpdateFromReader(user auth.User, reader io.Reader) error {
|
||||
defer mux.Unlock()
|
||||
existingProfile, err := Load(user)
|
||||
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 {
|
||||
|
||||
@@ -27,6 +27,7 @@ func Test_createRoutes_index(t *testing.T) {
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
}
|
||||
|
||||
|
||||
func Test_createRoutes_redirect(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
|
||||
|
||||
@@ -40,7 +40,7 @@ error:
|
||||
events-stream:
|
||||
title: Unexpected Error
|
||||
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
|
||||
properly.
|
||||
events-timeout:
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"pinia": "^2.2.2",
|
||||
"postcss": "^8.4.41",
|
||||
"sortablejs": "^1.15.2",
|
||||
"splitpanes": "^3.1.5",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tailwindcss": "^3.4.10",
|
||||
@@ -85,7 +86,7 @@
|
||||
"@types/d3-shape": "^3.1.6",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/node": "^22.5.0",
|
||||
"@vitejs/plugin-vue": "5.1.2",
|
||||
"@vue/compiler-sfc": "^3.4.38",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -49,7 +49,7 @@ importers:
|
||||
version: 11.0.3(vue@3.4.38(typescript@5.5.4))
|
||||
'@vueuse/integrations':
|
||||
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':
|
||||
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))
|
||||
@@ -98,6 +98,9 @@ importers:
|
||||
postcss:
|
||||
specifier: ^8.4.41
|
||||
version: 8.4.41
|
||||
sortablejs:
|
||||
specifier: ^1.15.2
|
||||
version: 1.15.2
|
||||
splitpanes:
|
||||
specifier: ^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))
|
||||
vitepress:
|
||||
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:
|
||||
specifier: ^3.4.38
|
||||
version: 3.4.38(typescript@5.5.4)
|
||||
@@ -172,7 +175,7 @@ importers:
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9
|
||||
'@types/node':
|
||||
specifier: ^22.5.1
|
||||
specifier: ^22.5.0
|
||||
version: 22.5.1
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 5.1.2
|
||||
@@ -2661,6 +2664,9 @@ packages:
|
||||
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
sortablejs@1.15.2:
|
||||
resolution: {integrity: sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==}
|
||||
|
||||
source-map-js@1.2.0:
|
||||
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4305,7 +4311,7 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- 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:
|
||||
'@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))
|
||||
@@ -4313,6 +4319,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
focus-trap: 7.5.4
|
||||
fuse.js: 7.0.0
|
||||
sortablejs: 1.15.2
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
@@ -5638,6 +5645,8 @@ snapshots:
|
||||
ansi-styles: 6.2.1
|
||||
is-fullwidth-code-point: 5.0.0
|
||||
|
||||
sortablejs@1.15.2: {}
|
||||
|
||||
source-map-js@1.2.0: {}
|
||||
|
||||
source-map@0.6.1:
|
||||
@@ -6098,7 +6107,7 @@ snapshots:
|
||||
'@types/node': 22.5.1
|
||||
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:
|
||||
'@docsearch/css': 3.6.1
|
||||
'@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/shared': 3.4.38
|
||||
'@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
|
||||
mark.js: 8.11.1
|
||||
minisearch: 7.1.0
|
||||
|
||||
Reference in New Issue
Block a user