mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 21:33:18 +01:00
Changes scroll progress to behave differently when fetching scroll back content. (#640)
This commit is contained in:
@@ -13,12 +13,14 @@
|
|||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</pane>
|
</pane>
|
||||||
<pane v-for="other in activeContainers" :key="other.id" v-if="!isMobile">
|
<pane v-for="other in activeContainers" :key="other.id" v-if="!isMobile">
|
||||||
<scrollable-view>
|
<log-container
|
||||||
<template v-slot:header>
|
:id="other.id"
|
||||||
<container-title :value="other.name" closable @close="removeActiveContainer(other)"></container-title>
|
:title="other.name"
|
||||||
</template>
|
show-title
|
||||||
<log-viewer-with-source :id="other.id"></log-viewer-with-source>
|
scrollable
|
||||||
</scrollable-view>
|
closable
|
||||||
|
@close="removeActiveContainer(other)"
|
||||||
|
></log-container>
|
||||||
</pane>
|
</pane>
|
||||||
</splitpanes>
|
</splitpanes>
|
||||||
</pane>
|
</pane>
|
||||||
@@ -41,26 +43,22 @@
|
|||||||
import { mapActions, mapGetters, mapState } from "vuex";
|
import { mapActions, mapGetters, mapState } from "vuex";
|
||||||
import { Splitpanes, Pane } from "splitpanes";
|
import { Splitpanes, Pane } from "splitpanes";
|
||||||
|
|
||||||
import LogViewerWithSource from "./components/LogViewerWithSource";
|
import LogContainer from "./components/LogContainer";
|
||||||
import ScrollableView from "./components/ScrollableView";
|
|
||||||
import SideMenu from "./components/SideMenu";
|
import SideMenu from "./components/SideMenu";
|
||||||
import MobileMenu from "./components/MobileMenu";
|
import MobileMenu from "./components/MobileMenu";
|
||||||
import Search from "./components/Search";
|
import Search from "./components/Search";
|
||||||
import ContainerTitle from "./components/ContainerTitle";
|
|
||||||
import Icon from "./components/Icon";
|
import Icon from "./components/Icon";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
LogViewerWithSource,
|
|
||||||
SideMenu,
|
SideMenu,
|
||||||
|
LogContainer,
|
||||||
MobileMenu,
|
MobileMenu,
|
||||||
ScrollableView,
|
|
||||||
Splitpanes,
|
Splitpanes,
|
||||||
Pane,
|
Pane,
|
||||||
Search,
|
Search,
|
||||||
ContainerTitle,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="observer" class="control" :class="{ 'is-loading': isLoading }"></div>
|
<div ref="observer" class="control"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "InfiniteLoader",
|
name: "InfiniteLoader",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {};
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
onLoadMore: Function,
|
onLoadMore: Function,
|
||||||
@@ -21,9 +19,7 @@ export default {
|
|||||||
if (this.onLoadMore && this.enabled) {
|
if (this.onLoadMore && this.enabled) {
|
||||||
const scrollingParent = this.$el.closest("[data-scrolling]") || document.documentElement;
|
const scrollingParent = this.$el.closest("[data-scrolling]") || document.documentElement;
|
||||||
const previousHeight = scrollingParent.scrollHeight;
|
const previousHeight = scrollingParent.scrollHeight;
|
||||||
this.isLoading = true;
|
|
||||||
await this.onLoadMore();
|
await this.onLoadMore();
|
||||||
this.isLoading = false;
|
|
||||||
this.$nextTick(() => (scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight));
|
this.$nextTick(() => (scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
45
assets/components/LogContainer.vue
Normal file
45
assets/components/LogContainer.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<scrollable-view :scrollable="scrollable">
|
||||||
|
<template v-slot:header v-if="showTitle">
|
||||||
|
<container-title :value="title" :closable="closable" @close="$emit('close')"></container-title>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ setLoading }">
|
||||||
|
<log-viewer-with-source :id="id" @loading-more="setLoading($event)"></log-viewer-with-source>
|
||||||
|
</template>
|
||||||
|
</scrollable-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LogViewerWithSource from "./LogViewerWithSource";
|
||||||
|
import ScrollableView from "./ScrollableView";
|
||||||
|
import ContainerTitle from "./ContainerTitle";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
showTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
scrollable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
closable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: "LogContainer",
|
||||||
|
components: {
|
||||||
|
LogViewerWithSource,
|
||||||
|
ScrollableView,
|
||||||
|
ContainerTitle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -60,6 +60,7 @@ export default {
|
|||||||
async loadOlderLogs() {
|
async loadOlderLogs() {
|
||||||
if (this.messages.length < 300) return;
|
if (this.messages.length < 300) return;
|
||||||
|
|
||||||
|
this.$emit("loading-more", true);
|
||||||
const to = this.messages[0].date;
|
const to = this.messages[0].date;
|
||||||
const last = this.messages[299].date;
|
const last = this.messages[299].date;
|
||||||
const delta = to - last;
|
const delta = to - last;
|
||||||
@@ -74,6 +75,7 @@ export default {
|
|||||||
.map((line) => this.parseMessage(line));
|
.map((line) => this.parseMessage(line));
|
||||||
this.messages.unshift(...newMessages);
|
this.messages.unshift(...newMessages);
|
||||||
}
|
}
|
||||||
|
this.$emit("loading-more", false);
|
||||||
},
|
},
|
||||||
parseMessage(data) {
|
parseMessage(data) {
|
||||||
let i = data.indexOf(" ");
|
let i = data.indexOf(" ");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<log-event-source :id="id" v-slot="eventSource">
|
<log-event-source :id="id" v-slot="eventSource" @loading-more="$emit('loading-more', $event)">
|
||||||
<log-viewer :messages="eventSource.messages"></log-viewer>
|
<log-viewer :messages="eventSource.messages"></log-viewer>
|
||||||
</log-event-source>
|
</log-event-source>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
>
|
>
|
||||||
<span></span> <span></span> <span></span>
|
<span></span> <span></span> <span></span>
|
||||||
</a>
|
</a>
|
||||||
<svg class="logo">
|
<router-link :to="{ name: 'default' }">
|
||||||
<use href="#logo"></use>
|
<svg class="logo">
|
||||||
</svg>
|
<use href="#logo"></use>
|
||||||
|
</svg>
|
||||||
|
</router-link>
|
||||||
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
|
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
|
||||||
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
|
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
|
||||||
<li v-for="item in visibleContainers" :key="item.id">
|
<li v-for="item in visibleContainers" :key="item.id">
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scroll-progress">
|
<div class="scroll-progress">
|
||||||
<svg width="100" height="100" viewBox="0 0 100 100">
|
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
|
||||||
<circle r="44" cx="50" cy="50" :style="{ '--progress': scrollProgress }" />
|
<circle r="44" cx="50" cy="50" :style="{ '--progress': scrollProgress }" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="is-overlay columns is-vcentered is-centered has-text-weight-light">
|
<div class="is-overlay columns is-vcentered is-centered has-text-weight-light">
|
||||||
<span class="column is-narrow is-paddingless is-size-2">
|
<template v-if="indeterminate">
|
||||||
{{ Math.ceil(scrollProgress * 100) }}
|
<div class="column is-narrow is-paddingless is-size-2">
|
||||||
</span>
|
∞
|
||||||
<span class="column is-narrow is-paddingless">
|
</div>
|
||||||
%
|
</template>
|
||||||
</span>
|
<template v-else>
|
||||||
|
<span class="column is-narrow is-paddingless is-size-2">
|
||||||
|
{{ Math.ceil(scrollProgress * 100) }}
|
||||||
|
</span>
|
||||||
|
<span class="column is-narrow is-paddingless">
|
||||||
|
%
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -20,6 +27,16 @@ import throttle from "lodash.throttle";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ScrollProgress",
|
name: "ScrollProgress",
|
||||||
|
props: {
|
||||||
|
indeterminate: {
|
||||||
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
autoHide: {
|
||||||
|
default: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
scrollProgress: 0,
|
scrollProgress: 0,
|
||||||
@@ -39,6 +56,9 @@ export default {
|
|||||||
this.detachEvents();
|
this.detachEvents();
|
||||||
this.attachEvents();
|
this.attachEvents();
|
||||||
},
|
},
|
||||||
|
indeterminate() {
|
||||||
|
this.$nextTick(() => this.onScroll());
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["activeContainers"]),
|
...mapState(["activeContainers"]),
|
||||||
@@ -54,16 +74,18 @@ export default {
|
|||||||
onScroll() {
|
onScroll() {
|
||||||
const p = this.parentElement == document ? document.documentElement : this.parentElement;
|
const p = this.parentElement == document ? document.documentElement : this.parentElement;
|
||||||
this.scrollProgress = p.scrollTop / (p.scrollHeight - p.clientHeight);
|
this.scrollProgress = p.scrollTop / (p.scrollHeight - p.clientHeight);
|
||||||
this.animation.cancel();
|
if (this.autoHide) {
|
||||||
this.animation = this.$el.animate(
|
this.animation.cancel();
|
||||||
{ opacity: [1, 0] },
|
this.animation = this.$el.animate(
|
||||||
{
|
{ opacity: [1, 0] },
|
||||||
duration: 500,
|
{
|
||||||
delay: 2000,
|
duration: 500,
|
||||||
fill: "both",
|
delay: 2000,
|
||||||
easing: "ease-out",
|
fill: "both",
|
||||||
}
|
easing: "ease-out",
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -79,10 +101,46 @@ export default {
|
|||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
stroke: var(--primary-color);
|
stroke: var(--primary-color);
|
||||||
stroke-dashoffset: calc(276.32px - var(--progress) * 276.32px);
|
stroke-dashoffset: calc(276.32 - var(--progress) * 276.32);
|
||||||
stroke-dasharray: 276.32px 276.32px;
|
stroke-dasharray: 276.32px 276.32px;
|
||||||
|
stroke-linecap: round;
|
||||||
stroke-width: 3;
|
stroke-width: 3;
|
||||||
will-change: stroke-dashoffset;
|
will-change: stroke-dashoffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg.indeterminate {
|
||||||
|
animation: 2s linear infinite svg-animation;
|
||||||
|
|
||||||
|
circle {
|
||||||
|
animation: 1.4s ease-in-out infinite both circle-animation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes svg-animation {
|
||||||
|
0% {
|
||||||
|
transform: rotateZ(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotateZ(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes circle-animation {
|
||||||
|
0%,
|
||||||
|
25% {
|
||||||
|
stroke-dashoffset: 275;
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
75% {
|
||||||
|
stroke-dashoffset: 70;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 275;
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
</header>
|
</header>
|
||||||
<main ref="content" :data-scrolling="scrollable">
|
<main ref="content" :data-scrolling="scrollable">
|
||||||
<div class="is-scrollbar-progress is-hidden-mobile">
|
<div class="is-scrollbar-progress is-hidden-mobile">
|
||||||
<scroll-progress v-show="paused"></scroll-progress>
|
<scroll-progress v-show="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot :setLoading="setLoading"></slot>
|
||||||
<div ref="scrollObserver" class="is-scroll-observer"></div>
|
<div ref="scrollObserver" class="is-scroll-observer"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
paused: false,
|
paused: false,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
|
loading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -73,6 +74,9 @@ export default {
|
|||||||
this.$refs.scrollObserver.scrollIntoView({ behavior });
|
this.$refs.scrollObserver.scrollIntoView({ behavior });
|
||||||
this.hasMore = false;
|
this.hasMore = false;
|
||||||
},
|
},
|
||||||
|
setLoading(loading) {
|
||||||
|
this.loading = loading;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<scrollable-view :scrollable="activeContainers.length > 0">
|
<log-container
|
||||||
<template v-slot:header v-if="activeContainers.length > 0">
|
:id="id"
|
||||||
<container-title :value="allContainersById[id].name"></container-title>
|
:title="title"
|
||||||
</template>
|
:show-title="activeContainers.length > 0"
|
||||||
<log-viewer-with-source :id="id"></log-viewer-with-source>
|
:scrollable="activeContainers.length > 0"
|
||||||
</scrollable-view>
|
>
|
||||||
|
</log-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapGetters, mapState } from "vuex";
|
import { mapActions, mapGetters, mapState } from "vuex";
|
||||||
|
|
||||||
import LogViewerWithSource from "../components/LogViewerWithSource";
|
import LogContainer from "../components/LogContainer";
|
||||||
import ScrollableView from "../components/ScrollableView";
|
|
||||||
import ContainerTitle from "../components/ContainerTitle";
|
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["id", "name"],
|
props: ["id"],
|
||||||
name: "Container",
|
name: "Container",
|
||||||
components: {
|
components: {
|
||||||
LogViewerWithSource,
|
LogContainer,
|
||||||
ScrollableView,
|
|
||||||
ContainerTitle,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="panel-tabs">
|
<p class="panel-tabs" v-if="!search">
|
||||||
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">Running</a>
|
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">Running</a>
|
||||||
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">All</a>
|
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">All</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user