1
0
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:
Amir Raminfar
2020-01-06 16:28:45 -08:00
committed by GitHub
parent 86bb4e12b3
commit 678b197d6a
14 changed files with 146 additions and 92 deletions

View File

@@ -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>

View File

@@ -26,6 +26,7 @@ exports[`<App /> renders correctly 1`] = `
pushotherpanes="true"
>
<pane-stub
class="has-min-height"
maxsize="100"
minsize="0"
>

View 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>

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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
View File

@@ -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"

View File

@@ -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"