mirror of
https://github.com/amir20/dozzle.git
synced 2026-01-04 12:05:07 +01:00
Fixes mobile to use document as container for scrolling (#223)
* Uses intersectionObserver instead * Use intersectionObserver * Updates mods * Adds title when more than one container is active * Updates logic to use native scrolling when only one logger view is open * Fixes broken test * Uses close instead of closed * Fixes scrollingParent
This commit is contained in:
@@ -7,19 +7,14 @@
|
||||
</pane>
|
||||
<pane :size="isMobile ? 100 : 100 - settings.menuWidth" min-size="10">
|
||||
<splitpanes>
|
||||
<pane>
|
||||
<pane class="has-min-height">
|
||||
<search></search>
|
||||
<router-view></router-view>
|
||||
</pane>
|
||||
<pane v-for="other in activeContainers" :key="other.id">
|
||||
<pane v-for="other in activeContainers" :key="other.id" v-if="!isMobile">
|
||||
<scrollable-view>
|
||||
<template v-slot:header>
|
||||
<div class="name columns is-marginless">
|
||||
<span class="column">{{ other.name }}</span>
|
||||
<span class="column is-narrow">
|
||||
<button class="delete is-medium" @click="removeActiveContainer(other)"></button>
|
||||
</span>
|
||||
</div>
|
||||
<container-title :value="other.name" closable @close="removeActiveContainer(other)"></container-title>
|
||||
</template>
|
||||
<log-viewer-with-source :id="other.id"></log-viewer-with-source>
|
||||
</scrollable-view>
|
||||
@@ -39,6 +34,7 @@ import ScrollableView from "./components/ScrollableView";
|
||||
import SideMenu from "./components/SideMenu";
|
||||
import MobileMenu from "./components/MobileMenu";
|
||||
import Search from "./components/Search";
|
||||
import ContainerTitle from "./components/ContainerTitle";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
@@ -49,7 +45,8 @@ export default {
|
||||
ScrollableView,
|
||||
Splitpanes,
|
||||
Pane,
|
||||
Search
|
||||
Search,
|
||||
ContainerTitle
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -86,13 +83,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.name {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
::v-deep .splitpanes__splitter {
|
||||
min-width: 4px;
|
||||
background: #666;
|
||||
@@ -104,4 +94,8 @@ export default {
|
||||
.button.has-no-border {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.has-min-height {
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,6 +26,7 @@ exports[`<App /> renders correctly 1`] = `
|
||||
pushotherpanes="true"
|
||||
>
|
||||
<pane-stub
|
||||
class="has-min-height"
|
||||
maxsize="100"
|
||||
minsize="0"
|
||||
>
|
||||
|
||||
30
assets/components/ContainerTitle.vue
Normal file
30
assets/components/ContainerTitle.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template lang="html">
|
||||
<div class="name columns is-marginless">
|
||||
<span class="column">{{ value }}</span>
|
||||
<span class="column is-narrow" v-if="closable">
|
||||
<button class="delete is-medium" @click="$emit('close')"></button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
name: "ContainerTitle"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.name {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,6 @@ export default {
|
||||
name: "InfiniteLoader",
|
||||
data() {
|
||||
return {
|
||||
scrollingParent: null,
|
||||
isLoading: false
|
||||
};
|
||||
},
|
||||
@@ -16,16 +15,16 @@ export default {
|
||||
enabled: Boolean
|
||||
},
|
||||
mounted() {
|
||||
this.scrollingParent = this.$el.closest("[data-scrolling]");
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
async entries => {
|
||||
if (entries[0].intersectionRatio <= 0) return;
|
||||
if (this.onLoadMore && this.enabled) {
|
||||
const previousHeight = this.scrollingParent.scrollHeight;
|
||||
const scrollingParent = this.$el.closest("[data-scrolling]") || document.documentElement;
|
||||
const previousHeight = scrollingParent.scrollHeight;
|
||||
this.isLoading = true;
|
||||
await this.onLoadMore();
|
||||
this.isLoading = false;
|
||||
this.$nextTick(() => (this.scrollingParent.scrollTop += this.scrollingParent.scrollHeight - previousHeight));
|
||||
this.$nextTick(() => (scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight));
|
||||
}
|
||||
},
|
||||
{ threshholds: 1 }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
|
||||
<slot v-bind:messages="messages"></slot>
|
||||
<slot :messages="messages"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -40,12 +40,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
...mapState(["containers"]),
|
||||
activeContainersById() {
|
||||
return this.activeContainers.reduce((map, obj) => {
|
||||
map[obj.id] = obj;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
...mapGetters(["activeContainersById"])
|
||||
},
|
||||
methods: {
|
||||
...mapActions({})
|
||||
@@ -60,7 +55,7 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
aside {
|
||||
padding: 1em;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<template lang="html">
|
||||
<scrollable-view>
|
||||
<log-viewer-with-source :id="id"></log-viewer-with-source>
|
||||
</scrollable-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollableView from "./ScrollableView";
|
||||
import LogViewerWithSource from "./LogViewerWithSource";
|
||||
|
||||
export default {
|
||||
props: ["id"],
|
||||
name: "ScrollableLogsWithSource",
|
||||
components: {
|
||||
LogViewerWithSource,
|
||||
ScrollableView
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,17 +1,18 @@
|
||||
<template lang="html">
|
||||
<section>
|
||||
<section :class="{ 'is-full-height-scrollable': scrollable }">
|
||||
<header v-if="$slots.header">
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<main ref="content" @scroll.passive="onScroll" data-scrolling>
|
||||
<main ref="content" :data-scrolling="scrollable">
|
||||
<slot></slot>
|
||||
<div ref="scrollObserver"></div>
|
||||
</main>
|
||||
<div class="scroll-bar-notification">
|
||||
<transition name="fade">
|
||||
<button
|
||||
class="button"
|
||||
:class="hasMore ? 'is-warning' : 'is-primary'"
|
||||
@click="scrollToBottom('smooth')"
|
||||
@click="scrollToBottom('instant')"
|
||||
v-show="paused"
|
||||
>
|
||||
<ion-icon name="download"></ion-icon>
|
||||
@@ -23,6 +24,12 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
name: "ScrollableView",
|
||||
data() {
|
||||
return {
|
||||
@@ -39,21 +46,19 @@ export default {
|
||||
this.hasMore = true;
|
||||
}
|
||||
}).observe(content, { childList: true, subtree: true });
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
entries => (this.paused = entries[0].intersectionRatio == 0),
|
||||
{ threshholds: [0, 1] }
|
||||
);
|
||||
|
||||
intersectionObserver.observe(this.$refs.scrollObserver);
|
||||
},
|
||||
|
||||
methods: {
|
||||
scrollToBottom(behavior = "instant") {
|
||||
const { content } = this.$refs;
|
||||
if (typeof content.scroll === "function") {
|
||||
content.scroll({ top: content.scrollHeight, behavior });
|
||||
} else {
|
||||
content.scrollTop = content.scrollHeight;
|
||||
}
|
||||
this.$refs.scrollObserver.scrollIntoView({ behavior });
|
||||
this.hasMore = false;
|
||||
},
|
||||
onScroll(e) {
|
||||
const { content } = this.$refs;
|
||||
this.paused = content.scrollTop + content.clientHeight + 1 < content.scrollHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -62,13 +67,16 @@ export default {
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
|
||||
&.is-full-height-scrollable {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.scroll-bar-notification {
|
||||
text-align: right;
|
||||
margin-right: 65px;
|
||||
|
||||
@@ -49,12 +49,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(["containers", "activeContainers"]),
|
||||
activeContainersById() {
|
||||
return this.activeContainers.reduce((map, obj) => {
|
||||
map[obj.id] = obj;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
...mapGetters(["activeContainersById"])
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
@@ -68,6 +63,7 @@ aside {
|
||||
padding: 1em;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
|
||||
.hide-overflow {
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<scrollable-logs-with-source :id="id"></scrollable-logs-with-source>
|
||||
</div>
|
||||
<scrollable-view :scrollable="activeContainers.length > 0">
|
||||
<template v-slot:header v-if="activeContainers.length > 0">
|
||||
<container-title :value="allContainersById[id].name"></container-title>
|
||||
</template>
|
||||
<log-viewer-with-source :id="id"></log-viewer-with-source>
|
||||
</scrollable-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollableLogsWithSource from "../components/ScrollableLogsWithSource";
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
|
||||
import LogViewerWithSource from "../components/LogViewerWithSource";
|
||||
import ScrollableView from "../components/ScrollableView";
|
||||
import ContainerTitle from "../components/ContainerTitle";
|
||||
|
||||
export default {
|
||||
props: ["id", "name"],
|
||||
name: "Container",
|
||||
components: {
|
||||
ScrollableLogsWithSource
|
||||
LogViewerWithSource,
|
||||
ScrollableView,
|
||||
ContainerTitle
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: "loading"
|
||||
};
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.name,
|
||||
titleTemplate: "%s - Dozzle"
|
||||
title: this.title
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activeContainers"]),
|
||||
...mapGetters(["allContainersById"])
|
||||
},
|
||||
watch: {
|
||||
id() {
|
||||
this.title = this.allContainersById[this.id].name;
|
||||
},
|
||||
allContainersById() {
|
||||
this.title = this.allContainersById[this.id].name;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template lang="html">
|
||||
<div class="is-fullheight">
|
||||
<div>
|
||||
<section class="section">
|
||||
<div class="has-underline">
|
||||
<h2 class="title is-4">About</h2>
|
||||
@@ -74,8 +74,7 @@ export default {
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: "Settings",
|
||||
titleTemplate: "%s - Dozzle"
|
||||
title: "Settings"
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -100,10 +99,6 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.is-fullheight {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,20 @@ const actions = {
|
||||
commit("UPDATE_SETTINGS", setting);
|
||||
}
|
||||
};
|
||||
const getters = {};
|
||||
const getters = {
|
||||
activeContainersById(state) {
|
||||
return state.activeContainers.reduce((map, obj) => {
|
||||
map[obj.id] = obj;
|
||||
return map;
|
||||
}, {});
|
||||
},
|
||||
allContainersById(state) {
|
||||
return state.containers.reduce((map, obj) => {
|
||||
map[obj.id] = obj;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
};
|
||||
|
||||
const es = new EventSource(`${BASE_PATH}/api/events/stream`);
|
||||
es.addEventListener("containers-changed", e => setTimeout(() => store.dispatch("FETCH_CONTAINERS"), 1000), false);
|
||||
|
||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -2030,9 +2030,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@vue/component-compiler-utils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.1.0.tgz",
|
||||
"integrity": "sha512-OJ7swvl8LtKtX5aYP8jHhO6fQBIRIGkU6rvWzK+CGJiNOnvg16nzcBkd9qMZzW8trI2AsqAKx263nv7kb5rhZw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-+lN3nsfJJDGMNz7fCpcoYIORrXo0K3OTsdr8jCM7FuqdI4+70TY6gxY6viJ2Xi1clqyPg7LpeOWwjF31vSMmUw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"consolidate": "^0.15.1",
|
||||
@@ -2040,12 +2040,29 @@
|
||||
"lru-cache": "^4.1.2",
|
||||
"merge-source-map": "^1.1.0",
|
||||
"postcss": "^7.0.14",
|
||||
"postcss-selector-parser": "^5.0.0",
|
||||
"postcss-selector-parser": "^6.0.2",
|
||||
"prettier": "^1.18.2",
|
||||
"source-map": "~0.6.1",
|
||||
"vue-template-es2015-compiler": "^1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"postcss-selector-parser": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
|
||||
"integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cssesc": "^3.0.0",
|
||||
"indexes-of": "^1.0.1",
|
||||
"uniq": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -10740,9 +10757,9 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.24.0.tgz",
|
||||
"integrity": "sha512-1TsPyMhLTx+9DLlmwg02iBW2p4poGA7LlkWJLpUY/XticFKNhPcx+l4FsIJLKl6oSUfXmAKpVljHEez1hwjqiw==",
|
||||
"version": "1.24.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.24.2.tgz",
|
||||
"integrity": "sha512-0JxdMMRd0fOmGFQFRI91vh4n0Ed766ib9JwPUa+1C37zn3VaqlHxbknUn/6LqP/MSfvNPxRYoCrYf5g8vu4OHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@vue/component-compiler-utils": "^3.1.0",
|
||||
"@vue/component-compiler-utils": "^3.1.1",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
@@ -56,7 +56,7 @@
|
||||
"node-fetch": "^2.6.0",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^1.19.1",
|
||||
"sass": "^1.24.0",
|
||||
"sass": "^1.24.2",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
|
||||
Reference in New Issue
Block a user