Compare commits

...

46 Commits

Author SHA1 Message Date
Amir Raminfar 3f81f355bb chore: release v6.2.0 2024-02-03 14:08:26 -08:00
Amir Raminfar 25cb3c4c53 chore: adds tests for store (#2751) 2024-02-03 09:06:05 -08:00
Amir Raminfar 1d4c162fba Revert "chore: updates tests"
This reverts commit 19a83b7b62.
2024-02-02 16:48:58 -08:00
Amir Raminfar 19a83b7b62 chore: updates tests 2024-02-02 16:20:08 -08:00
Amir Raminfar 454156ae2c fix: fixes a regression bug in map. fixes #2749 (#2750) 2024-02-02 15:54:00 -08:00
EDIflyer 2d8b263df4 fix: reword message on similar container switch countdown banner (#2748) 2024-02-02 14:42:39 -08:00
Amir Raminfar fc8b67a902 fix: fixes race issues (#2747) 2024-02-02 13:56:48 -08:00
Amir Raminfar b8869784d4 fix: fixes a potential deadlock when reading stats (#2745) 2024-02-02 09:57:06 -08:00
Amir Raminfar 099c053341 feat: sorts most recent containers on top for fuzzy search (#2746) 2024-02-02 09:41:26 -08:00
Amir Raminfar bd86b2289b feat: updates labels between running and all containers (#2742) 2024-02-01 22:07:16 +00:00
Amir Raminfar 8677a34087 feat: collects all stats like cpu and mem in background for up to 5 minutes (#2740) 2024-02-01 12:43:44 -08:00
renovate[bot] 2c398cc227 chore(deps): update all non-major dependencies (#2741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-01 20:29:39 +00:00
Amir Raminfar bb0ca30b81 feat: search textbox is now draggable (#2737) 2024-01-30 17:22:58 +00:00
renovate[bot] f9eb8186d1 chore(deps): update dependency @types/node to ^20.11.10 (#2735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-29 00:16:05 +00:00
Amir Raminfar d767be3757 chore: updates go modules (#2734) 2024-01-28 19:06:49 +00:00
renovate[bot] 86f9839211 chore(deps): update all non-major dependencies (#2733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-27 18:52:22 +00:00
Amir Raminfar 67f25b7bb0 chore: renames cts to cjs (#2732) 2024-01-25 10:03:46 -08:00
Amir Raminfar 025ce28134 chore: release v6.1.1 2024-01-24 10:27:28 -08:00
renovate[bot] 25fced7bb9 chore(deps): update all non-major dependencies (#2731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-24 17:52:53 +00:00
Amir Raminfar ef636c86fc fix: removes deprecated code (#2730) 2024-01-24 17:48:29 +00:00
Amir Raminfar 808abd57bb fix: improves logfmt (#2729) 2024-01-23 13:08:03 -08:00
Amir Raminfar d07dc2e983 fix: fixes cpu on popup being too long (#2728) 2024-01-23 09:01:15 -08:00
renovate[bot] 099f778ca2 fix(deps): update all non-major dependencies (#2724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-22 16:32:02 +00:00
renovate[bot] 9a760a2738 chore(deps): update dependency jsdom to v24 (#2723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-22 08:23:33 -08:00
renovate[bot] b53c6c8275 fix(deps): update dependency @iconify-json/carbon to ^1.1.28 (#2722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-22 00:46:05 +00:00
renovate[bot] 8dbe9a5774 chore(deps): update all non-major dependencies (#2720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-20 22:33:32 +00:00
Amir Raminfar 5d28917d11 fix(deps): update module github.com/docker/docker to v25 (#2719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-19 14:43:18 -08:00
renovate[bot] 586d717aff fix(deps): update all non-major dependencies (#2717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-19 16:32:24 +00:00
Amir Raminfar 8aba7debe8 chore: release v6.1.0 2024-01-18 09:11:38 -08:00
renovate[bot] c6647133dd chore(deps): update all non-major dependencies to ^3.4.15 (#2713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-18 17:08:47 +00:00
Amir Raminfar 82765f4b68 feat: adds support for compact mode. fixes #2710 (#2712) 2024-01-18 17:04:58 +00:00
Amir Raminfar 0c33d0f921 feat: defaults locale to auto (#2711) 2024-01-18 08:56:38 -08:00
Amir Raminfar 89fe9e6b91 feat: adds a dropdown to switch language locale (#2708) 2024-01-18 01:22:06 +00:00
Amir Raminfar ef359d339c feat: adds support for LogFmt. fixes #2695 (#2705) 2024-01-17 14:01:40 -08:00
Amir Raminfar 9d2a8af435 feat: makes button for page size smaller (#2707) 2024-01-17 14:01:18 -08:00
renovate[bot] 6f96303f1d chore(deps): update all non-major dependencies (#2709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-17 21:54:37 +00:00
Amir Raminfar 48d7109079 Revert "feat: adds support for LogFmt. fixes #2695"
This reverts commit ccd7f0dd86.
2024-01-16 13:39:04 -08:00
Amir Raminfar ccd7f0dd86 feat: adds support for LogFmt. fixes #2695 2024-01-16 13:30:57 -08:00
Amir Raminfar 390d1a3062 chore: release v6.0.8 2024-01-15 10:39:24 -08:00
Amir Raminfar c6c2c13404 feat: adds option to home page to show more than default containers per page (#2703) 2024-01-15 18:38:21 +00:00
renovate[bot] f80dd56bdf chore(deps): update all non-major dependencies (#2702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-15 02:22:22 +00:00
Amir Raminfar 89babeab9a chore: moves warn to debug for releases 2024-01-12 11:37:38 -08:00
Feriman 9abab4ce77 read only docker.sock (#2698) 2024-01-12 11:35:49 -08:00
Feriman 19c3daf2f7 read only docker.sock (#2699) 2024-01-12 11:35:31 -08:00
AmadeusGraves 04fb4f3fa4 Update locale es.yml (#2690) 2024-01-11 05:29:01 -08:00
Amir Raminfar c5680a9b3f chore: makes faked data better. (#2689) 2024-01-10 13:42:23 -08:00
76 changed files with 1617 additions and 1076 deletions
+4
View File
@@ -60,6 +60,10 @@ jobs:
uses: actions/checkout@v4
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
- name: Stactic checker
uses: dominikh/staticcheck-action@v1.3.0
with:
install-go: false
int-test:
name: Integration Tests
timeout-minutes: 60
+1 -1
View File
@@ -1 +1 @@
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run main.go --level debug
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run -race main.go --level debug
+1 -1
View File
@@ -14,7 +14,7 @@ COPY package.json ./
RUN pnpm install --offline --ignore-scripts --no-optional
# Copy assets and translations to build
COPY .* *.config.ts *.config.js *.config.cts ./
COPY .* *.config.ts *.config.js *.config.cjs ./
COPY assets ./assets
COPY locales ./locales
COPY public ./public
+1 -1
View File
@@ -15,7 +15,7 @@ fake_assets:
.PHONY: test
test: fake_assets
go test -cover ./...
go test -cover -race ./...
.PHONY: build
build: dist
+3 -3
View File
@@ -31,7 +31,7 @@ Dozzle is a small container (4 MB compressed). Pull the latest release with:
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock:ro -p 8888:8080 amir20/dozzle:latest
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/).
@@ -43,7 +43,7 @@ Here is the Docker Compose file:
container_name: dozzle
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- 8888:8080
@@ -76,7 +76,7 @@ If it's not enabled please follow [this tutorial](https://github.com/containers/
Once you have the podman remote socket you can run Dozzle on podman.
```
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8888:8080 amir20/dozzle:latest
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock:ro -d -p 8888:8080 amir20/dozzle:latest
```
## Security
+11
View File
@@ -20,6 +20,7 @@ declare global {
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const automaticRedirect: typeof import('./stores/settings')['automaticRedirect']
const collapseNav: typeof import('./stores/settings')['collapseNav']
const compact: typeof import('./stores/settings')['compact']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
@@ -69,6 +70,7 @@ declare global {
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./stores/settings')['lightTheme']
const locale: typeof import('./stores/settings')['locale']
const logSearchContext: typeof import('./composable/logSearchContext')['logSearchContext']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
@@ -288,6 +290,7 @@ declare global {
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSimpleRefHistory: typeof import('./utils/index')['useSimpleRefHistory']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
@@ -358,6 +361,7 @@ declare global {
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
@@ -374,6 +378,7 @@ declare module 'vue' {
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly compact: UnwrapRef<typeof import('./stores/settings')['compact']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
@@ -423,6 +428,7 @@ declare module 'vue' {
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -641,6 +647,7 @@ declare module 'vue' {
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
@@ -704,6 +711,7 @@ declare module 'vue' {
}
}
declare module '@vue/runtime-core' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
@@ -720,6 +728,7 @@ declare module '@vue/runtime-core' {
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly compact: UnwrapRef<typeof import('./stores/settings')['compact']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
@@ -769,6 +778,7 @@ declare module '@vue/runtime-core' {
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -987,6 +997,7 @@ declare module '@vue/runtime-core' {
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
+16 -5
View File
@@ -1,5 +1,14 @@
!
<template>
<div class="text-right">
Show per page
<dropdown-menu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
v-if="containers.length > 0"
/>
</div>
<table class="table table-lg bg-base">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
@@ -95,12 +104,14 @@ const fields = {
},
};
const { containers, perPage = 15 } = defineProps<{
const { containers } = defineProps<{
containers: Container[];
perPage?: number;
}>();
type keys = keyof typeof fields;
const perPage = useStorage("DOZZLE_TABLE_PAGE_SIZE", 15);
const pageSizes = [15, 30, 50, 100];
const storage = useStorage<{ column: keys; direction: 1 | -1 }>("DOZZLE_TABLE_CONTAINERS_SORT", {
column: "created",
direction: -1,
@@ -115,12 +126,12 @@ const sortedContainers = computedWithControl(
},
);
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage));
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage.value));
const isPaginated = computed(() => totalPages.value > 1);
const currentPage = ref(1);
const paginated = computed(() => {
const start = (currentPage.value - 1) * perPage;
const end = start + perPage;
const start = (currentPage.value - 1) * perPage.value;
const end = start + perPage.value;
return sortedContainers.value.slice(start, end);
});
+3 -3
View File
@@ -31,9 +31,9 @@ function createFuzzySearchModal() {
initialState: {
container: {
containers: [
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running"),
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running"),
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "exited"),
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running", []),
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running", []),
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "exited", []),
],
},
},
+4 -4
View File
@@ -94,15 +94,15 @@ const data = computed(() => {
return results.value
.toSorted((a, b) => {
if (a.score === b.score) {
if (a.item.state === "running" && b.item.state !== "running") {
if (a.item.state === b.item.state) {
return b.item.created - a.item.created;
} else if (a.item.state === "running" && b.item.state !== "running") {
return -1;
} else {
return 1;
}
} else if (a.score && b.score) {
return a.score - b.score;
} else {
return 0;
return a.score - b.score;
}
})
.slice(0, maxResults);
@@ -7,7 +7,7 @@
</div>
<div>
<span class="font-light capitalize"> Load </span>
<span class="font-semibold"> {{ container.stat.cpu }}% </span>
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
</div>
<div>
<span class="font-light capitalize"> MEM </span>
@@ -11,11 +11,11 @@ const { container } = useContainerContext();
const cpuData = computedWithControl(
() => container.value.stat,
() => {
const history = container.value.statHistory;
const history = container.value.statsHistory;
const points: Point<unknown>[] = history.map((stat, i) => ({
x: i,
y: Math.max(0, stat.snapshot.cpu),
value: Math.max(0, stat.snapshot.cpu).toFixed(2) + "%",
y: Math.max(0, stat.cpu),
value: Math.max(0, stat.cpu).toFixed(2) + "%",
}));
return points;
},
@@ -24,11 +24,11 @@ const cpuData = computedWithControl(
const memoryData = computedWithControl(
() => container.value.stat,
() => {
const history = container.value.statHistory;
const history = container.value.statsHistory;
const points: Point<string>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.memory,
value: formatBytes(stat.snapshot.memoryUsage),
y: stat.memory,
value: formatBytes(stat.memoryUsage),
}));
return points;
},
+7 -1
View File
@@ -1,5 +1,5 @@
<template>
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }">
<li
v-for="item in messages"
:key="item.id"
@@ -57,5 +57,11 @@ defineProps<{
&.large {
@apply text-lg;
}
&.compact {
> li {
@apply py-0;
}
}
}
</style>
@@ -12,7 +12,7 @@ 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 x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
const selectedPoint = defineEmit<[value: Point<unknown>]>();
+1 -1
View File
@@ -4,7 +4,7 @@
<transition name="fade">
<div
v-show="show && (delayedShow || glopbalShow)"
class="fixed z-50 rounded border border-secondary/50 bg-base-lighter p-4 shadow"
class="fixed z-50 rounded border border-base-content/20 bg-base-lighter p-4 shadow"
ref="content"
>
<slot name="content"></slot>
+10 -1
View File
@@ -1,6 +1,12 @@
<template>
<transition name="slide">
<div class="fixed right-4 top-2 z-10 flex items-center" v-show="showSearch" v-if="search">
<div
class="fixed z-10 flex w-full justify-end p-2"
v-show="showSearch"
v-if="search"
ref="container"
:style="style"
>
<div class="input input-primary flex h-auto items-center !shadow-lg">
<mdi:magnify />
<input
@@ -19,8 +25,11 @@
<script lang="ts" setup>
const input = ref<HTMLInputElement>();
const container = ref<HTMLDivElement>();
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
const { style } = useDraggable(container);
onKeyStroke("f", (e) => {
if (e.ctrlKey || e.metaKey) {
showSearch.value = true;
+11 -15
View File
@@ -20,14 +20,11 @@
<transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
<li
v-for="item in menuItems"
:key="item.id"
:class="isLabel(item) ? 'menu-title' : item.state"
:data-testid="item.id"
:key="isContainer(item) ? item.id : item.keyLabel"
:class="isContainer(item) ? item.state : 'menu-title'"
:data-testid="isContainer(item) ? null : item.keyLabel"
>
<template v-if="isLabel(item)">
{{ item.label }}
</template>
<popup v-else>
<popup v-if="isContainer(item)">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="active-primary"
@@ -51,6 +48,9 @@
<container-popup :container="item"></container-popup>
</template>
</popup>
<template v-else>
{{ $t(item.keyLabel) }}
</template>
</li>
</transition-group>
</transition>
@@ -65,8 +65,6 @@
import { Container } from "@/models/Container";
import { sessionHost } from "@/composable/storage";
const { t } = useI18n();
const store = useContainerStore();
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
@@ -104,15 +102,13 @@ const groupedContainers = computed(() =>
),
);
type MenuLabel = { label: string; id: string; state: string };
const pinnedLabel = { label: t("label.pinned"), id: "pinned", state: "label" } as MenuLabel;
const allLabel = { label: t("label.containers"), id: "containers", state: "label" } as MenuLabel;
function isLabel(item: Container | MenuLabel): item is MenuLabel {
return (item as MenuLabel).label !== undefined;
function isContainer(item: any): item is Container {
return item.hasOwnProperty("image");
}
const menuItems = computed(() => {
const pinnedLabel = { keyLabel: "label.pinned" };
const allLabel = { keyLabel: showAllContainers.value ? "label.all-containers" : "label.running-containers" };
if (groupedContainers.value.pinned.length > 0) {
return [pinnedLabel, ...groupedContainers.value.pinned, allLabel, ...groupedContainers.value.unpinned];
} else {
+1 -1
View File
@@ -5,7 +5,7 @@
</label>
<div
tabindex="0"
class="min-w-52 dropdown-content rounded-box z-50 mt-1 border border-base-content/20 bg-base p-2 shadow"
class="dropdown-content z-50 mt-1 min-w-52 rounded-box border border-base-content/20 bg-base p-2 shadow"
>
<slot name="content"></slot>
</div>
+11 -15
View File
@@ -1,9 +1,9 @@
<template>
<details class="dropdown" ref="details" v-on-click-outside="close">
<summary class="btn btn-primary flex-nowrap" v-bind="$attrs">
<slot name="trigger"> {{ values[modelValue] ?? defaultLabel }} <carbon:caret-down /></slot>
<slot name="trigger"> {{ label }} <carbon:caret-down /></slot>
</summary>
<ul class="menu dropdown-content rounded-box z-50 mt-1 w-52 border border-base-content/20 bg-base p-2 shadow">
<ul class="menu dropdown-content z-50 mt-1 w-52 rounded-box border border-base-content/20 bg-base p-2 shadow">
<slot>
<li v-for="item in options">
<a @click="modelValue = item.value">
@@ -17,27 +17,23 @@
</details>
</template>
<script lang="ts" setup>
<script lang="ts" setup generic="T">
import { vOnClickOutside } from "@vueuse/components";
type DropdownItem = {
label: string;
value: string;
value: T;
};
const { options = [], defaultLabel = "" } = defineProps<{ options?: DropdownItem[]; defaultLabel?: string }>();
const { modelValue } = defineModels<{
modelValue: string;
modelValue: T;
}>();
const values = computed(() =>
options.reduce(
(acc, curr) => {
acc[curr.value] = curr.label;
return acc;
},
{} as Record<string, string>,
),
);
const { options = [], defaultLabel = "" } = defineProps<{
options?: DropdownItem[];
defaultLabel?: string;
}>();
const label = computed(() => options.find((item) => item.value === modelValue.value)?.label ?? defaultLabel);
const details = ref<HTMLElement | null>(null);
const close = () => details.value?.removeAttribute("open");
watch(modelValue, () => close());
+1 -1
View File
@@ -18,7 +18,7 @@ describe("Container", () => {
];
test.each(names)("name %s should be %s and %s", (name, expectedName, expectedSwarmId) => {
const c = new Container("id", new Date(), "image", name!, "command", "host", {}, "status", "created");
const c = new Container("id", new Date(), "image", name!, "command", "host", {}, "status", "created", []);
expect(c.name).toBe(expectedName);
expect(c.swarmId).toBe(expectedSwarmId);
});
+6 -6
View File
@@ -1,6 +1,5 @@
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
import { useExponentialMovingAverage } from "@/utils";
import { useExponentialMovingAverage, useSimpleRefHistory } from "@/utils";
import { Ref } from "vue";
type Stat = Omit<ContainerStat, "id">;
@@ -19,7 +18,7 @@ const hosts = computed(() =>
export class Container {
private _stat: Ref<Stat>;
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
private readonly _statsHistory: Ref<Stat[]>;
public readonly swarmId: string | null = null;
public readonly isSwarm: boolean = false;
private readonly movingAverageStat: Ref<Stat>;
@@ -34,10 +33,11 @@ export class Container {
public readonly labels = {} as Record<string, string>,
public status: string,
public state: ContainerState,
stats: Stat[],
public health?: ContainerHealth,
) {
this._stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
this.throttledStatHistory = useThrottledRefHistory(this._stat, { capacity: 300, deep: true, throttle: 1000 });
this._statsHistory = useSimpleRefHistory(this._stat, { capacity: 300, deep: true, initial: stats });
this.movingAverageStat = useExponentialMovingAverage(this._stat, 0.2);
const match = name.match(SWARM_ID_REGEX);
@@ -48,8 +48,8 @@ export class Container {
}
}
get statHistory() {
return unref(this.throttledStatHistory.history);
get statsHistory() {
return unref(this._statsHistory);
}
get movingAverage() {
+9 -5
View File
@@ -1,19 +1,23 @@
import { type App } from "vue";
import { createI18n } from "vue-i18n";
import { locale } from "@/stores/settings";
import messages from "@intlify/unplugin-vue-i18n/messages";
const locale = messages?.hasOwnProperty(navigator.language) ? navigator.language : navigator.language.slice(0, 2);
const defaultLocale = messages?.hasOwnProperty(navigator.language)
? navigator.language
: navigator.language.slice(0, 2);
const i18n = createI18n({
legacy: false,
locale: locale,
locale: locale.value || defaultLocale,
fallbackLocale: "en",
messages,
});
export const install = (app: App) => {
app.use(i18n);
};
watch(locale, (value) => {
i18n.global.locale.value = value || defaultLocale;
});
export const install = (app: App) => app.use(i18n);
export default i18n;
+30 -6
View File
@@ -21,6 +21,8 @@
<section class="grid-cols-2 gap-4 @3xl:grid">
<div class="flex flex-col gap-2 text-balance @3xl:pr-8">
<toggle v-model="compact"> {{ $t("settings.compact") }} </toggle>
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
@@ -29,6 +31,21 @@
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
<labeled-input>
<template #label>
{{ $t("settings.locale") }}
</template>
<template #input>
<dropdown-menu
v-model="locale"
:options="[
{ label: 'Auto', value: '' },
...availableLocales.map((l) => ({ label: l.toLocaleUpperCase(), value: l })),
]"
/>
</template>
</labeled-input>
<labeled-input>
<template #label>
{{ $t("settings.datetime-format") }}
@@ -124,6 +141,7 @@ import { ComplexLogEntry, SimpleLogEntry } from "@/models/LogEntry";
import {
automaticRedirect,
compact,
hourStyle,
dateLocale,
lightTheme,
@@ -134,20 +152,26 @@ import {
size,
smallerScrollbars,
softWrap,
locale,
} from "@/stores/settings";
const { t } = useI18n();
const { t, availableLocales } = useI18n();
setTitle(t("title.settings"));
const { latest, hasUpdate } = useReleases();
const keys = ref<string[][]>([]);
const hoursAgo = (hours: number) => {
const date = new Date();
date.setHours(date.getHours() - hours);
return date;
};
const fakeMessages = [
new SimpleLogEntry("This is a preview of the logs", 1, new Date(), "info", undefined, "stdout"),
new SimpleLogEntry("A warning log looks like this", 2, new Date(), "warn", undefined, "stdout"),
new SimpleLogEntry("This is a multi line error message", 3, new Date(), "error", "start", "stderr"),
new SimpleLogEntry("with a second line", 4, new Date(), "error", "middle", "stderr"),
new SimpleLogEntry("This is a preview of the logs", 1, hoursAgo(16), "info", undefined, "stdout"),
new SimpleLogEntry("A warning log looks like this", 2, hoursAgo(12), "warn", undefined, "stdout"),
new SimpleLogEntry("This is a multi line error message", 3, hoursAgo(7), "error", "start", "stderr"),
new SimpleLogEntry("with a second line", 4, hoursAgo(2), "error", "middle", "stderr"),
new SimpleLogEntry("and finally third line.", 5, new Date(), "error", "end", "stderr"),
new ComplexLogEntry(
{
@@ -164,7 +188,7 @@ const fakeMessages = [
keys,
),
new SimpleLogEntry(
"This is a very very long message which would wrap by default. Disabling soft wraps would disable this.",
"This is a very very long message which would wrap by default. Disabling soft wraps would disable this. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
7,
new Date(),
"debug",
+2 -1
View File
@@ -64,7 +64,7 @@ export const useContainerStore = defineStore("container", () => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
container.state = "exited";
}
});
@@ -127,6 +127,7 @@ export const useContainerStore = defineStore("container", () => {
c.labels,
c.status,
c.state,
c.stats,
c.health,
);
}),
+6
View File
@@ -3,6 +3,7 @@ import { toRefs } from "@vueuse/core";
export type Settings = {
search: boolean;
size: "small" | "medium" | "large";
compact: boolean;
menuWidth: number;
smallerScrollbars: boolean;
showTimestamp: boolean;
@@ -14,9 +15,11 @@ export type Settings = {
softWrap: boolean;
collapseNav: boolean;
automaticRedirect: boolean;
locale: string;
};
export const DEFAULT_SETTINGS: Settings = {
search: true,
compact: false,
size: "medium",
menuWidth: 15,
smallerScrollbars: false,
@@ -29,12 +32,14 @@ export const DEFAULT_SETTINGS: Settings = {
softWrap: true,
collapseNav: false,
automaticRedirect: true,
locale: "",
};
export const settings = useProfileStorage("settings", DEFAULT_SETTINGS);
export const {
collapseNav,
compact,
softWrap,
hourStyle,
dateLocale,
@@ -46,5 +51,6 @@ export const {
menuWidth,
size,
search,
locale,
automaticRedirect,
} = toRefs(settings);
+1
View File
@@ -15,6 +15,7 @@ export type ContainerJson = {
readonly state: ContainerState;
readonly host: string;
readonly labels: Record<string, string>;
readonly stats: ContainerStat[];
readonly health?: ContainerHealth;
};
+24
View File
@@ -51,3 +51,27 @@ export function useExponentialMovingAverage<T extends Record<string, number>>(so
return ema;
}
interface UseSimpleRefHistoryOptions<T> {
capacity: number;
deep?: boolean;
initial?: T[];
}
export function useSimpleRefHistory<T>(source: Ref<T>, options: UseSimpleRefHistoryOptions<T>) {
const { capacity, deep = true, initial = [] as T[] } = options;
const history = ref<T[]>(initial) as Ref<T[]>;
watch(
source,
(value) => {
history.value.push(value);
if (history.value.length > capacity) {
history.value.shift();
}
},
{ deep },
);
return history;
}
+5 -5
View File
@@ -3,7 +3,7 @@ services:
custom_base:
container_name: custom_base
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- DOZZLE_FILTER=name=custom_base
- DOZZLE_BASE=/foobarbase
@@ -15,7 +15,7 @@ services:
simple-auth:
container_name: simple-auth
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./e2e/data:/data
environment:
- DOZZLE_AUTH_PROVIDER=simple
@@ -25,7 +25,7 @@ services:
dozzle:
container_name: dozzle
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
@@ -50,7 +50,7 @@ services:
container_name: proxy
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CONTAINERS=1
ports:
@@ -58,7 +58,7 @@ services:
playwright:
container_name: playwright
image: mcr.microsoft.com/playwright:v1.40.1-jammy
image: mcr.microsoft.com/playwright:v1.41.2-jammy
working_dir: /app
volumes:
- .:/app
+4 -4
View File
@@ -3,11 +3,11 @@
"type": "module",
"name": "docs",
"devDependencies": {
"@unocss/preset-typography": "^0.58.3",
"@unocss/reset": "^0.58.3",
"@unocss/transformer-directives": "^0.58.3",
"@unocss/preset-typography": "^0.58.4",
"@unocss/reset": "^0.58.4",
"@unocss/transformer-directives": "^0.58.4",
"dozzle": "workspace:*",
"sitemap": "^7.1.1",
"unocss": "^0.58.3"
"unocss": "^0.58.4"
}
}
+134 -127
View File
@@ -6,14 +6,14 @@ settings:
devDependencies:
'@unocss/preset-typography':
specifier: ^0.58.3
version: 0.58.3
specifier: ^0.58.4
version: 0.58.4
'@unocss/reset':
specifier: ^0.58.3
version: 0.58.3
specifier: ^0.58.4
version: 0.58.4
'@unocss/transformer-directives':
specifier: ^0.58.3
version: 0.58.3
specifier: ^0.58.4
version: 0.58.4
dozzle:
specifier: workspace:*
version: link:..
@@ -21,8 +21,8 @@ devDependencies:
specifier: ^7.1.1
version: 7.1.1
unocss:
specifier: ^0.58.3
version: 0.58.3(postcss@8.4.32)(vite@5.0.10)
specifier: ^0.58.4
version: 0.58.4(postcss@8.4.32)(vite@5.0.10)
packages:
@@ -569,15 +569,15 @@ packages:
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
dev: true
/@iconify/utils@2.1.14:
resolution: {integrity: sha512-9pKIntkbLbjVVFxH32td21Am3AGGJfyI2KY2d8yDQxkZe4BBZtufJI8NgcamFn8B5QKLU9ai2VMo8OEov8jAtw==}
/@iconify/utils@2.1.20:
resolution: {integrity: sha512-t8TeKlYK/5i9yTY9VAGAE4P0qQHd/0vH+VSRO+bdpxlt8wqB6f2I0/IrciRsdeFZPMoL8IICgP7lgl2ZtbG8Tw==}
dependencies:
'@antfu/install-pkg': 0.1.1
'@antfu/utils': 0.7.7
'@iconify/types': 2.0.0
debug: 4.3.4
kolorist: 1.8.0
local-pkg: 0.4.3
local-pkg: 0.5.0
transitivePeerDependencies:
- supports-color
dev: true
@@ -769,212 +769,212 @@ packages:
'@types/node': 17.0.45
dev: true
/@unocss/astro@0.58.3(vite@5.0.10):
resolution: {integrity: sha512-qJL+XkWYJhEIX4AmOtbfb2Zu4holTDpRscfvVci4T+2VWjyE3mgtsyNzi9ZChe/hdEPRa7g26gSpNQeMhjh/Kw==}
/@unocss/astro@0.58.4(vite@5.0.10):
resolution: {integrity: sha512-feS8+f3oPmCeR1XF7isQjs3Z9ojM5Ssv0vCNR/dexPFdROfccK/7sIu1YnHWtVg1trPc1kMfI8XJRqfrHMdd5w==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
peerDependenciesMeta:
vite:
optional: true
dependencies:
'@unocss/core': 0.58.3
'@unocss/reset': 0.58.3
'@unocss/vite': 0.58.3(vite@5.0.10)
'@unocss/core': 0.58.4
'@unocss/reset': 0.58.4
'@unocss/vite': 0.58.4(vite@5.0.10)
vite: 5.0.10
transitivePeerDependencies:
- rollup
dev: true
/@unocss/cli@0.58.3:
resolution: {integrity: sha512-veGdHhwm7GRvruXCMdqoFu3wVozr7ELEVWsFB6GpqWdGYIJ1i72M18l72UHDA2+TuDillZONnAQ5AvX9x/sYAw==}
/@unocss/cli@0.58.4:
resolution: {integrity: sha512-8dAs1TzzWCtb3FxoTsVtR2+JGkdmchuP0RvGlSywADf5FqYn9nbP0m3z4STtpPavHO9JaGbabStyjS0nA8Ck6w==}
engines: {node: '>=14'}
hasBin: true
dependencies:
'@ampproject/remapping': 2.2.1
'@rollup/pluginutils': 5.1.0
'@unocss/config': 0.58.3
'@unocss/core': 0.58.3
'@unocss/preset-uno': 0.58.3
'@unocss/config': 0.58.4
'@unocss/core': 0.58.4
'@unocss/preset-uno': 0.58.4
cac: 6.7.14
chokidar: 3.5.3
colorette: 2.0.20
consola: 3.2.3
fast-glob: 3.3.2
magic-string: 0.30.5
pathe: 1.1.1
pathe: 1.1.2
perfect-debounce: 1.0.0
transitivePeerDependencies:
- rollup
dev: true
/@unocss/config@0.58.3:
resolution: {integrity: sha512-8BQDoLzf/BkyfnkQsjnXI84oj+Spqkr7Bf2AbOGcX14vof0qqHSDvJXQV1e0u7jv2QETe2D1+PI4fnkJCumaRw==}
/@unocss/config@0.58.4:
resolution: {integrity: sha512-b/inDCOnhUPzobhj+SxRI7xeSwoAQU+3D3J5JoWZNPBI4OaJdwpuz40uLXl9VsPz5N6dc8/qa+FrSsgCjUcLTg==}
engines: {node: '>=14'}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
unconfig: 0.3.11
dev: true
/@unocss/core@0.58.3:
resolution: {integrity: sha512-9hTxzsrSLh+07ql/lGhE+8ZbE9MTTeZeMx131cPf2jDJUxAZooLE5pBCoK0k77ZJGcribRrwPGkUScBNOK0cYQ==}
/@unocss/core@0.58.4:
resolution: {integrity: sha512-wh4pxXUCkhC+C/0ct74PMYeZgX+oWuHj2mnvr09nM0WmvOm1HeeISrDCCBGcxmKbErUk+D1v3JgzXF7b6j2l4g==}
dev: true
/@unocss/extractor-arbitrary-variants@0.58.3:
resolution: {integrity: sha512-QszC2atLcvzyoZFsjgtMBbILN4lrYI60iVRWdii+GGiKVtoIaKRWiA/3WERkvYGVPseVWOMflUpfxNeq+s9zUw==}
/@unocss/extractor-arbitrary-variants@0.58.4:
resolution: {integrity: sha512-TQlVtSQk8YdBU9KuTA5k6JBJ8d/aPrIsCWjDPfBvAqDvrll/dzwBCLoLTQMdCHFjCXCOYWKPum1ET+Wn9R+XbA==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
dev: true
/@unocss/inspector@0.58.3:
resolution: {integrity: sha512-FqkoHiO23lGGcQ+qJbE1Kb8+kPJWc/LxBz3B4Ehml1vQryncNh4p+3sczVn5YVTfPDGBXBCkP05Q+PJRKabPXQ==}
/@unocss/inspector@0.58.4:
resolution: {integrity: sha512-BbsaIMqP6dqa3qCfkC0nkvjCg11JRigvFPU4319imTPcTHQNQSg35t2mVFPGZ+QYHlU/3ouBeogUQfJDHBmtVQ==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/rule-utils': 0.58.3
'@unocss/core': 0.58.4
'@unocss/rule-utils': 0.58.4
gzip-size: 6.0.0
sirv: 2.0.4
dev: true
/@unocss/postcss@0.58.3(postcss@8.4.32):
resolution: {integrity: sha512-y1WQNvLUidypCu/tr6oJfaV4pjd8Lsk1N27ASEVsvockOH3MekRYpHtJfTl2fMk+1Y98AHv7hPAVjM2NlvhDow==}
/@unocss/postcss@0.58.4(postcss@8.4.32):
resolution: {integrity: sha512-pg2qCGakV1TyMApPdvuvqqmPDhgogPWF14J97BT5zIfGYITAJSmBsm7d3+06w6EuqIS+vcYRw+qCV3oX6qTeiA==}
engines: {node: '>=14'}
peerDependencies:
postcss: ^8.4.21
dependencies:
'@unocss/config': 0.58.3
'@unocss/core': 0.58.3
'@unocss/rule-utils': 0.58.3
'@unocss/config': 0.58.4
'@unocss/core': 0.58.4
'@unocss/rule-utils': 0.58.4
css-tree: 2.3.1
fast-glob: 3.3.2
magic-string: 0.30.5
postcss: 8.4.32
dev: true
/@unocss/preset-attributify@0.58.3:
resolution: {integrity: sha512-iDXNfnSC0SI51UnMltHmMcPr2SYYkimo86i+SBQqc/WBGcCF7fFqFj8G2WsZfwHvU9SdAHF8tYIwNq06w1WSeg==}
/@unocss/preset-attributify@0.58.4:
resolution: {integrity: sha512-r7pDXgcOgGMmrMoqM4/w20KKn4PxF+/vpElatAGXW3V/tHJWFOLGzk+wYeCMgMxeC+vPzqyJkjTfoqYlZrlokQ==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
dev: true
/@unocss/preset-icons@0.58.3:
resolution: {integrity: sha512-SA4Eu4rOQ9+zUgIyK6RacS01ygm0PJWkqKlD8ccrBqEyZapqiU+vLL+v6X8YVjoZjR+5CVgcMD5Km7zEQgqXQw==}
/@unocss/preset-icons@0.58.4:
resolution: {integrity: sha512-xdOe3PLfEWS4y+rA7Gxh/kDI82VkW0PbdoYO2FBwXH7a0JegdUDD5zuOTsTKAIymvQ7eVH53WXN9mcuhfNmdvQ==}
dependencies:
'@iconify/utils': 2.1.14
'@unocss/core': 0.58.3
'@iconify/utils': 2.1.20
'@unocss/core': 0.58.4
ofetch: 1.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@unocss/preset-mini@0.58.3:
resolution: {integrity: sha512-vPC97vZPY6J9uZ+KmK4x7atKFlZJPH4tR7+SmzTmguaGIHZJG8k1cjBCg+5M7P4MaxINRMukUQS8/mM/uWFqvQ==}
/@unocss/preset-mini@0.58.4:
resolution: {integrity: sha512-cpIWUYxLi4VCkkjxpNJOVqH9D32NX1ebRFaoAVnvCRsoj5TRsZJSdxs0WMN7reHT/CUHh0Wh6d3TfMCFxzbZ4g==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/extractor-arbitrary-variants': 0.58.3
'@unocss/rule-utils': 0.58.3
'@unocss/core': 0.58.4
'@unocss/extractor-arbitrary-variants': 0.58.4
'@unocss/rule-utils': 0.58.4
dev: true
/@unocss/preset-tagify@0.58.3:
resolution: {integrity: sha512-9CEh4p8M8zFuNFzmPIs1paExWRcwr0Gp6lSMffFnqaVToeRBgEH7VnRj6/R3ZPAmQ2rEemZ1+3eOQlsspEE6aw==}
/@unocss/preset-tagify@0.58.4:
resolution: {integrity: sha512-1E3vQvg5Qy4tbgovvrV0JaIXFfcdhK4NCSz+MuUk5ZYgKGVDh4IEw0WPuic534nlLm9+YFqyCSLLyIs3bT/GYQ==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
dev: true
/@unocss/preset-typography@0.58.3:
resolution: {integrity: sha512-hOQa2Sjkxo5v+jMwPXYv1MpHSrirf73FKPqwwHlvEUSCq9iweGDOU/MVOc5fI9qCg0SrfWhIvrZb4ASlgAuzWQ==}
/@unocss/preset-typography@0.58.4:
resolution: {integrity: sha512-0Opt3Y0At4F03092iwGgz7HswRC2ElBXHR+IO9wWxSbldtpdmKzFJ/GBHEalzQjacWnyyzmRjabjA2quoQ8FQA==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/preset-mini': 0.58.3
'@unocss/core': 0.58.4
'@unocss/preset-mini': 0.58.4
dev: true
/@unocss/preset-uno@0.58.3:
resolution: {integrity: sha512-E/g2BS4KXS9E/4OqyJSt0xSB6gbbk2VGjgIXrpcSXuDr2S2F29XLVlhJA5HJBADPlEfbo41z7Mk3LA3nQPWxQQ==}
/@unocss/preset-uno@0.58.4:
resolution: {integrity: sha512-FWBPGIt0UcQHu58l75kvhB4vF2v+s2m0OW9DnIrPm3aY7Qj3q4yA2wdahR37qSsH+6vR3JP4+mV9WBrGjIsjrQ==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/preset-mini': 0.58.3
'@unocss/preset-wind': 0.58.3
'@unocss/rule-utils': 0.58.3
'@unocss/core': 0.58.4
'@unocss/preset-mini': 0.58.4
'@unocss/preset-wind': 0.58.4
'@unocss/rule-utils': 0.58.4
dev: true
/@unocss/preset-web-fonts@0.58.3:
resolution: {integrity: sha512-g+ru8gX74uZVSfKgdSGp46XQ+wMr66Hp3wtI01yyu9wqmJRAVWQmeehFYZ0hDnGgX20veYSbG+ybZfxIKeTy6w==}
/@unocss/preset-web-fonts@0.58.4:
resolution: {integrity: sha512-vcy20fIK37GdhesRpiWGvCvkJDQsSiRF1jxw3dy8J5n9kFpIV8DQoPWUIE0ePF4i5ky2dHSDxKaNOP1bxHdKGA==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
ofetch: 1.3.3
dev: true
/@unocss/preset-wind@0.58.3:
resolution: {integrity: sha512-/YhvKDFGnTNvKxNaBv1dazHaqNmBM0Ulh0U9lhycGz11qsJTQSl/Y9ZP64fVC7fuo+Uiaj8AN/9gpmpVrCgt4A==}
/@unocss/preset-wind@0.58.4:
resolution: {integrity: sha512-e2F0HsqPXw+nD85MKIZ63mx5yUx5g8iPOtMw9/eEUeK67HrD+jFxeLq0hAM3ou3a4589QIzeg23LXAkM7gCzvg==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/preset-mini': 0.58.3
'@unocss/rule-utils': 0.58.3
'@unocss/core': 0.58.4
'@unocss/preset-mini': 0.58.4
'@unocss/rule-utils': 0.58.4
dev: true
/@unocss/reset@0.58.3:
resolution: {integrity: sha512-Q2KiRQlam2iYsTZgKdvnXEfUN4TA2oVpGIVD9Wa0ggs0XlYj5aOo0g0+4Tgqqn+YaviZQeJKnDs/JWE+ygHhZA==}
/@unocss/reset@0.58.4:
resolution: {integrity: sha512-ZZTrAdl4WWmMjQdOqcOSWdgFH6kdFKZjPu4c6Ijxk7KvY2BW3nttTTBa7IYeuXFHVfcExUFqlOgRurt+NeWYyQ==}
dev: true
/@unocss/rule-utils@0.58.3:
resolution: {integrity: sha512-0Px9gIW+VOKetZuYET19uamIRpk7A9c8sCzQuGlNvCLXKEWamqXz5asLtnvPzw6SwCXEQDgWXE9i+aeoXaM0Jg==}
/@unocss/rule-utils@0.58.4:
resolution: {integrity: sha512-52Jp4I+joGTaDm7ehB/7uZ2kJL+9BZcYRDUVk4IDacDH5W9yxf1F75LzYT8jJVWXD/HIhiS0r9V6qhcBq2OWZw==}
engines: {node: '>=14'}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
magic-string: 0.30.5
dev: true
/@unocss/scope@0.58.3:
resolution: {integrity: sha512-Bkf6sk/0wry+fa5P8eLnzjC4pdrRlBY29g4F64qjsMBR0gk0stFRNzeoMOk412gmJXWjjlAQgNYiBZDHoPghZw==}
/@unocss/scope@0.58.4:
resolution: {integrity: sha512-JrX98xoYnv0HN41WyzlvCCU1T3jnDOry+V8mm2dB9DvByzxI484X6gtKbOXlJFAPwOlZPU5Bru/XTDaL6yQQww==}
dev: true
/@unocss/transformer-attributify-jsx-babel@0.58.3:
resolution: {integrity: sha512-ar+s1rUVHpTy5Yz31WP4DGF2IHxyD4sk/t9ayvR2nOZddAZipdLGSShG03GLkRv4h2/r0x+BIyJGdwAC0BgVZQ==}
/@unocss/transformer-attributify-jsx-babel@0.58.4:
resolution: {integrity: sha512-yJd+OxAdgAxSQHVrqC6z19ibPqwwWO7J0TFf2caiP3hidSJ0iOvxcC0h62YCMr9dg4MsCpXaiHmGzxXbWGJ9nQ==}
dependencies:
'@babel/core': 7.23.7
'@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.7)
'@babel/preset-typescript': 7.23.3(@babel/core@7.23.7)
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
transitivePeerDependencies:
- supports-color
dev: true
/@unocss/transformer-attributify-jsx@0.58.3:
resolution: {integrity: sha512-H6wLJ5aAdHz8K/Z9/7OfiCBpOmKM7Gah2YtooT/Vfxu66bGehZO4QF6fcla6St53HifNvZ5odhlzqVEyHvQEaQ==}
/@unocss/transformer-attributify-jsx@0.58.4:
resolution: {integrity: sha512-Y4YMVkgY/EF7x0llV8Y42WGs8b77hbXkPstRLNeRD5x8KBNKnjYUXADwhTBEKXjDR8/y5Ewie8u43i1nbh1XuA==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
dev: true
/@unocss/transformer-compile-class@0.58.3:
resolution: {integrity: sha512-VmnByb3N8uGAEXjnfhra3DzKq8ZeVCL30n46GG5RTC03MK0rZmKVOmBOBIB99rmSV+D/WVrb12Gf4fHsoLca7g==}
/@unocss/transformer-compile-class@0.58.4:
resolution: {integrity: sha512-GPz462ogvtb64W0iweuB0Dcm1snGsGte0RlVpFUeG2eWlVqKMRvNRtYuive2Ky2zSZKefWJQBGk3Ti+7CM4wFA==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
dev: true
/@unocss/transformer-directives@0.58.3:
resolution: {integrity: sha512-JMfeA8GJz106UqafqsCDp6BBEU7TozZHpLw414CKZjOW1CuMmaKEGrlr2UCjCYgM1vH7KEFKRMwTRUEV3NvywQ==}
/@unocss/transformer-directives@0.58.4:
resolution: {integrity: sha512-5g2XVhwpmu2IrGHqPwxh+S3ZtkC/AwAtLIBLWB1QNhY3HVzAAaOzcif6uarngrCTTDQSDmsjzm8TSAq26LtCmQ==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/rule-utils': 0.58.3
'@unocss/core': 0.58.4
'@unocss/rule-utils': 0.58.4
css-tree: 2.3.1
dev: true
/@unocss/transformer-variant-group@0.58.3:
resolution: {integrity: sha512-/8CyzLwzpJC5cdiA/Wd5/Pg+HEIK+xxJJ3/VXoo93OPNCCbA9/h6DPwDh1ogKk15c6b5H75Ow6zKq1rYQAz2EA==}
/@unocss/transformer-variant-group@0.58.4:
resolution: {integrity: sha512-zpJ4TMor7aJ3SoLJm4jNBOSqikyXUdQEO1AxkILd9Zcn7JWRgudfFXiXnFP+WGJcs9mMLoUiISxzT8SKNCckfA==}
dependencies:
'@unocss/core': 0.58.3
'@unocss/core': 0.58.4
dev: true
/@unocss/vite@0.58.3(vite@5.0.10):
resolution: {integrity: sha512-gmB2//z7lDEK7Bw5HbHTSQ3abOM0iveAY/W3L3FFXpvduoxMQyuI5dDk0hOCtzhAWeJoynnVN4MBGVmXM4Y/Mg==}
/@unocss/vite@0.58.4(vite@5.0.10):
resolution: {integrity: sha512-TqD5fIXv6NF3v10FFrCII//GRbkou6Dn/OzW+d4T5f0KM5+T6DutljpYUdGo0+2QXKDroUWLAspFUaZUx8iwVw==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
dependencies:
'@ampproject/remapping': 2.2.1
'@rollup/pluginutils': 5.1.0
'@unocss/config': 0.58.3
'@unocss/core': 0.58.3
'@unocss/inspector': 0.58.3
'@unocss/scope': 0.58.3
'@unocss/transformer-directives': 0.58.3
'@unocss/config': 0.58.4
'@unocss/core': 0.58.4
'@unocss/inspector': 0.58.4
'@unocss/scope': 0.58.4
'@unocss/transformer-directives': 0.58.4
chokidar: 3.5.3
fast-glob: 3.3.2
magic-string: 0.30.5
@@ -1333,9 +1333,12 @@ packages:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
dev: true
/local-pkg@0.4.3:
resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
/local-pkg@0.5.0:
resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
engines: {node: '>=14'}
dependencies:
mlly: 1.4.2
pkg-types: 1.0.3
dev: true
/locate-path@6.0.0:
@@ -1471,6 +1474,10 @@ packages:
resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==}
dev: true
/pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
dev: true
/perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
dev: true
@@ -1636,11 +1643,11 @@ packages:
mlly: 1.4.2
dev: true
/unocss@0.58.3(postcss@8.4.32)(vite@5.0.10):
resolution: {integrity: sha512-2rnvghfiIDRQ2cOrmN4P7J7xV2p3yBK+bPAt1aoUxCXcszkLczAnQzh9c7IZ+p70kSVstK45cJTYV6TMzOLF7Q==}
/unocss@0.58.4(postcss@8.4.32)(vite@5.0.10):
resolution: {integrity: sha512-JYeQddAIObJPr6nuxahOgku0MIzjIaQ2P73KtJr0zSuzx6kiq20jf67FgDIOP1Ks6s7iJd7Ga3yuY2h49XjDjg==}
engines: {node: '>=14'}
peerDependencies:
'@unocss/webpack': 0.58.3
'@unocss/webpack': 0.58.4
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
peerDependenciesMeta:
'@unocss/webpack':
@@ -1648,26 +1655,26 @@ packages:
vite:
optional: true
dependencies:
'@unocss/astro': 0.58.3(vite@5.0.10)
'@unocss/cli': 0.58.3
'@unocss/core': 0.58.3
'@unocss/extractor-arbitrary-variants': 0.58.3
'@unocss/postcss': 0.58.3(postcss@8.4.32)
'@unocss/preset-attributify': 0.58.3
'@unocss/preset-icons': 0.58.3
'@unocss/preset-mini': 0.58.3
'@unocss/preset-tagify': 0.58.3
'@unocss/preset-typography': 0.58.3
'@unocss/preset-uno': 0.58.3
'@unocss/preset-web-fonts': 0.58.3
'@unocss/preset-wind': 0.58.3
'@unocss/reset': 0.58.3
'@unocss/transformer-attributify-jsx': 0.58.3
'@unocss/transformer-attributify-jsx-babel': 0.58.3
'@unocss/transformer-compile-class': 0.58.3
'@unocss/transformer-directives': 0.58.3
'@unocss/transformer-variant-group': 0.58.3
'@unocss/vite': 0.58.3(vite@5.0.10)
'@unocss/astro': 0.58.4(vite@5.0.10)
'@unocss/cli': 0.58.4
'@unocss/core': 0.58.4
'@unocss/extractor-arbitrary-variants': 0.58.4
'@unocss/postcss': 0.58.4(postcss@8.4.32)
'@unocss/preset-attributify': 0.58.4
'@unocss/preset-icons': 0.58.4
'@unocss/preset-mini': 0.58.4
'@unocss/preset-tagify': 0.58.4
'@unocss/preset-typography': 0.58.4
'@unocss/preset-uno': 0.58.4
'@unocss/preset-web-fonts': 0.58.4
'@unocss/preset-wind': 0.58.4
'@unocss/reset': 0.58.4
'@unocss/transformer-attributify-jsx': 0.58.4
'@unocss/transformer-attributify-jsx-babel': 0.58.4
'@unocss/transformer-compile-class': 0.58.4
'@unocss/transformer-directives': 0.58.4
'@unocss/transformer-variant-group': 0.58.4
'@unocss/vite': 0.58.4(vite@5.0.10)
vite: 5.0.10
transitivePeerDependencies:
- postcss
+1 -1
View File
@@ -34,6 +34,6 @@ test.describe("es locale", () => {
test.use({ locale: "es" });
test("translated text", async ({ page }) => {
await expect(page.getByTestId("containers")).toHaveText("Contenedores");
await expect(page.getByTestId("label.running-containers")).toHaveText("Contenedores en ejecución");
});
});
+1 -1
View File
@@ -5,5 +5,5 @@ test("simple authentication", async ({ page }) => {
await page.locator('input[name="username"]').fill("admin");
await page.locator('input[name="password"]').fill("password");
await page.locator('button[type="submit"]').click();
await expect(page.getByTestId("containers")).toHaveText("Containers");
await expect(page.getByTestId("label.running-containers")).toHaveText("Running Containers");
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+18 -11
View File
@@ -4,9 +4,8 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/alexflint/go-arg v1.4.3
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/docker v25.0.2+incompatible
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1
github.com/gogo/protobuf v1.3.2 // indirect
@@ -20,8 +19,8 @@ require (
github.com/spf13/afero v1.11.0
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.4
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)
@@ -29,33 +28,41 @@ require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/go-chi/chi/v5 v5.0.11
github.com/go-chi/jwtauth/v5 v5.3.0
github.com/go-logfmt/logfmt v0.6.0
github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark-meta v1.1.0
)
require (
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.18 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.19 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/sdk v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.16.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
golang.org/x/tools v0.17.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
)
+64 -33
View File
@@ -14,45 +14,62 @@ github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsVi
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/beme/abide v0.0.0-20190723115211-635a09831760 h1:FvTM5NSN5HYvfKpgL+8x73U5v063vHsd7AX05eV1DnM=
github.com/beme/abide v0.0.0-20190723115211-635a09831760/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+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/docker v25.0.1+incompatible h1:k5TYd5rIVQRSqcTwCID+cyVA0yRg86+Pcrz1ls0/frA=
github.com/docker/docker v25.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v25.0.2+incompatible h1:/OaKeauroa10K4Nqavw4zlhcDq/WBcPMc5DbjOGgozY=
github.com/docker/docker v25.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.3.0 h1:X7RKGks1lrVeIe2omGyz47pNaNjG2YmwlRN5UKhN8qg=
github.com/go-chi/jwtauth/v5 v5.3.0/go.mod h1:2PoGm/KbnzRN9ILY6HFZAI6fTnb1gEZAKogAyqkd6fY=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
@@ -61,9 +78,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.18 h1:HHZkYS5wWDDyAiNBwztEtDoX07WDhGEdixm8G06R50o=
github.com/lestrrat-go/jwx/v2 v2.0.18/go.mod h1:fAJ+k5eTgKdDqanzCuK6DAt3W7n3cs2/FX7JhQdk83U=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY=
github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -107,15 +123,28 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -133,16 +162,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -150,21 +178,17 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -182,19 +206,26 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+77 -68
View File
@@ -12,6 +12,7 @@ import (
"strings"
"time"
"github.com/amir20/dozzle/internal/utils"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
@@ -44,29 +45,41 @@ func (s StdType) String() string {
}
type DockerCLI interface {
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, container.LogsOptions) (io.ReadCloser, error)
Events(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
ContainerStats(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error)
Ping(ctx context.Context) (types.Ping, error)
ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error
ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error
ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error
ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error
}
type Client struct {
type Client interface {
ListContainers() ([]Container, error)
FindContainer(string) (Container, error)
ContainerLogs(context.Context, string, string, StdType) (io.ReadCloser, error)
Events(context.Context, chan<- ContainerEvent) <-chan error
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
Ping(context.Context) (types.Ping, error)
Host() *Host
ContainerActions(action string, containerID string) error
}
type _client struct {
cli DockerCLI
filters filters.Args
host *Host
}
func NewClient(cli DockerCLI, filters filters.Args, host *Host) *Client {
return &Client{cli, filters, host}
func NewClient(cli DockerCLI, filters filters.Args, host *Host) Client {
return &_client{cli, filters, host}
}
// NewClientWithFilters creates a new instance of Client with docker filters
func NewClientWithFilters(f map[string][]string) (*Client, error) {
func NewClientWithFilters(f map[string][]string) (Client, error) {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
@@ -85,7 +98,7 @@ func NewClientWithFilters(f map[string][]string) (*Client, error) {
return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}), nil
}
func NewClientWithTlsAndFilter(f map[string][]string, host Host) (*Client, error) {
func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error) {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
@@ -121,7 +134,7 @@ func NewClientWithTlsAndFilter(f map[string][]string, host Host) (*Client, error
return NewClient(cli, filterArgs, &host), nil
}
func (d *Client) FindContainer(id string) (Container, error) {
func (d *_client) FindContainer(id string) (Container, error) {
var container Container
containers, err := d.ListContainers()
if err != nil {
@@ -149,10 +162,10 @@ func (d *Client) FindContainer(id string) (Container, error) {
return container, nil
}
func (d *Client) ContainerActions(action string, containerID string) error {
func (d *_client) ContainerActions(action string, containerID string) error {
switch action {
case "start":
return d.cli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{})
return d.cli.ContainerStart(context.Background(), containerID, container.StartOptions{})
case "stop":
return d.cli.ContainerStop(context.Background(), containerID, container.StopOptions{})
case "restart":
@@ -162,8 +175,8 @@ func (d *Client) ContainerActions(action string, containerID string) error {
}
}
func (d *Client) ListContainers() ([]Container, error) {
containerListOptions := types.ContainerListOptions{
func (d *_client) ListContainers() ([]Container, error) {
containerListOptions := container.ListOptions{
Filters: d.filters,
All: true,
}
@@ -191,6 +204,7 @@ func (d *Client) ListContainers() ([]Container, error) {
Host: d.host.ID,
Health: findBetweenParentheses(c.Status),
Labels: c.Labels,
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
}
containers = append(containers, container)
}
@@ -202,68 +216,60 @@ func (d *Client) ListContainers() ([]Container, error) {
return containers, nil
}
func (d *Client) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
func (d *_client) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
response, err := d.cli.ContainerStats(ctx, id, true)
if err != nil {
return err
}
go func() {
log.Debugf("starting to stream stats for: %s", id)
defer response.Body.Close()
decoder := json.NewDecoder(response.Body)
var v *types.StatsJSON
for {
if err := decoder.Decode(&v); err != nil {
if err == context.Canceled || err == io.EOF {
log.Debugf("stopping stats streaming for container %s", id)
return
}
log.Errorf("decoder for stats api returned an unknown error %v", err)
}
log.Debugf("starting to stream stats for: %s", id)
defer response.Body.Close()
decoder := json.NewDecoder(response.Body)
var v *types.StatsJSON
for {
if err := decoder.Decode(&v); err != nil {
return err
}
var (
memPercent, cpuPercent float64
mem, memLimit float64
previousCPU uint64
previousSystem uint64
)
daemonOSType := response.OSType
var (
memPercent, cpuPercent float64
mem, memLimit float64
previousCPU uint64
previousSystem uint64
)
daemonOSType := response.OSType
if daemonOSType != "windows" {
previousCPU = v.PreCPUStats.CPUUsage.TotalUsage
previousSystem = v.PreCPUStats.SystemUsage
cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v)
mem = calculateMemUsageUnixNoCache(v.MemoryStats)
memLimit = float64(v.MemoryStats.Limit)
memPercent = calculateMemPercentUnixNoCache(memLimit, mem)
} else {
cpuPercent = calculateCPUPercentWindows(v)
mem = float64(v.MemoryStats.PrivateWorkingSet)
}
if daemonOSType != "windows" {
previousCPU = v.PreCPUStats.CPUUsage.TotalUsage
previousSystem = v.PreCPUStats.SystemUsage
cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v)
mem = calculateMemUsageUnixNoCache(v.MemoryStats)
memLimit = float64(v.MemoryStats.Limit)
memPercent = calculateMemPercentUnixNoCache(memLimit, mem)
} else {
cpuPercent = calculateCPUPercentWindows(v)
mem = float64(v.MemoryStats.PrivateWorkingSet)
}
log.Tracef("containerId = %s, cpuPercent = %f, memPercent = %f, memUsage = %f, daemonOSType = %s", id, cpuPercent, memPercent, mem, daemonOSType)
log.Tracef("containerId = %s, cpuPercent = %f, memPercent = %f, memUsage = %f, daemonOSType = %s", id, cpuPercent, memPercent, mem, daemonOSType)
if cpuPercent > 0 || mem > 0 {
select {
case <-ctx.Done():
return
case stats <- ContainerStat{
ID: id,
CPUPercent: cpuPercent,
MemoryPercent: memPercent,
MemoryUsage: mem,
}:
}
if cpuPercent > 0 || mem > 0 {
select {
case <-ctx.Done():
return nil
case stats <- ContainerStat{
ID: id,
CPUPercent: cpuPercent,
MemoryPercent: memPercent,
MemoryUsage: mem,
}:
}
}
}()
return nil
}
}
func (d *Client) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
func (d *_client) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
if since != "" {
@@ -274,7 +280,7 @@ func (d *Client) ContainerLogs(ctx context.Context, id string, since string, std
}
}
options := types.ContainerLogsOptions{
options := container.LogsOptions{
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
Follow: true,
@@ -291,7 +297,7 @@ func (d *Client) ContainerLogs(ctx context.Context, id string, since string, std
return reader, nil
}
func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-chan error {
func (d *_client) Events(ctx context.Context, messages chan<- ContainerEvent) <-chan error {
dockerMessages, errors := d.cli.Events(ctx, types.EventsOptions{})
go func() {
@@ -300,15 +306,18 @@ func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-c
select {
case <-ctx.Done():
return
case err := <-errors:
log.Fatalf("error while listening to docker events: %v. Exiting...", err)
case message, ok := <-dockerMessages:
if !ok {
log.Errorf("docker events channel closed")
return
}
if message.Type == "container" && len(message.Actor.ID) > 0 {
messages <- ContainerEvent{
ActorID: message.Actor.ID[:12],
Name: message.Action,
Name: string(message.Action),
Host: d.host.ID,
}
}
@@ -319,8 +328,8 @@ func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-c
return errors
}
func (d *Client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
options := types.ContainerLogsOptions{
func (d *_client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
options := container.LogsOptions{
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
Timestamps: true,
@@ -338,11 +347,11 @@ func (d *Client) ContainerLogsBetweenDates(ctx context.Context, id string, from
return reader, nil
}
func (d *Client) Ping(ctx context.Context) (types.Ping, error) {
func (d *_client) Ping(ctx context.Context) (types.Ping, error) {
return d.cli.Ping(ctx)
}
func (d *Client) Host() *Host {
func (d *_client) Host() *Host {
return d.host
}
+19 -41
View File
@@ -22,7 +22,7 @@ type mockedProxy struct {
DockerCLI
}
func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) {
func (m *mockedProxy) ContainerList(context.Context, container.ListOptions) ([]types.Container, error) {
args := m.Called()
containers, ok := args.Get(0).([]types.Container)
if !ok && args.Get(0) != nil {
@@ -32,7 +32,7 @@ func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions)
}
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options container.LogsOptions) (io.ReadCloser, error) {
args := m.Called(ctx, id, options)
reader, ok := args.Get(0).(io.ReadCloser)
if !ok && args.Get(0) != nil {
@@ -50,7 +50,7 @@ func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, st
return types.ContainerStats{}, nil
}
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error {
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error {
args := m.Called(ctx, containerID, options)
err := args.Get(0)
@@ -89,7 +89,7 @@ func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string,
func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers()
assert.Empty(t, list, "list should be empty")
@@ -101,7 +101,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
func Test_dockerClient_ListContainers_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers()
assert.Nil(t, list, "list should be nil")
@@ -124,25 +124,15 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers()
require.NoError(t, err, "error should not return an error.")
assert.Equal(t, list, []Container{
{
ID: "1234567890_a",
Name: "a_test_container",
Names: []string{"/a_test_container"},
Host: "localhost",
},
{
ID: "abcdefghijkl",
Name: "z_test_container",
Names: []string{"/z_test_container"},
Host: "localhost",
},
})
Ids := []string{"1234567890_a", "abcdefghijkl"}
for i, container := range list {
assert.Equal(t, container.ID, Ids[i])
}
proxy.AssertExpectations(t)
}
@@ -158,10 +148,10 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
b = append(b, []byte(expected)...)
reader := io.NopCloser(bytes.NewReader(b))
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
options := container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
actual, _ := io.ReadAll(logReader)
@@ -175,7 +165,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
@@ -202,18 +192,12 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
container, err := client.FindContainer("abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, container, Container{
ID: "abcdefghijkl",
Name: "z_test_container",
Names: []string{"/z_test_container"},
Host: "localhost",
Tty: false,
})
assert.Equal(t, container.ID, "abcdefghijkl")
proxy.AssertExpectations(t)
}
@@ -231,7 +215,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
_, err := client.FindContainer("not_valid")
require.Error(t, err, "error should be thrown")
@@ -252,7 +236,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
}
proxy := new(mockedProxy)
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
@@ -263,13 +247,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
container, err := client.FindContainer("abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, container, Container{
ID: "abcdefghijkl",
Name: "z_test_container",
Names: []string{"/z_test_container"},
Host: "localhost",
Tty: false,
})
assert.Equal(t, container.ID, "abcdefghijkl")
actions := []string{"start", "stop", "restart"}
for _, action := range actions {
@@ -294,7 +272,7 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) {
}
proxy := new(mockedProxy)
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
+116
View File
@@ -0,0 +1,116 @@
package docker
import (
"context"
"sync"
log "github.com/sirupsen/logrus"
)
type ContainerStore struct {
containers map[string]*Container
client Client
statsCollector *StatsCollector
subscribers sync.Map
}
func NewContainerStore(ctx context.Context, client Client) *ContainerStore {
s := &ContainerStore{
containers: make(map[string]*Container),
client: client,
subscribers: sync.Map{},
statsCollector: NewStatsCollector(client),
}
containers, err := s.client.ListContainers()
if err != nil {
log.Fatalf("error while listing containers: %v", err)
}
for _, c := range containers {
c := c // create a new variable to avoid capturing the loop variable
s.containers[c.ID] = &c
}
go s.init(ctx)
go s.statsCollector.StartCollecting(ctx)
return s
}
func (s *ContainerStore) List() []Container {
containers := make([]Container, 0, len(s.containers))
for _, c := range s.containers {
containers = append(containers, *c)
}
return containers
}
func (s *ContainerStore) Client() Client {
return s.client
}
func (s *ContainerStore) Subscribe(ctx context.Context, events chan ContainerEvent) {
s.subscribers.Store(ctx, events)
}
func (s *ContainerStore) SubscribeStats(ctx context.Context, stats chan ContainerStat) {
s.statsCollector.Subscribe(ctx, stats)
}
func (s *ContainerStore) init(ctx context.Context) {
events := make(chan ContainerEvent)
s.client.Events(ctx, events)
stats := make(chan ContainerStat)
s.statsCollector.Subscribe(ctx, stats)
for {
select {
case event := <-events:
log.Debugf("received event: %+v", event)
switch event.Name {
case "start":
if container, err := s.client.FindContainer(event.ActorID); err == nil {
log.Debugf("container %s started", container.ID)
s.containers[container.ID] = &container
}
case "destroy":
log.Debugf("container %s destroyed", event.ActorID)
delete(s.containers, event.ActorID)
case "die":
if container, ok := s.containers[event.ActorID]; ok {
log.Debugf("container %s died", container.ID)
container.State = "exited"
}
case "health_status: healthy", "health_status: unhealthy":
healthy := "unhealthy"
if event.Name == "health_status: healthy" {
healthy = "healthy"
}
if container, ok := s.containers[event.ActorID]; ok {
log.Debugf("container %s is %s", container.ID, healthy)
container.Health = healthy
}
}
s.subscribers.Range(func(key, value any) bool {
select {
case value.(chan ContainerEvent) <- event:
case <-key.(context.Context).Done():
s.subscribers.Delete(key)
}
return true
})
case stat := <-stats:
if container, ok := s.containers[stat.ID]; ok {
stat.ID = ""
container.Stats.Push(stat)
}
case <-ctx.Done():
return
}
}
}
+92
View File
@@ -0,0 +1,92 @@
package docker
import (
"context"
"testing"
"github.com/magiconair/properties/assert"
"github.com/stretchr/testify/mock"
)
type mockedClient struct {
mock.Mock
Client
}
func (m *mockedClient) ListContainers() ([]Container, error) {
args := m.Called()
return args.Get(0).([]Container), args.Error(1)
}
func (m *mockedClient) FindContainer(id string) (Container, error) {
args := m.Called(id)
return args.Get(0).(Container), args.Error(1)
}
func (m *mockedClient) Events(ctx context.Context, events chan<- ContainerEvent) <-chan error {
args := m.Called(ctx, events)
return args.Get(0).(chan error)
}
func (m *mockedClient) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
args := m.Called(ctx, id, stats)
return args.Error(0)
}
func TestContainerStore_List(t *testing.T) {
client := new(mockedClient)
client.On("ListContainers").Return([]Container{
{
ID: "1234",
Name: "test",
},
}, nil)
client.On("Events", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(make(chan error))
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
store := NewContainerStore(ctx, client)
containers := store.List()
assert.Equal(t, containers[0].ID, "1234")
}
func TestContainerStore_die(t *testing.T) {
client := new(mockedClient)
client.On("ListContainers").Return([]Container{
{
ID: "1234",
Name: "test",
State: "running",
},
}, nil)
client.On("Events", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(make(chan error)).
Run(func(args mock.Arguments) {
ctx := args.Get(0).(context.Context)
events := args.Get(1).(chan<- ContainerEvent)
go func() {
events <- ContainerEvent{
Name: "die",
ActorID: "1234",
Host: "localhost",
}
<-ctx.Done()
}()
})
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- docker.ContainerStat")).Return(nil)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
store := NewContainerStore(ctx, client)
// Wait until we get the event
events := make(chan ContainerEvent)
store.Subscribe(ctx, events)
<-events
containers := store.List()
assert.Equal(t, containers[0].State, "exited")
}
+26 -1
View File
@@ -9,10 +9,12 @@ import (
"fmt"
"hash/fnv"
"io"
"regexp"
"strings"
"sync"
"time"
"github.com/go-logfmt/logfmt"
log "github.com/sirupsen/logrus"
)
@@ -88,7 +90,7 @@ func (g *EventGenerator) consumeReader() {
if readerError != nil {
if readerError != ErrBadHeader {
log.Debugf("reader error: %v", readerError)
log.Tracef("reader error: %v", readerError)
g.Errors <- readerError
close(g.buffer)
break
@@ -155,6 +157,8 @@ func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
}
}
var validLogFmtKey = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
func createEvent(message string, streamType StdType) *LogEvent {
h := fnv.New32a()
h.Write([]byte(message))
@@ -177,6 +181,27 @@ func createEvent(message string, streamType StdType) *LogEvent {
} else {
logEvent.Message = data
}
} else if strings.Contains(message, "=") {
buffer := bufPool.Get().(*bytes.Buffer)
buffer.Reset()
defer bufPool.Put(buffer)
buffer.WriteString(message)
decoder := logfmt.NewDecoder(buffer)
data := make(map[string]string)
decoder.ScanRecord()
allValid := true
for decoder.ScanKeyval() {
key := decoder.Key()
value := decoder.Value()
if !validLogFmtKey.Match(key) {
allValid = false
break
}
data[string(key)] = string(value)
}
if allValid && len(data) > 1 {
logEvent.Message = data
}
}
}
}
+1 -2
View File
@@ -81,8 +81,7 @@ func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
func Test_createEvent(t *testing.T) {
type args struct {
message string
streamType StdType
message string
}
tests := []struct {
name string
+5
View File
@@ -45,6 +45,11 @@ func guessLogLevel(logEvent *LogEvent) string {
if level, ok := value["level"].(string); ok {
return level
}
case map[string]string:
if level, ok := value["level"]; ok {
return level
}
}
return ""
+88
View File
@@ -0,0 +1,88 @@
package docker
import (
"context"
"errors"
"io"
"sync"
log "github.com/sirupsen/logrus"
)
type StatsCollector struct {
stream chan ContainerStat
subscribers sync.Map
client Client
cancelers sync.Map
}
func NewStatsCollector(client Client) *StatsCollector {
return &StatsCollector{
stream: make(chan ContainerStat),
subscribers: sync.Map{},
client: client,
cancelers: sync.Map{},
}
}
func (c *StatsCollector) Subscribe(ctx context.Context, stats chan ContainerStat) {
c.subscribers.Store(ctx, stats)
}
func (sc *StatsCollector) StartCollecting(ctx context.Context) {
if containers, err := sc.client.ListContainers(); err == nil {
for _, c := range containers {
if c.State == "running" {
go func(client Client, id string) {
ctx, cancel := context.WithCancel(ctx)
sc.cancelers.Store(id, cancel)
if err := client.ContainerStats(ctx, id, sc.stream); err != nil {
if !errors.Is(err, context.Canceled) && !errors.Is(err, io.EOF) {
log.Errorf("unexpected error when streaming container stats: %v", err)
}
}
}(sc.client, c.ID)
}
}
} else {
log.Errorf("error while listing containers: %v", err)
}
go func() {
events := make(chan ContainerEvent)
sc.client.Events(ctx, events)
for event := range events {
switch event.Name {
case "start":
go func(client Client, id string) {
if err := client.ContainerStats(ctx, id, sc.stream); err != nil {
if !errors.Is(err, context.Canceled) && !errors.Is(err, io.EOF) {
log.Errorf("unexpected error when streaming container stats: %v", err)
}
}
}(sc.client, event.ActorID)
case "die":
if cancel, ok := sc.cancelers.LoadAndDelete(event.ActorID); ok {
cancel.(context.CancelFunc)()
}
}
}
}()
for {
select {
case <-ctx.Done():
return
case stat := <-sc.stream:
sc.subscribers.Range(func(key, value interface{}) bool {
select {
case value.(chan ContainerStat) <- stat:
case <-key.(context.Context).Done():
sc.subscribers.Delete(key)
}
return true
})
}
}
}
+16 -13
View File
@@ -2,23 +2,26 @@ package docker
import (
"math"
"github.com/amir20/dozzle/internal/utils"
)
// Container represents an internal representation of docker containers
type Container struct {
ID string `json:"id"`
Names []string `json:"names"`
Name string `json:"name"`
Image string `json:"image"`
ImageID string `json:"imageId"`
Command string `json:"command"`
Created int64 `json:"created"`
State string `json:"state"`
Status string `json:"status"`
Health string `json:"health,omitempty"`
Host string `json:"host,omitempty"`
Tty bool `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
ID string `json:"id"`
Names []string `json:"names"`
Name string `json:"name"`
Image string `json:"image"`
ImageID string `json:"imageId"`
Command string `json:"command"`
Created int64 `json:"created"`
State string `json:"state"`
Status string `json:"status"`
Health string `json:"health,omitempty"`
Host string `json:"host,omitempty"`
Tty bool `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
}
// ContainerStat represent stats instant for a container
+1 -1
View File
@@ -35,5 +35,5 @@ func HttpRequest(addr string, base string) error {
return nil
}
return fmt.Errorf("Healthcheck failed with status code %d", resp.StatusCode)
return fmt.Errorf("healthcheck failed with status code %d", resp.StatusCode)
}
+5 -3
View File
@@ -17,7 +17,7 @@ const (
profileFilename = "profile.json"
)
var missingProfileErr = errors.New("Profile file does not exist")
var errMissingProfileErr = errors.New("Profile file does not exist")
type Settings struct {
Search bool `json:"search"`
@@ -30,9 +30,11 @@ type Settings struct {
CollapseNav bool `json:"collapseNav"`
AutomaticRedirect bool `json:"automaticRedirect"`
Size string `json:"size,omitempty"`
Compact bool `json:"compact"`
LightTheme string `json:"lightTheme,omitempty"`
HourStyle string `json:"hourStyle,omitempty"`
DateLocale string `json:"dateLocale,omitempty"`
Locale string `json:"locale"`
}
type Profile struct {
@@ -64,7 +66,7 @@ func UpdateFromReader(user auth.User, reader io.Reader) error {
mux.Lock()
defer mux.Unlock()
existingProfile, err := Load(user)
if err != nil && err != missingProfileErr {
if err != nil && err != errMissingProfileErr {
return err
}
@@ -110,7 +112,7 @@ func Load(user auth.User) (Profile, error) {
profilePath := filepath.Join(path, profileFilename)
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
return Profile{}, missingProfileErr
return Profile{}, errMissingProfileErr
}
f, err := os.Open(profilePath)
+45
View File
@@ -0,0 +1,45 @@
package utils
import (
"encoding/json"
"sync"
)
type RingBuffer[T any] struct {
Size int
data []T
start int
mutex sync.RWMutex
}
func NewRingBuffer[T any](size int) *RingBuffer[T] {
return &RingBuffer[T]{
Size: size,
data: make([]T, 0, size),
}
}
func (r *RingBuffer[T]) Push(data T) {
r.mutex.Lock()
defer r.mutex.Unlock()
if len(r.data) == r.Size {
r.data[r.start] = data
r.start = (r.start + 1) % r.Size
} else {
r.data = append(r.data, data)
}
}
func (r *RingBuffer[T]) Data() []T {
r.mutex.RLock()
defer r.mutex.RUnlock()
if len(r.data) == r.Size {
return append(r.data[r.start:], r.data[:r.start]...)
} else {
return r.data
}
}
func (r *RingBuffer[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(r.Data())
}
+27
View File
@@ -0,0 +1,27 @@
package utils
import (
"reflect"
"testing"
)
func TestRingBuffer(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
data := rb.Data()
expectedData := []int{1, 2, 3}
if !reflect.DeepEqual(data, expectedData) {
t.Errorf("Expected data to be %v, got %v", expectedData, data)
}
rb.Push(4)
data = rb.Data()
expectedData = []int{2, 3, 4}
if !reflect.DeepEqual(data, expectedData) {
t.Errorf("Expected data to be %v, got %v", expectedData, data)
}
}
+1 -1
View File
@@ -132,7 +132,7 @@ data: []
event: containers-changed
data: []
data: [{"id":"1234","names":null,"name":"test","image":"test","imageId":"","command":"","created":0,"state":"","status":"","stats":[]}]
event: container-start
+15 -57
View File
@@ -1,12 +1,9 @@
package web
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sync"
"github.com/amir20/dozzle/internal/analytics"
"github.com/amir20/dozzle/internal/docker"
@@ -29,9 +26,6 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
events := make(chan docker.ContainerEvent)
stats := make(chan docker.ContainerStat)
b := analytics.BeaconEvent{
Name: "events",
Version: h.config.Version,
@@ -44,48 +38,22 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
HasActions: h.config.EnableActions,
}
{
wg := sync.WaitGroup{}
wg.Add(len(h.clients))
results := make(chan []docker.Container, len(h.clients))
allContainers := make([]docker.Container, 0)
events := make(chan docker.ContainerEvent)
stats := make(chan docker.ContainerStat)
for _, client := range h.clients {
client.Events(ctx, events)
go func(client DockerClient) {
defer wg.Done()
if containers, err := client.ListContainers(); err == nil {
results <- containers
go func(client DockerClient) {
for _, c := range containers {
if c.State == "running" {
if err := client.ContainerStats(ctx, c.ID, stats); err != nil && !errors.Is(err, context.Canceled) {
log.Errorf("error while streaming container stats: %v", err)
}
}
}
}(client)
} else {
log.Errorf("error while listing containers: %v", err)
}
}(client)
}
wg.Wait()
close(results)
allContainers := []docker.Container{}
for containers := range results {
allContainers = append(allContainers, containers...)
}
if err := sendContainersJSON(allContainers, w); err != nil {
log.Errorf("error writing containers to event stream: %v", err)
}
b.RunningContainers = len(allContainers)
f.Flush()
for _, store := range h.stores {
allContainers = append(allContainers, store.List()...)
store.SubscribeStats(ctx, stats)
store.Subscribe(ctx, events)
}
if err := sendContainersJSON(allContainers, w); err != nil {
log.Errorf("error writing containers to event stream: %v", err)
}
b.RunningContainers = len(allContainers)
f.Flush()
if !h.config.NoAnalytics {
go func() {
if err := analytics.SendBeacon(b); err != nil {
@@ -109,17 +77,9 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
}
switch event.Name {
case "start", "die":
log.Debugf("triggering docker event: %v", event.Name)
if event.Name == "start" {
log.Debugf("found new container with id: %v", event.ActorID)
if err := h.clients[event.Host].ContainerStats(ctx, event.ActorID, stats); err != nil && !errors.Is(err, context.Canceled) {
log.Errorf("error when streaming new container stats: %v", err)
}
containers, err := h.clients[event.Host].ListContainers()
if err != nil {
log.Errorf("error when listing containers: %v", err)
}
containers := h.stores[event.Host].List()
if err := sendContainersJSON(containers, w); err != nil {
log.Errorf("error encoding containers to stream: %v", err)
return
@@ -150,11 +110,9 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
return
}
f.Flush()
default:
log.Tracef("ignoring docker event: %v", event.Name)
// do nothing
}
case <-ctx.Done():
log.Debugf("context done, closing event stream")
return
}
}
@@ -2,12 +2,14 @@ package web
import (
"context"
"time"
"net/http"
"net/http/httptest"
"testing"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/utils"
"github.com/beme/abide"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -35,36 +37,26 @@ func Test_handler_streamEvents_happy(t *testing.T) {
ActorID: "1234",
Host: "localhost",
}
time.Sleep(100 * time.Millisecond)
cancel()
}()
})
mockedClient.On("FindContainer", "1234").Return(docker.Container{
ID: "1234",
Name: "test",
Image: "test",
Stats: utils.NewRingBuffer[docker.ContainerStat](300), // 300 seconds of stats
}, nil)
handler := createDefaultHandler(mockedClient)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_error_request(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything, mock.Anything).Return(errChannel)
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
ctx, cancel := context.WithCancel(context.Background())
req = req.WithContext(ctx)
go func() {
cancel()
}()
handler := createDefaultHandler(mockedClient)
clients := map[string]docker.Client{
"localhost": mockedClient,
}
// This is needed so that the server is initialized for store
server := CreateServer(clients, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
handler := server.Handler
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
+2 -1
View File
@@ -4,12 +4,13 @@ import (
"fmt"
"net/http"
"github.com/amir20/dozzle/internal/docker"
log "github.com/sirupsen/logrus"
)
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
log.Trace("Executing healthcheck request")
var client DockerClient
var client docker.Client
for _, v := range h.clients {
client = v
break
+3 -10
View File
@@ -85,16 +85,9 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
g := docker.NewEventGenerator(reader, container.Tty)
encoder := json.NewEncoder(w)
loop:
for {
select {
case event, ok := <-g.Events:
if !ok {
break loop
}
if err := encoder.Encode(event); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
}
for event := range g.Events {
if err := encoder.Encode(event); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ func (h *handler) releases(w http.ResponseWriter, r *http.Request) {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Warnf("error reading releases: %v", err)
log.Debugf("error reading releases: %v", err)
return
}
+11 -18
View File
@@ -2,16 +2,14 @@ package web
import (
"context"
"io"
"io/fs"
"time"
"net/http"
"strings"
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/docker"
"github.com/docker/docker/api/types"
"github.com/go-chi/chi/v5"
log "github.com/sirupsen/logrus"
)
@@ -47,30 +45,25 @@ type Authorizer interface {
}
type handler struct {
clients map[string]DockerClient
clients map[string]docker.Client
stores map[string]*docker.ContainerStore
content fs.FS
config *Config
}
// Client is a proxy around the docker client
type DockerClient interface {
ListContainers() ([]docker.Container, error)
FindContainer(string) (docker.Container, error)
ContainerLogs(context.Context, string, string, docker.StdType) (io.ReadCloser, error)
Events(context.Context, chan<- docker.ContainerEvent) <-chan error
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, docker.StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- docker.ContainerStat) error
Ping(context.Context) (types.Ping, error)
Host() *docker.Host
ContainerActions(action string, containerID string) error
}
func CreateServer(clients map[string]docker.Client, content fs.FS, config Config) *http.Server {
stores := make(map[string]*docker.ContainerStore)
for host, client := range clients {
stores[host] = docker.NewContainerStore(context.Background(), client)
}
func CreateServer(clients map[string]DockerClient, content fs.FS, config Config) *http.Server {
handler := &handler{
clients: clients,
content: content,
config: &config,
stores: stores,
}
return &http.Server{Addr: config.Addr, Handler: createRouter(handler)}
}
@@ -135,7 +128,7 @@ func createRouter(h *handler) *chi.Mux {
return r
}
func (h *handler) clientFromRequest(r *http.Request) DockerClient {
func (h *handler) clientFromRequest(r *http.Request) docker.Client {
host := chi.URLParam(r, "host")
if host == "" {
+4 -4
View File
@@ -17,7 +17,7 @@ import (
type MockedClient struct {
mock.Mock
DockerClient
docker.Client
}
func (m *MockedClient) FindContainer(id string) (docker.Container, error) {
@@ -59,7 +59,7 @@ func (m *MockedClient) Host() *docker.Host {
return args.Get(0).(*docker.Host)
}
func createHandler(client DockerClient, content fs.FS, config Config) *chi.Mux {
func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux {
if client == nil {
client = new(MockedClient)
client.(*MockedClient).On("ListContainers").Return([]docker.Container{}, nil)
@@ -74,7 +74,7 @@ func createHandler(client DockerClient, content fs.FS, config Config) *chi.Mux {
content = afero.NewIOFS(fs)
}
clients := map[string]DockerClient{
clients := map[string]docker.Client{
"localhost": client,
}
return createRouter(&handler{
@@ -84,6 +84,6 @@ func createHandler(client DockerClient, content fs.FS, config Config) *chi.Mux {
})
}
func createDefaultHandler(client DockerClient) *chi.Mux {
func createDefaultHandler(client docker.Client) *chi.Mux {
return createHandler(client, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
}
+4
View File
@@ -6,6 +6,8 @@ toolbar:
show-all: Zeige alle Streams
label:
containers: Container
running-containers: Laufende Container
all-containers: Alle Container
total-containers: Gesamte Container
running: Laufende
total-cpu-usage: Gesamte CPU Auslastung
@@ -62,6 +64,7 @@ placeholder:
search-containers: Suche Container (⌘ + k, ⌃k)
settings:
display: Anzeige
locale: Sprache überschreiben
small-scrollbars: Verwende kleinere Scrollbars
show-timesamps: Zeige Zeitstempel
soft-wrap: Zeilenumbruch
@@ -79,6 +82,7 @@ settings:
Eine neue Version ist verfügbar! Aktualisiere auf <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Zeige stdout und stderr Labels
automatic-redirect: Automatische Weiterleitung zu neuen Containern mit demselben Namen
compact: Aktiviere den kompakten Modus für Logs
releases:
features: Ein neues Feature | {count} Features
bugFixes: Ein Bug Fix | {count} Fixes
+6 -2
View File
@@ -9,6 +9,8 @@ toolbar:
restart: Restart
label:
containers: Containers
running-containers: Running Containers
all-containers: All Containers
total-containers: Total Containers
running: Running
total-cpu-usage: Total CPU Usage
@@ -41,7 +43,7 @@ error:
events-timeout:
title: Something is not right
message: >-
Dozzle UI timeed out while connecting to API. Please check network
Dozzle UI timed out while connecting to API. Please check network
connection and try again.
alert:
redirected:
@@ -51,7 +53,7 @@ alert:
title: Similar container found
message: >-
Dozzle found a similar container {containerId} that is running on the same
host. Do you want to switch to it?
host and will automatically switch to it unless you click 'Cancel'.
title:
page-not-found: Page not found
login: Authentication Required
@@ -65,6 +67,7 @@ placeholder:
search-containers: Search containers (⌘ + k, ⌃k)
settings:
display: Display
locale: Override language
small-scrollbars: Use smaller scrollbars
show-timesamps: Show timestamps
soft-wrap: Soft wrap lines
@@ -81,6 +84,7 @@ settings:
rel="noreferrer noopener">{nextVersion}</a>.
show-std: Show stdout and stderr labels
automatic-redirect: Automatically redirect to new containers with the same name
compact: Enable compact mode for logs
releases:
features: one new feature | {count} features
bugFixes: one bug fix | {count} fixes
+10
View File
@@ -4,8 +4,13 @@ toolbar:
search: Buscar
show: Mostrar sólo {std}
show-all: Mostrar todos los flujos
stop: Detener
start: Iniciar
restart: Reiniciar
label:
containers: Contenedores
running-containers: Contenedores en ejecución
all-containers: Todos los contenedores
total-containers: Contenedores Totales
running: En ejecución
total-cpu-usage: Uso total del Procesador
@@ -20,6 +25,7 @@ label:
created: Creado
avg-cpu: Promedio de CPU (%)
avg-mem: Promedio de MEM (%)
pinned: Fijado
tooltip:
search: Buscar contenedores (⌘ + K, CTRL + K)
pin-column: Anclar como columna
@@ -58,9 +64,11 @@ placeholder:
search-containers: Buscar contenedores (⌘ + K, CTRL + K)
settings:
display: Vista
locale: Sobrescribir idioma
small-scrollbars: Utilizar barras de desplazamiento más pequeñas
show-timesamps: Mostrar marcas de tiempo
soft-wrap: Líneas de texto con ajuste suave
datetime-format: Sobrescribir el formato de fecha y hora
12-24-format: >-
Por defecto, Dozzle utilizará la configuración regional de tu navegador para formatear la hora. Usted puede
forzar el estilo de 12 o 24 horas.
@@ -76,6 +84,7 @@ settings:
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Mostrar etiquetas de salida estándar y salida de error estándar
automatic-redirect: Redireccionar automáticamente a nuevos contenedores con el mismo nombre
compact: Modo compacto
releases:
features: Características
bugFixes: Correcciones de errores
@@ -86,6 +95,7 @@ releases:
no_releases: No hay lanzamientos
log_actions:
copy_log: Copiar registro
jump_to_context: Ir al contexto
toasts:
copied:
title: Copiado
+4
View File
@@ -6,6 +6,8 @@ toolbar:
show-all: Mostrar todos os fluxos
label:
containers: Contentores
running-containers: Contentores em execução
all-containers: Todos os contentores
total-containers: Contentores Totais
running: Em execução
total-cpu-usage: Utilização total da CPU
@@ -58,6 +60,7 @@ placeholder:
search-containers: Pesquisar contentores (⌘ + K, CTRL + K)
settings:
display: Visão
locale: Localidade
small-scrollbars: Usar barras de rolagem mais pequenas
show-timesamps: Mostrar carimbos de tempo
soft-wrap: Linhas de texto de embrulho suave
@@ -76,3 +79,4 @@ settings:
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Mostrar etiquetas de saída padrão e saída de erro padrão
automatic-redirect: Redirecionar automaticamente para novos contentores com o mesmo nome
compact: Compactar a interface do utilizador
+4
View File
@@ -6,6 +6,8 @@ toolbar:
show-all: Показать все потоки
label:
containers: Контейнеры
running-containers: Запущенные контейнеры
all-containers: Все контейнеры
total-containers: Всего Контейнеров
running: Запущенные
total-cpu-usage: Использование процессора
@@ -58,6 +60,7 @@ placeholder:
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
settings:
display: Вид
locale: Язык
small-scrollbars: Уменьшенная полоса прокрутки
show-timesamps: Показывать временные метки
soft-wrap: Плавный перенос текста
@@ -74,3 +77,4 @@ settings:
Доступна новая версия! Обновить до <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Показывать метки stdout и stderr
automatic-redirect: Автоматическое перенаправление на новые контейнеры с тем же именем.
compact: Компактный режим
+4
View File
@@ -6,6 +6,8 @@ toolbar:
show-all: 顯示全部資料流
label:
containers: 容器
running-containers: 運行中的容器
all-containers: 所有容器
total-containers: 總容器數
running: 運行中
total-cpu-usage: 總 CPU 使用率
@@ -41,6 +43,7 @@ placeholder:
search-containers: 查詢容器 (⌘ + k, ⌃k)
settings:
display: 顯示
locale: 覆寫語言
small-scrollbars: 使用較小的捲軸
show-timesamps: 顯示時間戳記
soft-wrap: 自動換行
@@ -56,3 +59,4 @@ settings:
update-available: >-
有新版本可用!請更新至 <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>。
show-std: 顯示 stdout 和 stderr 標籤
compact: 緊湊模式
+4
View File
@@ -6,6 +6,8 @@ toolbar:
show-all: 显示全部
label:
containers: 容器
running-containers: 运行中的容器
all-containers: 所有容器
total-containers: 所有容器
running: 运行中
total-cpu-usage: 所有CPU使用率
@@ -58,6 +60,7 @@ placeholder:
search-containers: 搜索容器 (⌘ + k, ⌃k)
settings:
display: 显示
locale: 覆盖语言
small-scrollbars: 使用较小的滚动条
show-timesamps: 显示时间戳
soft-wrap: 断行
@@ -74,5 +77,6 @@ settings:
新版本可用!更新到 <a href="{href}" rel="noreferrer noopener">{nextVersion}</a>。
show-std: 显示stdout和stderr标签
automatic-redirect: 自动重定向到同名的新容器
compact: 紧凑模式
log_actions:
copy_log: 复制日志
+10 -9
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"embed"
"errors"
"io/fs"
"net/http"
"os"
@@ -120,12 +121,12 @@ func doStartEvent(arg args) {
}
func createClients(args args,
localClientFactory func(map[string][]string) (*docker.Client, error),
remoteClientFactory func(map[string][]string, docker.Host) (*docker.Client, error),
hostname string) map[string]web.DockerClient {
clients := make(map[string]web.DockerClient)
localClientFactory func(map[string][]string) (docker.Client, error),
remoteClientFactory func(map[string][]string, docker.Host) (docker.Client, error),
hostname string) map[string]docker.Client {
clients := make(map[string]docker.Client)
if localClient := createLocalClient(args, localClientFactory); localClient != nil {
if localClient, err := createLocalClient(args, localClientFactory); err == nil {
if hostname != "" {
localClient.Host().Name = hostname
}
@@ -154,7 +155,7 @@ func createClients(args args,
return clients
}
func createServer(args args, clients map[string]web.DockerClient) *http.Server {
func createServer(args args, clients map[string]docker.Client) *http.Server {
_, dev := os.LookupEnv("DEV")
var provider web.AuthProvider = web.NONE
@@ -221,7 +222,7 @@ func createServer(args args, clients map[string]web.DockerClient) *http.Server {
return web.CreateServer(clients, assets, config)
}
func createLocalClient(args args, localClientFactory func(map[string][]string) (*docker.Client, error)) *docker.Client {
func createLocalClient(args args, localClientFactory func(map[string][]string) (docker.Client, error)) (docker.Client, error) {
for i := 1; ; i++ {
dockerClient, err := localClientFactory(args.Filter)
if err == nil {
@@ -230,7 +231,7 @@ func createLocalClient(args args, localClientFactory func(map[string][]string) (
log.Debugf("Could not connect to local Docker Engine: %s", err)
} else {
log.Debugf("Connected to local Docker Engine")
return dockerClient
return dockerClient, nil
}
}
if args.WaitForDockerSeconds > 0 {
@@ -242,7 +243,7 @@ func createLocalClient(args args, localClientFactory func(map[string][]string) (
break
}
}
return nil
return nil, errors.New("could not connect to local Docker Engine")
}
func parseArgs() args {
+12 -11
View File
@@ -7,6 +7,7 @@ import (
"github.com/amir20/dozzle/internal/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -17,7 +18,7 @@ type fakeCLI struct {
mock.Mock
}
func (f *fakeCLI) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) {
func (f *fakeCLI) ContainerList(context.Context, container.ListOptions) ([]types.Container, error) {
args := f.Called()
return args.Get(0).([]types.Container), args.Error(1)
}
@@ -25,7 +26,7 @@ func (f *fakeCLI) ContainerList(context.Context, types.ContainerListOptions) ([]
func Test_valid_localhost(t *testing.T) {
client := new(fakeCLI)
client.On("ContainerList").Return([]types.Container{}, nil)
fakeClientFactory := func(filter map[string][]string) (*docker.Client, error) {
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) {
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
ID: "localhost",
}), nil
@@ -33,7 +34,7 @@ func Test_valid_localhost(t *testing.T) {
args := args{}
actualClient := createLocalClient(args, fakeClientFactory)
actualClient, _ := createLocalClient(args, fakeClientFactory)
assert.NotNil(t, actualClient)
client.AssertExpectations(t)
@@ -42,7 +43,7 @@ func Test_valid_localhost(t *testing.T) {
func Test_invalid_localhost(t *testing.T) {
client := new(fakeCLI)
client.On("ContainerList").Return([]types.Container{}, errors.New("error"))
fakeClientFactory := func(filter map[string][]string) (*docker.Client, error) {
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) {
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
ID: "localhost",
}), nil
@@ -50,7 +51,7 @@ func Test_invalid_localhost(t *testing.T) {
args := args{}
actualClient := createLocalClient(args, fakeClientFactory)
actualClient, _ := createLocalClient(args, fakeClientFactory)
assert.Nil(t, actualClient)
client.AssertExpectations(t)
@@ -59,7 +60,7 @@ func Test_invalid_localhost(t *testing.T) {
func Test_valid_remote(t *testing.T) {
local := new(fakeCLI)
local.On("ContainerList").Return([]types.Container{}, errors.New("error"))
fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
ID: "localhost",
}), nil
@@ -67,7 +68,7 @@ func Test_valid_remote(t *testing.T) {
remote := new(fakeCLI)
remote.On("ContainerList").Return([]types.Container{}, nil)
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
return docker.NewClient(remote, filters.NewArgs(), &docker.Host{
ID: "test",
}), nil
@@ -89,7 +90,7 @@ func Test_valid_remote(t *testing.T) {
func Test_valid_remote_and_local(t *testing.T) {
local := new(fakeCLI)
local.On("ContainerList").Return([]types.Container{}, nil)
fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
ID: "localhost",
}), nil
@@ -97,7 +98,7 @@ func Test_valid_remote_and_local(t *testing.T) {
remote := new(fakeCLI)
remote.On("ContainerList").Return([]types.Container{}, nil)
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
return docker.NewClient(remote, filters.NewArgs(), &docker.Host{
ID: "test",
}), nil
@@ -118,13 +119,13 @@ func Test_valid_remote_and_local(t *testing.T) {
func Test_no_clients(t *testing.T) {
local := new(fakeCLI)
local.On("ContainerList").Return([]types.Container{}, errors.New("error"))
fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
ID: "localhost",
}), nil
}
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
client := new(fakeCLI)
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
ID: "test",
+28 -28
View File
@@ -1,12 +1,12 @@
{
"name": "dozzle",
"version": "6.0.7",
"version": "6.2.0",
"description": "Realtime log viewer for docker containers.",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
"url": "https://github.com/amir20/dozzle/issues"
},
"packageManager": "pnpm@8.14.1",
"packageManager": "pnpm@8.15.1",
"type": "module",
"repository": {
"type": "git",
@@ -28,7 +28,7 @@
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@iconify-json/carbon": "^1.1.27",
"@iconify-json/carbon": "^1.1.28",
"@iconify-json/cil": "^1.1.8",
"@iconify-json/ic": "^1.1.17",
"@iconify-json/mdi": "^1.1.64",
@@ -38,20 +38,20 @@
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.10",
"@vueuse/components": "^10.7.1",
"@vueuse/core": "^10.7.1",
"@vueuse/integrations": "^10.7.1",
"@vueuse/math": "^10.7.1",
"@vueuse/router": "^10.7.1",
"autoprefixer": "^10.4.16",
"@vueuse/components": "^10.7.2",
"@vueuse/core": "^10.7.2",
"@vueuse/integrations": "^10.7.2",
"@vueuse/math": "^10.7.2",
"@vueuse/router": "^10.7.2",
"autoprefixer": "^10.4.17",
"d3-array": "^3.2.4",
"d3-ease": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"d3-transition": "^3.0.1",
"daisyui": "^4.5.0",
"date-fns": "^3.2.0",
"daisyui": "^4.6.1",
"date-fns": "^3.3.1",
"entities": "^4.5.0",
"fuse.js": "^7.0.0",
"lodash.debounce": "^4.0.8",
@@ -60,20 +60,20 @@
"postcss-mixins": "^9.0.4",
"splitpanes": "^3.1.5",
"tailwindcss": "^3.4.1",
"unplugin-auto-import": "^0.17.3",
"unplugin-icons": "^0.18.2",
"unplugin-auto-import": "^0.17.5",
"unplugin-icons": "^0.18.3",
"unplugin-vue-components": "^0.26.0",
"unplugin-vue-macros": "^2.7.9",
"vite": "5.0.11",
"unplugin-vue-macros": "^2.7.10",
"vite": "5.0.12",
"vite-plugin-pages": "^0.32.0",
"vite-plugin-vue-layouts": "^0.11.0",
"vue": "^3.4.8",
"vue-i18n": "^9.9.0",
"vue": "^3.4.15",
"vue-i18n": "^9.9.1",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@pinia/testing": "^0.1.3",
"@playwright/test": "^1.40.1",
"@playwright/test": "^1.41.2",
"@types/d3-array": "^3.2.1",
"@types/d3-ease": "^3.0.2",
"@types/d3-scale": "^4.0.8",
@@ -81,25 +81,25 @@
"@types/d3-shape": "^3.1.6",
"@types/d3-transition": "^3.0.8",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^20.10.8",
"@types/node": "^20.11.16",
"@types/semver": "^7.5.6",
"@vitejs/plugin-vue": "5.0.3",
"@vue/compiler-sfc": "^3.4.8",
"@vue/test-utils": "^2.4.3",
"bumpp": "^9.2.1",
"c8": "^9.0.0",
"@vue/compiler-sfc": "^3.4.15",
"@vue/test-utils": "^2.4.4",
"bumpp": "^9.3.0",
"c8": "^9.1.0",
"concurrently": "^8.2.2",
"eventsourcemock": "^2.0.0",
"jest-serializer-vue": "^3.1.0",
"jsdom": "^23.2.0",
"lint-staged": "^15.2.0",
"prettier": "^3.1.1",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.1",
"prettier": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.5.11",
"simple-git-hooks": "^2.9.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"vitepress": "1.0.0-rc.36",
"vitest": "^1.1.3",
"vitepress": "1.0.0-rc.41",
"vitest": "^1.2.2",
"vue-tsc": "^1.8.27"
},
"lint-staged": {
+560 -519
View File
File diff suppressed because it is too large Load Diff