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..4b2480d8c 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,10 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers" totalCount: table.getRowCount(), })} - row.original.id)} /> + container.id)} + /> ); }, @@ -152,15 +165,47 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers" interface ContainerActionBarProps { selectedIds: string[]; + selectedContainers: RouterOutputs["docker"]["getContainers"]["containers"]; } -const ContainerActionBar = ({ selectedIds }: ContainerActionBarProps) => { +const ContainerActionBar = ({ selectedContainers, selectedIds }: ContainerActionBarProps) => { return ( - - - - + + + + + ); }; @@ -168,14 +213,30 @@ const ContainerActionBar = ({ selectedIds }: ContainerActionBarProps) => { interface ContainerActionBarButtonProps { icon: TablerIcon; color: MantineColor; - action: "start" | "stop" | "restart" | "remove"; + action: "start" | "stop" | "restart" | "remove" | "addToHomarr"; selectedIds: string[]; + selectedContainers: RouterOutputs["docker"]["getContainers"]["containers"]; } const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => { const t = useScopedI18n("docker.action"); - const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation(); const utils = clientApi.useUtils(); + const { openModal } = useModalAction(AddDockerAppToHomarr); + + if (props.action === "addToHomarr") { + const handleClick = () => { + openModal({ + selectedContainers: props.selectedContainers, + }); + }; + return ( + + ); + } + + const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation(); const handleClickAsync = async () => { await mutateAsync( diff --git a/packages/api/src/router/app.ts b/packages/api/src/router/app.ts index da4184e89..61b0e0ec6 100644 --- a/packages/api/src/router/app.ts +++ b/packages/api/src/router/app.ts @@ -8,6 +8,7 @@ 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"; +import { getIconForName } from "@homarr/icons"; export const appRouter = createTRPCRouter({ all: protectedProcedure @@ -98,6 +99,24 @@ 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 ?? + "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/svg/homarr.svg", + href: app.href, + })), + ); + }), update: permissionRequiredProcedure .requiresPermission("app-modify-all") .input(convertIntersectionToZodObject(validation.app.edit)) diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 1ef500ee0..b733a9b67 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -9,6 +9,7 @@ export type HomarrDocumentationPath = | "/blog/2023/12/22/updated-documentation" | "/blog/2024/09/23/version-1.0" | "/blog/2024/12/17/open-beta-1.0" + | "/blog/2024/12/31/migrate-secret-enryption-key" | "/blog/archive" | "/blog/authors" | "/blog/authors/ajnart" @@ -100,11 +101,13 @@ export type HomarrDocumentationPath = | "/docs/tags/open-media-vault" | "/docs/tags/overseerr" | "/docs/tags/permissions" + | "/docs/tags/pgid" | "/docs/tags/pi-hole" | "/docs/tags/ping" | "/docs/tags/programming" | "/docs/tags/proxmox" | "/docs/tags/proxy" + | "/docs/tags/puid" | "/docs/tags/roles" | "/docs/tags/rss" | "/docs/tags/search" @@ -135,6 +138,7 @@ export type HomarrDocumentationPath = | "/docs/advanced/icons" | "/docs/advanced/keyboard-shortcuts" | "/docs/advanced/proxy" + | "/docs/advanced/running-as-different-user" | "/docs/advanced/single-sign-on" | "/docs/category/advanced" | "/docs/category/community" 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..cf517f035 --- /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 { useForm, zodResolver } 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 AddDockerAppToHomarr = createModal(({ actions, innerProps }) => { + const t = useI18n(); + const form = useForm({ + validate: zodResolver( + 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..52c9a3a3c --- /dev/null +++ b/packages/modals-collection/src/docker/index.ts @@ -0,0 +1 @@ +export { 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 9a87ad643..377838eae 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2544,6 +2544,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, };