diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
index 8b973d92f..c83fcc939 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -22,13 +22,3 @@ body:
- High (App breaking feature)
validations:
required: true
- - type: checkboxes
- id: idiot-check
- attributes:
- label: Please tick the boxes
- description: Before submitting, please ensure that
- options:
- - label: You've read the [docs](https://github.com/ajnart/homarr#readme)
- required: true
- - label: You've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
- required: true
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..b826a6a46
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,28 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Next.js: debug server-side",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "yarn dev"
+ },
+ {
+ "name": "Next.js: debug client-side",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://localhost:3000"
+ },
+ {
+ "name": "Next.js: debug full stack",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "yarn dev",
+ "serverReadyAction": {
+ "pattern": "started server on .+, url: (https?://.+)",
+ "uriFormat": "%s",
+ "action": "debugWithChrome"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/configs/default.json b/data/configs/default.json
index ba6a91144..d159270a2 100644
--- a/data/configs/default.json
+++ b/data/configs/default.json
@@ -18,6 +18,9 @@
},
"Date": {
"enabled": false
+ },
+ "Docker": {
+ "enabled": true
}
}
}
\ No newline at end of file
diff --git a/data/constants.ts b/data/constants.ts
index 8054f1409..cc319bc33 100644
--- a/data/constants.ts
+++ b/data/constants.ts
@@ -1,2 +1,2 @@
export const REPO_URL = 'ajnart/homarr';
-export const CURRENT_VERSION = 'v0.8.0';
+export const CURRENT_VERSION = 'v0.8.2';
diff --git a/next.config.js b/next.config.js
index a6c9032b0..7344769da 100644
--- a/next.config.js
+++ b/next.config.js
@@ -6,9 +6,5 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
module.exports = withBundleAnalyzer({
reactStrictMode: false,
- eslint: {
- ignoreDuringBuilds: true,
- },
output: 'standalone',
- basePath: env.BASE_URL,
});
diff --git a/package.json b/package.json
index 6ef4d50e4..98042f379 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "homarr",
- "version": "0.8.0",
+ "version": "0.8.2",
"description": "Homarr - A homepage for your server.",
"repository": {
"type": "git",
@@ -25,66 +25,66 @@
},
"dependencies": {
"@ctrl/deluge": "^4.1.0",
- "@ctrl/qbittorrent": "^4.0.0",
- "@ctrl/shared-torrent": "^4.1.0",
+ "@ctrl/qbittorrent": "^4.1.0",
+ "@ctrl/shared-torrent": "^4.1.1",
"@ctrl/transmission": "^4.1.1",
- "@dnd-kit/core": "^6.0.1",
- "@dnd-kit/sortable": "^7.0.0",
+ "@dnd-kit/core": "^6.0.5",
+ "@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
- "@mantine/core": "^4.2.8",
- "@mantine/dates": "^4.2.8",
- "@mantine/dropzone": "^4.2.8",
- "@mantine/form": "^4.2.8",
- "@mantine/hooks": "^4.2.8",
- "@mantine/next": "^4.2.8",
- "@mantine/notifications": "^4.2.8",
- "@mantine/prism": "^4.2.8",
+ "@mantine/core": "^4.2.12",
+ "@mantine/dates": "^4.2.12",
+ "@mantine/dropzone": "^4.2.12",
+ "@mantine/form": "^4.2.12",
+ "@mantine/hooks": "^4.2.12",
+ "@mantine/next": "^4.2.12",
+ "@mantine/notifications": "^4.2.12",
+ "@mantine/prism": "^4.2.12",
"@nivo/core": "^0.79.0",
"@nivo/line": "^0.79.1",
- "@tabler/icons": "^1.68.0",
+ "@tabler/icons": "^1.76.0",
+ "add": "^2.0.6",
"axios": "^0.27.2",
"cookies-next": "^2.1.1",
- "dayjs": "^1.11.3",
+ "dayjs": "^1.11.4",
"dockerode": "^3.3.2",
- "framer-motion": "^6.3.1",
+ "framer-motion": "^6.5.1",
"js-file-download": "^0.4.12",
- "next": "^12.2.0",
- "prism-react-renderer": "^1.3.1",
- "react": "^17.0.1",
- "react-dom": "^17.0.1",
- "systeminformation": "^5.11.16",
- "uuid": "^8.3.2"
+ "next": "^12.2.3",
+ "prism-react-renderer": "^1.3.5",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "systeminformation": "^5.12.1",
+ "uuid": "^8.3.2",
+ "yarn": "^1.22.19"
},
"devDependencies": {
- "@babel/core": "^7.17.8",
- "@next/bundle-analyzer": "^12.2.0",
- "@next/eslint-plugin-next": "^12.2.0",
- "@storybook/react": "^6.5.4",
+ "@babel/core": "^7.18.9",
+ "@next/bundle-analyzer": "^12.2.3",
+ "@next/eslint-plugin-next": "^12.2.3",
+ "@storybook/react": "^6.5.9",
"@types/dockerode": "^3.3.9",
- "@types/node": "^17.0.23",
- "@types/react": "17.0.43",
+ "@types/node": "^18.0.6",
+ "@types/react": "^18.0.15",
"@types/uuid": "^8.3.4",
- "@typescript-eslint/eslint-plugin": "^5.16.0",
- "@typescript-eslint/parser": "^5.16.0",
- "eslint": "^8.11.0",
+ "@typescript-eslint/eslint-plugin": "^5.30.7",
+ "@typescript-eslint/parser": "^5.30.7",
+ "eslint": "^8.20.0",
"eslint-config-airbnb": "^19.0.4",
- "eslint-config-airbnb-typescript": "^16.1.0",
- "eslint-config-mantine": "1.1.0",
- "eslint-plugin-import": "^2.25.4",
- "eslint-plugin-jest": "^26.1.3",
- "eslint-plugin-jsx-a11y": "^6.5.1",
- "eslint-plugin-react": "^7.29.4",
- "eslint-plugin-react-hooks": "^4.3.0",
- "eslint-plugin-storybook": "^0.5.11",
- "eslint-plugin-testing-library": "^5.2.0",
+ "eslint-config-airbnb-typescript": "^17.0.0",
+ "eslint-config-mantine": "^2.0.0",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-jest": "^26.6.0",
+ "eslint-plugin-jsx-a11y": "^6.6.1",
+ "eslint-plugin-react": "^7.30.1",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-storybook": "^0.6.1",
+ "eslint-plugin-testing-library": "^5.5.1",
"eslint-plugin-unused-imports": "^2.0.0",
- "jest": "^28.1.0",
- "prettier": "^2.6.2",
+ "jest": "^28.1.3",
+ "prettier": "^2.7.1",
"require-from-string": "^2.0.2",
- "typescript": "4.6.4"
- },
- "resolutions": {
- "@types/react": "17.0.30"
+ "typescript": "^4.7.4",
+ "yarn-upgrade-all": "^0.7.1"
},
"packageManager": "yarn@3.2.1"
}
diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx
index c9024d47c..2d41feaee 100644
--- a/src/components/AppShelf/AddAppShelfItem.tsx
+++ b/src/components/AppShelf/AddAppShelfItem.tsx
@@ -12,7 +12,6 @@ import {
Select,
Switch,
Tabs,
- Text,
TextInput,
Title,
Tooltip,
@@ -23,7 +22,7 @@ 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 { tryMatchPort, ServiceTypeList, StatusCodes } from '../../tools/types';
import Tip from '../layout/Tip';
export function AddItemShelfButton(props: any) {
@@ -55,7 +54,8 @@ export function AddItemShelfButton(props: any) {
);
}
-function MatchIcon(name: string, form: any) {
+function MatchIcon(name: string | undefined, form: any) {
+ if (name === undefined || name === '') return null;
fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-')
@@ -77,24 +77,6 @@ function MatchService(name: string, form: any) {
}
}
-function MatchPort(name: string, form: any) {
- const portmap = [
- { name: 'qbittorrent', value: '8080' },
- { name: 'sonarr', value: '8989' },
- { name: 'radarr', value: '7878' },
- { name: 'lidarr', value: '8686' },
- { name: 'readarr', value: '8787' },
- { 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());
- if (port) {
- form.setFieldValue('url', `http://localhost:${port.value}`);
- }
-}
-
const DEFAULT_ICON = '/favicon.svg';
export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) {
@@ -154,7 +136,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
if (form.values.name !== debounced || form.values.icon !== DEFAULT_ICON) return;
MatchIcon(form.values.name, form);
MatchService(form.values.name, form);
- MatchPort(form.values.name, form);
+ tryMatchPort(form.values.name, form);
}, [debounced]);
// Try to set const hostname to new URL(form.values.url).hostname)
diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx
index abfed55cc..a47346830 100644
--- a/src/components/AppShelf/AppShelf.tsx
+++ b/src/components/AppShelf/AppShelf.tsx
@@ -178,21 +178,21 @@ const AppShelf = (props: any) => {
) : null}
{downloadEnabled ? (
-
-
+
-
-
-
-
+ }}
+ >
+
+
+
+
) : null}
diff --git a/src/components/Config/ConfigChanger.tsx b/src/components/Config/ConfigChanger.tsx
index a34846556..14e8cf5e5 100644
--- a/src/components/Config/ConfigChanger.tsx
+++ b/src/components/Config/ConfigChanger.tsx
@@ -1,5 +1,5 @@
import { Center, Loader, Select, Tooltip } from '@mantine/core';
-import { setCookies } from 'cookies-next';
+import { setCookie } from 'cookies-next';
import { useEffect, useState } from 'react';
import { useConfig } from '../../tools/state';
@@ -26,7 +26,7 @@ export default function ConfigChanger() {
label="Config loader"
onChange={(e) => {
loadConfig(e ?? 'default');
- setCookies('config-name', e ?? 'default', {
+ setCookie('config-name', e ?? 'default', {
maxAge: 60 * 60 * 24 * 30,
sameSite: 'strict',
});
diff --git a/src/components/Config/LoadConfig.tsx b/src/components/Config/LoadConfig.tsx
index e98550b6a..6935c1f72 100644
--- a/src/components/Config/LoadConfig.tsx
+++ b/src/components/Config/LoadConfig.tsx
@@ -10,7 +10,7 @@ import { DropzoneStatus, FullScreenDropzone } from '@mantine/dropzone';
import { showNotification } from '@mantine/notifications';
import { useRef } from 'react';
import { useRouter } from 'next/router';
-import { setCookies } from 'cookies-next';
+import { setCookie } from 'cookies-next';
import { useConfig } from '../../tools/state';
import { Config } from '../../tools/types';
import { migrateToIdConfig } from '../../tools/migrate';
@@ -90,7 +90,7 @@ export default function LoadConfigComponent(props: any) {
icon: ,
message: undefined,
});
- setCookies('config-name', newConfig.name, {
+ setCookie('config-name', newConfig.name, {
maxAge: 60 * 60 * 24 * 30,
sameSite: 'strict',
});
diff --git a/src/components/Docker/DockerMenu.tsx b/src/components/Docker/DockerMenu.tsx
deleted file mode 100644
index 6b9bdc8fe..000000000
--- a/src/components/Docker/DockerMenu.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { Menu, Text, useMantineTheme } from '@mantine/core';
-import { showNotification, updateNotification } from '@mantine/notifications';
-import {
- IconCheck,
- IconCodePlus,
- 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 default function DockerMenu(props: any) {
- const { container }: { container: Dockerode.ContainerInfo } = props;
- const theme = useMantineTheme();
- if (container === undefined) {
- return null;
- }
- return (
-
- );
-}
diff --git a/src/components/Docker/DockerTable.tsx b/src/components/Docker/DockerTable.tsx
deleted file mode 100644
index 2f7d6707b..000000000
--- a/src/components/Docker/DockerTable.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Table, Checkbox, Group, Badge, createStyles } from '@mantine/core';
-import Dockerode from 'dockerode';
-import ContainerState from './ContainerState';
-
-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 DockerTable({
- containers,
- selection,
- setSelection,
-}: {
- setSelection: any;
- containers: Dockerode.ContainerInfo[];
- selection: Dockerode.ContainerInfo[];
-}) {
- const { classes, cx } = useStyles();
-
- const toggleRow = (container: Dockerode.ContainerInfo) =>
- setSelection((current: Dockerode.ContainerInfo[]) =>
- current.includes(container) ? current.filter((c) => c !== container) : [...current, container]
- );
- const toggleAll = () =>
- setSelection((current: any) =>
- current.length === containers.length ? [] : containers.map((c) => c)
- );
-
- const rows = containers.map((element) => {
- const selected = selection.includes(element);
- return (
-
- |
- toggleRow(element)}
- transitionDuration={0}
- />
- |
- {element.Names[0].replace('/', '')} |
- {element.Image} |
-
-
- {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
- )}
-
- |
-
-
- |
-
- );
- });
-
- return (
-
- your docker containers
-
-
- |
- 0 && selection.length !== containers.length}
- transitionDuration={0}
- />
- |
- Name |
- Image |
- Ports |
- State |
-
-
- {rows}
-
- );
-}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index 3964e7a83..9c78905b9 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -1,26 +1,8 @@
-import {
- ActionIcon,
- Box,
- Burger,
- createStyles,
- Drawer,
- Group,
- Header as Head,
- ScrollArea,
- Title,
- Transition,
-} from '@mantine/core';
+import { Box, createStyles, Group, Header as Head } from '@mantine/core';
import { useBooleanToggle } from '@mantine/hooks';
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
-import {
- CalendarModule,
- DateModule,
- TotalDownloadsModule,
- WeatherModule,
- DashdotModule,
-} from '../modules';
-import { ModuleWrapper } from '../modules/moduleWrapper';
-import DockerDrawer from '../Docker/DockerDrawer';
+
+import DockerMenuButton from '../modules/docker/DockerModule';
import SearchBar from '../modules/search/SearchModule';
import { SettingsMenuButton } from '../Settings/SettingsMenu';
import { Logo } from './Logo';
@@ -53,51 +35,9 @@ export function Header(props: any) {
-
+
-
- {
- toggleHidden();
- toggleOpened();
- }}
- />
-
- Modules}
- opened
- onClose={() => {
- toggleHidden();
- }}
- >
- toggleOpened()}
- >
- {(styles) => (
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
index a40c6a801..0df7c4374 100644
--- a/src/components/layout/Layout.tsx
+++ b/src/components/layout/Layout.tsx
@@ -19,8 +19,8 @@ export default function Layout({ children, style }: any) {
return (
}
- navbar={widgetPosition ? : <>>}
- aside={widgetPosition ? <>> : }
+ navbar={widgetPosition ? : undefined}
+ aside={widgetPosition ? undefined : }
footer={}
>
diff --git a/src/components/layout/Logo.tsx b/src/components/layout/Logo.tsx
index 5e1476e7e..a4530a1de 100644
--- a/src/components/layout/Logo.tsx
+++ b/src/components/layout/Logo.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
import { useColorTheme } from '../../tools/color';
import { useConfig } from '../../tools/state';
-export function Logo({ style }: any) {
+export function Logo({ style, withoutText }: any) {
const { config } = useConfig();
const { primaryColor, secondaryColor } = useColorTheme();
@@ -17,26 +17,28 @@ export function Logo({ style }: any) {
position: 'relative',
}}
/>
-
-
- {config.settings.title || 'Homarr'}
-
-
+
+ {config.settings.title || 'Homarr'}
+
+
+ )}
);
}
diff --git a/src/components/layout/Widgets.tsx b/src/components/layout/Widgets.tsx
index 0eceae4c4..8e1418fa0 100644
--- a/src/components/layout/Widgets.tsx
+++ b/src/components/layout/Widgets.tsx
@@ -1,23 +1,16 @@
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) {
- const matches = useMediaQuery('(min-width: 800px)');
-
return (
- <>
- {matches && (
-
-
-
-
-
-
-
- )}
- >
+
+
+
+
+
+
+
);
}
diff --git a/src/components/modules/calendar/CalendarModule.tsx b/src/components/modules/calendar/CalendarModule.tsx
index 49624e0bc..f90784142 100644
--- a/src/components/modules/calendar/CalendarModule.tsx
+++ b/src/components/modules/calendar/CalendarModule.tsx
@@ -63,7 +63,7 @@ export default function CalendarComponent(props: any) {
if (!service || !service.apiKey) {
return Promise.resolve({ data: [] });
}
- return axios.post(`/api/modules/calendar?type=${type}`, { ...service });
+ return axios.post(`/api/modules/calendar?type=${type}`, { id: service.id });
}
useEffect(() => {
diff --git a/src/components/modules/dash./DashdotModule.tsx b/src/components/modules/dash./DashdotModule.tsx
index ff6175629..8afb5ec62 100644
--- a/src/components/modules/dash./DashdotModule.tsx
+++ b/src/components/modules/dash./DashdotModule.tsx
@@ -181,7 +181,7 @@ export function DashdotComponent() {
Storage:
- {(totalUsed / (totalSize || 1)).toFixed(1)}%{'\n'}
+ {((100 * totalUsed) / (totalSize || 1)).toFixed(1)}%{'\n'}
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
diff --git a/src/components/Docker/ContainerActionBar.tsx b/src/components/modules/docker/ContainerActionBar.tsx
similarity index 53%
rename from src/components/Docker/ContainerActionBar.tsx
rename to src/components/modules/docker/ContainerActionBar.tsx
index b4c17dd47..39461472d 100644
--- a/src/components/Docker/ContainerActionBar.tsx
+++ b/src/components/modules/docker/ContainerActionBar.tsx
@@ -9,46 +9,49 @@ import {
IconRefresh,
IconRotateClockwise,
IconTrash,
- IconX,
} from '@tabler/icons';
import axios from 'axios';
import Dockerode from 'dockerode';
-import { tryMatchService } from '../../tools/addToHomarr';
-import { useConfig } from '../../tools/state';
-import { AddAppShelfItemForm } from '../AppShelf/AddAppShelfItem';
+import { tryMatchService } from '../../../tools/addToHomarr';
+import { AddAppShelfItemForm } from '../../AppShelf/AddAppShelfItem';
-function sendDockerCommand(action: string, containerId: string, containerName: string) {
+function sendDockerCommand(
+ action: string,
+ containerId: string,
+ containerName: string,
+ reload: () => void
+) {
showNotification({
id: containerId,
loading: true,
- title: `${action}ing container ${containerName.substring(1)}`,
+ title: `${action}ing container ${containerName}`,
message: undefined,
autoClose: false,
disallowClose: true,
});
- axios.get(`/api/docker/container/${containerId}?action=${action}`).then((res) => {
- setTimeout(() => {
- if (res.data.success === true) {
- updateNotification({
- id: containerId,
- title: `Container ${containerName} ${action}ed`,
- message: `Your container was successfully ${action}ed`,
- icon: ,
- autoClose: 2000,
- });
- }
- if (res.data.success === false) {
- updateNotification({
- id: containerId,
- color: 'red',
- title: 'There was an error with your container.',
- message: undefined,
- icon: ,
- autoClose: 2000,
- });
- }
- }, 500);
- });
+ axios
+ .get(`/api/docker/container/${containerId}?action=${action}`)
+ .then((res) => {
+ updateNotification({
+ id: containerId,
+ title: `Container ${containerName} ${action}ed`,
+ message: `Your container was successfully ${action}ed`,
+ icon: ,
+ autoClose: 2000,
+ });
+ })
+ .catch((err) => {
+ updateNotification({
+ id: containerId,
+ color: 'red',
+ title: 'There was an error',
+ message: err.response.data.reason,
+ autoClose: 2000,
+ });
+ })
+ .finally(() => {
+ reload();
+ });
}
export interface ContainerActionBarProps {
@@ -57,7 +60,6 @@ export interface ContainerActionBarProps {
}
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
- const { config, setConfig } = useConfig();
const [opened, setOpened] = useBooleanToggle(false);
return (
@@ -79,9 +81,9 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
onClick={() =>
Promise.all(
selected.map((container) =>
- sendDockerCommand('restart', container.Id, container.Names[0].substring(1))
+ sendDockerCommand('restart', container.Id, container.Names[0].substring(1), reload)
)
- ).then(() => reload())
+ )
}
variant="light"
color="orange"
@@ -93,22 +95,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
leftIcon={}
onClick={() =>
Promise.all(
- selected.map((container) => {
- if (
- container.State === 'stopped' ||
- container.State === 'created' ||
- container.State === 'exited'
- ) {
- return showNotification({
- id: container.Id,
- title: `Failed to stop ${container.Names[0].substring(1)}`,
- message: "You can't stop a stopped container",
- autoClose: 1000,
- });
- }
- return sendDockerCommand('stop', container.Id, container.Names[0].substring(1));
- })
- ).then(() => reload())
+ selected.map((container) =>
+ sendDockerCommand('stop', container.Id, container.Names[0].substring(1), reload)
+ )
+ )
}
variant="light"
color="red"
@@ -121,9 +111,9 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
onClick={() =>
Promise.all(
selected.map((container) =>
- sendDockerCommand('start', container.Id, container.Names[0].substring(1))
+ sendDockerCommand('start', container.Id, container.Names[0].substring(1), reload)
)
- ).then(() => reload())
+ )
}
variant="light"
color="green"
@@ -143,7 +133,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
if (selected.length !== 1) {
showNotification({
autoClose: 5000,
- title: Please only add one service at a time!,
+ title: Please only add one service at a time!,
color: 'red',
message: undefined,
});
@@ -161,18 +151,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
radius="md"
onClick={() =>
Promise.all(
- selected.map((container) => {
- if (container.State === 'running') {
- return showNotification({
- id: container.Id,
- title: `Failed to delete ${container.Names[0].substring(1)}`,
- message: "You can't delete a running container",
- autoClose: 1000,
- });
- }
- return sendDockerCommand('remove', container.Id, container.Names[0].substring(1));
- })
- ).then(() => reload())
+ selected.map((container) =>
+ sendDockerCommand('remove', container.Id, container.Names[0].substring(1), reload)
+ )
+ )
}
>
Remove
diff --git a/src/components/Docker/ContainerState.tsx b/src/components/modules/docker/ContainerState.tsx
similarity index 100%
rename from src/components/Docker/ContainerState.tsx
rename to src/components/modules/docker/ContainerState.tsx
diff --git a/src/components/Docker/DockerDrawer.tsx b/src/components/modules/docker/DockerModule.tsx
similarity index 54%
rename from src/components/Docker/DockerDrawer.tsx
rename to src/components/modules/docker/DockerModule.tsx
index 678990cbc..993182398 100644
--- a/src/components/Docker/DockerDrawer.tsx
+++ b/src/components/modules/docker/DockerModule.tsx
@@ -1,31 +1,58 @@
-import { ActionIcon, Drawer, Group, LoadingOverlay } from '@mantine/core';
-import { IconBrandDocker } from '@tabler/icons';
+import { ActionIcon, Drawer, Group, LoadingOverlay, Text } from '@mantine/core';
import axios from 'axios';
import { useEffect, useState } from 'react';
import Docker from 'dockerode';
+import { IconBrandDocker, IconX } from '@tabler/icons';
+import { showNotification } from '@mantine/notifications';
import ContainerActionBar from './ContainerActionBar';
import DockerTable from './DockerTable';
+import { useConfig } from '../../../tools/state';
+import { IModule } from '../modules';
-export default function DockerDrawer(props: any) {
+export const DockerModule: IModule = {
+ title: 'Docker',
+ description: 'Allows you to easily manage your torrents',
+ icon: IconBrandDocker,
+ component: DockerMenuButton,
+};
+
+export default function DockerMenuButton(props: any) {
const [opened, setOpened] = useState(false);
const [containers, setContainers] = useState([]);
const [selection, setSelection] = useState([]);
const [visible, setVisible] = useState(false);
-
- function reload() {
- setVisible(true);
- setTimeout(() => {
- axios.get('/api/docker/containers').then((res) => {
- setContainers(res.data);
- setSelection([]);
- setVisible(false);
- });
- }, 300);
- }
+ const { config } = useConfig();
useEffect(() => {
reload();
}, []);
+
+ function reload() {
+ setVisible(true);
+ setTimeout(() => {
+ axios
+ .get('/api/docker/containers')
+ .then((res) => {
+ setContainers(res.data);
+ setSelection([]);
+ setVisible(false);
+ })
+ .catch(() =>
+ // Send an Error notification
+ showNotification({
+ autoClose: 1500,
+ title: Docker integration failed,
+ color: 'red',
+ icon: ,
+ message: 'Did you forget to mount the docker socket ?',
+ })
+ );
+ }, 300);
+ }
+ const exists = config.modules?.[DockerModule.title]?.enabled ?? false;
+ if (!exists) {
+ return null;
+ }
// Check if the user has at least one container
if (containers.length < 1) return null;
return (
diff --git a/src/components/modules/docker/DockerTable.tsx b/src/components/modules/docker/DockerTable.tsx
new file mode 100644
index 000000000..c8cf76f22
--- /dev/null
+++ b/src/components/modules/docker/DockerTable.tsx
@@ -0,0 +1,125 @@
+import { Table, Checkbox, Group, Badge, createStyles, ScrollArea, TextInput } from '@mantine/core';
+import { IconSearch } from '@tabler/icons';
+import Dockerode from 'dockerode';
+import { useEffect, useState } from 'react';
+import ContainerState from './ContainerState';
+
+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 DockerTable({
+ containers,
+ selection,
+ setSelection,
+}: {
+ setSelection: any;
+ containers: Dockerode.ContainerInfo[];
+ selection: Dockerode.ContainerInfo[];
+}) {
+ const [usedContainers, setContainers] = useState(containers);
+ const { classes, cx } = useStyles();
+ const [search, setSearch] = useState('');
+
+ useEffect(() => {
+ setContainers(containers);
+ }, [containers]);
+
+ const handleSearchChange = (event: React.ChangeEvent) => {
+ const { value } = event.currentTarget;
+ setSearch(value);
+ setContainers(filterContainers(containers, value));
+ };
+
+ function filterContainers(data: Dockerode.ContainerInfo[], search: string) {
+ const query = search.toLowerCase().trim();
+ return data.filter((item) =>
+ item.Names.some((name) => name.toLowerCase().includes(query) || item.Image.includes(query))
+ );
+ }
+
+ const toggleRow = (container: Dockerode.ContainerInfo) =>
+ setSelection((current: Dockerode.ContainerInfo[]) =>
+ current.includes(container) ? current.filter((c) => c !== container) : [...current, container]
+ );
+ const toggleAll = () =>
+ setSelection((current: any) =>
+ current.length === usedContainers.length ? [] : usedContainers.map((c) => c)
+ );
+
+ const rows = usedContainers.map((element) => {
+ const selected = selection.includes(element);
+ return (
+
+ |
+ toggleRow(element)}
+ transitionDuration={0}
+ />
+ |
+ {element.Names[0].replace('/', '')} |
+ {element.Image} |
+
+
+ {element.Ports.sort((a, b) => a.PrivatePort - b.PrivatePort)
+ // Remove duplicates with filter function
+ .filter(
+ (port, index, self) =>
+ index === self.findIndex((t) => t.PrivatePort === port.PrivatePort)
+ )
+ .slice(-3)
+ .map((port) => (
+
+ {port.PrivatePort}:{port.PublicPort}
+
+ ))}
+ {element.Ports.length > 3 && (
+ {element.Ports.length - 3} more
+ )}
+
+ |
+
+
+ |
+
+ );
+ });
+
+ return (
+
+ }
+ value={search}
+ onChange={handleSearchChange}
+ />
+
+ your docker containers
+
+
+ |
+ 0 && selection.length !== usedContainers.length}
+ transitionDuration={0}
+ />
+ |
+ Name |
+ Image |
+ Ports |
+ State |
+
+
+ {rows}
+
+
+ );
+}
diff --git a/src/components/modules/docker/index.ts b/src/components/modules/docker/index.ts
new file mode 100644
index 000000000..8e0a617dc
--- /dev/null
+++ b/src/components/modules/docker/index.ts
@@ -0,0 +1 @@
+export { DockerModule } from './DockerModule';
diff --git a/src/components/modules/downloads/DownloadsModule.tsx b/src/components/modules/downloads/DownloadsModule.tsx
index 03eefef29..83137dd00 100644
--- a/src/components/modules/downloads/DownloadsModule.tsx
+++ b/src/components/modules/downloads/DownloadsModule.tsx
@@ -15,6 +15,7 @@ import { useEffect, useState } from 'react';
import axios from 'axios';
import { NormalizedTorrent } from '@ctrl/shared-torrent';
import { useViewportSize } from '@mantine/hooks';
+import { showNotification } from '@mantine/notifications';
import { IModule } from '../modules';
import { useConfig } from '../../../tools/state';
import { AddItemShelfButton } from '../../AppShelf/AddAppShelfItem';
@@ -52,12 +53,30 @@ export default function DownloadComponent() {
useEffect(() => {
setIsLoading(true);
if (downloadServices.length === 0) return;
- setSafeInterval(() => {
+ const interval = setInterval(() => {
// Send one request with each download service inside
- axios.post('/api/modules/downloads', { config }).then((response) => {
- setTorrents(response.data);
- setIsLoading(false);
- });
+ axios
+ .post('/api/modules/downloads')
+ .then((response) => {
+ setTorrents(response.data);
+ setIsLoading(false);
+ })
+ .catch((error) => {
+ setTorrents([]);
+ // eslint-disable-next-line no-console
+ console.error('Error while fetching torrents', error.response.data);
+ setIsLoading(false);
+ showNotification({
+ title: 'Error fetching torrents',
+ autoClose: 1000,
+ disallowClose: true,
+ id: 'fail-torrent-downloads-module',
+ color: 'red',
+ message:
+ 'Please check your config for any potential errors, check the console for more info',
+ });
+ clearInterval(interval);
+ });
}, 5000);
}, []);
diff --git a/src/components/modules/downloads/TotalDownloadsModule.tsx b/src/components/modules/downloads/TotalDownloadsModule.tsx
index 55f9be34d..ab25947dc 100644
--- a/src/components/modules/downloads/TotalDownloadsModule.tsx
+++ b/src/components/modules/downloads/TotalDownloadsModule.tsx
@@ -6,6 +6,7 @@ import { NormalizedTorrent } from '@ctrl/shared-torrent';
import { linearGradientDef } from '@nivo/core';
import { Datum, ResponsiveLine } from '@nivo/line';
import { useListState } from '@mantine/hooks';
+import { showNotification } from '@mantine/notifications';
import { AddItemShelfButton } from '../../AppShelf/AddAppShelfItem';
import { useConfig } from '../../../tools/state';
import { humanFileSize } from '../../../tools/humanFileSize';
@@ -42,11 +43,28 @@ export default function TotalDownloadsComponent() {
const totalDownloadSpeed = torrents.reduce((acc, torrent) => acc + torrent.downloadSpeed, 0);
const totalUploadSpeed = torrents.reduce((acc, torrent) => acc + torrent.uploadSpeed, 0);
useEffect(() => {
- if (downloadServices.length === 0) return;
- setSafeInterval(() => {
- axios.post('/api/modules/downloads', { config }).then((response) => {
- setTorrents(response.data);
- });
+ const interval = setSafeInterval(() => {
+ // Send one request with each download service inside
+ axios
+ .post('/api/modules/downloads')
+ .then((response) => {
+ setTorrents(response.data);
+ })
+ .catch((error) => {
+ setTorrents([]);
+ // eslint-disable-next-line no-console
+ console.error('Error while fetching torrents', error.response.data);
+ showNotification({
+ title: 'Torrent speed module failed to fetch torrents',
+ autoClose: 1000,
+ disallowClose: true,
+ id: 'fail-torrent-speed-module',
+ color: 'red',
+ message:
+ 'Error fetching torrents, please check your config for any potential errors, check the console for more info',
+ });
+ clearInterval(interval);
+ });
}, 1000);
}, [config.services]);
diff --git a/src/components/modules/index.ts b/src/components/modules/index.ts
index 85b4b765b..d0b89acad 100644
--- a/src/components/modules/index.ts
+++ b/src/components/modules/index.ts
@@ -5,3 +5,4 @@ export * from './downloads';
export * from './ping';
export * from './search';
export * from './weather';
+export * from './docker';
diff --git a/src/components/modules/modules.tsx b/src/components/modules/modules.tsx
index e1303ae33..5ba0c6373 100644
--- a/src/components/modules/modules.tsx
+++ b/src/components/modules/modules.tsx
@@ -1,11 +1,14 @@
// This interface is to be used in all the modules of the project
// Each module should have its own interface and call the following function:
// TODO: Add a function to register a module
+
+import { TablerIcon } from '@tabler/icons';
+
// Note: Maybe use context to keep track of the modules
export interface IModule {
title: string;
description: string;
- icon: React.ReactNode;
+ icon: TablerIcon;
component: React.ComponentType;
options?: Option;
}
diff --git a/src/components/modules/search/SearchModule.tsx b/src/components/modules/search/SearchModule.tsx
index eae9b52ca..75a206097 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, Tooltip } from '@mantine/core';
+import { Kbd, createStyles, Autocomplete } from '@mantine/core';
import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks';
import { useEffect, useRef, useState } from 'react';
import {
diff --git a/src/components/modules/weather/WeatherModule.tsx b/src/components/modules/weather/WeatherModule.tsx
index 36393c677..913ae9f8a 100644
--- a/src/components/modules/weather/WeatherModule.tsx
+++ b/src/components/modules/weather/WeatherModule.tsx
@@ -157,7 +157,7 @@ export default function WeatherComponent(props: any) {
});
}, [cityInput]);
if (!weather.current_weather) {
- return (
+ return (
<>
diff --git a/src/middleware.ts b/src/middleware.ts
index b8d9cd942..e3720397e 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,15 +1,15 @@
-import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
-export function middleware(req: NextRequest, ev: NextFetchEvent) {
- const ok = req.cookies.get('password') === process.env.PASSWORD;
- const url = req.nextUrl.clone();
+// eslint-disable-next-line consistent-return
+export function middleware(request: NextRequest) {
+ const cookie = request.cookies.get('password');
+ const isPasswordCorrect = cookie === process.env.PASSWORD;
if (
- !ok &&
- url.pathname !== '/login' &&
- process.env.PASSWORD &&
- url.pathname !== '/api/configs/tryPassword'
+ !isPasswordCorrect &&
+ request.nextUrl.pathname !== '/login' &&
+ request.nextUrl.pathname !== '/api/configs/trylogin'
) {
- url.pathname = '/login';
+ return NextResponse.rewrite(new URL('/login', request.url));
}
- return NextResponse.rewrite(url);
}
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index a98ece27d..b0d0b5de1 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -58,7 +58,7 @@ const useStyles = createStyles((theme) => ({
},
}));
-export function Illustration(props: React.ComponentPropsWithoutRef<'svg'>) {
+function Illustration(props: React.ComponentPropsWithoutRef<'svg'>) {
const theme = useMantineTheme();
return (