1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 21:33:18 +01:00

feat: adds preview for settings (#2669)

This commit is contained in:
Amir Raminfar
2024-01-08 12:21:37 -08:00
committed by GitHub
parent 27fe0663cf
commit 72a580574a
10 changed files with 164 additions and 87 deletions

View File

@@ -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']

View 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>

View File

@@ -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: {},

View File

@@ -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 {

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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[][]>,
) { ) {

View File

@@ -14,26 +14,26 @@
</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">
<div class="flex flex-col gap-2 text-balance md:pr-8">
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle> <toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
</div>
<div>
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle> <toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
</div>
<div>
<toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle> <toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle>
</div>
<div>
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle> <toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
</div>
<div class="flex items-center gap-6"> <labeled-input>
<template #label>
{{ $t("settings.12-24-format") }}
</template>
<template #input>
<dropdown-menu <dropdown-menu
v-model="hourStyle" v-model="hourStyle"
:options="[ :options="[
@@ -42,9 +42,14 @@
{ label: '24', value: '24' }, { label: '24', value: '24' },
]" ]"
/> />
{{ $t("settings.12-24-format") }} </template>
</div> </labeled-input>
<div class="flex items-center gap-6">
<labeled-input>
<template #label>
{{ $t("settings.font-size") }}
</template>
<template #input>
<dropdown-menu <dropdown-menu
v-model="size" v-model="size"
:options="[ :options="[
@@ -53,9 +58,14 @@
{ label: 'Large', value: 'large' }, { label: 'Large', value: 'large' },
]" ]"
/> />
{{ $t("settings.font-size") }} </template>
</div> </labeled-input>
<div class="flex items-center gap-6">
<labeled-input>
<template #label>
{{ $t("settings.color-scheme") }}
</template>
<template #input>
<dropdown-menu <dropdown-menu
v-model="lightTheme" v-model="lightTheme"
:options="[ :options="[
@@ -64,16 +74,25 @@
{ label: 'Light', value: 'light' }, { label: 'Light', value: 'light' },
]" ]"
/> />
{{ $t("settings.color-scheme") }} </template>
</labeled-input>
</div> </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"
/>
</section> </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 {

View File

@@ -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