diff --git a/src/components/Docker/ContainerActionBar.tsx b/src/components/Docker/ContainerActionBar.tsx new file mode 100644 index 000000000..90bfe4596 --- /dev/null +++ b/src/components/Docker/ContainerActionBar.tsx @@ -0,0 +1,82 @@ +import { Button, Group } from '@mantine/core'; +import { showNotification, updateNotification } from '@mantine/notifications'; +import { + IconCheck, + IconPlayerPlay, + IconPlayerStop, + IconRotateClockwise, + IconX, +} from '@tabler/icons'; +import axios from 'axios'; +import Dockerode from 'dockerode'; + +function sendNotification(action: string, containerId: string, containerName: string) { + showNotification({ + id: 'load-data', + loading: true, + title: `${action}ing container ${containerName}`, + message: 'Your password is being checked...', + autoClose: false, + disallowClose: true, + }); + axios.get(`/api/docker/container/${containerId}?action=${action}`).then((res) => { + setTimeout(() => { + if (res.data.success === true) { + updateNotification({ + id: 'load-data', + title: 'Container restarted', + message: 'Your container was successfully restarted', + icon: , + autoClose: 2000, + }); + } + if (res.data.success === false) { + updateNotification({ + id: 'load-data', + color: 'red', + title: 'There was an error restarting your container.', + message: 'Your container has encountered issues while restarting.', + icon: , + autoClose: 2000, + }); + } + }, 500); + }); +} + +function restart(container: Dockerode.ContainerInfo) { + sendNotification('restart', container.Id, container.Names[0]); +} +function stop(container: Dockerode.ContainerInfo) { + console.log('stoping container', container.Id); +} +function start(container: Dockerode.ContainerInfo) { + console.log('starting container', container.Id); +} + +export interface ContainerActionBarProps { + selected: Dockerode.ContainerInfo[]; +} + +export default function ContainerActionBar(props: ContainerActionBarProps) { + const { selected } = props; + return ( + + + + + + ); +} diff --git a/src/components/Docker/ContainerState.tsx b/src/components/Docker/ContainerState.tsx new file mode 100644 index 000000000..d5c6b5077 --- /dev/null +++ b/src/components/Docker/ContainerState.tsx @@ -0,0 +1,49 @@ +import { Badge, BadgeVariant, MantineSize } from '@mantine/core'; +import Dockerode from 'dockerode'; + +export interface ContainerStateProps { + state: Dockerode.ContainerInfo['State']; +} + +export default function ContainerState(props: ContainerStateProps) { + const { state } = props; + const options: { + size: MantineSize; + radius: MantineSize; + variant: BadgeVariant; + } = { + size: 'md', + radius: 'md', + variant: 'outline', + }; + switch (state) { + case 'running': { + return ( + + Running + + ); + } + case 'created': { + return ( + + Created + + ); + } + case 'exited': { + return ( + + Stopped + + ); + } + default: { + return ( + + Unknown + + ); + } + } +} diff --git a/src/components/Docker/DockerDrawer.tsx b/src/components/Docker/DockerDrawer.tsx index f295e1b02..c6742f55c 100644 --- a/src/components/Docker/DockerDrawer.tsx +++ b/src/components/Docker/DockerDrawer.tsx @@ -1,37 +1,114 @@ -import { ActionIcon, Drawer, Group, List, Text } from '@mantine/core'; +import { + ActionIcon, + Badge, + Checkbox, + createStyles, + Drawer, + Group, + List, + Menu, + ScrollArea, + Table, + Text, +} from '@mantine/core'; import { IconBrandDocker } from '@tabler/icons'; import axios from 'axios'; import { useEffect, useState } from 'react'; import Docker from 'dockerode'; +import DockerMenu from './DockerMenu'; +import ContainerState from './ContainerState'; +import ContainerActionBar from './ContainerActionBar'; + +const useStyles = createStyles((theme) => ({ + rowSelected: { + backgroundColor: + theme.colorScheme === 'dark' + ? theme.fn.rgba(theme.colors[theme.primaryColor][7], 0.2) + : theme.colors[theme.primaryColor][0], + }, +})); export default function DockerDrawer(props: any) { const [opened, setOpened] = useState(false); - const [containers, setContainers] = useState([]); + const [containers, setContainers] = useState([]); + const { classes, cx } = useStyles(); + const [selection, setSelection] = useState([]); + const toggleRow = (container: Docker.ContainerInfo) => + setSelection((current) => + current.includes(container) ? current.filter((c) => c !== container) : [...current, container] + ); + const toggleAll = () => + setSelection((current) => + current.length === containers.length ? [] : containers.map((c) => c) + ); + useEffect(() => { axios.get('/api/docker/containers').then((res) => { setContainers(res.data); }); }, []); + const rows = containers.map((element) => { + const selected = selection.includes(element); + return ( + + + toggleRow(element)} + transitionDuration={0} + /> + + {element.Names[0].replace('/', '')} + {element.Image} + + + {element.Ports.slice(-3).map((port) => ( + + {port.PrivatePort}:{port.PublicPort} + + ))} + {element.Ports.length > 3 && ( + + {element.Ports.length - 3} more + + )} + + + + + + + ); + }); + return ( <> - setOpened(false)} - title="Register" - padding="xl" - size="full" - > - - {containers.map((container) => ( - - {container.Names[0]} - {container.State} - {container.Status} - {container.Image} - - ))} - + setOpened(false)} padding="xl" size="full"> + + + + + + + + + + + + + + {rows} +
your docker containers
+ 0 && selection.length !== containers.length} + transitionDuration={0} + /> + NameImagePortsState
+
+ { + setTimeout(() => { + if (res.data.success === true) { + updateNotification({ + id: 'load-data', + title: 'Container restarted', + message: 'Your container was successfully restarted', + icon: , + autoClose: 2000, + }); + } + if (res.data.success === false) { + updateNotification({ + id: 'load-data', + color: 'red', + title: 'There was an error restarting your container.', + message: 'Your container has encountered issues while restarting.', + icon: , + autoClose: 2000, + }); + } + }, 500); + }); +} + +function restart(container: Dockerode.ContainerInfo) { + sendNotification('restart', container.Id, container.Names[0]); +} +function stop(container: Dockerode.ContainerInfo) { + console.log('stoping container', container.Id); +} +function start(container: Dockerode.ContainerInfo) { + console.log('starting container', container.Id); +} + +export default function DockerMenu(props: any) { + const { container }: { container: Dockerode.ContainerInfo } = props; + const theme = useMantineTheme(); + if (container === undefined) { + return null; + } + return ( + + Actions + } onClick={() => restart(container)}> + Restart + + {container.State === 'running' ? ( + }> + Stop + + ) : ( + }> + Start + + )} + {/* }> + Pull latest image + + }> + Logs + */} + Homarr + }> + Add to Homarr + + + ); +} diff --git a/src/pages/api/docker/container/[id].tsx b/src/pages/api/docker/container/[id].tsx new file mode 100644 index 000000000..5e6c7d54c --- /dev/null +++ b/src/pages/api/docker/container/[id].tsx @@ -0,0 +1,52 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import Docker from 'dockerode'; + +const docker = new Docker(); + +async function Get(req: NextApiRequest, res: NextApiResponse) { + // Get the slug of the request + const { id } = req.query as { id: string }; + const { action } = req.query; + // Get the action on the request (start, stop, restart) + if (action !== 'start' && action !== 'stop' && action !== 'restart') { + return res.status(400).json({ + statusCode: 400, + message: 'Invalid action', + }); + } + if (!id) { + return res.status(400).json({ + message: 'Missing ID', + }); + } + // Get the container with the ID + const container = docker.getContainer(id); + // Get the container info + container.inspect((err, data) => { + if (err) { + res.status(500).json({ + message: err, + }); + } + }); + if (action === 'restart') { + await container.restart(); + return res.status(200).json({ + success: true, + }); + } + return res.status(200).json({ + success: true, + }); +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + // Filter out if the reuqest is a Put or a GET + if (req.method === 'GET') { + return Get(req, res); + } + return res.status(405).json({ + statusCode: 405, + message: 'Method not allowed', + }); +}; diff --git a/src/pages/api/docker/containers.tsx b/src/pages/api/docker/containers.tsx index 5849673a2..6a75d985d 100644 --- a/src/pages/api/docker/containers.tsx +++ b/src/pages/api/docker/containers.tsx @@ -1,4 +1,3 @@ -import axios from 'axios'; import { NextApiRequest, NextApiResponse } from 'next'; import Docker from 'dockerode'; @@ -6,12 +5,11 @@ import Docker from 'dockerode'; const docker = new Docker(); async function Get(req: NextApiRequest, res: NextApiResponse) { - const con: Docker.Container = docker.getContainer('hello'); docker.listContainers({ all: true }, (err, containers) => { if (err) { res.status(500).json({ error: err }); } - res.status(200).json(containers); + return res.status(200).json(containers); }); }