1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-26 07:13:41 +01:00

feat: changes scroll progress to reflect total available logs instead of just the logs in the browser (#3236)

This commit is contained in:
Amir Raminfar
2024-08-26 13:12:24 -07:00
committed by GitHub
parent 3a0eaf1609
commit 0a03923587
6 changed files with 111 additions and 89 deletions

View File

@@ -1,13 +1,15 @@
<template>
<ul
class="events group py-4"
class="events group pt-4"
:class="{ 'disable-wrap': !softWrap, [size]: true, compact }"
v-if="messages.length > 0"
>
<li
v-for="item in messages"
ref="list"
:key="item.id"
:data-key="item.id"
:data-time="item.date.getTime()"
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
class="group/entry"
>
@@ -24,7 +26,7 @@
<script lang="ts" setup>
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const { loading } = useScrollContext();
const { loading, progress, currentDate } = useScrollContext();
const { messages } = defineProps<{
messages: LogEntry<string | JSONObject>[];
@@ -36,6 +38,33 @@ const { messages } = defineProps<{
watchEffect(() => {
loading.value = messages.length === 0;
});
const { containers } = useLoggingContext();
const list = ref<HTMLElement[]>([]);
useIntersectionObserver(
list,
(entries) => {
if (containers.value.length != 1) return;
const container = containers.value[0];
for (const entry of entries) {
if (entry.isIntersecting) {
const time = entry.target.getAttribute("data-time");
if (time) {
const date = new Date(parseInt(time));
const diff = new Date().getTime() - container.created.getTime();
progress.value = (date.getTime() - container.created.getTime()) / diff;
currentDate.value = date;
}
}
}
},
{
rootMargin: "-10% 0px -10% 0px",
threshold: 1,
},
);
</script>
<style scoped lang="postcss">
.events {

View File

@@ -1,8 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group py-4 medium">
<li data-v-cf9ff940="" data-key="1" class="group/entry">
"<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="">
<!--v-if-->
<!--v-if-->
@@ -21,8 +21,8 @@ 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 py-4 medium">
<li data-v-cf9ff940="" data-key="1" class="group/entry">
"<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="">
<!--v-if-->
<!--v-if-->
@@ -41,8 +41,8 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
`;
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-cf9ff940="" class="events group py-4 medium">
<li data-v-cf9ff940="" data-key="1" class="group/entry">
"<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="">
<!--v-if-->
<!--v-if-->
@@ -61,8 +61,8 @@ 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 py-4 medium">
<li data-v-cf9ff940="" data-key="2" class="group/entry">
"<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="">
<!--v-if-->
<!--v-if-->
@@ -83,8 +83,8 @@ 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 py-4 medium">
<li data-v-cf9ff940="" data-key="1" class="group/entry">
"<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="">
<!--v-if-->
<!--v-if-->

View File

@@ -1,60 +1,45 @@
<template>
<transition name="fadeout">
<div class="pointer-events-none relative inline-block" ref="root" v-show="!autoHide || show">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
</svg>
<div class="absolute inset-0 flex items-center justify-center font-light">
<template v-if="indeterminate">
<div class="text-4xl">&#8734;</div>
</template>
<template v-else-if="!isNaN(scrollProgress)">
<div class="inline-flex flex-col items-end gap-2" ref="root" v-show="!autoHide || show">
<div class="relative inline-block">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
</svg>
<div class="absolute inset-0 flex items-center justify-center font-light">
<span class="text-4xl">
{{ Math.ceil(scrollProgress * 100) }}
{{ Math.ceil(progress * 100) }}
</span>
<span> % </span>
</template>
</div>
</div>
<DistanceTime :date="date" class="whitespace-nowrap text-sm" />
</div>
</transition>
</template>
<script lang="ts" setup>
const { indeterminate = false, autoHide = false } = defineProps<{
const {
indeterminate = false,
autoHide = false,
progress,
date = new Date(),
} = defineProps<{
indeterminate?: boolean;
autoHide?: boolean;
progress: number;
date?: Date;
}>();
const scrollProgress = ref(0);
const root = ref<HTMLElement>();
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const scrollElement = ref<HTMLElement | Document>((root.value?.closest("[data-scrolling]") as HTMLElement) ?? document);
const { y: scrollY } = useScroll(scrollElement as Ref<HTMLElement | Document>, { throttle: 100 });
const show = autoResetRef(false, 2000);
onMounted(() => {
watch(
pinnedLogs,
() => {
scrollElement.value = (root.value?.closest("[data-scrolling]") as HTMLElement) ?? document;
},
{ immediate: true, flush: "post" },
);
});
watchPostEffect(() => {
const parent =
scrollElement.value === document
? (scrollElement.value as Document).documentElement
: (scrollElement.value as HTMLElement);
scrollProgress.value = Math.max(0, Math.min(1, scrollY.value / (parent.scrollHeight - parent.clientHeight)));
if (autoHide) {
show.value = true;
}
});
watch(
() => progress,
() => {
if (autoHide) {
show.value = true;
}
},
);
</script>
<style scoped lang="postcss">
svg {
@@ -72,7 +57,7 @@ svg {
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dashoffset: calc(276.32px - v-bind(progress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;

View File

@@ -7,8 +7,16 @@
<slot name="header"></slot>
</header>
<main :data-scrolling="scrollable ? true : undefined" class="snap-y overflow-auto">
<div class="invisible mr-28 text-right md:visible" v-show="scrollContext.paused">
<ScrollProgress :indeterminate="loadingMore" :auto-hide="!loadingMore" class="!fixed top-16 z-10" />
<div class="invisible relative md:visible" v-show="scrollContext.paused">
<div class="absolute right-44 top-4">
<ScrollProgress
:indeterminate="loadingMore"
:auto-hide="!loadingMore"
:progress="scrollContext.progress"
:date="scrollContext.currentDate"
class="!fixed z-10 min-w-40"
/>
</div>
</div>
<div ref="scrollableContent">
<slot></slot>
@@ -17,7 +25,7 @@
</div>
</div>
<div
class="animate-background h-0.5 bg-gradient-to-br from-primary via-transparent to-primary blur-xs"
class="animate-background h-1 bg-gradient-to-br from-primary via-transparent to-primary"
v-show="!scrollContext.paused && !scrollContext.loading"
></div>
<div ref="scrollObserver" class="h-px"></div>
@@ -26,7 +34,7 @@
<div class="mr-16 text-right">
<transition name="fade">
<button
class="btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow transition-colors"
class="transition-colorsblur-xs dark btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()"
v-show="scrollContext.paused"
@@ -41,7 +49,7 @@
<script lang="ts" setup>
const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
let hasMore = $ref(false);
let hasMore = ref(false);
const scrollObserver = ref<HTMLElement>();
const scrollableContent = ref<HTMLElement>();
@@ -49,39 +57,30 @@ const scrollContext = provideScrollContext();
const { loadingMore } = useLoggingContext();
const mutationObserver = new MutationObserver((e) => {
if (!scrollContext.paused) {
scrollToBottom();
} else {
const record = e[e.length - 1];
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore = true;
useIntersectionObserver(scrollObserver, ([entry]) => (scrollContext.paused = entry.intersectionRatio == 0), {
threshold: [0, 1],
rootMargin: "80px 0px",
});
useMutationObserver(
scrollableContent,
(records) => {
if (!scrollContext.paused) {
scrollToBottom();
} else {
const record = records[records.length - 1];
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore.value = true;
}
}
}
});
const intersectionObserver = new IntersectionObserver(
(entries) => (scrollContext.paused = entries[0].intersectionRatio == 0),
{
threshold: [0, 1],
rootMargin: "80px 0px",
},
{ childList: true, subtree: true },
);
onMounted(() => {
if (scrollableContent.value) mutationObserver.observe(scrollableContent.value, { childList: true, subtree: true });
if (scrollObserver.value) intersectionObserver.observe(scrollObserver.value);
});
onUnmounted(() => {
mutationObserver.disconnect();
intersectionObserver.disconnect();
});
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
scrollObserver.value?.scrollIntoView({ behavior });
hasMore = false;
hasMore.value = false;
}
</script>
<style scoped lang="postcss">

View File

@@ -21,9 +21,14 @@ export const provideLoggingContext = (containers: Ref<Container[]>) => {
};
export const useLoggingContext = () => {
const context = inject(loggingContextKey);
if (!context) {
throw new Error("No logging context provided");
}
const context = inject(
loggingContextKey,
reactive({
streamConfig: { stdout: true, stderr: true },
containers: [],
loadingMore: false,
}),
);
return toRefs(context);
};

View File

@@ -1,6 +1,8 @@
type ScrollContext = {
loading: boolean;
paused: boolean;
progress: number;
currentDate: Date;
};
// export for testing
@@ -22,5 +24,7 @@ function defauleValue() {
return reactive({
loading: false,
paused: false,
progress: 1,
currentDate: new Date(),
});
}