Compare commits

..

4 Commits

Author SHA1 Message Date
Amir Raminfar 4dfc0e3e59 fixes static errors 2025-04-21 11:11:21 -07:00
Amir Raminfar 4ebd0c3624 removes br check inside static logic 2025-04-21 11:03:19 -07:00
Amir Raminfar 10baad1d4a adds gzip to sse 2025-04-21 09:50:14 -07:00
Amir Raminfar cceab9bd29 feat: enables brotli globally 2025-04-21 09:37:39 -07:00
106 changed files with 1960 additions and 2117 deletions
+2 -2
View File
@@ -31,7 +31,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.24.3
go-version: 1.24.2
check-latest: true
- name: Checkout code
uses: actions/checkout@v4
@@ -121,7 +121,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.17.0
uses: docker/build-push-action@v6.15.0
with:
push: true
context: .
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.17.0
uses: docker/build-push-action@v6.15.0
with:
context: .
push: true
+2 -2
View File
@@ -54,7 +54,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: "1.24.3"
go-version: "1.24.2"
check-latest: true
- name: Checkout code
uses: actions/checkout@v4
@@ -77,7 +77,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.24.3"
go-version: "1.24.2"
check-latest: true
- name: Generate dependencies
run: make fake_assets shared_key.pem shared_cert.pem
+1 -1
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:23.11.1-alpine AS node
FROM --platform=$BUILDPLATFORM node:23.11.0-alpine AS node
RUN corepack enable
-2
View File
@@ -151,7 +151,6 @@ declare global {
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toRelativeTime: typeof import('./utils/index')['toRelativeTime']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
@@ -543,7 +542,6 @@ declare module 'vue' {
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toRelativeTime: UnwrapRef<typeof import('./utils/index')['toRelativeTime']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
+1 -2
View File
@@ -29,7 +29,6 @@ declare module 'vue' {
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default']
ContainerDropdown: typeof import('./components/ContainerDropdown.vue')['default']
ContainerEventLogItem: typeof import('./components/LogViewer/ContainerEventLogItem.vue')['default']
ContainerHealth: typeof import('./components/ContainerViewer/ContainerHealth.vue')['default']
ContainerLog: typeof import('./components/ContainerViewer/ContainerLog.vue')['default']
@@ -37,6 +36,7 @@ declare module 'vue' {
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default']
DateTime: typeof import('./components/common/DateTime.vue')['default']
DistanceTime: typeof import('./components/common/DistanceTime.vue')['default']
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
@@ -104,7 +104,6 @@ declare module 'vue' {
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
Popup: typeof import('./components/Popup.vue')['default']
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
RelativeTime: typeof import('./components/common/RelativeTime.vue')['default']
'Ri:terminalWindowFill': typeof import('~icons/ri/terminal-window-fill')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
+2 -2
View File
@@ -22,7 +22,7 @@
>
{{ release.name }}
</a>
<span class="ml-1 text-xs"><RelativeTime :date="release.createdAt" /></span>
<span class="ml-1 text-xs"><distance-time :date="release.createdAt" /></span>
</div>
<div class="text-base-content/80 text-sm">
{{ release.body }}
@@ -39,7 +39,7 @@
>
{{ release.name }}
</a>
<span class="ml-1 text-xs"><RelativeTime :date="release.createdAt" /></span>
<span class="ml-1 text-xs"><distance-time :date="release.createdAt" /></span>
<Tag class="bg-red ml-auto px-1 py-1 text-xs" v-if="release.latest">
{{ $t("releases.latest") }}
</Tag>
-24
View File
@@ -1,24 +0,0 @@
<template>
<div class="dropdown">
<button tabindex="0" role="button" class="btn btn-xs md:btn-sm"><slot /> <carbon:caret-down /></button>
<ul tabindex="0" class="dropdown-content menu rounded-box bg-base-100 shadow-sm">
<li v-for="other in containers">
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }" class="text-nowrap">
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="other.state"
></div>
{{ other.name }}
<div v-if="other.state === 'running'">running</div>
<RelativeTime :date="other.finishedAt" class="text-base-content/70 text-xs" v-else />
</router-link>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { type Container } from "@/models/Container";
const { containers } = defineProps<{
containers: Container[];
}>();
</script>
+24 -28
View File
@@ -1,32 +1,28 @@
<template>
<table class="w-full border-separate border-spacing-x-1">
<tbody>
<tr>
<th class="text-right font-light capitalize">STATE</th>
<td class="font-semibold uppercase">{{ container.state }}</td>
</tr>
<tr v-if="container.startedAt.getFullYear() > 0">
<th class="text-right font-light capitalize">STARTED</th>
<td class="font-semibold">
<RelativeTime :date="container.startedAt" />
</td>
</tr>
<tr v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
<th class="text-right font-light capitalize">FINISHED</th>
<td class="font-semibold">
<RelativeTime :date="container.finishedAt" />
</td>
</tr>
<tr v-if="container.state == 'running'">
<th class="text-right font-light capitalize">Load</th>
<td class="font-semibold">{{ container.stat.cpu.toFixed(2) }}%</td>
</tr>
<tr v-if="container.state == 'running'">
<th class="text-right font-light capitalize">MEM</th>
<td class="font-semibold">{{ formatBytes(container.stat.memoryUsage) }}</td>
</tr>
</tbody>
</table>
<div>
<span class="font-light capitalize"> STATE </span>
<span class="font-semibold uppercase"> {{ container.state }} </span>
</div>
<div v-if="container.startedAt.getFullYear() > 0">
<span class="font-light capitalize"> STARTED </span>
<span class="font-semibold">
<DistanceTime :date="container.startedAt" strict />
</span>
</div>
<div v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
<span class="font-light capitalize"> FINISHED </span>
<span class="font-semibold">
<DistanceTime :date="container.finishedAt" strict />
</span>
</div>
<div v-if="container.state == 'running'">
<span class="font-light capitalize"> Load </span>
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
</div>
<div v-if="container.state == 'running'">
<span class="font-light capitalize"> MEM </span>
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
</div>
</template>
<script lang="ts" setup>
+106 -112
View File
@@ -1,118 +1,112 @@
!
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-row">
<div v-if="Object.keys(hosts).length > 1" class="flex-1">
<div role="tablist" class="tabs-boxed tabs block" v-if="Object.keys(hosts).length < 4">
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
aria-label="Show All"
v-model="selectedHost"
:value="null"
/>
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
:aria-label="host.name"
v-for="host in hosts"
:value="host.id"
:key="host.id"
v-model="selectedHost"
/>
</div>
<DropdownMenu
class="btn-sm"
v-model="selectedHost"
:options="[
{ label: 'Show All', value: null },
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
]"
v-else
/>
</div>
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
{{ $t("label.per-page") }}
<DropdownMenu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
/>
</div>
</div>
<div class="rounded-box border-base-content/10 overflow-x-auto border">
<table class="table-md md:table-lg table-zebra table">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
v-for="(value, key) in fields"
:key="key"
@click.prevent="sort(key)"
:class="{ 'selected-sort': key === sortField }"
v-show="isVisible(key)"
>
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
<span>{{ $t(value.label) }}</span>
<span class="h-4" data-icon>
<mdi:arrow-up />
</span>
</a>
</th>
</tr>
</thead>
<tbody class="bg-base-300/30">
<tr v-for="container in paginated" :key="container.id" class="hover:bg-base-100/80!">
<td v-if="isVisible('name')">
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
<td v-if="isVisible('state')">{{ container.state }}</td>
<td v-if="isVisible('created')">
<RelativeTime :date="container.created" />
</td>
<td v-if="isVisible('cpu')">
<div class="flex flex-row items-center gap-1">
<progress
class="progress progress-primary"
:value="Math.min(container.movingAverage.cpu, 100)"
:max="100"
></progress>
<span class="text-sm">{{ container.movingAverage.cpu.toFixed(0) }}%</span>
</div>
</td>
<td v-if="isVisible('mem')">
<div class="flex flex-row items-center gap-1">
<progress
class="progress progress-primary"
:value="container.movingAverage.memory"
max="100"
></progress>
<span class="text-sm">{{ container.movingAverage.memory.toFixed(0) }}%</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated">
<div class="flex flex-row">
<div v-if="Object.keys(hosts).length > 1" class="flex-1">
<div role="tablist" class="tabs-boxed tabs block" v-if="Object.keys(hosts).length < 4">
<input
class="btn btn-square join-item"
type="radio"
v-model="currentPage"
:aria-label="`${i}`"
:value="i"
v-for="i in totalPages"
name="host"
role="tab"
class="tab rounded-sm!"
aria-label="Show All"
v-model="selectedHost"
:value="null"
/>
</nav>
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
:aria-label="host.name"
v-for="host in hosts"
:value="host.id"
:key="host.id"
v-model="selectedHost"
/>
</div>
<DropdownMenu
class="btn-sm"
v-model="selectedHost"
:options="[
{ label: 'Show All', value: null },
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
]"
v-else
/>
</div>
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
{{ $t("label.per-page") }}
<DropdownMenu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
/>
</div>
</div>
<table class="table-lg bg-base-200 table">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
v-for="(value, key) in fields"
:key="key"
@click.prevent="sort(key)"
:class="{ 'selected-sort': key === sortField }"
v-show="isVisible(key)"
>
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
<span>{{ $t(value.label) }}</span>
<span class="h-4" data-icon>
<mdi:arrow-up />
</span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-for="container in paginated" :key="container.id">
<td v-if="isVisible('name')">
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
<td v-if="isVisible('state')">{{ container.state }}</td>
<td v-if="isVisible('created')">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</td>
<td v-if="isVisible('cpu')">
<div class="flex flex-row items-center gap-1">
<progress
class="progress progress-primary"
:value="Math.min(container.movingAverage.cpu, 100)"
:max="100"
></progress>
<span class="text-sm">{{ container.movingAverage.cpu.toFixed(0) }}%</span>
</div>
</td>
<td v-if="isVisible('mem')">
<div class="flex flex-row items-center gap-1">
<progress class="progress progress-primary" :value="container.movingAverage.memory" max="100"></progress>
<span class="text-sm">{{ container.movingAverage.memory.toFixed(0) }}%</span>
</div>
</td>
</tr>
</tbody>
</table>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated">
<input
class="btn btn-square join-item"
type="radio"
v-model="currentPage"
:aria-label="`${i}`"
:value="i"
v-for="i in totalPages"
/>
</nav>
</div>
</template>
@@ -203,7 +197,7 @@ function isVisible(field: keys) {
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
[data-icon] {
display: none;
@@ -214,7 +208,7 @@ function isVisible(field: keys) {
}
th {
@apply border-base-200 border-b-2;
@apply border-base-100 border-b-2;
&.selected-sort {
font-weight: bold;
@apply border-primary;
@@ -220,7 +220,7 @@ const toggleAllLevels = computed({
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
li.line {
@apply bg-base-content/20 h-px;
@@ -15,7 +15,7 @@ defineProps<{
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
[health="unhealthy"] {
@apply text-red;
}
@@ -1,45 +1,44 @@
<template>
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
<div class="@container flex flex-1 items-center gap-1.5 truncate md:gap-2">
<label class="swap swap-rotate size-4">
<input type="checkbox" v-model="pinned" />
<carbon:star-filled class="swap-on text-secondary" />
<carbon:star class="swap-off" />
</label>
<div class="inline-flex items-center text-sm">
<div class="breadcrumbs overflow-x-visible p-0 font-mono">
<div class="breadcrumbs p-0 font-mono">
<ul>
<li v-if="config.hosts.length > 1" class="font-thin max-md:hidden">
{{ container.hostLabel }}
</li>
<li>
<template v-if="otherContainers.length === 0">{{ container.name }}</template>
<div v-else>
<div class="dropdown">
<button tabindex="0" role="button" class="btn btn-xs md:btn-sm">
{{ container.name }} <carbon:caret-down />
</button>
<ul tabindex="0" class="dropdown-content menu rounded-box bg-base-100 shadow-sm">
<li v-for="other in otherContainers">
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }">
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="other.state"
></div>
<div v-if="other.isSwarm">{{ other.swarmId }}</div>
<div v-else>{{ other.name }}</div>
<div v-if="other.state === 'running'">running</div>
<RelativeTime :date="other.finishedAt" class="text-base-content/70 text-xs" v-else />
</router-link>
</li>
</ul>
</div>
<div class="wrapper" ref="wrapper" v-else>
<button popovertarget="popover-container-list" class="btn btn-xs md:btn-sm anchor">
{{ container.name }} <carbon:caret-down />
</button>
<ul popover id="popover-container-list" class="dropdown menu rounded-box bg-base-100 tethered shadow-sm">
<li v-for="other in otherContainers">
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }">
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="other.state"
></div>
<div v-if="other.isSwarm">{{ other.swarmId }}</div>
<div v-else>{{ other.name }}</div>
<div v-if="other.state === 'running'">running</div>
<DistanceTime :date="other.created" strict class="text-base-content/70 text-xs" v-else />
</router-link>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
<ContainerHealth :health="container.health" v-if="container.health" />
<Tag class="hidden! font-mono @xl:block!" size="small">
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
{{ container.image.replace(/@sha.*/, "") }}
</Tag>
</div>
@@ -67,6 +66,34 @@ const otherContainers = computed(() =>
.filter((c) => c.name === container.name && c.id !== container.id)
.sort((a, b) => +b.created - +a.created),
);
const wrapper = useTemplateRef("wrapper");
onMounted(async () => {
if (!("anchorName" in document.documentElement.style)) {
// @ts-ignore
const module = await import("@oddbird/css-anchor-positioning/fn");
// @ts-ignore
await module.default([wrapper.value]);
}
});
</script>
<style scoped></style>
<style scoped>
/* https://github.com/oddbird/css-anchor-positioning/issues/282 */
.wrapper {
anchor-scope: --anchor;
}
.anchor {
anchor-name: --anchor;
}
.tethered {
margin: 0;
padding: 0;
position-anchor: --anchor;
position: absolute;
top: anchor(bottom);
left: anchor(left);
}
</style>
+5 -8
View File
@@ -13,11 +13,8 @@
v-model="query"
:placeholder="$t('placeholder.search-containers')"
/>
<form method="dialog" class="flex">
<button v-if="isMobile">
<mdi:close />
</button>
<button v-else class="swap hover:swap-active outline-hidden">
<form method="dialog">
<button class="swap hover:swap-active">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
@@ -33,7 +30,7 @@
<a
class="grid auto-cols-max grid-cols-[min-content_auto] gap-2 py-4"
@click.prevent="selected(result.item)"
:class="{ 'menu-focus': index === selectedIndex }"
:class="index === selectedIndex ? 'menu-focus' : ''"
>
<div :class="{ 'text-primary': result.item.state === 'running' }">
<template v-if="result.item.type === 'container'">
@@ -53,7 +50,7 @@
<span data-name v-html="matchedName(result)"></span>
</div>
<RelativeTime :date="result.item.created" class="text-xs font-light" />
<DistanceTime :date="result.item.created" class="text-xs font-light" />
<span
@click.stop.prevent="addColumn(result.item)"
:title="$t('tooltip.pin-column')"
@@ -217,7 +214,7 @@ function matchedName({ item, matches = [] }: FuseResult<Item>) {
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
:deep(mark) {
@apply bg-transparent text-inherit underline underline-offset-2;
}
@@ -2,9 +2,11 @@
<ScrollableView :scrollable="scrollable" v-if="group.containers.length && ready">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<ContainerDropdown :containers="group.containers">
{{ $t("label.container", group.containers.length) }}
</ContainerDropdown>
<div class="@container flex flex-1 items-center gap-1.5 truncate md:gap-2">
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ $t("label.container", group.containers.length) }}</div>
</div>
</div>
<MultiContainerStat class="ml-auto" :containers="group.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
</div>
+2 -1
View File
@@ -31,7 +31,8 @@
<a class="text-sm capitalize" @click="toggleShowAllContainers()">
<mdi:check class="w-4" v-if="showAllContainers" />
<div v-else class="w-4"></div>
{{ $t("label.show-all-containers") }}
Show all containers
</a>
</li>
</ul>
+1 -1
View File
@@ -2,7 +2,7 @@
<ScrollableView :scrollable="scrollable" v-if="host">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 items-center gap-1.5 truncate md:gap-2">
<div class="flex flex-1 gap-1.5 truncate md:gap-2">
<ph:computer-tower />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ host.name }}</div>
@@ -30,7 +30,7 @@ const showDrawer = useDrawer();
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
.text-light {
@apply text-base-content/70;
}
@@ -13,7 +13,7 @@ const { logEntry } = defineProps<{
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
[data-event="container-stopped"] {
@apply text-red;
}
+8 -24
View File
@@ -7,9 +7,7 @@
</div>
<span class="sr-only">Loading...</span>
</ul>
<div v-else-if="noLogs && !waitingForMoreLog" class="p-4">
{{ $t("label.no-logs") }}
</div>
<div v-else-if="noLogs && !waitingForMoreLog" class="p-4">Container has no logs yet</div>
<slot :messages="messages" v-else></slot>
<IndeterminateBar :color />
</template>
@@ -47,25 +45,11 @@ const fetchMore = async () => {
}
};
const sizes = computedWithControl(eventSourceURL, () => {
const sizeOptions = [
"w-2/12",
"w-3/12",
"w-4/12",
"w-5/12",
"w-6/12",
"w-7/12",
"w-8/12",
"w-9/12",
"w-10/12",
"w-11/12",
"w-full",
];
const result = [];
const iterations = 18;
for (let i = 0; i < iterations; i++) {
result.push(sizeOptions[Math.floor(Math.random() * sizeOptions.length)]);
}
return result;
});
const shuffle = (items: any[]) => {
return items.sort(() => Math.random() - 0.5);
};
const sizes = computedWithControl(eventSourceURL, () =>
shuffle(["w-3/5", "w-2/3", "w-9/12", "w-1/2", "w-1/3", "w-3/4"]),
);
</script>
+1 -1
View File
@@ -2,7 +2,7 @@
<aside>
<header class="flex items-center gap-4">
<h1 class="text-2xl max-md:hidden">{{ container.name }}</h1>
<h2 class="text-sm"><RelativeTime :date="container.created" /></h2>
<h2 class="text-sm"><DistanceTime :date="container.created" /></h2>
</header>
<div class="mt-8 flex flex-col gap-2">
+2 -2
View File
@@ -4,7 +4,7 @@
<h1 class="text-lg max-md:hidden">
<DateTime :date="entry.date" />
</h1>
<h2 class="text-sm"><RelativeTime :date="entry.date" /> on {{ entry.std }}</h2>
<h2 class="text-sm"><DistanceTime :date="entry.date" /> on {{ entry.std }}</h2>
</header>
<div class="mt-8 flex flex-col gap-10">
@@ -166,7 +166,7 @@ function syntaxHighlight(json: any) {
useSortable(list, fields);
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
.font-mono {
font-family:
ui-monospace,
+3 -11
View File
@@ -1,17 +1,9 @@
<template>
<div class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<LogStd :std="logEntry.std" class="shrink-0 select-none" v-if="showStd" />
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<RandomColorTag class="w-30 shrink-0 select-none md:w-40" :value="host.name" v-if="showHostname" />
<RandomColorTag
class="w-30 shrink-0 select-none group-[.compact]:flex-1 md:w-40"
:value="container.name"
v-if="showContainerName"
truncateRight
/>
<LogDate :date="logEntry.date" v-if="showTimestamp" class="shrink-0 select-none" />
</div>
<RandomColorTag class="shrink-0 select-none" :value="host.name" v-if="showHostname" />
<RandomColorTag class="shrink-0 select-none" :value="container.name" v-if="showContainerName" truncateRight />
<LogDate :date="logEntry.date" v-if="showTimestamp" class="shrink-0 select-none" />
<LogLevel
class="flex select-none"
:level="logEntry.level"
+2 -2
View File
@@ -11,7 +11,7 @@ defineProps<{
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
@@ -35,7 +35,7 @@ defineProps<{
}
</style>
<style>
@reference "@/main.css";
@import "@/main.css" reference;
[data-level="debug"],
[data-level="trace"] {
@apply !bg-purple;
+1 -1
View File
@@ -54,7 +54,7 @@ useIntersectionObserver(
);
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
.events {
font-family:
ui-monospace,
+1 -1
View File
@@ -13,7 +13,7 @@ defineProps<{
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
[std="stdout"] {
@apply text-blue;
}
@@ -103,7 +103,7 @@ const downloadUrl = computed(() =>
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
li.line {
@apply bg-base-content/20 h-px;
}
@@ -55,42 +55,15 @@ watch(
{ immediate: true },
);
const limits = computed(() => {
const hostLimits = new Map<string, { cpu: number; memory: number }>();
for (const container of containers) {
if (!hostLimits.has(container.host)) {
hostLimits.set(container.host, {
cpu: 0,
memory: 0,
});
}
if (hostLimits.get(container.host)!.cpu < hosts.value[container.host].nCPU) {
if (container.cpuLimit == 0) {
hostLimits.get(container.host)!.cpu = hosts.value[container.host].nCPU;
} else {
hostLimits.get(container.host)!.cpu = hostLimits.get(container.host)!.cpu + container.cpuLimit;
}
}
if (hostLimits.get(container.host)!.memory < hosts.value[container.host].memTotal) {
if (container.memoryLimit == 0) {
hostLimits.get(container.host)!.memory = hosts.value[container.host].memTotal;
} else {
hostLimits.get(container.host)!.memory = hostLimits.get(container.host)!.memory + container.memoryLimit;
}
}
}
return hostLimits.values().reduce(
(acc, { cpu, memory }) => {
return {
cpu: acc.cpu + cpu,
memory: acc.memory + memory,
};
},
const limits = computed(() =>
containers.reduce(
(acc, c) => ({
cpu: acc.cpu + (c.cpuLimit > 0 ? c.cpuLimit : hosts.value[c.host].nCPU),
memory: acc.memory + (c.memoryLimit > 0 ? c.memoryLimit : hosts.value[c.host].memTotal),
}),
{ cpu: 0, memory: 0 },
);
});
),
);
useIntervalFn(() => {
totalStat.value = containers.reduce(
@@ -1,5 +1,5 @@
<template>
<div class="tag grid overflow-hidden rounded-sm text-center text-sm text-white">
<div class="tag grid w-40 overflow-hidden rounded-sm text-center text-sm text-white">
<div class="random-color col-start-1 row-start-1 brightness-75"></div>
<div class="col-start-1 row-start-1 truncate px-2 brightness-100" :class="truncateRight ? '[direction:rtl]' : ''">
{{ value }}
@@ -35,7 +35,7 @@ const linkify = (text: string) =>
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
.log-wrapper :deep(a) {
@apply text-primary underline-offset-4 hover:underline;
}
@@ -5,12 +5,10 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<!--v-if-->
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
@@ -27,12 +25,10 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<!--v-if-->
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
</div>
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
@@ -49,12 +45,10 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<!--v-if-->
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
@@ -2,8 +2,12 @@
<ScrollableView :scrollable="scrollable" v-if="containers.length && ready">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<octicon:container-24 />
<ContainerDropdown :containers="containers">{{ $t("label.container", containers.length) }}</ContainerDropdown>
<div class="@container flex flex-1 items-center gap-1.5 truncate md:gap-2">
<octicon:container-24 />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ containers.length }} containers</div>
</div>
</div>
<MultiContainerStat class="ml-auto" :containers="containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
</div>
+6 -6
View File
@@ -3,8 +3,8 @@
<teleport to="body">
<transition name="fade">
<div
v-show="show && (delayedShow || globalShow)"
class="ring-base-content/20 bg-base-100 fixed z-50 rounded-sm p-3 shadow-sm ring"
v-show="show && (delayedShow || glopbalShow)"
class="border-base-content/20 bg-base-100 fixed z-50 rounded-sm border p-4 shadow-sm"
ref="content"
>
<slot name="content"></slot>
@@ -16,14 +16,14 @@
<script lang="ts" setup>
import { globalShowPopup } from "@/composable/popup";
const globalShow = globalShowPopup();
const show = ref(globalShow.value);
const glopbalShow = globalShowPopup();
const show = ref(glopbalShow.value);
const delayedShow = refDebounced(show, 1000);
const content = ref<HTMLElement>();
const onMouseEnter = (e: Event) => {
show.value = true;
globalShow.value = true;
glopbalShow.value = true;
if (content.value && e.target instanceof HTMLElement) {
const { left, top, width } = e.target.getBoundingClientRect();
@@ -37,7 +37,7 @@ const onMouseEnter = (e: Event) => {
const onMouseLeave = () => {
show.value = false;
globalShow.value = false;
glopbalShow.value = false;
};
const el: Ref<HTMLElement> = useCurrentElement();
+1 -1
View File
@@ -12,7 +12,7 @@
<span> % </span>
</div>
</div>
<RelativeTime :date="date" class="text-sm whitespace-nowrap" />
<DistanceTime :date="date" class="text-sm whitespace-nowrap" />
</div>
</transition>
</template>
+2 -2
View File
@@ -2,7 +2,7 @@
<section :class="{ 'h-screen min-h-0': scrollable }" class="flex flex-col">
<header
v-if="$slots.header"
class="border-base-content/10 bg-base-200 sticky top-[calc(55px+env(safe-area-inset-top))] z-2 border-b py-2 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0"
class="border-base-content/10 bg-base-200 sticky top-[55px] z-2 border-b py-2 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0"
>
<slot name="header"></slot>
</header>
@@ -28,7 +28,7 @@
<div class="mr-16 text-right">
<transition name="fade">
<button
class="btn btn-primary text-primary-content fixed bottom-8 rounded-sm p-3 shadow-sm transition-colors"
class="transition-colorsblur-xs dark btn btn-primary text-primary-content fixed bottom-8 rounded-sm p-3 shadow-sm"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()"
v-show="scrollContext.paused"
@@ -2,8 +2,15 @@
<ScrollableView :scrollable="scrollable" v-if="service.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<ph:stack-simple />
<ContainerDropdown :containers="service.containers">{{ service.name }}</ContainerDropdown>
<div class="@container flex flex-1 items-center gap-1.5 truncate md:gap-2">
<ph:stack-simple />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ service.name }}</div>
</div>
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
{{ $t("label.container", service.containers.length) }}
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="service.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
</div>
+8 -7
View File
@@ -1,16 +1,17 @@
<template>
<aside class="fixed flex h-screen w-[inherit] flex-col gap-4 p-3" data-testid="navigation">
<h1>
<router-link :to="{ name: '/' }" class="flex w-full items-center gap-4 overflow-hidden text-4xl font-thin">
<Logo class="h-14 w-14 shrink-0" />
Dozzle
<router-link :to="{ name: '/' }">
<LogoWithText class="logo [&_.secondary-fill]:fill-secondary [&_.content-fill]:fill-base-content h-16 w-40" />
</router-link>
<small class="mt-4 block text-sm font-light" v-if="hostname">{{ hostname }}</small>
<small class="mb-4 block text-xs font-light" v-if="hostname">
{{ hostname }}
</small>
</h1>
<button
class="input input-sm hover:border-primary mt-2 inline-flex w-auto cursor-pointer items-center gap-2 self-start font-light"
class="input input-sm hover:border-primary inline-flex w-auto cursor-pointer items-center gap-2 self-start font-light"
@click="$emit('search')"
:title="$t('tooltip.search')"
data-testid="search"
@@ -20,12 +21,12 @@
<key-shortcut char="k" class="text-base-content/70"></key-shortcut>
</button>
<SideMenu class="flex-1" />
<SideMenu class="mt-2 flex-1" />
</aside>
</template>
<script lang="ts" setup>
import Logo from "@/logo.svg";
import LogoWithText from "@/logo-text.svg";
const { hostname } = config;
</script>
+9 -4
View File
@@ -2,12 +2,17 @@
<ScrollableView :scrollable="scrollable" v-if="stack.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
<div class="@container flex flex-1 items-center gap-1.5 truncate md:gap-2">
<ph:stack />
<div class="font-mono text-sm font-semibold">{{ stack.name }}</div>
<ContainerDropdown :containers="stack.containers">
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ stack.name }}</div>
</div>
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
{{ $t("label.container", stack.containers.length) }}
</ContainerDropdown>
</Tag>
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
{{ $t("label.service", stack.services.length) }}
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="stack.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
+2 -2
View File
@@ -3,7 +3,7 @@
<header class="flex items-center gap-4">
<material-symbols:terminal class="size-8" />
<h1 class="text-2xl max-md:hidden">{{ container.name }}</h1>
<h2 class="text-sm">Started <RelativeTime :date="container.created" /></h2>
<h2 class="text-sm">Started <DistanceTime :date="container.created" /></h2>
</header>
<div class="mt-8 flex flex-col gap-2">
@@ -58,7 +58,7 @@ onUnmounted(() => {
});
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
.shell {
& :deep(.terminal) {
+30
View File
@@ -0,0 +1,30 @@
<template>
<time :datetime="date.toISOString()" data-ci-skip>{{ text }}</time>
</template>
<script lang="ts" setup>
import { formatDistanceToNow } from "date-fns/formatDistanceToNow";
import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict";
const {
date,
strict = false,
suffix = true,
} = defineProps<{
date: Date;
strict?: boolean;
suffix?: boolean;
}>();
const text = ref<string>();
watch($$(date), updateFromNow, { immediate: true });
function updateFromNow() {
const fn = strict ? formatDistanceToNowStrict : formatDistanceToNow;
text.value = fn(date, {
addSuffix: suffix,
});
}
useIntervalFn(updateFromNow, 30_000, { immediateCallback: true });
</script>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<details class="dropdown dropdown-end" ref="details" v-on-click-outside="close">
<details class="dropdown" ref="details" v-on-click-outside="close">
<summary class="btn btn-primary flex-nowrap" v-bind="$attrs">
<slot name="trigger"> {{ label }} <carbon:caret-down /></slot>
</summary>
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<label class="label text-base-content cursor-pointer gap-4">
<div class="flex-1 whitespace-normal"><slot name="label" /></div>
<div class="flex-1"><slot name="label" /></div>
<slot name="input" />
</label>
</template>
+21 -23
View File
@@ -1,29 +1,27 @@
<template>
<nav class="border-base-content/20 bg-base-200 pt-safe fixed top-0 z-10 w-full border-b" data-testid="navigation">
<div class="p-2">
<div class="flex items-center">
<router-link :to="{ name: '/' }">
<Logo class="h-10" />
</router-link>
<nav class="border-base-content/20 bg-base-200 fixed top-0 z-10 w-full border-b p-2" data-testid="navigation">
<div class="flex items-center">
<router-link :to="{ name: '/' }">
<Logo class="logo [&_.secondary-fill]:fill-secondary h-8" />
</router-link>
<div class="ml-auto flex items-center gap-2">
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
<mdi:magnify class="size-5" />
</a>
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
<input type="checkbox" v-model="show" />
<mdi:close class="swap-on" />
<mdi:hamburger-menu class="swap-off" />
</label>
</div>
<div class="ml-auto flex items-center gap-2">
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
<mdi:magnify class="size-5" />
</a>
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
<input type="checkbox" v-model="show" />
<mdi:close class="swap-on" />
<mdi:hamburger-menu class="swap-off" />
</label>
</div>
<transition name="fade">
<div v-show="show" class="flex h-[calc(100svh-55px)]">
<SideMenu class="flex-1" />
</div>
</transition>
</div>
<transition name="fade">
<div v-show="show" class="flex h-[calc(100svh-55px)]">
<SideMenu class="flex-1" />
</div>
</transition>
</nav>
</template>
@@ -37,7 +35,7 @@ watch(route, () => {
});
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
-17
View File
@@ -1,17 +0,0 @@
<template>
<time :datetime="date.toISOString()" data-ci-skip>{{ text }}</time>
</template>
<script lang="ts" setup>
const { date } = defineProps<{
date: Date;
}>();
const text = ref<string>();
const updateFromNow = () => {
text.value = toRelativeTime(date, locale.value === "" ? undefined : locale.value);
};
watch(() => date, updateFromNow, { immediate: true });
useIntervalFn(updateFromNow, 30_000);
</script>
+8 -13
View File
@@ -1,18 +1,13 @@
<template>
<dialog ref="panel" class="modal-right modal items-start outline-hidden backdrop:bg-none">
<div class="modal-box" :width="width">
<div class="pt-safe relative">
<form method="dialog" class="absolute right-0">
<button v-if="isMobile">
<mdi:close />
</button>
<button v-else class="swap hover:swap-active outline-hidden">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
<slot v-if="open"></slot>
</div>
<form method="dialog">
<button class="swap hover:swap-active absolute top-4 right-4 outline-hidden">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
<slot v-if="open"></slot>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
@@ -38,7 +33,7 @@ defineExpose({
useEventListener(panel, "close", () => (open.value = false));
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
.modal-right :where(.modal-box) {
@apply bg-base-100 fixed right-0 h-lvh max-h-screen translate-x-24 scale-100 rounded-none shadow-none;
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div>
<MobileMenu v-if="isMobile && !forceMenuHidden" @search="showFuzzySearch"></MobileMenu>
<mobile-menu v-if="isMobile && !forceMenuHidden" @search="showFuzzySearch"></mobile-menu>
<Splitpanes @resized="onResized($event)">
<Pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav && !forceMenuHidden">
<SidePanel @search="showFuzzySearch" />
@@ -95,7 +95,7 @@ function onResized({ panes }: { panes: { size: number }[] }) {
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
:deep(.splitpanes--vertical > .splitpanes__splitter) {
@apply bg-base-100 hover:bg-secondary min-w-[5px];
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="hero max-h-500:pt-4 min-h-dvh content-start pt-[20dvh]">
<div class="hero min-h-dvh">
<div class="hero-content">
<router-view></router-view>
</div>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 396 114"><path class="secondary-fill" d="M89.408 46.477a6.49 6.49 0 0 0-6.488 6.49 6.488 6.488 0 1 0 12.978 0 6.49 6.49 0 0 0-6.49-6.49m-42.836 0a6.49 6.49 0 1 0 0 12.98 6.49 6.49 0 0 0 0-12.98M28.5 38.295a6.5 6.5 0 0 1-1.897-4.593 6.474 6.474 0 0 1 6.49-6.49c1.663 0 3.327.631 4.592 1.896l-1.517 1.12a51.67 51.67 0 0 1 31.822-10.91c11.999 0 23.028 4.078 31.823 10.91L98.3 29.113a6.47 6.47 0 0 1 4.592-1.897c1.659 0 3.324.63 4.588 1.897a6.52 6.52 0 0 1 1.892 4.593 6.47 6.47 0 0 1-1.892 4.594l1.514 1.113c21.944 21.942-15.406 31.822-15.406 31.822H42.392c-1.077 0-38.88-8.35-15.406-31.822zM102.214 94.411a7.894 7.894 0 0 1-7.894-7.895 7.893 7.893 0 1 1 15.787 0 7.89 7.89 0 0 1-7.893 7.895M37.355 95.13a7.894 7.894 0 0 1-7.895-7.896 7.894 7.894 0 0 1 7.895-7.893 7.894 7.894 0 0 1 7.896 7.893 7.894 7.894 0 0 1-7.896 7.895M44.372 72.866h47.402s-22.981 7.899-47.402 0"/><path class="content-fill" d="M142 79.928V33.446h13.836c3.571 0 6.886.574 9.947 1.786 3.06 1.147 5.675 2.805 7.779 4.846 4.272 4.144 6.822 10.01 6.822 16.514 0 3.379-.637 6.567-1.913 9.5-1.275 2.933-3.06 5.356-5.228 7.396-4.4 4.017-10.648 6.44-17.407 6.44zm4.91-41.89v37.3h8.735c3.889 0 7.332-.83 10.265-2.423 2.997-1.595 5.356-3.762 6.95-6.568 1.658-2.805 2.487-6.057 2.487-9.628 0-5.292-1.977-10.01-5.356-13.39-3.315-3.315-8.48-5.292-14.346-5.292zm86.587 18.554c0 4.4-1.02 8.416-2.997 12.05q-3.06 5.452-8.416 8.608c-3.634 2.104-7.588 3.188-12.051 3.188-3.252 0-6.312-.637-9.245-1.849-2.869-1.211-5.356-2.869-7.397-5.037-2.04-2.04-3.634-4.527-4.845-7.46-1.148-2.933-1.786-5.993-1.786-9.245q0-6.6 3.061-12.051c1.976-3.635 4.782-6.568 8.416-8.672C201.808 34.02 205.825 33 210.224 33c3.252 0 6.313.574 9.246 1.785 2.869 1.276 5.356 2.933 7.396 4.974 2.04 2.168 3.698 4.654 4.846 7.523 1.211 2.934 1.785 5.994 1.785 9.31m-5.037.255c0-5.229-1.913-10.075-5.101-13.581s-7.97-5.739-13.326-5.739c-3.443 0-6.567.893-9.373 2.55-2.805 1.786-4.973 4.081-6.503 6.95q-2.296 4.4-2.296 9.565c0 5.164 1.913 10.074 5.101 13.517 3.188 3.507 7.97 5.739 13.262 5.739 3.444 0 6.568-.893 9.373-2.615 2.87-1.657 5.037-4.017 6.568-6.886 1.53-2.87 2.295-6.057 2.295-9.5m43.932 18.618v4.463h-33.539v-4.463l27.672-37.555H239.81v-4.464h32.454v4.464l-27.8 37.555zm38.192 0v4.463h-33.538v-4.463l27.672-37.555h-26.715v-4.464h32.454v4.464l-27.8 37.555zm8.927 4.463V33.446h4.91v41.891h22.061v4.591zm34.622 0V33.446h29.075v4.527h-24.165v16.068h22.125v4.527h-22.125v16.833h24.42v4.527z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 934 B

-10
View File
@@ -11,16 +11,6 @@
--color-orange: oklch(70% 0.186 48.13);
}
@utility pt-safe {
padding-top: env(safe-area-inset-top);
}
@custom-variant max-h-500 {
@media (max-height: 500px) {
@slot;
}
}
@plugin "daisyui/theme" {
name: "dark";
default: false;
+1 -12
View File
@@ -39,7 +39,6 @@ watch(currentContainer, () => (redirectTrigger.value = false));
watchEffect(() => {
if (redirectTrigger.value) return;
if (automaticRedirect.value === "none") return;
if (!currentContainer.value) return;
if (currentContainer.value.state === "running") return;
if (Date.now() - +currentContainer.value.finishedAt > 5 * 60 * 1000) return;
@@ -50,7 +49,7 @@ watchEffect(() => {
if (!nextContainer) return;
if (automaticRedirect.value === "delayed") {
if (automaticRedirect.value) {
redirectTrigger.value = true;
showToast(
{
@@ -74,16 +73,6 @@ watchEffect(() => {
},
{ timed: 4000 },
);
} else {
router.push({ name: "/container/[id]", params: { id: nextContainer.id } });
showToast(
{
title: t("alert.redirected.title"),
message: t("alert.redirected.message", { containerId: nextContainer.id }),
type: "info",
},
{ expire: 3000 },
);
}
});
</script>
+3 -3
View File
@@ -4,7 +4,7 @@
<form action="" method="post" @submit.prevent="onLogin" ref="form" class="flex flex-col gap-8">
<label class="form-control w-full">
<label
class="input input-lg floating-label input-bordered has-[:focus]:input-primary flex w-full items-center gap-2 border-2"
class="input floating-label input-bordered has-[:focus]:input-primary flex items-center gap-2 border-2"
:class="{ 'input-error': error }"
>
<span class="ml-5">{{ $t("label.username") }}</span>
@@ -28,7 +28,7 @@
</label>
<label class="form-control w-full">
<label
class="input input-lg floating-label input-bordered has-[:focus]:input-primary flex w-full items-center gap-2 border-2"
class="input floating-label input-bordered has-[:focus]:input-primary flex items-center gap-2 border-2"
>
<span class="ml-5">{{ $t("label.password") }}</span>
<mdi:key class="has-[+:focus]:text-primary" />
@@ -44,7 +44,7 @@
</label>
</label>
<button class="btn btn-primary mt-2 uppercase" type="submit" :disabled="loading">
<button class="btn btn-primary uppercase" type="submit" :disabled="loading">
<span class="loading loading-spinner" v-if="loading"></span>
{{ $t("button.login") }}
</button>
+3 -18
View File
@@ -119,28 +119,13 @@
<div class="has-underline">
<h2>{{ $t("settings.options") }}</h2>
</div>
<LabeledInput>
<template #label>
{{ $t("settings.automatic-redirect") }}
</template>
<template #input>
<DropdownMenu
v-model="automaticRedirect"
:options="[
{ label: 'Instant', value: 'instant' },
{ label: 'Delayed', value: 'delayed' },
{ label: 'None', value: 'none' },
]"
/>
</template>
</LabeledInput>
<Toggle v-model="search">
{{ $t("settings.search") }} <key-shortcut char="f" class="align-top"></key-shortcut>
</Toggle>
<Toggle v-model="showAllContainers">{{ $t("settings.show-stopped-containers") }}</Toggle>
<Toggle v-model="automaticRedirect">{{ $t("settings.automatic-redirect") }}</Toggle>
</section>
</PageWithLinks>
</template>
@@ -221,7 +206,7 @@ const fakeMessages = computedWithControl(
);
</script>
<style scoped>
@reference "@/main.css";
@import "@/main.css" reference;
.has-underline {
@apply border-base-content/50 mb-4 border-b py-4;
+2 -10
View File
@@ -14,7 +14,7 @@ export type Settings = {
dateLocale: "auto" | "en-US" | "en-GB" | "de-DE" | "en-CA";
softWrap: boolean;
collapseNav: boolean;
automaticRedirect: "instant" | "delayed" | "none";
automaticRedirect: boolean;
locale: string;
};
export const DEFAULT_SETTINGS: Settings = {
@@ -31,20 +31,12 @@ export const DEFAULT_SETTINGS: Settings = {
dateLocale: "auto",
softWrap: true,
collapseNav: false,
automaticRedirect: "delayed",
automaticRedirect: true,
locale: "",
};
export const settings = useProfileStorage("settings", DEFAULT_SETTINGS);
// @ts-ignore: automaticRedirect is now a string enum, but might be a boolean in older data
if (settings.value.automaticRedirect === true) {
settings.value.automaticRedirect = "delayed";
// @ts-ignore: automaticRedirect is now a string enum, but might be a boolean in older data
} else if (settings.value.automaticRedirect === false) {
settings.value.automaticRedirect = "none";
}
export const {
collapseNav,
compact,
-24
View File
@@ -109,27 +109,3 @@ export function hashCode(str: string) {
}
return hash;
}
const units: [Intl.RelativeTimeFormatUnit, number][] = [
["year", 31536000],
["month", 2592000],
["week", 604800],
["day", 86400],
["hour", 3600],
["minute", 60],
["second", 1],
];
export function toRelativeTime(date: Date, locale: string | undefined): string {
const diffInSeconds = (date.getTime() - new Date().getTime()) / 1000;
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
for (const [unit, seconds] of units) {
const value = Math.round(diffInSeconds / seconds);
if (Math.abs(value) >= 1) {
return rtf.format(value, unit);
}
}
return rtf.format(0, "second");
}
-4
View File
@@ -32,10 +32,6 @@ services:
:::
> [!NOTE] Docker Socket Proxy users
> If you are using a remote agent you **CANNOT** add a socket proxy on top of the agent. Dozzle agents **REPLACE** using a proxy, see [Remote Hosts](/guide/remote-hosts.md) for more info and how to use a socket proxy instead of an agent.
The agent will start and listen on port `7007`. You can connect to the agent using the Dozzle UI by providing the agent's IP address and port. The agent will only show the containers that are available on the host where the agent is running.
> [!TIP]
+3 -19
View File
@@ -28,22 +28,11 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
environment:
# Uncomment to enable container actions (stop, start, restart). See https://dozzle.dev/guide/actions
# - DOZZLE_ENABLE_ACTIONS=true
#
# Uncomment to allow access to container shells. See https://dozzle.dev/guide/shell
# - DOZZLE_ENABLE_SHELL=true
#
# Uncomment to enable authentication. See https://dozzle.dev/guide/authentication
# - DOZZLE_AUTH_PROVIDER=simple
```
:::
> [!TIP]
> Dozzle supports actions, such as stopping, starting, and restarting containers, or attaching to container shells. But they are disabled by default for security reasons. To enable them, uncomment the corresponding environment variables.
> Dozzle also supports connecting to remote agents to monitor multiple Docker hosts. See [agent](/guide/agent) to learn more.
Dozzle also supports connecting to remote agents to monitor multiple Docker hosts. See [agent](/guide/agent) to learn more.
## Docker Swarm
@@ -81,9 +70,6 @@ See [swarm mode](/guide/swarm-mode) for more information.
Dozzle supports running in Kubernetes. It only needs to be deployed on one node within the cluster.
<details>
<summary>Kubernetes Configuration</summary>
```yaml [k8s-dozzle.yml]
# rbac.yaml
apiVersion: v1
@@ -144,11 +130,9 @@ spec:
value: "k8s"
```
</details>
Then apply the configuration using the following command:
Apply the configuration using the following command:
```sh
```bash
kubectl apply -f k8s-dozzle.yml
```
-3
View File
@@ -139,9 +139,6 @@ spec:
value: "default"
```
> [!NOTE]
> Dozzle supports multiple namespaces, you can set the `DOZZLE_NAMESPACES` environment variable to a comma separated list of namespaces. When multiple namespaces are specified, Dozzle will monitor each namespace separately and combine the results.
### Labels and Filters
`DOZZLE_FILTER` behave similarlty to Docker filters. You can limit the scope of Dozzle using the `DOZZLE_FILTER` environment variable. For example, to scope only to `env=prod`:
+1 -1
View File
@@ -37,6 +37,6 @@ services:
:::
In the above example, `/var/log/system.log` is mounted from the host and used with `tail -f` to follow the file. `tail` is smart to follow log rotations. During testing, using Alpine used about `~50KB` of memory.
In the above example, `/var/log/system.log` is mounted from the host and used with `tail -f` to follow the file. `tail` is smart to follow log rotations. During testing, using Alpine only uses about `300KB` of memory.
The second tab shows a `docker-compose` file which is useful if you want the log stream to survive server reboot.
+1 -3
View File
@@ -8,8 +8,6 @@ Dozzle supports connecting to remote Docker hosts. This is useful when running D
However, with Dozzle agents, you can connect to remote hosts without exposing the Docker socket. See the [agent](/guide/agent) page for more information.
Dozzle agents remove the need to remotely expose the Docker socket but cannot be uses with a Docker Socket proxy inside the Dozzle agent stack. If you wish to use a Socket Proxy on it's own without an agent see the [connecting with a socket proxy](#connecting-with-a-socket-proxy) section.
> [!WARNING]
> Remote hosts have been replaced with agents. Agents provide a more secure way to connect to remote hosts. Although remote hosts are still supported, it is recommended to use agents. See the [agent](/guide/agent) page for more information and examples. For comparison, see the [comparing agents with remote connections](/guide/agent#comparing-agents-with-remote-connection) section. I won't be able to investigate user's issues with remote hosts as it is very time consuming.
@@ -44,7 +42,7 @@ services:
## Connecting with a Socket Proxy
If you are in a private network, then you can use [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy) which exposes `docker.sock` file without the need for TLS. This will remove the need for a Dozzle agent and Dozzle will connect directly to the Socket Proxy instead. Dozzle will never try to write to Docker but it will need access to list APIs. The following command will start a proxy with minimal access:
If you are in a private network, then you can use [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy) which exposes `docker.sock` file without the need for TLS. Dozzle will never try to write to Docker but it will need access to list APIs. The following command will start a proxy with minimal access:
```sh
$ docker container run --privileged -e CONTAINERS=1 -e INFO=1 -v /var/run/docker.sock:/var/run/docker.sock -p 2375:2375 tecnativa/docker-socket-proxy
+1 -1
View File
@@ -27,7 +27,7 @@ Configurations can be done with flags or environment variables. The table below
| `--namespace` | `DOZZLE_NAMESPACE` | `""` |
> [!TIP]
> Some flags like `--remote-host` or `--remote-agent` can be used multiple times. For example, `--remote-agent 167.99.1.1:7007 --remote-agent 167.99.1.2:7007` or comma-separated `DOZZLE_REMOTE_AGENT=167.99.1.1:7007,167.99.1.2:7007`.
> Some flags like `--remote-host` or `--remote-agent` can be used multiple times. For example, `--remote-agent tcp://167.99.1.1:7007 --remote-agent tcp://167.99.1.2:7007` or comma-separated `DOZZLE_REMOTE_AGENT=tcp://167.99.1.1:7007,tcp://167.99.1.2:7007`.
## Generate users.yml
-3
View File
@@ -4,9 +4,6 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
esbuild@<=0.24.2: '>=0.25.0'
importers:
.:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 88 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+20 -21
View File
@@ -18,7 +18,7 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/sys v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)
@@ -27,21 +27,21 @@ require (
github.com/andybalholm/brotli v1.1.1
github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/jwtauth/v5 v5.3.3
github.com/go-faker/faker/v4 v4.6.1
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/puzpuzpuz/xsync/v4 v4.1.0
github.com/go-faker/faker/v4 v4.6.0
github.com/gorilla/websocket v1.5.3
github.com/puzpuzpuz/xsync/v4 v4.0.0
github.com/rs/zerolog v1.34.0
github.com/samber/lo v1.50.0
github.com/samber/lo v1.49.1
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/yuin/goldmark v1.7.12
golang.org/x/crypto v0.38.0
golang.org/x/sync v0.14.0
google.golang.org/grpc v1.72.1
github.com/yuin/goldmark v1.7.10
golang.org/x/crypto v0.37.0
golang.org/x/sync v0.13.0
google.golang.org/grpc v1.71.1
google.golang.org/protobuf v1.36.6
k8s.io/api v0.33.1
k8s.io/apimachinery v0.33.1
k8s.io/client-go v0.33.1
k8s.io/metrics v0.33.1
k8s.io/api v0.32.3
k8s.io/apimachinery v0.32.3
k8s.io/client-go v0.32.3
k8s.io/metrics v0.32.3
)
require (
@@ -65,7 +65,7 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
@@ -97,21 +97,20 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.10.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gotest.tools/v3 v3.0.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
go 1.24.3
go 1.24.2
-53
View File
@@ -54,8 +54,6 @@ github.com/go-chi/jwtauth/v5 v5.3.3 h1:50Uzmacu35/ZP9ER2Ht6SazwPsnLQ9LRJy6zTZJpH
github.com/go-chi/jwtauth/v5 v5.3.3/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs=
github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0=
github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt4k=
github.com/go-faker/faker/v4 v4.6.1/go.mod h1:arSdxNCSt7mOhdk8tEolvHeIJ7eX4OX80wXjKKvkKBY=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -83,8 +81,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -94,8 +90,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -169,8 +163,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v4 v4.0.0 h1:F1za+MBXzDQtQq+OVgFsojSX4w66rsNDmQNebPFAncA=
github.com/puzpuzpuz/xsync/v4 v4.0.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@@ -178,8 +170,6 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
@@ -212,10 +202,6 @@ 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.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI=
github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
@@ -246,8 +232,6 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
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=
@@ -272,8 +256,6 @@ golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
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=
@@ -285,8 +267,6 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
@@ -305,8 +285,6 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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=
@@ -318,8 +296,6 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -331,8 +307,6 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -356,10 +330,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -380,44 +350,21 @@ gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/metrics v0.32.3 h1:2vsBvw0v8rIIlczZ/lZ8Kcqk9tR6Fks9h+dtFNbc2a4=
k8s.io/metrics v0.32.3/go.mod h1:9R1Wk5cb+qJpCQon9h52mgkVCcFeYxcY+YkumfwHVCU=
k8s.io/metrics v0.33.0 h1:sKe5sC9qb1RakMhs8LWYNuN2ne6OTCWexj8Jos3rO2Y=
k8s.io/metrics v0.33.0/go.mod h1:XewckTFXmE2AJiP7PT3EXaY7hi7bler3t2ZLyOdQYzU=
k8s.io/metrics v0.33.1 h1:Ypd5ITCf+fM+LDNFk7hESXTc3vh02CQYGiwRoVRaGsM=
k8s.io/metrics v0.33.1/go.mod h1:wK8cFTK5ykBdhL0Wy4RZwLH28XM7j/Klc+NQrMRWVxg=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+1 -1
View File
@@ -315,7 +315,7 @@ func (s *ContainerStore) init() {
case "update":
started := false
updatedContainer, _ := s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, xsync.ComputeOp) {
if loaded && event.Container != nil {
if loaded {
newContainer := event.Container
if newContainer.State == "running" && c.State != "running" {
started = true
@@ -1,21 +1,16 @@
package support_web
package container
import (
"html"
"strings"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/support/search"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map/v2"
)
func EscapeHTMLValues(logEvent *container.LogEvent) {
func escape(logEvent *LogEvent) {
switch value := logEvent.Message.(type) {
case string:
value = html.EscapeString(value)
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
logEvent.Message = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
logEvent.Message = html.EscapeString(value)
case *orderedmap.OrderedMap[string, any]:
escapeAnyMap(value)
@@ -38,10 +33,7 @@ func escapeAnyMap(orderedMap *orderedmap.OrderedMap[string, any]) {
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
switch value := pair.Value.(type) {
case string:
value = html.EscapeString(value)
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
value = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
orderedMap.Set(pair.Key, value)
orderedMap.Set(pair.Key, html.EscapeString(value))
case *orderedmap.OrderedMap[string, any]:
escapeAnyMap(value)
case *orderedmap.OrderedMap[string, string]:
@@ -53,9 +45,6 @@ func escapeAnyMap(orderedMap *orderedmap.OrderedMap[string, any]) {
func escapeStringMap(orderedMap *orderedmap.OrderedMap[string, string]) {
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
value := html.EscapeString(pair.Value)
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
value = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
orderedMap.Set(pair.Key, value)
orderedMap.Set(pair.Key, html.EscapeString(pair.Value))
}
}
+1
View File
@@ -87,6 +87,7 @@ func (g *EventGenerator) consumeReader() {
logEvent := createEvent(message, streamType)
logEvent.ContainerID = g.containerID
logEvent.Level = guessLogLevel(logEvent)
escape(logEvent)
g.buffer <- logEvent
}
+35 -84
View File
@@ -2,11 +2,9 @@ package k8s
import (
"context"
"errors"
"fmt"
"io"
"strings"
"sync"
"time"
"os"
@@ -15,13 +13,9 @@ import (
"github.com/amir20/dozzle/internal/utils"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
lop "github.com/samber/lo/parallel"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
@@ -31,19 +25,15 @@ import (
type K8sClient struct {
Clientset *kubernetes.Clientset
namespace []string
namespace string
config *rest.Config
host container.Host
}
func NewK8sClient(namespace []string) (*K8sClient, error) {
func NewK8sClient(namespace string) (*K8sClient, error) {
var config *rest.Config
var err error
if len(namespace) == 0 {
namespace = []string{metav1.NamespaceAll}
}
// Check if we're running in cluster
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
config, err = rest.InClusterConfig()
@@ -126,36 +116,15 @@ func (k *K8sClient) ListContainers(ctx context.Context, labels container.Contain
}
log.Debug().Str("selector", selector).Msg("Listing containers with labels")
}
containerList := lop.Map(k.namespace, func(namespace string, index int) lo.Tuple2[[]container.Container, error] {
pods, err := k.Clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: selector})
if err != nil {
return lo.T2[[]container.Container, error](nil, fmt.Errorf("failed to list pods in namespace %s: %w", namespace, err))
}
var containers []container.Container
for _, pod := range pods.Items {
containers = append(containers, podToContainers(&pod)...)
}
return lo.T2[[]container.Container, error](containers, nil)
})
pods, err := k.Clientset.CoreV1().Pods(k.namespace).List(ctx, metav1.ListOptions{LabelSelector: selector})
if err != nil {
return nil, err
}
var containers []container.Container
var lastError error
success := false
for _, t2 := range containerList {
items, err := t2.Unpack()
if err != nil {
log.Error().Err(err).Msg("failed to fetch containers")
lastError = err
continue
}
success = true
containers = append(containers, items...)
for _, pod := range pods.Items {
containers = append(containers, podToContainers(&pod)...)
}
if !success {
return nil, lastError
}
return containers, nil
}
@@ -224,57 +193,39 @@ func (k *K8sClient) ContainerLogsBetweenDates(ctx context.Context, id string, st
}
func (k *K8sClient) ContainerEvents(ctx context.Context, ch chan<- container.ContainerEvent) error {
watchers := lo.Map(k.namespace, func(namespace string, index int) watch.Interface {
watcher, err := k.Clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.ListOptions{})
if err != nil {
log.Error().Err(err).Msg("Failed to watch pods")
return nil
watch, err := k.Clientset.CoreV1().Pods(k.namespace).Watch(ctx, metav1.ListOptions{})
if err != nil {
return err
}
for event := range watch.ResultChan() {
log.Debug().Interface("event.type", event.Type).Msg("Received kubernetes event")
pod, ok := event.Object.(*corev1.Pod)
if !ok {
continue
}
return watcher
})
if len(watchers) == 0 {
return errors.New("no namespaces to watch")
}
name := ""
switch event.Type {
case "ADDED":
name = "create"
case "DELETED":
name = "destroy"
case "MODIFIED":
name = "update"
}
wg := sync.WaitGroup{}
wg.Add(len(watchers))
for _, watcher := range watchers {
go func(w watch.Interface) {
defer wg.Done()
for event := range w.ResultChan() {
log.Debug().Interface("event.type", event.Type).Msg("Received kubernetes event")
pod, ok := event.Object.(*corev1.Pod)
if !ok {
continue
}
name := ""
switch event.Type {
case "ADDED":
name = "create"
case "DELETED":
name = "destroy"
case "MODIFIED":
name = "update"
}
for _, c := range podToContainers(pod) {
ch <- container.ContainerEvent{
Name: name,
ActorID: c.ID,
Host: pod.Spec.NodeName,
Time: time.Now(),
Container: &c,
}
}
for _, c := range podToContainers(pod) {
ch <- container.ContainerEvent{
Name: name,
ActorID: c.ID,
Host: pod.Spec.NodeName,
Time: time.Now(),
Container: &c,
}
}(watcher)
}
}
wg.Wait()
return nil
}
+20 -24
View File
@@ -7,8 +7,6 @@ import (
"time"
"github.com/amir20/dozzle/internal/container"
lop "github.com/samber/lo/parallel"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -97,30 +95,28 @@ func (sc *K8sStatsCollector) Start(parentCtx context.Context) bool {
for {
select {
case <-ticker.C:
lop.ForEach(sc.client.namespace, func(item string, index int) {
metricList, err := sc.metrics.MetricsV1beta1().PodMetricses(item).List(ctx, metav1.ListOptions{})
if err != nil {
log.Panic().Err(err).Msg("failed to get pod metrics")
}
for _, pod := range metricList.Items {
for _, c := range pod.Containers {
stat := container.ContainerStat{
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
}
log.Trace().Interface("stat", stat).Msg("k8s stats")
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
select {
case stats <- stat:
case <-c.Done():
sc.subscribers.Delete(c)
}
return true
})
metricList, err := sc.metrics.MetricsV1beta1().PodMetricses(sc.client.namespace).List(ctx, metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
for _, pod := range metricList.Items {
for _, c := range pod.Containers {
stat := container.ContainerStat{
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
}
log.Trace().Interface("stat", stat).Msg("k8s stats")
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
select {
case stats <- stat:
case <-c.Done():
sc.subscribers.Delete(c)
}
return true
})
}
})
}
case <-ctx.Done():
return true
}
+1 -1
View File
@@ -28,7 +28,7 @@ type Settings struct {
ShowAllContainers bool `json:"showAllContainers"`
SoftWrap bool `json:"softWrap"`
CollapseNav bool `json:"collapseNav"`
AutomaticRedirect string `json:"automaticRedirect"`
AutomaticRedirect bool `json:"automaticRedirect"`
Size string `json:"size,omitempty"`
Compact bool `json:"compact"`
LightTheme string `json:"lightTheme,omitempty"`
-1
View File
@@ -19,7 +19,6 @@ func StartEvent(args Args, mode string, client container.Client, subCommand stri
RemoteClients: len(args.RemoteHost),
SubCommand: subCommand,
HasActions: args.EnableActions,
HasShell: args.EnableShell,
HasCustomAddress: args.Addr != ":8080",
HasCustomBase: args.Base != "/",
HasHostname: args.Hostname != "",
+1 -5
View File
@@ -31,7 +31,7 @@ type Args struct {
Mode string `arg:"env:DOZZLE_MODE" default:"server" help:"sets the mode to run in (server, swarm)"`
TimeoutString string `arg:"--timeout,env:DOZZLE_TIMEOUT" default:"10s" help:"sets the timeout for docker client"`
Timeout time.Duration `arg:"-"`
Namespace []string `arg:"env:DOZZLE_NAMESPACE" help:"sets the namespace to use in k8s"`
Namespace string `arg:"env:DOZZLE_NAMESPACE" default:"" help:"sets the namespace to use in k8s"`
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"`
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
@@ -72,10 +72,6 @@ func ParseArgs() (Args, interface{}) {
args.RemoteHost[i] = strings.TrimSpace(value)
}
for i, value := range args.Namespace {
args.Namespace[i] = strings.TrimSpace(value)
}
if args.TimeoutString != "" {
timeout, err := time.ParseDuration(args.TimeoutString)
if err != nil {
+13 -26
View File
@@ -10,11 +10,6 @@ import (
orderedmap "github.com/wk8/go-ordered-map/v2"
)
const (
MarkerStart = "\uE000"
MarkerEnd = "\uE001"
)
func ParseRegex(search string) (*regexp.Regexp, error) {
flags := ""
@@ -35,7 +30,7 @@ func Search(re *regexp.Regexp, logEvent *container.LogEvent) bool {
switch value := logEvent.Message.(type) {
case string:
if re.MatchString(value) {
logEvent.Message = re.ReplaceAllString(value, MarkerStart+"$0"+MarkerEnd)
logEvent.Message = re.ReplaceAllString(value, "<mark>$0</mark>")
return true
}
@@ -61,9 +56,9 @@ func searchMapAny(re *regexp.Regexp, orderedMap *orderedmap.OrderedMap[string, a
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
switch value := pair.Value.(type) {
case string:
if replaced, matched := searchString(re, value); matched {
if re.MatchString(value) {
found = true
orderedMap.Set(pair.Key, replaced)
orderedMap.Set(pair.Key, re.ReplaceAllString(value, "<mark>$0</mark>"))
}
case []any:
@@ -89,7 +84,7 @@ func searchMapAny(re *regexp.Regexp, orderedMap *orderedmap.OrderedMap[string, a
case int, float64, bool:
formatted := fmt.Sprintf("%v", value)
if re.MatchString(formatted) {
orderedMap.Set(pair.Key, re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd))
orderedMap.Set(pair.Key, re.ReplaceAllString(formatted, "<mark>$0</mark>"))
found = true
}
@@ -106,10 +101,11 @@ func searchMap(re *regexp.Regexp, data map[string]interface{}) bool {
for key, value := range data {
switch value := value.(type) {
case string:
if replaced, matched := searchString(re, value); matched {
if re.MatchString(value) {
data[key] = re.ReplaceAllString(value, "<mark>$0</mark>")
found = true
data[key] = replaced
}
case []any:
if searchArray(re, value) {
found = true
@@ -123,7 +119,7 @@ func searchMap(re *regexp.Regexp, data map[string]interface{}) bool {
case int, float64, bool:
formatted := fmt.Sprintf("%v", value)
if re.MatchString(formatted) {
data[key] = re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd)
data[key] = re.ReplaceAllString(formatted, "<mark>$0</mark>")
found = true
}
default:
@@ -137,9 +133,9 @@ func searchMap(re *regexp.Regexp, data map[string]interface{}) bool {
func searchMapString(re *regexp.Regexp, orderedMap *orderedmap.OrderedMap[string, string]) bool {
found := false
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
if replaced, matched := searchString(re, pair.Value); matched {
if re.MatchString(pair.Value) {
orderedMap.Set(pair.Key, re.ReplaceAllString(pair.Value, "<mark>$0</mark>"))
found = true
orderedMap.Set(pair.Key, replaced)
}
}
return found
@@ -150,14 +146,14 @@ func searchArray(re *regexp.Regexp, data []any) bool {
for i, value := range data {
switch value := value.(type) {
case string:
if replaced, matched := searchString(re, value); matched {
if re.MatchString(value) {
data[i] = re.ReplaceAllString(value, "<mark>$0</mark>")
found = true
data[i] = replaced
}
case int, float64, bool:
formatted := fmt.Sprintf("%v", value)
if re.MatchString(formatted) {
data[i] = re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd)
data[i] = re.ReplaceAllString(formatted, "<mark>$0</mark>")
found = true
}
case []any:
@@ -173,12 +169,3 @@ func searchArray(re *regexp.Regexp, data []any) bool {
return found
}
func searchString(re *regexp.Regexp, value string) (string, bool) {
if re.MatchString(value) {
replaced := re.ReplaceAllString(value, MarkerStart+"$0"+MarkerEnd)
return replaced, true
}
return value, false
}
+3 -2
View File
@@ -2,7 +2,7 @@
HTTP/1.1 200 OK
Connection: close
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline' blob:; img-src 'self' data:;
Content-Type: text/html; charset=utf-8
Content-Type: text/plain; charset=utf-8
foo page
@@ -10,7 +10,7 @@ foo page
HTTP/1.1 200 OK
Connection: close
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline' blob:; img-src 'self' data:;
Content-Type: text/html; charset=utf-8
Content-Type: text/plain; charset=utf-8
index page
@@ -87,6 +87,7 @@ Content-Type: text/html
/* snapshot: Test_handler_between_dates_with_everything_complex */
{"m":{"msg":"a complex log message"},"ts":1589396197772,"id":62280847,"l":"unknown","s":"stdout","c":"123456"}
/* snapshot: Test_handler_between_dates_with_fill */
{"m":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
{"m":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
+2 -1
View File
@@ -14,12 +14,13 @@ import (
func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
sseWriter, err := support_web.NewSSEWriter(r.Context(), w, r)
defer sseWriter.Close()
if err != nil {
log.Error().Err(err).Msg("error creating sse writer")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer sseWriter.Close()
events := make(chan container.ContainerEvent)
stats := make(chan container.ContainerStat)
-1
View File
@@ -103,7 +103,6 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
log.Fatal().Err(err).Msg("Could not parse index.html")
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err = tmpl.Execute(w, data)
if err != nil {
log.Fatal().Err(err).Msg("Could not execute index.html")
+1 -7
View File
@@ -158,7 +158,6 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
break
}
support_web.EscapeHTMLValues(event) // only escape when not exporting
buffer.Push(event)
}
}
@@ -247,12 +246,12 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
}
sseWriter, err := support_web.NewSSEWriter(r.Context(), w, r)
defer sseWriter.Close()
if err != nil {
log.Error().Err(err).Msg("error creating sse writer")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer sseWriter.Close()
userLabels := h.config.Labels
if h.config.Authorization.Provider != NONE {
@@ -392,8 +391,6 @@ loop:
if _, ok := levels[logEvent.Level]; !ok {
continue
}
support_web.EscapeHTMLValues(logEvent)
sseWriter.Message(logEvent)
case c := <-newContainers:
if _, err := h.hostService.FindContainer(c.Host, c.ID, userLabels); err == nil {
@@ -408,9 +405,6 @@ loop:
}
case backfillEvents := <-backfill:
for _, event := range backfillEvents {
support_web.EscapeHTMLValues(event)
}
if err := sseWriter.Event("logs-backfill", backfillEvents); err != nil {
log.Error().Err(err).Msg("error encoding container event")
}
-2
View File
@@ -30,8 +30,6 @@ label:
host-menu: Værter og Containere
swarm-menu: Tjenester og Stacks
group-menu: Brugerdefinerede Grupper
no-logs: Container har ingen logs endnu
show-all-containers: Vis alle containere
tooltip:
search: Søg containere (⌘ + k, ⌃k)
pin-column: Fastgør som kolonne
-2
View File
@@ -30,8 +30,6 @@ label:
host-menu: Hosts und Container
swarm-menu: Services und Stacks
group-menu: Benutzerdefinierte Gruppen
no-logs: Container hat noch keine Logs
show-all-containers: Zeige alle Container
tooltip:
search: Suche Container (⌘ + k, ⌃k)
pin-column: Als Spalte anheften
-2
View File
@@ -30,8 +30,6 @@ label:
host-menu: Hosts and Containers
swarm-menu: Services and Stacks
group-menu: Custom Groups
no-logs: Container has no logs yet
show-all-containers: Show all containers
tooltip:
search: Search containers (⌘ + k, ⌃k)
pin-column: Pin as column
+3 -5
View File
@@ -30,8 +30,6 @@ label:
host-menu: Hosts y Contenedores
swarm-menu: Servicios y Stacks
group-menu: Grupos Personalizados
no-logs: El contenedor aún no tiene registros
show-all-containers: Mostrar todos los contenedores
tooltip:
search: Buscar contenedores (⌘ + K, CTRL + K)
pin-column: Anclar como columna
@@ -102,9 +100,9 @@ settings:
middle-line: con una segunda línea
end-line: y finalmente una tercera línea.
releases:
features: una nueva característica | {count} características
bugFixes: una corrección de error | {count} correcciones
breaking: un cambio importante | {count} cambios importantes
features: Características
bugFixes: Correcciones de errores
breaking: Cambios importantes
three_parts: "{features}, {bugFixes} y {breaking}"
two_parts: "{features} y {bugFixes}"
latest: Último
+4 -6
View File
@@ -7,8 +7,8 @@ toolbar:
stop: Stop
start: Démarrer
restart: Redémarrer
show-hostname: Afficher le nom d'hôte
show-container-name: Afficher le nom du conteneur
show-hostname: Show hostname
show-container-name: Show container name
label:
containers: Conteneurs
container: Pas de conteneur | 1 conteneur | {count} conteneurs
@@ -30,8 +30,6 @@ label:
host-menu: Hôtes et Conteneurs
swarm-menu: Services et Stacks
group-menu: Groupes Personnalisés
no-logs: Le conteneur n'a pas encore de logs
show-all-containers: Afficher tous les conteneurs
tooltip:
search: Recherche de conteneurs (⌘ + k, ⌃k)
pin-column: Epinglé en colonne
@@ -68,8 +66,8 @@ button:
logout: Déconnexion
login: Connexion
settings: Paramètres
cancel: Annuler
redirect: Rediriger
cancel: Cancel
redirect: Redirect
placeholder:
search-containers: Recherche de conteneurs (⌘ + k, ⌃k)
search: Recherche
-136
View File
@@ -1,136 +0,0 @@
toolbar:
clear: Bersihkan
download: Unduh
search: Cari
show: Tampilkan {std}
show-all: Tampilkan semua
stop: Hentikan
start: Mulai
restart: Mulai ulang
show-hostname: Tampilkan nama host
show-container-name: Tampilkan nama kontainer
label:
containers: Kontainer
container: Tidak ada kontainer | 1 kontainer | {count} kontainer
service: Tidak ada layanan | 1 layanan | {count} layanan
services: Layanan
running-containers: Kontainer Berjalan
all-containers: Semua Kontainer
host: Host
hosts: Host
password: Kata sandi
username: Nama pengguna
container-name: Nama Kontainer
status: Status
created: Dibuat
avg-cpu: Rata-rata CPU (%)
avg-mem: Rata-rata MEM (%)
pinned: Disematkan
per-page: Baris per halaman
host-menu: Host dan Kontainer
swarm-menu: Layanan dan Stack
group-menu: Grup Kustom
no-logs: Kontainer belum memiliki log
show-all-containers: Tampilkan semua kontainer
tooltip:
search: Cari kontainer (⌘ + k, ⌃k)
pin-column: Sematkan sebagai kolom
merge-services: Gabungkan semua layanan dalam satu tampilan
merge-containers: Gabungkan semua kontainer dalam satu tampilan
merge-hosts: Gabungkan semua kontainer di host ini dalam satu tampilan
error:
page-not-found: Halaman ini tidak ada
invalid-auth: Nama pengguna atau kata sandi tidak valid
logs-skipped: Melewati {total} entri
container-not-found: Kontainer tidak ditemukan
events-stream:
title: Kesalahan Tak Terduga
message: >-
UI Dozzle tidak dapat terhubung ke API. Periksa pengaturan jaringan Anda. Jika Anda menggunakan reverse proxy, pastikan konfigurasinya sudah benar.
events-timeout:
title: Ada yang tidak beres
message: >-
UI Dozzle gagal terhubung ke API karena waktu habis. Periksa koneksi jaringan dan coba lagi.
alert:
redirected:
title: Dialihkan ke kontainer baru
message: Dozzle secara otomatis mengalihkan Anda ke kontainer baru {containerId}.
similar-container-found:
title: Kontainer serupa ditemukan
message: >-
Dozzle menemukan kontainer serupa {containerId} yang berjalan di host yang sama dan akan otomatis beralih kecuali Anda mengklik 'Batal'.
title:
page-not-found: Halaman tidak ditemukan
login: Autentikasi Diperlukan
dashboard: 1 kontainer | {count} kontainer
settings: Pengaturan
button:
logout: Keluar
login: Masuk
settings: Pengaturan
cancel: Batal
redirect: Alihkan
placeholder:
search-containers: Cari kontainer (⌘ + k, ⌃k)
search: Cari
settings:
display: Tampilan
locale: Ganti bahasa
small-scrollbars: Gunakan scrollbar kecil
show-timesamps: Tampilkan cap waktu
soft-wrap: Bungkus baris secara lunak
datetime-format: Ganti format tanggal dan waktu
font-size: Ukuran font untuk log
color-scheme: Skema warna
options: Opsi
show-stopped-containers: Tampilkan kontainer yang dihentikan
about: Tentang
search: Aktifkan pencarian dengan Dozzle menggunakan
using-version: Anda menggunakan Dozzle {version}.
update-available: >-
Versi baru tersedia! Perbarui ke <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Tampilkan label stdout dan stderr
automatic-redirect: Alihkan otomatis ke kontainer baru dengan nama yang sama
compact: Aktifkan mode ringkas untuk log
log:
preview: Ini adalah pratinjau log
warning: Log peringatan seperti ini
complex: Ini adalah entri log kompleks dalam format json
simple: Ini adalah pesan yang sangat panjang yang akan terbungkus secara default. Menonaktifkan pembungkusan lunak akan menonaktifkannya. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
multi-line-error:
start-line: Ini adalah pesan kesalahan multi-baris
middle-line: dengan baris kedua
end-line: dan akhirnya baris ketiga.
releases:
features: satu fitur baru | {count} fitur
bugFixes: satu perbaikan bug | {count} perbaikan
breaking: satu perubahan besar | {count} perubahan besar
three_parts: "{first}, {second}, dan {third}"
two_parts: "{first} dengan {second}"
latest: Terbaru
no_releases: Anda sudah menggunakan versi terbaru
log_actions:
copy_log: Salin log
jump_to_context: Lompat ke konteks
toasts:
copied:
title: Disalin
message: Log disalin ke clipboard
analytics:
creating_table: Membuat tabel sementara...
downloading: Mengambil log kontainer... ({size})
evaluating_query: Mengevaluasi kueri...
total_records: Total {count} entri.
showing_first: Menampilkan {count} pertama.
+5 -7
View File
@@ -11,9 +11,9 @@ toolbar:
show-container-name: Mostra nome container
label:
containers: Container
container: Nessun container | 1 container | {count} container
service: Nessun servizio | 1 servizio | {count} servizi
services: Servizi
container: No containers | 1 container | {count} containers
service: No services | 1 service | {count} services
services: Services
running-containers: Container in esecuzione
all-containers: Tutti i Container
host: Host
@@ -30,8 +30,6 @@ label:
host-menu: Host e Container
swarm-menu: Servizi e Stack
group-menu: Gruppi Personalizzati
no-logs: Il container non ha ancora log
show-all-containers: Mostra tutti i container
tooltip:
search: Ricerca container (⌘ + k, ⌃k)
pin-column: Blocca come colonna
@@ -68,8 +66,8 @@ button:
logout: Logout
login: Login
settings: Impostazioni
cancel: Annulla
redirect: Reindirizza
cancel: Cancel
redirect: Redirect
placeholder:
search-containers: Ricerca container (⌘ + k, ⌃k)
search: Cerca
-125
View File
@@ -1,125 +0,0 @@
toolbar:
clear: Wissen
download: Downloaden
search: Zoeken
show: Toon {std}
show-all: Toon alles
stop: Stoppen
start: Starten
restart: Herstarten
show-hostname: Toon hostnaam
show-container-name: Toon containernaam
label:
containers: Containers
container: Geen containers | 1 container | {count} containers
service: Geen services | 1 service | {count} services
services: Services
running-containers: Actieve containers
all-containers: Alle containers
host: Host
hosts: Hosts
password: Wachtwoord
username: Gebruikersnaam
container-name: Containernaam
status: Status
created: Aangemaakt
avg-cpu: Gem. CPU (%)
avg-mem: Gem. geheugen (%)
pinned: Vastgezet
per-page: Rijen per pagina
host-menu: Hosts en Containers
swarm-menu: Services en Stacks
group-menu: Aangepaste groepen
no-logs: Container heeft nog geen logbestanden
show-all-containers: Toon alle containers
tooltip:
search: Zoek containers (⌘ + k, ⌃k)
pin-column: Vastzetten als kolom
merge-services: Voeg alle services samen in één weergave
merge-containers: Voeg alle containers samen in één weergave
merge-hosts: Voeg alle containers op deze host samen in één weergave
error:
page-not-found: Deze pagina bestaat niet
invalid-auth: Gebruikersnaam of wachtwoord is ongeldig
logs-skipped: \{total} items overgeslagen
container-not-found: Container niet gevonden
events-stream:
title: Onverwachte fout
message: >-
De Dozzle-interface kon geen verbinding maken met de API. Controleer je netwerkinstellingen.
Gebruik je een reverse proxy? Zorg er dan voor dat deze correct is geconfigureerd.
events-timeout:
title: Er gaat iets mis
message: >-
De verbinding met de API is verlopen. Controleer je netwerkverbinding en probeer het opnieuw.
alert:
redirected:
title: Doorgestuurd naar nieuwe container
message: Dozzle heeft je automatisch doorgestuurd naar de nieuwe container {containerId}.
similar-container-found:
title: Vergelijkbare container gevonden
message: >-
Dozzle heeft een vergelijkbare container gevonden ({containerId}) op dezelfde host. Je wordt hier automatisch naartoe omgeleid, tenzij je op 'Annuleren' klikt.
title:
page-not-found: Pagina niet gevonden
login: Aanmelding vereist
dashboard: 1 container | {count} containers
settings: Instellingen
button:
logout: Afmelden
login: Aanmelden
settings: Instellingen
cancel: Annuleren
redirect: Doorsturen
placeholder:
search-containers: Zoek containers (⌘ + k, ⌃k)
search: Zoeken
settings:
display: Weergave
locale: Taal aanpassen
small-scrollbars: Kleinere scrollbalk gebruiken
show-timesamps: Tijdstempel tonen
soft-wrap: Regels zacht afbreken
datetime-format: Datum- en tijdnotatie aanpassen
font-size: Lettergrootte voor logs
color-scheme: Kleurmodus
options: Opties
show-stopped-containers: Gestopte containers tonen
about: Over
search: Doorzoeken inschakelen via Dozzle
using-version: Je gebruikt Dozzle {version}.
update-available: >-
Er is een nieuwe versie beschikbaar! Update naar <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Toon stdout- en stderr-labels
automatic-redirect: Automatisch omleiden naar nieuwe containers met dezelfde naam
compact: Compacte modus inschakelen voor logs
log:
preview: Voorbeeld van de logweergave
warning: Een waarschuwingslog ziet er zo uit
complex: Dit is een complexe logvermelding in JSON-formaat
simple: Dit is een hele lange logregel die standaard wordt afgebroken. Als je zachte afbreking uitschakelt, gebeurt dit niet. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
multi-line-error:
start-line: Dit is een foutmelding over meerdere regels
middle-line: met een tweede regel
end-line: en tenslotte een derde regel.
releases:
features: één nieuwe functie | {count} functies
bugFixes: één bugfix | {count} bugfixes
breaking: één breaking change | {count} breaking changes
three_parts: "{first}, {second} en {third}"
two_parts: "{first} met {second}"
latest: Meest recent
no_releases: Je gebruikt de nieuwste versie
log_actions:
copy_log: Log kopiëren
jump_to_context: Ga naar context
toasts:
copied:
title: Gekopieerd
message: Log gekopieerd naar klembord
analytics:
creating_table: Tijdelijke tabel wordt aangemaakt...
downloading: Containerlogs worden opgehaald... ({size})
evaluating_query: Query wordt geëvalueerd...
total_records: In totaal {count} resultaten.
showing_first: Eerste {count} worden weergegeven.
-9
View File
@@ -7,8 +7,6 @@ toolbar:
stop: Stop
start: Start
restart: Restart
show-hostname: Pokaż nazwę hosta
show-container-name: Pokaż nazwę kontenera
label:
service: Brak usług | 1 usługa | {count} usług
total-containers: Całkowita liczba kontenerów
@@ -34,14 +32,9 @@ label:
host-menu: Hosty i Kontenery
swarm-menu: Usługi i Stosy
group-menu: Grupy Niestandardowe
no-logs: Kontener nie ma jeszcze logów
show-all-containers: Pokaż wszystkie kontenery
tooltip:
search: Przeszukaj kontenery (⌘ + k, ⌃k)
pin-column: Przypnij jako kolumna
merge-services: Scal wszystkie usługi w jeden widok
merge-containers: Scal wszystkie kontenery w jeden widok
merge-hosts: Scal wszystkie kontenery na tym hoście w jeden widok
error:
page-not-found: Ta strona nie istnieje
invalid-auth: Nazwa użytkownika lub hasło są niepoprawne
@@ -74,8 +67,6 @@ button:
logout: Wyloguj się
login: Zaloguj się
settings: Ustawienia
cancel: Anuluj
redirect: Przekieruj
placeholder:
search-containers: Przeszukaj kontery (⌘ + k, ⌃k)
search: Szukaj
+23 -34
View File
@@ -6,9 +6,6 @@ toolbar:
show-all: Mostrar todos os fluxos
label:
containers: Contentores
container: Nenhum contentor | 1 contentor | {count} contentores
service: Nenhum serviço | 1 serviço | {count} serviços
services: Serviços
running-containers: Contentores em execução
all-containers: Todos os contentores
total-containers: Contentores Totais
@@ -29,14 +26,9 @@ label:
host-menu: Anfitriões e Contentores
swarm-menu: Serviços e Stacks
group-menu: Grupos Personalizados
no-logs: O contentor ainda não tem logs
show-all-containers: Mostrar todos os contentores
tooltip:
search: Pesquisar contentores (⌘ + K, CTRL + K)
pin-column: Alfinete como coluna
merge-services: Fundir todos os serviços numa vista
merge-containers: Fundir todos os contentores numa vista
merge-hosts: Fundir todos os contentores neste anfitrião numa vista
error:
page-not-found: Esta página não existe.
invalid-auth: O nome de usuário e a senha não são válidos.
@@ -68,8 +60,6 @@ button:
logout: Terminar sessão
login: Iniciar sessão
settings: Configurações
cancel: Cancelar
redirect: Redirecionar
placeholder:
search-containers: Pesquisar contentores (⌘ + K, CTRL + K)
search: Pesquisa
@@ -96,30 +86,29 @@ settings:
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
log:
preview: Esta é uma prévia dos logs
warning: Um log de aviso se parece com isto
complex: Esta é uma entrada de log complexa em json
simple: Esta é uma mensagem muito longa que seria quebrada por padrão. Desativar quebras suaves desativaria isso. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
multi-line-error:
start-line: Esta é uma mensagem de erro multi-linha
middle-line: com uma segunda linha
end-line: e finalmente uma terceira linha.
releases:
features: uma nova funcionalidade | {count} funcionalidades
bugFixes: uma correção de bug | {count} correções
breaking: uma mudança significativa | {count} mudanças significativas
three_parts: "{first}, {second} e {third}"
two_parts: "{first} com {second}"
latest: Mais recente
no_releases: Você tem a versão mais recente
log_actions:
copy_log: Copiar log
jump_to_context: Ir para contexto
toasts:
copied:
title: Copiado
message: Log copiado para a área de transferência
releases:
features: uma nova funcionalidade | {count} funcionalidades
bugFixes: uma correção de bug | {count} correções
breaking: uma mudança significativa | {count} mudanças significativas
three_parts: "{first}, {second} e {third}"
two_parts: "{first} com {second}"
latest: Mais recente
no_releases: Você tem a versão mais recente
log_actions:
copy_log: Copiar log
jump_to_context: Ir para contexto
settings:
log:
preview: Esta é uma prévia dos logs
warning: Um log de aviso se parece com isto
complex: Esta é uma entrada de log complexa em json
simple: Esta é uma mensagem muito longa que seria quebrada por padrão. Desativar quebras suaves desativaria isso. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
multi-line-error:
start-line: Esta é uma mensagem de erro multi-linha
middle-line: com uma segunda linha
end-line: e finalmente uma terceira linha.
analytics:
creating_table: A criar tabela temporária...
-2
View File
@@ -30,8 +30,6 @@ label:
host-menu: Hosts e Containers
swarm-menu: Serviços e Stacks
group-menu: Grupos Personalizados
no-logs: O container ainda não tem logs
show-all-containers: Mostrar todos os containers
tooltip:
search: Pesquisar containers (⌘ + k, ⌃k)
pin-column: Fixar como coluna
+24 -26
View File
@@ -4,16 +4,16 @@ toolbar:
search: Поиск
show: Показать только {std}
show-all: Показать все потоки
stop: Остановить
start: Запустить
restart: Перезапустить
stop: Stop
start: Start
restart: Restart
show-hostname: Показать имя хоста
show-container-name: Показать имя контейнера
label:
containers: Контейнеры
container: Нет контейнеров | 1 контейнер | {count} контейнеров
service: Нет сервисов | 1 сервис | {count} сервисов
services: Сервисы
container: No containers | 1 container | {count} containers
service: No services | 1 service | {count} services
services: Services
running-containers: Запущенные контейнеры
all-containers: Все контейнеры
host: Хост
@@ -25,19 +25,17 @@ label:
created: Создан
avg-cpu: средний процессор (%)
avg-mem: средняя память (%)
pinned: Закреплено
per-page: Строк на странице
pinned: Pinned
per-page: Rows per page
host-menu: Хосты и Контейнеры
swarm-menu: Сервисы и Стеки
group-menu: Пользовательские Группы
no-logs: У контейнера еще нет логов
show-all-containers: Показать все контейнеры
tooltip:
search: Поиск контейнеров (⌘ + k, ⌃k)
pin-column: Закрепить столбец
merge-services: Объединить все сервисы в один вид
merge-containers: Объединить все контейнеры в один вид
merge-hosts: Объединить все контейнеры на этом хосте в один вид
merge-services: Merge all services into one view
merge-containers: Merge all containers into one view
merge-hosts: Merge all containers on this host into one view
error:
page-not-found: Эта страница не доступна.
invalid-auth: Имя пользователя или пароль неверны.
@@ -68,8 +66,8 @@ button:
logout: Выйти
login: Войти
settings: Настройки
cancel: Отмена
redirect: Перенаправить
cancel: Cancel
redirect: Redirect
placeholder:
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
search: Поиск
@@ -102,20 +100,20 @@ settings:
middle-line: со второй строкой
end-line: и наконец третьей строкой.
releases:
features: одна новая функция | {count} функций
bugFixes: одно исправление ошибки | {count} исправлений
breaking: одно критическое изменение | {count} критических изменений
three_parts: "{first}, {second} и {third}"
two_parts: "{first} и {second}"
latest: Последняя
no_releases: У вас последняя версия
features: one new feature | {count} features
bugFixes: one bug fix | {count} fixes
breaking: one breaking change | {count} breaking changes
three_parts: "{first}, {second} and {third}"
two_parts: "{first} with {second}"
latest: Latest
no_releases: You have the latest version
log_actions:
copy_log: Копировать лог
jump_to_context: Перейти к контексту
copy_log: Copy log
jump_to_context: Jump to context
toasts:
copied:
title: Скопировано
message: Лог скопирован в буфер обмена
title: Copied
message: Log copied to clipboard
analytics:
creating_table: Создание временной таблицы...
downloading: Получение логов контейнеров... ({size})
-10
View File
@@ -7,13 +7,8 @@ toolbar:
stop: Durdur
start: Başlat
restart: Yeniden Başlat
show-hostname: Sunucu adını göster
show-container-name: Konteyner adını göster
label:
containers: Konteynerlar
container: Konteyner yok | 1 konteyner | {count} konteyner
service: Servis yok | 1 servis | {count} servis
services: Servisler
running-containers: Çalışan Konteynerlar
all-containers: Tüm Konteynerlar
total-containers: Toplam Konteyner
@@ -32,12 +27,9 @@ label:
avg-cpu: Ort. CPU (%)
avg-mem: Ort. Bellek (%)
pinned: Sabitlenmiş
per-page: Sayfa başına satır
host-menu: Sunucular ve Konteynerler
swarm-menu: Servisler ve Yığınlar
group-menu: Özel Gruplar
no-logs: Konteyner henüz log içermiyor
show-all-containers: Tüm konteynerleri göster
tooltip:
search: Konteynerlerde ara (⌘ + k, ⌃k)
pin-column: Sütun olarak sabitle
@@ -77,8 +69,6 @@ button:
logout: Çıkış Yap
login: Giriş Yap
settings: Ayarlar
cancel: İptal
redirect: Yönlendir
placeholder:
search-containers: Konteynerlerde ara (⌘ + k, ⌃k)
search: Ara
-2
View File
@@ -16,8 +16,6 @@ label:
services: 服務
running-containers: 運作中的容器
all-containers: 所有容器
no-logs: 容器尚無日誌
show-all-containers: 顯示所有容器
host: 主機
hosts: 主機
password: 密碼
+3 -5
View File
@@ -16,8 +16,6 @@ label:
services: 服务
running-containers: 运行中的容器
all-containers: 所有容器
no-logs: 容器尚无日志
show-all-containers: 显示所有容器
host: 主机
hosts: 主机
password: 密码
@@ -37,7 +35,7 @@ tooltip:
pin-column: 固定为列
merge-services: 合并所有服务到一个视图
merge-containers: 合并所有容器到一个视图
merge-hosts: 合并此主机上的所有容器到一个视图
merge-hosts: Merge all containers on this host into one view
error:
page-not-found: 此页面不存在。
invalid-auth: 用户名和密码无效。
@@ -58,7 +56,7 @@ alert:
similar-container-found:
title: 找到相似的容器
message: >-
Dozzle发现了一个相似的容器 {containerId},它在同一主机上运行。您想切换到它吗?
Dozzle发现了一个相似的容器 {containerId},它在同一主机上运行。您想切换到它吗?d
title:
page-not-found: 页面未找到
login: 需要身份验证
@@ -88,7 +86,7 @@ settings:
search: 使用Dozzle启用搜索
using-version: 您正在使用Dozzle {version}。
update-available: >-
新版本可用!更新到 <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>。
新版本可用!更新到 <a href="{href}" rel="noreferrer noopener">{nextVersion}</a>。
show-std: 显示stdout和stderr标签
automatic-redirect: 自动重定向到同名的新容器
compact: 紧凑模式
+29 -27
View File
@@ -1,12 +1,12 @@
{
"name": "dozzle",
"version": "8.12.16",
"version": "8.12.10",
"description": "Realtime log viewer for docker containers.",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
"url": "https://github.com/amir20/dozzle/issues"
},
"packageManager": "pnpm@10.11.0",
"packageManager": "pnpm@10.8.1",
"type": "module",
"repository": {
"type": "git",
@@ -33,18 +33,19 @@
"@iconify-json/carbon": "^1.2.8",
"@iconify-json/cil": "^1.2.2",
"@iconify-json/ic": "^1.2.2",
"@iconify-json/material-symbols": "^1.2.21",
"@iconify-json/material-symbols": "^1.2.20",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/mdi-light": "^1.2.2",
"@iconify-json/octicon": "^1.2.6",
"@iconify-json/octicon": "^1.2.5",
"@iconify-json/ph": "^1.2.2",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@intlify/unplugin-vue-i18n": "^6.0.7",
"@oddbird/css-anchor-positioning": "^0.5.1",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "4.1.7",
"@vueuse/components": "^13.2.0",
"@vueuse/core": "^13.2.0",
"@vueuse/integrations": "^13.2.0",
"@vueuse/router": "^13.2.0",
"@tailwindcss/vite": "4.1.4",
"@vueuse/components": "^13.1.0",
"@vueuse/core": "^13.1.0",
"@vueuse/integrations": "^13.1.0",
"@vueuse/router": "^13.1.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"ansi-to-html": "^0.7.2",
@@ -54,7 +55,8 @@
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"d3-transition": "^3.0.1",
"daisyui": "5.0.35",
"daisyui": "5.0.27",
"date-fns": "^4.1.0",
"entities": "^6.0.0",
"fuse.js": "^7.1.0",
"lodash.debounce": "^4.0.8",
@@ -62,23 +64,23 @@
"sortablejs": "^1.15.6",
"splitpanes": "^4.0.3",
"strip-ansi": "^7.1.0",
"tailwindcss": "4.1.7",
"unplugin-auto-import": "^19.2.0",
"tailwindcss": "4.1.4",
"unplugin-auto-import": "^19.1.2",
"unplugin-icons": "^22.1.0",
"unplugin-vue-components": "^28.5.0",
"unplugin-vue-macros": "^2.14.5",
"unplugin-vue-router": "^0.12.0",
"vite": "6.3.5",
"vite": "6.3.2",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-svg-loader": "^5.1.0",
"vitepress": "1.6.3",
"vue": "^3.5.14",
"vue": "^3.5.13",
"vue-i18n": "^11.1.3",
"vue-router": "^4.5.1"
"vue-router": "^4.5.0"
},
"devDependencies": {
"@apache-arrow/esnext-esm": "^20.0.0",
"@iconify-json/material-symbols-light": "^1.2.21",
"@apache-arrow/esnext-esm": "^19.0.1",
"@iconify-json/material-symbols-light": "^1.2.20",
"@iconify-json/ri": "^1.2.5",
"@pinia/testing": "^1.0.1",
"@playwright/test": "^1.52.0",
@@ -89,24 +91,24 @@
"@types/d3-shape": "^3.1.7",
"@types/d3-transition": "^3.0.9",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^22.15.19",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "^3.5.14",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "5.2.3",
"@vue/compiler-sfc": "^3.5.13",
"@vue/test-utils": "^2.4.6",
"bumpp": "^10.1.1",
"bumpp": "^10.1.0",
"c8": "^10.1.3",
"concurrently": "^9.1.2",
"eventsourcemock": "^2.0.0",
"jsdom": "^26.1.0",
"lint-staged": "^16.0.0",
"lint-staged": "^15.5.1",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"simple-git-hooks": "^2.13.0",
"simple-git-hooks": "^2.12.1",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"vitest": "^3.1.3",
"vue-component-type-helpers": "3.0.0-alpha.6",
"vue-tsc": "3.0.0-alpha.6"
"vitest": "^3.1.1",
"vue-component-type-helpers": "3.0.0-alpha.4",
"vue-tsc": "3.0.0-alpha.4"
},
"lint-staged": {
"*.{js,vue,css,ts,html,md}": [
+1392 -958
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More