1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-24 06:28:42 +01:00

feat: adds animations to bottom when in live mode (#3231)

This commit is contained in:
Amir Raminfar
2024-08-26 07:58:39 -07:00
committed by GitHub
parent afcb80938e
commit aec8139a19
9 changed files with 83 additions and 17 deletions

View File

@@ -107,6 +107,7 @@ declare global {
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const provideLogDetails: typeof import('./composable/showLogDetails')['provideLogDetails']
const provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext']
const provideScrollContext: typeof import('./composable/scrollContext')['provideScrollContext']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@@ -123,6 +124,7 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const scrollContextKey: typeof import('./composable/scrollContext')['scrollContextKey']
const search: typeof import('./stores/settings')['search']
const sessionHost: typeof import('./composable/storage')['sessionHost']
const setActivePinia: typeof import('pinia')['setActivePinia']
@@ -291,6 +293,7 @@ declare global {
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollContext: typeof import('./composable/scrollContext')['useScrollContext']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composable/search')['useSearchFilter']
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
@@ -466,6 +469,7 @@ declare module 'vue' {
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -482,6 +486,7 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
@@ -648,6 +653,7 @@ declare module 'vue' {
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['useScrollContext']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
@@ -816,6 +822,7 @@ declare module '@vue/runtime-core' {
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -832,6 +839,7 @@ declare module '@vue/runtime-core' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
@@ -998,6 +1006,7 @@ declare module '@vue/runtime-core' {
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['useScrollContext']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>

View File

@@ -74,7 +74,10 @@ describe("<ContainerEventSource />", () => {
LogViewer,
},
provide: {
scrollingPaused: computed(() => false),
[scrollContextKey as symbol]: {
paused: computed(() => false),
loading: computed(() => false),
},
[loggingContextKey as symbol]: {
containers: computed(() => [{ id: "abc", image: "test:v123", host: "localhost" }]),
streamConfig: reactive({ stdout: true, stderr: true }),

View File

@@ -19,22 +19,24 @@
/>
</li>
</ul>
<div v-else class="m-4 text-center">
<span class="loading loading-ring loading-md text-primary"></span>
</div>
</template>
<script lang="ts" setup>
import { toRaw } from "vue";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
defineProps<{
const { loading } = useScrollContext();
const { messages } = defineProps<{
messages: LogEntry<string | JSONObject>[];
visibleKeys: string[][];
lastSelectedItem: LogEntry<string | JSONObject> | undefined;
showContainerName: boolean;
}>();
watchEffect(() => {
loading.value = messages.length === 0;
});
</script>
<style scoped lang="postcss">
.events {

View File

@@ -104,7 +104,7 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
exports[`<ContainerEventSource /> > renders loading correctly 1`] = `
"<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div>
<div data-v-cf9ff940="" class="m-4 text-center"><span data-v-cf9ff940="" class="loading loading-ring loading-md text-primary"></span></div>"
<!--v-if-->"
`;
exports[`<ContainerEventSource /> > should parse messages 1`] = `

View File

@@ -7,12 +7,19 @@
<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="paused">
<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>
<div ref="scrollableContent">
<slot></slot>
<div v-if="scrollContext.loading" class="m-4 text-center">
<span class="loading loading-ring loading-md text-primary"></span>
</div>
</div>
<div
class="animate-background h-0.5 bg-gradient-to-br from-primary via-transparent to-primary blur-xs"
v-show="!scrollContext.paused && !scrollContext.loading"
></div>
<div ref="scrollObserver" class="h-px"></div>
</main>
@@ -22,7 +29,7 @@
class="btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow transition-colors"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()"
v-show="paused"
v-show="scrollContext.paused"
>
<mdi:chevron-double-down />
</button>
@@ -34,17 +41,16 @@
<script lang="ts" setup>
const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
let paused = $ref(false);
let hasMore = $ref(false);
const scrollObserver = ref<HTMLElement>();
const scrollableContent = ref<HTMLElement>();
provide("scrollingPaused", $$(paused));
const scrollContext = provideScrollContext();
const { loadingMore } = useLoggingContext();
const mutationObserver = new MutationObserver((e) => {
if (!paused) {
if (!scrollContext.paused) {
scrollToBottom();
} else {
const record = e[e.length - 1];
@@ -55,10 +61,13 @@ const mutationObserver = new MutationObserver((e) => {
}
});
const intersectionObserver = new IntersectionObserver((entries) => (paused = entries[0].intersectionRatio == 0), {
const intersectionObserver = new IntersectionObserver(
(entries) => (scrollContext.paused = entries[0].intersectionRatio == 0),
{
threshold: [0, 1],
rootMargin: "80px 0px",
});
},
);
onMounted(() => {
mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
@@ -80,6 +89,21 @@ function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
.fade-leave-to {
@apply opacity-0;
}
.animate-background {
background-size: 400% 400%;
animation: gradient-animation 4s ease infinite;
}
@keyframes gradient-animation {
0%,
100% {
background-position: 0% 0%;
}
50% {
background-position: 100% 100%;
}
}
</style>
<style>

View File

@@ -107,7 +107,7 @@ export type LogStreamSource = ReturnType<typeof useLogStream>;
function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const messages: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const buffer: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
const { paused: scrollingPaused } = useScrollContext();
function flushNow() {
if (messages.value.length + buffer.value.length > config.maxLogs) {

View File

@@ -6,6 +6,7 @@ type LogContext = {
loadingMore: boolean;
};
// export for testing
export const loggingContextKey = Symbol("loggingContext") as InjectionKey<LogContext>;
export const provideLoggingContext = (containers: Ref<Container[]>) => {

View File

@@ -0,0 +1,24 @@
type ScrollContext = {
loading: boolean;
paused: boolean;
};
// export for testing
export const scrollContextKey = Symbol("scrollContext") as InjectionKey<ScrollContext>;
export const provideScrollContext = () => {
const context = reactive({
loading: false,
paused: false,
});
provide(scrollContextKey, context);
return context;
};
export const useScrollContext = () => {
const context = inject(scrollContextKey);
if (!context) {
throw new Error("No scroll context provided");
}
return toRefs(context);
};

View File

@@ -10,6 +10,9 @@ export default {
content: ["./assets/**/*.{vue,js,ts}", "./public/index.html"],
theme: {
extend: {
blur: {
xs: "1px",
},
animation: {
"bounce-fast": "bounce 0.5s 2 both",
},