diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/error.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/error.tsx new file mode 100644 index 000000000..4fe1ae7b0 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/error.tsx @@ -0,0 +1,27 @@ +"use client"; + +import Link from "next/link"; +import { Anchor, Center, Stack, Text } from "@mantine/core"; +import { IconShipOff } from "@tabler/icons-react"; + +import { useI18n } from "@homarr/translation/client"; + +export default function DockerErrorPage() { + const t = useI18n(); + + return ( +
+ + + + + {t("docker.error.internalServerError")} + + + {t("common.action.checkLogs")} + + + +
+ ); +} diff --git a/packages/api/src/router/docker/docker-router.ts b/packages/api/src/router/docker/docker-router.ts index 127573173..0559e6211 100644 --- a/packages/api/src/router/docker/docker-router.ts +++ b/packages/api/src/router/docker/docker-router.ts @@ -5,6 +5,7 @@ import type { Container } from "dockerode"; import { db, like, or } from "@homarr/db"; import { icons } from "@homarr/db/schema/sqlite"; import type { DockerContainerState } from "@homarr/definitions"; +import { logger } from "@homarr/log"; import { createCacheChannel } from "@homarr/redis"; import { z } from "@homarr/validation"; @@ -17,42 +18,60 @@ const dockerCache = createCacheChannel<{ export const dockerRouter = createTRPCRouter({ getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => { - const { timestamp, data } = await dockerCache.consumeAsync(async () => { - const dockerInstances = DockerSingleton.getInstance(); - const containers = await Promise.all( - // Return all the containers of all the instances into only one item - dockerInstances.map(({ instance, host: key }) => - instance.listContainers({ all: true }).then((containers) => - containers.map((container) => ({ - ...container, - instance: key, - })), + const result = await dockerCache + .consumeAsync(async () => { + const dockerInstances = DockerSingleton.getInstance(); + const containers = await Promise.all( + // Return all the containers of all the instances into only one item + dockerInstances.map(({ instance, host: key }) => + instance.listContainers({ all: true }).then((containers) => + containers.map((container) => ({ + ...container, + instance: key, + })), + ), ), - ), - ).then((containers) => containers.flat()); - - const extractImage = (container: Docker.ContainerInfo) => - container.Image.split("/").at(-1)?.split(":").at(0) ?? ""; - const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`)); - const dbIcons = - likeQueries.length >= 1 - ? await db.query.icons.findMany({ - where: or(...likeQueries), - }) - : []; - - return { - containers: containers.map((container) => ({ - ...container, - iconUrl: - dbIcons.find((icon) => { - const extractedImage = extractImage(container); - if (!extractedImage) return false; - return icon.name.toLowerCase().includes(extractedImage.toLowerCase()); - })?.url ?? null, - })), - }; - }); + ).then((containers) => containers.flat()); + + const extractImage = (container: Docker.ContainerInfo) => + container.Image.split("/").at(-1)?.split(":").at(0) ?? ""; + const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`)); + const dbIcons = + likeQueries.length >= 1 + ? await db.query.icons.findMany({ + where: or(...likeQueries), + }) + : []; + + return { + containers: containers.map((container) => ({ + ...container, + iconUrl: + dbIcons.find((icon) => { + const extractedImage = extractImage(container); + if (!extractedImage) return false; + return icon.name.toLowerCase().includes(extractedImage.toLowerCase()); + })?.url ?? null, + })), + }; + }) + .catch((error) => { + logger.error(error); + return { + isError: true, + error: error as unknown, + }; + }); + + if ("isError" in result) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while fetching the containers", + cause: result.error, + }); + } + + const { data, timestamp } = result; return { containers: sanitizeContainers(data.containers), diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 3ad0c7e85..f47b15da7 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -678,6 +678,7 @@ "previous": "Previous", "next": "Next", "checkoutDocs": "Check out the documentation", + "checkLogs": "Check logs for more details", "tryAgain": "Try again", "loading": "Loading" }, @@ -1308,9 +1309,6 @@ "description": "Click to create a new app" }, "error": { - "action": { - "logs": "Check logs for more details" - }, "noIntegration": "No integration selected", "noData": "No integration data available" }, @@ -2301,6 +2299,9 @@ } } } + }, + "error": { + "internalServerError": "Failed to fetch Docker containers" } }, "permission": { diff --git a/packages/widgets/src/errors/base-component.tsx b/packages/widgets/src/errors/base-component.tsx index 61f50389a..8c4e12b40 100644 --- a/packages/widgets/src/errors/base-component.tsx +++ b/packages/widgets/src/errors/base-component.tsx @@ -23,7 +23,7 @@ export const BaseWidgetError = (props: BaseWidgetErrorProps) => { {translateIfNecessary(t, props.message)} {props.showLogsLink && ( - {t("widget.common.error.action.logs")} + {t("common.action.checkLogs")} )}