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']
|
||||
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.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']
|
||||
ContainerStat: typeof import('./components/LogViewer/ContainerStat.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']
|
||||
InfiniteLoader: typeof import('./components/InfiniteLoader.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']
|
||||
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.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 { createRouter, createWebHistory } from "vue-router";
|
||||
import LogEventSource from "./LogEventSource.vue";
|
||||
import LogViewer from "./LogViewer.vue";
|
||||
import ContainerLogViewer from "./ContainerLogViewer.vue";
|
||||
|
||||
vi.mock("@/stores/config", () => ({
|
||||
__esModule: true,
|
||||
@@ -71,7 +71,7 @@ describe("<LogEventSource />", () => {
|
||||
global: {
|
||||
plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})],
|
||||
components: {
|
||||
LogViewer,
|
||||
ContainerLogViewer,
|
||||
},
|
||||
provide: {
|
||||
[containerContext as symbol]: {
|
||||
@@ -83,7 +83,7 @@ describe("<LogEventSource />", () => {
|
||||
},
|
||||
slots: {
|
||||
default: `
|
||||
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
|
||||
<template #scoped="params"><container-log-viewer :messages="params.messages" /></template>
|
||||
`,
|
||||
},
|
||||
props: {},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true }">
|
||||
<li
|
||||
v-for="item in filtered"
|
||||
v-for="item in messages"
|
||||
:key="item.id"
|
||||
:data-key="item.id"
|
||||
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
|
||||
@@ -14,36 +14,14 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRaw } from "vue";
|
||||
import { useRouteHash } from "@vueuse/router";
|
||||
|
||||
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
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>
|
||||
<style scoped lang="postcss">
|
||||
.events {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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>
|
||||
</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>
|
||||
<label class="label inline-flex cursor-pointer gap-4 font-normal">
|
||||
<input type="checkbox" class="toggle toggle-primary" v-model="modelValue" />
|
||||
<slot />
|
||||
</label>
|
||||
<labeled-input>
|
||||
<template #label>
|
||||
<slot />
|
||||
</template>
|
||||
<template #input>
|
||||
<input type="checkbox" class="toggle toggle-primary" v-model="modelValue" />
|
||||
</template>
|
||||
</labeled-input>
|
||||
</template>
|
||||
|
||||
<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 Position = "start" | "end" | "middle" | undefined;
|
||||
export type Std = "stdout" | "stderr";
|
||||
export type Level = "debug" | "info" | "warn" | "error" | "fatal" | "trace" | "unknown";
|
||||
export interface LogEvent {
|
||||
readonly m: string | JSONObject;
|
||||
readonly ts: number;
|
||||
readonly id: number;
|
||||
readonly l: string;
|
||||
readonly l: Level;
|
||||
readonly p: Position;
|
||||
readonly s: "stdout" | "stderr" | "unknown";
|
||||
}
|
||||
@@ -25,7 +26,7 @@ export abstract class LogEntry<T extends string | JSONObject> {
|
||||
public readonly id: number,
|
||||
public readonly date: Date,
|
||||
public readonly std: Std,
|
||||
public readonly level?: string,
|
||||
public readonly level?: Level,
|
||||
) {
|
||||
this._message = message;
|
||||
}
|
||||
@@ -42,7 +43,7 @@ export class SimpleLogEntry extends LogEntry<string> {
|
||||
message: string,
|
||||
id: number,
|
||||
date: Date,
|
||||
public readonly level: string,
|
||||
public readonly level: Level,
|
||||
public readonly position: Position,
|
||||
public readonly std: Std,
|
||||
) {
|
||||
@@ -60,7 +61,7 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
|
||||
message: JSONObject,
|
||||
id: number,
|
||||
date: Date,
|
||||
public readonly level: string,
|
||||
public readonly level: Level,
|
||||
public readonly std: Std,
|
||||
visibleKeys?: Ref<string[][]>,
|
||||
) {
|
||||
|
||||
@@ -14,66 +14,85 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-4">
|
||||
<section class="flex flex-col">
|
||||
<div class="has-underline">
|
||||
<h2>{{ $t("settings.display") }}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
|
||||
</div>
|
||||
<div>
|
||||
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
|
||||
</div>
|
||||
<div>
|
||||
<toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle>
|
||||
</div>
|
||||
<section class="grid-cols-2 gap-4 md:grid">
|
||||
<div class="flex flex-col gap-2 text-balance md:pr-8">
|
||||
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
|
||||
|
||||
<div>
|
||||
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
|
||||
</div>
|
||||
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
|
||||
|
||||
<div class="flex items-center gap-6">
|
||||
<dropdown-menu
|
||||
v-model="hourStyle"
|
||||
:options="[
|
||||
{ label: 'Auto', value: 'auto' },
|
||||
{ label: '12', value: '12' },
|
||||
{ label: '24', value: '24' },
|
||||
]"
|
||||
<toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle>
|
||||
|
||||
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
|
||||
|
||||
<labeled-input>
|
||||
<template #label>
|
||||
{{ $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") }}
|
||||
</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">
|
||||
<div class="has-underline">
|
||||
<h2>{{ $t("settings.options") }}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -89,6 +108,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ComplexLogEntry, SimpleLogEntry } from "@/models/LogEntry";
|
||||
|
||||
import {
|
||||
automaticRedirect,
|
||||
hourStyle,
|
||||
@@ -106,6 +127,30 @@ const { t } = useI18n();
|
||||
|
||||
setTitle(t("title.settings"));
|
||||
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>
|
||||
<style lang="postcss" scoped>
|
||||
.has-underline {
|
||||
|
||||
@@ -68,9 +68,7 @@ settings:
|
||||
small-scrollbars: Use smaller scrollbars
|
||||
show-timesamps: Show timestamps
|
||||
soft-wrap: Soft wrap lines
|
||||
12-24-format: >-
|
||||
By default, Dozzle will use your browser's locale to format time. You can
|
||||
force to 12 or 24 hour style.
|
||||
12-24-format: Time format
|
||||
font-size: Font size to use for logs
|
||||
color-scheme: Color scheme
|
||||
options: Options
|
||||
|
||||
Reference in New Issue
Block a user