feat: groups containers by stack or compose when possible (#2893)
@@ -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"><null></span>
|
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null"><null></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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(() =>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ button:
|
|||||||
settings: Настройки
|
settings: Настройки
|
||||||
placeholder:
|
placeholder:
|
||||||
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
|
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
|
||||||
|
search: Поиск
|
||||||
settings:
|
settings:
|
||||||
display: Вид
|
display: Вид
|
||||||
locale: Язык
|
locale: Язык
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ button:
|
|||||||
settings: 設定
|
settings: 設定
|
||||||
placeholder:
|
placeholder:
|
||||||
search-containers: 查詢容器 (⌘ + k, ⌃k)
|
search-containers: 查詢容器 (⌘ + k, ⌃k)
|
||||||
|
search: 查詢
|
||||||
settings:
|
settings:
|
||||||
display: 顯示
|
display: 顯示
|
||||||
locale: 覆寫語言
|
locale: 覆寫語言
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ button:
|
|||||||
settings: 设置
|
settings: 设置
|
||||||
placeholder:
|
placeholder:
|
||||||
search-containers: 搜索容器 (⌘ + k, ⌃k)
|
search-containers: 搜索容器 (⌘ + k, ⌃k)
|
||||||
|
search: 搜索
|
||||||
settings:
|
settings:
|
||||||
display: 显示
|
display: 显示
|
||||||
locale: 覆盖语言
|
locale: 覆盖语言
|
||||||
|
|||||||