diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx index e5ca818de..5398aaff3 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx @@ -2,7 +2,14 @@ import type { MantineColor } from "@mantine/core"; import { Avatar, Badge, Box, Button, Group, Text } from "@mantine/core"; -import { IconPlayerPlay, IconPlayerStop, IconRefresh, IconRotateClockwise, IconTrash } from "@tabler/icons-react"; +import { + IconCategoryPlus, + IconPlayerPlay, + IconPlayerStop, + IconRefresh, + IconRotateClockwise, + IconTrash, +} from "@tabler/icons-react"; import type { MRT_ColumnDef } from "mantine-react-table"; import { MantineReactTable } from "mantine-react-table"; @@ -10,6 +17,8 @@ import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { useTimeAgo } from "@homarr/common"; import type { DockerContainerState } from "@homarr/definitions"; +import { useModalAction } from "@homarr/modals"; +import { AddDockerAppToHomarr } from "@homarr/modals-collection"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import type { TranslationFunction } from "@homarr/translation"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; @@ -125,6 +134,7 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers" ); }, renderToolbarAlertBannerContent: ({ groupedAlert, table }) => { + const dockerContainers = table.getSelectedRowModel().rows.map((row) => row.original); return ( {groupedAlert} @@ -134,7 +144,7 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers" totalCount: table.getRowCount(), })} - row.original.id)} /> + ); }, @@ -151,16 +161,29 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers" } interface ContainerActionBarProps { - selectedIds: string[]; + selectedContainers: RouterOutputs["docker"]["getContainers"]["containers"]; } -const ContainerActionBar = ({ selectedIds }: ContainerActionBarProps) => { +const ContainerActionBar = ({ selectedContainers }: ContainerActionBarProps) => { + const t = useScopedI18n("docker.action"); + const { openModal } = useModalAction(AddDockerAppToHomarr); + const handleClick = () => { + openModal({ + selectedContainers, + }); + }; + + const selectedIds = selectedContainers.map((container) => container.id); + return ( + ); }; @@ -174,9 +197,10 @@ interface ContainerActionBarButtonProps { const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => { const t = useScopedI18n("docker.action"); - const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation(); const utils = clientApi.useUtils(); + const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation(); + const handleClickAsync = async () => { await mutateAsync( { ids: props.selectedIds }, diff --git a/packages/api/src/router/app.ts b/packages/api/src/router/app.ts index 5eb65804b..2d1a70ed1 100644 --- a/packages/api/src/router/app.ts +++ b/packages/api/src/router/app.ts @@ -3,12 +3,15 @@ import { TRPCError } from "@trpc/server"; import { asc, createId, eq, inArray, like } from "@homarr/db"; import { apps } from "@homarr/db/schema"; import { selectAppSchema } from "@homarr/db/validationSchemas"; +import { getIconForName } from "@homarr/icons"; import { validation, z } from "@homarr/validation"; import { convertIntersectionToZodObject } from "../schema-merger"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { canUserSeeAppAsync } from "./app/app-access-control"; +const defaultIcon = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/svg/homarr.svg"; + export const appRouter = createTRPCRouter({ getPaginated: protectedProcedure .input(validation.common.paginated) @@ -118,6 +121,21 @@ export const appRouter = createTRPCRouter({ href: input.href, }); }), + createMany: permissionRequiredProcedure + .requiresPermission("app-create") + .input(validation.app.createMany) + .output(z.void()) + .mutation(async ({ ctx, input }) => { + await ctx.db.insert(apps).values( + input.map((app) => ({ + id: createId(), + name: app.name, + description: app.description, + iconUrl: app.iconUrl ?? getIconForName(ctx.db, app.name).sync()?.url ?? defaultIcon, + href: app.href, + })), + ); + }), update: permissionRequiredProcedure .requiresPermission("app-modify-all") .input(convertIntersectionToZodObject(validation.app.edit)) diff --git a/packages/icons/src/auto-icon-searcher.ts b/packages/icons/src/auto-icon-searcher.ts index 0eef30e5e..06f3d9b76 100644 --- a/packages/icons/src/auto-icon-searcher.ts +++ b/packages/icons/src/auto-icon-searcher.ts @@ -2,8 +2,8 @@ import type { Database } from "@homarr/db"; import { like } from "@homarr/db"; import { icons } from "@homarr/db/schema"; -export const getIconForNameAsync = async (db: Database, name: string) => { - return await db.query.icons.findFirst({ +export const getIconForName = (db: Database, name: string) => { + return db.query.icons.findFirst({ where: like(icons.name, `%${name}%`), }); }; diff --git a/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx b/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx new file mode 100644 index 000000000..9a5f2d5fc --- /dev/null +++ b/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx @@ -0,0 +1,91 @@ +import { Button, Group, Image, List, LoadingOverlay, Stack, Text, TextInput } from "@mantine/core"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { useZodForm } from "@homarr/form"; +import { createModal } from "@homarr/modals"; +import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; +import { useI18n } from "@homarr/translation/client"; +import { z } from "@homarr/validation"; + +interface AddDockerAppToHomarrProps { + selectedContainers: RouterOutputs["docker"]["getContainers"]["containers"]; +} + +export const AddDockerAppToHomarrModal = createModal(({ actions, innerProps }) => { + const t = useI18n(); + const form = useZodForm( + z.object({ + containerUrls: z.array(z.string().url().nullable()), + }), + { + initialValues: { + containerUrls: innerProps.selectedContainers.map((container) => { + if (container.ports[0]) { + return `http://${container.ports[0].IP}:${container.ports[0].PublicPort}`; + } + + return null; + }), + }, + }, + ); + const { mutate, isPending } = clientApi.app.createMany.useMutation({ + onSuccess() { + actions.closeModal(); + showSuccessNotification({ + title: t("docker.action.addToHomarr.notification.success.title"), + message: t("docker.action.addToHomarr.notification.success.message"), + }); + }, + onError() { + showErrorNotification({ + title: t("docker.action.addToHomarr.notification.error.title"), + message: t("docker.action.addToHomarr.notification.error.message"), + }); + }, + }); + const handleSubmit = () => { + mutate( + innerProps.selectedContainers.map((container, index) => ({ + name: container.name, + iconUrl: container.iconUrl, + description: null, + href: form.values.containerUrls[index] ?? null, + })), + ); + }; + return ( +
+ + + + {innerProps.selectedContainers.map((container, index) => ( + } + key={container.id} + > + + {container.name} + + + + ))} + + + + + + + + ); +}).withOptions({ + defaultTitle(t) { + return t("docker.action.addToHomarr.modal.title"); + }, +}); diff --git a/packages/modals-collection/src/docker/index.ts b/packages/modals-collection/src/docker/index.ts new file mode 100644 index 000000000..31b956245 --- /dev/null +++ b/packages/modals-collection/src/docker/index.ts @@ -0,0 +1 @@ +export { AddDockerAppToHomarrModal as AddDockerAppToHomarr } from "./add-docker-app-to-homarr"; diff --git a/packages/modals-collection/src/index.ts b/packages/modals-collection/src/index.ts index 66672e577..30d81d892 100644 --- a/packages/modals-collection/src/index.ts +++ b/packages/modals-collection/src/index.ts @@ -2,3 +2,4 @@ export * from "./boards"; export * from "./invites"; export * from "./groups"; export * from "./search-engines"; +export * from "./docker"; diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 11cf49ac6..4acd411c9 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2562,6 +2562,22 @@ "message": "Something went wrong while refreshing the containers" } } + }, + "addToHomarr": { + "label": "Add to Homarr", + "notification": { + "success": { + "title": "Added to Homarr", + "message": "Selected apps have been added to Homarr" + }, + "error": { + "title": "Could not add to Homarr", + "message": "Selected apps could not be added to Homarr" + } + }, + "modal": { + "title": "Add docker container(-s) to Homarr" + } } }, "error": { diff --git a/packages/validation/src/app.ts b/packages/validation/src/app.ts index 6ac45ac35..507c2f8bf 100644 --- a/packages/validation/src/app.ts +++ b/packages/validation/src/app.ts @@ -11,5 +11,8 @@ const editAppSchema = manageAppSchema.and(z.object({ id: z.string() })); export const appSchemas = { manage: manageAppSchema, + createMany: z + .array(manageAppSchema.omit({ iconUrl: true }).and(z.object({ iconUrl: z.string().min(1).nullable() }))) + .min(1), edit: editAppSchema, };