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 1180980
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 11 deletions.
79 changes: 70 additions & 9 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,30 +165,78 @@ 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 (
<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}
/>
<ContainerActionBarButton
icon={IconCategoryPlus}
color="red"
action="addToHomarr"
selectedIds={selectedIds}
selectedContainers={selectedContainers}
/>
</Group>
);
};

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 (
<Button leftSection={<props.icon />} color={props.color} onClick={handleClick} variant="light" radius="md">
{t(`${props.action}.label`)}
</Button>
);
}

const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();

const handleClickAsync = async () => {
await mutateAsync(
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 @@ -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
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 1180980

Please sign in to comment.