mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 12:44:44 +00:00
Compare commits
332 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9aed4ba19f | |||
| 30a4d9a6be | |||
| 36b4bb65e7 | |||
| 8767d3461d | |||
| cd9f5971a4 | |||
| 3c7021af42 | |||
| 0b9b9f12da | |||
| 16eb71e0c0 | |||
| 17dfcf3d9a | |||
| 2dbbe47c5c | |||
| 6076c4eb63 | |||
| 818fec244a | |||
| 1f85f046cf | |||
| e6efe6ff59 | |||
| e96ab50486 | |||
| f74b1c91f2 | |||
| 0cbfcb6097 | |||
| 27d0244e5d | |||
| 7b19e83fea | |||
| 832594532a | |||
| 298b39f6b9 | |||
| e04fbbb51a | |||
| 685b17ef4e | |||
| 582aa207f1 | |||
| 8116868669 | |||
| cd125e83be | |||
| 6ba0f6037f | |||
| 5bc02e49f2 | |||
| bf634382e5 | |||
| 551fb1579d | |||
| 12891b17ea | |||
| 96b7189c44 | |||
| 8489426540 | |||
| c592553db2 | |||
| 15e4cc7bce | |||
| 023a3490dc | |||
| ee920b119e | |||
| fbcd52bcdd | |||
| 8b3bc673d0 | |||
| 25341e2b7c | |||
| 5c8437436e | |||
| 879dbb5ad0 | |||
| 7b4d084cb2 | |||
| c93ecbfdae | |||
| d79fcc2774 | |||
| 98b5c106ec | |||
| c44610fd14 | |||
| 24a34146f2 | |||
| af96a5b18d | |||
| f98bdc054a | |||
| d92d153175 | |||
| 82e201973b | |||
| 52b1425a41 | |||
| 547c3917c5 | |||
| 2800421e8d | |||
| 57d1c290d9 | |||
| 650413d510 | |||
| 0e677bdee7 | |||
| 4455cc15cc | |||
| bc48387749 | |||
| 8c625a3113 | |||
| 9775df895b | |||
| 5fc2d29e4f | |||
| 60cfdd4a70 | |||
| ad52a14d42 | |||
| 792f640e41 | |||
| eb47420444 | |||
| 8492d73714 | |||
| c8565e08c9 | |||
| 63a0d544e4 | |||
| e7591f9a64 | |||
| f242776772 | |||
| 51008ba604 | |||
| 96c68686bf | |||
| 93ddbca73a | |||
| a30de127ca | |||
| 8e5611bc50 | |||
| 9e2d6a484a | |||
| 7be7b17cc1 | |||
| a619149412 | |||
| 488156df89 | |||
| 1c068cc6f9 | |||
| c6f2f8ccb2 | |||
| 518c78c3d1 | |||
| fe034d1a38 | |||
| 49e6cd1c9b | |||
| 3e78f81a53 | |||
| e84f0cdb97 | |||
| 804bd5b9e1 | |||
| 9c9f247e0c | |||
| db43f7a62d | |||
| efef1dca6b | |||
| 3417ec3e99 | |||
| bda832a8a3 | |||
| 30e9ab68d7 | |||
| 9ba1a0f76a | |||
| 0521f343e3 | |||
| 9ecc21dcb2 | |||
| d99f4b2766 | |||
| 5c83e33ace | |||
| a4ac2c60ac | |||
| bbb7fb8ba9 | |||
| 0ebd399768 | |||
| 9971b41434 | |||
| 7f4d005a67 | |||
| 2709417ba3 | |||
| c4386d2d84 | |||
| fbcf989885 | |||
| 07352f6e16 | |||
| 6fecfce3c7 | |||
| 2884ffafeb | |||
| 67ac202b04 | |||
| 839afd8fcf | |||
| 202b07bfb4 | |||
| 15bd0999a2 | |||
| 4450c2527f | |||
| 614e62b782 | |||
| 77cce86e29 | |||
| 93dc547b9d | |||
| 0681a3c0c6 | |||
| 9bf35e3e1f | |||
| 087e8694d1 | |||
| 34b034c822 | |||
| bd598699e3 | |||
| 8867b2aa7c | |||
| 296e8fc50e | |||
| da065a1966 | |||
| 5262a54463 | |||
| f37c5d09ed | |||
| 298e030db0 | |||
| 6f2ef4499b | |||
| e4d1064843 | |||
| 0c1ad7c0c7 | |||
| fb9333a11b | |||
| ed67d7d085 | |||
| d34136589b | |||
| 5226c92f78 | |||
| 9e507b31ec | |||
| ac7b120176 | |||
| c15a680797 | |||
| 1c07142bce | |||
| c886071559 | |||
| 44b6809abe | |||
| 13db2cfb58 | |||
| a1af2c60cd | |||
| 37d322bec8 | |||
| ccd61ce095 | |||
| 01c63a3650 | |||
| 5a99378845 | |||
| dfed83df0b | |||
| 75b0d5c4fb | |||
| d654f3eceb | |||
| 6cdcbd8244 | |||
| cb18e42602 | |||
| 37d78117f1 | |||
| 0cf4e6d2b6 | |||
| ec5196ea6e | |||
| 089c2e9b17 | |||
| 5f2420960b | |||
| e73ee6735e | |||
| 03b4c5adb9 | |||
| b92703c7bb | |||
| 2bb4d10b66 | |||
| e02b08c21d | |||
| 539ace566c | |||
| a29373d1a2 | |||
| 3c0eac4541 | |||
| beee7ea9f8 | |||
| 770416805e | |||
| c2fcecaec0 | |||
| 3a30bcf937 | |||
| 54f9a04b9d | |||
| 62b7c38f88 | |||
| b474128a5f | |||
| cb46130880 | |||
| 8cf9fbe77c | |||
| 9c5da96eb4 | |||
| 5abf538cb4 | |||
| a1d79d4fc6 | |||
| 468c8982b0 | |||
| a9b4be43c8 | |||
| 1350895676 | |||
| 591bee332f | |||
| 3257f4b04f | |||
| 872729a93b | |||
| a7d6a5088a | |||
| ebf832b501 | |||
| 11fd3e14bb | |||
| eb4df34d42 | |||
| 6bc6dd5c03 | |||
| 460f1167b0 | |||
| f04563e868 | |||
| c4881cf407 | |||
| 9945f31e7c | |||
| c13fd882ed | |||
| da6257c233 | |||
| 0b5da03ffb | |||
| 617fc3d387 | |||
| d82f9e4db2 | |||
| efabc926cf | |||
| c5505e9ac5 | |||
| bb14f9121e | |||
| 050666a39c | |||
| a03d83676f | |||
| 6d4bff0ce8 | |||
| 5e1b532f52 | |||
| 4ea19f97da | |||
| 3420eb968e | |||
| f9f48c3f81 | |||
| 5fad7b1efc | |||
| 1337ad97f1 | |||
| 520d6c05b1 | |||
| 52bd643b63 | |||
| 928136cb62 | |||
| 4c5bd9a7be | |||
| 9378bb5f51 | |||
| c036debd17 | |||
| d330ca4746 | |||
| 645b9a8311 | |||
| 862bd449fd | |||
| 0a7ea0251a | |||
| 5809a5478b | |||
| 8bbdfb4a9e | |||
| 74b5ff48fc | |||
| c6c24aaf9f | |||
| e133b59969 | |||
| 61fea7afaa | |||
| 46aa4ab005 | |||
| 8a8b2607a1 | |||
| 5df604e480 | |||
| 7a169dd555 | |||
| af49e6fd6f | |||
| 36a03d91fc | |||
| 143246237d | |||
| a0dbfeaba5 | |||
| d19d6efa8b | |||
| fd97c50da8 | |||
| de109abd76 | |||
| 0e5830df20 | |||
| 20e158c7bd | |||
| b77c908719 | |||
| 5f9dc2780c | |||
| 9c905da702 | |||
| 4729e7f29e | |||
| 10c5a2739c | |||
| 8d75729bca | |||
| 3f61b8cfdc | |||
| 3a55fdfff0 | |||
| 941fb60336 | |||
| 133261fba2 | |||
| 9fc01567a2 | |||
| 4118204948 | |||
| 12bb8d3f68 | |||
| 88517ff0f7 | |||
| 6dd3758f98 | |||
| e3630465ed | |||
| 4715f18c07 | |||
| b1d4520360 | |||
| 467b5e8e19 | |||
| ce39d3837f | |||
| 884dd94242 | |||
| df2ff662dd | |||
| f602d4e69d | |||
| 27f898b696 | |||
| 400afddc45 | |||
| edf7edced3 | |||
| 0ef5c36900 | |||
| a008ede927 | |||
| b4e88937cc | |||
| fd3992cabd | |||
| 47bed89fe2 | |||
| 361fff7803 | |||
| a6ba94e03f | |||
| d39e2514b2 | |||
| 155757dd74 | |||
| cc97e90a3e | |||
| 68160dd9c4 | |||
| 7694c95e1a | |||
| 5b91a4a9e8 | |||
| 7c56449874 | |||
| 3dbe83fd3e | |||
| 2a3bce8cd8 | |||
| d3303b2436 | |||
| 5d2717d584 | |||
| e22fd87d45 | |||
| 3a6f1bf92c | |||
| ae3283adda | |||
| 145158a925 | |||
| e46e1a0814 | |||
| 7a624cd065 | |||
| fe7c8743b9 | |||
| ccc1aae78e | |||
| 935e17c200 | |||
| 63bb1f24a3 | |||
| cdc8189ad1 | |||
| f6daab2111 | |||
| 7ed461e6ef | |||
| 15636348b5 | |||
| a370ad48d5 | |||
| 8900631b20 | |||
| fa3ef24999 | |||
| deaf58c47b | |||
| 1d4573a657 | |||
| 84300fc734 | |||
| 2b9873e76b | |||
| c589c599fc | |||
| 2702e80314 | |||
| bb1dda41ff | |||
| ddafd95b69 | |||
| d6e87a7137 | |||
| 54eca89c4e | |||
| fbe34d7377 | |||
| e2b497f158 | |||
| ca9fcf8e1d | |||
| 4fb012c0e4 | |||
| b77448f2a9 | |||
| 3bc921c8d7 | |||
| 54c08cf79b | |||
| 199c51722c | |||
| 379af164dd | |||
| dc4ba652b4 | |||
| 0e8561a1c5 | |||
| 60ec928a8e | |||
| 26f3ab65a7 | |||
| 0cc209484d | |||
| f09a7815af | |||
| 70b573cec0 | |||
| 0d79196eda | |||
| 8eb4e05055 | |||
| cd793a9b53 | |||
| fad75e2f5b | |||
| d6710453e5 |
+1
-1
@@ -5,4 +5,4 @@ node_modules
|
||||
dist
|
||||
.git
|
||||
e2e
|
||||
|
||||
docs
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
version = 1
|
||||
merge.notify_on_conflict = false
|
||||
@@ -1,10 +1,9 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: amir20
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@@ -24,9 +24,10 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS, Windows or Mac Os]
|
||||
- Docker version [e.g. `docker version`'s output]
|
||||
- Browser & version [e.g. chrome, safari]
|
||||
- Version [e.g. 1.26.1. Can be found at `<Dozzle-host>/version`]
|
||||
|
||||
- OS: [e.g. iOS, Windows or Mac Os]
|
||||
- Docker version [e.g. `docker version`'s output]
|
||||
- Browser & version [e.g. chrome, safari]
|
||||
- Version [e.g. 1.26.1. Can be found at `<Dozzle-host>/version`]
|
||||
|
||||
**If applicable include logs with `--level debug` and browser logs**
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
title: ""
|
||||
labels: enhancement
|
||||
assignees: amir20
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "automerge"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "automerge"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
labels:
|
||||
- "gomod"
|
||||
- "dependencies"
|
||||
- "automerge"
|
||||
schedule:
|
||||
interval: daily
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
labels:
|
||||
- "npm"
|
||||
- "dependencies"
|
||||
- "automerge"
|
||||
schedule:
|
||||
interval: daily
|
||||
- package-ecosystem: npm
|
||||
directory: "/e2e"
|
||||
labels:
|
||||
- "npm"
|
||||
- "dependencies"
|
||||
- "automerge"
|
||||
schedule:
|
||||
interval: daily
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/e2e"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "automerge"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base", ":automergeMinor"]
|
||||
}
|
||||
@@ -8,26 +8,30 @@ jobs:
|
||||
name: JavaScript Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
name: Install Node
|
||||
with:
|
||||
version: 6.20.1
|
||||
node-version: latest
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
- name: Run Tests
|
||||
run: pnpm run test
|
||||
run: pnpm run test -- --outputTruncateLength=500
|
||||
go-test:
|
||||
name: Go Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Go Tests with Coverage
|
||||
@@ -38,10 +42,18 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Build images
|
||||
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Build
|
||||
uses: docker/bake-action@v3
|
||||
with:
|
||||
workdir: ./e2e
|
||||
load: true
|
||||
set: |
|
||||
*.cache-from=type=gha
|
||||
*.cache-to=type=gha,mode=max
|
||||
- name: Run tests
|
||||
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
|
||||
run: docker compose -f e2e/docker-compose.yml up --exit-code-from cypress
|
||||
buildx:
|
||||
needs: [go-test, npm-test, int-test]
|
||||
name: Release
|
||||
@@ -53,22 +65,22 @@ jobs:
|
||||
with:
|
||||
images: amir20/dozzle
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: TAG=${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=amir20/dozzle:latest
|
||||
cache-to: type=inline
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
git-release:
|
||||
needs: [buildx]
|
||||
name: Github Release
|
||||
@@ -80,13 +92,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 6.20.1
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Release to Github
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm run release -- --github.release --no-increment --no-git --ci
|
||||
run: npx changelogithub
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
buildx:
|
||||
name: Push branches and PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle' }}
|
||||
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
|
||||
steps:
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
@@ -18,19 +18,19 @@ jobs:
|
||||
with:
|
||||
images: amir20/dozzle
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: TAG=${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=amir20/dozzle:master
|
||||
cache-to: type=inline
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
+44
-35
@@ -7,30 +7,54 @@ on:
|
||||
- master
|
||||
name: Test
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
- name: Run Typecheck
|
||||
run: pnpm run typecheck
|
||||
npm-test:
|
||||
name: JavaScript Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
name: Install Node
|
||||
with:
|
||||
version: 6.20.1
|
||||
node-version: latest
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
- name: Run Tests
|
||||
run: pnpm run test
|
||||
run: pnpm run test -- --outputTruncateLength=500
|
||||
go-test:
|
||||
name: Go Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Go Tests with Coverage
|
||||
@@ -41,30 +65,15 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Build
|
||||
uses: docker/bake-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build images
|
||||
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build --build-arg BUILDKIT_INLINE_CACHE=1
|
||||
- name: Push images
|
||||
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml push
|
||||
- name: Set commit message for push
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
echo "GIT_LOG_MESSAGE<<EOF" >> $GITHUB_ENV
|
||||
git log -1 --pretty=%B ${GITHUB_SHA} >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
- name: Set commit message for pull request
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
echo "GIT_LOG_MESSAGE<<EOF" >> $GITHUB_ENV
|
||||
git log -1 --pretty=%B ${{github.event.pull_request.head.sha}} >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
workdir: ./e2e
|
||||
load: true
|
||||
set: |
|
||||
*.cache-from=type=gha
|
||||
*.cache-to=type=gha,mode=max
|
||||
- name: Run tests
|
||||
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
|
||||
run: docker compose -f e2e/docker-compose.yml up --exit-code-from cypress
|
||||
|
||||
@@ -7,3 +7,5 @@ dozzle
|
||||
coverage
|
||||
.pnpm-debug.log
|
||||
.vscode
|
||||
coverage.out
|
||||
.netlify
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
pnpm lint-staged
|
||||
@@ -1 +1,3 @@
|
||||
shamefully-hoist=true
|
||||
shared-workspace-lockfile=false
|
||||
include-workspace-root=true
|
||||
ignore-workspace-root-check=true
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
docs/.vitepress/cache
|
||||
docs/.vitepress/dist
|
||||
dist
|
||||
+13
-13
@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
|
||||
+6
-7
@@ -1,18 +1,17 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:19-alpine as node
|
||||
|
||||
RUN npm install -g pnpm
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine as node
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install dependencies from lock file
|
||||
COPY pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
COPY pnpm-*.yaml ./
|
||||
RUN pnpm fetch --ignore-scripts --no-optional
|
||||
|
||||
# Copy package.json and install dependencies
|
||||
COPY package.json ./
|
||||
RUN pnpm install -r --offline --ignore-scripts
|
||||
RUN pnpm install --offline --ignore-scripts --no-optional
|
||||
|
||||
# Copy assets and translations to build
|
||||
COPY .* vite.config.ts index.html ./
|
||||
@@ -22,7 +21,7 @@ COPY locales ./locales
|
||||
# Build assets
|
||||
RUN pnpm build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.19.2-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.20.3-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache ca-certificates && mkdir /dozzle
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesn’t store any log files. It is for live monitoring of your container logs only.
|
||||
|
||||

|
||||
https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b28a-450c5cd20c94.mp4
|
||||
|
||||
[](https://goreportcard.com/report/github.com/amir20/dozzle)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||
@@ -37,16 +37,7 @@ The simplest way to use dozzle is to run the docker container. Also, mount the D
|
||||
|
||||
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`.
|
||||
|
||||
### With Docker swarm
|
||||
|
||||
docker service create \
|
||||
--name=dozzle \
|
||||
--publish=8888:8080 \
|
||||
--constraint=node.role==manager \
|
||||
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
amir20/dozzle:latest
|
||||
|
||||
### With Docker compose
|
||||
### Connecting with Docker compose
|
||||
|
||||
version: "3"
|
||||
services:
|
||||
@@ -58,8 +49,35 @@ Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). Yo
|
||||
ports:
|
||||
- 9999:8080
|
||||
|
||||
### Connecting to remote hosts
|
||||
|
||||
### Enabling health check
|
||||
Dozzle supports connecting to multiple remote hosts via `tcp://` using TLS or without. Appropriate certs need to be mounted for Dozzle to be able to successfully connect. At this point, `ssh://` is not supported because Dozzle docker image does not ship with any ssh clients.
|
||||
|
||||
To configure remote hosts, `--remote-host` or `DOZZLE_REMOTE_HOST` need to provided and the `pem` files need to be mounted to `/cert` directory. The `/cert` directory expects to have `/certs/{ca,cert,key}.pem` or `/certs/{host}/{ca,cert,key}.pem` in case of multiple hosts.
|
||||
|
||||
Below are examples of using `--remote-host` via CLI:
|
||||
|
||||
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/certs:/certs -p 8080:8080 amir20/dozzle --remote-host tcp://167.99.1.1:2376
|
||||
|
||||
Multiple `--remote-host` flags can be used to specify multiple hosts.
|
||||
|
||||
Or to use compose:
|
||||
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /path/to/certs:/certs
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_REMOTE_HOST: tcp://167.99.1.1:2376,tcp://167.99.1.2:2376
|
||||
|
||||
You need to make sure appropriate certs are provided in `/certs/167.99.1.1/{ca,cert,key}.pem` and `/certs/167.99.1.2/{ca,cert,key}.pem` for both hosts to work.
|
||||
|
||||
### Adding health check
|
||||
|
||||
Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `healthcheck` can be enabled manually.
|
||||
|
||||
@@ -81,7 +99,6 @@ Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `health
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
|
||||
#### Security
|
||||
|
||||
You can control the device Dozzle binds to by passing `--addr` parameter. For example,
|
||||
@@ -98,7 +115,7 @@ this would then only allow you to view containers with a name starting with "foo
|
||||
|
||||
#### Authentication
|
||||
|
||||
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`.
|
||||
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use [docker secrets](https://docs.docker.com/engine/swarm/secrets/) `--usernamefile` and `--passwordfile`.
|
||||
|
||||
#### Changing base URL
|
||||
|
||||
@@ -119,17 +136,19 @@ If you do not want to be tracked at all, see the `--no-analytics` flag below.
|
||||
|
||||
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars.
|
||||
|
||||
| Flag | Env Variable | Default |
|
||||
| ---------------- | --------------------- | ------- |
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| n/a | `DOCKER_API_VERSION` | not set |
|
||||
| `--tailSize` | `DOZZLE_TAILSIZE` | `300` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
| `--username` | `DOZZLE_USERNAME` | `""` |
|
||||
| `--password` | `DOZZLE_PASSWORD` | `""` |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
|
||||
| Flag | Env Variable | Default |
|
||||
| ---------------- | ---------------------- | ------- |
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
| `--username` | `DOZZLE_USERNAME` | `""` |
|
||||
| `--password` | `DOZZLE_PASSWORD` | `""` |
|
||||
| `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` |
|
||||
| `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
|
||||
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
|
||||
|
||||
## Troubleshooting and FAQs
|
||||
|
||||
@@ -181,6 +200,28 @@ Dozzle has a [special route](https://github.com/amir20/dozzle/blob/master/assets
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>I installed Dozzle but memory consumption doesn't show up!</summary>
|
||||
|
||||
_This is an issue specific to ARM devices_
|
||||
|
||||
Dozzle uses the Docker API to gather information about the containers' memory usage. If the memory usage is not showing up, then it is likely that the Docker API is not returning the memory usage.
|
||||
|
||||
You can verify this by running `docker info`, and you should see the following:
|
||||
|
||||
```
|
||||
WARNING: No memory limit support
|
||||
WARNING: No swap limit support
|
||||
```
|
||||
|
||||
In this case, you'll need to add the following line to your `/boot/cmdline.txt` file and reboot your device.
|
||||
|
||||
```
|
||||
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
+19
-1
@@ -21,7 +21,25 @@ func SendStartEvent(se StartEvent) error {
|
||||
},
|
||||
}
|
||||
|
||||
jsonValue, err := json.Marshal(postBody)
|
||||
return doRequest(postBody)
|
||||
}
|
||||
|
||||
func SendRequestEvent(re RequestEvent) error {
|
||||
postBody := map[string]interface{}{
|
||||
"client_id": re.ClientId,
|
||||
"events": []map[string]interface{}{
|
||||
{
|
||||
"name": "request",
|
||||
"params": re,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return doRequest(postBody)
|
||||
}
|
||||
|
||||
func doRequest(body map[string]interface{}) error {
|
||||
jsonValue, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+14
-7
@@ -1,11 +1,18 @@
|
||||
package analytics
|
||||
|
||||
type StartEvent struct {
|
||||
ClientId string `json:"-"`
|
||||
Version string `json:"version"`
|
||||
FilterLength int `json:"filterLength"`
|
||||
CustomAddress bool `json:"customAddress"`
|
||||
CustomBase bool `json:"customBase"`
|
||||
TailSize int `json:"tailSize"`
|
||||
Protected bool `json:"protected"`
|
||||
ClientId string `json:"-"`
|
||||
Version string `json:"version"`
|
||||
FilterLength int `json:"filterLength"`
|
||||
RemoteHostLength int `json:"remoteHostLength"`
|
||||
CustomAddress bool `json:"customAddress"`
|
||||
CustomBase bool `json:"customBase"`
|
||||
Protected bool `json:"protected"`
|
||||
HasHostname bool `json:"hasHostname"`
|
||||
}
|
||||
|
||||
type RequestEvent struct {
|
||||
ClientId string `json:"-"`
|
||||
TotalContainers int `json:"totalContainers"`
|
||||
RunningContainers int `json:"runningContainers"`
|
||||
}
|
||||
|
||||
Vendored
+42
-2
@@ -1,4 +1,7 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const $$: typeof import('vue/macros')['$$']
|
||||
@@ -14,6 +17,7 @@ declare global {
|
||||
const arrayEquals: typeof import('./utils/index')['arrayEquals']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const collapseNav: typeof import('./composables/settings')['collapseNav']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
@@ -28,7 +32,9 @@ declare global {
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
@@ -105,6 +111,7 @@ declare global {
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const search: typeof import('./composables/settings')['search']
|
||||
const sessionHost: typeof import('./composables/storage')['sessionHost']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const setTitle: typeof import('./composables/title')['setTitle']
|
||||
@@ -138,14 +145,19 @@ declare global {
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
@@ -231,14 +243,18 @@ declare global {
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
@@ -250,6 +266,7 @@ declare global {
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSearchFilter: typeof import('./composables/search')['useSearchFilter']
|
||||
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
@@ -298,8 +315,10 @@ declare global {
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
@@ -309,9 +328,14 @@ declare global {
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
|
||||
}
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module '@vue/runtime-core' {
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
|
||||
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
|
||||
@@ -326,6 +350,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
@@ -340,7 +365,9 @@ declare module '@vue/runtime-core' {
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||
@@ -417,6 +444,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
|
||||
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
|
||||
@@ -450,14 +478,19 @@ declare module '@vue/runtime-core' {
|
||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
@@ -543,14 +576,18 @@ declare module '@vue/runtime-core' {
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
@@ -562,6 +599,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
|
||||
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
|
||||
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
|
||||
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
@@ -610,8 +648,10 @@ declare module '@vue/runtime-core' {
|
||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
|
||||
Vendored
+26
-16
@@ -1,5 +1,7 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
@@ -7,12 +9,19 @@ export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
CarbonCaretDown: typeof import('~icons/carbon/caret-down')['default']
|
||||
CilColumns: typeof import('~icons/cil/columns')['default']
|
||||
CilFindInPage: typeof import('~icons/cil/find-in-page')['default']
|
||||
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
|
||||
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
|
||||
'Cil:circle': typeof import('~icons/cil/circle')['default']
|
||||
'Cil:columns': typeof import('~icons/cil/columns')['default']
|
||||
'Cil:findInPage': typeof import('~icons/cil/find-in-page')['default']
|
||||
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
|
||||
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
|
||||
ContainerHealth: typeof import('./components/LogViewer/ContainerHealth.vue')['default']
|
||||
ContainerPopup: typeof import('./components/LogViewer/ContainerPopup.vue')['default']
|
||||
ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
|
||||
ContainerTitle: typeof import('./components/LogViewer/ContainerTitle.vue')['default']
|
||||
DateTime: typeof import('./components/DateTime.vue')['default']
|
||||
DistanceTime: typeof import('./components/DistanceTime.vue')['default']
|
||||
DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.vue')['default']
|
||||
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
|
||||
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
|
||||
@@ -22,20 +31,21 @@ declare module '@vue/runtime-core' {
|
||||
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
|
||||
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
|
||||
LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
|
||||
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
|
||||
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
|
||||
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
|
||||
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
|
||||
MdiLightChevronDoubleDown: typeof import('~icons/mdi-light/chevron-double-down')['default']
|
||||
MdiLightChevronLeft: typeof import('~icons/mdi-light/chevron-left')['default']
|
||||
MdiLightChevronRight: typeof import('~icons/mdi-light/chevron-right')['default']
|
||||
MdiLightCog: typeof import('~icons/mdi-light/cog')['default']
|
||||
MdiLightMagnify: typeof import('~icons/mdi-light/magnify')['default']
|
||||
'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default']
|
||||
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
|
||||
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
|
||||
'Mdi:lightChevronRight': typeof import('~icons/mdi-light/chevron-right')['default']
|
||||
'Mdi:lightCog': typeof import('~icons/mdi-light/cog')['default']
|
||||
'Mdi:lightLogout': typeof import('~icons/mdi-light/logout')['default']
|
||||
'Mdi:lightMagnify': typeof import('~icons/mdi-light/magnify')['default']
|
||||
MobileMenu: typeof import('./components/MobileMenu.vue')['default']
|
||||
OcticonContainer24: typeof import('~icons/octicon/container24')['default']
|
||||
OcticonDownload24: typeof import('~icons/octicon/download24')['default']
|
||||
OcticonTrash24: typeof import('~icons/octicon/trash24')['default']
|
||||
PastTime: typeof import('./components/PastTime.vue')['default']
|
||||
RelativeTime: typeof import('./components/RelativeTime.vue')['default']
|
||||
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
|
||||
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
|
||||
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
|
||||
Popup: typeof import('./components/Popup.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<time :datetime="date.toISOString()">{{ format(date) }}</time>
|
||||
<div>
|
||||
<time :datetime="date.toISOString()" class="is-hidden-mobile">{{ dateStr }}</time>
|
||||
<time :datetime="date.toISOString()">{{ timeStr }}</time>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
date: Date;
|
||||
}>();
|
||||
|
||||
@@ -13,9 +16,6 @@ const timeFormatter = $computed(
|
||||
() => new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour })
|
||||
);
|
||||
|
||||
function format(date: Date) {
|
||||
const dateStr = dateFormatter.format(date);
|
||||
const timeStr = timeFormatter.format(date);
|
||||
return `${dateStr} ${timeStr}`;
|
||||
}
|
||||
const dateStr = $computed(() => dateFormatter.format(props.date));
|
||||
const timeStr = $computed(() => timeFormatter.format(props.date));
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<time :datetime="date.toISOString()">{{ text }}</time>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import formatDistanceToNow from "date-fns/formatDistanceToNow";
|
||||
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
|
||||
|
||||
const {
|
||||
date,
|
||||
strict = false,
|
||||
suffix = true,
|
||||
} = defineProps<{
|
||||
date: Date;
|
||||
strict?: boolean;
|
||||
suffix?: boolean;
|
||||
}>();
|
||||
|
||||
const text = ref<string>();
|
||||
function updateFromNow() {
|
||||
const fn = strict ? formatDistanceToNowStrict : formatDistanceToNow;
|
||||
text.value = fn(date, {
|
||||
addSuffix: suffix,
|
||||
});
|
||||
}
|
||||
useIntervalFn(updateFromNow, 30_000, { immediateCallback: true });
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
|
||||
<span class="icon">
|
||||
<mdi-dots-vertical />
|
||||
<mdi:dots-vertical />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -26,12 +26,14 @@
|
||||
|
||||
& > .icon {
|
||||
height: 100%;
|
||||
|
||||
& > svg {
|
||||
align-self: flex-start;
|
||||
height: 0.85em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-top {
|
||||
& .dropdown-menu {
|
||||
top: 0;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<span class="icon is-small" :class="item.state">
|
||||
<octicon-container-24 />
|
||||
<octicon:container-24 />
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
@@ -26,7 +26,7 @@
|
||||
@click.stop.prevent="addColumn(item)"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
>
|
||||
<cil-columns />
|
||||
<cil:columns />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,6 +105,15 @@ function addColumn(container: Container) {
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.panel {
|
||||
min-height: 200px;
|
||||
width: auto;
|
||||
margin-left: 0.25rem !important;
|
||||
margin-right: 0.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.running {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@@ -121,9 +130,11 @@ function addColumn(container: Container) {
|
||||
|
||||
:deep(a.dropdown-item) {
|
||||
padding-right: 1em;
|
||||
|
||||
.media-right {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover .media-right {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<div class="columns is-1 is-variable">
|
||||
<div class="columns is-1 is-variable is-mobile">
|
||||
<div class="column is-narrow" v-if="showTimestamp">
|
||||
<log-date :date="logEntry.date"></log-date>
|
||||
</div>
|
||||
<div class="column is-narrow is-flex">
|
||||
<log-level :level="logEntry.level"></log-level>
|
||||
</div>
|
||||
<div class="column">
|
||||
<ul class="fields" :class="{ expanded }" @click="expanded = !expanded">
|
||||
<li v-for="(value, name) in validValues(logEntry.message)">
|
||||
@@ -38,6 +41,7 @@ function validValues(obj: Record<string, any>) {
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: "expand json";
|
||||
color: var(--secondary-color);
|
||||
@@ -55,6 +59,7 @@ function validValues(obj: Record<string, any>) {
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
|
||||
& + li {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="icon is-small health" :health="health" v-if="health" :title="health">
|
||||
<cil:check-circle v-if="health == 'healthy'" />
|
||||
<cil:x-circle v-else-if="health == 'unhealthy'" />
|
||||
<cil:circle v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ContainerHealth } from "@/types/Container";
|
||||
|
||||
defineProps<{
|
||||
health: ContainerHealth;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.health {
|
||||
&[health="unhealthy"] {
|
||||
color: var(--red-color);
|
||||
}
|
||||
|
||||
&[health="healthy"] {
|
||||
color: var(--green-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="has-text-weight-light"> RUNNING </span>
|
||||
<span class="has-text-weight-semibold">
|
||||
<distance-time :date="container.created" strict :suffix="false"></distance-time>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="has-text-weight-light"> LOAD </span>
|
||||
<span class="has-text-weight-semibold"> {{ container.getLastStat().snapshot.cpu }}% </span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="has-text-weight-light"> MEM </span>
|
||||
<span class="has-text-weight-semibold"> {{ formatBytes(container.getLastStat().snapshot.memoryUsage) }} </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
const { container } = defineProps<{
|
||||
container: Container;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -4,13 +4,13 @@
|
||||
class="column is-narrow"
|
||||
:data="memoryData"
|
||||
label="mem"
|
||||
:stat-value="formatBytes(container.stat.memoryUsage)"
|
||||
:stat-value="formatBytes(unref(container.stat).memoryUsage)"
|
||||
></stat-monitor>
|
||||
<stat-monitor
|
||||
class="column is-narrow"
|
||||
:data="cpuData"
|
||||
label="load"
|
||||
:stat-value="container.stat.cpu + '%'"
|
||||
:stat-value="unref(container.stat).cpu + '%'"
|
||||
></stat-monitor>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,12 +49,6 @@ const memoryData = computedWithControl(
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.has-spacer {
|
||||
&::after {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
.has-border {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<div class="columns is-marginless has-text-weight-bold is-family-monospace">
|
||||
<span class="column is-ellipsis">
|
||||
{{ container.name }}
|
||||
<div class="column is-narrow" v-if="container.health">
|
||||
<container-health :health="container.health"></container-health>
|
||||
</div>
|
||||
<div class="column is-ellipsis">
|
||||
{{ container.name }}<span v-if="container.isSwarm">.{{ container.swarmId }}</span>
|
||||
<span class="tag is-dark">{{ container.image.replace(/@sha.*/, "") }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,4 +17,8 @@ import { type ComputedRef } from "vue";
|
||||
const container = inject("container") as ComputedRef<Container>;
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }} </a>
|
||||
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold">{{ value }}</span>
|
||||
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold" v-html="value"></span>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="level is-justify-content-start">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<octicon-trash-24 class="mr-4" />
|
||||
<octicon:trash-24 class="mr-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
@@ -12,11 +12,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="dropdown-item" :href="`${base}/api/logs/download?id=${container.id}`">
|
||||
<a class="dropdown-item" :href="`${base}/api/logs/download?id=${container.id}&host=${sessionHost}`">
|
||||
<div class="level is-justify-content-start">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<octicon-download-24 class="mr-4" />
|
||||
<octicon:download-24 class="mr-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="level is-justify-content-start">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<mdi-light-magnify class="mr-4" />
|
||||
<mdi:light-magnify class="mr-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
|
||||
@@ -58,6 +58,7 @@ function onClearClicked() {
|
||||
button.delete {
|
||||
background-color: var(--scheme-main-ter);
|
||||
opacity: 0.6;
|
||||
|
||||
&:after,
|
||||
&:before {
|
||||
background-color: var(--text-color);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<relative-time :date="date" class="date"></relative-time>
|
||||
<date-time :date="date" class="date"></date-time>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("<LogEventSource />", () => {
|
||||
) {
|
||||
settings.value.hourStyle = hourStyle;
|
||||
search.searchFilter.value = searchFilter;
|
||||
if(searchFilter){
|
||||
if (searchFilter) {
|
||||
search.showSearch.value = true;
|
||||
}
|
||||
|
||||
@@ -91,22 +91,22 @@ describe("<LogEventSource />", () => {
|
||||
|
||||
test("should connect to EventSource", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(1);
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
expect(sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].readyState).toBe(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("should close EventSource", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
wrapper.unmount();
|
||||
expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(2);
|
||||
expect(sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].readyState).toBe(2);
|
||||
});
|
||||
|
||||
test("should parse messages", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
|
||||
});
|
||||
|
||||
@@ -121,8 +121,8 @@ describe("<LogEventSource />", () => {
|
||||
describe("render html correctly", () => {
|
||||
test("should render messages", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
|
||||
});
|
||||
|
||||
@@ -134,8 +134,8 @@ describe("<LogEventSource />", () => {
|
||||
|
||||
test("should render messages with color", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
|
||||
data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
|
||||
});
|
||||
|
||||
@@ -147,8 +147,8 @@ describe("<LogEventSource />", () => {
|
||||
|
||||
test("should render messages with html entities", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
||||
});
|
||||
|
||||
@@ -160,8 +160,8 @@ describe("<LogEventSource />", () => {
|
||||
|
||||
test("should render dates with 12 hour style", async () => {
|
||||
const wrapper = createLogEventSource({ hourStyle: "12" });
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
||||
});
|
||||
|
||||
@@ -173,8 +173,8 @@ describe("<LogEventSource />", () => {
|
||||
|
||||
test("should render dates with 24 hour style", async () => {
|
||||
const wrapper = createLogEventSource({ hourStyle: "24" });
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
||||
});
|
||||
|
||||
@@ -186,11 +186,11 @@ describe("<LogEventSource />", () => {
|
||||
|
||||
test("should render messages with filter", async () => {
|
||||
const wrapper = createLogEventSource({ searchFilter: "test" });
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
|
||||
});
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div :class="level" :data-position="position"></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Position } from "@/models/LogEntry";
|
||||
|
||||
defineProps<{
|
||||
level?: string;
|
||||
position?: Position;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
div {
|
||||
display: inline-block;
|
||||
width: 0.7em;
|
||||
height: 0.7em;
|
||||
border-radius: 0.5em;
|
||||
align-self: auto;
|
||||
margin-top: 0.4em;
|
||||
|
||||
&[data-position="start"] {
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
height: 70%;
|
||||
margin-bottom: -0.2em;
|
||||
margin-top: auto;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&[data-position="middle"] {
|
||||
border-radius: 0;
|
||||
height: auto;
|
||||
margin: -0.2em 0;
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
&[data-position="end"] {
|
||||
border-radius: 0 0 0.5em 0.5em;
|
||||
height: 70%;
|
||||
margin-top: -0.2em;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&.debug,
|
||||
&.trace {
|
||||
background-color: var(--purple-color);
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: var(--green-color);
|
||||
}
|
||||
|
||||
&.error,
|
||||
&.fatal {
|
||||
background-color: var(--red-color);
|
||||
}
|
||||
|
||||
&.warn {
|
||||
background-color: var(--orange-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="level is-justify-content-start">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<cil-find-in-page class="mr-4" />
|
||||
<cil:find-in-page class="mr-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
@@ -72,10 +72,12 @@ watch(
|
||||
display: flex;
|
||||
word-wrap: break-word;
|
||||
padding: 0.2em 1em;
|
||||
|
||||
&:last-child {
|
||||
scroll-snap-align: end;
|
||||
scroll-margin-block-end: 5rem;
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background-color: rgba(125, 125, 125, 0.08);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<div class="columns is-1 is-variable">
|
||||
<div class="columns is-1 is-variable is-mobile">
|
||||
<div class="column is-narrow" v-if="showTimestamp">
|
||||
<log-date :date="logEntry.date"></log-date>
|
||||
</div>
|
||||
<div class="column is-narrow is-flex">
|
||||
<log-level :level="logEntry.level" :position="logEntry.position"></log-level>
|
||||
</div>
|
||||
<div class="text column" v-html="colorize(logEntry.message)"></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,7 +13,7 @@
|
||||
import { SimpleLogEntry } from "@/models/LogEntry";
|
||||
import AnsiConvertor from "ansi-to-html";
|
||||
|
||||
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
|
||||
const ansiConvertor = new AnsiConvertor({ escapeXML: false });
|
||||
defineProps<{
|
||||
logEntry: SimpleLogEntry;
|
||||
}>();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
|
||||
</div>
|
||||
<div class="has-background-body-color is-top-left">
|
||||
<span class="has-text-weight-light has-spacer">{{ label }}</span>
|
||||
<span class="has-text-weight-light">{{ label }}</span>
|
||||
<span class="has-text-weight-bold">
|
||||
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
|
||||
</span>
|
||||
@@ -15,7 +15,7 @@
|
||||
<script lang="ts" setup>
|
||||
const { data, label, statValue } = defineProps<{ data: Point<unknown>[]; label: string; statValue: string | number }>();
|
||||
|
||||
let selectedPoint: Point<unknown> = $ref();
|
||||
let selectedPoint: Point<unknown> | undefined = $ref();
|
||||
|
||||
function onSelectedPoint(point: Point<unknown>) {
|
||||
selectedPoint = point;
|
||||
@@ -25,12 +25,6 @@ let mouseOver = $ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.has-spacer {
|
||||
&::after {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
.has-border {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -1,200 +1,231 @@
|
||||
// Vitest Snapshot v1
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
|
||||
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
|
||||
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
|
||||
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
|
||||
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
|
||||
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
|
||||
</div>
|
||||
</div>
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
|
||||
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><test>foo bar</test></div>
|
||||
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
|
||||
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
|
||||
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><test>foo bar</test></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
|
||||
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
|
||||
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
|
||||
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
|
||||
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
|
||||
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
|
||||
</div>
|
||||
</div>
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42</time></div>
|
||||
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><test>foo bar</test></div>
|
||||
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
|
||||
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
|
||||
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><test>foo bar</test></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
|
||||
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
|
||||
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
|
||||
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
|
||||
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
|
||||
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
|
||||
</div>
|
||||
</div>
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
|
||||
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">This is a message.</div>
|
||||
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
|
||||
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
|
||||
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">This is a message.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
|
||||
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
|
||||
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
|
||||
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
|
||||
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
|
||||
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
|
||||
</div>
|
||||
</div>
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
|
||||
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
|
||||
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
|
||||
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
|
||||
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
|
||||
<li data-key=\\"2\\" class=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
|
||||
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
|
||||
<li data-v-2e92daca=\\"\\" data-key=\\"2\\" class=\\"\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"line-options\\">
|
||||
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#2\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
|
||||
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
|
||||
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
|
||||
</div>
|
||||
</div>
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
|
||||
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><mark>test</mark> bar</div>
|
||||
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
|
||||
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
|
||||
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><mark>test</mark> bar</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
|
||||
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
|
||||
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
|
||||
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
|
||||
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
|
||||
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
|
||||
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
|
||||
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
|
||||
</div>
|
||||
</div>
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
|
||||
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
|
||||
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><test>foo bar</test></div>
|
||||
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
|
||||
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
|
||||
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
|
||||
</div>
|
||||
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><test>foo bar</test></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > renders correctly 1`] = `
|
||||
"<div class=\\"infinte-loader\\" data-v-1cd63c6e=\\"\\">
|
||||
<div class=\\"spinner\\" data-v-1cd63c6e=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"bounce1\\" data-v-1cd63c6e=\\"\\"></div>
|
||||
<div class=\\"bounce2\\" data-v-1cd63c6e=\\"\\"></div>
|
||||
<div class=\\"bounce3\\" data-v-1cd63c6e=\\"\\"></div>
|
||||
"<div data-v-1cd63c6e=\\"\\" class=\\"infinte-loader\\">
|
||||
<div data-v-1cd63c6e=\\"\\" class=\\"spinner\\" style=\\"display: none;\\">
|
||||
<div data-v-1cd63c6e=\\"\\" class=\\"bounce1\\"></div>
|
||||
<div data-v-1cd63c6e=\\"\\" class=\\"bounce2\\"></div>
|
||||
<div data-v-1cd63c6e=\\"\\" class=\\"bounce3\\"></div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class=\\"events medium\\" data-v-2e92daca=\\"\\"></ul>"
|
||||
|
||||
<ul data-v-2e92daca=\\"\\" class=\\"events medium\\"></ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > should parse messages 1`] = `
|
||||
@@ -202,5 +233,7 @@ SimpleLogEntry {
|
||||
"_message": "This is a message.",
|
||||
"date": 2019-06-12T10:55:42.459Z,
|
||||
"id": 1,
|
||||
"level": undefined,
|
||||
"position": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</svg>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="column ml-4 is-family-monospace is-ellipsis" v-if="$route.name == 'container'">
|
||||
{{ allContainersById[route.params.id].name }}
|
||||
<div class="column ml-4 is-family-monospace is-ellipsis" v-if="$route.name == 'container-id'">
|
||||
{{ allContainersById[route.params.id as string]?.name }}
|
||||
</div>
|
||||
|
||||
<div class="column is-narrow push-right">
|
||||
@@ -24,6 +24,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-label level is-mobile is-hidden-mobile" :class="{ 'is-active': showNav }">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<button class="button is-small is-rounded" @click="$emit('search')" :title="$t('tooltip.search')">
|
||||
<span class="icon">
|
||||
<mdi:light-magnify />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-small is-rounded">
|
||||
<span class="icon">
|
||||
<mdi:light-cog />
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered" v-if="secured">
|
||||
<div>
|
||||
<a class="button is-small is-rounded" :href="`${base}/logout`" :title="$t('button.logout')">
|
||||
<span class="icon">
|
||||
<mdi:light-logout />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
|
||||
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
|
||||
<li v-for="item in visibleContainers" :key="item.id">
|
||||
@@ -42,6 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { base, secured } = config;
|
||||
const store = useContainerStore();
|
||||
const route = useRoute();
|
||||
const { visibleContainers, allContainersById } = storeToRefs(store);
|
||||
@@ -63,6 +94,10 @@ aside {
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
|
||||
.level.is-hidden-mobile.is-active {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<time :datetime="date.toISOString()">{{ text }}</time>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import formatDistance from "date-fns/formatDistance";
|
||||
|
||||
const { date } = defineProps<{
|
||||
date: Date;
|
||||
}>();
|
||||
|
||||
const text = ref<string>();
|
||||
function updateFromNow() {
|
||||
text.value = formatDistance(date, new Date(), {
|
||||
addSuffix: true,
|
||||
});
|
||||
}
|
||||
useIntervalFn(updateFromNow, 30_000, { immediateCallback: true });
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div @mouseenter="onMouseEnter" @mouseleave="show = false" ref="trigger"><slot></slot></div>
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-show="show && delayedShow" class="content" ref="content">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { refDebounced } from "@vueuse/core";
|
||||
let show = ref(false);
|
||||
let delayedShow = refDebounced(show, 1000);
|
||||
let content: HTMLElement | null = $ref(null);
|
||||
let trigger: HTMLElement | null = $ref(null);
|
||||
|
||||
function onMouseEnter(e: MouseEvent) {
|
||||
show.value = true;
|
||||
if (e.target && content && e.target instanceof Element) {
|
||||
const { left, top, width } = e.target.getBoundingClientRect();
|
||||
const x = left + width + 10;
|
||||
const y = top;
|
||||
|
||||
content.style.left = `${x}px`;
|
||||
content.style.top = `${y}px`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: var(--scheme-main-ter);
|
||||
border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
</style>
|
||||
@@ -22,7 +22,7 @@
|
||||
@click="scrollToBottom()"
|
||||
v-show="paused"
|
||||
>
|
||||
<mdi-light-chevron-double-down />
|
||||
<mdi:light-chevron-double-down />
|
||||
</button>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -45,7 +45,8 @@ const mutationObserver = new MutationObserver((e) => {
|
||||
scrollToBottom();
|
||||
} else {
|
||||
const record = e[e.length - 1];
|
||||
if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
|
||||
const children = (record.target as HTMLElement).children;
|
||||
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
|
||||
hasMore = true;
|
||||
}
|
||||
}
|
||||
@@ -97,6 +98,7 @@ section {
|
||||
.is-scrollbar-progress {
|
||||
text-align: right;
|
||||
margin-right: 110px;
|
||||
|
||||
.scroll-progress {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
@@ -111,6 +113,7 @@ section {
|
||||
.is-scrollbar-notification {
|
||||
text-align: right;
|
||||
margin-right: 65px;
|
||||
|
||||
button {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
@@ -138,9 +141,11 @@ section {
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
@@ -150,6 +155,7 @@ section {
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease-in;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@keyup.esc="resetSearch()"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<mdi-light-magnify />
|
||||
<mdi:light-magnify />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -52,6 +52,7 @@ onUnmounted(() => resetSearch());
|
||||
margin-left: 1em;
|
||||
background-color: var(--scheme-main-ter);
|
||||
opacity: 0.6;
|
||||
|
||||
&:after,
|
||||
&:before {
|
||||
background-color: var(--text-color);
|
||||
@@ -68,6 +69,7 @@ onUnmounted(() => resetSearch());
|
||||
|
||||
.input {
|
||||
color: var(--body-color);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--border-color);
|
||||
}
|
||||
|
||||
+107
-48
@@ -2,51 +2,90 @@
|
||||
<aside>
|
||||
<div class="columns is-marginless">
|
||||
<div class="column is-paddingless">
|
||||
<router-link :to="{ name: 'index' }">
|
||||
<svg class="logo">
|
||||
<use href="#logo"></use>
|
||||
</svg>
|
||||
</router-link>
|
||||
<h1>
|
||||
<router-link :to="{ name: 'index' }">
|
||||
<svg class="logo">
|
||||
<use href="#logo"></use>
|
||||
</svg>
|
||||
</router-link>
|
||||
|
||||
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
|
||||
{{ hostname }}
|
||||
</small>
|
||||
</h1>
|
||||
<div v-if="config.hosts.length > 1" class="mb-3">
|
||||
<o-dropdown v-model="sessionHost" aria-role="list">
|
||||
<template #trigger>
|
||||
<o-button variant="primary" type="button" size="small">
|
||||
<span>{{ sessionHost }}</span>
|
||||
<span class="icon">
|
||||
<carbon:caret-down />
|
||||
</span>
|
||||
</o-button>
|
||||
</template>
|
||||
|
||||
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in config.hosts" :key="value">
|
||||
<span>{{ value }}</span>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-narrow has-text-right px-1">
|
||||
<button class="button is-rounded" @click="$emit('search')" :title="$t('tooltip.search')">
|
||||
</div>
|
||||
<div class="columns is-marginless">
|
||||
<div class="column is-narrow py-0 pl-0 pr-1">
|
||||
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
|
||||
<span class="icon">
|
||||
<mdi-light-magnify />
|
||||
<mdi:light-magnify />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow has-text-right px-0">
|
||||
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded">
|
||||
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
|
||||
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded is-small">
|
||||
<span class="icon">
|
||||
<mdi-light-cog />
|
||||
<mdi:light-cog />
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="column is-narrow py-0 px-0" v-if="secured">
|
||||
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
|
||||
<span class="icon">
|
||||
<mdi:light-logout />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="menu-label is-hidden-mobile">{{ $t("label.containers") }}</p>
|
||||
<ul class="menu-list is-hidden-mobile" v-if="ready">
|
||||
<li v-for="item in visibleContainers" :key="item.id" :class="item.state">
|
||||
<router-link
|
||||
:to="{ name: 'container-id', params: { id: item.id } }"
|
||||
active-class="is-active"
|
||||
:title="item.name"
|
||||
>
|
||||
<div class="container is-flex is-align-items-center">
|
||||
<div class="is-flex-grow-1 is-ellipsis">
|
||||
{{ item.name }}
|
||||
<li v-for="item in sortedContainers" :key="item.id" :class="item.state">
|
||||
<popup>
|
||||
<router-link
|
||||
:to="{ name: 'container-id', params: { id: item.id } }"
|
||||
active-class="is-active"
|
||||
:title="item.name"
|
||||
>
|
||||
<div class="container is-flex is-align-items-center">
|
||||
<div class="is-flex-grow-1 is-ellipsis">
|
||||
<span class="has-text-weight-semibold">{{ item.name }}</span
|
||||
><span class="has-text-weight-light has-light-opacity" v-if="item.isSwarm">.{{ item.swarmId }}</span>
|
||||
</div>
|
||||
<div class="is-flex-shrink-1 is-flex icons">
|
||||
<div
|
||||
class="icon is-small pin"
|
||||
@click.stop.prevent="store.appendActiveContainer(item)"
|
||||
v-show="!activeContainersById[item.id]"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
>
|
||||
<cil:columns />
|
||||
</div>
|
||||
|
||||
<container-health :health="item.health"></container-health>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-flex-shrink-1 column-icon">
|
||||
<span
|
||||
class="icon is-small"
|
||||
@click.stop.prevent="store.appendActiveContainer(item)"
|
||||
v-show="!activeContainersById[item.id]"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
>
|
||||
<cil-columns />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</router-link>
|
||||
<template #content>
|
||||
<container-popup :container="item"></container-popup>
|
||||
</template>
|
||||
</popup>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="menu-list is-hidden-mobile loading" v-else>
|
||||
@@ -56,12 +95,26 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
import { sessionHost } from "@/composables/storage";
|
||||
|
||||
const { base, secured, hostname } = config;
|
||||
const store = useContainerStore();
|
||||
|
||||
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
|
||||
|
||||
const sortedContainers = computed(() =>
|
||||
visibleContainers.value.sort((a, b) => {
|
||||
if (a.state === "running" && b.state !== "running") {
|
||||
return -1;
|
||||
} else if (a.state !== "running" && b.state === "running") {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const activeContainersById = computed(() =>
|
||||
activeContainers.value.reduce((acc, item) => {
|
||||
acc[item.id] = item;
|
||||
@@ -82,6 +135,15 @@ aside {
|
||||
}
|
||||
}
|
||||
|
||||
.has-light-opacity {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.logo {
|
||||
width: 122px;
|
||||
height: 54px;
|
||||
fill: var(--logo-color);
|
||||
}
|
||||
|
||||
.loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -90,26 +152,23 @@ li.exited a {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 122px;
|
||||
height: 54px;
|
||||
fill: var(--logo-color);
|
||||
.icons {
|
||||
column-gap: 0.35em;
|
||||
align-items: baseline;
|
||||
}
|
||||
a {
|
||||
.pin {
|
||||
display: none;
|
||||
|
||||
.menu-list li {
|
||||
.column-icon {
|
||||
visibility: hidden;
|
||||
|
||||
& > span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .column-icon {
|
||||
visibility: visible;
|
||||
&:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type ComputedRef, type Ref } from "vue";
|
||||
import { encodeXML } from "entities";
|
||||
import debounce from "lodash.debounce";
|
||||
import {
|
||||
type LogEvent,
|
||||
@@ -11,7 +12,12 @@ import {
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
function parseMessage(data: string): LogEntry<string | JSONObject> {
|
||||
const e = JSON.parse(data) as LogEvent;
|
||||
const e = JSON.parse(data, (key, value) => {
|
||||
if (typeof value === "string") {
|
||||
return encodeXML(value);
|
||||
}
|
||||
return value;
|
||||
}) as LogEvent;
|
||||
return asLogEntry(e);
|
||||
}
|
||||
|
||||
@@ -58,7 +64,9 @@ export function useLogStream(container: ComputedRef<Container>) {
|
||||
lastEventId = "";
|
||||
}
|
||||
|
||||
es = new EventSource(`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}`);
|
||||
es = new EventSource(
|
||||
`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}&host=${sessionHost.value}`
|
||||
);
|
||||
es.addEventListener("container-stopped", () => {
|
||||
es?.close();
|
||||
es = null;
|
||||
|
||||
@@ -10,6 +10,7 @@ export const DEFAULT_SETTINGS: {
|
||||
lightTheme: "auto" | "dark" | "light";
|
||||
hourStyle: "auto" | "24" | "12";
|
||||
softWrap: boolean;
|
||||
collapseNav: boolean;
|
||||
} = {
|
||||
search: true,
|
||||
size: "medium",
|
||||
@@ -20,6 +21,7 @@ export const DEFAULT_SETTINGS: {
|
||||
lightTheme: "auto",
|
||||
hourStyle: "auto",
|
||||
softWrap: true,
|
||||
collapseNav: false,
|
||||
};
|
||||
|
||||
const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
|
||||
@@ -69,7 +71,13 @@ const softWrap = computed({
|
||||
set: (value) => (settings.value.softWrap = value),
|
||||
});
|
||||
|
||||
const collapseNav = computed({
|
||||
get: () => settings.value.collapseNav,
|
||||
set: (value) => (settings.value.collapseNav = value),
|
||||
});
|
||||
|
||||
export {
|
||||
collapseNav,
|
||||
softWrap,
|
||||
hourStyle,
|
||||
lightTheme,
|
||||
@@ -79,5 +87,5 @@ export {
|
||||
menuWidth,
|
||||
size,
|
||||
search,
|
||||
settings
|
||||
settings,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const sessionHost = useSessionStorage("host", "localhost");
|
||||
|
||||
export { sessionHost };
|
||||
@@ -1,5 +1,6 @@
|
||||
const { hostname } = config;
|
||||
let subtitle = $ref("");
|
||||
const title = $computed(() => `${subtitle} - Dozzle`);
|
||||
const title = $computed(() => `${subtitle} - Dozzle` + (hostname ? ` @ ${hostname}` : ""));
|
||||
|
||||
useTitle($$(title));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<main v-if="!authorizationNeeded">
|
||||
<mobile-menu v-if="isMobile"></mobile-menu>
|
||||
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
|
||||
<splitpanes @resized="onResized($event)">
|
||||
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
|
||||
<side-menu @search="showFuzzySearch"></side-menu>
|
||||
@@ -25,17 +25,17 @@
|
||||
</pane>
|
||||
</splitpanes>
|
||||
<button
|
||||
@click="collapseNav = !collapseNav"
|
||||
class="button is-rounded"
|
||||
@click="collapse"
|
||||
class="button is-small is-rounded"
|
||||
:class="{ collapsed: collapseNav }"
|
||||
id="hide-nav"
|
||||
v-if="!isMobile"
|
||||
>
|
||||
<span class="icon ml-2" v-if="collapseNav">
|
||||
<mdi-light-chevron-right />
|
||||
<mdi:light-chevron-right />
|
||||
</span>
|
||||
<span class="icon" v-else>
|
||||
<mdi-light-chevron-left />
|
||||
<mdi:light-chevron-left />
|
||||
</span>
|
||||
</button>
|
||||
</main>
|
||||
@@ -47,7 +47,6 @@ import { Splitpanes, Pane } from "splitpanes";
|
||||
import { useProgrammatic } from "@oruga-ui/oruga-next";
|
||||
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
|
||||
|
||||
const collapseNav = ref(false);
|
||||
const { oruga } = useProgrammatic();
|
||||
const { authorizationNeeded } = config;
|
||||
|
||||
@@ -74,6 +73,9 @@ function showFuzzySearch() {
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
function collapse() {
|
||||
collapseNav.value = !collapseNav.value;
|
||||
}
|
||||
function onResized(e: any) {
|
||||
if (e.length == 2) {
|
||||
menuWidth.value = e[0].size;
|
||||
@@ -85,6 +87,7 @@ function onResized(e: any) {
|
||||
:deep(.splitpanes--vertical > .splitpanes__splitter) {
|
||||
min-width: 3px;
|
||||
background: var(--border-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--border-hover-color);
|
||||
}
|
||||
@@ -108,11 +111,13 @@ function onResized(e: any) {
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
|
||||
&.collapsed {
|
||||
left: -40px;
|
||||
width: 60px;
|
||||
padding-left: 40px;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
color: var(--text-strong-color);
|
||||
background: var(--scheme-main);
|
||||
|
||||
&:hover {
|
||||
left: -25px;
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
import type { ContainerStat, ContainerState } from "@/types/Container";
|
||||
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
|
||||
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
|
||||
import { Ref } from "vue";
|
||||
|
||||
type Stat = Omit<ContainerStat, "id">;
|
||||
|
||||
const SWARM_ID_REGEX = /\.([a-z0-9]{25})$/i;
|
||||
|
||||
export class Container {
|
||||
public stat: Ref<Stat>;
|
||||
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
|
||||
public readonly swarmId: string | null = null;
|
||||
public readonly isSwarm: boolean = false;
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly created: number,
|
||||
public readonly created: Date,
|
||||
public readonly image: string,
|
||||
public readonly name: string,
|
||||
public readonly command: string,
|
||||
public status: string,
|
||||
public state: ContainerState
|
||||
public state: ContainerState,
|
||||
public health?: ContainerHealth
|
||||
) {
|
||||
this.stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
|
||||
this.throttledStatHistory = useThrottledRefHistory(this.stat, { capacity: 300, deep: true, throttle: 1000 });
|
||||
|
||||
const match = name.match(SWARM_ID_REGEX);
|
||||
|
||||
if (match) {
|
||||
this.swarmId = match[1];
|
||||
this.name = name.replace(`.${this.swarmId}`, "");
|
||||
this.isSwarm = true;
|
||||
}
|
||||
}
|
||||
|
||||
public getStatHistory() {
|
||||
|
||||
@@ -11,16 +11,18 @@ export interface HasComponent {
|
||||
|
||||
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
|
||||
export type JSONObject = { [x: string]: JSONValue };
|
||||
|
||||
export type Position = "start" | "end" | "middle" | undefined;
|
||||
export interface LogEvent {
|
||||
readonly m: string | JSONObject;
|
||||
readonly ts: number;
|
||||
readonly id: number;
|
||||
readonly l: string;
|
||||
readonly p: Position;
|
||||
}
|
||||
|
||||
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
|
||||
protected readonly _message: T;
|
||||
constructor(message: T, public readonly id: number, public readonly date: Date) {
|
||||
constructor(message: T, public readonly id: number, public readonly date: Date, public readonly level?: string) {
|
||||
this._message = message;
|
||||
}
|
||||
|
||||
@@ -32,6 +34,15 @@ export abstract class LogEntry<T extends string | JSONObject> implements HasComp
|
||||
}
|
||||
|
||||
export class SimpleLogEntry extends LogEntry<string> {
|
||||
constructor(
|
||||
message: string,
|
||||
id: number,
|
||||
date: Date,
|
||||
public readonly level: string,
|
||||
public readonly position: Position
|
||||
) {
|
||||
super(message, id, date, level);
|
||||
}
|
||||
getComponent(): Component {
|
||||
return SimpleLogItem;
|
||||
}
|
||||
@@ -40,8 +51,14 @@ export class SimpleLogEntry extends LogEntry<string> {
|
||||
export class ComplexLogEntry extends LogEntry<JSONObject> {
|
||||
private readonly filteredMessage: ComputedRef<JSONObject>;
|
||||
|
||||
constructor(message: JSONObject, id: number, date: Date, visibleKeys?: Ref<string[][]>) {
|
||||
super(message, id, date);
|
||||
constructor(
|
||||
message: JSONObject,
|
||||
id: number,
|
||||
date: Date,
|
||||
public readonly level: string,
|
||||
visibleKeys?: Ref<string[][]>
|
||||
) {
|
||||
super(message, id, date, level);
|
||||
if (visibleKeys) {
|
||||
this.filteredMessage = computed(() => {
|
||||
if (!visibleKeys.value.length) {
|
||||
@@ -67,13 +84,13 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
|
||||
}
|
||||
|
||||
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
|
||||
return new ComplexLogEntry(event._message, event.id, event.date, visibleKeys);
|
||||
return new ComplexLogEntry(event._message, event.id, event.date, event.level, visibleKeys);
|
||||
}
|
||||
}
|
||||
|
||||
export class DockerEventLogEntry extends LogEntry<string> {
|
||||
constructor(message: string, date: Date, public readonly event: string) {
|
||||
super(message, date.getTime(), date);
|
||||
super(message, date.getTime(), date, "info");
|
||||
}
|
||||
getComponent(): Component {
|
||||
return DockerEventLogItem;
|
||||
@@ -90,7 +107,7 @@ export class SkippedLogsEntry extends LogEntry<string> {
|
||||
public readonly firstSkipped: LogEntry<string | JSONObject>,
|
||||
lastSkipped: LogEntry<string | JSONObject>
|
||||
) {
|
||||
super("", date.getTime(), date);
|
||||
super("", date.getTime(), date, "info");
|
||||
this._totalSkipped = totalSkipped;
|
||||
this.lastSkipped = lastSkipped;
|
||||
}
|
||||
@@ -110,12 +127,16 @@ export class SkippedLogsEntry extends LogEntry<string> {
|
||||
public get totalSkipped(): number {
|
||||
return this._totalSkipped;
|
||||
}
|
||||
|
||||
public get lastSkippedItem(): LogEntry<string | JSONObject> {
|
||||
return this.lastSkipped;
|
||||
}
|
||||
}
|
||||
|
||||
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
|
||||
if (typeof event.m === "string") {
|
||||
return new SimpleLogEntry(event.m, event.id, new Date(event.ts));
|
||||
return new SimpleLogEntry(event.m, event.id, new Date(event.ts), event.l, event.p);
|
||||
} else {
|
||||
return new ComplexLogEntry(event.m, event.id, new Date(event.ts));
|
||||
return new ComplexLogEntry(event.m, event.id, new Date(event.ts), event.l);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-14
@@ -1,16 +1,5 @@
|
||||
import { type App } from "vue";
|
||||
import {
|
||||
Autocomplete,
|
||||
Button,
|
||||
Dropdown,
|
||||
Switch,
|
||||
Radio,
|
||||
Skeleton,
|
||||
Field,
|
||||
Tooltip,
|
||||
Modal,
|
||||
Config,
|
||||
} from "@oruga-ui/oruga-next";
|
||||
import { Autocomplete, Button, Dropdown, Switch, Skeleton, Field, Modal, Config } from "@oruga-ui/oruga-next";
|
||||
import { bulmaConfig } from "@oruga-ui/theme-bulma";
|
||||
|
||||
export const install = (app: App) => {
|
||||
@@ -19,9 +8,7 @@ export const install = (app: App) => {
|
||||
.use(Button)
|
||||
.use(Dropdown)
|
||||
.use(Switch)
|
||||
.use(Tooltip)
|
||||
.use(Modal)
|
||||
.use(Radio)
|
||||
.use(Field)
|
||||
.use(Skeleton)
|
||||
.use(Config, bulmaConfig);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { type App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
|
||||
export const install = (app:App) => {
|
||||
export const install = (app: App) => {
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
};
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="hero is-small mt-4">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column is-narrow" v-if="secured">
|
||||
<a class="button is-primary is-small" :href="`${base}/logout`">{{ $t("button.logout") }}</a>
|
||||
</div>
|
||||
</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">{{ $t("label.total-containers") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="title">{{ runningContainers.length }}</p>
|
||||
<p class="heading">{{ $t("label.running") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="title" data-ci-skip>{{ totalCpu }}%</p>
|
||||
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="title" data-ci-skip>{{ formatBytes(totalMem) }}</p>
|
||||
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="title">{{ version }}</p>
|
||||
<p class="heading">{{ $t("label.dozzle-version") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="columns is-centered section is-marginless">
|
||||
<div class="column is-4">
|
||||
<div class="panel">
|
||||
<p class="panel-heading">{{ $t("label.containers") }}</p>
|
||||
<div class="panel-block">
|
||||
<p class="control has-icons-left">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="$t('placeholder.search-containers')"
|
||||
v-model="query"
|
||||
@keyup.esc="query = ''"
|
||||
@keyup.enter="onEnter()"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<search-icon />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="panel-tabs" v-if="query === ''">
|
||||
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
|
||||
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
|
||||
</p>
|
||||
<router-link
|
||||
:to="{ name: 'container-id', params: { id: item.id } }"
|
||||
v-for="item in data.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 lang="ts" setup>
|
||||
import SearchIcon from "~icons/mdi-light/magnify";
|
||||
import { useFuse } from "@vueuse/integrations/useFuse";
|
||||
|
||||
const { base, version, secured } = config;
|
||||
const containerStore = useContainerStore();
|
||||
const { containers } = storeToRefs(containerStore);
|
||||
const router = useRouter();
|
||||
|
||||
const sort = $ref("running");
|
||||
const query = ref("");
|
||||
|
||||
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
|
||||
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
|
||||
|
||||
const { results } = useFuse(query, containers, {
|
||||
fuseOptions: { keys: ["name"] },
|
||||
matchAllWhenSearchEmpty: false,
|
||||
});
|
||||
const data = computed(() => {
|
||||
if (results.value.length) {
|
||||
return results.value.map(({ item }) => item);
|
||||
}
|
||||
switch (sort) {
|
||||
case "all":
|
||||
return mostRecentContainers;
|
||||
case "running":
|
||||
return runningContainers;
|
||||
default:
|
||||
throw `Invalid sort order: ${sort}`;
|
||||
}
|
||||
});
|
||||
|
||||
let totalCpu = $ref(0);
|
||||
useIntervalFn(
|
||||
() => {
|
||||
totalCpu = runningContainers.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
|
||||
},
|
||||
1000,
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
let totalMem = $ref(0);
|
||||
useIntervalFn(
|
||||
() => {
|
||||
totalMem = runningContainers.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
|
||||
},
|
||||
1000,
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function onEnter() {
|
||||
if (data.value.length > 0) {
|
||||
const item = data.value[0];
|
||||
router.push({ name: "container-id", params: { id: item.id } });
|
||||
}
|
||||
}
|
||||
</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>
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div class="hero is-halfheight">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<section class="columns is-centered section">
|
||||
<div class="column is-4">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<form action="" method="post" @submit.prevent="onLogin" ref="form">
|
||||
<div class="field">
|
||||
<label class="label">{{ $t("label.username") }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
v-model="username"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t("label.password") }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
v-model="password"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="error">{{ $t("error.invalid-auth") }}</p>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-centered mt-5">
|
||||
<p class="control">
|
||||
<button class="button is-primary" type="submit">{{ $t("button.login") }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n();
|
||||
|
||||
setTitle(t("title.login"));
|
||||
|
||||
let error = $ref(false);
|
||||
let username = $ref("");
|
||||
let password = $ref("");
|
||||
let form: HTMLFormElement = $ref();
|
||||
|
||||
async function onLogin() {
|
||||
const response = await fetch(`${config.base}/api/validateCredentials`, {
|
||||
body: new FormData(form),
|
||||
method: "post",
|
||||
});
|
||||
|
||||
if (response.status == 200) {
|
||||
error = false;
|
||||
window.location.href = `${config.base}/`;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: splash
|
||||
</route>
|
||||
@@ -1,203 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="section">
|
||||
<div class="has-underline">
|
||||
<h2 class="title is-4">{{ $t("settings.about") }}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span v-html="$t('settings.using-version', { version: currentVersion })"></span>
|
||||
<div
|
||||
v-if="hasUpdate"
|
||||
v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="has-underline">
|
||||
<h2 class="title is-4">{{ $t("settings.display") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
|
||||
</div>
|
||||
<div class="item">
|
||||
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column is-narrow">
|
||||
<o-field>
|
||||
<o-dropdown v-model="hourStyle" aria-role="list">
|
||||
<template #trigger>
|
||||
<o-button variant="primary" type="button">
|
||||
<span class="is-capitalized">{{ hourStyle }}</span>
|
||||
<span class="icon">
|
||||
<carbon-caret-down />
|
||||
</span>
|
||||
</o-button>
|
||||
</template>
|
||||
|
||||
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in ['auto', '12', '24']" :key="value">
|
||||
<span class="is-capitalized">{{ value }}</span>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</o-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
{{ $t("settings.12-24-format") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column is-narrow">
|
||||
<o-field>
|
||||
<o-dropdown v-model="size" aria-role="list">
|
||||
<template #trigger>
|
||||
<o-button variant="primary" type="button">
|
||||
<span class="is-capitalized">{{ size }}</span>
|
||||
<span class="icon">
|
||||
<carbon-caret-down />
|
||||
</span>
|
||||
</o-button>
|
||||
</template>
|
||||
|
||||
<o-dropdown-item
|
||||
:value="value"
|
||||
aria-role="listitem"
|
||||
v-for="value in ['small', 'medium', 'large']"
|
||||
:key="value"
|
||||
>
|
||||
<span class="is-capitalized">{{ value }}</span>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</o-field>
|
||||
</div>
|
||||
<div class="column">{{ $t("settings.font-size") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column is-narrow">
|
||||
<o-field>
|
||||
<o-dropdown v-model="lightTheme" aria-role="list">
|
||||
<template #trigger>
|
||||
<o-button variant="primary" type="button">
|
||||
<span class="is-capitalized">{{ lightTheme }}</span>
|
||||
<span class="icon">
|
||||
<carbon-caret-down />
|
||||
</span>
|
||||
</o-button>
|
||||
</template>
|
||||
|
||||
<o-dropdown-item
|
||||
:value="value"
|
||||
aria-role="listitem"
|
||||
v-for="value in ['auto', 'dark', 'light']"
|
||||
:key="value"
|
||||
>
|
||||
<span class="is-capitalized">{{ value }}</span>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</o-field>
|
||||
</div>
|
||||
<div class="column">{{ $t("settings.color-scheme") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<div class="has-underline">
|
||||
<h2 class="title is-4">{{ $t("settings.options") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<o-switch v-model="search">
|
||||
<span v-html="$t('settings.search')"></span>
|
||||
</o-switch>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import gt from "semver/functions/gt";
|
||||
import {
|
||||
search,
|
||||
lightTheme,
|
||||
smallerScrollbars,
|
||||
showTimestamp,
|
||||
hourStyle,
|
||||
showAllContainers,
|
||||
size,
|
||||
softWrap,
|
||||
} from "@/composables/settings";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
setTitle(t("title.settings"));
|
||||
|
||||
const currentVersion = $ref(config.version);
|
||||
let nextRelease = $ref({ html_url: "", name: "" });
|
||||
let hasUpdate = $ref(false);
|
||||
|
||||
async function fetchNextRelease() {
|
||||
if (!["dev", "master"].includes(currentVersion)) {
|
||||
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
|
||||
if (response.ok) {
|
||||
const release = await response.json();
|
||||
hasUpdate = gt(release.tag_name, currentVersion);
|
||||
nextRelease = release;
|
||||
}
|
||||
} else {
|
||||
hasUpdate = true;
|
||||
nextRelease = {
|
||||
html_url: "",
|
||||
name: "master",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fetchNextRelease();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
a.next-release {
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.has-underline {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1em 0px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 4px;
|
||||
background-color: #444;
|
||||
}
|
||||
</style>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const store = useContainerStore();
|
||||
const { visibleContainers } = storeToRefs(store);
|
||||
|
||||
watch(visibleContainers, (newValue) => {
|
||||
if (newValue) {
|
||||
if (route.query.name) {
|
||||
const [container, _] = visibleContainers.value.filter((c) => c.name == route.query.name);
|
||||
if (container) {
|
||||
router.push({ name: "container-id", params: { id: container.id } });
|
||||
} else {
|
||||
console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
|
||||
router.push({ name: "index" });
|
||||
}
|
||||
} else {
|
||||
console.error(`Expection query parameter name to be set. Redirecting to /`);
|
||||
router.push({ name: "index" });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template></template>
|
||||
@@ -4,6 +4,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import search from "@/components/Search.vue";
|
||||
const store = useContainerStore();
|
||||
const { id } = defineProps<{ id: string }>();
|
||||
|
||||
|
||||
+34
-20
@@ -1,17 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="hero is-small mt-4">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column is-narrow" v-if="secured">
|
||||
<a class="button is-primary is-small" :href="`${base}/logout`">{{ $t("button.logout") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="level section">
|
||||
<section class="level section pb-0-is-mobile">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="title">{{ containers.length }}</p>
|
||||
@@ -44,8 +33,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="columns is-centered section is-marginless">
|
||||
<div class="column is-4">
|
||||
<section class="columns is-centered section is-marginless pt-0-is-mobile">
|
||||
<div class="column is-12-mobile is-6-tablet is-5-desktop is-4-fullhd">
|
||||
<div class="panel">
|
||||
<p class="panel-heading">{{ $t("label.containers") }}</p>
|
||||
<div class="panel-block">
|
||||
@@ -59,7 +48,7 @@
|
||||
@keyup.enter="onEnter()"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<search-icon />
|
||||
<mdi:light-magnify />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -76,7 +65,7 @@
|
||||
<span class="name">{{ item.name }}</span>
|
||||
|
||||
<div class="subtitle is-7 status">
|
||||
<past-time :date="new Date(item.created * 1000)"></past-time>
|
||||
<distance-time :date="item.created"></distance-time>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -86,10 +75,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SearchIcon from "~icons/mdi-light/magnify";
|
||||
import { useFuse } from "@vueuse/integrations/useFuse";
|
||||
|
||||
const { base, version, secured } = config;
|
||||
const { version } = config;
|
||||
const containerStore = useContainerStore();
|
||||
const { containers } = storeToRefs(containerStore);
|
||||
const router = useRouter();
|
||||
@@ -97,13 +85,25 @@ const router = useRouter();
|
||||
const sort = $ref("running");
|
||||
const query = ref("");
|
||||
|
||||
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
|
||||
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => +b.created - +a.created));
|
||||
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
|
||||
|
||||
const { results } = useFuse(query, containers, {
|
||||
const list = computed(() => {
|
||||
return containers.value.map(({ id, created, name, state }) => {
|
||||
return {
|
||||
id,
|
||||
created,
|
||||
name,
|
||||
state,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const { results } = useFuse(query, list, {
|
||||
fuseOptions: { keys: ["name"] },
|
||||
matchAllWhenSearchEmpty: false,
|
||||
});
|
||||
|
||||
const data = computed(() => {
|
||||
if (results.value.length) {
|
||||
return results.value.map(({ item }) => item);
|
||||
@@ -146,17 +146,21 @@ function onEnter() {
|
||||
<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;
|
||||
@@ -164,6 +168,16 @@ function onEnter() {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.pb-0-is-mobile {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.pt-0-is-mobile {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 10px 3px;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ setTitle(t("title.login"));
|
||||
let error = $ref(false);
|
||||
let username = $ref("");
|
||||
let password = $ref("");
|
||||
let form: HTMLFormElement = $ref();
|
||||
let form: HTMLFormElement | undefined = $ref();
|
||||
|
||||
async function onLogin() {
|
||||
const response = await fetch(`${config.base}/api/validateCredentials`, {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<o-button variant="primary" type="button">
|
||||
<span class="is-capitalized">{{ hourStyle }}</span>
|
||||
<span class="icon">
|
||||
<carbon-caret-down />
|
||||
<carbon:caret-down />
|
||||
</span>
|
||||
</o-button>
|
||||
</template>
|
||||
@@ -64,7 +64,7 @@
|
||||
<o-button variant="primary" type="button">
|
||||
<span class="is-capitalized">{{ size }}</span>
|
||||
<span class="icon">
|
||||
<carbon-caret-down />
|
||||
<carbon:caret-down />
|
||||
</span>
|
||||
</o-button>
|
||||
</template>
|
||||
@@ -92,7 +92,7 @@
|
||||
<o-button variant="primary" type="button">
|
||||
<span class="is-capitalized">{{ lightTheme }}</span>
|
||||
<span class="icon">
|
||||
<carbon-caret-down />
|
||||
<carbon:caret-down />
|
||||
</span>
|
||||
</o-button>
|
||||
</template>
|
||||
@@ -131,7 +131,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import gt from "semver/functions/gt";
|
||||
import {
|
||||
search,
|
||||
lightTheme,
|
||||
@@ -147,7 +146,7 @@ const { t } = useI18n();
|
||||
|
||||
setTitle(t("title.settings"));
|
||||
|
||||
const currentVersion = $ref(config.version);
|
||||
const currentVersion = config.version;
|
||||
let nextRelease = $ref({ html_url: "", name: "" });
|
||||
let hasUpdate = $ref(false);
|
||||
|
||||
@@ -156,7 +155,7 @@ async function fetchNextRelease() {
|
||||
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
|
||||
if (response.ok) {
|
||||
const release = await response.json();
|
||||
hasUpdate = gt(release.tag_name, currentVersion);
|
||||
hasUpdate = release.tag_name.slice(1).localeCompare(currentVersion, undefined, { numeric: true, sensitivity: 'base' }) > 0;
|
||||
nextRelease = release;
|
||||
}
|
||||
} else {
|
||||
@@ -177,6 +176,7 @@ fetchNextRelease();
|
||||
|
||||
a.next-release {
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ interface Config {
|
||||
authorizationNeeded: boolean | "false" | "true";
|
||||
secured: boolean | "false" | "true";
|
||||
maxLogs: number;
|
||||
hostname: string;
|
||||
hosts: string[] | string;
|
||||
}
|
||||
|
||||
const pageConfig = JSON.parse(text);
|
||||
@@ -20,10 +22,13 @@ if (config.version == "{{ .Version }}") {
|
||||
config.base = "";
|
||||
config.authorizationNeeded = false;
|
||||
config.secured = false;
|
||||
config.hostname = "localhost";
|
||||
config.hosts = ["localhost"];
|
||||
} else {
|
||||
config.version = config.version.replace(/^v/, "");
|
||||
config.authorizationNeeded = config.authorizationNeeded === "true";
|
||||
config.secured = config.secured === "true";
|
||||
config.hosts = (config.hosts as string).split(",");
|
||||
}
|
||||
|
||||
export default config as Config;
|
||||
|
||||
+46
-23
@@ -1,11 +1,13 @@
|
||||
import { acceptHMRUpdate, defineStore } from "pinia";
|
||||
import { Ref, UnwrapNestedRefs } from "vue";
|
||||
import type { ContainerJson, ContainerStat } from "@/types/Container";
|
||||
import type { ContainerHealth, ContainerJson, ContainerStat } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
export const useContainerStore = defineStore("container", () => {
|
||||
const containers: Ref<Container[]> = ref([]);
|
||||
const activeContainerIds: Ref<string[]> = ref([]);
|
||||
let es: EventSource | null = null;
|
||||
const ready = ref(false);
|
||||
|
||||
const allContainersById = computed(() =>
|
||||
containers.value.reduce((acc, container) => {
|
||||
@@ -21,25 +23,48 @@ export const useContainerStore = defineStore("container", () => {
|
||||
|
||||
const activeContainers = computed(() => activeContainerIds.value.map((id) => allContainersById.value[id]));
|
||||
|
||||
const es = new EventSource(`${config.base}/api/events/stream`);
|
||||
es.addEventListener("containers-changed", (e: Event) =>
|
||||
setContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
|
||||
watch(
|
||||
sessionHost,
|
||||
() => {
|
||||
connect();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
es.addEventListener("container-stat", (e) => {
|
||||
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
|
||||
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
|
||||
if (container) {
|
||||
const { id, ...rest } = stat;
|
||||
container.stat = rest;
|
||||
}
|
||||
});
|
||||
es.addEventListener("container-die", (e) => {
|
||||
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
|
||||
const container = allContainersById.value[event.actorId];
|
||||
if (container) {
|
||||
container.state = "dead";
|
||||
}
|
||||
});
|
||||
|
||||
function connect() {
|
||||
es?.close();
|
||||
ready.value = false;
|
||||
es = new EventSource(`${config.base}/api/events/stream?host=${sessionHost.value}`);
|
||||
|
||||
es.addEventListener("containers-changed", (e: Event) =>
|
||||
setContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
|
||||
);
|
||||
es.addEventListener("container-stat", (e) => {
|
||||
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
|
||||
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
|
||||
if (container) {
|
||||
const { id, ...rest } = stat;
|
||||
container.stat = rest;
|
||||
}
|
||||
});
|
||||
es.addEventListener("container-die", (e) => {
|
||||
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
|
||||
const container = allContainersById.value[event.actorId];
|
||||
if (container) {
|
||||
container.state = "dead";
|
||||
}
|
||||
});
|
||||
|
||||
es.addEventListener("container-health", (e) => {
|
||||
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; health: ContainerHealth };
|
||||
const container = allContainersById.value[event.actorId];
|
||||
if (container) {
|
||||
container.health = event.health;
|
||||
}
|
||||
});
|
||||
|
||||
watchOnce(containers, () => (ready.value = true));
|
||||
}
|
||||
|
||||
const setContainers = (newContainers: ContainerJson[]) => {
|
||||
containers.value = newContainers.map((c) => {
|
||||
@@ -47,9 +72,10 @@ export const useContainerStore = defineStore("container", () => {
|
||||
if (existing) {
|
||||
existing.status = c.status;
|
||||
existing.state = c.state;
|
||||
existing.health = c.health;
|
||||
return existing;
|
||||
}
|
||||
return new Container(c.id, c.created, c.image, c.name, c.command, c.status, c.state);
|
||||
return new Container(c.id, new Date(c.created * 1000), c.image, c.name, c.command, c.status, c.state, c.health);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -58,9 +84,6 @@ export const useContainerStore = defineStore("container", () => {
|
||||
const removeActiveContainer = ({ id }: Container) =>
|
||||
activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1);
|
||||
|
||||
const ready = ref(false);
|
||||
watchOnce(containers, () => (ready.value = true));
|
||||
|
||||
return {
|
||||
containers,
|
||||
activeContainerIds,
|
||||
|
||||
+15
-1
@@ -34,7 +34,6 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
|
||||
@import "@oruga-ui/theme-bulma/dist/scss/components/button.scss";
|
||||
@import "@oruga-ui/theme-bulma/dist/scss/components/modal.scss";
|
||||
@import "@oruga-ui/theme-bulma/dist/scss/components/switch.scss";
|
||||
@import "@oruga-ui/theme-bulma/dist/scss/components/tooltip.scss";
|
||||
@import "@oruga-ui/theme-bulma/dist/scss/components/dropdown.scss";
|
||||
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
|
||||
@import "splitpanes/dist/splitpanes.css";
|
||||
@@ -97,6 +96,13 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
|
||||
--text-light-color: #{$grey};
|
||||
}
|
||||
|
||||
:root {
|
||||
--green-color: #00b5ad;
|
||||
--red-color: #f44336;
|
||||
--purple-color: #9c27b0;
|
||||
--orange-color: #ff9800;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@include dark;
|
||||
}
|
||||
@@ -177,6 +183,9 @@ html.has-custom-scrollbars {
|
||||
|
||||
.modal {
|
||||
z-index: 1000;
|
||||
.modal-background {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.button .button-wrapper > span {
|
||||
@@ -206,3 +215,8 @@ mark {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.button.is-rounded:hover {
|
||||
color: var(--text-strong-color);
|
||||
background: var(--scheme-main-ter);
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -13,6 +13,8 @@ export type ContainerJson = {
|
||||
readonly command: string;
|
||||
readonly status: string;
|
||||
readonly state: ContainerState;
|
||||
readonly health?: ContainerHealth;
|
||||
};
|
||||
|
||||
export type ContainerState = "created" | "running" | "exited" | "dead" | "paused" | "restarting";
|
||||
export type ContainerHealth = "healthy" | "unhealthy" | "starting";
|
||||
|
||||
+70
-3
@@ -5,6 +5,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -14,6 +18,7 @@ import (
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -35,7 +40,7 @@ type dockerProxy interface {
|
||||
type Client interface {
|
||||
ListContainers() ([]Container, error)
|
||||
FindContainer(string) (Container, error)
|
||||
ContainerLogs(context.Context, string, int, string) (io.ReadCloser, error)
|
||||
ContainerLogs(context.Context, string, string) (io.ReadCloser, error)
|
||||
Events(context.Context) (<-chan ContainerEvent, <-chan error)
|
||||
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time) (io.ReadCloser, error)
|
||||
ContainerStats(context.Context, string, chan<- ContainerStat) error
|
||||
@@ -62,6 +67,58 @@ func NewClientWithFilters(f map[string][]string) Client {
|
||||
return &dockerClient{cli, filterArgs}
|
||||
}
|
||||
|
||||
func NewClientWithTlsAndFilter(f map[string][]string, connection string) Client {
|
||||
filterArgs := filters.NewArgs()
|
||||
for key, values := range f {
|
||||
for _, value := range values {
|
||||
filterArgs.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("filterArgs = %v", filterArgs)
|
||||
|
||||
remoteUrl, err := url.Parse(connection)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if remoteUrl.Scheme != "tcp" {
|
||||
log.Fatal("Only tcp scheme is supported")
|
||||
}
|
||||
|
||||
host := remoteUrl.Hostname()
|
||||
basePath := "/certs"
|
||||
|
||||
if _, err := os.Stat(filepath.Join(basePath, host)); !os.IsNotExist(err) {
|
||||
basePath = filepath.Join(basePath, host)
|
||||
}
|
||||
|
||||
cacertPath := filepath.Join(basePath, "ca.pem")
|
||||
certPath := filepath.Join(basePath, "cert.pem")
|
||||
keyPath := filepath.Join(basePath, "key.pem")
|
||||
|
||||
opts := []client.Opt{
|
||||
client.WithHost(connection),
|
||||
}
|
||||
|
||||
if _, err := os.Stat(cacertPath); os.IsNotExist(err) {
|
||||
log.Debugf("%s does not exist, using plain HTTP", cacertPath)
|
||||
} else {
|
||||
log.Debugf("Using TLS client config with certs at: %s", basePath)
|
||||
opts = append(opts, client.WithTLSClientConfig(cacertPath, certPath, keyPath))
|
||||
}
|
||||
|
||||
opts = append(opts, client.WithAPIVersionNegotiation())
|
||||
|
||||
cli, err := client.NewClientWithOpts(opts...)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return &dockerClient{cli, filterArgs}
|
||||
}
|
||||
|
||||
func (d *dockerClient) FindContainer(id string) (Container, error) {
|
||||
var container Container
|
||||
containers, err := d.ListContainers()
|
||||
@@ -106,6 +163,7 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
Created: c.Created,
|
||||
State: c.State,
|
||||
Status: c.Status,
|
||||
Health: findBetweenParentheses(c.Status),
|
||||
}
|
||||
containers = append(containers, container)
|
||||
}
|
||||
@@ -169,7 +227,7 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int, since string) (io.ReadCloser, error) {
|
||||
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string) (io.ReadCloser, error) {
|
||||
log.WithField("id", id).WithField("since", since).Debug("streaming logs for container")
|
||||
|
||||
if since != "" {
|
||||
@@ -184,7 +242,7 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
Tail: strconv.Itoa(tailSize),
|
||||
Tail: "300",
|
||||
Timestamps: true,
|
||||
Since: since,
|
||||
}
|
||||
@@ -260,3 +318,12 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
|
||||
func (d *dockerClient) Ping(ctx context.Context) (types.Ping, error) {
|
||||
return d.cli.Ping(ctx)
|
||||
}
|
||||
|
||||
var PARENTHESIS_RE = regexp.MustCompile(`\(([a-zA-Z]+)\)`)
|
||||
|
||||
func findBetweenParentheses(s string) string {
|
||||
if results := PARENTHESIS_RE.FindStringSubmatch(s); results != nil {
|
||||
return results[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -125,7 +125,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
||||
binary.BigEndian.PutUint32(b[4:], uint32(len(expected)))
|
||||
b = append(b, []byte(expected)...)
|
||||
|
||||
reader := ioutil.NopCloser(bytes.NewReader(b))
|
||||
reader := io.NopCloser(bytes.NewReader(b))
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
@@ -133,9 +133,9 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
||||
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, 300, "since")
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, "since")
|
||||
|
||||
actual, _ := ioutil.ReadAll(logReader)
|
||||
actual, _ := io.ReadAll(logReader)
|
||||
assert.Equal(t, expected, string(actual), "message doesn't match expected")
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
expected := "INFO Testing logs..."
|
||||
|
||||
reader := ioutil.NopCloser(strings.NewReader(expected))
|
||||
reader := io.NopCloser(strings.NewReader(expected))
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
@@ -154,9 +154,9 @@ func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
|
||||
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, 300, "")
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, "")
|
||||
|
||||
actual, _ := ioutil.ReadAll(logReader)
|
||||
actual, _ := io.ReadAll(logReader)
|
||||
assert.Equal(t, expected, string(actual), "message doesn't match expected")
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
@@ -170,7 +170,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
reader, err := client.ContainerLogs(context.Background(), id, 300, "")
|
||||
reader, err := client.ContainerLogs(context.Background(), id, "")
|
||||
|
||||
assert.Nil(t, reader, "reader should be nil")
|
||||
assert.Error(t, err, "error should have been returned")
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"hash/fnv"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type eventGenerator struct {
|
||||
reader *bufio.Reader
|
||||
channel chan *LogEvent
|
||||
next *LogEvent
|
||||
lastError error
|
||||
}
|
||||
|
||||
func NewEventIterator(reader *bufio.Reader) *eventGenerator {
|
||||
generator := &eventGenerator{reader: reader, channel: make(chan *LogEvent, 100)}
|
||||
go generator.consume()
|
||||
return generator
|
||||
}
|
||||
|
||||
func (g *eventGenerator) Next() (*LogEvent, error) {
|
||||
var currentEvent *LogEvent
|
||||
var nextEvent *LogEvent
|
||||
if g.next != nil {
|
||||
currentEvent = g.next
|
||||
g.next = nil
|
||||
nextEvent = g.Peek()
|
||||
} else {
|
||||
event, ok := <-g.channel
|
||||
if !ok {
|
||||
return nil, g.lastError
|
||||
}
|
||||
|
||||
currentEvent = event
|
||||
nextEvent = g.Peek()
|
||||
}
|
||||
|
||||
currentLevel := guessLogLevel(currentEvent)
|
||||
|
||||
if nextEvent != nil {
|
||||
if currentEvent.IsCloseToTime(nextEvent) && currentLevel != "" && !nextEvent.HasLevel() {
|
||||
currentEvent.Position = START
|
||||
nextEvent.Position = MIDDLE
|
||||
}
|
||||
|
||||
// If next item is not close to current item or has level, set current item position to end
|
||||
if currentEvent.Position == MIDDLE && (nextEvent.HasLevel() || !currentEvent.IsCloseToTime(nextEvent)) {
|
||||
currentEvent.Position = END
|
||||
}
|
||||
|
||||
// If next item is close to current item and has no level, set next item position to middle
|
||||
if currentEvent.Position == MIDDLE && !nextEvent.HasLevel() && currentEvent.IsCloseToTime(nextEvent) {
|
||||
nextEvent.Position = MIDDLE
|
||||
}
|
||||
// Set next item level to current item level
|
||||
if currentEvent.Position == START || currentEvent.Position == MIDDLE {
|
||||
nextEvent.Level = currentEvent.Level
|
||||
}
|
||||
} else if currentEvent.Position == MIDDLE {
|
||||
currentEvent.Position = END
|
||||
}
|
||||
|
||||
return currentEvent, nil
|
||||
}
|
||||
|
||||
func (g *eventGenerator) LastError() error {
|
||||
return g.lastError
|
||||
}
|
||||
|
||||
func (g *eventGenerator) Peek() *LogEvent {
|
||||
if g.next != nil {
|
||||
return g.next
|
||||
}
|
||||
select {
|
||||
case event := <-g.channel:
|
||||
g.next = event
|
||||
return g.next
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *eventGenerator) consume() {
|
||||
for {
|
||||
message, readerError := g.reader.ReadString('\n')
|
||||
|
||||
if message != "" {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(message))
|
||||
|
||||
logEvent := &LogEvent{Id: h.Sum32(), Message: message}
|
||||
|
||||
if index := strings.IndexAny(message, " "); index != -1 {
|
||||
logId := message[:index]
|
||||
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
|
||||
logEvent.Timestamp = timestamp.UnixMilli()
|
||||
message = strings.TrimSuffix(message[index+1:], "\n")
|
||||
logEvent.Message = message
|
||||
if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") {
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(message), &data); err != nil {
|
||||
log.Errorf("json unmarshal error while streaming %v", err.Error())
|
||||
} else {
|
||||
logEvent.Message = data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logEvent.Level = guessLogLevel(logEvent)
|
||||
g.channel <- logEvent
|
||||
}
|
||||
|
||||
if readerError != nil {
|
||||
g.lastError = readerError
|
||||
close(g.channel)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var NON_ASCII_REGEX = regexp.MustCompile("^[^a-zA-z ]+[^ewidtf]?")
|
||||
var KEY_VALUE_REGEX = regexp.MustCompile(`level=(\w+)`)
|
||||
|
||||
func guessLogLevel(logEvent *LogEvent) string {
|
||||
switch value := logEvent.Message.(type) {
|
||||
case string:
|
||||
levels := []string{"error", "warn", "info", "debug", "trace", "fatal"}
|
||||
stripped := NON_ASCII_REGEX.ReplaceAllString(value, "")
|
||||
for _, level := range levels {
|
||||
if match, _ := regexp.MatchString("(?i)^"+level+"[^a-z]", stripped); match {
|
||||
return level
|
||||
}
|
||||
|
||||
if strings.Contains(value, "["+strings.ToUpper(level)+"]") {
|
||||
return level
|
||||
}
|
||||
|
||||
if strings.Contains(value, " "+strings.ToUpper(level)+" ") {
|
||||
return level
|
||||
}
|
||||
}
|
||||
|
||||
if matches := KEY_VALUE_REGEX.FindStringSubmatch(value); matches != nil {
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
case map[string]interface{}:
|
||||
if level, ok := value["level"].(string); ok {
|
||||
return level
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewEventIterator(t *testing.T) {
|
||||
input := "example input"
|
||||
reader := bufio.NewReader(strings.NewReader(input))
|
||||
|
||||
generator := NewEventIterator(reader)
|
||||
require.NotNil(t, generator, "Expected generator to not be nil, but got nil")
|
||||
}
|
||||
|
||||
func TestEventGenerator_Next(t *testing.T) {
|
||||
input := "example input"
|
||||
reader := bufio.NewReader(strings.NewReader(input))
|
||||
|
||||
generator := NewEventIterator(reader)
|
||||
|
||||
event, err := generator.Next()
|
||||
require.NoError(t, err, "Expected no error, but got: %v", err)
|
||||
require.NotNil(t, event, "Expected event to not be nil, but got nil")
|
||||
}
|
||||
|
||||
func TestEventGenerator_LastError(t *testing.T) {
|
||||
input := "example input"
|
||||
reader := bufio.NewReader(strings.NewReader(input))
|
||||
|
||||
generator := NewEventIterator(reader)
|
||||
|
||||
require.Nil(t, generator.LastError(), "Expected LastError to return nil, but got: %v", generator.LastError())
|
||||
|
||||
generator.Next()
|
||||
|
||||
// expert error to be EOF
|
||||
assert.Equal(t, generator.LastError(), io.EOF, "Expected LastError to return EOF, but got: %v", generator.LastError().Error())
|
||||
}
|
||||
|
||||
func TestEventGenerator_Peek(t *testing.T) {
|
||||
input := "example input"
|
||||
reader := bufio.NewReader(strings.NewReader(input))
|
||||
|
||||
generator := NewEventIterator(reader)
|
||||
|
||||
event := generator.Peek()
|
||||
|
||||
require.NotNil(t, event, "Expected event to not be nil, but got nil")
|
||||
assert.Equal(t, event.Message, input, "Expected event message to be %s, but got: %s", input, event.Message.(string))
|
||||
}
|
||||
+24
-3
@@ -1,5 +1,7 @@
|
||||
package docker
|
||||
|
||||
import "math"
|
||||
|
||||
// Container represents an internal representation of docker containers
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
@@ -11,6 +13,7 @@ type Container struct {
|
||||
Created int64 `json:"created"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Health string `json:"health,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerStat represent stats instant for a container
|
||||
@@ -27,8 +30,26 @@ type ContainerEvent struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type LogPosition string
|
||||
|
||||
const (
|
||||
START LogPosition = "start"
|
||||
MIDDLE LogPosition = "middle"
|
||||
END LogPosition = "end"
|
||||
)
|
||||
|
||||
type LogEvent struct {
|
||||
Message any `json:"m,omitempty"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Id uint32 `json:"id,omitempty"`
|
||||
Message any `json:"m,omitempty"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Id uint32 `json:"id,omitempty"`
|
||||
Level string `json:"l,omitempty"`
|
||||
Position LogPosition `json:"p,omitempty"`
|
||||
}
|
||||
|
||||
func (l *LogEvent) HasLevel() bool {
|
||||
return l.Level != ""
|
||||
}
|
||||
|
||||
func (l *LogEvent) IsCloseToTime(other *LogEvent) bool {
|
||||
return math.Abs(float64(l.Timestamp-other.Timestamp)) < 10
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
.vite-ssg-dist
|
||||
.vite-ssg-temp
|
||||
*.local
|
||||
dist
|
||||
dist-ssr
|
||||
node_modules
|
||||
.idea/
|
||||
*.log
|
||||
!.vscode
|
||||
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
cache
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createRequire } from "module";
|
||||
import { defineConfig } from "vitepress";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { SitemapStream, streamToPromise } from "sitemap";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkg = require("dozzle/package.json");
|
||||
|
||||
const links = [] as { url: string; lastmod?: number }[];
|
||||
|
||||
export default defineConfig({
|
||||
lang: "en-US",
|
||||
title: "Dozzle",
|
||||
description: "A lightweight, open-source, and secure log viewer for Docker.",
|
||||
|
||||
lastUpdated: true,
|
||||
cleanUrls: true,
|
||||
|
||||
head: [
|
||||
[
|
||||
"script",
|
||||
{
|
||||
async: "",
|
||||
src: "https://www.googletagmanager.com/gtag/js?id=G-X3Z4496XFK",
|
||||
},
|
||||
],
|
||||
[
|
||||
"script",
|
||||
{},
|
||||
`function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-X3Z4496XFK")`,
|
||||
],
|
||||
],
|
||||
themeConfig: {
|
||||
editLink: {
|
||||
pattern: "https://github.com/amir20/dozzle/edit/master/docs/:path",
|
||||
},
|
||||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "Guide", link: "/guide/what-is-dozzle", activeMatch: "/guide/" },
|
||||
{
|
||||
text: `v${pkg.version}`,
|
||||
items: [
|
||||
{
|
||||
text: "Releases",
|
||||
link: "https://github.com/amir20/dozzle/releases",
|
||||
},
|
||||
{
|
||||
text: "New Issue",
|
||||
link: "https://github.com/amir20/dozzle/issues/new/choose",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
text: "Introduction",
|
||||
items: [
|
||||
{ text: "What is Dozzle?", link: "/guide/what-is-dozzle" },
|
||||
{ text: "Getting Started", link: "/guide/getting-started" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Advanced Configuration",
|
||||
items: [
|
||||
{ text: "Authentication", link: "/guide/authentication" },
|
||||
{ text: "Healthcheck", link: "/guide/healthcheck" },
|
||||
{ text: "Remote Hosts", link: "/guide/remote-hosts" },
|
||||
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
|
||||
{ text: "Analytics", link: "/guide/analytics" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
items: [
|
||||
{ text: "FAQ", link: "/guide/faq" },
|
||||
{ text: "Debugging", link: "/guide/debugging" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: "Released under the MIT License. Open sourced and sponsored by Docker OSS.",
|
||||
copyright: "Copyright © 2019-present <a href='https://amirraminfar.me'>Amir Raminfar</a>",
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/amir20/dozzle" },
|
||||
{
|
||||
icon: {
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M21.81 10.25c-.06-.04-.56-.43-1.64-.43c-.28 0-.56.03-.84.08c-.21-1.4-1.38-2.11-1.43-2.14l-.29-.17l-.18.27c-.24.36-.43.77-.51 1.19c-.2.8-.08 1.56.33 2.21c-.49.28-1.29.35-1.46.35H2.62c-.34 0-.62.28-.62.63c0 1.15.18 2.3.58 3.38c.45 1.19 1.13 2.07 2 2.61c.98.6 2.59.94 4.42.94c.79 0 1.61-.07 2.42-.22c1.12-.2 2.2-.59 3.19-1.16A8.3 8.3 0 0 0 16.78 16c1.05-1.17 1.67-2.5 2.12-3.65h.19c1.14 0 1.85-.46 2.24-.85c.26-.24.45-.53.59-.87l.08-.24l-.19-.14m-17.96.99h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H3.85c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.43 0h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H6.28c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.47 0h1.75c.1 0 .17-.07.17-.16V9.5c0-.08-.06-.16-.17-.16H8.75c-.08 0-.15.07-.15.16v1.58c0 .09.06.16.15.16m2.44 0h1.77c.08 0 .15-.07.15-.16V9.5c0-.08-.06-.16-.15-.16h-1.77c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16M6.28 9h1.76c.08 0 .16-.09.16-.18V7.25c0-.09-.07-.16-.16-.16H6.28c-.09 0-.16.06-.16.16v1.57c.01.09.07.18.16.18m2.47 0h1.75c.1 0 .17-.09.17-.18V7.25c0-.09-.06-.16-.17-.16H8.75c-.08 0-.15.06-.15.16v1.57c0 .09.06.18.15.18m2.44 0h1.77c.08 0 .15-.09.15-.18V7.25c0-.09-.07-.16-.15-.16h-1.77c-.08 0-.15.06-.15.16v1.57c0 .09.07.18.15.18m0-2.28h1.77c.08 0 .15-.07.15-.16V5c0-.1-.07-.17-.15-.17h-1.77c-.08 0-.15.06-.15.17v1.56c0 .08.07.16.15.16m2.46 4.52h1.76c.09 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16h-1.76c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16"/></svg>`,
|
||||
},
|
||||
link: "https://hub.docker.com/r/amir20/dozzle",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
transformHtml: (_, id, { pageData }) => {
|
||||
if (!/[\\/]404\.html$/.test(id))
|
||||
links.push({
|
||||
// you might need to change this if not using clean urls mode
|
||||
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2"),
|
||||
lastmod: pageData.lastUpdated,
|
||||
});
|
||||
},
|
||||
|
||||
buildEnd: async ({ outDir }) => {
|
||||
const sitemap = new SitemapStream({
|
||||
hostname: "https://dozzle.dev/",
|
||||
});
|
||||
const writeStream = createWriteStream(resolve(outDir, "sitemap.xml"));
|
||||
sitemap.pipe(writeStream);
|
||||
links.forEach((link) => sitemap.write(link));
|
||||
const promise = streamToPromise(sitemap);
|
||||
sitemap.end();
|
||||
await promise;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<div border="~ solid rounded light-100 dark:dark-50" bg="[#eee] dark:[#222]">
|
||||
<div flex gap="2" p-2>
|
||||
<div circle-red />
|
||||
<div circle-yellow />
|
||||
<div circle-green />
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMutationObserver } from "@vueuse/core";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const isDark = ref(false);
|
||||
onMounted(() => {
|
||||
isDark.value = document.documentElement.classList.contains("dark");
|
||||
useMutationObserver(
|
||||
document.documentElement,
|
||||
(mutations) => {
|
||||
isDark.value = document.documentElement.classList.contains("dark");
|
||||
},
|
||||
{
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<browser-window drop-shadow-md>
|
||||
<video muted loop autoplay playsinline poster="../media/poster.png" v-if="isDark">
|
||||
<source src="../media/dozzle-dark.webm" type="video/webm" />
|
||||
<source src="../media/dozzle-dark.mp4" type="video/mp4" />
|
||||
<img src="../media/poster.png" alt="" />
|
||||
</video>
|
||||
<video muted loop autoplay playsinline poster="../media/poster.png" v-else>
|
||||
<source src="../media/dozzle-light.webm" type="video/webm" />
|
||||
<source src="../media/dozzle-light.mp4" type="video/mp4" />
|
||||
<img src="../media/poster.png" alt="" />
|
||||
</video>
|
||||
</browser-window>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,16 @@
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import { h } from "vue";
|
||||
import Theme from "vitepress/theme";
|
||||
import "uno.css";
|
||||
import "./style.css";
|
||||
import HeroVideo from "./components/HeroVideo.vue";
|
||||
|
||||
export default {
|
||||
...Theme,
|
||||
Layout: () => {
|
||||
return h(Theme.Layout, null, {
|
||||
"home-hero-image": () => h(HeroVideo),
|
||||
});
|
||||
},
|
||||
enhanceApp({ app, router, siteData }) {},
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 440 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 742 KiB |
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-brand: #646cff;
|
||||
--vp-c-brand-light: #747bff;
|
||||
--vp-c-brand-lighter: #9499ff;
|
||||
--vp-c-brand-lightest: #bcc0ff;
|
||||
--vp-c-brand-dark: #535bf2;
|
||||
--vp-c-brand-darker: #454ce1;
|
||||
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand);
|
||||
--vp-button-brand-hover-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
|
||||
--vp-button-brand-active-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: linear-gradient(124deg, #de4daa 30%, #f6d327);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(-45deg, hsl(44, 100%, 77%) 50%, #47caff 50%);
|
||||
--vp-home-hero-image-filter: blur(40px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(72px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||
--vp-custom-block-tip-text: var(--vp-c-brand-darker);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||
--tag-line-color: #333;
|
||||
--bar-title-color: #ef5734;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||
--vp-custom-block-tip-text: var(--vp-c-brand-lightest);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||
--tag-line-color: white;
|
||||
--bar-title-color: hsl(44, 100%, 77%);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand) !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* VitePress: Custom fix
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
Use lighter colors for links in dark mode for a11y.
|
||||
Also specify some classes twice to have higher specificity
|
||||
over scoped class data attribute.
|
||||
*/
|
||||
.dark .vp-doc a,
|
||||
.dark .vp-doc a > code,
|
||||
.dark .VPNavBarMenuLink.VPNavBarMenuLink:hover,
|
||||
.dark .VPNavBarMenuLink.VPNavBarMenuLink.active,
|
||||
.dark .link.link:hover,
|
||||
.dark .link.link.active,
|
||||
.dark .edit-link-button.edit-link-button,
|
||||
.dark .pager-link .title {
|
||||
color: var(--vp-c-brand-lighter);
|
||||
}
|
||||
|
||||
.dark .vp-doc a:hover,
|
||||
.dark .vp-doc a > code:hover {
|
||||
color: var(--vp-c-brand-lightest);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Transition by color instead of opacity */
|
||||
.dark .vp-doc .custom-block a {
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dozzle css
|
||||
* --------------------------------------------------------------------------
|
||||
**/
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.is-home .VPHero {
|
||||
margin-top: -1em;
|
||||
}
|
||||
}
|
||||
|
||||
.VPHero .container {
|
||||
max-width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.is-home .container .main {
|
||||
padding-right: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.VPHomeHero .tagline {
|
||||
@apply font-sans text-4xl font-light;
|
||||
color: var(--tag-line-color);
|
||||
}
|
||||
|
||||
.VPHomeHero .name {
|
||||
@apply font-playfair font-light text-7xl;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
@apply font-playfair font-light text-3xl;
|
||||
color: var(--bar-title-color);
|
||||
}
|
||||
|
||||
.VPHero .container .image {
|
||||
max-width: 1200px;
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
BrowserWindow: typeof import('./.vitepress/theme/components/BrowserWindow.vue')['default']
|
||||
HeroVideo: typeof import('./.vitepress/theme/components/HeroVideo.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Handler, HandlerEvent, HandlerContext } from "@netlify/functions";
|
||||
|
||||
let cache;
|
||||
let cacheTime = 0;
|
||||
|
||||
const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
|
||||
if (cache && Date.now() - cacheTime < 1000 * 60 * 10) {
|
||||
const headers = {
|
||||
"x-cache": "HIT",
|
||||
};
|
||||
return {
|
||||
headers,
|
||||
statusCode: 200,
|
||||
body: JSON.stringify(cache),
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch("https://hub.docker.com/v2/repositories/amir20/dozzle");
|
||||
const data = await response.json();
|
||||
const { full_description, ...rest } = data;
|
||||
cache = rest;
|
||||
cacheTime = Date.now();
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify(rest),
|
||||
};
|
||||
};
|
||||
|
||||
export { handler };
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Anonymous Analytics
|
||||
---
|
||||
|
||||
# Data Collection of Analytics
|
||||
|
||||
Dozzle collects anonymous user configurations using Google Analytics. _Why?_ Dozzle is an open source project with no funding. As a result, there is no time to do user studies of Dozzle. Analytics is collected to prioritize features and fixes based on how people use Dozzle. This data is completely public and can be viewed live using [dashboard](/guide/usage-dashboard).
|
||||
|
||||
## Where is data stored
|
||||
|
||||
Dozzle's sends anonymous data to Google Analytics. Retention policy is 14 months which is lowest [possible value for Google Analytics](https://support.google.com/analytics/answer/7667196).
|
||||
|
||||
## Opting out
|
||||
|
||||
Dozzle analytics helps to prioritize features and spend time on most important features. If you do not want to be tracked at all, use `--no-analytics` flag or `DOZZLE_NO_ANALYTICS` env variable.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Authentication
|
||||
---
|
||||
|
||||
# Setting Up Authentication
|
||||
|
||||
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use docker secrets `--usernamefile` and `--passwordfile`.
|
||||
|
||||
::: code-group
|
||||
|
||||
```sh [cli]
|
||||
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --username amirraminfar --password supersecretpassword
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_USERNAME: amirraminfar
|
||||
DOZZLE_PASSWORD: supersecretpassword
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Setting up authentication with Docker secrets
|
||||
|
||||
Dozzle also support path to file for username and password which can be used to with Docker Secrets.
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
environment:
|
||||
DOZZLE_LEVEL: debug
|
||||
DOZZLE_USERNAME_FILE: /run/secrets/dozzle_user
|
||||
DOZZLE_PASSWORD_FILE: /run/secrets/dozzle_password
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
secrets:
|
||||
- dozzle_user
|
||||
- dozzle_password
|
||||
ports:
|
||||
- 8080:8080
|
||||
|
||||
secrets:
|
||||
dozzle_user:
|
||||
file: dozzle_user.txt
|
||||
dozzle_password:
|
||||
file: dozzle_password.txt
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Debugging
|
||||
---
|
||||
|
||||
# Debugging with logs
|
||||
|
||||
By default, Dozzle does not output a lot of logs. However this can be changed with `--level` flag. The default value is `info` which only prints limited logs. You can use `debug` or `trace` which will show details about memory, configuration and other stats. `DOZZLE_LEVEL` can be used in compose configurations. Below is an example of using `docker-compose.yml` file to enable `debug` level.
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_LEVEL: debug
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: FAQ
|
||||
---
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
## I installed Dozzle, but logs are slow or they never load. What do I do?
|
||||
|
||||
Dozzle uses Server Sent Events (SSE) which connects to a server using a HTTP stream without closing the connection. If any proxy tries to buffer this connection, then Dozzle never receives the data and hangs forever waiting for the reverse proxy to flush the buffer. Since version `1.23.0`, Dozzle sends the `X-Accel-Buffering: no` header which should stop reverse proxies buffering. However, some proxies may ignore this header. In those cases, you need to explicitly disable any buffering.
|
||||
|
||||
Below is an example with nginx and using `proxy_pass` to disable buffering:
|
||||
|
||||
```
|
||||
server {
|
||||
...
|
||||
|
||||
location / {
|
||||
proxy_pass http://<dozzle.container.ip.address>:8080;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://<dozzle.container.ip.address>:8080;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## We have tools that uses Dozzle when a new container is created. How can I get a direct link to a container by name?
|
||||
|
||||
Dozzle has a special [route](https://github.com/amir20/dozzle/blob/master/assets/pages/show.vue) that can be used to search containers by name and then forward to that container. For example, if you have a container with name `"foo.bar"` and id `abc123`, you can send your users to `/show?name=foo.bar` which will be forwarded to `/container/abc123`.
|
||||
|
||||
## I installed Dozzle but memory consumption doesn't show up!
|
||||
|
||||
_This is an issue specific to ARM devices._
|
||||
|
||||
Dozzle uses the Docker API to gather information about the containers' memory usage. If the memory usage is not showing up, then it is likely that the Docker API is not returning the memory usage.
|
||||
|
||||
You can verify this by running docker info, and you should see the following:
|
||||
|
||||
```
|
||||
WARNING: No memory limit support
|
||||
WARNING: No swap limit support
|
||||
```
|
||||
|
||||
In this case, you'll need to add the following line to your `/boot/cmdline.txt` file and reboot your device.
|
||||
|
||||
```
|
||||
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Getting Started
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
||||
This section will help you to setup Dozzle locally. Dozzle can also be used to connect to remote hosts via `tcp://` and tls. See remote host if you want to connect to other hosts.
|
||||
|
||||
## Using Docker CLI
|
||||
|
||||
The easiest way to setup Dozzle is to use the CLI and mount `docker.sock` file. This file is usually located at `/var/run/docker.sock` and can be mounted with the `--volume` flag. You also need to expose the port to view Dozzle. By default, Dozzle listens on port 8080, but you can change the external port using `-p`.
|
||||
|
||||
```sh
|
||||
docker run --detach --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle
|
||||
```
|
||||
|
||||
## Using Docker Compose
|
||||
|
||||
Docker compose makes it easier to configure Dozzle as part of an existing configuration.
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
container_name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 9999:8080
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Healthcheck
|
||||
---
|
||||
|
||||
# Adding healthcheck
|
||||
|
||||
Dozzle has internal support for healthcheck using `dozzle healthcheck` command. It is not enabled by default as it adds extra CPU usage. To use `healthcheck` you need to configure it. Below is an example that checks the health of Dozzle every 3 seconds.
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8080:8080
|
||||
healthcheck:
|
||||
test: ["CMD", "/dozzle", "healthcheck"]
|
||||
interval: 3s
|
||||
timeout: 30s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Remote Host Setup
|
||||
---
|
||||
|
||||
# Remote Host Setup
|
||||
|
||||
Dozzle supports connecting to multiple remote hosts via `tcp://` using TLS and non-secured connections. Dozzle will need to have appropriate certs mounted to use secured connection. `ssh://` is not supported because Dozzle docker image does not ship with any ssh clients.
|
||||
|
||||
## Connecting to remote hosts
|
||||
|
||||
Remote hosts can be configured with `--remote-host` or `DOZZLE_REMOTE_HOST`. All certs must be mounted to `/certs` directory. The `/cert` directory expects to have `/certs/{ca,cert,key}.pem` or `/certs/{host}/{ca,cert,key}.pem` in case of multiple hosts.
|
||||
|
||||
Multiple `--remote-host` flags can be used to specify multiple hosts. However, using `DOZZLE_REMOTE_HOST` the value should be comma separated.
|
||||
|
||||
::: code-group
|
||||
|
||||
```sh [cli]
|
||||
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/certs:/certs -p 8080:8080 amir20/dozzle --remote-host tcp://167.99.1.1:2376 --remote-host tcp://167.99.1.2:2376
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /path/to/certs:/certs
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_REMOTE_HOST: tcp://167.99.1.1:2376,tcp://167.99.1.2:2376
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Connecting with a socket proxy
|
||||
|
||||
If you are in a private network then you can use [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy) which expose `docker.sock` file without the need of TLS. Dozzle will never try to write to Docker but it will need access to list APIs. The following command will start a proxy with minimal access.
|
||||
|
||||
```sh
|
||||
docker container run --privileged -e CONTAINERS=1 -v /var/run/docker.sock:/var/run/docker.sock -p 2375:2375 tecnativa/docker-socket-proxy
|
||||
```
|
||||
|
||||
Note that `CONTAINERS=1` is required to list running containers. `EVENTS` is also needed but it is enabled by default.
|
||||
|
||||
Running Dozzle without any certs should work. Here is an example:
|
||||
|
||||
```sh
|
||||
docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --remote-host tcp://123.1.1.1:2375
|
||||
```
|
||||
|
||||
::: warning
|
||||
Exposing `docker.sock` publicly is not safe. Only use a proxy for an internal network where all clients are trusted.
|
||||
:::
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Environment variables and configuration
|
||||
---
|
||||
|
||||
# Environment variables and configuration
|
||||
|
||||
Configurations can be done with flags or environment variables. The table below outlines all supported options and their respective env vars.
|
||||
|
||||
| Flag | Env Variable | Default |
|
||||
| ---------------- | ---------------------- | ------- |
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
| `--username` | `DOZZLE_USERNAME` | `""` |
|
||||
| `--password` | `DOZZLE_PASSWORD` | `""` |
|
||||
| `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` |
|
||||
| `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
|
||||
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Usage Dashboard
|
||||
---
|
||||
|
||||
# Usage Dashboard
|
||||
|
||||
This data is based on collected data which is completely anonymous.
|
||||
|
||||
<iframe width="1024" height="800" src="https://lookerstudio.google.com/embed/reporting/50f34d3a-3c7e-47b2-a85e-09da19e2a666/page/GbvFC" frameborder="0" style="border:0" allowfullscreen></iframe>
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: What is Dozzle?
|
||||
---
|
||||
|
||||
# What is Dozzle?
|
||||
|
||||
Dozzle is open-sourced project sponsored by Docker OSS. It is a log viewer designed to simplify the process of monitoring and debugging containers. It is a lightweight, web-based application that provides real-time log streaming, filtering, and searching capabilities through an intuitive user interface.
|
||||
|
||||
Users can quickly and easily access logs generated by their Docker containers, making it an essential tool for debugging and troubleshooting applications running in a Docker environment. Out of the box, Dozzle supports JSON logs with intelligent color coding.
|
||||
|
||||
Dozzle is easy to install and can be configured with minimal effort, making it an ideal solution for developers and system administrators looking for an efficient and user-friendly log viewer for their Docker environment. The tool is available under the MIT license and is actively maintained by its developer, Amir Raminfar.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
title: Home
|
||||
|
||||
hero:
|
||||
name: "Dozzle"
|
||||
tagline: A lightweight, web-based Docker log viewer that provides real-time monitoring and easy troubleshooting.
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /guide/getting-started
|
||||
|
||||
features:
|
||||
- title: Real-time logging
|
||||
details: Allows you to view logs of other Docker containers in real-time. As new log entries are generated, they are streamed to the web interface without needing to refresh the page.
|
||||
- title: Lightweight
|
||||
details: An application written in Go consuming very little memory and CPU. It can be run alongside other containers without causing performance issues.
|
||||
- title: Multi-host Support
|
||||
details: Dozzle UI support connecting to multiple remote hosts with a simple drop down to choose between different hosts.
|
||||
---
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@netlify/functions": "^1.4.0",
|
||||
"@unocss/preset-typography": "^0.51.8",
|
||||
"@unocss/reset": "^0.51.8",
|
||||
"@unocss/transformer-directives": "^0.51.8",
|
||||
"dozzle": "workspace:*",
|
||||
"sitemap": "^7.1.1",
|
||||
"unocss": "^0.51.8"
|
||||
}
|
||||
}
|
||||
Generated
+1129
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user