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

View File

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

View File

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

View File

@@ -104,7 +104,7 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
exports[`<ContainerEventSource /> > renders loading correctly 1`] = ` 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 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`] = ` exports[`<ContainerEventSource /> > should parse messages 1`] = `

View File

@@ -7,12 +7,19 @@
<slot name="header"></slot> <slot name="header"></slot>
</header> </header>
<main :data-scrolling="scrollable ? true : undefined" class="snap-y overflow-auto"> <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" /> <ScrollProgress :indeterminate="loadingMore" :auto-hide="!loadingMore" class="!fixed top-16 z-10" />
</div> </div>
<div ref="scrollableContent"> <div ref="scrollableContent">
<slot></slot> <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>
<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> <div ref="scrollObserver" class="h-px"></div>
</main> </main>
@@ -22,7 +29,7 @@
class="btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow transition-colors" 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' : ''" :class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()" @click="scrollToBottom()"
v-show="paused" v-show="scrollContext.paused"
> >
<mdi:chevron-double-down /> <mdi:chevron-double-down />
</button> </button>
@@ -34,17 +41,16 @@
<script lang="ts" setup> <script lang="ts" setup>
const { scrollable = false } = defineProps<{ scrollable?: boolean }>(); const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
let paused = $ref(false);
let hasMore = $ref(false); let hasMore = $ref(false);
const scrollObserver = ref<HTMLElement>(); const scrollObserver = ref<HTMLElement>();
const scrollableContent = ref<HTMLElement>(); const scrollableContent = ref<HTMLElement>();
provide("scrollingPaused", $$(paused)); const scrollContext = provideScrollContext();
const { loadingMore } = useLoggingContext(); const { loadingMore } = useLoggingContext();
const mutationObserver = new MutationObserver((e) => { const mutationObserver = new MutationObserver((e) => {
if (!paused) { if (!scrollContext.paused) {
scrollToBottom(); scrollToBottom();
} else { } else {
const record = e[e.length - 1]; 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(
threshold: [0, 1], (entries) => (scrollContext.paused = entries[0].intersectionRatio == 0),
rootMargin: "80px 0px", {
}); threshold: [0, 1],
rootMargin: "80px 0px",
},
);
onMounted(() => { onMounted(() => {
mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true }); mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
@@ -80,6 +89,21 @@ function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
.fade-leave-to { .fade-leave-to {
@apply opacity-0; @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>
<style> <style>

View File

@@ -107,7 +107,7 @@ export type LogStreamSource = ReturnType<typeof useLogStream>;
function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) { function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const messages: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]); const messages: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const buffer: 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() { function flushNow() {
if (messages.value.length + buffer.value.length > config.maxLogs) { if (messages.value.length + buffer.value.length > config.maxLogs) {

View File

@@ -6,6 +6,7 @@ type LogContext = {
loadingMore: boolean; loadingMore: boolean;
}; };
// export for testing
export const loggingContextKey = Symbol("loggingContext") as InjectionKey<LogContext>; export const loggingContextKey = Symbol("loggingContext") as InjectionKey<LogContext>;
export const provideLoggingContext = (containers: Ref<Container[]>) => { 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"], content: ["./assets/**/*.{vue,js,ts}", "./public/index.html"],
theme: { theme: {
extend: { extend: {
blur: {
xs: "1px",
},
animation: { animation: {
"bounce-fast": "bounce 0.5s 2 both", "bounce-fast": "bounce 0.5s 2 both",
}, },