Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3796e97855 | ||
|
|
1da3ba8c5f | ||
|
|
b972129678 | ||
|
|
e6a343dca0 | ||
|
|
0f5cf053de | ||
|
|
25ec792acb | ||
|
|
7ae9230e32 | ||
|
|
6288380186 | ||
|
|
2ebb6d128d | ||
|
|
cd7ea2f904 | ||
|
|
cef015a722 | ||
|
|
95bc0072b5 | ||
|
|
a3b489c9cd | ||
|
|
575d7d3494 | ||
|
|
eff39d888c | ||
|
|
6ad98503ac | ||
|
|
58fad3d105 | ||
|
|
9ae8b08725 | ||
|
|
3d91d6e53d | ||
|
|
fa1067ff42 | ||
|
|
78587dff85 | ||
|
|
c19315176a | ||
|
|
1237b08e91 | ||
|
|
742ad32e7c | ||
|
|
bea8c71297 | ||
|
|
3b38395ddb | ||
|
|
e3f2edc02b | ||
|
|
fe698ca641 | ||
|
|
eaefd38269 | ||
|
|
8aff4146bb | ||
|
|
1637c15f76 | ||
|
|
7c6677ceaa | ||
|
|
c04e9a1b9f | ||
|
|
0bdd84de13 | ||
|
|
b1ee9d230d | ||
|
|
3ca87a0d9c | ||
|
|
595f69dfe7 | ||
|
|
b1ba21a78b | ||
|
|
9017b8c27a | ||
|
|
bc848a8baf | ||
|
|
3d61f6db05 | ||
|
|
b25de1d921 | ||
|
|
0d63b72cf6 | ||
|
|
9936aeec6c | ||
|
|
8885764e72 | ||
|
|
46db2b035e | ||
|
|
74273b8bf7 | ||
|
|
dcd7ccc099 | ||
|
|
5bfd62b57a | ||
|
|
dc9739c1f3 | ||
|
|
2a0d0194a7 | ||
|
|
920c10c318 | ||
|
|
fee69db8a3 | ||
|
|
ea663368e7 | ||
|
|
334d6feffd | ||
|
|
b8ebc2e2c0 | ||
|
|
2e6b35354d | ||
|
|
42d2b57814 | ||
|
|
e88b535127 | ||
|
|
0bb8d8e2e6 | ||
|
|
5678b3192d | ||
|
|
117530f1d6 | ||
|
|
ee5973b9f3 | ||
|
|
fc99b7320f | ||
|
|
a8536a101e | ||
|
|
5302ace413 | ||
|
|
2e255ace60 | ||
|
|
b7515fb32a | ||
|
|
396a6bb3e9 | ||
|
|
79fcbdcd62 | ||
|
|
e2348f5b38 | ||
|
|
d345998052 | ||
|
|
08cc04379d | ||
|
|
027af5a853 | ||
|
|
abc27165a2 | ||
|
|
3aa420a589 | ||
|
|
045ff730bc | ||
|
|
075f17ee36 | ||
|
|
cb3177fa06 | ||
|
|
d3e00adf4e | ||
|
|
9100958e22 | ||
|
|
eed1c0c568 | ||
|
|
f692bee5cd | ||
|
|
86cfaaee6d | ||
|
|
e8b5fd0523 | ||
|
|
2fadf6e680 | ||
|
|
835549d503 | ||
|
|
0540ceaf43 | ||
|
|
6812d1ac62 | ||
|
|
1375a59105 | ||
|
|
ec5ece7975 | ||
|
|
2efcc07706 | ||
|
|
9cef13ae74 | ||
|
|
3466ce0004 |
2
.reflex
@@ -1 +1 @@
|
||||
-r '\.go$' -R '^node_modules/' -R '^static/' -R '^.cache/' -G '*_test.go' -s -- go run main.go routes.go --level debug
|
||||
-r '\.go$' -R 'node_modules' -R '^static/' -R '^.cache/' -G '*_test.go' -s -- go run main.go routes.go --level debug
|
||||
|
||||
@@ -54,4 +54,6 @@ ENV PATH=/bin
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /dozzle/dozzle /dozzle
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/dozzle"]
|
||||
|
||||
@@ -138,6 +138,6 @@ To Build and test locally:
|
||||
2. Install Go.
|
||||
3. Globally install [packr utility](https://github.com/gobuffalo/packr) with `go get -u github.com/gobuffalo/packr/packr` outside of dozzle directory.
|
||||
4. Install [reflex](https://github.com/cespare/reflex) with `get -u github.com/cespare/reflex` outside of dozzle.
|
||||
5. Install node modules with `npm install`.
|
||||
6. Do `npm start`
|
||||
5. Install node modules with `yarn`.
|
||||
6. Do `yarn dev`
|
||||
|
||||
|
||||
@@ -91,6 +91,9 @@ X-Accel-Buffering: no
|
||||
|
||||
data: INFO Testing logs...
|
||||
|
||||
event: container-stopped
|
||||
data: end of stream
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy_container_stopped */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
@@ -102,6 +105,9 @@ X-Accel-Buffering: no
|
||||
event: container-stopped
|
||||
data: end of stream
|
||||
|
||||
event: container-stopped
|
||||
data: end of stream
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy_with_id */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
@@ -111,4 +117,7 @@ Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
data: 2020-05-13T18:55:37.772853839Z INFO Testing logs...
|
||||
id: 2020-05-13T18:55:37.772853839Z
|
||||
id: 2020-05-13T18:55:37.772853839Z
|
||||
|
||||
event: container-stopped
|
||||
data: end of stream
|
||||
@@ -26,5 +26,18 @@ export default {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
|
||||
button.delete {
|
||||
background-color: var(--scheme-main-ter);
|
||||
opacity: 0.6;
|
||||
&:after,
|
||||
&:before {
|
||||
background-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,35 +25,38 @@ export default {
|
||||
created() {
|
||||
this.es = null;
|
||||
this.loadLogs(this.id);
|
||||
this.flushBuffer = debounce(this.flushNow, 250, { maxWait: 1000 });
|
||||
},
|
||||
methods: {
|
||||
loadLogs(id) {
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
this.messages = [];
|
||||
this.buffer = [];
|
||||
this.es = null;
|
||||
}
|
||||
this.reset();
|
||||
this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}`);
|
||||
|
||||
this.es.addEventListener("container-stopped", (e) => {
|
||||
this.es.close();
|
||||
this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date() });
|
||||
flushNow();
|
||||
this.flushBuffer();
|
||||
this.flushBuffer.flush();
|
||||
});
|
||||
this.es.addEventListener("error", (e) => console.log("EventSource failed: " + JSON.stringify(e)));
|
||||
|
||||
const flushBuffer = debounce(() => flushNow(), 250, { maxWait: 1000 });
|
||||
const flushNow = () => {
|
||||
this.messages.push(...this.buffer);
|
||||
this.buffer = [];
|
||||
};
|
||||
this.es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
|
||||
this.es.onmessage = (e) => {
|
||||
this.buffer.push(this.parseMessage(e.data));
|
||||
flushBuffer();
|
||||
this.flushBuffer();
|
||||
};
|
||||
this.$once("hook:beforeDestroy", () => this.es.close());
|
||||
},
|
||||
flushNow() {
|
||||
this.messages.push(...this.buffer);
|
||||
this.buffer = [];
|
||||
},
|
||||
reset() {
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
this.es = null;
|
||||
this.flushBuffer.cancel();
|
||||
}
|
||||
this.messages = [];
|
||||
this.buffer = [];
|
||||
},
|
||||
async loadOlderLogs() {
|
||||
if (this.messages.length < 300) return;
|
||||
|
||||
@@ -62,7 +65,7 @@ export default {
|
||||
const delta = to - last;
|
||||
const from = new Date(to.getTime() + delta);
|
||||
const logs = await (
|
||||
await fetch(`/api/logs?id=${this.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
|
||||
await fetch(`${config.base}/api/logs?id=${this.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
|
||||
).text();
|
||||
if (logs) {
|
||||
const newMessages = logs
|
||||
|
||||
39
assets/components/PastTime.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<time :datetime="date.toISOString()">{{ text }}</time>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formatDistance from "date-fns/formatDistance";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
date: {
|
||||
required: true,
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
text: "",
|
||||
interval: null,
|
||||
};
|
||||
},
|
||||
name: "PastTime",
|
||||
mounted() {
|
||||
this.updateFromNow();
|
||||
this.interval = setInterval(() => this.updateFromNow(), 30000);
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
methods: {
|
||||
updateFromNow() {
|
||||
this.text = formatDistance(this.date, new Date(), {
|
||||
addSuffix: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100">
|
||||
<circle r="44" cx="50" cy="50" :style="{ '--progress': scrollProgress }" />
|
||||
</svg>
|
||||
<div class="percent 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">
|
||||
{{ Math.ceil(scrollProgress * 100) }}
|
||||
</span>
|
||||
@@ -84,13 +84,5 @@ export default {
|
||||
stroke-width: 3;
|
||||
will-change: stroke-dashoffset;
|
||||
}
|
||||
|
||||
.percent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<main ref="content" :data-scrolling="scrollable">
|
||||
<div class="scrollbar-progress is-hidden-mobile">
|
||||
<div class="is-scrollbar-progress is-hidden-mobile">
|
||||
<scroll-progress v-show="paused"></scroll-progress>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<div ref="scrollObserver"></div>
|
||||
<div ref="scrollObserver" class="is-scroll-observer"></div>
|
||||
</main>
|
||||
|
||||
<div class="scrollbar-notification">
|
||||
<div class="is-scrollbar-notification">
|
||||
<transition name="fade">
|
||||
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
|
||||
<icon name="download"></icon>
|
||||
@@ -78,6 +78,7 @@ section {
|
||||
|
||||
&.is-full-height-scrollable {
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -86,7 +87,7 @@ section {
|
||||
scroll-snap-type: y proximity;
|
||||
}
|
||||
|
||||
.scrollbar-progress {
|
||||
.is-scrollbar-progress {
|
||||
text-align: right;
|
||||
margin-right: 110px;
|
||||
.scroll-progress {
|
||||
@@ -96,7 +97,11 @@ section {
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar-notification {
|
||||
.is-scroll-observer {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.is-scrollbar-notification {
|
||||
text-align: right;
|
||||
margin-right: 65px;
|
||||
button {
|
||||
|
||||
@@ -82,8 +82,18 @@ export default {
|
||||
z-index: 10;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
|
||||
.delete {
|
||||
button.delete {
|
||||
margin-left: 1em;
|
||||
background-color: var(--scheme-main-ter);
|
||||
opacity: 0.6;
|
||||
&:after,
|
||||
&:before {
|
||||
background-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
<aside>
|
||||
<div class="columns is-marginless">
|
||||
<div class="column is-paddingless">
|
||||
<svg class="logo">
|
||||
<use href="#logo"></use>
|
||||
</svg>
|
||||
<router-link :to="{ name: 'default' }">
|
||||
<svg class="logo">
|
||||
<use href="#logo"></use>
|
||||
</svg>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="column is-narrow has-text-right x">
|
||||
<router-link
|
||||
|
||||
@@ -6,10 +6,7 @@ import Switch from "buefy/dist/esm/switch";
|
||||
import store from "./store";
|
||||
import config from "./store/config";
|
||||
import App from "./App.vue";
|
||||
import Container from "./pages/Container.vue";
|
||||
import Settings from "./pages/Settings.vue";
|
||||
import Index from "./pages/Index.vue";
|
||||
import Show from "./pages/Show.vue";
|
||||
import { Container, Settings, Index, Show, ContainerNotFound, PageNotFound } from "./pages";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
Vue.use(Meta);
|
||||
@@ -28,6 +25,11 @@ const routes = [
|
||||
name: "container",
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/container/*",
|
||||
component: ContainerNotFound,
|
||||
name: "container-not-found",
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
component: Settings,
|
||||
@@ -38,6 +40,11 @@ const routes = [
|
||||
component: Show,
|
||||
name: "show",
|
||||
},
|
||||
{
|
||||
path: "/*",
|
||||
component: PageNotFound,
|
||||
name: "page-not-found",
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
|
||||
@@ -13,6 +13,7 @@ import { mapActions, mapGetters, mapState } from "vuex";
|
||||
import LogViewerWithSource from "../components/LogViewerWithSource";
|
||||
import ScrollableView from "../components/ScrollableView";
|
||||
import ContainerTitle from "../components/ContainerTitle";
|
||||
import store from "../store";
|
||||
|
||||
export default {
|
||||
props: ["id", "name"],
|
||||
|
||||
18
assets/pages/ContainerNotFound.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="hero is-halfheight">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<h1 class="title">
|
||||
Container not found.
|
||||
<small class="subtitle">It may have been removed.</small>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ContainerNotFound",
|
||||
};
|
||||
</script>
|
||||
@@ -1,9 +0,0 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import Index from "./Index";
|
||||
|
||||
describe("<Index />", () => {
|
||||
test("renders correctly", () => {
|
||||
const wrapper = shallowMount(Index);
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,153 @@
|
||||
<template>
|
||||
<div class="hero is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<h1 class="title">Please choose a container from the list to view the logs</h1>
|
||||
<div>
|
||||
<section class="hero is-small mt-4">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Hello, there!
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="level section">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="title">{{ containers.length }}</p>
|
||||
<p class="heading">Total Containers</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="title">{{ runningContainers.length }}</p>
|
||||
<p class="heading">Running</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="title">{{ version }}</p>
|
||||
<p class="heading">Dozzle Version</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="columns is-centered">
|
||||
<div class="column is-4">
|
||||
<div class="panel">
|
||||
<p class="panel-heading">
|
||||
Containers
|
||||
</p>
|
||||
<div class="panel-block">
|
||||
<p class="control has-icons-left">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Search Containers"
|
||||
v-model="search"
|
||||
@keyup.esc="search = null"
|
||||
@keyup.enter="onEnter()"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<icon name="search"></icon>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="panel-tabs">
|
||||
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">Running</a>
|
||||
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">All</a>
|
||||
</p>
|
||||
<router-link
|
||||
:to="{ name: 'container', params: { id: item.id, name: item.name } }"
|
||||
v-for="item in results.slice(0, 10)"
|
||||
:key="item.id"
|
||||
class="panel-block"
|
||||
>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
|
||||
<div class="subtitle is-7 status">
|
||||
<past-time :date="new Date(item.created * 1000)"></past-time>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
import Icon from "../components/Icon";
|
||||
import PastTime from "../components/PastTime";
|
||||
import config from "../store/config";
|
||||
|
||||
export default {
|
||||
props: [],
|
||||
name: "Default",
|
||||
name: "Index",
|
||||
components: { Icon, PastTime },
|
||||
data() {
|
||||
return {
|
||||
version: config.version,
|
||||
search: null,
|
||||
sort: "running",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onEnter() {
|
||||
if (this.results.length == 1) {
|
||||
const [item] = this.results;
|
||||
this.$router.push({ name: "container", params: { id: item.id, name: item.name } });
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["containers"]),
|
||||
mostRecentContainers() {
|
||||
return [...this.containers].sort((a, b) => b.created - a.created);
|
||||
},
|
||||
runningContainers() {
|
||||
return this.mostRecentContainers.filter((c) => c.state === "running");
|
||||
},
|
||||
allContainers() {
|
||||
return this.containers;
|
||||
},
|
||||
results() {
|
||||
if (this.search) {
|
||||
const term = this.search.toLowerCase();
|
||||
return this.allContainers.filter((c) => c.name.toLowerCase().includes(term));
|
||||
}
|
||||
switch (this.sort) {
|
||||
case "all":
|
||||
return this.mostRecentContainers;
|
||||
case "running":
|
||||
return this.runningContainers;
|
||||
|
||||
default:
|
||||
throw `Invalid sort order: ${this.sort}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.panel {
|
||||
border: 1px solid var(--border-color);
|
||||
.panel-block,
|
||||
.panel-tabs {
|
||||
border-color: var(--border-color);
|
||||
.is-active {
|
||||
border-color: var(--border-hover-color);
|
||||
}
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.status {
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 10px 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
18
assets/pages/PageNotFound.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="hero is-halfheight">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<h1 class="title">
|
||||
Oops,
|
||||
<small class="subtitle">this page doesn't exist</small>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "PageNotFound",
|
||||
};
|
||||
</script>
|
||||
@@ -52,30 +52,34 @@
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<h2 class="title is-6 is-marginless">Font size</h2>
|
||||
Modify the font size when viewing logs.
|
||||
|
||||
<b-dropdown v-model="size" aria-role="list" style="margin: -8px 10px 0;">
|
||||
<button class="button is-primary" type="button" slot="trigger">
|
||||
<span class="is-capitalized">{{ size }}</span>
|
||||
<span class="icon"><icon name="chevron-down"></icon></span>
|
||||
</button>
|
||||
<b-dropdown-item
|
||||
:value="value"
|
||||
aria-role="listitem"
|
||||
v-for="value in ['small', 'medium', 'large']"
|
||||
:key="value"
|
||||
>
|
||||
<div class="media">
|
||||
<span class="icon keep-size">
|
||||
<icon name="check" v-if="value == size"></icon>
|
||||
</span>
|
||||
<div class="media-content">
|
||||
<h3 class="is-capitalized">{{ value }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
<div class="columns is-vcentered is-mobile is-variable is-2">
|
||||
<div class="column is-narrow">
|
||||
<b-dropdown v-model="size" aria-role="list">
|
||||
<button class="button is-primary" type="button" slot="trigger">
|
||||
<span class="is-capitalized">{{ size }}</span>
|
||||
<span class="icon"><icon name="chevron-down"></icon></span>
|
||||
</button>
|
||||
<b-dropdown-item
|
||||
:value="value"
|
||||
aria-role="listitem"
|
||||
v-for="value in ['small', 'medium', 'large']"
|
||||
:key="value"
|
||||
>
|
||||
<div class="media">
|
||||
<span class="icon keep-size">
|
||||
<icon name="check" v-if="value == size"></icon>
|
||||
</span>
|
||||
<div class="media-content">
|
||||
<h3 class="is-capitalized">{{ value }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
<div class="column">
|
||||
Font size to use for logs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -142,8 +146,6 @@ export default {
|
||||
|
||||
a.next-release {
|
||||
text-decoration: underline;
|
||||
color: #00d1b2;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -154,7 +156,7 @@ a.next-release {
|
||||
}
|
||||
|
||||
.has-underline {
|
||||
border-bottom: 1px solid var(--title-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1em 0px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Index /> renders correctly 1`] = `
|
||||
<div
|
||||
class="hero is-fullheight"
|
||||
>
|
||||
<div
|
||||
class="hero-body"
|
||||
>
|
||||
<div
|
||||
class="container has-text-centered"
|
||||
>
|
||||
<h1
|
||||
class="title"
|
||||
>
|
||||
Please choose a container from the list to view the logs
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
6
assets/pages/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as Index } from "./Index.vue";
|
||||
export { default as ContainerNotFound } from "./ContainerNotFound.vue";
|
||||
export { default as Show } from "./Show.vue";
|
||||
export { default as Container } from "./Container.vue";
|
||||
export { default as Settings } from "./Settings.vue";
|
||||
export { default as PageNotFound } from "./PageNotFound.vue";
|
||||
@@ -1,2 +1,6 @@
|
||||
const config = JSON.parse(document.querySelector("script#config__json").textContent);
|
||||
if (config.version == "{{ .Version }}") {
|
||||
config.version = "dev";
|
||||
config.base = "";
|
||||
}
|
||||
export default config;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
$body-family: "Roboto", sans-serif;
|
||||
$body-background-color: var(--body-background-color);
|
||||
$body-color: var(--body-color);
|
||||
|
||||
$scheme-main: var(--scheme-main);
|
||||
$scheme-main-bis: var(--scheme-main-bis);
|
||||
@@ -15,8 +14,16 @@ $border-hover: var(--border-hover-color);
|
||||
$menu-item-active-background-color: var(--menu-item-active-background-color);
|
||||
$menu-item-color: var(--menu-item-color);
|
||||
$menu-item-hover-background-color: var(--menu-item-hover-background-color);
|
||||
$menu-item-hover-color: var(--menu-item-hover-color);
|
||||
|
||||
$title-color: var(--title-color);
|
||||
$text-strong: var(--text-strong-color);
|
||||
$text: var(--text-color);
|
||||
|
||||
$panel-heading-background-color: var(--panel-heading-background-color);
|
||||
$panel-heading-color: var(--panel-heading-color);
|
||||
|
||||
$link: $turquoise;
|
||||
$link-active: $grey-dark;
|
||||
|
||||
@import "~bulma";
|
||||
@import "../node_modules/splitpanes/dist/splitpanes.css";
|
||||
@@ -37,13 +44,17 @@ html {
|
||||
--secondary-color: #{$yellow};
|
||||
|
||||
--body-background-color: #{$black-bis};
|
||||
--body-color: #{$grey-lighter};
|
||||
|
||||
--menu-item-active-background-color: var(--primary-color);
|
||||
--menu-item-color: hsl(0, 6%, 87%);
|
||||
--menu-item-hover-background-color: #{$white-ter};
|
||||
--menu-item-hover-color: #{$black-ter};
|
||||
|
||||
--title-color: #{$grey-lightest};
|
||||
--panel-heading-background-color: var(--secondary-color);
|
||||
--panel-heading-color: var(--scheme-main-bis);
|
||||
|
||||
--text-strong-color: #{$grey-lightest};
|
||||
--text-color: #{$grey-lighter};
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
@@ -63,8 +74,13 @@ html {
|
||||
|
||||
--menu-item-color: #{$grey-dark};
|
||||
--menu-item-hover-background-color: #eee8e7;
|
||||
--menu-item-hover-color: #{black-ter};
|
||||
|
||||
--title-color: #{$grey-dark};
|
||||
--panel-heading-background-color: var(--secondary-color);
|
||||
--panel-heading-color: var(--text-strong-color);
|
||||
|
||||
--text-strong-color: #{$grey-dark};
|
||||
--text-color: #{$grey-darker};
|
||||
}
|
||||
|
||||
html {
|
||||
|
||||
@@ -95,9 +95,8 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers []Container
|
||||
var containers = make([]Container, 0, len(list))
|
||||
for _, c := range list {
|
||||
|
||||
container := Container{
|
||||
ID: c.ID[:12],
|
||||
Names: c.Names,
|
||||
@@ -116,10 +115,6 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
return strings.ToLower(containers[i].Name) < strings.ToLower(containers[j].Name)
|
||||
})
|
||||
|
||||
if containers == nil {
|
||||
containers = []Container{}
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
FROM amir20/docker-alpine-puppeteer:edge
|
||||
FROM amir20/docker-alpine-puppeteer:v1
|
||||
|
||||
COPY --chown=pptruser:pptruser package*.json yarn.lock /app/
|
||||
RUN yarn
|
||||
COPY package*.json yarn.lock /app/
|
||||
RUN yarn
|
||||
|
||||
COPY --chown=pptruser:pptruser . /app/
|
||||
COPY . /app/
|
||||
|
||||
CMD [ "yarn", "test"]
|
||||
CMD ["yarn", "test"]
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 72 KiB |
22
integration/__tests__/custom_base.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { removeTimes } = require("../utils");
|
||||
const { CUSTOM_URL: URL } = process.env;
|
||||
|
||||
describe("Dozzle with custom base", () => {
|
||||
beforeEach(async () => {
|
||||
await page.goto(URL, { waitUntil: "networkidle2" });
|
||||
});
|
||||
|
||||
it("renders full page on desktop", async () => {
|
||||
await removeTimes(page);
|
||||
const image = await page.screenshot({ fullPage: true });
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
});
|
||||
|
||||
it("and shows one container with correct title", async () => {
|
||||
await removeTimes(page);
|
||||
const menuTitle = await page.$eval("aside ul.menu-list li a", (e) => e.title);
|
||||
|
||||
expect(menuTitle).toEqual("custom_base");
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,17 @@
|
||||
const puppeteer = require("puppeteer");
|
||||
const { removeTimes } = require("../utils");
|
||||
const iPhoneX = puppeteer.devices["iPhone X"];
|
||||
const iPadLandscape = puppeteer.devices["iPad landscape"];
|
||||
|
||||
const { BASE } = process.env;
|
||||
const { DEFAULT_URL: URL } = process.env;
|
||||
|
||||
describe("home page", () => {
|
||||
beforeEach(async () => {
|
||||
await page.goto(BASE, { waitUntil: "networkidle2" });
|
||||
await page.goto(URL, { waitUntil: "networkidle2" });
|
||||
});
|
||||
|
||||
it("renders full page on desktop", async () => {
|
||||
await removeTimes(page);
|
||||
const image = await page.screenshot({ fullPage: true });
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
@@ -17,6 +19,7 @@ describe("home page", () => {
|
||||
|
||||
it("renders ipad viewport", async () => {
|
||||
await page.emulate(iPadLandscape);
|
||||
await removeTimes(page);
|
||||
const image = await page.screenshot();
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
@@ -24,6 +27,7 @@ describe("home page", () => {
|
||||
|
||||
it("renders iphone viewport", async () => {
|
||||
await page.emulate(iPhoneX);
|
||||
await removeTimes(page);
|
||||
const image = await page.screenshot();
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
@@ -43,7 +47,7 @@ describe("home page", () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.goto(BASE, { waitUntil: "networkidle2" });
|
||||
await page.goto(URL, { waitUntil: "networkidle2" });
|
||||
});
|
||||
|
||||
it("and shows one container with correct title", async () => {
|
||||
41
integration/__tests__/light_mode.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const puppeteer = require("puppeteer");
|
||||
const { removeTimes } = require("../utils");
|
||||
const iPhoneX = puppeteer.devices["iPhone X"];
|
||||
const iPadLandscape = puppeteer.devices["iPad landscape"];
|
||||
|
||||
const { DEFAULT_URL: URL } = process.env;
|
||||
|
||||
describe("Dozzle with light mode", () => {
|
||||
beforeAll(async () => {
|
||||
await page.goto(URL + "/settings", { waitUntil: "networkidle2" });
|
||||
await page.$$eval("label.switch", (elements) => {
|
||||
elements.filter((e) => e.textContent.trim() === "Use light theme")[0].click();
|
||||
});
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await page.goto(URL, { waitUntil: "networkidle2" });
|
||||
});
|
||||
|
||||
it("renders full page on desktop", async () => {
|
||||
await removeTimes(page);
|
||||
const image = await page.screenshot({ fullPage: true });
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
});
|
||||
|
||||
it("renders ipad viewport", async () => {
|
||||
await page.emulate(iPadLandscape);
|
||||
await removeTimes(page);
|
||||
const image = await page.screenshot();
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
});
|
||||
|
||||
it("renders iphone viewport", async () => {
|
||||
await page.emulate(iPhoneX);
|
||||
await removeTimes(page);
|
||||
const image = await page.screenshot();
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,14 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
custom_base:
|
||||
container_name: custom_base
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- DOZZLE_FILTER=name=custom_base
|
||||
- DOZZLE_BASE=/foobarbase
|
||||
build:
|
||||
context: ..
|
||||
dozzle:
|
||||
container_name: dozzle
|
||||
volumes:
|
||||
@@ -12,7 +21,11 @@ services:
|
||||
build:
|
||||
context: .
|
||||
command: yarn test
|
||||
volumes:
|
||||
- ./__tests__:/app/__tests__
|
||||
environment:
|
||||
- BASE=http://dozzle:8080/
|
||||
- DEFAULT_URL=http://dozzle:8080/
|
||||
- CUSTOM_URL=http://custom_base:8080/foobarbase
|
||||
depends_on:
|
||||
- dozzle
|
||||
- custom_base
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"jest": "^26.0.1",
|
||||
"jest-image-snapshot": "^4.0.0",
|
||||
"puppeteer": "^4.0.0"
|
||||
"puppeteer": "^5.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-puppeteer",
|
||||
|
||||
8
integration/utils.js
Normal file
@@ -0,0 +1,8 @@
|
||||
async function removeTimes(page) {
|
||||
await page.waitForSelector("time");
|
||||
await page.evaluate(() => {
|
||||
(document.querySelectorAll("time") || []).forEach((el) => el.remove());
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { removeTimes };
|
||||
@@ -1281,6 +1281,11 @@ detect-newline@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
||||
|
||||
devtools-protocol@0.0.767361:
|
||||
version "0.0.767361"
|
||||
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.767361.tgz#5977f2558b84f9df36f62501bdddb82f3ae7b66b"
|
||||
integrity sha512-ziRTdhEVQ9jEwedaUaXZ7kl9w9TF/7A3SXQ0XuqrJB+hMS62POHZUWTbumDN2ehRTfvWqTPc2Jw4gUl/jggmHA==
|
||||
|
||||
diff-sequences@^26.0.0:
|
||||
version "26.0.0"
|
||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.0.0.tgz#0760059a5c287637b842bd7085311db7060e88a6"
|
||||
@@ -2663,9 +2668,9 @@ lodash.sortby@^4.7.0:
|
||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
|
||||
lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
version "4.17.19"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
|
||||
make-dir@^3.0.0:
|
||||
version "3.0.2"
|
||||
@@ -2768,11 +2773,6 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
mitt@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.0.1.tgz#9e8a075b4daae82dd91aac155a0ece40ca7cb393"
|
||||
integrity sha512-FhuJY+tYHLnPcBHQhbUFzscD5512HumCPE4URXZUgPi3IvOJi4Xva5IIgy3xX56GqCmw++MAm5UURG6kDBYTdg==
|
||||
|
||||
mixin-deep@^1.2.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
|
||||
@@ -3136,16 +3136,17 @@ punycode@^2.1.0, punycode@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
puppeteer@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-4.0.1.tgz#ebc2ee61157ed1aa25be3843fda97807df1d51f5"
|
||||
integrity sha512-LIiSWTRqpTnnm3R2yAoMBx1inSeKwVZy66RFSkgSTDINzheJZPd5z5mMbPM0FkvwWAZ27a+69j5nZf+Fpyhn3Q==
|
||||
puppeteer@^5.0.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.2.0.tgz#c37bf605e6ec103428c872d820f30f2617bf38ad"
|
||||
integrity sha512-Hru70mFT+dts5W3l1MVg46EfJiWE63qjmXlDvC2kkCeEzLgt6KrwEkDJcJKKzERTvy9xXhOvjyGNx36fd78mVQ==
|
||||
dependencies:
|
||||
debug "^4.1.0"
|
||||
devtools-protocol "0.0.767361"
|
||||
extract-zip "^2.0.0"
|
||||
https-proxy-agent "^4.0.0"
|
||||
mime "^2.0.3"
|
||||
mitt "^2.0.1"
|
||||
pkg-dir "^4.2.0"
|
||||
progress "^2.0.1"
|
||||
proxy-from-env "^1.0.0"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
27
package.json
@@ -1,18 +1,20 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.4",
|
||||
"description": "Realtime log viewer for docker containers. ",
|
||||
"scripts": {
|
||||
"prestart": "yarn clean",
|
||||
"start": "npm-run-all -p watch:*",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:assets": "webpack --mode=development --watch",
|
||||
"watch:server": "reflex -c .reflex",
|
||||
"dev": "npm-run-all -p dev-server watch:server",
|
||||
"dev-server": "webpack-dev-server --open",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "yarn webpack --mode=production",
|
||||
"clean": "rm -rf static/ a_main-packr.go",
|
||||
"release": "release-it",
|
||||
"test": "TZ=UTC jest",
|
||||
"integration": "docker-compose -f integration/docker-compose.test.yml up --build --force-recreate integration"
|
||||
"integration": "docker-compose -f integration/docker-compose.test.yml up --build --force-recreate --exit-code-from integration"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,7 +30,7 @@
|
||||
"ansi-to-html": "^0.6.14",
|
||||
"buefy": "^0.8.20",
|
||||
"bulma": "^0.9.0",
|
||||
"date-fns": "^2.14.0",
|
||||
"date-fns": "^2.15.0",
|
||||
"dompurify": "^2.0.12",
|
||||
"hotkeys-js": "^3.8.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
@@ -39,17 +41,17 @@
|
||||
"vue": "^2.6.11",
|
||||
"vue-meta": "^2.4.0",
|
||||
"vue-router": "^3.3.4",
|
||||
"vuex": "^3.4.0"
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.3",
|
||||
"@babel/plugin-transform-runtime": "^7.10.3",
|
||||
"@babel/core": "^7.10.5",
|
||||
"@babel/plugin-transform-runtime": "^7.10.5",
|
||||
"@vue/component-compiler-utils": "^3.1.2",
|
||||
"@vue/test-utils": "^1.0.3",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^26.1.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"caniuse-lite": "^1.0.30001090",
|
||||
"caniuse-lite": "^1.0.30001104",
|
||||
"css-loader": "^3.6.0",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
@@ -63,16 +65,17 @@
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"prettier": "^2.0.5",
|
||||
"release-it": "^13.6.4",
|
||||
"sass": "^1.26.9",
|
||||
"sass-loader": "^8.0.2",
|
||||
"release-it": "^13.6.5",
|
||||
"sass": "^1.26.10",
|
||||
"sass-loader": "^9.0.2",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-jest": "^3.0.6",
|
||||
"vue-loader": "^15.9.3",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-pwa-manifest": "^4.2.0"
|
||||
},
|
||||
"husky": {
|
||||
|
||||
@@ -126,6 +126,7 @@ Loop:
|
||||
select {
|
||||
case message, ok := <-messages:
|
||||
if !ok {
|
||||
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
|
||||
break Loop
|
||||
}
|
||||
fmt.Fprintf(w, "data: %s\n", message)
|
||||
|
||||
@@ -10,12 +10,12 @@ module.exports = (env, argv) => ({
|
||||
maxAssetSize: 350000,
|
||||
maxEntrypointSize: 600000,
|
||||
},
|
||||
devtool: argv.mode === "development" ? "inline-cheap-source-map" : false,
|
||||
devtool: argv.mode !== "production" ? "inline-cheap-source-map" : false,
|
||||
entry: ["./assets/main.js", "./assets/styles.scss"],
|
||||
output: {
|
||||
path: path.resolve(__dirname, "./static"),
|
||||
filename: "[name].js",
|
||||
publicPath: "{{ .Base }}",
|
||||
publicPath: process.env.WEBPACK_DEV_SERVER ? "/" : "{{ .Base }}",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@@ -26,19 +26,18 @@ module.exports = (env, argv) => ({
|
||||
{
|
||||
test: /\.(sass|scss|css)$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: "css-loader",
|
||||
query: {
|
||||
importLoaders: 1,
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
hmr: argv.mode !== "production",
|
||||
},
|
||||
},
|
||||
"css-loader",
|
||||
{
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
ident: "postcss",
|
||||
plugins: (loader) => [
|
||||
require("postcss-import")(),
|
||||
require("postcss-cssnext")({
|
||||
features: {
|
||||
customProperties: { warnings: false },
|
||||
@@ -75,4 +74,15 @@ module.exports = (env, argv) => ({
|
||||
},
|
||||
extensions: ["*", ".js", ".vue", ".json"],
|
||||
},
|
||||
devServer: {
|
||||
port: 8081,
|
||||
inline: true,
|
||||
hot: true,
|
||||
historyApiFallback: true,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||