Skip to content

Commit

Permalink
feat: docker add to homarr
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-rw committed Jan 4, 2025
1 parent f507645 commit 90cae7e
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 10 deletions.
66 changes: 58 additions & 8 deletions apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@

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";

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";
Expand Down Expand Up @@ -125,6 +134,7 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
);
},
renderToolbarAlertBannerContent: ({ groupedAlert, table }) => {
const dockerContainers = table.getSelectedRowModel().rows.map((row) => row.original);
return (
<Group gap={"sm"}>
{groupedAlert}
Expand All @@ -134,7 +144,10 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
totalCount: table.getRowCount(),
})}
</Text>
<ContainerActionBar selectedIds={table.getSelectedRowModel().rows.map((row) => row.original.id)} />
<ContainerActionBar
selectedContainers={dockerContainers}
selectedIds={dockerContainers.map((container) => container.id)}
/>
</Group>
);
},
Expand All @@ -152,15 +165,50 @@ 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) => {
const t = useScopedI18n("docker.action");
const { openModal } = useModalAction(AddDockerAppToHomarr);
const handleClick = () => {
openModal({
selectedContainers,
});
};
return (
<Group gap="xs">
<ContainerActionBarButton icon={IconPlayerPlay} color="green" action="start" selectedIds={selectedIds} />
<ContainerActionBarButton icon={IconPlayerStop} color="red" action="stop" selectedIds={selectedIds} />
<ContainerActionBarButton icon={IconRotateClockwise} color="orange" action="restart" selectedIds={selectedIds} />
<ContainerActionBarButton icon={IconTrash} color="red" action="remove" selectedIds={selectedIds} />
<ContainerActionBarButton
icon={IconPlayerPlay}
color="green"
action="start"
selectedIds={selectedIds}
selectedContainers={selectedContainers}
/>
<ContainerActionBarButton
icon={IconPlayerStop}
color="red"
action="stop"
selectedIds={selectedIds}
selectedContainers={selectedContainers}
/>
<ContainerActionBarButton
icon={IconRotateClockwise}
color="orange"
action="restart"
selectedIds={selectedIds}
selectedContainers={selectedContainers}
/>
<ContainerActionBarButton
icon={IconTrash}
color="red"
action="remove"
selectedIds={selectedIds}
selectedContainers={selectedContainers}
/>
<Button leftSection={<IconCategoryPlus />} color={"red"} onClick={handleClick} variant="light" radius="md">
{t("addToHomarr.label")}
</Button>
</Group>
);
};
Expand All @@ -170,13 +218,15 @@ interface ContainerActionBarButtonProps {
color: MantineColor;
action: "start" | "stop" | "restart" | "remove";
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 { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();

const handleClickAsync = async () => {
await mutateAsync(
{ ids: props.selectedIds },
Expand Down
19 changes: 19 additions & 0 deletions packages/api/src/router/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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";
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions packages/definitions/src/docs/homarr-docs-sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions packages/icons/src/auto-icon-searcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}%`),
});
};
91 changes: 91 additions & 0 deletions packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx
Original file line number Diff line number Diff line change
@@ -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<AddDockerAppToHomarrProps>(({ 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 (
<form onSubmit={form.onSubmit(handleSubmit)}>
<LoadingOverlay visible={isPending} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
<Stack>
<List>
{innerProps.selectedContainers.map((container, index) => (
<List.Item
styles={{ itemWrapper: { width: "100%" }, itemLabel: { flex: 1 } }}
icon={<Image src={container.iconUrl} alt="container image" w={30} h={30} />}
key={container.id}
>
<Group justify="space-between">
<Text>{container.name}</Text>
<TextInput {...form.getInputProps(`containerUrls.${index}`)} />
</Group>
</List.Item>
))}
</List>
<Group justify="end">
<Button onClick={actions.closeModal} variant="light">
{t("common.action.cancel")}
</Button>
<Button disabled={!form.isValid()} type="submit">
{t("common.action.add")}
</Button>
</Group>
</Stack>
</form>
);
}).withOptions({
defaultTitle(t) {
return t("docker.action.addToHomarr.modal.title");
},
});
1 change: 1 addition & 0 deletions packages/modals-collection/src/docker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AddDockerAppToHomarr } from "./add-docker-app-to-homarr";
1 change: 1 addition & 0 deletions packages/modals-collection/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./boards";
export * from "./invites";
export * from "./groups";
export * from "./search-engines";
export * from "./docker";
16 changes: 16 additions & 0 deletions packages/translation/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions packages/validation/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

0 comments on commit 90cae7e

Please sign in to comment.