Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3a8c9cbcfa | ||
|
|
8586fee5ab | ||
|
|
8ccae473ed | ||
|
|
02434afef3 | ||
|
|
192ee98dd1 | ||
|
|
79e9dc7be6 | ||
|
|
19fe7e8d18 | ||
|
|
e4ecfca5be | ||
|
|
f37925b637 | ||
|
|
822b783063 | ||
|
|
7dcf1d4f15 | ||
|
|
d0af303d6f | ||
|
|
fd31d394a5 | ||
|
|
1c0af19c88 | ||
|
|
9d77613ee9 | ||
|
|
62f747797c | ||
|
|
fccb7fc2d4 | ||
|
|
7fecbdd000 | ||
|
|
46a2f2b810 | ||
|
|
e9dcdda64d | ||
|
|
67a0644c37 | ||
|
|
21fc2ce2fd | ||
|
|
20425bf6b1 | ||
|
|
828c288570 | ||
|
|
cde2589755 | ||
|
|
5fb2f452e2 | ||
|
|
6aea252d3e | ||
|
|
7337dcb5d4 | ||
|
|
deeb5fc100 | ||
|
|
5b15a0b29d | ||
|
|
e2ad2e0193 | ||
|
|
5b48426fc1 | ||
|
|
a24947ab3b | ||
|
|
8926b451d0 | ||
|
|
afd37d3455 | ||
|
|
4f84beb835 | ||
|
|
d54d894a66 | ||
|
|
7c854d31a7 | ||
|
|
4c2caad4a0 | ||
|
|
9684fd978b | ||
|
|
a79a3f680f | ||
|
|
1e18423b04 | ||
|
|
82b208c0bf | ||
|
|
8a3e9504a4 | ||
|
|
d8748c6e27 | ||
|
|
af8192fbc0 | ||
|
|
3492587d63 | ||
|
|
b8c522021d | ||
|
|
5850c319f2 |
2
.github/.kodiak.toml
vendored
2
.github/.kodiak.toml
vendored
@@ -1,2 +0,0 @@
|
||||
version = 1
|
||||
merge.notify_on_conflict = false
|
||||
47
.github/dependabot.yml
vendored
47
.github/dependabot.yml
vendored
@@ -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"
|
||||
12
.github/workflows/deploy.yml
vendored
12
.github/workflows/deploy.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 6.20.1
|
||||
- name: Install dependencies
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.19.x
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Go Tests with Coverage
|
||||
@@ -53,14 +53,14 @@ jobs:
|
||||
with:
|
||||
images: amir20/dozzle
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.0.0
|
||||
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.1.1
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 6.20.1
|
||||
- name: Install dependencies
|
||||
|
||||
8
.github/workflows/dev.yml
vendored
8
.github/workflows/dev.yml
vendored
@@ -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,14 +18,14 @@ jobs:
|
||||
with:
|
||||
images: amir20/dozzle
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.0.0
|
||||
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.1.1
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 6.20.1
|
||||
- name: Install dependencies
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.19.x
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Go Tests with Coverage
|
||||
@@ -44,15 +44,17 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.0.0
|
||||
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
|
||||
uses: docker/login-action@v2.1.0
|
||||
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
|
||||
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
|
||||
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'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:18-alpine as node
|
||||
FROM --platform=$BUILDPLATFORM node:19-alpine as node
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
@@ -8,11 +8,11 @@ WORKDIR /build
|
||||
|
||||
# Install dependencies from lock file
|
||||
COPY pnpm-lock.yaml ./
|
||||
RUN pnpm fetch --prod
|
||||
RUN pnpm fetch
|
||||
|
||||
# Copy package.json and install dependencies
|
||||
COPY package.json ./
|
||||
RUN pnpm install -r --offline --prod --ignore-scripts
|
||||
RUN pnpm install -r --offline --ignore-scripts
|
||||
|
||||
# Copy assets and translations to build
|
||||
COPY .* vite.config.ts index.html ./
|
||||
@@ -22,7 +22,7 @@ COPY locales ./locales
|
||||
# Build assets
|
||||
RUN pnpm build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.19.1-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.19.3-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache ca-certificates && mkdir /dozzle
|
||||
|
||||
|
||||
24
README.md
24
README.md
@@ -98,7 +98,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
|
||||
|
||||
@@ -129,6 +129,8 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
| `--username` | `DOZZLE_USERNAME` | `""` |
|
||||
| `--password` | `DOZZLE_PASSWORD` | `""` |
|
||||
| `--usernamefile` | `DOZZLE_USERNAME_FILE`| `""` |
|
||||
| `--passwordfile` | `DOZZLE_PASSWORD_FILE`| `""` |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
|
||||
|
||||
## Troubleshooting and FAQs
|
||||
@@ -181,6 +183,26 @@ 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)
|
||||
|
||||
14
assets/auto-imports.d.ts
vendored
14
assets/auto-imports.d.ts
vendored
@@ -14,6 +14,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']
|
||||
@@ -68,6 +69,8 @@ declare global {
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
@@ -100,6 +103,7 @@ declare global {
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveDirective: typeof import('vue')['resolveDirective']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const search: typeof import('./composables/settings')['search']
|
||||
@@ -207,6 +211,7 @@ declare global {
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useLogStream: typeof import('./composables/eventsource')['useLogStream']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
@@ -250,6 +255,7 @@ declare global {
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
@@ -307,7 +313,7 @@ declare global {
|
||||
}
|
||||
// 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')['$']>
|
||||
@@ -322,6 +328,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']>
|
||||
@@ -376,6 +383,8 @@ declare module '@vue/runtime-core' {
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||
@@ -408,6 +417,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
|
||||
@@ -515,6 +525,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
@@ -558,6 +569,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||
|
||||
3
assets/components.d.ts
vendored
3
assets/components.d.ts
vendored
@@ -29,6 +29,7 @@ declare module '@vue/runtime-core' {
|
||||
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']
|
||||
MdiLightLogout: typeof import('~icons/mdi-light/logout')['default']
|
||||
MdiLightMagnify: typeof import('~icons/mdi-light/magnify')['default']
|
||||
MobileMenu: typeof import('./components/MobileMenu.vue')['default']
|
||||
OcticonContainer24: typeof import('~icons/octicon/container24')['default']
|
||||
@@ -44,6 +45,8 @@ declare module '@vue/runtime-core' {
|
||||
SideMenu: typeof import('./components/SideMenu.vue')['default']
|
||||
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
|
||||
SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default']
|
||||
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
|
||||
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
|
||||
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
import { useFuse } from "@vueuse/integrations/useFuse";
|
||||
|
||||
const { maxResults: resultLimit = 20 } = defineProps<{
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<log-date :date="logEntry.date"></log-date>
|
||||
</div>
|
||||
<div class="column">
|
||||
<ul class="fields" @click="expanded = !expanded">
|
||||
<ul class="fields" :class="{ expanded }" @click="expanded = !expanded">
|
||||
<li v-for="(value, name) in validValues(logEntry.message)">
|
||||
<span class="has-text-grey">{{ name }}=</span>
|
||||
<span class="has-text-weight-bold" v-html="markSearch(value)"></span>
|
||||
@@ -47,6 +47,12 @@ function validValues(obj: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded:hover {
|
||||
&::after {
|
||||
content: "collapse json";
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
& + li {
|
||||
|
||||
@@ -1,27 +1,51 @@
|
||||
<template>
|
||||
<div class="is-size-7 is-uppercase columns is-marginless is-mobile" v-if="container.stat">
|
||||
<div class="column is-narrow has-text-weight-bold">
|
||||
{{ container.state }}
|
||||
</div>
|
||||
<div class="column is-narrow" v-if="container.stat.memoryUsage !== null">
|
||||
<span class="has-text-weight-light has-spacer">mem</span>
|
||||
<span class="has-text-weight-bold">
|
||||
{{ formatBytes(container.stat.memoryUsage) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="column is-narrow" v-if="container.stat.cpu !== null">
|
||||
<span class="has-text-weight-light has-spacer">load</span>
|
||||
<span class="has-text-weight-bold"> {{ container.stat.cpu }}% </span>
|
||||
</div>
|
||||
<div class="is-size-7 is-uppercase columns is-marginless is-mobile is-vcentered" v-if="container.stat">
|
||||
<stat-monitor
|
||||
class="column is-narrow"
|
||||
:data="memoryData"
|
||||
label="mem"
|
||||
:stat-value="formatBytes(container.stat.memoryUsage)"
|
||||
></stat-monitor>
|
||||
<stat-monitor
|
||||
class="column is-narrow"
|
||||
:data="cpuData"
|
||||
label="load"
|
||||
:stat-value="container.stat.cpu + '%'"
|
||||
></stat-monitor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
import { type ComputedRef } from "vue";
|
||||
|
||||
const container = inject("container") as ComputedRef<Container>;
|
||||
|
||||
const cpuData = computedWithControl(
|
||||
() => container.value.getLastStat(),
|
||||
() => {
|
||||
const history = container.value.getStatHistory();
|
||||
const points: Point<unknown>[] = history.map((stat, i) => ({
|
||||
x: i,
|
||||
y: stat.snapshot.cpu,
|
||||
value: stat.snapshot.cpu + "%",
|
||||
}));
|
||||
return points;
|
||||
}
|
||||
);
|
||||
|
||||
const memoryData = computedWithControl(
|
||||
() => container.value.getLastStat(),
|
||||
() => {
|
||||
const history = container.value.getStatHistory();
|
||||
const points: Point<string>[] = history.map((stat, i) => ({
|
||||
x: i,
|
||||
y: stat.snapshot.memory,
|
||||
value: formatBytes(stat.snapshot.memoryUsage),
|
||||
}));
|
||||
return points;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -30,4 +54,23 @@ const container = inject("container") as ComputedRef<Container>;
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
.has-border {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 3px;
|
||||
padding: 1px 1px 0 1px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
|
||||
.has-background-body-color {
|
||||
background-color: var(--body-background-color);
|
||||
}
|
||||
|
||||
.is-top-left {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0.75em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
import { type ComputedRef } from "vue";
|
||||
|
||||
const container = inject("container") as ComputedRef<Container>;
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef } from "vue";
|
||||
import { type Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
const { showSearch } = useSearchFilter();
|
||||
const { base } = config;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<scrollable-view :scrollable="scrollable" v-if="container">
|
||||
<template #header v-if="showTitle">
|
||||
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
|
||||
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile has-boxshadow">
|
||||
<div class="column is-clipped is-paddingless">
|
||||
<container-title @close="$emit('close')" />
|
||||
</div>
|
||||
<div class="column is-narrow is-paddingless">
|
||||
<container-stat v-if="container.stat" />
|
||||
<container-stat />
|
||||
</div>
|
||||
|
||||
<div class="mr-2 column is-narrow is-paddingless">
|
||||
|
||||
@@ -13,33 +13,7 @@ defineProps<{
|
||||
padding-right: 5px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.date {
|
||||
background-color: #262626;
|
||||
color: #258ccd;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
.date {
|
||||
background-color: #262626;
|
||||
color: #258ccd;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.date {
|
||||
background-color: #f0f0f0;
|
||||
color: #009900;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
.date {
|
||||
background-color: #f0f0f0;
|
||||
color: #009900;
|
||||
}
|
||||
background-color: var(--scheme-main-ter);
|
||||
color: #258ccd;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
import { type ComputedRef } from "vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, toRaw } from "vue";
|
||||
import { useRouteHash } from "@vueuse/router";
|
||||
import { type Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
56
assets/components/LogViewer/StatMonitor.vue
Normal file
56
assets/components/LogViewer/StatMonitor.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="has-text-centered is-relative host" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
|
||||
<div class="has-border has-boxshadow">
|
||||
<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-bold">
|
||||
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { data, label, statValue } = defineProps<{ data: Point<unknown>[]; label: string; statValue: string | number }>();
|
||||
|
||||
let selectedPoint: Point<unknown> = $ref();
|
||||
|
||||
function onSelectedPoint(point: Point<unknown>) {
|
||||
selectedPoint = point;
|
||||
}
|
||||
|
||||
let mouseOver = $ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.has-spacer {
|
||||
&::after {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
.has-border {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 3px;
|
||||
padding: 1px 1px 0 1px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
|
||||
.has-background-body-color {
|
||||
background-color: var(--body-background-color);
|
||||
}
|
||||
|
||||
.host:hover span {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.is-top-left {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0.75em;
|
||||
}
|
||||
</style>
|
||||
62
assets/components/LogViewer/StatSparkline.vue
Normal file
62
assets/components/LogViewer/StatSparkline.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<svg :width="width" :height="height" @mousemove="onMove">
|
||||
<path :d="path" class="area" />
|
||||
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="line" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { extent } from "d3-array";
|
||||
import { scaleLinear } from "d3-scale";
|
||||
import { area, curveStep } from "d3-shape";
|
||||
|
||||
const d3 = { extent, scaleLinear, area, curveStep };
|
||||
const { data, width = 150, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
|
||||
const x = d3.scaleLinear().range([width, 0]);
|
||||
const y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "selected-point", value: Point<unknown>): void;
|
||||
}>();
|
||||
|
||||
const shape = d3
|
||||
.area<Point<unknown>>()
|
||||
.curve(d3.curveStep)
|
||||
.x((d) => x(d.x))
|
||||
.y0(height)
|
||||
.y1((d) => y(d.y));
|
||||
|
||||
const path = computed(() => {
|
||||
x.domain(d3.extent(data, (d) => d.x) as [number, number]);
|
||||
y.domain(d3.extent(data, (d) => d.y) as [number, number]);
|
||||
|
||||
return shape(data) ?? "";
|
||||
});
|
||||
|
||||
let lineX = $ref(0);
|
||||
|
||||
function onMove(e: MouseEvent) {
|
||||
const { offsetX } = e;
|
||||
const xValue = x.invert(offsetX);
|
||||
const index = Math.round(xValue);
|
||||
lineX = x(index);
|
||||
const point = data[index];
|
||||
emit("selected-point", point);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.area) {
|
||||
fill: var(--primary-color);
|
||||
}
|
||||
|
||||
:deep(.line) {
|
||||
stroke: var(--secondary-color);
|
||||
stroke-width: 2;
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg:hover :deep(.line) {
|
||||
display: unset;
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
|
||||
<div class="is-scrollbar-notification">
|
||||
<transition name="fade">
|
||||
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom()" v-show="paused">
|
||||
<button
|
||||
class="button has-boxshadow"
|
||||
:class="hasMore ? 'has-more' : ''"
|
||||
@click="scrollToBottom()"
|
||||
v-show="paused"
|
||||
>
|
||||
<mdi-light-chevron-double-down />
|
||||
</button>
|
||||
</transition>
|
||||
@@ -109,18 +114,18 @@ section {
|
||||
button {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
background-color: var(--secondary-color);
|
||||
transition: background-color 1s ease-out;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
background-color: var(--primary-color);
|
||||
transition: background-color 0.24s ease-out;
|
||||
border: none !important;
|
||||
color: #222;
|
||||
color: #eee;
|
||||
|
||||
&.has-more {
|
||||
background-color: var(--primary-color);
|
||||
background-color: var(--secondary-color);
|
||||
animation-name: bounce;
|
||||
animation-duration: 1000ms;
|
||||
animation-fill-mode: both;
|
||||
color: #fff;
|
||||
|
||||
color: #222;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,29 @@
|
||||
</svg>
|
||||
</router-link>
|
||||
</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 />
|
||||
</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 />
|
||||
</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">
|
||||
@@ -40,7 +49,7 @@
|
||||
class="icon is-small"
|
||||
@click.stop.prevent="store.appendActiveContainer(item)"
|
||||
v-show="!activeContainersById[item.id]"
|
||||
title="$t('tooltip.pin-column')"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
>
|
||||
<cil-columns />
|
||||
</span>
|
||||
@@ -58,6 +67,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Container } from "@/types/Container";
|
||||
|
||||
const { base, secured } = config;
|
||||
const store = useContainerStore();
|
||||
|
||||
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
DockerEventLogEntry,
|
||||
SkippedLogsEntry,
|
||||
} from "@/models/LogEntry";
|
||||
import { type Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
function parseMessage(data: string): LogEntry<string | JSONObject> {
|
||||
const e = JSON.parse(data) as LogEvent;
|
||||
@@ -37,7 +37,7 @@ export function useLogStream(container: ComputedRef<Container>) {
|
||||
} else {
|
||||
messages.push(...buffer);
|
||||
buffer = [];
|
||||
messages.splice(0, messages.length - config.maxLogs);
|
||||
messages = messages.slice(-config.maxLogs);
|
||||
}
|
||||
} else {
|
||||
messages.push(...buffer);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,8 +25,8 @@
|
||||
</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"
|
||||
@@ -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;
|
||||
@@ -112,7 +114,8 @@ function onResized(e: any) {
|
||||
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;
|
||||
|
||||
31
assets/models/Container.ts
Normal file
31
assets/models/Container.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { ContainerStat, ContainerState } from "@/types/Container";
|
||||
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
|
||||
import { Ref } from "vue";
|
||||
|
||||
type Stat = Omit<ContainerStat, "id">;
|
||||
|
||||
export class Container {
|
||||
public stat: Ref<Stat>;
|
||||
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly created: number,
|
||||
public readonly image: string,
|
||||
public readonly name: string,
|
||||
public readonly command: string,
|
||||
public status: string,
|
||||
public state: ContainerState
|
||||
) {
|
||||
this.stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
|
||||
this.throttledStatHistory = useThrottledRefHistory(this.stat, { capacity: 300, deep: true, throttle: 1000 });
|
||||
}
|
||||
|
||||
public getStatHistory() {
|
||||
return unref(this.throttledStatHistory.history);
|
||||
}
|
||||
|
||||
public getLastStat() {
|
||||
return unref(this.throttledStatHistory.last);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
@@ -164,6 +153,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;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { acceptHMRUpdate, defineStore } from "pinia";
|
||||
import { ref, Ref, computed } from "vue";
|
||||
|
||||
import { showAllContainers } from "@/composables/settings";
|
||||
import config from "@/stores/config";
|
||||
import type { Container, ContainerStat } from "@/types/Container";
|
||||
import { watchOnce } from "@vueuse/core";
|
||||
import { Ref, UnwrapNestedRefs } from "vue";
|
||||
import type { ContainerJson, ContainerStat } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
export const useContainerStore = defineStore("container", () => {
|
||||
const containers = ref<Container[]>([]);
|
||||
const activeContainerIds = ref<string[]>([]);
|
||||
const containers: Ref<Container[]> = ref([]);
|
||||
const activeContainerIds: Ref<string[]> = ref([]);
|
||||
|
||||
const allContainersById = computed(() =>
|
||||
containers.value.reduce((acc, container) => {
|
||||
@@ -25,33 +22,36 @@ 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) => (containers.value = JSON.parse((e as MessageEvent).data)),
|
||||
false
|
||||
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];
|
||||
if (container) {
|
||||
container.stat = stat;
|
||||
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";
|
||||
}
|
||||
});
|
||||
|
||||
const setContainers = (newContainers: ContainerJson[]) => {
|
||||
containers.value = newContainers.map((c) => {
|
||||
const existing = allContainersById.value[c.id];
|
||||
if (existing) {
|
||||
existing.status = c.status;
|
||||
existing.state = c.state;
|
||||
return existing;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
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";
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
return new Container(c.id, c.created, c.image, c.name, c.command, c.status, c.state);
|
||||
});
|
||||
};
|
||||
|
||||
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
|
||||
const appendActiveContainer = ({ id }: Container) => activeContainerIds.value.push(id);
|
||||
|
||||
@@ -75,10 +75,10 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
|
||||
|
||||
--border-color: #{$grey-lighter};
|
||||
--border-hover-color: var(--secondary-color);
|
||||
--logo-color: #{$grey-darker};
|
||||
--logo-color: var(--secondary-color);
|
||||
|
||||
--primary-color: #{$turquoise};
|
||||
--secondary-color: #d8f0ca;
|
||||
--secondary-color: rgb(249 115 22);
|
||||
|
||||
--body-background-color: #{$white-bis};
|
||||
--action-toolbar-background-color: #{$light-toolbar-color};
|
||||
@@ -159,6 +159,12 @@ html.has-custom-scrollbars {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-device-width: 480px) {
|
||||
body {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.splitpanes__splitter {
|
||||
z-index: 99;
|
||||
}
|
||||
@@ -177,6 +183,14 @@ html.has-custom-scrollbars {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.has-dropshadow {
|
||||
filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
|
||||
}
|
||||
|
||||
.has-boxshadow {
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
mark {
|
||||
border-radius: 2px;
|
||||
background-color: var(--secondary-color);
|
||||
@@ -192,3 +206,8 @@ mark {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.button.is-rounded:hover {
|
||||
color: var(--text-strong-color);
|
||||
background: var(--scheme-main-ter);
|
||||
}
|
||||
|
||||
23
assets/types/Container.d.ts
vendored
23
assets/types/Container.d.ts
vendored
@@ -1,17 +1,18 @@
|
||||
export interface Container {
|
||||
readonly id: string;
|
||||
readonly created: number;
|
||||
readonly image: string;
|
||||
readonly name: string;
|
||||
readonly status: string;
|
||||
readonly command: string;
|
||||
state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
|
||||
stat?: ContainerStat;
|
||||
}
|
||||
|
||||
export interface ContainerStat {
|
||||
readonly id: string;
|
||||
readonly cpu: number;
|
||||
readonly memory: number;
|
||||
readonly memoryUsage: number;
|
||||
}
|
||||
|
||||
export type ContainerJson = {
|
||||
readonly id: string;
|
||||
readonly created: number;
|
||||
readonly image: string;
|
||||
readonly name: string;
|
||||
readonly command: string;
|
||||
readonly status: string;
|
||||
readonly state: ContainerState;
|
||||
};
|
||||
|
||||
export type ContainerState = "created" | "running" | "exited" | "dead" | "paused" | "restarting";
|
||||
|
||||
1
assets/types/Point.d.ts
vendored
Normal file
1
assets/types/Point.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
type Point<T> = { x: number; y: number; value?: T };
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Container } from "@/types/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import { computed, ComputedRef } from "vue";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM cypress/included:10.9.0
|
||||
FROM cypress/included:11.2.0
|
||||
|
||||
RUN apt install curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
@@ -6,8 +6,8 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@frsource/cypress-plugin-visual-regression-diff": "^2.0.0",
|
||||
"cypress": "^10.8.0",
|
||||
"typescript": "^4.8.3"
|
||||
"@frsource/cypress-plugin-visual-regression-diff": "^3.1.2",
|
||||
"cypress": "^11.0.0",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
127
e2e/pnpm-lock.yaml
generated
127
e2e/pnpm-lock.yaml
generated
@@ -1,14 +1,14 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
'@frsource/cypress-plugin-visual-regression-diff': ^2.0.0
|
||||
cypress: ^10.8.0
|
||||
typescript: ^4.8.3
|
||||
'@frsource/cypress-plugin-visual-regression-diff': ^3.1.2
|
||||
cypress: ^11.0.0
|
||||
typescript: ^4.8.4
|
||||
|
||||
dependencies:
|
||||
'@frsource/cypress-plugin-visual-regression-diff': 2.0.0_cypress@10.8.0
|
||||
cypress: 10.8.0
|
||||
typescript: 4.8.3
|
||||
'@frsource/cypress-plugin-visual-regression-diff': 3.1.2_cypress@11.0.0
|
||||
cypress: 11.0.0
|
||||
typescript: 4.8.4
|
||||
|
||||
packages:
|
||||
|
||||
@@ -52,21 +52,28 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@frsource/cypress-plugin-visual-regression-diff/2.0.0_cypress@10.8.0:
|
||||
resolution: {integrity: sha512-hwMEZzD0tFbNNNbEjdF8q2ELpxhby25VQ665QUJy1soGPiGwY+rBDdktFg3EXVGs/Euq0Nj8hq/K18BWYoX10g==}
|
||||
/@frsource/base64/1.0.4:
|
||||
resolution: {integrity: sha512-IphM1ro1cvV5CqJWzX/LvPJcUE26cwgt/zMtBdssvTrfSNhh9KjfS2VXTA8SivkvPHK1DsdPnoMoXitmx8c3Cg==}
|
||||
dev: false
|
||||
|
||||
/@frsource/cypress-plugin-visual-regression-diff/3.1.2_cypress@11.0.0:
|
||||
resolution: {integrity: sha512-Tl04uSoyRyt/mUBEWnU49ZOYtQNRJIel6DwrBADGfohgQ81AGBTbO7yXOhVqIrUZ/PIJaAJG2J2vw6MDw1ObmA==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
cypress: '>=4.5.0'
|
||||
dependencies:
|
||||
cypress: 10.8.0
|
||||
'@frsource/base64': 1.0.4
|
||||
cypress: 11.0.0
|
||||
glob: 8.0.3
|
||||
meta-png: 1.0.3
|
||||
move-file: 2.1.0
|
||||
pixelmatch: 5.3.0
|
||||
pngjs: 6.0.0
|
||||
sharp: 0.30.7
|
||||
sharp: 0.31.2
|
||||
dev: false
|
||||
|
||||
/@types/node/14.18.29:
|
||||
resolution: {integrity: sha512-LhF+9fbIX4iPzhsRLpK5H7iPdvW8L4IwGciXQIOEcuF62+9nw/VQVsOViAOOGxY3OlOKGLFv0sWwJXdwQeTn6A==}
|
||||
/@types/node/14.18.33:
|
||||
resolution: {integrity: sha512-qelS/Ra6sacc4loe/3MSjXNL1dNQ/GjxNHVzuChwMfmk7HuycRLVQN2qNY3XahK+fZc5E2szqQSKUyAF0E+2bg==}
|
||||
dev: false
|
||||
|
||||
/@types/sinonjs__fake-timers/8.1.1:
|
||||
@@ -81,7 +88,7 @@ packages:
|
||||
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@types/node': 14.18.29
|
||||
'@types/node': 14.18.33
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
@@ -195,6 +202,12 @@ packages:
|
||||
concat-map: 0.0.1
|
||||
dev: false
|
||||
|
||||
/brace-expansion/2.0.1:
|
||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
dev: false
|
||||
|
||||
/buffer-crc32/0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: false
|
||||
@@ -232,8 +245,8 @@ packages:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
dev: false
|
||||
|
||||
/ci-info/3.4.0:
|
||||
resolution: {integrity: sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==}
|
||||
/ci-info/3.5.0:
|
||||
resolution: {integrity: sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==}
|
||||
dev: false
|
||||
|
||||
/clean-stack/2.2.0:
|
||||
@@ -329,15 +342,15 @@ packages:
|
||||
which: 2.0.2
|
||||
dev: false
|
||||
|
||||
/cypress/10.8.0:
|
||||
resolution: {integrity: sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA==}
|
||||
/cypress/11.0.0:
|
||||
resolution: {integrity: sha512-mYXGi2Wjmy9shRjAUDugSMOr4uuzE2nl7hXQi3oQkIQsnwwBx2HNB8Vbfsix3A0zyPXlL5jTcbb6rCVWKRaXbg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@cypress/request': 2.88.10
|
||||
'@cypress/xvfb': 1.2.4_supports-color@8.1.1
|
||||
'@types/node': 14.18.29
|
||||
'@types/node': 14.18.33
|
||||
'@types/sinonjs__fake-timers': 8.1.1
|
||||
'@types/sizzle': 2.3.3
|
||||
arch: 2.2.0
|
||||
@@ -351,7 +364,7 @@ packages:
|
||||
cli-table3: 0.6.3
|
||||
commander: 5.1.0
|
||||
common-tags: 1.8.2
|
||||
dayjs: 1.11.5
|
||||
dayjs: 1.11.6
|
||||
debug: 4.3.4_supports-color@8.1.1
|
||||
enquirer: 2.3.6
|
||||
eventemitter2: 6.4.7
|
||||
@@ -367,12 +380,12 @@ packages:
|
||||
listr2: 3.14.0_enquirer@2.3.6
|
||||
lodash: 4.17.21
|
||||
log-symbols: 4.1.0
|
||||
minimist: 1.2.6
|
||||
minimist: 1.2.7
|
||||
ospath: 1.2.2
|
||||
pretty-bytes: 5.6.0
|
||||
proxy-from-env: 1.0.0
|
||||
request-progress: 3.0.0
|
||||
semver: 7.3.7
|
||||
semver: 7.3.8
|
||||
supports-color: 8.1.1
|
||||
tmp: 0.2.1
|
||||
untildify: 4.0.0
|
||||
@@ -386,8 +399,8 @@ packages:
|
||||
assert-plus: 1.0.0
|
||||
dev: false
|
||||
|
||||
/dayjs/1.11.5:
|
||||
resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==}
|
||||
/dayjs/1.11.6:
|
||||
resolution: {integrity: sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==}
|
||||
dev: false
|
||||
|
||||
/debug/3.2.7_supports-color@8.1.1:
|
||||
@@ -598,6 +611,17 @@ packages:
|
||||
path-is-absolute: 1.0.1
|
||||
dev: false
|
||||
|
||||
/glob/8.0.3:
|
||||
resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
inflight: 1.0.6
|
||||
inherits: 2.0.4
|
||||
minimatch: 5.1.0
|
||||
once: 1.4.0
|
||||
dev: false
|
||||
|
||||
/global-dirs/3.0.0:
|
||||
resolution: {integrity: sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -665,7 +689,7 @@ packages:
|
||||
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
ci-info: 3.4.0
|
||||
ci-info: 3.5.0
|
||||
dev: false
|
||||
|
||||
/is-fullwidth-code-point/3.0.0:
|
||||
@@ -758,7 +782,7 @@ packages:
|
||||
log-update: 4.0.0
|
||||
p-map: 4.0.0
|
||||
rfdc: 1.3.0
|
||||
rxjs: 7.5.6
|
||||
rxjs: 7.5.7
|
||||
through: 2.3.8
|
||||
wrap-ansi: 7.0.0
|
||||
dev: false
|
||||
@@ -800,6 +824,10 @@ packages:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
dev: false
|
||||
|
||||
/meta-png/1.0.3:
|
||||
resolution: {integrity: sha512-ts8SCT3qTvHBA723m1NYUKwzGDxYNCkMrHYrX0t7YaIch8giqpX+KyJEpCUFW7XfCWXiX5KSlQr4p3Ci9lrSug==}
|
||||
dev: false
|
||||
|
||||
/mime-db/1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -828,8 +856,15 @@ packages:
|
||||
brace-expansion: 1.1.11
|
||||
dev: false
|
||||
|
||||
/minimist/1.2.6:
|
||||
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
|
||||
/minimatch/5.1.0:
|
||||
resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
dev: false
|
||||
|
||||
/minimist/1.2.7:
|
||||
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
|
||||
dev: false
|
||||
|
||||
/mkdirp-classic/0.5.3:
|
||||
@@ -855,11 +890,11 @@ packages:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||
dev: false
|
||||
|
||||
/node-abi/3.24.0:
|
||||
resolution: {integrity: sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==}
|
||||
/node-abi/3.28.0:
|
||||
resolution: {integrity: sha512-fRlDb4I0eLcQeUvGq7IY3xHrSb0c9ummdvDSYWfT9+LKP+3jCKw/tKoqaM7r1BAoiAC6GtwyjaGnOz6B3OtF+A==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
semver: 7.3.7
|
||||
semver: 7.3.8
|
||||
dev: false
|
||||
|
||||
/node-addon-api/5.0.0:
|
||||
@@ -945,10 +980,10 @@ packages:
|
||||
detect-libc: 2.0.1
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.6
|
||||
minimist: 1.2.7
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 1.0.2
|
||||
node-abi: 3.24.0
|
||||
node-abi: 3.28.0
|
||||
pump: 3.0.0
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
@@ -992,7 +1027,7 @@ packages:
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.6
|
||||
minimist: 1.2.7
|
||||
strip-json-comments: 2.0.1
|
||||
dev: false
|
||||
|
||||
@@ -1030,10 +1065,10 @@ packages:
|
||||
glob: 7.2.3
|
||||
dev: false
|
||||
|
||||
/rxjs/7.5.6:
|
||||
resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==}
|
||||
/rxjs/7.5.7:
|
||||
resolution: {integrity: sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==}
|
||||
dependencies:
|
||||
tslib: 2.4.0
|
||||
tslib: 2.4.1
|
||||
dev: false
|
||||
|
||||
/safe-buffer/5.2.1:
|
||||
@@ -1044,24 +1079,24 @@ packages:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
|
||||
/semver/7.3.7:
|
||||
resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==}
|
||||
/semver/7.3.8:
|
||||
resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
dev: false
|
||||
|
||||
/sharp/0.30.7:
|
||||
resolution: {integrity: sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
/sharp/0.31.2:
|
||||
resolution: {integrity: sha512-DUdNVEXgS5A97cTagSLIIp8dUZ/lZtk78iNVZgHdHbx1qnQR7JAHY0BnXnwwH39Iw+VKhO08CTYhIg0p98vQ5Q==}
|
||||
engines: {node: '>=14.15.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
color: 4.2.3
|
||||
detect-libc: 2.0.1
|
||||
node-addon-api: 5.0.0
|
||||
prebuild-install: 7.1.1
|
||||
semver: 7.3.7
|
||||
semver: 7.3.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
@@ -1224,8 +1259,8 @@ packages:
|
||||
punycode: 2.1.1
|
||||
dev: false
|
||||
|
||||
/tslib/2.4.0:
|
||||
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
|
||||
/tslib/2.4.1:
|
||||
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
||||
dev: false
|
||||
|
||||
/tunnel-agent/0.6.0:
|
||||
@@ -1243,8 +1278,8 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/typescript/4.8.3:
|
||||
resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==}
|
||||
/typescript/4.8.4:
|
||||
resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
10
go.mod
10
go.mod
@@ -5,7 +5,7 @@ require (
|
||||
github.com/alexflint/go-arg v1.4.3
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.18+incompatible
|
||||
github.com/docker/docker v20.10.21+incompatible
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
@@ -19,9 +19,9 @@ require (
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/spf13/afero v1.9.3
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
@@ -40,4 +40,4 @@ require (
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
)
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
12
go.sum
12
go.sum
@@ -62,8 +62,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v20.10.18+incompatible h1:SN84VYXTBNGn92T/QwIRPlum9zfemfitN7pbsp26WSc=
|
||||
github.com/docker/docker v20.10.18+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog=
|
||||
github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
@@ -183,17 +183,21 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
|
||||
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
||||
49
locales/ru.yml
Normal file
49
locales/ru.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
toolbar:
|
||||
clear: Очистить
|
||||
download: Скачать
|
||||
search: Поиск
|
||||
label:
|
||||
containers: Контейнеры
|
||||
total-containers: Всего Контейнеров
|
||||
running: Запущенные
|
||||
total-cpu-usage: Использование процессора
|
||||
total-mem-usage: Использование памяти
|
||||
dozzle-version: Версия Dozzle
|
||||
all: Все
|
||||
password: Пароль
|
||||
username: Имя пользователя
|
||||
tooltip:
|
||||
search: Поиск контейнеров (⌘ + k, ⌃k)
|
||||
pin-column: Закрепить столбец
|
||||
error:
|
||||
page-not-found: Эта страница не доступна.
|
||||
invalid-auth: Имя пользователя или пароль неверны.
|
||||
logs-skipped: Пропущено {total} записей
|
||||
title:
|
||||
page-not-found: Страница не найдена
|
||||
login: Требуется авторизация
|
||||
settings: Настройки
|
||||
button:
|
||||
logout: Выйти
|
||||
login: Войти
|
||||
placeholder:
|
||||
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
|
||||
settings:
|
||||
display: Вид
|
||||
small-scrollbars: Уменьшенная полоса прокрутки
|
||||
show-timesamps: Показывать временные метки
|
||||
soft-wrap: Плавный перенос текста
|
||||
12-24-format: >-
|
||||
Формат времени. По умолчанию, Dozzle будет использовать локализацию вашего браузера.
|
||||
font-size: Размер шрифта
|
||||
color-scheme: Цветовая схема
|
||||
options: Опции
|
||||
show-stopped-containers: Показывать остановленные контейнеры
|
||||
about: Информация
|
||||
search: >-
|
||||
Включить поиск с помощью Dozzle, используя <code>command+f</code> или
|
||||
<code>ctrl+f</code>
|
||||
using-version: Вы используете версию Dozzle {version}.
|
||||
update-available: >-
|
||||
Доступна новая версия! Обновить до <a :href="{href}" class="next-release"
|
||||
target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
20
main.go
20
main.go
@@ -24,6 +24,16 @@ var (
|
||||
version = "head"
|
||||
)
|
||||
|
||||
type DockerSecret struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (s *DockerSecret) UnmarshalText(b []byte) error {
|
||||
v, err := os.ReadFile(string(b))
|
||||
s.Value = strings.Trim(string(v), "\r\n")
|
||||
return err
|
||||
}
|
||||
|
||||
type args struct {
|
||||
Addr string `arg:"env:DOZZLE_ADDR" default:":8080" help:"sets host:port to bind for server. This is rarely needed inside a docker container."`
|
||||
Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."`
|
||||
@@ -31,6 +41,8 @@ type args struct {
|
||||
TailSize int `arg:"env:DOZZLE_TAILSIZE" default:"300" help:"update the initial tail size when fetching logs."`
|
||||
Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."`
|
||||
Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"`
|
||||
UsernameFile *DockerSecret `arg:"env:DOZZLE_USERNAME_FILE" help:"sets the secret path read username for auth."`
|
||||
PasswordFile *DockerSecret `arg:"env:DOZZLE_PASSWORD_FILE" help:"sets the secret path read password for auth"`
|
||||
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
|
||||
WaitForDockerSeconds int `arg:"--wait-for-docker-seconds,env:DOZZLE_WAIT_FOR_DOCKER_SECONDS" help:"wait for docker to be available for at most this many seconds before starting the server."`
|
||||
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
|
||||
@@ -93,6 +105,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if args.Username == "" && args.UsernameFile != nil {
|
||||
args.Username = args.UsernameFile.Value
|
||||
}
|
||||
|
||||
if args.Password == "" && args.PasswordFile != nil {
|
||||
args.Password = args.PasswordFile.Value
|
||||
}
|
||||
|
||||
if args.Username != "" || args.Password != "" {
|
||||
if args.Username == "" || args.Password == "" {
|
||||
log.Fatalf("Username AND password are required for authentication")
|
||||
|
||||
78
package.json
78
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "4.1.9",
|
||||
"version": "4.4.1",
|
||||
"description": "Realtime log viewer for docker containers. ",
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"bugs": {
|
||||
@@ -22,57 +22,69 @@
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/carbon": "^1.1.8",
|
||||
"@iconify-json/carbon": "^1.1.10",
|
||||
"@iconify-json/cil": "^1.1.2",
|
||||
"@iconify-json/mdi": "^1.1.33",
|
||||
"@iconify-json/mdi": "^1.1.34",
|
||||
"@iconify-json/mdi-light": "^1.1.2",
|
||||
"@iconify-json/octicon": "^1.1.19",
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.2",
|
||||
"@oruga-ui/oruga-next": "^0.5.6",
|
||||
"@iconify-json/octicon": "^1.1.23",
|
||||
"@oruga-ui/oruga-next": "^0.5.8",
|
||||
"@oruga-ui/theme-bulma": "^0.2.7",
|
||||
"@vitejs/plugin-vue": "3.1.0",
|
||||
"@vue/compiler-sfc": "^3.2.39",
|
||||
"@vueuse/core": "^9.3.0",
|
||||
"@vueuse/integrations": "^9.3.0",
|
||||
"@vueuse/router": "^9.3.0",
|
||||
"@vueuse/core": "^9.5.0",
|
||||
"@vueuse/integrations": "^9.5.0",
|
||||
"@vueuse/router": "^9.5.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"bulma": "^0.9.4",
|
||||
"d3-array": "^3.2.0",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"pinia": "^2.0.22",
|
||||
"sass": "^1.55.0",
|
||||
"semver": "^7.3.7",
|
||||
"splitpanes": "^3.1.1",
|
||||
"typescript": "^4.8.3",
|
||||
"unplugin-auto-import": "^0.11.2",
|
||||
"unplugin-icons": "^0.14.10",
|
||||
"unplugin-vue-components": "^0.22.7",
|
||||
"vite": "3.1.3",
|
||||
"vite-plugin-pages": "^0.26.0",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vue": "^3.2.39",
|
||||
"pinia": "^2.0.23",
|
||||
"semver": "^7.3.8",
|
||||
"splitpanes": "^3.1.5",
|
||||
"vue": "^3.2.45",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.5"
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
|
||||
"@pinia/testing": "^0.0.14",
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-selection": "^3.0.3",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-transition": "^3.0.2",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^18.7.22",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/semver": "^7.3.13",
|
||||
"@vitejs/plugin-vue": "3.2.0",
|
||||
"@vue/compiler-sfc": "^3.2.45",
|
||||
"@vue/test-utils": "^2.2.2",
|
||||
"c8": "^7.12.0",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"jsdom": "^20.0.0",
|
||||
"husky": "^8.0.2",
|
||||
"jest-serializer-vue": "^3.0.0",
|
||||
"jsdom": "^20.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"release-it": "^15.4.2",
|
||||
"release-it": "^15.5.0",
|
||||
"sass": "^1.56.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"vitest": "^0.23.4",
|
||||
"vue-tsc": "^0.40.13"
|
||||
"typescript": "^4.8.4",
|
||||
"unplugin-auto-import": "^0.12.0",
|
||||
"unplugin-icons": "^0.14.13",
|
||||
"unplugin-vue-components": "^0.22.9",
|
||||
"vite": "3.2.4",
|
||||
"vite-plugin-pages": "^0.27.1",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vitest": "^0.25.2",
|
||||
"vue-tsc": "^1.0.9"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css}": [
|
||||
|
||||
1849
pnpm-lock.yaml
generated
1849
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
renovate.json
Normal file
6
renovate.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user