Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
afbed43185 | ||
|
|
d083430c73 | ||
|
|
79f553ff0c | ||
|
|
a02551f5ec |
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,13 +1,9 @@
|
||||
<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>
|
||||
<section class="is-small mt-1" v-if="secured && isMobile">
|
||||
<div class="container">
|
||||
<div class="ml-1">
|
||||
<a class="button is-primary is-small" :href="`${base}/logout`">{{ $t("button.logout") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -45,7 +41,7 @@
|
||||
</section>
|
||||
|
||||
<section class="columns is-centered section is-marginless">
|
||||
<div class="column is-4">
|
||||
<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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
17
docker/calculation.go
Normal file
17
docker/calculation.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package docker
|
||||
|
||||
import "github.com/docker/docker/api/types"
|
||||
|
||||
func calculateMemUsageUnixNoCache(mem types.MemoryStats) float64 {
|
||||
// re implementation of the docker calculation
|
||||
// https://github.com/docker/cli/blob/53f8ed4bec07084db4208f55987a2ea94b7f01d6/cli/command/container/stats_helpers.go#L227-L249
|
||||
// cgroup v1
|
||||
if v, isCGroup := mem.Stats["total_inactive_file"]; isCGroup && v < mem.Usage {
|
||||
return float64(mem.Usage - v)
|
||||
}
|
||||
// cgroup v2
|
||||
if v := mem.Stats["inactive_file"]; v < mem.Usage {
|
||||
return float64(mem.Usage - v)
|
||||
}
|
||||
return float64(mem.Usage)
|
||||
}
|
||||
57
docker/calculation_test.go
Normal file
57
docker/calculation_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_calculateMemUsageUnixNoCache(t *testing.T) {
|
||||
type args struct {
|
||||
mem types.MemoryStats
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "with cgroup v1",
|
||||
args: args{
|
||||
mem: types.MemoryStats{
|
||||
Usage: 100,
|
||||
Stats: map[string]uint64{
|
||||
"total_inactive_file": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: 99,
|
||||
},
|
||||
{
|
||||
name: "with cgroup v2",
|
||||
args: args{
|
||||
mem: types.MemoryStats{
|
||||
Usage: 100,
|
||||
Stats: map[string]uint64{
|
||||
"inactive_file": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: 98,
|
||||
},
|
||||
{
|
||||
name: "without cgroup",
|
||||
args: args{
|
||||
mem: types.MemoryStats{
|
||||
Usage: 100,
|
||||
},
|
||||
},
|
||||
want: 100,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, calculateMemUsageUnixNoCache(tt.args.mem), "calculateMemUsageUnixNoCache(%v)", tt.args.mem)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
|
||||
cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(v.PreCPUStats.CPUUsage.TotalUsage)
|
||||
systemDelta = float64(v.CPUStats.SystemUsage) - float64(v.PreCPUStats.SystemUsage)
|
||||
cpuPercent = int64((cpuDelta / systemDelta) * float64(ncpus) * 100)
|
||||
memUsage = int64(v.MemoryStats.Usage - v.MemoryStats.Stats["cache"])
|
||||
memUsage = int64(calculateMemUsageUnixNoCache(v.MemoryStats))
|
||||
memPercent = int64(float64(memUsage) / float64(v.MemoryStats.Limit) * 100)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM cypress/included:10.8.0
|
||||
FROM cypress/included:10.11.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.0.2",
|
||||
"cypress": "^10.11.0",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
121
e2e/pnpm-lock.yaml
generated
121
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.0.2
|
||||
cypress: ^10.11.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.0.2_cypress@10.11.0
|
||||
cypress: 10.11.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.0.2_cypress@10.11.0:
|
||||
resolution: {integrity: sha512-2KvHg6M5FFe1DPjnHtOjBHsAjsrHK4JNdv2dSutiOGe3LX5ji3H+4zs1j8n9BPVTdILXhYrG6lT8jcRyquHl2w==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
cypress: '>=4.5.0'
|
||||
dependencies:
|
||||
cypress: 10.8.0
|
||||
'@frsource/base64': 1.0.4
|
||||
cypress: 10.11.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.1
|
||||
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/10.11.0:
|
||||
resolution: {integrity: sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==}
|
||||
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,8 +1065,8 @@ 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
|
||||
dev: false
|
||||
@@ -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.1:
|
||||
resolution: {integrity: sha512-GR8M1wBwOiFKLkm9JPun27OQnNRZdHfSf9VwcdZX6UrRmM1/XnOrLFTF0GAil+y/YK4E6qcM/ugxs80QirsHxg==}
|
||||
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
|
||||
@@ -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
|
||||
|
||||
6
go.mod
6
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
|
||||
@@ -20,8 +20,8 @@ require (
|
||||
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/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
|
||||
|
||||
10
go.sum
10
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=
|
||||
@@ -185,15 +185,17 @@ 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/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")
|
||||
|
||||
74
package.json
74
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "4.1.8",
|
||||
"version": "4.4.0",
|
||||
"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.9",
|
||||
"@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.4.0",
|
||||
"@vueuse/integrations": "^9.4.0",
|
||||
"@vueuse/router": "^9.4.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.41",
|
||||
"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.41",
|
||||
"@vue/test-utils": "^2.2.1",
|
||||
"c8": "^7.12.0",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"jsdom": "^20.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.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"vitest": "^0.23.4",
|
||||
"vue-tsc": "^0.40.13"
|
||||
"typescript": "^4.8.4",
|
||||
"unplugin-auto-import": "^0.11.4",
|
||||
"unplugin-icons": "^0.14.13",
|
||||
"unplugin-vue-components": "^0.22.9",
|
||||
"vite": "3.2.3",
|
||||
"vite-plugin-pages": "^0.27.1",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vitest": "^0.25.0",
|
||||
"vue-tsc": "^1.0.9"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css}": [
|
||||
|
||||
1769
pnpm-lock.yaml
generated
1769
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user