diff --git a/public/locales/en/common.json b/public/locales/en/common.json index e355c2efd..ba4ee5f55 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -99,7 +99,12 @@ "unhealthy": "Unhealthy", "not_found": "Not Found", "exited": "Exited", - "partial": "Partial" + "partial": "Partial", + "created": "Created", + "paused": "Paused", + "restarting": "Restarting", + "removing": "Removing", + "dead": "Dead" }, "ping": { "error": "Error", diff --git a/src/components/services/status.jsx b/src/components/services/status.jsx index 88ff9096f..594a0a190 100644 --- a/src/components/services/status.jsx +++ b/src/components/services/status.jsx @@ -1,6 +1,22 @@ import { useTranslation } from "next-i18next/pages"; import useSWR from "swr"; +const WARNING_CLASS = "text-orange-400/50 dark:text-orange-400/80"; + +// docker container states other than running +const STATE_LABELS = { + created: "docker.created", + dead: "docker.dead", + exited: "docker.exited", + "not found": "docker.not_found", + paused: "docker.paused", + removing: "docker.removing", + restarting: "docker.restarting", +}; + +// states that mean something is wrong rather than merely not started +const WARNING_STATES = new Set(["dead", "exited", "not found", "restarting"]); + export default function Status({ service, style }) { const { t } = useTranslation(); @@ -16,33 +32,30 @@ export default function Status({ service, style }) { if (statusError) { statusLabel = t("docker.error"); colorClass = "text-rose-500/80"; - } else if (data) { - if (data.status?.includes("running")) { - colorClass = "text-emerald-500/80"; + } else if (data?.status?.includes("running")) { + colorClass = "text-emerald-500/80"; - if (!data.health) { - statusLabel = data.status.replace("running", t("docker.running")); - } else { - statusLabel = data.health === "healthy" ? t("docker.healthy") : data.health; + if (!data.health) { + statusLabel = data.status.replace("running", t("docker.running")); + } else { + statusLabel = data.health === "healthy" ? t("docker.healthy") : data.health; - if (data.health === "starting") { - statusLabel = t("docker.starting"); - colorClass = "text-blue-500/80"; - } + if (data.health === "starting") { + statusLabel = t("docker.starting"); + colorClass = "text-blue-500/80"; + } - if (data.health === "unhealthy") { - statusLabel = t("docker.unhealthy"); - colorClass = "text-orange-400/50 dark:text-orange-400/80"; - } + if (data.health === "unhealthy") { + statusLabel = t("docker.unhealthy"); + colorClass = WARNING_CLASS; } } - - if (data.status === "not found" || data.status === "exited" || data.status?.startsWith("partial")) { - if (data.status === "not found") statusLabel = t("docker.not_found"); - else if (data.status === "exited") statusLabel = t("docker.exited"); - else statusLabel = data.status.replace("partial", t("docker.partial")); - colorClass = "text-orange-400/50 dark:text-orange-400/80"; - } + } else if (data?.status?.startsWith("partial")) { + statusLabel = data.status.replace("partial", t("docker.partial")); + colorClass = WARNING_CLASS; + } else if (data && STATE_LABELS[data.status]) { + statusLabel = t(STATE_LABELS[data.status]); + if (WARNING_STATES.has(data.status)) colorClass = WARNING_CLASS; } if (style === "dot") { diff --git a/src/components/services/status.test.jsx b/src/components/services/status.test.jsx index 54364f78c..75ff2b654 100644 --- a/src/components/services/status.test.jsx +++ b/src/components/services/status.test.jsx @@ -75,6 +75,46 @@ describe("components/services/status", () => { expect(screen.getByText("docker.starting")).toBeInTheDocument(); }); + it("renders the remaining container states instead of falling through to unknown", () => { + const states = { + created: "docker.created", + paused: "docker.paused", + restarting: "docker.restarting", + removing: "docker.removing", + dead: "docker.dead", + }; + + Object.entries(states).forEach(([status, label]) => { + useSWR.mockReturnValue({ data: { statuses: { c: { status } } }, error: undefined }); + render(); + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + expect(screen.queryByText("docker.unknown")).not.toBeInTheDocument(); + }); + + it("colors problem states as warnings and merely stopped states neutrally", () => { + useSWR.mockReturnValue({ data: { statuses: { c: { status: "restarting" } } }, error: undefined }); + const { container: restarting } = render(); + expect(restarting.querySelector(".bg-orange-400")).toBeInTheDocument(); + + useSWR.mockReturnValue({ data: { statuses: { c: { status: "paused" } } }, error: undefined }); + const { container: paused } = render(); + expect(paused.querySelector(".bg-orange-400")).not.toBeInTheDocument(); + }); + + it("does not surface health for containers that are not running", () => { + useSWR.mockReturnValue({ + data: { statuses: { c: { status: "paused", health: "unhealthy" } } }, + error: undefined, + }); + + render(); + + expect(screen.getByText("docker.paused")).toBeInTheDocument(); + expect(screen.queryByText("docker.unhealthy")).not.toBeInTheDocument(); + }); + it("renders a dot when style is dot", () => { useSWR.mockReturnValue({ data: { statuses: { c: { status: "running" } } }, error: undefined });