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

chore: refactors code by moving loader to a log entry (#3951)

This commit is contained in:
Amir Raminfar
2025-06-06 09:28:57 -07:00
committed by GitHub
parent 7257e35f1b
commit 9db559c9ce
9 changed files with 94 additions and 77 deletions

View File

@@ -53,6 +53,7 @@ declare module 'vue' {
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default'] KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default'] LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
Links: typeof import('./components/Links.vue')['default'] Links: typeof import('./components/Links.vue')['default']
LoadMoreLogItem: typeof import('./components/LogViewer/LoadMoreLogItem.vue')['default']
LogAnalytics: typeof import('./components/LogViewer/LogAnalytics.vue')['default'] LogAnalytics: typeof import('./components/LogViewer/LogAnalytics.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default'] LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default'] LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default']

View File

@@ -1,31 +0,0 @@
<template>
<div ref="root" class="flex min-h-[1px] justify-center">
<span class="loading loading-bars loading-md text-primary mt-4" v-show="isLoading"></span>
</div>
</template>
<script lang="ts" setup>
const { onLoadMore = () => Promise.resolve(), enabled } = defineProps<{
onLoadMore: () => Promise<void>;
enabled: boolean;
}>();
const isLoading = ref(false);
const root = ref<HTMLElement>();
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (onLoadMore && enabled) {
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await onLoadMore();
isLoading.value = false;
await nextTick();
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
}
});
onMounted(() => observer.observe(root.value!));
onUnmounted(() => observer.disconnect());
</script>

View File

@@ -1,5 +1,4 @@
<template> <template>
<InfiniteLoader :onLoadMore="fetchMore" :enabled="!loadingMore && messages.length > 10" />
<ul class="flex animate-pulse flex-col gap-4 p-4" v-if="loading || (noLogs && waitingForMoreLog)"> <ul class="flex animate-pulse flex-col gap-4 p-4" v-if="loading || (noLogs && waitingForMoreLog)">
<div class="flex flex-row gap-2" v-for="size in sizes"> <div class="flex flex-row gap-2" v-for="size in sizes">
<div class="bg-base-content/50 h-3 w-40 shrink-0 rounded-full opacity-50"></div> <div class="bg-base-content/50 h-3 w-40 shrink-0 rounded-full opacity-50"></div>
@@ -22,10 +21,8 @@ const { entity, streamSource } = $defineProps<{
entity: T; entity: T;
}>(); }>();
const { messages, loadOlderLogs, isLoadingMore, opened, loading, error, eventSourceURL } = streamSource( const { messages, opened, loading, error, eventSourceURL } = streamSource(toRef(() => entity));
toRef(() => entity),
);
const { loadingMore } = useLoggingContext();
const color = computed(() => { const color = computed(() => {
if (error.value) return "error"; if (error.value) return "error";
if (loading.value) return "secondary"; if (loading.value) return "secondary";
@@ -41,14 +38,6 @@ defineExpose({
clear: () => (messages.value = []), clear: () => (messages.value = []),
}); });
const fetchMore = async () => {
if (!isLoadingMore.value) {
loadingMore.value = true;
await loadOlderLogs();
loadingMore.value = false;
}
};
const sizes = computedWithControl(eventSourceURL, () => { const sizes = computedWithControl(eventSourceURL, () => {
const sizeOptions = [ const sizeOptions = [
"w-2/12", "w-2/12",

View File

@@ -0,0 +1,29 @@
<template>
<div ref="root" class="flex min-h-[1px] flex-1 content-center justify-center p-2">
<span class="loading loading-bars loading-md text-primary" v-show="isLoading"></span>
</div>
</template>
<script lang="ts" setup>
import { LoadMoreLogEntry } from "@/models/LogEntry";
const { logEntry } = defineProps<{
logEntry: LoadMoreLogEntry;
}>();
const isLoading = ref(false);
const root = ref<HTMLElement>();
useIntersectionObserver(root, async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (isLoading.value) return;
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await logEntry.loadMore();
isLoading.value = false;
await nextTick();
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
});
</script>
<style scoped></style>

View File

@@ -16,16 +16,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type JSONObject, LogEntry } from "@/models/LogEntry"; import { type JSONObject, LogEntry } from "@/models/LogEntry";
const { loading, progress, currentDate } = useScrollContext(); const { progress, currentDate } = useScrollContext();
const { messages } = defineProps<{ const { messages } = defineProps<{
messages: LogEntry<string | JSONObject>[]; messages: LogEntry<string | JSONObject>[];
}>(); }>();
watchEffect(() => {
loading.value = messages.length === 0;
});
const { containers } = useLoggingContext(); const { containers } = useLoggingContext();
const list = ref<HTMLElement[]>([]); const list = ref<HTMLElement[]>([]);

View File

@@ -2,6 +2,9 @@
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = ` exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false"> "<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" data-key="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center p-2"><span class="loading loading-bars loading-md text-primary" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry"> <li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch"> <div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<!--v-if--> <!--v-if-->
@@ -24,6 +27,9 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = ` exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false"> "<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" data-key="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center p-2"><span class="loading loading-bars loading-md text-primary" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry"> <li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch"> <div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<!--v-if--> <!--v-if-->
@@ -46,6 +52,9 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = ` exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false"> "<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" data-key="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center p-2"><span class="loading loading-bars loading-md text-primary" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry"> <li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch"> <div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<!--v-if--> <!--v-if-->
@@ -67,14 +76,14 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
`; `;
exports[`<ContainerEventSource /> > should parse messages 1`] = ` exports[`<ContainerEventSource /> > should parse messages 1`] = `
SimpleLogEntry { LoadMoreLogEntry {
"_message": "This is a message.", "_message": "",
"containerID": undefined, "containerID": "",
"date": 2019-06-12T10:55:42.459Z, "date": 2019-06-12T10:55:42.709Z,
"id": 1, "id": 1560336942709,
"level": undefined, "level": undefined,
"position": undefined, "loader": [Function],
"rawMessage": "This is a message.", "rawMessage": "info",
"std": "stderr", "std": "stderr",
} }
`; `;

View File

@@ -9,6 +9,7 @@ import {
ContainerEventLogEntry, ContainerEventLogEntry,
ComplexLogEntry, ComplexLogEntry,
SkippedLogsEntry, SkippedLogsEntry,
LoadMoreLogEntry,
} from "@/models/LogEntry"; } from "@/models/LogEntry";
import { Service, Stack } from "@/models/Stack"; import { Service, Stack } from "@/models/Stack";
import { Container, GroupedContainers } from "@/models/Container"; import { Container, GroupedContainers } from "@/models/Container";
@@ -60,6 +61,8 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const loading = ref(true); const loading = ref(true);
const error = ref(false); const error = ref(false);
const { paused: scrollingPaused } = useScrollContext(); const { paused: scrollingPaused } = useScrollContext();
const { streamConfig, hasComplexLogs, levels, loadingMore } = useLoggingContext();
let initial = true;
function flushNow() { function flushNow() {
if (messages.value.length + buffer.value.length > config.maxLogs) { if (messages.value.length + buffer.value.length > config.maxLogs) {
@@ -86,9 +89,15 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
buffer.value = []; buffer.value = [];
} }
} else { } else {
if (messages.value.length == 0) { if (initial) {
// sort the buffer the very first time because of multiple logs in parallel // sort the buffer the very first time because of multiple logs in parallel
buffer.value.sort((a, b) => a.date.getTime() - b.date.getTime()); buffer.value.sort((a, b) => a.date.getTime() - b.date.getTime());
if (loadMoreUrl) {
const loadMoreItem = new LoadMoreLogEntry(new Date(), loadOlderLogs);
messages.value = [loadMoreItem];
}
initial = false;
} }
messages.value = [...messages.value, ...buffer.value]; messages.value = [...messages.value, ...buffer.value];
buffer.value = []; buffer.value = [];
@@ -110,8 +119,6 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
buffer.value = []; buffer.value = [];
} }
const { streamConfig, hasComplexLogs, levels } = useLoggingContext();
const params = computed(() => { const params = computed(() => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (streamConfig.value.stdout) params.append("stdout", "1"); if (streamConfig.value.stdout) params.append("stdout", "1");
@@ -131,6 +138,7 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
opened.value = false; opened.value = false;
loading.value = true; loading.value = true;
error.value = false; error.value = false;
initial = true;
es = new EventSource(urlWithParams.value); es = new EventSource(urlWithParams.value);
es.addEventListener("container-event", (e) => { es.addEventListener("container-event", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { const event = JSON.parse((e as MessageEvent).data) as {
@@ -174,14 +182,13 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
watch(urlWithParams, () => connect(), { immediate: true }); watch(urlWithParams, () => connect(), { immediate: true });
const isLoadingMore = ref(false);
async function loadBetween(from: Date, to: Date, lastSeenId: number, minimum: number = 0) { async function loadBetween(from: Date, to: Date, lastSeenId: number, minimum: number = 0) {
if (!loadMoreUrl) throw new Error("No loadMoreUrl");
const abortController = new AbortController(); const abortController = new AbortController();
const signal = abortController.signal; const signal = abortController.signal;
if (isLoadingMore.value) throw new Error("Already loading"); if (loadingMore.value) throw new Error("Already loading");
try { try {
isLoadingMore.value = true; loadingMore.value = true;
const urlWithMoreParams = computed(() => { const urlWithMoreParams = computed(() => {
const loadMoreParams = new URLSearchParams(params.value); const loadMoreParams = new URLSearchParams(params.value);
loadMoreParams.append("from", from.toISOString()); loadMoreParams.append("from", from.toISOString());
@@ -204,21 +211,24 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
signal, signal,
}; };
} finally { } finally {
isLoadingMore.value = false; loadingMore.value = false;
} }
} }
async function loadOlderLogs() { async function loadOlderLogs(entry: LoadMoreLogEntry) {
if (!loadMoreUrl) throw new Error("No loadMoreUrl"); if (!loadMoreUrl) throw new Error("No loadMoreUrl");
const to = messages.value[0].date; if (!(messages.value[0] instanceof LoadMoreLogEntry)) throw new Error("No loadMoreLogEntry on first item");
const lastSeenId = messages.value[0].id;
const [loader, ...existingLogs] = messages.value;
const to = existingLogs[0].date;
const lastSeenId = existingLogs[0].id;
const last = messages.value[Math.min(messages.value.length - 1, 300)].date; const last = messages.value[Math.min(messages.value.length - 1, 300)].date;
const delta = to.getTime() - last.getTime(); const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta); const from = new Date(to.getTime() + delta);
try { try {
const { logs, signal } = await loadBetween(from, to, lastSeenId, 100); const { logs: newLogs, signal } = await loadBetween(from, to, lastSeenId, 100);
if (logs && signal.aborted === false) { if (newLogs && signal.aborted === false) {
messages.value = [...logs, ...messages.value]; messages.value = [loader, ...newLogs, ...existingLogs];
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -251,7 +261,6 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
return { return {
messages, messages,
loadOlderLogs, loadOlderLogs,
isLoadingMore,
hasComplexLogs, hasComplexLogs,
opened, opened,
error, error,

View File

@@ -1,5 +1,4 @@
type ScrollContext = { type ScrollContext = {
loading: boolean;
paused: boolean; paused: boolean;
progress: number; progress: number;
currentDate: Date; currentDate: Date;
@@ -9,20 +8,18 @@ type ScrollContext = {
export const scrollContextKey = Symbol("scrollContext") as InjectionKey<ScrollContext>; export const scrollContextKey = Symbol("scrollContext") as InjectionKey<ScrollContext>;
export const provideScrollContext = () => { export const provideScrollContext = () => {
const context = defauleValue(); const context = defaultValue();
provide(scrollContextKey, context); provide(scrollContextKey, context);
return context; return context;
}; };
export const useScrollContext = () => { export const useScrollContext = () => {
const defaultValue = defauleValue(); const context = inject(scrollContextKey, defaultValue());
const context = inject(scrollContextKey, defaultValue);
return toRefs(context); return toRefs(context);
}; };
function defauleValue() { function defaultValue() {
return reactive({ return reactive({
loading: false,
paused: false, paused: false,
progress: 1, progress: 1,
currentDate: new Date(), currentDate: new Date(),

View File

@@ -4,6 +4,7 @@ import ComplexLogItem from "@/components/LogViewer/ComplexLogItem.vue";
import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue"; import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import ContainerEventLogItem from "@/components/LogViewer/ContainerEventLogItem.vue"; import ContainerEventLogItem from "@/components/LogViewer/ContainerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue"; import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
import LoadMoreLogItem from "@/components/LogViewer/LoadMoreLogItem.vue";
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>; export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue }; export type JSONObject = { [x: string]: JSONValue };
@@ -188,6 +189,23 @@ export class SkippedLogsEntry extends LogEntry<string> {
} }
} }
export class LoadMoreLogEntry extends LogEntry<string> {
constructor(
date: Date,
private readonly loader: (i: LoadMoreLogEntry) => Promise<void>,
) {
super("", "", date.getTime(), date, "stderr", "info");
}
getComponent(): Component {
return LoadMoreLogItem;
}
async loadMore(): Promise<void> {
await this.loader(this);
}
}
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> { export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (isObject(event.m)) { if (isObject(event.m)) {
return new ComplexLogEntry( return new ComplexLogEntry(