feat: add error page for docker tools page (#1655)
This commit is contained in:
27
apps/nextjs/src/app/[locale]/manage/tools/docker/error.tsx
Normal file
27
apps/nextjs/src/app/[locale]/manage/tools/docker/error.tsx
Normal file
@@ -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 (
|
||||||
|
<Center>
|
||||||
|
<Stack align="center">
|
||||||
|
<IconShipOff size={48} stroke={1.5} />
|
||||||
|
<Stack align="center" gap="xs">
|
||||||
|
<Text size="lg" fw={500}>
|
||||||
|
{t("docker.error.internalServerError")}
|
||||||
|
</Text>
|
||||||
|
<Anchor size="sm" component={Link} href="/manage/tools/logs">
|
||||||
|
{t("common.action.checkLogs")}
|
||||||
|
</Anchor>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import type { Container } from "dockerode";
|
|||||||
import { db, like, or } from "@homarr/db";
|
import { db, like, or } from "@homarr/db";
|
||||||
import { icons } from "@homarr/db/schema/sqlite";
|
import { icons } from "@homarr/db/schema/sqlite";
|
||||||
import type { DockerContainerState } from "@homarr/definitions";
|
import type { DockerContainerState } from "@homarr/definitions";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
import { createCacheChannel } from "@homarr/redis";
|
import { createCacheChannel } from "@homarr/redis";
|
||||||
import { z } from "@homarr/validation";
|
import { z } from "@homarr/validation";
|
||||||
|
|
||||||
@@ -17,42 +18,60 @@ const dockerCache = createCacheChannel<{
|
|||||||
|
|
||||||
export const dockerRouter = createTRPCRouter({
|
export const dockerRouter = createTRPCRouter({
|
||||||
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
||||||
const { timestamp, data } = await dockerCache.consumeAsync(async () => {
|
const result = await dockerCache
|
||||||
const dockerInstances = DockerSingleton.getInstance();
|
.consumeAsync(async () => {
|
||||||
const containers = await Promise.all(
|
const dockerInstances = DockerSingleton.getInstance();
|
||||||
// Return all the containers of all the instances into only one item
|
const containers = await Promise.all(
|
||||||
dockerInstances.map(({ instance, host: key }) =>
|
// Return all the containers of all the instances into only one item
|
||||||
instance.listContainers({ all: true }).then((containers) =>
|
dockerInstances.map(({ instance, host: key }) =>
|
||||||
containers.map((container) => ({
|
instance.listContainers({ all: true }).then((containers) =>
|
||||||
...container,
|
containers.map((container) => ({
|
||||||
instance: key,
|
...container,
|
||||||
})),
|
instance: key,
|
||||||
|
})),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
).then((containers) => containers.flat());
|
||||||
).then((containers) => containers.flat());
|
|
||||||
|
|
||||||
const extractImage = (container: Docker.ContainerInfo) =>
|
const extractImage = (container: Docker.ContainerInfo) =>
|
||||||
container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
||||||
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
||||||
const dbIcons =
|
const dbIcons =
|
||||||
likeQueries.length >= 1
|
likeQueries.length >= 1
|
||||||
? await db.query.icons.findMany({
|
? await db.query.icons.findMany({
|
||||||
where: or(...likeQueries),
|
where: or(...likeQueries),
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
containers: containers.map((container) => ({
|
containers: containers.map((container) => ({
|
||||||
...container,
|
...container,
|
||||||
iconUrl:
|
iconUrl:
|
||||||
dbIcons.find((icon) => {
|
dbIcons.find((icon) => {
|
||||||
const extractedImage = extractImage(container);
|
const extractedImage = extractImage(container);
|
||||||
if (!extractedImage) return false;
|
if (!extractedImage) return false;
|
||||||
return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
|
return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
|
||||||
})?.url ?? null,
|
})?.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 {
|
return {
|
||||||
containers: sanitizeContainers(data.containers),
|
containers: sanitizeContainers(data.containers),
|
||||||
|
|||||||
@@ -678,6 +678,7 @@
|
|||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"checkoutDocs": "Check out the documentation",
|
"checkoutDocs": "Check out the documentation",
|
||||||
|
"checkLogs": "Check logs for more details",
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"loading": "Loading"
|
"loading": "Loading"
|
||||||
},
|
},
|
||||||
@@ -1309,9 +1310,6 @@
|
|||||||
"description": "Click <here></here> to create a new app"
|
"description": "Click <here></here> to create a new app"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"action": {
|
|
||||||
"logs": "Check logs for more details"
|
|
||||||
},
|
|
||||||
"noIntegration": "No integration selected",
|
"noIntegration": "No integration selected",
|
||||||
"noData": "No integration data available"
|
"noData": "No integration data available"
|
||||||
},
|
},
|
||||||
@@ -2305,6 +2303,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"internalServerError": "Failed to fetch Docker containers"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const BaseWidgetError = (props: BaseWidgetErrorProps) => {
|
|||||||
<Text ta="center">{translateIfNecessary(t, props.message)}</Text>
|
<Text ta="center">{translateIfNecessary(t, props.message)}</Text>
|
||||||
{props.showLogsLink && (
|
{props.showLogsLink && (
|
||||||
<Anchor component={Link} href="/manage/tools/logs" target="_blank" ta="center" size="sm">
|
<Anchor component={Link} href="/manage/tools/logs" target="_blank" ta="center" size="sm">
|
||||||
{t("widget.common.error.action.logs")}
|
{t("common.action.checkLogs")}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Reference in New Issue
Block a user