From df7e833b84eb8b05adca1845174d490075120974 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 20 Jun 2022 09:00:42 +0200 Subject: [PATCH 01/25] :construction: Work in progress on docker integration --- package.json | 2 + src/components/Docker/DockerDrawer.tsx | 48 ++++++ src/components/layout/Header.tsx | 2 + src/pages/api/docker/containers.tsx | 27 ++++ yarn.lock | 202 ++++++++++++++++++++++++- 5 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 src/components/Docker/DockerDrawer.tsx create mode 100644 src/pages/api/docker/containers.tsx diff --git a/package.json b/package.json index 3c02b4067..a04acfa24 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "axios": "^0.27.2", "cookies-next": "^2.0.4", "dayjs": "^1.11.3", + "dockerode": "^3.3.2", "framer-motion": "^6.3.1", "js-file-download": "^0.4.12", "next": "12.1.6", @@ -59,6 +60,7 @@ "@next/bundle-analyzer": "^12.1.4", "@next/eslint-plugin-next": "^12.1.4", "@storybook/react": "^6.5.4", + "@types/dockerode": "^3.3.9", "@types/node": "^17.0.23", "@types/react": "17.0.43", "@types/uuid": "^8.3.4", diff --git a/src/components/Docker/DockerDrawer.tsx b/src/components/Docker/DockerDrawer.tsx new file mode 100644 index 000000000..f295e1b02 --- /dev/null +++ b/src/components/Docker/DockerDrawer.tsx @@ -0,0 +1,48 @@ +import { ActionIcon, Drawer, Group, List, Text } from '@mantine/core'; +import { IconBrandDocker } from '@tabler/icons'; +import axios from 'axios'; +import { useEffect, useState } from 'react'; +import Docker from 'dockerode'; + +export default function DockerDrawer(props: any) { + const [opened, setOpened] = useState(false); + const [containers, setContainers] = useState([]); + useEffect(() => { + axios.get('/api/docker/containers').then((res) => { + setContainers(res.data); + }); + }, []); + return ( + <> + setOpened(false)} + title="Register" + padding="xl" + size="full" + > + + {containers.map((container) => ( + + {container.Names[0]} + {container.State} + {container.Status} + {container.Image} + + ))} + + + + setOpened(true)} + > + + + + + ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index cbfd807be..119a44cb8 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -18,6 +18,7 @@ import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem'; import { SettingsMenuButton } from '../Settings/SettingsMenu'; import { ModuleWrapper } from '../modules/moduleWrapper'; import { CalendarModule, TotalDownloadsModule, WeatherModule, DateModule } from '../modules'; +import DockerDrawer from '../Docker/DockerDrawer'; const HEADER_HEIGHT = 60; @@ -47,6 +48,7 @@ export function Header(props: any) { + diff --git a/src/pages/api/docker/containers.tsx b/src/pages/api/docker/containers.tsx new file mode 100644 index 000000000..5849673a2 --- /dev/null +++ b/src/pages/api/docker/containers.tsx @@ -0,0 +1,27 @@ +import axios from 'axios'; +import { NextApiRequest, NextApiResponse } from 'next'; + +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); + }); +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + // Filter out if the reuqest is a POST 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/yarn.lock b/yarn.lock index 4b950084b..929ce5a86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3874,6 +3874,26 @@ __metadata: languageName: node linkType: hard +"@types/docker-modem@npm:*": + version: 3.0.2 + resolution: "@types/docker-modem@npm:3.0.2" + dependencies: + "@types/node": "*" + "@types/ssh2": "*" + checksum: 1f23db30e6e9bdd4c6d6e43572fb7ac7251d106a1906a9f3faabac393897712a5a9cd5a471baedc0ac8055dab3f48eda331f41a1e2c7c6bbe3c7f433e039151c + languageName: node + linkType: hard + +"@types/dockerode@npm:^3.3.9": + version: 3.3.9 + resolution: "@types/dockerode@npm:3.3.9" + dependencies: + "@types/docker-modem": "*" + "@types/node": "*" + checksum: 3d03c68addb37c50e9557fff17171d26423aa18e544cb24e4caa81ebcec39ccc1cafed7adbfb8f4220d8ed23028d231717826bb77a786d425885c4f4cc37536d + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3": version: 3.7.3 resolution: "@types/eslint-scope@npm:3.7.3" @@ -4151,6 +4171,25 @@ __metadata: languageName: node linkType: hard +"@types/ssh2-streams@npm:*": + version: 0.1.9 + resolution: "@types/ssh2-streams@npm:0.1.9" + dependencies: + "@types/node": "*" + checksum: 190f3c235bf19787cd255f366d3ac9233875750095f3c73d15e72a1e67a826aed7e7c389603c5e89cb6420b87ff6dffc566f9174e546ddb7ff8c8dc2e8b00def + languageName: node + linkType: hard + +"@types/ssh2@npm:*": + version: 0.5.52 + resolution: "@types/ssh2@npm:0.5.52" + dependencies: + "@types/node": "*" + "@types/ssh2-streams": "*" + checksum: bc1c76ac727ad73ddd59ba849cf0ea3ed2e930439e7a363aff24f04f29b74f9b1976369b869dc9a018223c9fb8ad041c09a0f07aea8cf46a8c920049188cddae + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.1 resolution: "@types/stack-utils@npm:2.0.1" @@ -5196,6 +5235,15 @@ __metadata: languageName: node linkType: hard +"asn1@npm:^0.2.4": + version: 0.2.6 + resolution: "asn1@npm:0.2.6" + dependencies: + safer-buffer: ~2.1.0 + checksum: 39f2ae343b03c15ad4f238ba561e626602a3de8d94ae536c46a4a93e69578826305366dc09fbb9b56aec39b4982a463682f259c38e59f6fa380cd72cd61e493d + languageName: node + linkType: hard + "assert@npm:^1.1.1": version: 1.5.0 resolution: "assert@npm:1.5.0" @@ -5519,7 +5567,7 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.0.2": +"base64-js@npm:^1.0.2, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 @@ -5541,6 +5589,15 @@ __metadata: languageName: node linkType: hard +"bcrypt-pbkdf@npm:^1.0.2": + version: 1.0.2 + resolution: "bcrypt-pbkdf@npm:1.0.2" + dependencies: + tweetnacl: ^0.14.3 + checksum: 4edfc9fe7d07019609ccf797a2af28351736e9d012c8402a07120c4453a3b789a15f2ee1530dc49eee8f7eb9379331a8dd4b3766042b9e502f74a68e7f662291 + languageName: node + linkType: hard + "better-opn@npm:^2.1.1": version: 2.1.1 resolution: "better-opn@npm:2.1.1" @@ -5587,6 +5644,17 @@ __metadata: languageName: node linkType: hard +"bl@npm:^4.0.3": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: ^5.5.0 + inherits: ^2.0.4 + readable-stream: ^3.4.0 + checksum: 9e8521fa7e83aa9427c6f8ccdcba6e8167ef30cc9a22df26effcc5ab682ef91d2cbc23a239f945d099289e4bbcfae7a192e9c28c84c6202e710a0dfec3722662 + languageName: node + linkType: hard + "bluebird@npm:^3.5.5": version: 3.7.2 resolution: "bluebird@npm:3.7.2" @@ -5842,6 +5910,23 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: ^1.3.1 + ieee754: ^1.1.13 + checksum: e2cf8429e1c4c7b8cbd30834ac09bd61da46ce35f5c22a78e6c2f04497d6d25541b16881e30a019c6fd3154150650ccee27a308eff3e26229d788bbdeb08ab84 + languageName: node + linkType: hard + +"buildcheck@npm:0.0.3": + version: 0.0.3 + resolution: "buildcheck@npm:0.0.3" + checksum: baf30605c56e80c2ca0502e40e18f2ebc7075bb4a861c941c0b36cd468b27957ed11a62248003ce99b9e5f91a7dfa859b30aad4fa50f0090c77a6f596ba20e6d + languageName: node + linkType: hard + "builtin-status-codes@npm:^3.0.0": version: 3.0.0 resolution: "builtin-status-codes@npm:3.0.0" @@ -6679,6 +6764,17 @@ __metadata: languageName: node linkType: hard +"cpu-features@npm:~0.0.4": + version: 0.0.4 + resolution: "cpu-features@npm:0.0.4" + dependencies: + buildcheck: 0.0.3 + nan: ^2.15.0 + node-gyp: latest + checksum: a20d58e41e63182b34753dfe23bd1d967944ec13d84b70849b5d334fb4a558b7e71e7f955ed86c8e75dd65b5c5b882f1c494174d342cb6d8a062d77f79d39596 + languageName: node + linkType: hard + "cpy@npm:^8.1.2": version: 8.1.2 resolution: "cpy@npm:8.1.2" @@ -7243,6 +7339,28 @@ __metadata: languageName: node linkType: hard +"docker-modem@npm:^3.0.0": + version: 3.0.5 + resolution: "docker-modem@npm:3.0.5" + dependencies: + debug: ^4.1.1 + readable-stream: ^3.5.0 + split-ca: ^1.0.1 + ssh2: ^1.4.0 + checksum: 79027f8e719a77031790af628f9aa1d72607ec3769149de3a4b683930f2e4d113ae0e3a7345b32ff3b2289f886879f4fcf216afb17908178ba00f9661c4e0dd6 + languageName: node + linkType: hard + +"dockerode@npm:^3.3.2": + version: 3.3.2 + resolution: "dockerode@npm:3.3.2" + dependencies: + docker-modem: ^3.0.0 + tar-fs: ~2.0.1 + checksum: 69b60547ed2e6156e6ec1df16fccea9150c935ee0b0517723b4d05a5d840a01d4cd945341390d24b7fa301383be64145d563d9319be56d487a5bcbf9f872ee59 + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -7466,7 +7584,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0": +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -8716,6 +8834,13 @@ __metadata: languageName: node linkType: hard +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d + languageName: node + linkType: hard + "fs-extra@npm:^10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -9446,6 +9571,7 @@ __metadata: "@nivo/line": ^0.79.1 "@storybook/react": ^6.5.4 "@tabler/icons": ^1.68.0 + "@types/dockerode": ^3.3.9 "@types/node": ^17.0.23 "@types/react": 17.0.43 "@types/uuid": ^8.3.4 @@ -9454,6 +9580,7 @@ __metadata: axios: ^0.27.2 cookies-next: ^2.0.4 dayjs: ^1.11.3 + dockerode: ^3.3.2 eslint: ^8.11.0 eslint-config-airbnb: ^19.0.4 eslint-config-airbnb-typescript: ^16.1.0 @@ -9704,7 +9831,7 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:^1.1.4": +"ieee754@npm:^1.1.13, ieee754@npm:^1.1.4": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e @@ -11841,6 +11968,13 @@ __metadata: languageName: node linkType: hard +"mkdirp-classic@npm:^0.5.2": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac + languageName: node + linkType: hard + "mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.3": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -11920,7 +12054,7 @@ __metadata: languageName: node linkType: hard -"nan@npm:^2.12.1": +"nan@npm:^2.12.1, nan@npm:^2.15.0, nan@npm:^2.16.0": version: 2.16.0 resolution: "nan@npm:2.16.0" dependencies: @@ -13695,7 +13829,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.6.0": +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": version: 3.6.0 resolution: "readable-stream@npm:3.6.0" dependencies: @@ -14178,7 +14312,7 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0": +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 @@ -14659,6 +14793,13 @@ __metadata: languageName: node linkType: hard +"split-ca@npm:^1.0.1": + version: 1.0.1 + resolution: "split-ca@npm:1.0.1" + checksum: 1e7409938a95ee843fe2593156a5735e6ee63772748ee448ea8477a5a3e3abde193c3325b3696e56a5aff07c7dcf6b1f6a2f2a036895b4f3afe96abb366d893f + languageName: node + linkType: hard + "split-string@npm:^3.0.1, split-string@npm:^3.0.2": version: 3.1.0 resolution: "split-string@npm:3.1.0" @@ -14675,6 +14816,23 @@ __metadata: languageName: node linkType: hard +"ssh2@npm:^1.4.0": + version: 1.11.0 + resolution: "ssh2@npm:1.11.0" + dependencies: + asn1: ^0.2.4 + bcrypt-pbkdf: ^1.0.2 + cpu-features: ~0.0.4 + nan: ^2.16.0 + dependenciesMeta: + cpu-features: + optional: true + nan: + optional: true + checksum: e40cb9f171741a807c170dc555078aa8c49dc93dd36fc9c8be026fce1cfd31f0d37078d9b60a0f2cfb11d0e007ed5407376b72f8a0ef9f2cb89574632bbfb824 + languageName: node + linkType: hard + "ssri@npm:^6.0.1": version: 6.0.2 resolution: "ssri@npm:6.0.2" @@ -15125,6 +15283,31 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:~2.0.1": + version: 2.0.1 + resolution: "tar-fs@npm:2.0.1" + dependencies: + chownr: ^1.1.1 + mkdirp-classic: ^0.5.2 + pump: ^3.0.0 + tar-stream: ^2.0.0 + checksum: 26cd297ed2421bc8038ce1a4ca442296b53739f409847d495d46086e5713d8db27f2c03ba2f461d0f5ddbc790045628188a8544f8ae32cbb6238b279b68d0247 + languageName: node + linkType: hard + +"tar-stream@npm:^2.0.0": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: ^4.0.3 + end-of-stream: ^1.4.1 + fs-constants: ^1.0.0 + inherits: ^2.0.3 + readable-stream: ^3.1.1 + checksum: 699831a8b97666ef50021c767f84924cfee21c142c2eb0e79c63254e140e6408d6d55a065a2992548e72b06de39237ef2b802b99e3ece93ca3904a37622a66f3 + languageName: node + linkType: hard + "tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.1.11 resolution: "tar@npm:6.1.11" @@ -15504,6 +15687,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:^0.14.3": + version: 0.14.5 + resolution: "tweetnacl@npm:0.14.5" + checksum: 6061daba1724f59473d99a7bb82e13f211cdf6e31315510ae9656fefd4779851cb927adad90f3b488c8ed77c106adc0421ea8055f6f976ff21b27c5c4e918487 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" From 72aba9d8cd44a656a2578938e429c52346476ef4 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 27 Jun 2022 19:25:26 +0200 Subject: [PATCH 02/25] :construction: Work in progress on the Docker integration --- src/components/Docker/ContainerActionBar.tsx | 82 +++++++++++++ src/components/Docker/ContainerState.tsx | 49 ++++++++ src/components/Docker/DockerDrawer.tsx | 115 ++++++++++++++++--- src/components/Docker/DockerMenu.tsx | 93 +++++++++++++++ src/pages/api/docker/container/[id].tsx | 52 +++++++++ src/pages/api/docker/containers.tsx | 4 +- 6 files changed, 373 insertions(+), 22 deletions(-) create mode 100644 src/components/Docker/ContainerActionBar.tsx create mode 100644 src/components/Docker/ContainerState.tsx create mode 100644 src/components/Docker/DockerMenu.tsx create mode 100644 src/pages/api/docker/container/[id].tsx 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); }); } From 035224b02bd855638b9a2a727252f2661e476a8b Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 27 Jun 2022 23:38:54 +0200 Subject: [PATCH 03/25] :sparkles: add start/stop/restart feature on containers --- src/components/Docker/ContainerActionBar.tsx | 68 +++++++++++++------- src/components/Docker/DockerDrawer.tsx | 28 ++++---- src/pages/api/docker/container/[id].tsx | 38 +++++++++-- 3 files changed, 93 insertions(+), 41 deletions(-) diff --git a/src/components/Docker/ContainerActionBar.tsx b/src/components/Docker/ContainerActionBar.tsx index 90bfe4596..474732619 100644 --- a/src/components/Docker/ContainerActionBar.tsx +++ b/src/components/Docker/ContainerActionBar.tsx @@ -4,6 +4,7 @@ import { IconCheck, IconPlayerPlay, IconPlayerStop, + IconRefresh, IconRotateClockwise, IconX, } from '@tabler/icons'; @@ -12,10 +13,10 @@ import Dockerode from 'dockerode'; function sendNotification(action: string, containerId: string, containerName: string) { showNotification({ - id: 'load-data', + id: containerId, loading: true, title: `${action}ing container ${containerName}`, - message: 'Your password is being checked...', + message: undefined, autoClose: false, disallowClose: true, }); @@ -23,19 +24,19 @@ function sendNotification(action: string, containerId: string, containerName: st setTimeout(() => { if (res.data.success === true) { updateNotification({ - id: 'load-data', - title: 'Container restarted', - message: 'Your container was successfully restarted', + id: containerId, + title: `Container ${containerName} ${action}ed`, + message: `Your container was successfully ${action}ed`, icon: , autoClose: 2000, }); } if (res.data.success === false) { updateNotification({ - id: 'load-data', + id: containerId, color: 'red', - title: 'There was an error restarting your container.', - message: 'Your container has encountered issues while restarting.', + title: 'There was an error with your container.', + message: undefined, icon: , autoClose: 2000, }); @@ -44,39 +45,58 @@ function sendNotification(action: string, containerId: string, containerName: st }); } -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[]; + reload: () => void; } -export default function ContainerActionBar(props: ContainerActionBarProps) { - const { selected } = props; +export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) { return ( - - + ); } diff --git a/src/components/Docker/DockerDrawer.tsx b/src/components/Docker/DockerDrawer.tsx index c6742f55c..8e07dda66 100644 --- a/src/components/Docker/DockerDrawer.tsx +++ b/src/components/Docker/DockerDrawer.tsx @@ -33,6 +33,12 @@ export default function DockerDrawer(props: any) { const [containers, setContainers] = useState([]); const { classes, cx } = useStyles(); const [selection, setSelection] = useState([]); + function reload() { + axios.get('/api/docker/containers').then((res) => { + setContainers(res.data); + }); + } + const toggleRow = (container: Docker.ContainerInfo) => setSelection((current) => current.includes(container) ? current.filter((c) => c !== container) : [...current, container] @@ -43,9 +49,7 @@ export default function DockerDrawer(props: any) { ); useEffect(() => { - axios.get('/api/docker/containers').then((res) => { - setContainers(res.data); - }); + reload(); }, []); const rows = containers.map((element) => { const selected = selection.includes(element); @@ -62,15 +66,15 @@ export default function DockerDrawer(props: any) { {element.Image} - {element.Ports.slice(-3).map((port) => ( - - {port.PrivatePort}:{port.PublicPort} - - ))} + {element.Ports.sort((a, b) => a.PrivatePort - b.PrivatePort) + .slice(-3) + .map((port) => ( + + {port.PrivatePort}:{port.PublicPort} + + ))} {element.Ports.length > 3 && ( - - {element.Ports.length - 3} more - + {element.Ports.length - 3} more )} @@ -85,7 +89,7 @@ export default function DockerDrawer(props: any) { <> setOpened(false)} padding="xl" size="full"> - + diff --git a/src/pages/api/docker/container/[id].tsx b/src/pages/api/docker/container/[id].tsx index 5e6c7d54c..dd7c2fa65 100644 --- a/src/pages/api/docker/container/[id].tsx +++ b/src/pages/api/docker/container/[id].tsx @@ -29,11 +29,39 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { }); } }); - if (action === 'restart') { - await container.restart(); - return res.status(200).json({ - success: true, - }); + + switch (action) { + case 'start': + container.start((err, data) => { + if (err) { + res.status(500).json({ + message: err, + }); + } + }); + break; + case 'stop': + container.stop((err, data) => { + if (err) { + res.status(500).json({ + message: err, + }); + } + }); + break; + case 'restart': + container.restart((err, data) => { + if (err) { + res.status(500).json({ + message: err, + }); + } + }); + break; + default: + res.status(400).json({ + message: 'Invalid action', + }); } return res.status(200).json({ success: true, From 812de35149a35f8c93ca6642891ed7ac7cef9a96 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 28 Jun 2022 10:34:25 +0200 Subject: [PATCH 04/25] :bug: Fix a bug where download module was always there --- src/components/AppShelf/AppShelf.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx index 083de4626..abfed55cc 100644 --- a/src/components/AppShelf/AppShelf.tsx +++ b/src/components/AppShelf/AppShelf.tsx @@ -152,6 +152,7 @@ const AppShelf = (props: any) => { const noCategory = config.services.filter( (e) => e.category === undefined || e.category === null ); + const downloadEnabled = config.modules?.[DownloadsModule.title]?.enabled ?? false; // Create an item with 0: true, 1: true, 2: true... For each category return ( // Return one item for each category @@ -176,6 +177,7 @@ const AppShelf = (props: any) => { {item()} ) : null} + {downloadEnabled ? ( { + ) : null} ); From 9945ef892e4930cec5aee74039cb3da636861477 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 28 Jun 2022 11:06:45 +0200 Subject: [PATCH 05/25] :iphone: Fix settings pannels height --- src/components/Settings/AdvancedSettings.tsx | 2 +- src/components/Settings/CommonSettings.tsx | 42 +------------------ src/components/Settings/Credits.tsx | 44 ++++++++++++++++++++ src/components/Settings/SettingsMenu.tsx | 16 ++++--- 4 files changed, 58 insertions(+), 46 deletions(-) create mode 100644 src/components/Settings/Credits.tsx diff --git a/src/components/Settings/AdvancedSettings.tsx b/src/components/Settings/AdvancedSettings.tsx index ad4517457..4c7d6a50e 100644 --- a/src/components/Settings/AdvancedSettings.tsx +++ b/src/components/Settings/AdvancedSettings.tsx @@ -37,7 +37,7 @@ export default function TitleChanger() { }; return ( - +
saveChanges(values))}> diff --git a/src/components/Settings/CommonSettings.tsx b/src/components/Settings/CommonSettings.tsx index 55c710359..91e52d8f5 100644 --- a/src/components/Settings/CommonSettings.tsx +++ b/src/components/Settings/CommonSettings.tsx @@ -1,7 +1,5 @@ -import { ActionIcon, Group, Text, SegmentedControl, TextInput, Anchor } from '@mantine/core'; +import { Group, Text, SegmentedControl, TextInput } from '@mantine/core'; import { useState } from 'react'; -import { IconBrandGithub as BrandGithub, IconBrandDiscord as BrandDiscord } from '@tabler/icons'; -import { CURRENT_VERSION } from '../../../data/constants'; import { useConfig } from '../../tools/state'; import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch'; import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch'; @@ -25,7 +23,7 @@ export default function CommonSettings(args: any) { ); return ( - + Search engine Tip: You can upload your config file by dragging and dropping it onto the page! - - - component="a" href="https://github.com/ajnart/homarr" size="lg"> - - - - {CURRENT_VERSION} - - - - - Made with ❤️ by @ - - ajnart - - - component="a" href="https://discord.gg/aCsmEV5RgA" size="lg"> - - - - ); } diff --git a/src/components/Settings/Credits.tsx b/src/components/Settings/Credits.tsx new file mode 100644 index 000000000..1d6271479 --- /dev/null +++ b/src/components/Settings/Credits.tsx @@ -0,0 +1,44 @@ +import { Group, ActionIcon, Anchor, Text } from '@mantine/core'; +import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons'; +import { CURRENT_VERSION } from '../../../data/constants'; + +export default function Credits(props: any) { + return ( + + + component="a" href="https://github.com/ajnart/homarr" size="lg"> + + + + {CURRENT_VERSION} + + + + + Made with ❤️ by @ + + ajnart + + + component="a" href="https://discord.gg/aCsmEV5RgA" size="lg"> + + + + + ); +} diff --git a/src/components/Settings/SettingsMenu.tsx b/src/components/Settings/SettingsMenu.tsx index e6bcb2bed..fcd6d1b91 100644 --- a/src/components/Settings/SettingsMenu.tsx +++ b/src/components/Settings/SettingsMenu.tsx @@ -1,18 +1,23 @@ -import { ActionIcon, Title, Tooltip, Drawer, Tabs } from '@mantine/core'; +import { ActionIcon, Title, Tooltip, Drawer, Tabs, ScrollArea } from '@mantine/core'; import { useHotkeys } from '@mantine/hooks'; import { useState } from 'react'; import { IconSettings } from '@tabler/icons'; import AdvancedSettings from './AdvancedSettings'; import CommonSettings from './CommonSettings'; +import Credits from './Credits'; function SettingsMenu(props: any) { return ( - + + + - + + + ); @@ -26,13 +31,14 @@ export function SettingsMenuButton(props: any) { <> Settings} + title={Settings} opened={props.opened || opened} onClose={() => setOpened(false)} > + Date: Tue, 28 Jun 2022 11:27:23 +0200 Subject: [PATCH 06/25] :sparkles: Add support for lists in module option This feature allows a module maker to use a list as the different possible values for a module integration. --- src/components/modules/moduleWrapper.tsx | 41 ++++++++++++++++++++++-- src/components/modules/modules.tsx | 3 +- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/components/modules/moduleWrapper.tsx b/src/components/modules/moduleWrapper.tsx index a28cde8a8..c7423f658 100644 --- a/src/components/modules/moduleWrapper.tsx +++ b/src/components/modules/moduleWrapper.tsx @@ -1,10 +1,18 @@ -import { Button, Card, Group, Menu, Switch, TextInput, useMantineColorScheme } from '@mantine/core'; +import { + Button, + Card, + Group, + Menu, + MultiSelect, + Switch, + TextInput, + useMantineColorScheme, +} from '@mantine/core'; import { useConfig } from '../../tools/state'; import { IModule } from './modules'; function getItems(module: IModule) { const { config, setConfig } = useConfig(); - const enabledModules = config.modules ?? {}; const items: JSX.Element[] = []; if (module.options) { const keys = Object.keys(module.options); @@ -15,6 +23,35 @@ function getItems(module: IModule) { types.forEach((type, index) => { const optionName = `${module.title}.${keys[index]}`; const moduleInConfig = config.modules?.[module.title]; + if (type === 'object') { + items.push( + { + setConfig({ + ...config, + modules: { + ...config.modules, + [module.title]: { + ...moduleInConfig, + options: { + ...moduleInConfig?.options, + [keys[index]]: { + ...moduleInConfig?.options?.[keys[index]], + value, + }, + }, + }, + }, + }); + }} + /> + ); + } if (type === 'string') { items.push( Date: Tue, 28 Jun 2022 12:10:46 +0200 Subject: [PATCH 07/25] :bug: Fix default values for modules The default value was not set correctly for modules. This has been fixed. It was also fixed in the Weather Module and the Date Module. --- src/components/modules/date/DateModule.tsx | 2 +- src/components/modules/moduleWrapper.tsx | 17 +++++++++++++---- .../modules/weather/WeatherModule.tsx | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/modules/date/DateModule.tsx b/src/components/modules/date/DateModule.tsx index ad5991736..3e212af83 100644 --- a/src/components/modules/date/DateModule.tsx +++ b/src/components/modules/date/DateModule.tsx @@ -23,7 +23,7 @@ export default function DateComponent(props: any) { const [date, setDate] = useState(new Date()); const setSafeInterval = useSetSafeInterval(); const { config } = useConfig(); - const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? false; + const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? true; const formatString = isFullTime ? 'HH:mm' : 'h:mm A'; // Change date on minute change // Note: Using 10 000ms instead of 1000ms to chill a little :) diff --git a/src/components/modules/moduleWrapper.tsx b/src/components/modules/moduleWrapper.tsx index c7423f658..6bbd69c86 100644 --- a/src/components/modules/moduleWrapper.tsx +++ b/src/components/modules/moduleWrapper.tsx @@ -28,8 +28,11 @@ function getItems(module: IModule) { { setConfig({ @@ -81,7 +84,11 @@ function getItems(module: IModule) { id={optionName} name={optionName} label={values[index].name} - defaultValue={(moduleInConfig?.options?.[keys[index]]?.value as string) ?? ''} + defaultValue={ + (moduleInConfig?.options?.[keys[index]]?.value as string) ?? + (values[index].value as string) ?? + '' + } onChange={(e) => {}} /> @@ -96,7 +103,9 @@ function getItems(module: IModule) { { diff --git a/src/components/modules/weather/WeatherModule.tsx b/src/components/modules/weather/WeatherModule.tsx index 1d2a522a0..8a6f6c98f 100644 --- a/src/components/modules/weather/WeatherModule.tsx +++ b/src/components/modules/weather/WeatherModule.tsx @@ -29,7 +29,7 @@ export const WeatherModule: IModule = { }, location: { name: 'Current location', - value: '', + value: 'Paris', }, }, }; @@ -135,7 +135,7 @@ export default function WeatherComponent(props: any) { const { config } = useConfig(); const [weather, setWeather] = useState({} as WeatherResponse); const cityInput: string = - (config?.modules?.[WeatherModule.title]?.options?.location?.value as string) ?? ''; + (config?.modules?.[WeatherModule.title]?.options?.location?.value as string) ?? 'Paris'; const isFahrenheit: boolean = (config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false; From 1a66bfb8be2bb97355695a4c7a0ba1f54c63bc92 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 28 Jun 2022 19:08:18 +0200 Subject: [PATCH 08/25] :sparkles: add a component and use it --- src/components/AppShelf/AddAppShelfItem.tsx | 14 ++--- src/components/Settings/CommonSettings.tsx | 59 +++++++++------------ src/components/layout/Tip.tsx | 19 +++++++ 3 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 src/components/layout/Tip.tsx diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index 206cb5ee4..2a0db4a98 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -24,6 +24,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useDebouncedValue } from '@mantine/hooks'; import { useConfig } from '../../tools/state'; import { ServiceTypeList, StatusCodes } from '../../tools/types'; +import Tip from '../layout/Tip'; export function AddItemShelfButton(props: any) { const [opened, setOpened] = useState(false); @@ -273,15 +274,8 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & }} error={form.errors.apiKey && 'Invalid API key'} /> - - Tip: Get your API key{' '} + + Get your API key{' '} void } & > here. - + )} {form.values.type === 'qBittorrent' && ( diff --git a/src/components/Settings/CommonSettings.tsx b/src/components/Settings/CommonSettings.tsx index 91e52d8f5..4d55eee18 100644 --- a/src/components/Settings/CommonSettings.tsx +++ b/src/components/Settings/CommonSettings.tsx @@ -6,6 +6,7 @@ import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionS import ConfigChanger from '../Config/ConfigChanger'; import SaveConfigComponent from '../Config/SaveConfig'; import ModuleEnabler from './ModuleEnabler'; +import Tip from '../layout/Tip'; export default function CommonSettings(args: any) { const { config, setConfig } = useConfig(); @@ -26,17 +27,13 @@ export default function CommonSettings(args: any) { Search engine - - Tip: %s can be used as a placeholder for the query. - + + Use the prefixes !yt and !t in front of your query to search on YouTube or + for a Torrent respectively. + {searchUrl === 'Custom' && ( - { - setCustomSearchUrl(event.currentTarget.value); - setConfig({ - ...config, - settings: { - ...config.settings, - searchUrl: event.currentTarget.value, - }, - }); - }} - /> + <> + %s can be used as a placeholder for the query. + { + setCustomSearchUrl(event.currentTarget.value); + setConfig({ + ...config, + settings: { + ...config.settings, + searchUrl: event.currentTarget.value, + }, + }); + }} + /> + )} @@ -80,16 +80,7 @@ export default function CommonSettings(args: any) { - - Tip: You can upload your config file by dragging and dropping it onto the page! - + Upload your config file by dragging and dropping it onto the page! ); } diff --git a/src/components/layout/Tip.tsx b/src/components/layout/Tip.tsx new file mode 100644 index 000000000..d21d709f8 --- /dev/null +++ b/src/components/layout/Tip.tsx @@ -0,0 +1,19 @@ +import { Text } from '@mantine/core'; + +interface TipProps { + children: React.ReactNode; +} + +export default function Tip(props: TipProps) { + return ( + + Tip: {props.children} + + ); +} From 3bda6c2b76c39134bd9d35ef183191bc72eab39a Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 28 Jun 2022 19:09:02 +0200 Subject: [PATCH 09/25] :fire: Remove the popover TIP when using the searchbar --- .../modules/search/SearchModule.tsx | 49 ++++++------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/src/components/modules/search/SearchModule.tsx b/src/components/modules/search/SearchModule.tsx index 5848c4c87..eae9b52ca 100644 --- a/src/components/modules/search/SearchModule.tsx +++ b/src/components/modules/search/SearchModule.tsx @@ -1,4 +1,4 @@ -import { Kbd, createStyles, Text, Popover, Autocomplete } from '@mantine/core'; +import { Kbd, createStyles, Text, Popover, Autocomplete, Tooltip } from '@mantine/core'; import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks'; import { useEffect, useRef, useState } from 'react'; import { @@ -107,40 +107,21 @@ export default function SearchBar(props: any) { }, 20); })} > - setOpened(true)} - onBlurCapture={() => setOpened(false)} - target={ - - } - > - - Tip: Use the prefixes !yt and !t in front of your query to search on YouTube - or for a Torrent respectively. - - + size="md" + styles={{ rightSection: { pointerEvents: 'none' } }} + placeholder="Search the web..." + {...props} + {...form.getInputProps('query')} + /> ); } From da7b478d81cbec0b1802ff969b6b10d6d50d8db8 Mon Sep 17 00:00:00 2001 From: MauriceNino Date: Mon, 27 Jun 2022 17:27:59 +0200 Subject: [PATCH 10/25] feat: add dash. integration --- src/components/AppShelf/AddAppShelfItem.tsx | 27 +- src/components/layout/Header.tsx | 21 +- src/components/layout/Widgets.tsx | 2 + .../modules/dash./DashdotModule.tsx | 246 ++++++++++++++++++ src/components/modules/dash./index.ts | 1 + src/components/modules/index.ts | 9 +- src/tools/types.ts | 15 +- 7 files changed, 275 insertions(+), 46 deletions(-) create mode 100644 src/components/modules/dash./DashdotModule.tsx create mode 100644 src/components/modules/dash./index.ts diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index 2a0db4a98..80ee6c259 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -1,27 +1,13 @@ import { - Modal, - Center, - Group, - TextInput, - Image, - Button, - Select, - LoadingOverlay, - ActionIcon, - Tooltip, - Title, - Anchor, - Text, - Tabs, - MultiSelect, - ScrollArea, - Switch, + ActionIcon, Anchor, Button, Center, + Group, Image, LoadingOverlay, Modal, MultiSelect, + ScrollArea, Select, Switch, Tabs, Text, TextInput, Title, Tooltip } from '@mantine/core'; import { useForm } from '@mantine/form'; -import { useEffect, useState } from 'react'; -import { IconApps as Apps } from '@tabler/icons'; -import { v4 as uuidv4 } from 'uuid'; import { useDebouncedValue } from '@mantine/hooks'; +import { IconApps as Apps } from '@tabler/icons'; +import { useEffect, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { useConfig } from '../../tools/state'; import { ServiceTypeList, StatusCodes } from '../../tools/types'; import Tip from '../layout/Tip'; @@ -85,6 +71,7 @@ function MatchPort(name: string, form: any) { { name: 'readarr', value: '8686' }, { name: 'deluge', value: '8112' }, { name: 'transmission', value: '9091' }, + { name: 'dash.', value: '3001' }, ]; // Match name with portmap key const port = portmap.find((p) => p.name === name.toLowerCase()); diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index cbfd807be..c3457a244 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,23 +1,23 @@ -import React from 'react'; import { - createStyles, - Header as Head, - Group, + ActionIcon, Box, Burger, + createStyles, Drawer, - Title, + Group, + Header as Head, ScrollArea, - ActionIcon, + Title, Transition, } from '@mantine/core'; import { useBooleanToggle } from '@mantine/hooks'; -import { Logo } from './Logo'; -import SearchBar from '../modules/search/SearchModule'; import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem'; -import { SettingsMenuButton } from '../Settings/SettingsMenu'; +import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules'; +import { DashdotModule } from '../modules/dash.'; import { ModuleWrapper } from '../modules/moduleWrapper'; -import { CalendarModule, TotalDownloadsModule, WeatherModule, DateModule } from '../modules'; +import SearchBar from '../modules/search/SearchModule'; +import { SettingsMenuButton } from '../Settings/SettingsMenu'; +import { Logo } from './Logo'; const HEADER_HEIGHT = 60; @@ -84,6 +84,7 @@ export function Header(props: any) { + diff --git a/src/components/layout/Widgets.tsx b/src/components/layout/Widgets.tsx index b82f489a9..0eceae4c4 100644 --- a/src/components/layout/Widgets.tsx +++ b/src/components/layout/Widgets.tsx @@ -1,6 +1,7 @@ import { Group } from '@mantine/core'; import { useMediaQuery } from '@mantine/hooks'; import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules'; +import { DashdotModule } from '../modules/dash.'; import { ModuleWrapper } from '../modules/moduleWrapper'; export default function Widgets(props: any) { @@ -14,6 +15,7 @@ export default function Widgets(props: any) { + )} diff --git a/src/components/modules/dash./DashdotModule.tsx b/src/components/modules/dash./DashdotModule.tsx new file mode 100644 index 000000000..979fdba76 --- /dev/null +++ b/src/components/modules/dash./DashdotModule.tsx @@ -0,0 +1,246 @@ +import { createStyles, useMantineColorScheme, useMantineTheme } from '@mantine/core'; +import { IconCalendar as CalendarIcon } from '@tabler/icons'; +import axios from 'axios'; +import { useEffect, useState } from 'react'; +import { useConfig } from '../../../tools/state'; +import { serviceItem } from '../../../tools/types'; +import { IModule } from '../modules'; + +const asModule = (t: T) => t; +export const DashdotModule = asModule({ + title: 'Dash.', + description: 'A module for displaying the graphs of your running Dash. instance.', + icon: CalendarIcon, + component: DashdotComponent, + options: { + cpuMultiView: { + name: 'CPU Multi-Core View', + value: false, + }, + storageMultiView: { + name: 'Storage Multi-Drive View', + value: false, + }, + useCompactView: { + name: 'Use Compact View', + value: false, + }, + showCpu: { + name: 'Show CPU Graph', + value: true, + }, + showStorage: { + name: 'Show Storage Graph', + value: true, + }, + showRam: { + name: 'Show RAM Graph', + value: true, + }, + showNetwork: { + name: 'Show Network Graphs', + value: true, + }, + showGpu: { + name: 'Show GPU Graph', + value: false, + }, + }, +}); + +const useStyles = createStyles((theme, _params) => ({ + heading: { + marginTop: 0, + marginBottom: 10, + }, + table: { + display: 'table', + }, + tableRow: { + display: 'table-row', + }, + tableLabel: { + display: 'table-cell', + paddingRight: 10, + }, + tableValue: { + display: 'table-cell', + whiteSpace: 'pre-wrap', + paddingBottom: 5, + }, + graphsContainer: { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + rowGap: 15, + columnGap: 10, + }, + iframe: { + flex: '1 0 auto', + maxWidth: '100%', + height: '140px', + borderRadius: theme.radius.lg, + }, +})); + +const bpsPrettyPrint = (bits?: number) => + !bits + ? '-' + : bits > 1000 * 1000 * 1000 + ? `${(bits / 1000 / 1000 / 1000).toFixed(1)} Gb/s` + : bits > 1000 * 1000 + ? `${(bits / 1000 / 1000).toFixed(1)} Mb/s` + : bits > 1000 + ? `${(bits / 1000).toFixed(1)} Kb/s` + : `${bits.toFixed(1)} b/s`; + +const bytePrettyPrint = (byte: number): string => + byte > 1024 * 1024 * 1024 + ? `${(byte / 1024 / 1024 / 1024).toFixed(1)} GiB` + : byte > 1024 * 1024 + ? `${(byte / 1024 / 1024).toFixed(1)} MiB` + : byte > 1024 + ? `${(byte / 1024).toFixed(1)} KiB` + : `${byte.toFixed(1)} B`; + +const useJson = (service: serviceItem | undefined, url: string) => { + const [data, setData] = useState(); + + const doRequest = async () => { + try { + const resp = await axios.get(url, { baseURL: service?.url }); + + setData(resp.data); + // eslint-disable-next-line no-empty + } catch (e) {} + }; + + useEffect(() => { + if (service?.url) { + doRequest(); + } + }, [service?.url]); + + return data; +}; + +export function DashdotComponent() { + const { config } = useConfig(); + const theme = useMantineTheme(); + const { classes } = useStyles(); + const { colorScheme } = useMantineColorScheme(); + + const dashConfig = config.modules?.[DashdotModule.title] + .options as typeof DashdotModule['options']; + const isCompact = dashConfig?.useCompactView?.value ?? false; + const dashdotService = config.services.filter((service) => service.type === 'Dash.')[0]; + + const cpuEnabled = dashConfig?.showCpu?.value ?? true; + const storageEnabled = dashConfig?.showStorage?.value ?? true; + const ramEnabled = dashConfig?.showRam?.value ?? true; + const networkEnabled = dashConfig?.showNetwork?.value ?? true; + const gpuEnabled = dashConfig?.showGpu?.value ?? false; + + const info = useJson(dashdotService, '/info'); + const storageLoad = useJson(dashdotService, '/load/storage'); + + const totalUsed = + (storageLoad?.layout as any[])?.reduce((acc, curr) => (curr.load ?? 0) + acc, 0) ?? 0; + const totalSize = + (info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0; + + const graphs = [ + { + name: 'CPU', + enabled: cpuEnabled, + params: { + multiView: dashConfig?.cpuMultiView?.value ?? false, + }, + }, + { + name: 'Storage', + enabled: storageEnabled && !isCompact, + params: { + multiView: dashConfig?.storageMultiView?.value ?? false, + }, + }, + { + name: 'RAM', + enabled: ramEnabled, + }, + { + name: 'Network', + enabled: networkEnabled, + spanTwo: true, + }, + { + name: 'GPU', + enabled: gpuEnabled, + spanTwo: true, + }, + ].filter((g) => g.enabled); + + return ( +
+

Dash.

+ + {!dashdotService ? ( +

No dash. service found. Please add one to your Homarr dashboard.

+ ) : !info ? ( +

Cannot acquire information from dash. - are you running the latest version?

+ ) : ( +
+
+ {storageEnabled && isCompact && ( +
+

Storage:

+

+ {(totalUsed / (totalSize || 1)).toFixed(1)}%{'\n'} + {bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)} +

+
+ )} + +
+

Network:

+

+ {bpsPrettyPrint(info?.network?.speedUp)} Up{'\n'} + {bpsPrettyPrint(info?.network?.speedDown)} Down +

+
+
+ + {graphs.map((graph) => ( +
your docker containers