Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17a9a6777a | ||
|
|
0752bb9a10 | ||
|
|
0360950eec | ||
|
|
99f7e0d980 | ||
|
|
b04e950358 | ||
|
|
328bc80c14 | ||
|
|
203e6ab780 | ||
|
|
2f467ff83e | ||
|
|
e9234a827c | ||
|
|
b773e61c69 | ||
|
|
898b5379da | ||
|
|
68833e4786 | ||
|
|
6a26c4ecf0 | ||
|
|
6118cda6de | ||
|
|
d5bac298ec | ||
|
|
2286bc65a7 | ||
|
|
254faa7145 | ||
|
|
a927ba66a5 | ||
|
|
6c9b6b7768 | ||
|
|
77abe748f0 | ||
|
|
6e17e178da | ||
|
|
55f4cff699 | ||
|
|
6ed4dcc86a | ||
|
|
fad25076f7 | ||
|
|
6c796ecd2c | ||
|
|
df9c0f6863 | ||
|
|
6be5d7e42d | ||
|
|
69dd53d669 | ||
|
|
3673ea9b72 | ||
|
|
0a4f53b7c9 | ||
|
|
f5a84c249a | ||
|
|
63a9f24193 | ||
|
|
fa37625f36 | ||
|
|
a73fda2a29 | ||
|
|
75c80bdc96 | ||
|
|
2414a65371 | ||
|
|
83ca9b0a89 | ||
|
|
6bceaee801 | ||
|
|
70dfa399e4 | ||
|
|
8e89aa7a28 | ||
|
|
9a1938c071 | ||
|
|
7e150daac7 | ||
|
|
3386dd3af9 | ||
|
|
293a03153c | ||
|
|
092fa34e1a | ||
|
|
20d42bf83c | ||
|
|
a7ee8c6061 | ||
|
|
1afcedc3e3 | ||
|
|
8e49c76039 | ||
|
|
c9599676c9 | ||
|
|
d547a4ca81 | ||
|
|
f2764fad0c | ||
|
|
cf381dfc1a | ||
|
|
0806405099 | ||
|
|
d4f2b50a5e | ||
|
|
584166f80d | ||
|
|
23f5d5e4e7 | ||
|
|
57d0c70ad7 | ||
|
|
d3e02e24c5 | ||
|
|
9fd2f387c5 | ||
|
|
2f71ef6063 | ||
|
|
1a7416d5c7 | ||
|
|
eec14bbe18 | ||
|
|
9a9997d5e0 | ||
|
|
2c041bc351 | ||
|
|
8f98c28b96 | ||
|
|
da3071cfa0 | ||
|
|
5484947ee3 | ||
|
|
c0f31b2ac5 | ||
|
|
d39c0e65aa | ||
|
|
d090da9dcf | ||
|
|
2ea63a2609 | ||
|
|
5766f7d1c5 | ||
|
|
0b762f6b88 | ||
|
|
51e708742d | ||
|
|
99d74aefa4 | ||
|
|
0105f6d1a4 | ||
|
|
9168739140 | ||
|
|
429c82c372 | ||
|
|
4f65f43267 | ||
|
|
bae86f4956 | ||
|
|
e0f550374f | ||
|
|
2dc9b9d921 | ||
|
|
bb11e88e26 | ||
|
|
63392b9aab | ||
|
|
6fe24d3bbf | ||
|
|
340f4d9baa | ||
|
|
aa37ca02b5 | ||
|
|
34b3b7e1e3 | ||
|
|
11067205e9 | ||
|
|
753e909411 | ||
|
|
14d2ae53bc | ||
|
|
93ab440dd9 | ||
|
|
612cf9408f | ||
|
|
da6287d060 | ||
|
|
b84b5a2d8d | ||
|
|
b219c984c4 | ||
|
|
b11dc46d7b | ||
|
|
bff0f0a5bb | ||
|
|
61637599d8 |
5
.babelrc
5
.babelrc
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"presets": [["@babel/preset-env", { "modules": false }]],
|
||||
"plugins": [["@babel/plugin-transform-runtime", { "regenerator": true }]],
|
||||
"presets": [["env", { "modules": false }]],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
|
||||
"presets": [["env", { "targets": { "node": "current" } }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
.github/workflows/deploy.yml
vendored
5
.github/workflows/deploy.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build images
|
||||
run: docker-compose -f integration/docker-compose.test.yml build
|
||||
run: docker-compose -f integration/docker-compose.test.yml build
|
||||
- name: Run tests
|
||||
run: docker-compose -f integration/docker-compose.test.yml run integration
|
||||
buildx:
|
||||
@@ -50,7 +50,8 @@ jobs:
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
buildx-version: latest
|
||||
qemu-version: latest
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Docker Login
|
||||
|
||||
19
.github/workflows/publish-dev-dockerimage.yaml
vendored
19
.github/workflows/publish-dev-dockerimage.yaml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Docker image (latest-dev)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: jerray/publish-docker-action@master
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
file: Dockerfile
|
||||
repository: amir20/dozzle
|
||||
tags: latest-dev
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
minifySvg: false,
|
||||
};
|
||||
@@ -10,16 +10,14 @@ COPY package*.json yarn.lock ./
|
||||
RUN yarn install --network-timeout 1000000
|
||||
|
||||
# Copy config files
|
||||
COPY .* ./
|
||||
COPY .* webpack*.js ./
|
||||
|
||||
# Copy assets to build
|
||||
COPY assets ./assets
|
||||
|
||||
|
||||
# Do the build
|
||||
RUN yarn build
|
||||
|
||||
|
||||
FROM golang:1.14-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
|
||||
49
README.md
49
README.md
@@ -6,13 +6,13 @@
|
||||
|
||||
# Dozzle - [dozzle.dev](https://dozzle.dev/)
|
||||
|
||||
Dozzle is a real-time log viewer for Docker. It's free. It's small. And it's in your browser.
|
||||
Dozzle is a simple, lightweight application that provides you with a web based interface to monitor your Docker container logs live. It doesn’t store log information, it is for live monitoring of your container logs only.
|
||||
|
||||
While dozzle should work for most, it is not meant to be a full logging solution. For enterprise use, I recommend you look at [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana).
|
||||
While dozzle should work for most, it is not meant to be a full logging solution. For enterprise applications, products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited.
|
||||
|
||||
But if you don't want to pay for these services, then Dozzle can help! Dozzle will be able to capture all logs from your containers and send them in real-time to your browser. Installation is also very easy. Dozzle is not a database. It does not store or save any logs. You can only see live logs while using Dozzle.
|
||||
Dozzle doesn't cost any money. Dozzle aims to stay simple, small and free.
|
||||
|
||||

|
||||

|
||||
|
||||
## Getting dozzle
|
||||
|
||||
@@ -86,6 +86,46 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
|
||||
| `--tailSize` | `DOZZLE_TAILSIZE` | `300` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Nginx Config
|
||||
|
||||
If you are using nginx as a reverse proxy, then you need to configure `/api` to enable server-sent events.
|
||||
|
||||
Below is an example configuration using SSL and `proxy_pass` with correct settings.
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name <example.com>;
|
||||
return 301 https://<example.com>$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name <example.com>;
|
||||
|
||||
ssl_certificate </path/to/your/certificate>;
|
||||
ssl_certificate_key </path/to/your/key>;
|
||||
|
||||
location / {
|
||||
proxy_pass http://<dozzle.container.ip.address>:8080;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://<dozzle.container.ip.address>:8080;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
chunked_transfer_encoding off;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -101,4 +141,3 @@ To Build and test locally:
|
||||
5. Install node modules with `npm install`.
|
||||
6. Do `npm start`
|
||||
|
||||
Instructions for Github actions can be found [here](.github/goreleaser/Dockerfile) which build and tests Dozzle.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* snapshot: Test_createRoutes_foobar */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self'; require-trusted-types-for 'script'
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
foo page
|
||||
@@ -8,6 +9,7 @@ foo page
|
||||
/* snapshot: Test_createRoutes_index */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self'; require-trusted-types-for 'script'
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
index page
|
||||
@@ -15,6 +17,7 @@ index page
|
||||
/* snapshot: Test_createRoutes_redirect */
|
||||
HTTP/1.1 301 Moved Permanently
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self'; require-trusted-types-for 'script'
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Location: /foobar/
|
||||
|
||||
@@ -23,6 +26,7 @@ Location: /foobar/
|
||||
/* snapshot: Test_createRoutes_version */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self'; require-trusted-types-for 'script'
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
dev
|
||||
@@ -39,14 +43,16 @@ HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
/* snapshot: Test_handler_streamEvents_error_request */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
/* snapshot: Test_handler_streamEvents_happy */
|
||||
HTTP/1.1 200 OK
|
||||
@@ -54,6 +60,7 @@ Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
event: containers-changed
|
||||
data: start
|
||||
@@ -71,7 +78,8 @@ HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy */
|
||||
HTTP/1.1 200 OK
|
||||
@@ -79,5 +87,6 @@ Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
data: INFO Testing logs...
|
||||
@@ -3,6 +3,8 @@ import { shallowMount, RouterLinkStub, createLocalVue } from "@vue/test-utils";
|
||||
import Vuex from "vuex";
|
||||
import App from "./App";
|
||||
|
||||
jest.mock("./store/config.js", () => ({ base: "" }));
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
localVue.use(Vuex);
|
||||
@@ -12,7 +14,6 @@ describe("<App />", () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
global.BASE_PATH = "";
|
||||
global.EventSource = EventSource;
|
||||
const state = {
|
||||
containers: [
|
||||
@@ -32,11 +33,6 @@ describe("<App />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("is a Vue instance", async () => {
|
||||
const wrapper = shallowMount(App, { stubs, store, localVue });
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("has right title", async () => {
|
||||
const wrapper = shallowMount(App, { stubs, store, localVue });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
@@ -116,8 +116,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
::v-deep .splitpanes__splitter {
|
||||
min-width: 4px;
|
||||
::v-deep .splitpanes--vertical > .splitpanes__splitter {
|
||||
min-width: 3px;
|
||||
background: #666;
|
||||
&:hover {
|
||||
background: rgb(255, 221, 87);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<svg class="icomoon" :class="['icon-' + name]">
|
||||
<use :href="'#icon-' + name"></use>
|
||||
<template functional>
|
||||
<svg class="icomoon" :class="['icon-' + props.name]">
|
||||
<use :href="'#icon-' + props.name"></use>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
functional: true,
|
||||
props: {
|
||||
name: {
|
||||
required: true,
|
||||
|
||||
@@ -13,9 +13,10 @@ jest.mock("lodash.debounce", () =>
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock("../store/config.js", () => ({ base: "" }));
|
||||
|
||||
describe("<LogEventSource />", () => {
|
||||
beforeEach(() => {
|
||||
global.BASE_PATH = "";
|
||||
global.EventSource = EventSource;
|
||||
MockDate.set("6/12/2019", 0);
|
||||
window.scrollTo = jest.fn();
|
||||
@@ -55,24 +56,9 @@ describe("<LogEventSource />", () => {
|
||||
});
|
||||
}
|
||||
|
||||
test("is a Vue instance", async () => {
|
||||
const wrapper = shallowMount(LogEventSource);
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("renders correctly", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
expect(wrapper.element).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<div
|
||||
class="control"
|
||||
/>
|
||||
|
||||
<ul
|
||||
class="events medium"
|
||||
/>
|
||||
</div>
|
||||
`);
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should connect to EventSource", async () => {
|
||||
@@ -126,7 +112,7 @@ describe("<LogEventSource />", () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
|
||||
const [message, _] = wrapper.find(LogViewer).vm.messages;
|
||||
const [message, _] = wrapper.findComponent(LogViewer).vm.messages;
|
||||
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<script>
|
||||
import debounce from "lodash.debounce";
|
||||
import InfiniteLoader from "./InfiniteLoader";
|
||||
import config from "../store/config";
|
||||
|
||||
export default {
|
||||
props: ["id"],
|
||||
@@ -30,9 +31,10 @@ export default {
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
this.messages = [];
|
||||
this.buffer = [];
|
||||
this.es = null;
|
||||
}
|
||||
this.es = new EventSource(`${BASE_PATH}/api/logs/stream?id=${this.id}`);
|
||||
this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}`);
|
||||
const flushBuffer = debounce(
|
||||
() => {
|
||||
this.messages.push(...this.buffer);
|
||||
|
||||
13
assets/components/__snapshots__/LogEventSource.spec.js.snap
Normal file
13
assets/components/__snapshots__/LogEventSource.spec.js.snap
Normal file
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LogEventSource /> renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="control"
|
||||
/>
|
||||
|
||||
<ul
|
||||
class="events medium"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
5
assets/favicon.svg
Normal file
5
assets/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" rx="6" fill="#222222"/>
|
||||
<path d="M82.3248 94.3863H123V104.093H67.8025V95.3506L106.164 44.3736H68.3808V34.5382H121.072V42.9594L82.3248 94.3863Z" fill="#FFDD57"/>
|
||||
<path d="M8 107.107L17.5656 14L43.8372 16.7013C51.9339 17.5338 58.9091 20.0604 64.7629 24.2812C70.6166 28.5019 74.8873 34.0893 77.5749 41.0432C80.3052 48.0016 81.2514 55.7674 80.4137 64.3407L79.8027 70.2877C78.9005 79.0698 76.4053 86.5894 72.3173 92.8468C68.2719 99.1084 62.914 103.684 56.2436 106.574C49.6158 109.468 42.1213 110.529 33.7602 109.755L8 107.107ZM28.8005 25.3655L21.3043 98.3288L34.2164 99.6565C43.6767 100.629 51.3299 98.4435 57.1758 93.0993C63.0644 87.7595 66.5671 79.6542 67.684 68.7832L68.2424 63.3477C69.3286 52.7752 67.6788 44.3123 63.293 37.9592C58.9542 31.5678 52.2295 27.8607 43.1188 26.8377L28.8005 25.3655Z" fill="#FFDD57"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 949 B |
@@ -5,19 +5,17 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Dozzle</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono|Gafata" rel="stylesheet" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link href="styles.scss" rel="stylesheet" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<script>
|
||||
window["BASE_PATH"] = "{{ .Base }}";
|
||||
window["VERSION"] = "{{ .Version }}";
|
||||
<script type="application/json" id="config__json">
|
||||
{
|
||||
"base": "{{ .Base }}",
|
||||
"version": "{{ .Version }}"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
style="position: absolute; width: 0; height: 0; overflow: hidden;"
|
||||
class="is-hidden"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
@@ -69,6 +67,5 @@
|
||||
</defs>
|
||||
</svg>
|
||||
<div id="app"></div>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +1,10 @@
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import Meta from "vue-meta";
|
||||
import { Dropdown, Switch } from "buefy";
|
||||
import Dropdown from "buefy/dist/esm/dropdown";
|
||||
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";
|
||||
@@ -34,7 +36,7 @@ const routes = [
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
base: BASE_PATH + "/",
|
||||
base: config.base + "/",
|
||||
routes,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "Dozzle Log Viewer",
|
||||
"short_name": "Dozzle",
|
||||
"theme_color": "#111111",
|
||||
"background_color": "#111111",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"start_url": "/"
|
||||
}
|
||||
@@ -2,11 +2,6 @@ import { shallowMount } from "@vue/test-utils";
|
||||
import Index from "./Index";
|
||||
|
||||
describe("<Index />", () => {
|
||||
test("is a Vue instance", () => {
|
||||
const wrapper = shallowMount(Index);
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("renders correctly", () => {
|
||||
const wrapper = shallowMount(Index);
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
>.
|
||||
<span v-if="hasUpdate">
|
||||
New version is available! Update to
|
||||
<a :href="nextRelease.html_url" class="next-release">{{ nextRelease.name }}</a
|
||||
<a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noopener">{{ nextRelease.name }}</a
|
||||
>.
|
||||
</span>
|
||||
</div>
|
||||
@@ -46,7 +46,12 @@
|
||||
<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']">
|
||||
<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>
|
||||
@@ -67,6 +72,7 @@ import gt from "semver/functions/gt";
|
||||
import valid from "semver/functions/valid";
|
||||
import { mapActions, mapState } from "vuex";
|
||||
import Icon from "../components/Icon";
|
||||
import config from "../store/config";
|
||||
|
||||
export default {
|
||||
props: [],
|
||||
@@ -76,7 +82,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentVersion: VERSION,
|
||||
currentVersion: config.version,
|
||||
nextRelease: null,
|
||||
hasUpdate: false,
|
||||
};
|
||||
@@ -112,7 +118,7 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
2
assets/store/config.js
Normal file
2
assets/store/config.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const config = JSON.parse(document.querySelector("script#config__json").textContent);
|
||||
export default config;
|
||||
@@ -2,6 +2,7 @@ import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import storage from "store/dist/store.modern";
|
||||
import { DEFAULT_SETTINGS, DOZZLE_SETTINGS_KEY } from "./settings";
|
||||
import config from "./config";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
@@ -50,7 +51,7 @@ const actions = {
|
||||
commit("SET_SEARCH", filter);
|
||||
},
|
||||
async FETCH_CONTAINERS({ commit }) {
|
||||
const containers = await (await fetch(`${BASE_PATH}/api/containers.json`)).json();
|
||||
const containers = await (await fetch(`${config.base}/api/containers.json`)).json();
|
||||
commit("SET_CONTAINERS", containers);
|
||||
},
|
||||
UPDATE_SETTING({ commit }, setting) {
|
||||
@@ -72,7 +73,7 @@ const getters = {
|
||||
},
|
||||
};
|
||||
|
||||
const es = new EventSource(`${BASE_PATH}/api/events/stream`);
|
||||
const es = new EventSource(`${config.base}/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));
|
||||
|
||||
|
||||
@@ -56,11 +56,9 @@ html.has-custom-scrollbars {
|
||||
}
|
||||
|
||||
.is-settings-control {
|
||||
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
&:hover {
|
||||
border-color: rgb(255, 221, 87) !important;
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
@@ -73,4 +71,3 @@ html.has-custom-scrollbars {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -34,7 +34,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.5.2 // indirect
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.5.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -131,6 +131,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -204,6 +206,8 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
|
||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM amir20/docker-alpine-puppeteer:edge
|
||||
|
||||
COPY --chown=pptruser:pptruser package*.json /app/
|
||||
COPY --chown=pptruser:pptruser package*.json yarn.lock /app/
|
||||
RUN yarn
|
||||
|
||||
COPY --chown=pptruser:pptruser . /app/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
@@ -7,11 +7,11 @@ services:
|
||||
environment:
|
||||
- DOZZLE_FILTER=name=dozzle
|
||||
build:
|
||||
context: ..
|
||||
context: ..
|
||||
integration:
|
||||
build:
|
||||
context: .
|
||||
command: npm test
|
||||
command: yarn test
|
||||
environment:
|
||||
- BASE=http://dozzle:8080/
|
||||
depends_on:
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"jest": "^25.2.6",
|
||||
"jest": "^26.0.1",
|
||||
"jest-image-snapshot": "^3.0.1",
|
||||
"puppeteer": "^2.1.1"
|
||||
"puppeteer": "^3.0.4"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-puppeteer",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
13
jest.config.js
Normal file
13
jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ["js", "json", "vue"],
|
||||
coveragePathIgnorePatterns: ["node_modules"],
|
||||
testPathIgnorePatterns: ["node_modules", "<rootDir>/integration/"],
|
||||
transformIgnorePatterns: ["node_modules"],
|
||||
watchPathIgnorePatterns: ["<rootDir>/node_modules/"],
|
||||
snapshotSerializers: ["jest-serializer-vue"],
|
||||
transform: {
|
||||
".*\\.vue$": "vue-jest",
|
||||
"^.+\\.js$": "babel-jest",
|
||||
},
|
||||
};
|
||||
18
main.go
18
main.go
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/gobuffalo/packr"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
@@ -75,23 +74,6 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func createRoutes(base string, h *handler) *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
if base != "/" {
|
||||
r.HandleFunc(base, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
|
||||
}))
|
||||
}
|
||||
s := r.PathPrefix(base).Subrouter()
|
||||
s.HandleFunc("/api/containers.json", h.listContainers)
|
||||
s.HandleFunc("/api/logs/stream", h.streamLogs)
|
||||
s.HandleFunc("/api/logs", h.fetchLogsBetweenDates)
|
||||
s.HandleFunc("/api/events/stream", h.streamEvents)
|
||||
s.HandleFunc("/version", h.version)
|
||||
s.PathPrefix("/").Handler(http.StripPrefix(base, http.HandlerFunc(h.index)))
|
||||
return r
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Infof("Dozzle version %s", version)
|
||||
dockerClient := docker.NewClientWithFilters(filters)
|
||||
|
||||
84
package.json
84
package.json
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "1.22.5",
|
||||
"version": "1.24.0",
|
||||
"description": "Realtime log viewer for docker containers. ",
|
||||
"scripts": {
|
||||
"prestart": "npm run clean",
|
||||
"start": "concurrently 'npm run watch-server' 'npm run watch-assets'",
|
||||
"watch-assets": "npx parcel watch --no-source-maps --public-url '__BASE__' assets/index.html -d static",
|
||||
"prestart": "yarn clean",
|
||||
"start": "concurrently 'yarn watch-server' 'yarn watch-assets'",
|
||||
"watch-assets": "webpack --mode=development --watch",
|
||||
"watch-server": "reflex -c .reflex",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npx parcel build --no-source-maps --public-url '__BASE__' assets/index.html -d static",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "yarn webpack --mode=production",
|
||||
"clean": "rm -rf static/ a_main-packr.go",
|
||||
"release": "release-it",
|
||||
"test": "jest",
|
||||
"integration": "docker-compose -f integration/docker-compose.test.yml run --rm integration"
|
||||
"integration": "docker-compose -f integration/docker-compose.test.yml up --build --force-recreate integration"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,8 +28,7 @@
|
||||
"ansi-to-html": "^0.6.14",
|
||||
"buefy": "^0.8.17",
|
||||
"bulma": "^0.8.2",
|
||||
"caniuse-lite": "^1.0.30001048",
|
||||
"date-fns": "^2.12.0",
|
||||
"date-fns": "^2.13.0",
|
||||
"hotkeys-js": "^3.7.6",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"semver": "^7.3.2",
|
||||
@@ -41,27 +40,42 @@
|
||||
"vuex": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/plugin-transform-runtime": "^7.9.0",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-transform-runtime": "^7.9.6",
|
||||
"@vue/component-compiler-utils": "^3.1.2",
|
||||
"@vue/test-utils": "^1.0.0-beta.33",
|
||||
"@vue/test-utils": "^1.0.2",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^25.4.0",
|
||||
"babel-jest": "^26.0.1",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"caniuse-lite": "^1.0.30001054",
|
||||
"concurrently": "^5.2.0",
|
||||
"css-loader": "^3.5.3",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^25.4.0",
|
||||
"jest": "^26.0.1",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"lint-staged": "^10.1.7",
|
||||
"lint-staged": "^10.2.2",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mockdate": "^2.0.5",
|
||||
"node-fetch": "^2.6.0",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"postcss-cssnext": "^3.1.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"prettier": "^2.0.5",
|
||||
"release-it": "^13.5.6",
|
||||
"release-it": "^13.5.8",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"vue-loader": "^15.9.2",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-manifest-plugin": "^2.2.0",
|
||||
"webpack-pwa-manifest": "^4.2.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@@ -73,40 +87,6 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
">5%"
|
||||
],
|
||||
"alias": {
|
||||
"vue": "./node_modules/vue/dist/vue.runtime.esm.js"
|
||||
},
|
||||
"jest": {
|
||||
"clearMocks": true,
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"vue"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"node_modules"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"node_modules",
|
||||
"<rootDir>/integration/"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules"
|
||||
],
|
||||
"watchPathIgnorePatterns": [
|
||||
"<rootDir>/node_modules/"
|
||||
],
|
||||
"snapshotSerializers": [
|
||||
"jest-serializer-vue"
|
||||
],
|
||||
"transform": {
|
||||
".*\\.vue$": "vue-jest",
|
||||
".+\\.js$": "babel-jest"
|
||||
}
|
||||
},
|
||||
"release-it": {
|
||||
"github": {
|
||||
"release": true
|
||||
|
||||
32
routes.go
32
routes.go
@@ -6,12 +6,37 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func createRoutes(base string, h *handler) *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
r.Use(setCSPHeaders)
|
||||
if base != "/" {
|
||||
r.HandleFunc(base, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
|
||||
}))
|
||||
}
|
||||
s := r.PathPrefix(base).Subrouter()
|
||||
s.HandleFunc("/api/containers.json", h.listContainers)
|
||||
s.HandleFunc("/api/logs/stream", h.streamLogs)
|
||||
s.HandleFunc("/api/logs", h.fetchLogsBetweenDates)
|
||||
s.HandleFunc("/api/events/stream", h.streamEvents)
|
||||
s.HandleFunc("/version", h.version)
|
||||
s.PathPrefix("/").Handler(http.StripPrefix(base, http.HandlerFunc(h.index)))
|
||||
return r
|
||||
}
|
||||
|
||||
func setCSPHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self'; require-trusted-types-for 'script'")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
fileServer := http.FileServer(h.box)
|
||||
if h.box.Has(req.URL.Path) && req.URL.Path != "" && req.URL.Path != "/" {
|
||||
@@ -21,7 +46,6 @@ func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
text = strings.Replace(text, "__BASE__", "{{ .Base }}", -1)
|
||||
tmpl, err := template.New("index.html").Parse(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -94,7 +118,7 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
log.Debugf("Starting to stream logs for %s", id)
|
||||
Loop:
|
||||
@@ -129,7 +153,7 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
ctx := r.Context()
|
||||
messages, err := h.client.Events(ctx)
|
||||
|
||||
78
webpack.config.js
Normal file
78
webpack.config.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const path = require("path");
|
||||
const { VueLoaderPlugin } = require("vue-loader");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const WebpackPwaManifest = require("webpack-pwa-manifest");
|
||||
|
||||
module.exports = (env, argv) => ({
|
||||
stats: { children: false, entrypoints: false, modules: false },
|
||||
performance: {
|
||||
maxAssetSize: 350000,
|
||||
maxEntrypointSize: 570000,
|
||||
},
|
||||
devtool: argv.mode === "development" ? "inline-cheap-source-map" : false,
|
||||
entry: ["./assets/main.js", "./assets/styles.scss"],
|
||||
output: {
|
||||
path: path.resolve(__dirname, "./static"),
|
||||
filename: "[name].js",
|
||||
publicPath: "{{ .Base }}",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(sass|scss|css)$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: "css-loader",
|
||||
query: {
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
ident: "postcss",
|
||||
plugins: (loader) => [
|
||||
require("postcss-import")(),
|
||||
require("postcss-cssnext")({
|
||||
features: {
|
||||
customProperties: { warnings: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
"sass-loader",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
hash: true,
|
||||
template: "assets/index.ejs",
|
||||
scriptLoading: "defer",
|
||||
favicon: "assets/favicon.svg",
|
||||
}),
|
||||
new WebpackPwaManifest({
|
||||
name: "Dozzle Log Viewer",
|
||||
short_name: "Dozzle",
|
||||
theme_color: "#222",
|
||||
background_color: "#222",
|
||||
display: "standalone",
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
vue$: "vue/dist/vue.runtime.esm.js",
|
||||
},
|
||||
extensions: ["*", ".js", ".vue", ".json"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user