Compare commits

...

37 Commits

Author SHA1 Message Date
Amir Raminfar
be7012a860 1.20.15 2020-01-10 21:24:09 -08:00
Amir Raminfar
4892dcd892 Fixes tests 2020-01-10 21:24:03 -08:00
Amir Raminfar
8538fb2f55 1.20.14 2020-01-10 21:20:14 -08:00
Amir Raminfar
2592d62ed9 Removes size for second pane 2020-01-10 21:20:04 -08:00
Amir Raminfar
f91c7ccb21 1.20.13 2020-01-10 08:18:43 -08:00
Amir Raminfar
24ade2f856 Updates libs 2020-01-10 08:18:38 -08:00
Amir
756a8e4643 1.20.12 2020-01-09 11:51:53 -08:00
Amir
34533cd830 Updates npm modules 2020-01-09 11:51:49 -08:00
Amir
21e88b645e 1.20.11 2020-01-07 11:32:40 -08:00
Amir
775715a17c Fixes #185 2020-01-07 11:32:35 -08:00
Amir
a59f7caafc Updates husky 2020-01-07 10:50:14 -08:00
Amir
6903299523 1.20.10 2020-01-07 10:39:56 -08:00
Amir
1f34ebfdc1 Fixes search placeholder 2020-01-07 10:38:56 -08:00
Amir
98ee491865 Fixes title 2020-01-07 10:37:57 -08:00
Amir
d408cfca1d 1.20.9 2020-01-06 17:03:00 -08:00
Amir
a8366174e9 Fixes ui bug with menu 2020-01-06 17:02:55 -08:00
Amir
1b97d18ef0 1.20.8 2020-01-06 16:29:27 -08:00
Amir Raminfar
678b197d6a 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
2020-01-06 16:28:45 -08:00
Amir Raminfar
86bb4e12b3 Fixes tty bug with #200 2020-01-04 12:15:54 -08:00
Amir Raminfar
32dd847f4f Refactors tty code 2020-01-04 12:02:47 -08:00
Amir Raminfar
35a5093f8e Cleans up settings page 2019-12-30 17:06:45 -08:00
Amir Raminfar
6b5f5aeae3 Updates buefy 2019-12-30 17:02:47 -08:00
Amir Raminfar
b41f315a25 1.20.6 2019-12-30 09:50:49 -08:00
Amir Raminfar
376ee2d730 Adds compression 2019-12-30 09:50:37 -08:00
Amir Raminfar
79a42bf9fb 1.20.5 2019-12-30 09:46:19 -08:00
Amir Raminfar
2eff0dbeee Removes standard-version 2019-12-30 09:46:09 -08:00
Amir Raminfar
da9cddb691 perf: fixes a performance issues with store using strict mode 2019-12-30 09:23:13 -08:00
Amir Raminfar
184e742b1b 1.20.3 2019-12-29 15:16:08 -08:00
Amir Raminfar
42287f8848 chore(release): 1.20.4 2019-12-29 15:16:07 -08:00
Amir Raminfar
6495531d45 1.20.2 2019-12-29 15:15:53 -08:00
Amir Raminfar
3045d6011f chore(release): 1.20.3 2019-12-29 15:15:52 -08:00
Amir Raminfar
8a78db30c6 1.20.1 2019-12-29 15:15:42 -08:00
Amir Raminfar
cbe8aede9c chore(release): 1.20.2 2019-12-29 15:15:41 -08:00
Amir Raminfar
a0019b1019 Fixes scripts 2019-12-29 15:14:34 -08:00
Amir Raminfar
4e6d9c4c40 chore(release): 1.20.1 2019-12-29 15:06:13 -08:00
Amir Raminfar
54a636163c Adds standversion 2019-12-29 15:05:29 -08:00
Amir Raminfar
cc99eaa819 fix: tries to fix a scroll bug in mobile 2019-12-29 15:05:02 -08:00
17 changed files with 930 additions and 270 deletions

View File

@@ -2,7 +2,7 @@ before:
hooks:
- npm run clean
- npm run build
- packr
- packr -z
builds:
- env:
- CGO_ENABLED=0

View File

@@ -1,25 +1,20 @@
<template lang="html">
<main>
<mobile-menu v-if="isMobile"></mobile-menu>
<splitpanes @resized="updateSetting({ menuWidth: $event[0].size })">
<splitpanes @resized="onResize($event)">
<pane min-size="10" :size="settings.menuWidth" v-if="!isMobile">
<side-menu></side-menu>
</pane>
<pane :size="isMobile ? 100 : 100 - settings.menuWidth" min-size="10">
<pane 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 {
@@ -75,19 +72,17 @@ export default {
fetchContainerList: "FETCH_CONTAINERS",
removeActiveContainer: "REMOVE_ACTIVE_CONTAINER",
updateSetting: "UPDATE_SETTING"
})
}),
onResize(e) {
if (e.length == 2) {
this.updateSetting({ menuWidth: Math.min(90, e[0].size) });
}
}
}
};
</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;
@@ -99,4 +94,8 @@ export default {
.button.has-no-border {
border-color: transparent !important;
}
.has-min-height {
min-height: 100vh;
}
</style>

View File

@@ -19,13 +19,13 @@ exports[`<App /> renders correctly 1`] = `
<pane-stub
maxsize="100"
minsize="10"
size="85"
>
<splitpanes-stub
dblclicksplitter="true"
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,12 +55,14 @@ export default {
<style scoped lang="scss">
aside {
padding: 1em;
position: absolute;
position: fixed;
top: 0;
left: 0;
right: 0;
background: #222;
z-index: 2;
max-height: 100vh;
overflow: auto;
.menu-label {
margin-top: 1em;

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,12 +67,16 @@ export default {
section {
display: flex;
flex-direction: column;
height: 100vh;
&.is-full-height-scrollable {
height: 100vh;
}
main {
flex: 1;
overflow: auto;
}
.scroll-bar-notification {
text-align: right;
margin-right: 65px;

View File

@@ -5,12 +5,14 @@
<input
class="input"
type="text"
placeholder="Filter"
placeholder="Find / RegEx"
ref="filter"
v-model="filter"
@keyup.esc="resetSearch()"
/>
<span class="icon is-small is-left"><ion-icon name="search"></ion-icon></span>
<span class="icon is-small is-left">
<ion-icon name="search"></ion-icon>
</span>
</p>
</div>
<div class="column is-1 has-text-centered">

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,8 @@ aside {
padding: 1em;
height: 100vh;
overflow: auto;
position: fixed;
width: inherit;
.hide-overflow {
text-overflow: ellipsis;

View File

@@ -1,23 +1,53 @@
<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
};
},
mounted() {
if (this.allContainersById[this.id]) {
this.title = this.allContainersById[this.id].name;
}
},
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,11 +1,10 @@
<template lang="html">
<div class="is-fullheight">
<div>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">About</h2>
</div>
<h2 class="title is-6 is-marginless">Version</h2>
<div>
You are using Dozzle <i>{{ currentVersion }}</i
>.
@@ -57,20 +56,6 @@ import gt from "semver/functions/gt";
import valid from "semver/functions/valid";
import { mapActions, mapState } from "vuex";
function computedSettings(names) {
return names.reduce((map, name) => {
map[name] = {
get() {
return this.settings[name];
},
set(value) {
this.updateSetting({ [name]: value });
}
};
return map;
}, {});
}
export default {
props: [],
name: "Settings",
@@ -89,8 +74,7 @@ export default {
},
metaInfo() {
return {
title: "Settings",
titleTemplate: "%s - Dozzle"
title: "Settings"
};
},
methods: {
@@ -100,15 +84,21 @@ export default {
},
computed: {
...mapState(["settings"]),
...computedSettings.bind(this)(["search", "size"])
...["search", "size"].reduce((map, name) => {
map[name] = {
get() {
return this.settings[name];
},
set(value) {
this.updateSetting({ [name]: value });
}
};
return map;
}, {})
}
};
</script>
<style lang="scss">
.is-fullheight {
min-height: 100vh;
}
.title {
color: #eee;
}

View File

@@ -57,14 +57,26 @@ 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);
mql.addListener(e => store.commit("SET_MOBILE_WIDTH", e.matches));
const store = new Vuex.Store({
strict: true,
state,
getters,
actions,

View File

@@ -123,6 +123,34 @@ func (d *dockerClient) ListContainers(showAll bool) ([]Container, error) {
return containers, nil
}
func logReader(reader io.ReadCloser, tty bool) func() (string, error) {
if tty {
scanner := bufio.NewScanner(reader)
return func() (string, error) {
if scanner.Scan() {
return scanner.Text(), nil
}
return "", io.EOF
}
}
hdr := make([]byte, 8)
var buffer bytes.Buffer
return func() (string, error) {
buffer.Reset()
_, err := reader.Read(hdr)
if err != nil {
return "", err
}
count := binary.BigEndian.Uint32(hdr[4:])
_, err = io.CopyN(&buffer, reader, int64(count))
if err != nil {
return "", err
}
return strings.TrimSpace(buffer.String()), nil
}
}
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int) (<-chan string, <-chan error) {
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: strconv.Itoa(tailSize), Timestamps: true}
reader, err := d.cli.ContainerLogs(ctx, id, options)
@@ -142,49 +170,23 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in
containerJSON, _ := d.cli.ContainerInspect(ctx, id)
if containerJSON.Config.Tty {
go func() {
defer close(messages)
defer close(errChannel)
defer reader.Close()
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
select {
case messages <- line:
case <-ctx.Done():
}
go func() {
defer close(messages)
defer close(errChannel)
defer reader.Close()
nextEntry := logReader(reader, containerJSON.Config.Tty)
for {
line, err := nextEntry()
if err != nil {
errChannel <- err
break
}
}()
} else {
go func() {
defer close(messages)
defer close(errChannel)
defer reader.Close()
hdr := make([]byte, 8)
var buffer bytes.Buffer
for {
_, err := reader.Read(hdr)
if err != nil {
errChannel <- err
break
}
count := binary.BigEndian.Uint32(hdr[4:])
_, err = io.CopyN(&buffer, reader, int64(count))
if err != nil {
errChannel <- err
break
}
select {
case messages <- buffer.String():
case <-ctx.Done():
}
buffer.Reset()
select {
case messages <- line:
case <-ctx.Done():
}
}()
}
}
}()
return messages, errChannel
}
@@ -202,15 +204,15 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
Until: strconv.FormatInt(to.Unix(), 10),
}
reader, _ := d.cli.ContainerLogs(ctx, id, options)
defer reader.Close()
containerJSON, _ := d.cli.ContainerInspect(ctx, id)
nextEntry := logReader(reader, containerJSON.Config.Tty)
var messages []string
hdr := make([]byte, 8)
var buffer bytes.Buffer
for {
_, err := reader.Read(hdr)
line, err := nextEntry()
if err != nil {
if err == io.EOF {
break
@@ -218,18 +220,7 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
return nil, err
}
}
count := binary.BigEndian.Uint32(hdr[4:])
_, err = io.CopyN(&buffer, reader, int64(count))
if err != nil {
if err == io.EOF {
break
} else {
return nil, err
}
}
messages = append(messages, strings.TrimSpace(buffer.String()))
buffer.Reset()
messages = append(messages, line)
}
return messages, nil

815
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "dozzle",
"version": "1.20.0",
"version": "1.20.15",
"description": "Realtime log viewer for docker containers. ",
"scripts": {
"prestart": "npm run clean",
@@ -25,13 +25,13 @@
"homepage": "https://github.com/amir20/dozzle#readme",
"dependencies": {
"ansi-to-html": "^0.6.13",
"buefy": "^0.8.8",
"buefy": "^0.8.9",
"bulma": "^0.8.0",
"date-fns": "^2.8.1",
"date-fns": "^2.9.0",
"hotkeys-js": "^3.7.3",
"lodash.debounce": "^4.0.8",
"semver": "^7.1.1",
"splitpanes": "^2.2.0",
"splitpanes": "^2.2.1",
"store": "^2.0.12",
"vue": "^2.6.11",
"vue-meta": "^2.3.1",
@@ -41,13 +41,14 @@
"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",
"concurrently": "^5.0.2",
"cz-conventional-changelog": "^3.0.2",
"eventsourcemock": "^2.0.0",
"husky": "^3.1.0",
"husky": "^4.0.6",
"jest": "^24.9.0",
"jest-serializer-vue": "^2.0.2",
"lint-staged": "^9.5.0",
@@ -55,7 +56,7 @@
"node-fetch": "^2.6.0",
"parcel-bundler": "^1.12.4",
"prettier": "^1.19.1",
"sass": "^1.24.0",
"sass": "^1.24.4",
"vue-hot-reload-api": "^2.3.4",
"vue-jest": "^3.0.5",
"vue-template-compiler": "^2.6.11"
@@ -104,5 +105,10 @@
".*\\.vue$": "vue-jest",
".+\\.js$": "babel-jest"
}
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}