fix(ui): restore log header on notched phones and widen mobile container names (#4972)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-08-31 11:51:50 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent f9dc18a0aa
commit db80ba682e
20 changed files with 111 additions and 15 deletions
+64 -14
View File
@@ -45,6 +45,17 @@
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
/>
</div>
<div class="flex items-center gap-1 md:hidden">
{{ $t("label.sort-by") }}
<DropdownMenu class="dropdown-left btn-xs" v-model="mobileSortField" :options="sortOptions" />
<button
class="btn btn-square btn-ghost btn-xs"
@click="direction *= -1"
:aria-label="$t('label.sort-direction')"
>
<mdi:arrow-up :class="direction > 0 ? '' : 'rotate-180'" />
</button>
</div>
<div class="join max-md:hidden">
<button
class="btn join-item btn-xs md:btn-sm"
@@ -65,7 +76,7 @@
</div>
<div class="rounded-box border-base-content/10 overflow-x-auto border">
<table class="table-md md:table-lg table-zebra table">
<thead>
<thead class="max-md:hidden">
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
v-for="(value, key) in fields"
@@ -90,20 +101,45 @@
v-memo="[container.id, container.state, container.health, statMode, isMobile]"
class="hover:bg-base-100/80!"
>
<td v-if="isVisible('name')" class="max-w-80 max-md:max-w-32">
<div class="flex items-center gap-2">
<ContainerStatusIcon :state="container.state" :health="container.health" class="shrink-0" />
<div class="min-w-0">
<router-link
class="block truncate"
:to="{ name: '/container/[id]', params: { id: container.id } }"
:title="container.name"
>
{{ container.name }}
</router-link>
<td v-if="isVisible('name')" class="max-w-80 max-md:max-w-none">
<div class="flex items-center gap-2 max-md:items-start">
<ContainerStatusIcon
:state="container.state"
:health="container.health"
class="shrink-0 max-md:mt-0.5"
/>
<div class="min-w-0 flex-1">
<div class="flex items-baseline gap-2">
<router-link
class="min-w-0 flex-1 truncate"
:to="{ name: '/container/[id]', params: { id: container.id } }"
:title="container.name"
>
{{ container.name }}
</router-link>
<RelativeTime
v-if="isMobile"
:date="container.created"
class="text-base-content/50 shrink-0 text-xs"
/>
</div>
<div v-if="container.customGroup" class="text-base-content/50 truncate text-xs">
{{ container.customGroup }}
</div>
<div v-if="isMobile && container.state === 'running'" class="mt-1.5 flex items-center gap-1.5">
<ContainerStatCell
:container="container"
type="cpu"
:host="hosts[container.host]"
:mode="statMode"
/>
<ContainerStatCell
:container="container"
type="mem"
:host="hosts[container.host]"
:mode="statMode"
/>
</div>
</div>
</div>
</td>
@@ -147,6 +183,7 @@
import { Container } from "@/models/Container";
import { toRefs } from "@vueuse/core";
const { t } = useI18n();
const { hosts } = useHosts();
const selectedHost = ref(null);
@@ -190,7 +227,7 @@ const fields: Record<
label: "label.avg-cpu",
mobileLabel: "label.cpu",
sortFunc: (a: Container, b: Container) => (a.movingAverage.cpu - b.movingAverage.cpu) * direction.value,
mobileVisible: true,
mobileVisible: false,
customClass: "min-w-48 max-md:min-w-0",
},
mem: {
@@ -198,7 +235,7 @@ const fields: Record<
mobileLabel: "label.mem",
sortFunc: (a: Container, b: Container) =>
(a.movingAverage.memoryUsage - b.movingAverage.memoryUsage) * direction.value,
mobileVisible: true,
mobileVisible: false,
customClass: "min-w-48 max-md:min-w-0",
},
};
@@ -237,6 +274,19 @@ const paginated = computed(() => {
return sortedContainers.value.slice(start, end);
});
const sortOptions = computed(() =>
Object.entries(fields)
.filter(([key]) => key !== "host" || Object.keys(hosts.value).length > 1)
.map(([key, value]) => ({ label: t(value.mobileLabel ?? value.label), value: key })),
);
const mobileSortField = computed({
get: () => sortField.value,
set: (field: keys) => {
if (field !== sortField.value) sort(field);
},
});
function sort(field: keys) {
if (sortField.value === field) {
direction.value *= -1;
+1 -1
View File
@@ -3,7 +3,7 @@
<header
v-if="$slots.header"
data-testid="scrollable-header"
class="border-base-content/10 bg-base-200 sticky top-[var(--mobile-nav-height)] z-20 border-b py-0.5 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0 md:py-2"
class="border-base-content/10 bg-base-200 sticky top-[var(--mobile-nav-offset)] z-20 border-b py-0.5 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0 md:py-2"
>
<slot name="header"></slot>
</header>
+4
View File
@@ -207,6 +207,10 @@ body {
* they stay locked together. The safe-area inset is already handled by body. */
:root {
--mobile-nav-height: 57px;
/* Viewport offset of the nav's bottom edge. The nav is fixed to the viewport
* so it sits above the body's safe-area padding, which means anything sticking
* to it must add the inset back in. */
--mobile-nav-offset: calc(var(--mobile-nav-height) + env(safe-area-inset-top));
}
[class*="shadow-"] {
+10
View File
@@ -26,5 +26,15 @@ test.describe("mobile stats", () => {
// The stats header must sit flush against the bottom of the fixed nav bar:
// any gap exposes empty space, any overlap clips the CPU/MEM chips underneath the nav.
expect(Math.abs(header!.y - (nav!.y + nav!.height))).toBeLessThanOrEqual(1);
// ...and stay there once the logs scroll under it.
await page.mouse.wheel(0, 600);
await expect
.poll(async () => {
const stuckNav = await page.getByTestId("navigation").boundingBox();
const stuckHeader = await page.getByTestId("scrollable-header").boundingBox();
return Math.abs(stuckHeader!.y - (stuckNav!.y + stuckNav!.height));
})
.toBeLessThanOrEqual(1);
});
});
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Fastgjort
per-page: Rækker per side
sort-by: Sortér efter
sort-direction: Skift sorteringsrækkefølge
host-menu: Værter og Containere
swarm-menu: Tjenester og Stacks
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Angepinnt
per-page: Zeilen pro Seite
sort-by: Sortieren nach
sort-direction: Sortierreihenfolge umkehren
host-menu: Hosts und Container
swarm-menu: Services und Stacks
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Pinned
per-page: Rows per page
sort-by: Sort by
sort-direction: Toggle sort direction
host-menu: Hosts and Containers
swarm-menu: Services and Stacks
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Fijado
per-page: Filas por página
sort-by: Ordenar por
sort-direction: Cambiar el orden
host-menu: Hosts y Contenedores
swarm-menu: Servicios y Stacks
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Epinglé
per-page: Colonnes par page
sort-by: Trier par
sort-direction: Inverser l'ordre de tri
host-menu: Hôtes et Conteneurs
swarm-menu: Services et Stacks
k8s-menu: Kubernetes
+2
View File
@@ -66,6 +66,8 @@ label:
mem: MEM
pinned: Disematkan
per-page: Baris per halaman
sort-by: Urutkan menurut
sort-direction: Ubah arah pengurutan
host-menu: Host dan Kontainer
swarm-menu: Layanan dan Stack
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Fissato
per-page: Righe per pagina
sort-by: Ordina per
sort-direction: Inverti l'ordine
host-menu: Host e Container
swarm-menu: Servizi e Stack
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: 메모리
pinned: 고정됨
per-page: 페이지당 행 수
sort-by: 정렬 기준
sort-direction: 정렬 순서 전환
host-menu: 호스트 및 컨테이너
swarm-menu: 서비스 및 스택
k8s-menu: 쿠버네티스
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Vastgezet
per-page: Rijen per pagina
sort-by: Sorteren op
sort-direction: Sorteervolgorde omkeren
host-menu: Hosts en Containers
swarm-menu: Services en Stacks
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Przypięte
per-page: Wierszy na strone
sort-by: Sortuj według
sort-direction: Zmień kierunek sortowania
host-menu: Hosty i Kontenery
swarm-menu: Usługi i Stosy
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Fixado
per-page: Linhas por página
sort-by: Ordenar por
sort-direction: Inverter a ordem
host-menu: Hosts e Containers
swarm-menu: Serviços e Stacks
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Закреплено
per-page: Строк на странице
sort-by: Сортировать по
sort-direction: Изменить порядок сортировки
host-menu: Хосты и Контейнеры
swarm-menu: Сервисы и Стеки
k8s-menu: Kubernetes
+2
View File
@@ -48,6 +48,8 @@ label:
avg-cpu: CPE povprečje (%)
pinned: Pripeto
per-page: Vrstic na stran
sort-by: Razvrsti po
sort-direction: Zamenjaj vrstni red
host-menu: Gostitelji in zabojniki
swarm-menu: Storitve in skladi
k8s-menu: Kubernetes
+2
View File
@@ -65,6 +65,8 @@ label:
mem: MEM
pinned: Sabitlenmiş
per-page: Sayfa başına satır
sort-by: Sırala
sort-direction: Sıralama yönünü değiştir
host-menu: Sunucular ve Konteynerler
swarm-menu: Servisler ve Yığınlar
k8s-menu: Kubernetes
+2
View File
@@ -76,6 +76,8 @@ label:
mem: MEM
pinned: 已釘選
per-page: 每頁列數
sort-by: 排序方式
sort-direction: 切換排序方向
host-menu: 主機和容器
swarm-menu: 服務和堆疊
k8s-menu: Kubernetes
+2
View File
@@ -76,6 +76,8 @@ label:
mem: MEM
pinned: 固定
per-page: 每页行数
sort-by: 排序方式
sort-direction: 切换排序方向
host-menu: 主机和容器
swarm-menu: 服务和堆栈
k8s-menu: Kubernetes