mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
feat: adds preview for settings (#2669)
This commit is contained in:
2
assets/components.d.ts
vendored
2
assets/components.d.ts
vendored
@@ -26,6 +26,7 @@ declare module 'vue' {
|
|||||||
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
|
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
|
||||||
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
|
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
|
||||||
ContainerHealth: typeof import('./components/LogViewer/ContainerHealth.vue')['default']
|
ContainerHealth: typeof import('./components/LogViewer/ContainerHealth.vue')['default']
|
||||||
|
ContainerLogViewer: typeof import('./components/LogViewer/ContainerLogViewer.vue')['default']
|
||||||
ContainerPopup: typeof import('./components/LogViewer/ContainerPopup.vue')['default']
|
ContainerPopup: typeof import('./components/LogViewer/ContainerPopup.vue')['default']
|
||||||
ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
|
ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
|
||||||
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
|
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
|
||||||
@@ -40,6 +41,7 @@ declare module 'vue' {
|
|||||||
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
|
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
|
||||||
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
|
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
|
||||||
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
|
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
|
||||||
|
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
|
||||||
Links: typeof import('./components/Links.vue')['default']
|
Links: typeof import('./components/Links.vue')['default']
|
||||||
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
|
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
|
||||||
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
|
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
|
||||||
|
|||||||
39
assets/components/LogViewer/ContainerLogViewer.vue
Normal file
39
assets/components/LogViewer/ContainerLogViewer.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<log-viewer :messages="filtered" :last-selected-item="lastSelectedItem" :visible-keys="visibleKeys" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouteHash } from "@vueuse/router";
|
||||||
|
|
||||||
|
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
messages: LogEntry<string | JSONObject>[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { container } = useContainerContext();
|
||||||
|
|
||||||
|
const visibleKeys = persistentVisibleKeys(container);
|
||||||
|
|
||||||
|
const { filteredPayload } = useVisibleFilter(visibleKeys);
|
||||||
|
const { filteredMessages } = useSearchFilter();
|
||||||
|
|
||||||
|
const { messages } = toRefs(props);
|
||||||
|
const visible = filteredPayload(messages);
|
||||||
|
const filtered = filteredMessages(visible);
|
||||||
|
|
||||||
|
const { lastSelectedItem } = useLogSearchContext() as {
|
||||||
|
lastSelectedItem: Ref<LogEntry<string | JSONObject> | undefined>;
|
||||||
|
};
|
||||||
|
const routeHash = useRouteHash();
|
||||||
|
watch(
|
||||||
|
routeHash,
|
||||||
|
(hash) => {
|
||||||
|
if (hash) {
|
||||||
|
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, flush: "post" },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<style scoped lang="postcss"></style>
|
||||||
@@ -10,7 +10,7 @@ import { computed, nextTick } from "vue";
|
|||||||
import { createI18n } from "vue-i18n";
|
import { createI18n } from "vue-i18n";
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import LogEventSource from "./LogEventSource.vue";
|
import LogEventSource from "./LogEventSource.vue";
|
||||||
import LogViewer from "./LogViewer.vue";
|
import ContainerLogViewer from "./ContainerLogViewer.vue";
|
||||||
|
|
||||||
vi.mock("@/stores/config", () => ({
|
vi.mock("@/stores/config", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@@ -71,7 +71,7 @@ describe("<LogEventSource />", () => {
|
|||||||
global: {
|
global: {
|
||||||
plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})],
|
plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})],
|
||||||
components: {
|
components: {
|
||||||
LogViewer,
|
ContainerLogViewer,
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
[containerContext as symbol]: {
|
[containerContext as symbol]: {
|
||||||
@@ -83,7 +83,7 @@ describe("<LogEventSource />", () => {
|
|||||||
},
|
},
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
|
<template #scoped="params"><container-log-viewer :messages="params.messages" /></template>
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
props: {},
|
props: {},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true }">
|
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true }">
|
||||||
<li
|
<li
|
||||||
v-for="item in filtered"
|
v-for="item in messages"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:data-key="item.id"
|
:data-key="item.id"
|
||||||
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
|
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
|
||||||
@@ -14,36 +14,14 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRaw } from "vue";
|
import { toRaw } from "vue";
|
||||||
import { useRouteHash } from "@vueuse/router";
|
|
||||||
|
|
||||||
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
messages: LogEntry<string | JSONObject>[];
|
messages: LogEntry<string | JSONObject>[];
|
||||||
|
visibleKeys: string[][];
|
||||||
|
lastSelectedItem: LogEntry<string | JSONObject> | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { container } = useContainerContext();
|
|
||||||
|
|
||||||
const visibleKeys = persistentVisibleKeys(container);
|
|
||||||
|
|
||||||
const { filteredPayload } = useVisibleFilter(visibleKeys);
|
|
||||||
const { filteredMessages } = useSearchFilter();
|
|
||||||
|
|
||||||
const { messages } = toRefs(props);
|
|
||||||
const visible = filteredPayload(messages);
|
|
||||||
const filtered = filteredMessages(visible);
|
|
||||||
|
|
||||||
const { lastSelectedItem } = useLogSearchContext();
|
|
||||||
const routeHash = useRouteHash();
|
|
||||||
watch(
|
|
||||||
routeHash,
|
|
||||||
(hash) => {
|
|
||||||
if (hash) {
|
|
||||||
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true, flush: "post" },
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="postcss">
|
<style scoped lang="postcss">
|
||||||
.events {
|
.events {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<log-event-source ref="source" #default="{ messages }" @loading-more="loadingMore($event)">
|
<log-event-source ref="source" #default="{ messages }" @loading-more="loadingMore($event)">
|
||||||
<log-viewer :messages="messages"></log-viewer>
|
<container-log-viewer :messages="messages" />
|
||||||
</log-event-source>
|
</log-event-source>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
10
assets/components/common/LabeledInput.vue
Normal file
10
assets/components/common/LabeledInput.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<label class="label cursor-pointer gap-4">
|
||||||
|
<div class="label-text"><slot name="label" /></div>
|
||||||
|
<slot name="input" />
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<label class="label inline-flex cursor-pointer gap-4 font-normal">
|
<labeled-input>
|
||||||
<input type="checkbox" class="toggle toggle-primary" v-model="modelValue" />
|
<template #label>
|
||||||
<slot />
|
<slot />
|
||||||
</label>
|
</template>
|
||||||
|
<template #input>
|
||||||
|
<input type="checkbox" class="toggle toggle-primary" v-model="modelValue" />
|
||||||
|
</template>
|
||||||
|
</labeled-input>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue
|
|||||||
export type JSONObject = { [x: string]: JSONValue };
|
export type JSONObject = { [x: string]: JSONValue };
|
||||||
export type Position = "start" | "end" | "middle" | undefined;
|
export type Position = "start" | "end" | "middle" | undefined;
|
||||||
export type Std = "stdout" | "stderr";
|
export type Std = "stdout" | "stderr";
|
||||||
|
export type Level = "debug" | "info" | "warn" | "error" | "fatal" | "trace" | "unknown";
|
||||||
export interface LogEvent {
|
export interface LogEvent {
|
||||||
readonly m: string | JSONObject;
|
readonly m: string | JSONObject;
|
||||||
readonly ts: number;
|
readonly ts: number;
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
readonly l: string;
|
readonly l: Level;
|
||||||
readonly p: Position;
|
readonly p: Position;
|
||||||
readonly s: "stdout" | "stderr" | "unknown";
|
readonly s: "stdout" | "stderr" | "unknown";
|
||||||
}
|
}
|
||||||
@@ -25,7 +26,7 @@ export abstract class LogEntry<T extends string | JSONObject> {
|
|||||||
public readonly id: number,
|
public readonly id: number,
|
||||||
public readonly date: Date,
|
public readonly date: Date,
|
||||||
public readonly std: Std,
|
public readonly std: Std,
|
||||||
public readonly level?: string,
|
public readonly level?: Level,
|
||||||
) {
|
) {
|
||||||
this._message = message;
|
this._message = message;
|
||||||
}
|
}
|
||||||
@@ -42,7 +43,7 @@ export class SimpleLogEntry extends LogEntry<string> {
|
|||||||
message: string,
|
message: string,
|
||||||
id: number,
|
id: number,
|
||||||
date: Date,
|
date: Date,
|
||||||
public readonly level: string,
|
public readonly level: Level,
|
||||||
public readonly position: Position,
|
public readonly position: Position,
|
||||||
public readonly std: Std,
|
public readonly std: Std,
|
||||||
) {
|
) {
|
||||||
@@ -60,7 +61,7 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
|
|||||||
message: JSONObject,
|
message: JSONObject,
|
||||||
id: number,
|
id: number,
|
||||||
date: Date,
|
date: Date,
|
||||||
public readonly level: string,
|
public readonly level: Level,
|
||||||
public readonly std: Std,
|
public readonly std: Std,
|
||||||
visibleKeys?: Ref<string[][]>,
|
visibleKeys?: Ref<string[][]>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -14,66 +14,85 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="flex flex-col gap-4">
|
<section class="flex flex-col">
|
||||||
<div class="has-underline">
|
<div class="has-underline">
|
||||||
<h2>{{ $t("settings.display") }}</h2>
|
<h2>{{ $t("settings.display") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<section class="grid-cols-2 gap-4 md:grid">
|
||||||
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
|
<div class="flex flex-col gap-2 text-balance md:pr-8">
|
||||||
</div>
|
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
|
||||||
<div>
|
|
||||||
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
|
||||||
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-6">
|
<toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle>
|
||||||
<dropdown-menu
|
|
||||||
v-model="hourStyle"
|
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
|
||||||
:options="[
|
|
||||||
{ label: 'Auto', value: 'auto' },
|
<labeled-input>
|
||||||
{ label: '12', value: '12' },
|
<template #label>
|
||||||
{ label: '24', value: '24' },
|
{{ $t("settings.12-24-format") }}
|
||||||
]"
|
</template>
|
||||||
|
<template #input>
|
||||||
|
<dropdown-menu
|
||||||
|
v-model="hourStyle"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Auto', value: 'auto' },
|
||||||
|
{ label: '12', value: '12' },
|
||||||
|
{ label: '24', value: '24' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</labeled-input>
|
||||||
|
|
||||||
|
<labeled-input>
|
||||||
|
<template #label>
|
||||||
|
{{ $t("settings.font-size") }}
|
||||||
|
</template>
|
||||||
|
<template #input>
|
||||||
|
<dropdown-menu
|
||||||
|
v-model="size"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Small', value: 'small' },
|
||||||
|
{ label: 'Medium', value: 'medium' },
|
||||||
|
{ label: 'Large', value: 'large' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</labeled-input>
|
||||||
|
|
||||||
|
<labeled-input>
|
||||||
|
<template #label>
|
||||||
|
{{ $t("settings.color-scheme") }}
|
||||||
|
</template>
|
||||||
|
<template #input>
|
||||||
|
<dropdown-menu
|
||||||
|
v-model="lightTheme"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Auto', value: 'auto' },
|
||||||
|
{ label: 'Dark', value: 'dark' },
|
||||||
|
{ label: 'Light', value: 'light' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</labeled-input>
|
||||||
|
</div>
|
||||||
|
<log-viewer
|
||||||
|
:messages="fakeMessages"
|
||||||
|
:visible-keys="keys"
|
||||||
|
:last-selected-item="undefined"
|
||||||
|
class="mobile-hidden overflow-hidden rounded-lg border border-base-content/50 shadow"
|
||||||
/>
|
/>
|
||||||
{{ $t("settings.12-24-format") }}
|
</section>
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-6">
|
|
||||||
<dropdown-menu
|
|
||||||
v-model="size"
|
|
||||||
:options="[
|
|
||||||
{ label: 'Small', value: 'small' },
|
|
||||||
{ label: 'Medium', value: 'medium' },
|
|
||||||
{ label: 'Large', value: 'large' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
{{ $t("settings.font-size") }}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-6">
|
|
||||||
<dropdown-menu
|
|
||||||
v-model="lightTheme"
|
|
||||||
:options="[
|
|
||||||
{ label: 'Auto', value: 'auto' },
|
|
||||||
{ label: 'Dark', value: 'dark' },
|
|
||||||
{ label: 'Light', value: 'light' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
{{ $t("settings.color-scheme") }}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="flex flex-col gap-2">
|
<section class="flex flex-col gap-2">
|
||||||
<div class="has-underline">
|
<div class="has-underline">
|
||||||
<h2>{{ $t("settings.options") }}</h2>
|
<h2>{{ $t("settings.options") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<toggle v-model="search">
|
<toggle v-model="search">
|
||||||
<div>{{ $t("settings.search") }} <key-shortcut char="f" class="align-top"></key-shortcut></div>
|
{{ $t("settings.search") }} <key-shortcut char="f" class="align-top"></key-shortcut>
|
||||||
</toggle>
|
</toggle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -89,6 +108,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { ComplexLogEntry, SimpleLogEntry } from "@/models/LogEntry";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
automaticRedirect,
|
automaticRedirect,
|
||||||
hourStyle,
|
hourStyle,
|
||||||
@@ -106,6 +127,30 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
setTitle(t("title.settings"));
|
setTitle(t("title.settings"));
|
||||||
const { latest, hasUpdate } = useReleases();
|
const { latest, hasUpdate } = useReleases();
|
||||||
|
|
||||||
|
const keys = ref<string[][]>([]);
|
||||||
|
|
||||||
|
const fakeMessages = [
|
||||||
|
new SimpleLogEntry("This is a preview of the logs", 1, new Date(), "info", undefined, "stdout"),
|
||||||
|
new SimpleLogEntry("A warning log looks like this", 2, new Date(), "warn", undefined, "stdout"),
|
||||||
|
new SimpleLogEntry("This is a multi line error message", 3, new Date(), "error", "start", "stderr"),
|
||||||
|
new SimpleLogEntry("with a second line", 4, new Date(), "error", "middle", "stderr"),
|
||||||
|
new SimpleLogEntry("and finally third line.", 5, new Date(), "error", "end", "stderr"),
|
||||||
|
new ComplexLogEntry(
|
||||||
|
{
|
||||||
|
message: "This is a complex log entry",
|
||||||
|
context: {
|
||||||
|
key: "value",
|
||||||
|
key2: "value2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
6,
|
||||||
|
new Date(),
|
||||||
|
"info",
|
||||||
|
"stdout",
|
||||||
|
keys,
|
||||||
|
),
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.has-underline {
|
.has-underline {
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ settings:
|
|||||||
small-scrollbars: Use smaller scrollbars
|
small-scrollbars: Use smaller scrollbars
|
||||||
show-timesamps: Show timestamps
|
show-timesamps: Show timestamps
|
||||||
soft-wrap: Soft wrap lines
|
soft-wrap: Soft wrap lines
|
||||||
12-24-format: >-
|
12-24-format: Time format
|
||||||
By default, Dozzle will use your browser's locale to format time. You can
|
|
||||||
force to 12 or 24 hour style.
|
|
||||||
font-size: Font size to use for logs
|
font-size: Font size to use for logs
|
||||||
color-scheme: Color scheme
|
color-scheme: Color scheme
|
||||||
options: Options
|
options: Options
|
||||||
|
|||||||
Reference in New Issue
Block a user