chore: cleanups across mounts + multi-container stats (#4716)
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-05-19 09:13:28 -07:00
committed by GitHub
parent 0201f813ba
commit 118a7deeab
24 changed files with 232 additions and 101 deletions
@@ -15,7 +15,7 @@
tabindex="0"
class="dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 mt-1 w-72 border p-2 text-xs shadow-sm"
>
<div class="text-base-content/60 mb-1.5 px-1 text-[11px] tracking-wide uppercase">Volumes</div>
<div class="text-base-content/60 mb-1.5 px-1 text-[11px] tracking-wide uppercase">{{ t("tooltip.volumes") }}</div>
<ul class="space-y-1.5">
<li
v-for="m in sortedMounts"
@@ -33,7 +33,7 @@
</div>
<div class="text-base-content/60 flex justify-between tabular-nums">
<span v-if="m.available">{{ formatBytes(m.used) }} / {{ formatBytes(m.total) }}</span>
<span v-else>not reachable from this host</span>
<span v-else>{{ t("tooltip.volume-unreachable") }}</span>
<RelativeTime v-if="m.lastChecked" :date="m.lastChecked" class="text-[10.5px]" />
</div>
</li>
@@ -52,6 +52,8 @@ const CRITICAL = 0.95;
const { container } = defineProps<{ container: Container; openUp?: boolean }>();
const { t } = useI18n();
interface DerivedMount {
destination: string;
total: number;
@@ -107,6 +109,6 @@ function formatPct(pct: number) {
const title = computed(() => {
if (!worst.value) return "";
return `${worst.value.destination} is ${formatPct(worst.value.pct)} full`;
return t("tooltip.volume-full", { destination: worst.value.destination, pct: formatPct(worst.value.pct) });
});
</script>
+4 -2
View File
@@ -34,9 +34,11 @@ const { networkRx, networkTx, diskRead, diskWrite } = defineProps<{
diskWrite: number;
}>();
const { t } = useI18n();
const tooltip = computed(
() =>
`Network: ↑ ${formatBytes(networkTx)}/s · ↓ ${formatBytes(networkRx)}/s\n` +
`Disk: ↑ ${formatBytes(diskWrite)}/s · ↓ ${formatBytes(diskRead)}/s`,
t("tooltip.network-io", { tx: formatBytes(networkTx), rx: formatBytes(networkRx) }) +
"\n" +
t("tooltip.disk-io", { write: formatBytes(diskWrite), read: formatBytes(diskRead) }),
);
</script>
@@ -11,7 +11,7 @@
:icon="PhCpu"
card-class="bg-primary/10 md:min-w-56"
icon-class="text-primary"
:title="`CPU ${totalStat.cpu.toFixed(2)}% / ${roundCPU(limits.cpu)} cores`"
:title="t('tooltip.cpu-usage', { cpu: totalStat.cpu.toFixed(2), cores: roundCPU(limits.cpu) })"
>
<template #value="{ hoveredValue }">
<span class="tabular-nums">
@@ -28,7 +28,9 @@
:icon="PhMemory"
card-class="bg-secondary/10 md:min-w-56"
icon-class="text-secondary"
:title="`Memory ${formatBytes(totalStat.memoryUsage)} / ${formatBytes(limits.memory)}`"
:title="
t('tooltip.memory-usage', { used: formatBytes(totalStat.memoryUsage), total: formatBytes(limits.memory) })
"
>
<template #value="{ hoveredValue }">
<span class="tabular-nums">
@@ -46,8 +48,7 @@
</template>
<script lang="ts" setup>
import { Stat } from "@/models/Container";
import { Container } from "@/models/Container";
import { Container, Stat, emptyStat } from "@/models/Container";
import StatCard from "@/components/LogViewer/StatCard.vue";
import IOCard from "@/components/LogViewer/IOCard.vue";
import Sparkline from "@/components/LogViewer/Sparkline.vue";
@@ -60,15 +61,9 @@ const { containers } = defineProps<{
containers: Container[];
}>();
const totalStat = ref<Stat>({
cpu: 0,
memory: 0,
memoryUsage: 0,
networkRxTotal: 0,
networkTxTotal: 0,
diskReadTotal: 0,
diskWriteTotal: 0,
});
const { t } = useI18n();
const totalStat = ref<Stat>(emptyStat());
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
const { hosts } = useHosts();
const networkRate = ref({ rx: 0, tx: 0 });
@@ -89,33 +84,22 @@ watch(
() => {
const initial: Stat[] = [];
for (let i = 1; i <= 300; i++) {
const stat = containers.reduce(
(acc, container) => {
const item = container.statsHistory.at(-i);
if (!item) {
return acc;
}
const cores = toContainerCores(container);
return {
cpu: acc.cpu + item.cpu / cores,
memory: acc.memory + item.memory,
memoryUsage: acc.memoryUsage + item.memoryUsage,
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
diskReadTotal: acc.diskReadTotal + item.diskReadTotal,
diskWriteTotal: acc.diskWriteTotal + item.diskWriteTotal,
};
},
{
cpu: 0,
memory: 0,
memoryUsage: 0,
networkRxTotal: 0,
networkTxTotal: 0,
diskReadTotal: 0,
diskWriteTotal: 0,
},
);
const stat = containers.reduce((acc, container) => {
const item = container.statsHistory.at(-i);
if (!item) {
return acc;
}
const cores = toContainerCores(container);
return {
cpu: acc.cpu + item.cpu / cores,
memory: acc.memory + item.memory,
memoryUsage: acc.memoryUsage + item.memoryUsage,
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
diskReadTotal: acc.diskReadTotal + item.diskReadTotal,
diskWriteTotal: acc.diskWriteTotal + item.diskWriteTotal,
};
}, emptyStat());
initial.push(stat);
}
totalStat.value = initial[0];
@@ -164,21 +148,18 @@ const limits = computed(() => {
useIntervalFn(() => {
const previousStat = totalStat.value;
totalStat.value = containers.reduce(
(acc, container) => {
const cores = toContainerCores(container);
return {
cpu: acc.cpu + container.stat.cpu / cores,
memory: acc.memory + container.stat.memory,
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
diskReadTotal: acc.diskReadTotal + container.stat.diskReadTotal,
diskWriteTotal: acc.diskWriteTotal + container.stat.diskWriteTotal,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0, diskReadTotal: 0, diskWriteTotal: 0 },
);
totalStat.value = containers.reduce((acc, container) => {
const cores = toContainerCores(container);
return {
cpu: acc.cpu + container.stat.cpu / cores,
memory: acc.memory + container.stat.memory,
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
diskReadTotal: acc.diskReadTotal + container.stat.diskReadTotal,
diskWriteTotal: acc.diskWriteTotal + container.stat.diskWriteTotal,
};
}, emptyStat());
networkRate.value = {
rx: Math.max(0, totalStat.value.networkRxTotal - previousStat.networkRxTotal),
+19 -8
View File
@@ -230,12 +230,17 @@ async function skipFeedback() {
}
}
let abortController: AbortController | null = null;
async function createDefaultAlerts() {
if (creating.value) return;
creating.value = true;
abortController?.abort();
abortController = new AbortController();
const signal = abortController.signal;
const chosen = signals.value.filter((s) => selectedSignals.value.includes(s.key));
try {
const dispatchersRes = await fetch(withBase("/api/notifications/dispatchers"));
const dispatchersRes = await fetch(withBase("/api/notifications/dispatchers"), { signal });
if (!dispatchersRes.ok) throw new Error("dispatchers fetch failed");
const dispatchers: Array<{ id: number; type: string }> = await dispatchersRes.json();
const cloud = dispatchers.find((d) => d.type === "cloud");
@@ -246,22 +251,23 @@ async function createDefaultAlerts() {
// fallback toast path. Acceptable for a welcome modal; the user can edit
// or delete rules from /notifications.
await Promise.all(
chosen.map((signal) =>
chosen.map((s) =>
fetch(withBase("/api/notifications/rules"), {
method: "POST",
headers: { "Content-Type": "application/json" },
signal,
body: JSON.stringify({
name: signal.ruleName,
name: s.ruleName,
enabled: true,
dispatcherId: cloud.id,
logExpression: "",
containerExpression: "true",
eventExpression: signal.kind === "event" ? signal.expression : "",
metricExpression: signal.kind === "metric" ? signal.expression : "",
eventExpression: s.kind === "event" ? s.expression : "",
metricExpression: s.kind === "metric" ? s.expression : "",
// Metric alerts: don't re-fire more than once an hour per container,
// and require the threshold to hold for the default sample window.
cooldown: signal.kind === "metric" ? 3600 : 0,
sampleWindow: signal.kind === "metric" ? 60 : 0,
cooldown: s.kind === "metric" ? 3600 : 0,
sampleWindow: s.kind === "metric" ? 60 : 0,
}),
}).then((res) => {
if (!res.ok) throw new Error("rule POST failed");
@@ -271,7 +277,8 @@ async function createDefaultAlerts() {
close();
router.push({ path: "/notifications" });
} catch {
} catch (err) {
if ((err as Error)?.name === "AbortError") return;
close();
showToast(
{
@@ -299,6 +306,10 @@ function close() {
modal.value?.close();
}
onBeforeUnmount(() => {
abortController?.abort();
});
function onClose() {
if (step.value === "step1" && !feedbackSent) {
feedbackSent = true;
+11 -9
View File
@@ -10,6 +10,16 @@ import { Ref } from "vue";
export type Stat = Omit<ContainerStat, "id">;
export const emptyStat = (): Stat => ({
cpu: 0,
memory: 0,
memoryUsage: 0,
networkRxTotal: 0,
networkTxTotal: 0,
diskReadTotal: 0,
diskWriteTotal: 0,
});
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
@@ -65,15 +75,7 @@ export class Container {
) {
this.mounts = mounts;
this.mountStats = mountStats;
const defaultStat = {
cpu: 0,
memory: 0,
memoryUsage: 0,
networkRxTotal: 0,
networkTxTotal: 0,
diskReadTotal: 0,
diskWriteTotal: 0,
} as Stat;
const defaultStat = emptyStat();
this._stat = ref(stats.at(-1) || defaultStat);
const recentStats = stats.slice(-300);
const padding = Array(300 - recentStats.length).fill(defaultStat);
+15 -15
View File
@@ -98,10 +98,10 @@ type Container struct {
FullyLoaded bool `protobuf:"varint,19,opt,name=fullyLoaded,proto3" json:"fullyLoaded,omitempty"`
Env []string `protobuf:"bytes,20,rep,name=env,proto3" json:"env,omitempty"`
Ports []string `protobuf:"bytes,21,rep,name=ports,proto3" json:"ports,omitempty"`
Mounts []*Mount `protobuf:"bytes,22,rep,name=mounts,proto3" json:"mounts,omitempty"`
RestartPolicy string `protobuf:"bytes,23,opt,name=restartPolicy,proto3" json:"restartPolicy,omitempty"`
NetworkMode string `protobuf:"bytes,24,opt,name=networkMode,proto3" json:"networkMode,omitempty"`
MountStats []*MountStat `protobuf:"bytes,25,rep,name=mountStats,proto3" json:"mountStats,omitempty"`
Mounts []*Mount `protobuf:"bytes,26,rep,name=mounts,proto3" json:"mounts,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -283,13 +283,6 @@ func (x *Container) GetPorts() []string {
return nil
}
func (x *Container) GetMounts() []*Mount {
if x != nil {
return x.Mounts
}
return nil
}
func (x *Container) GetRestartPolicy() string {
if x != nil {
return x.RestartPolicy
@@ -311,6 +304,13 @@ func (x *Container) GetMountStats() []*MountStat {
return nil
}
func (x *Container) GetMounts() []*Mount {
if x != nil {
return x.Mounts
}
return nil
}
type ContainerStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -1387,7 +1387,7 @@ var File_types_proto protoreflect.FileDescriptor
const file_types_proto_rawDesc = "" +
"\n" +
"\vtypes.proto\x12\bprotobuf\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf0\x06\n" +
"\vtypes.proto\x12\bprotobuf\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf6\x06\n" +
"\tContainer\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
@@ -1410,16 +1410,16 @@ const file_types_proto_rawDesc = "" +
"\bcpuLimit\x18\x12 \x01(\x01R\bcpuLimit\x12 \n" +
"\vfullyLoaded\x18\x13 \x01(\bR\vfullyLoaded\x12\x10\n" +
"\x03env\x18\x14 \x03(\tR\x03env\x12\x14\n" +
"\x05ports\x18\x15 \x03(\tR\x05ports\x12'\n" +
"\x06mounts\x18\x16 \x03(\v2\x0f.protobuf.MountR\x06mounts\x12$\n" +
"\x05ports\x18\x15 \x03(\tR\x05ports\x12$\n" +
"\rrestartPolicy\x18\x17 \x01(\tR\rrestartPolicy\x12 \n" +
"\vnetworkMode\x18\x18 \x01(\tR\vnetworkMode\x123\n" +
"\n" +
"mountStats\x18\x19 \x03(\v2\x13.protobuf.MountStatR\n" +
"mountStats\x1a9\n" +
"mountStats\x12'\n" +
"\x06mounts\x18\x1a \x03(\v2\x0f.protobuf.MountR\x06mounts\x1a9\n" +
"\vLabelsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa5\x02\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x16\x10\x17\"\xa5\x02\n" +
"\rContainerStat\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x1e\n" +
"\n" +
@@ -1572,8 +1572,8 @@ var file_types_proto_depIdxs = []int32{
16, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
2, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
20, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
3, // 5: protobuf.Container.mounts:type_name -> protobuf.Mount
4, // 6: protobuf.Container.mountStats:type_name -> protobuf.MountStat
4, // 5: protobuf.Container.mountStats:type_name -> protobuf.MountStat
3, // 6: protobuf.Container.mounts:type_name -> protobuf.Mount
20, // 7: protobuf.MountStat.lastChecked:type_name -> google.protobuf.Timestamp
21, // 8: protobuf.LogEvent.message:type_name -> google.protobuf.Any
20, // 9: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
+25 -8
View File
@@ -2,6 +2,7 @@ package container
import (
"context"
"sync/atomic"
"time"
"github.com/puzpuzpuz/xsync/v4"
@@ -20,9 +21,12 @@ const (
volumeQueueSize = 64
)
// volumeTracker is shared between the stat-producing path (observe) and the
// refresh worker. Both fields are accessed concurrently and must use atomics.
// lastCheckNanos stores time.Time as UnixNano; zero means "never checked".
type volumeTracker struct {
lastWriteTotal uint64
lastCheck time.Time
lastWriteTotal atomic.Uint64
lastCheckNanos atomic.Int64
}
type volumeMonitor struct {
@@ -57,17 +61,20 @@ func (v *volumeMonitor) observe(c *Container, stat ContainerStat) {
t, _ := v.trackers.LoadOrCompute(c.ID, func() (*volumeTracker, bool) {
// Initialize with the current write total so the first refresh is
// driven by the idle timer, not a phantom delta.
return &volumeTracker{lastWriteTotal: stat.DiskWriteTotal}, false
tr := &volumeTracker{}
tr.lastWriteTotal.Store(stat.DiskWriteTotal)
return tr, false
})
delta := stat.DiskWriteTotal - t.lastWriteTotal
if stat.DiskWriteTotal < t.lastWriteTotal {
last := t.lastWriteTotal.Load()
delta := stat.DiskWriteTotal - last
if stat.DiskWriteTotal < last {
// Counter reset (container restarted with same ID? unlikely but defend).
delta = stat.DiskWriteTotal
}
now := time.Now()
idle := t.lastCheck.IsZero() || now.Sub(t.lastCheck) >= idleRefreshInterval
lastNanos := t.lastCheckNanos.Load()
idle := lastNanos == 0 || time.Since(time.Unix(0, lastNanos)) >= idleRefreshInterval
if !idle && delta < writeThresholdBytes {
return
}
@@ -141,7 +148,17 @@ func (v *volumeMonitor) refresh(id string) {
if data := c.Stats.Data(); len(data) > 0 {
latestWrite = data[len(data)-1].DiskWriteTotal
}
v.trackers.Store(id, &volumeTracker{lastWriteTotal: latestWrite, lastCheck: time.Now()})
// Update in place under the per-key shard lock so concurrent observe()
// calls see consistent counters.
v.trackers.Compute(id, func(existing *volumeTracker, loaded bool) (*volumeTracker, xsync.ComputeOp) {
tr := existing
if !loaded || tr == nil {
tr = &volumeTracker{}
}
tr.lastWriteTotal.Store(latestWrite)
tr.lastCheckNanos.Store(time.Now().UnixNano())
return tr, xsync.UpdateOp
})
v.store.applyMountStats(id, stats)
}
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: Søg containere (⌘ + k, ⌃k)
pin-column: Fastgør som kolonne
merge-all: Sammenlæg alt i én stream
network-io: "Netværk: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Disk: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} kerner"
memory-usage: "Hukommelse {used} / {total}"
volumes: Volumener
volume-unreachable: ikke tilgængelig fra denne host
volume-full: "{destination} er {pct} fuld"
error:
page-not-found: Denne side eksisterer ikke
host-group-not-found: 'Ingen værter fundet i gruppen "{name}"'
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: Suche Container (⌘ + k, ⌃k)
pin-column: Als Spalte anheften
merge-all: Alles in einen Stream zusammenführen
network-io: "Netzwerk: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Festplatte: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} Kerne"
memory-usage: "Speicher {used} / {total}"
volumes: Volumes
volume-unreachable: von diesem Host nicht erreichbar
volume-full: "{destination} ist zu {pct} voll"
error:
page-not-found: Diese Seite existiert nicht.
host-group-not-found: 'Keine Hosts in der Gruppe "{name}" gefunden'
+7
View File
@@ -64,6 +64,13 @@ tooltip:
search: Search containers (⌘ + k, ⌃k)
pin-column: Pin as column
merge-all: Merge all into one stream
network-io: "Network: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Disk: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} cores"
memory-usage: "Memory {used} / {total}"
volumes: Volumes
volume-unreachable: not reachable from this host
volume-full: "{destination} is {pct} full"
error:
page-not-found: This page does not exist
host-group-not-found: 'No hosts found in group "{name}"'
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: Buscar contenedores (⌘ + K, CTRL + K)
pin-column: Anclar como columna
merge-all: Fusionar todo en un flujo
network-io: "Red: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Disco: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} núcleos"
memory-usage: "Memoria {used} / {total}"
volumes: Volúmenes
volume-unreachable: no accesible desde este host
volume-full: "{destination} está al {pct}"
error:
page-not-found: Esta página no existe.
host-group-not-found: 'No se encontraron hosts en el grupo "{name}"'
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: Recherche de conteneurs (⌘ + k, ⌃k)
pin-column: Epinglé en colonne
merge-all: Fusionner tout dans un flux
network-io: "Réseau : ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Disque : ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} cœurs"
memory-usage: "Mémoire {used} / {total}"
volumes: Volumes
volume-unreachable: inaccessible depuis cet hôte
volume-full: "{destination} est plein à {pct}"
error:
page-not-found: Cette page n'existe pas
host-group-not-found: 'Aucun hôte trouvé dans le groupe « {name} »'
+7
View File
@@ -64,6 +64,13 @@ tooltip:
search: Cari kontainer (⌘ + k, ⌃k)
pin-column: Sematkan sebagai kolom
merge-all: Gabungkan semua ke dalam satu aliran
network-io: "Jaringan: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Disk: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} core"
memory-usage: "Memori {used} / {total}"
volumes: Volume
volume-unreachable: tidak dapat dijangkau dari host ini
volume-full: "{destination} terisi {pct}"
error:
page-not-found: Halaman ini tidak ada
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: Ricerca container (⌘ + k, ⌃k)
pin-column: Blocca come colonna
merge-all: Unisci tutto in un flusso
network-io: "Rete: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Disco: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} core"
memory-usage: "Memoria {used} / {total}"
volumes: Volumi
volume-unreachable: non raggiungibile da questo host
volume-full: "{destination} è pieno al {pct}"
error:
page-not-found: Questa pagina non esiste
host-group-not-found: 'Nessun host trovato nel gruppo "{name}"'
+7
View File
@@ -61,6 +61,13 @@ tooltip:
search: 컨테이너 검색 (⌘ + k, ⌃k)
pin-column: 열로 고정
merge-all: 모두 하나의 스트림으로 병합
network-io: "네트워크: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "디스크: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} 코어"
memory-usage: "메모리 {used} / {total}"
volumes: 볼륨
volume-unreachable: 이 호스트에서 접근할 수 없음
volume-full: "{destination}이(가) {pct} 사용 중"
error:
page-not-found: 페이지를 찾을 수 없습니다
host-group-not-found: '"{name}" 그룹에 호스트가 없습니다'
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: Zoek containers (⌘ + k, ⌃k)
pin-column: Vastzetten als kolom
merge-all: Voeg alles samen in één stream
network-io: "Netwerk: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Schijf: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} kernen"
memory-usage: "Geheugen {used} / {total}"
volumes: Volumes
volume-unreachable: niet bereikbaar vanaf deze host
volume-full: "{destination} is {pct} vol"
error:
page-not-found: Deze pagina bestaat niet
host-group-not-found: 'Geen hosts gevonden in groep "{name}"'
+7
View File
@@ -66,6 +66,13 @@ tooltip:
search: Przeszukaj kontenery (⌘ + k, ⌃k)
pin-column: Przypnij jako kolumna
merge-all: Scal wszystko w jeden strumień
network-io: "Sieć: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Dysk: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} rdzeni"
memory-usage: "Pamięć {used} / {total}"
volumes: Wolumeny
volume-unreachable: nieosiągalne z tego hosta
volume-full: "{destination} zapełniony w {pct}"
error:
page-not-found: Ta strona nie istnieje
host-group-not-found: 'Nie znaleziono hostów w grupie „{name}”'
+7
View File
@@ -61,6 +61,13 @@ tooltip:
search: Pesquisar containers (⌘ + k, ⌃k)
pin-column: Fixar como coluna
merge-all: Unir tudo em um fluxo
network-io: "Rede: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Disco: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} núcleos"
memory-usage: "Memória {used} / {total}"
volumes: Volumes
volume-unreachable: inacessível a partir deste host
volume-full: "{destination} está {pct} cheio"
error:
page-not-found: Esta página não existe
host-group-not-found: 'Nenhum host encontrado no grupo "{name}"'
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: Поиск контейнеров (⌘ + k, ⌃k)
pin-column: Закрепить столбец
merge-all: Объединить все в один поток
network-io: "Сеть: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Диск: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "ЦП {cpu}% / {cores} ядер"
memory-usage: "Память {used} / {total}"
volumes: Тома
volume-unreachable: недоступно с этого хоста
volume-full: "{destination} заполнено на {pct}"
error:
page-not-found: Эта страница не доступна.
host-group-not-found: 'Хосты в группе «{name}» не найдены'
+7
View File
@@ -61,6 +61,13 @@ tooltip:
search: Iskanje zabojnikov (⌘ + k, ⌃k)
pin-column: Pripni kot stolpec
merge-all: Združi vse v en tok
network-io: "Omrežje: ↑ {tx}/s · ↓ {rx}/s"
disk-io: "Disk: ↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPE {cpu}% / {cores} jeder"
memory-usage: "Pomnilnik {used} / {total}"
volumes: Nosilci
volume-unreachable: nedosegljivo iz tega gostitelja
volume-full: "{destination} je {pct} poln"
error:
page-not-found: Ta stran ne obstaja
host-group-not-found: 'V skupini "{name}" ni najdenih gostiteljev'
+7
View File
@@ -67,6 +67,13 @@ tooltip:
search: Konteynerlerde ara (⌘ + k, ⌃k)
pin-column: Sütun olarak sabitle
merge-all: Tümünü tek bir akışta birleştir
network-io: "Ağ: ↑ {tx}/sn · ↓ {rx}/sn"
disk-io: "Disk: ↑ {write}/sn · ↓ {read}/sn"
cpu-usage: "CPU {cpu}% / {cores} çekirdek"
memory-usage: "Bellek {used} / {total}"
volumes: Birimler
volume-unreachable: bu sunucudan erişilemiyor
volume-full: "{destination} {pct} dolu"
error:
page-not-found: Bu sayfa bulunamadı
host-group-not-found: '"{name}" grubunda ana bilgisayar bulunamadı'
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: 搜尋容器 (⌘ + k, ⌃k)
pin-column: 釘選為欄位
merge-all: 將所有內容合併到一個流中
network-io: "網路:↑ {tx}/s · ↓ {rx}/s"
disk-io: "磁碟:↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} 核心"
memory-usage: "記憶體 {used} / {total}"
volumes:
volume-unreachable: 無法從此主機存取
volume-full: "{destination} 已使用 {pct}"
error:
page-not-found: 此頁面不存在
host-group-not-found: '在群組「{name}」中找不到主機'
+7
View File
@@ -62,6 +62,13 @@ tooltip:
search: 搜索 (⌘ + k, ⌃k)
pin-column: 固定为列
merge-all: 将所有内容合并到一个流中
network-io: "网络:↑ {tx}/s · ↓ {rx}/s"
disk-io: "磁盘:↑ {write}/s · ↓ {read}/s"
cpu-usage: "CPU {cpu}% / {cores} 核"
memory-usage: "内存 {used} / {total}"
volumes:
volume-unreachable: 无法从此主机访问
volume-full: "{destination} 已使用 {pct}"
error:
page-not-found: 此页面不存在。
host-group-not-found: '在组 "{name}" 中未找到主机'
+5 -1
View File
@@ -29,10 +29,14 @@ message Container {
bool fullyLoaded = 19;
repeated string env = 20;
repeated string ports = 21;
repeated Mount mounts = 22;
// Field 22 was previously `repeated string mounts` (raw paths). Renumbered
// to 26 with a richer Mount message; old tag reserved to avoid wire collisions
// with pre-v10.6 agents.
reserved 22;
string restartPolicy = 23;
string networkMode = 24;
repeated MountStat mountStats = 25;
repeated Mount mounts = 26;
}
message ContainerStat {