mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
🏗️ Migrate docker container actions to tRPC
This commit is contained in:
@@ -10,56 +10,16 @@ import {
|
|||||||
IconRotateClockwise,
|
IconRotateClockwise,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import axios from 'axios';
|
|
||||||
import Dockerode from 'dockerode';
|
import Dockerode from 'dockerode';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { RouterInputs, api } from '~/utils/api';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
||||||
import { MatchingImages, ServiceType, tryMatchPort } from '../../tools/types';
|
import { MatchingImages, ServiceType, tryMatchPort } from '../../tools/types';
|
||||||
import { AppType } from '../../types/app';
|
import { AppType } from '../../types/app';
|
||||||
|
|
||||||
function sendDockerCommand(
|
|
||||||
action: string,
|
|
||||||
containerId: string,
|
|
||||||
containerName: string,
|
|
||||||
reload: () => void,
|
|
||||||
t: (key: string) => string,
|
|
||||||
) {
|
|
||||||
notifications.show({
|
|
||||||
id: containerId,
|
|
||||||
loading: true,
|
|
||||||
title: `${t(`actions.${action}.start`)} ${containerName}`,
|
|
||||||
message: undefined,
|
|
||||||
autoClose: false,
|
|
||||||
withCloseButton: false,
|
|
||||||
});
|
|
||||||
axios
|
|
||||||
.get(`/api/docker/container/${containerId}?action=${action}`)
|
|
||||||
.then((res) => {
|
|
||||||
notifications.show({
|
|
||||||
id: containerId,
|
|
||||||
title: containerName,
|
|
||||||
message: `${t(`actions.${action}.end`)} ${containerName}`,
|
|
||||||
icon: <IconCheck />,
|
|
||||||
autoClose: 2000,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
notifications.update({
|
|
||||||
id: containerId,
|
|
||||||
color: 'red',
|
|
||||||
title: t('errors.unknownError.title'),
|
|
||||||
message: err.response.data.reason,
|
|
||||||
autoClose: 2000,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
reload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContainerActionBarProps {
|
export interface ContainerActionBarProps {
|
||||||
selected: Dockerode.ContainerInfo[];
|
selected: Dockerode.ContainerInfo[];
|
||||||
reload: () => void;
|
reload: () => void;
|
||||||
@@ -68,8 +28,9 @@ export interface ContainerActionBarProps {
|
|||||||
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
|
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
|
||||||
const { t } = useTranslation('modules/docker');
|
const { t } = useTranslation('modules/docker');
|
||||||
const [isLoading, setisLoading] = useState(false);
|
const [isLoading, setisLoading] = useState(false);
|
||||||
const { name: configName, config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0];
|
const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0];
|
||||||
|
const sendDockerCommand = useDockerActionMutation();
|
||||||
|
|
||||||
if (process.env.DISABLE_EDIT_MODE === 'true') {
|
if (process.env.DISABLE_EDIT_MODE === 'true') {
|
||||||
return null;
|
return null;
|
||||||
@@ -96,11 +57,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
<Button
|
<Button
|
||||||
leftIcon={<IconRotateClockwise />}
|
leftIcon={<IconRotateClockwise />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
Promise.all(
|
Promise.all(selected.map((container) => sendDockerCommand(container, 'restart')))
|
||||||
selected.map((container) =>
|
|
||||||
sendDockerCommand('restart', container.Id, container.Names[0].substring(1), reload, t)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
variant="light"
|
variant="light"
|
||||||
color="orange"
|
color="orange"
|
||||||
@@ -112,11 +69,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
<Button
|
<Button
|
||||||
leftIcon={<IconPlayerStop />}
|
leftIcon={<IconPlayerStop />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
Promise.all(
|
Promise.all(selected.map((container) => sendDockerCommand(container, 'stop')))
|
||||||
selected.map((container) =>
|
|
||||||
sendDockerCommand('stop', container.Id, container.Names[0].substring(1), reload, t)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -128,11 +81,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
<Button
|
<Button
|
||||||
leftIcon={<IconPlayerPlay />}
|
leftIcon={<IconPlayerPlay />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
Promise.all(
|
Promise.all(selected.map((container) => sendDockerCommand(container, 'start')))
|
||||||
selected.map((container) =>
|
|
||||||
sendDockerCommand('start', container.Id, container.Names[0].substring(1), reload, t)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
@@ -147,11 +96,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
Promise.all(
|
Promise.all(selected.map((container) => sendDockerCommand(container, 'remove')))
|
||||||
selected.map((container) =>
|
|
||||||
sendDockerCommand('remove', container.Id, container.Names[0].substring(1), reload, t)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
disabled={selected.length === 0}
|
disabled={selected.length === 0}
|
||||||
>
|
>
|
||||||
@@ -210,6 +155,55 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useDockerActionMutation = () => {
|
||||||
|
const { t } = useTranslation('modules/docker');
|
||||||
|
const utils = api.useContext();
|
||||||
|
const mutation = api.docker.action.useMutation();
|
||||||
|
|
||||||
|
return async (
|
||||||
|
container: Dockerode.ContainerInfo,
|
||||||
|
action: RouterInputs['docker']['action']['action']
|
||||||
|
) => {
|
||||||
|
const containerName = container.Names[0].substring(1);
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
id: container.Id,
|
||||||
|
loading: true,
|
||||||
|
title: `${t(`actions.${action}.start`)} ${containerName}`,
|
||||||
|
message: undefined,
|
||||||
|
autoClose: false,
|
||||||
|
withCloseButton: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await mutation.mutateAsync(
|
||||||
|
{ action, id: container.Id },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notifications.show({
|
||||||
|
id: container.Id,
|
||||||
|
title: containerName,
|
||||||
|
message: `${t(`actions.${action}.end`)} ${containerName}`,
|
||||||
|
icon: <IconCheck />,
|
||||||
|
autoClose: 2000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
notifications.update({
|
||||||
|
id: container.Id,
|
||||||
|
color: 'red',
|
||||||
|
title: t('errors.unknownError.title'),
|
||||||
|
message: err.message,
|
||||||
|
autoClose: 2000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
utils.docker.containers.invalidate();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated legacy code
|
* @deprecated legacy code
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import Dockerode from 'dockerode';
|
||||||
import { createTRPCRouter, publicProcedure } from '../../trpc';
|
import { createTRPCRouter, publicProcedure } from '../../trpc';
|
||||||
import DockerSingleton from './DockerSingleton';
|
import DockerSingleton from './DockerSingleton';
|
||||||
|
|
||||||
|
const dockerActionSchema = z.enum(['remove', 'start', 'stop', 'restart']);
|
||||||
|
|
||||||
export const dockerRouter = createTRPCRouter({
|
export const dockerRouter = createTRPCRouter({
|
||||||
containers: publicProcedure.query(async () => {
|
containers: publicProcedure.query(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -15,4 +19,54 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
action: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
action: dockerActionSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const docker = DockerSingleton.getInstance();
|
||||||
|
// Get the container with the ID
|
||||||
|
const container = docker.getContainer(input.id);
|
||||||
|
if (!container) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Container not found',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the action
|
||||||
|
try {
|
||||||
|
await startAction(container, input.action);
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
message: `Container ${input.id} ${input.action}ed`,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: `Unable to ${input.action} container ${input.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const startAction = async (
|
||||||
|
container: Dockerode.Container,
|
||||||
|
action: z.infer<typeof dockerActionSchema>
|
||||||
|
) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'remove':
|
||||||
|
return container.remove();
|
||||||
|
case 'start':
|
||||||
|
return container.start();
|
||||||
|
case 'stop':
|
||||||
|
return container.stop();
|
||||||
|
case 'restart':
|
||||||
|
return container.restart();
|
||||||
|
default:
|
||||||
|
return Promise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user