1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-24 06:28:42 +01:00

feat: groups containers by stack or compose when possible (#2893)

This commit is contained in:
Amir Raminfar
2024-04-14 14:05:27 -07:00
committed by GitHub
parent 56aa440c92
commit ec03e93298
19 changed files with 97 additions and 70 deletions

View File

@@ -16,7 +16,7 @@
</div> </div>
<div> <div>
<ul class="fields cursor-pointer space-x-4" :class="{ expanded }"> <ul class="fields cursor-pointer space-x-4" :class="{ expanded }">
<li v-for="(value, name) in validValues"> <li v-for="(value, name) in validValues" :key="name">
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span> <span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)"> <template v-else-if="Array.isArray(value)">
<span class="font-bold" v-html="markSearch(JSON.stringify(value))"> </span> <span class="font-bold" v-html="markSearch(JSON.stringify(value))"> </span>

View File

@@ -1,6 +1,6 @@
<template> <template>
<ul v-if="expanded" ref="root" class="ml-8"> <ul v-if="expanded" ref="root" class="ml-8">
<li v-for="(value, name) in fields"> <li v-for="(value, name) in fields" :key="name">
<template v-if="isObject(value)"> <template v-if="isObject(value)">
<span class="text-light">{{ name }}=</span> <span class="text-light">{{ name }}=</span>
<field-list <field-list

View File

@@ -8,9 +8,10 @@
</li> </li>
</ul> </ul>
</div> </div>
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in"> <transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
<ul class="menu p-0" v-if="!sessionHost"> <ul class="menu p-0" v-if="!sessionHost">
<li v-for="host in hosts"> <li v-for="host in hosts" :key="host.id">
<a @click.prevent="setHost(host.id)" :class="{ 'pointer-events-none text-base-content/50': !host.available }"> <a @click.prevent="setHost(host.id)" :class="{ 'pointer-events-none text-base-content/50': !host.available }">
<ph:computer-tower /> <ph:computer-tower />
{{ host.name }} {{ host.name }}
@@ -18,42 +19,44 @@
</a> </a>
</li> </li>
</ul> </ul>
<transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else> <ul class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
<li <li v-for="{ label, containers, icon } in menuItems" :key="label">
v-for="item in menuItems" <details open>
:key="isContainer(item) ? item.id : item.keyLabel" <summary class="font-light text-base-content/80">
:class="isContainer(item) ? item.state : 'menu-title'" <component :is="icon" />
:data-testid="isContainer(item) ? null : item.keyLabel" {{ label.startsWith("label.") ? $t(label) : label }}
> </summary>
<popup v-if="isContainer(item)"> <ul>
<router-link <li v-for="item in containers" :class="item.state" :key="item.id">
:to="{ name: 'container-id', params: { id: item.id } }" <popup>
active-class="active-primary" <router-link
@click.alt.stop.prevent="store.appendActiveContainer(item)" :to="{ name: 'container-id', params: { id: item.id } }"
:title="item.name" active-class="active-primary"
> @click.alt.stop.prevent="store.appendActiveContainer(item)"
<div class="truncate"> :title="item.name"
{{ item.name }}<span class="font-light opacity-70" v-if="item.isSwarm">{{ item.swarmId }}</span> >
</div> <div class="truncate">
<container-health :health="item.health"></container-health> {{ item.name }}<span class="font-light opacity-70" v-if="item.isSwarm">{{ item.swarmId }}</span>
<span </div>
class="pin" <container-health :health="item.health"></container-health>
@click.stop.prevent="store.appendActiveContainer(item)" <span
v-show="!activeContainersById[item.id]" class="pin"
:title="$t('tooltip.pin-column')" @click.stop.prevent="store.appendActiveContainer(item)"
> v-show="!activeContainersById[item.id]"
<cil:columns /> :title="$t('tooltip.pin-column')"
</span> >
</router-link> <cil:columns />
<template #content> </span>
<container-popup :container="item"></container-popup> </router-link>
</template> <template #content>
</popup> <container-popup :container="item"></container-popup>
<template v-else> </template>
{{ $t(item.keyLabel) }} </popup>
</template> </li>
</ul>
</details>
</li> </li>
</transition-group> </ul>
</transition> </transition>
</div> </div>
<div role="status" class="flex animate-pulse flex-col gap-4" v-else> <div role="status" class="flex animate-pulse flex-col gap-4" v-else>
@@ -66,6 +69,13 @@
import { Container } from "@/models/Container"; import { Container } from "@/models/Container";
import { sessionHost } from "@/composable/storage"; import { sessionHost } from "@/composable/storage";
// @ts-ignore
import Pin from "~icons/ph/map-pin-simple";
// @ts-ignore
import Stack from "~icons/ph/stack";
// @ts-ignore
import Containers from "~icons/octicon/container-24";
const store = useContainerStore(); const store = useContainerStore();
const { activeContainers, visibleContainers, ready } = storeToRefs(store); const { activeContainers, visibleContainers, ready } = storeToRefs(store);
@@ -75,7 +85,7 @@ function setHost(host: string | null) {
sessionHost.value = host; sessionHost.value = host;
} }
const debouncedIds = debouncedRef(pinnedContainers, 200); const debouncedPinnedContainers = debouncedRef(pinnedContainers, 200);
const sortedContainers = computed(() => const sortedContainers = computed(() =>
visibleContainers.value visibleContainers.value
.filter((c) => c.host === sessionHost.value) .filter((c) => c.host === sessionHost.value)
@@ -90,32 +100,39 @@ const sortedContainers = computed(() =>
}), }),
); );
const groupedContainers = computed(() =>
sortedContainers.value.reduce(
(acc, item) => {
if (debouncedIds.value.has(item.name)) {
acc.pinned.push(item);
} else {
acc.unpinned.push(item);
}
return acc;
},
{ pinned: [] as Container[], unpinned: [] as Container[] },
),
);
function isContainer(item: any): item is Container {
return item.hasOwnProperty("image");
}
const menuItems = computed(() => { const menuItems = computed(() => {
const pinnedLabel = { keyLabel: "label.pinned" }; const namespaced: Record<string, Container[]> = {};
const allLabel = { keyLabel: showAllContainers.value ? "label.all-containers" : "label.running-containers" }; const pinned = [];
if (groupedContainers.value.pinned.length > 0) { const singular = [];
return [pinnedLabel, ...groupedContainers.value.pinned, allLabel, ...groupedContainers.value.unpinned];
} else { for (const item of sortedContainers.value) {
return [allLabel, ...groupedContainers.value.unpinned]; const namespace = item.labels["com.docker.stack.namespace"] ?? item.labels["com.docker.compose.project"];
if (debouncedPinnedContainers.value.has(item.name)) {
pinned.push(item);
} else if (namespace) {
namespaced[namespace] ||= [];
namespaced[namespace].push(item);
} else {
singular.push(item);
}
} }
const items = [];
if (pinned.length) {
items.push({ label: "label.pinned", containers: pinned, icon: Pin });
}
for (const [label, containers] of Object.entries(namespaced).sort(([a], [b]) => a.localeCompare(b))) {
items.push({ label, containers, icon: Stack });
}
if (singular.length) {
items.push({
label: showAllContainers.value ? "label.all-containers" : "label.running-containers",
containers: singular,
icon: Containers,
});
}
return items;
}); });
const activeContainersById = computed(() => const activeContainersById = computed(() =>

View File

@@ -12,15 +12,16 @@
</small> </small>
</h1> </h1>
<a <button
class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary" class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary"
@click="$emit('search')" @click="$emit('search')"
:title="$t('tooltip.search')" :title="$t('tooltip.search')"
data-testid="search"
> >
<mdi:magnify /> <mdi:magnify />
Search {{ $t("placeholder.search") }}
<key-shortcut char="k"></key-shortcut> <key-shortcut char="k" class="text-base-content/70"></key-shortcut>
</a> </button>
<side-menu class="mt-4"></side-menu> <side-menu class="mt-4"></side-menu>
</aside> </aside>

View File

@@ -34,6 +34,6 @@ test.describe("es locale", () => {
test.use({ locale: "es" }); test.use({ locale: "es" });
test("translated text", async ({ page }) => { test("translated text", async ({ page }) => {
await expect(page.getByTestId("label.running-containers")).toHaveText("Contenedores en ejecución"); await expect(page.getByTestId("search")).toContainText("Buscar");
}); });
}); });

View File

@@ -5,5 +5,5 @@ test("simple authentication", async ({ page }) => {
await page.locator('input[name="username"]').fill("admin"); await page.locator('input[name="username"]').fill("admin");
await page.locator('input[name="password"]').fill("password"); await page.locator('input[name="password"]').fill("password");
await page.locator('button[type="submit"]').click(); await page.locator('button[type="submit"]').click();
await expect(page.getByTestId("label.running-containers")).toHaveText("Running Containers"); await expect(page.getByTestId("settings")).toBeVisible();
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -62,6 +62,7 @@ button:
settings: Einstellungen settings: Einstellungen
placeholder: placeholder:
search-containers: Suche Container (⌘ + k, ⌃k) search-containers: Suche Container (⌘ + k, ⌃k)
search: Suche
settings: settings:
display: Anzeige display: Anzeige
locale: Sprache überschreiben locale: Sprache überschreiben

View File

@@ -65,6 +65,7 @@ button:
settings: Settings settings: Settings
placeholder: placeholder:
search-containers: Search containers (⌘ + k, ⌃k) search-containers: Search containers (⌘ + k, ⌃k)
search: Search
settings: settings:
display: Display display: Display
locale: Override language locale: Override language

View File

@@ -62,6 +62,7 @@ button:
settings: Configuración settings: Configuración
placeholder: placeholder:
search-containers: Buscar contenedores (⌘ + K, CTRL + K) search-containers: Buscar contenedores (⌘ + K, CTRL + K)
search: Buscar
settings: settings:
display: Vista display: Vista
locale: Sobrescribir idioma locale: Sobrescribir idioma

View File

@@ -1,5 +1,5 @@
toolbar: toolbar:
clear: Effacer clear: Effacer
download: Téléchargement download: Téléchargement
search: Chercher search: Chercher
show: Montrer seulement {std} show: Montrer seulement {std}
@@ -64,6 +64,7 @@ button:
settings: Paramètres settings: Paramètres
placeholder: placeholder:
search-containers: Recherche de conteneurs (⌘ + k, ⌃k) search-containers: Recherche de conteneurs (⌘ + k, ⌃k)
search: Recherche
settings: settings:
display: Afficher display: Afficher
locale: Langue de remplacement locale: Langue de remplacement

View File

@@ -62,6 +62,7 @@ button:
settings: Configurazione settings: Configurazione
placeholder: placeholder:
search-containers: Ricerca container (⌘ + k, ⌃k) search-containers: Ricerca container (⌘ + k, ⌃k)
search: Cerca
settings: settings:
display: Visualizza display: Visualizza
locale: Sovrascrivi Linguaggio locale: Sovrascrivi Linguaggio

View File

@@ -58,6 +58,7 @@ button:
settings: Configurações settings: Configurações
placeholder: placeholder:
search-containers: Pesquisar contentores (⌘ + K, CTRL + K) search-containers: Pesquisar contentores (⌘ + K, CTRL + K)
search: Pesquisa
settings: settings:
display: Visão display: Visão
locale: Localidade locale: Localidade

View File

@@ -58,6 +58,7 @@ button:
settings: Настройки settings: Настройки
placeholder: placeholder:
search-containers: Поиск контейнеров (⌘ + k, ⌃k) search-containers: Поиск контейнеров (⌘ + k, ⌃k)
search: Поиск
settings: settings:
display: Вид display: Вид
locale: Язык locale: Язык

View File

@@ -41,6 +41,7 @@ button:
settings: 設定 settings: 設定
placeholder: placeholder:
search-containers: 查詢容器 (⌘ + k, ⌃k) search-containers: 查詢容器 (⌘ + k, ⌃k)
search: 查詢
settings: settings:
display: 顯示 display: 顯示
locale: 覆寫語言 locale: 覆寫語言

View File

@@ -58,6 +58,7 @@ button:
settings: 设置 settings: 设置
placeholder: placeholder:
search-containers: 搜索容器 (⌘ + k, ⌃k) search-containers: 搜索容器 (⌘ + k, ⌃k)
search: 搜索
settings: settings:
display: 显示 display: 显示
locale: 覆盖语言 locale: 覆盖语言