diff --git a/package.json b/package.json index 46b61bcb0..d3519c067 100644 --- a/package.json +++ b/package.json @@ -1,101 +1,102 @@ { - "name": "homarr", - "version": "0.9.2", - "description": "Homarr - A homepage for your server.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/ajnart/homarr" - }, - "scripts": { - "dev": "next dev", - "build": "next build", - "analyze": "ANALYZE=true next build", - "start": "next start", - "typecheck": "tsc --noEmit", - "export": "next build && next export", - "lint": "next lint", - "jest": "jest", - "jest:watch": "jest --watch", - "prettier:check": "prettier --check \"**/*.{ts,tsx}\"", - "prettier:write": "prettier --write \"**/*.{ts,tsx}\"", - "test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest", - "ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write" - }, - "dependencies": { - "@ctrl/deluge": "^4.1.0", - "@ctrl/qbittorrent": "^4.1.0", - "@ctrl/shared-torrent": "^4.1.1", - "@ctrl/transmission": "^4.1.1", - "@dnd-kit/core": "^6.0.5", - "@dnd-kit/sortable": "^7.0.1", - "@dnd-kit/utilities": "^3.2.0", - "@emotion/react": "^11.10.0", - "@emotion/server": "^11.10.0", - "@mantine/carousel": "^5.1.0", - "@mantine/core": "^5.2.3", - "@mantine/dates": "^5.2.3", - "@mantine/dropzone": "^5.2.3", - "@mantine/form": "^5.2.3", - "@mantine/hooks": "^5.2.3", - "@mantine/modals": "^5.2.3", - "@mantine/next": "^5.2.3", - "@mantine/notifications": "^5.2.3", - "@mantine/prism": "^5.0.0", - "@nivo/core": "^0.79.0", - "@nivo/line": "^0.79.1", - "@tabler/icons": "^1.78.0", - "add": "^2.0.6", - "axios": "^0.27.2", - "consola": "^2.15.3", - "cookies-next": "^2.1.1", - "country-flag-icons": "^1.5.5", - "dayjs": "^1.11.5", - "dockerode": "^3.3.2", - "embla-carousel-react": "^7.0.0", - "framer-motion": "^6.5.1", - "i18next": "^21.9.1", - "i18next-browser-languagedetector": "^6.1.5", - "i18next-http-backend": "^1.4.1", - "js-file-download": "^0.4.12", - "next": "12.1.6", - "next-i18next": "^11.3.0", - "prism-react-renderer": "^1.3.5", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sabnzbd-api": "^1.5.0", - "sharp": "^0.30.7", - "systeminformation": "^5.12.1", - "uuid": "^8.3.2", - "yarn": "^1.22.19" - }, - "devDependencies": { - "@next/bundle-analyzer": "^12.1.4", - "@next/eslint-plugin-next": "^12.1.4", - "@types/dockerode": "^3.3.9", - "@types/node": "17.0.1", - "@types/react": "17.0.1", - "@types/uuid": "^8.3.4", - "@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": "^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-testing-library": "^5.5.1", - "eslint-plugin-unused-imports": "^2.0.0", - "jest": "^28.1.3", - "prettier": "^2.7.1", - "typescript": "^4.7.4" - }, - "resolutions": { - "@types/react": "17.0.2", - "@types/react-dom": "17.0.2" - }, - "packageManager": "yarn@3.2.1" + "name": "homarr", + "version": "0.9.2", + "description": "Homarr - A homepage for your server.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/ajnart/homarr" + }, + "scripts": { + "dev": "next dev", + "build": "next build", + "analyze": "ANALYZE=true next build", + "start": "next start", + "typecheck": "tsc --noEmit", + "export": "next build && next export", + "lint": "next lint", + "jest": "jest", + "jest:watch": "jest --watch", + "prettier:check": "prettier --check \"**/*.{ts,tsx}\"", + "prettier:write": "prettier --write \"**/*.{ts,tsx}\"", + "test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest", + "ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write" + }, + "dependencies": { + "@ctrl/deluge": "^4.1.0", + "@ctrl/qbittorrent": "^4.1.0", + "@ctrl/shared-torrent": "^4.1.1", + "@ctrl/transmission": "^4.1.1", + "@dnd-kit/core": "^6.0.5", + "@dnd-kit/sortable": "^7.0.1", + "@dnd-kit/utilities": "^3.2.0", + "@emotion/react": "^11.10.0", + "@emotion/server": "^11.10.0", + "@mantine/carousel": "^5.1.0", + "@mantine/core": "^5.2.3", + "@mantine/dates": "^5.2.3", + "@mantine/dropzone": "^5.2.3", + "@mantine/form": "^5.2.3", + "@mantine/hooks": "^5.2.3", + "@mantine/modals": "^5.2.3", + "@mantine/next": "^5.2.3", + "@mantine/notifications": "^5.2.3", + "@mantine/prism": "^5.0.0", + "@nivo/core": "^0.79.0", + "@nivo/line": "^0.79.1", + "@tabler/icons": "^1.78.0", + "@tanstack/react-query": "^4.2.1", + "add": "^2.0.6", + "axios": "^0.27.2", + "consola": "^2.15.3", + "cookies-next": "^2.1.1", + "country-flag-icons": "^1.5.5", + "dayjs": "^1.11.5", + "dockerode": "^3.3.2", + "embla-carousel-react": "^7.0.0", + "framer-motion": "^6.5.1", + "i18next": "^21.9.1", + "i18next-browser-languagedetector": "^6.1.5", + "i18next-http-backend": "^1.4.1", + "js-file-download": "^0.4.12", + "next": "12.1.6", + "next-i18next": "^11.3.0", + "prism-react-renderer": "^1.3.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sabnzbd-api": "^1.5.0", + "sharp": "^0.30.7", + "systeminformation": "^5.12.1", + "uuid": "^8.3.2", + "yarn": "^1.22.19" + }, + "devDependencies": { + "@next/bundle-analyzer": "^12.1.4", + "@next/eslint-plugin-next": "^12.1.4", + "@types/dockerode": "^3.3.9", + "@types/node": "17.0.1", + "@types/react": "17.0.1", + "@types/uuid": "^8.3.4", + "@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": "^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-testing-library": "^5.5.1", + "eslint-plugin-unused-imports": "^2.0.0", + "jest": "^28.1.3", + "prettier": "^2.7.1", + "typescript": "^4.7.4" + }, + "resolutions": { + "@types/react": "17.0.2", + "@types/react-dom": "17.0.2" + }, + "packageManager": "yarn@3.2.1" } diff --git a/src/modules/usenet/UsenetHistoryList.tsx b/src/modules/usenet/UsenetHistoryList.tsx new file mode 100644 index 000000000..c2b8e8da5 --- /dev/null +++ b/src/modules/usenet/UsenetHistoryList.tsx @@ -0,0 +1,71 @@ +import { Center, Table, Text, Title, Tooltip, useMantineTheme } from '@mantine/core'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { FunctionComponent } from 'react'; +import { humanFileSize } from '../../tools/humanFileSize'; +import { UsenetHistoryItem } from './types'; + +dayjs.extend(duration); + +interface UsenetHistoryListProps { + items: UsenetHistoryItem[]; +} + +export const UsenetHistoryList: FunctionComponent = ({ items }) => { + const theme = useMantineTheme(); + + if (items.length <= 0) { + return ( +
+ Queue is empty +
+ ); + } + + return ( + + + + + + + + + + + + + + + {items.map((history) => ( + + + + + + ))} + +
NameSizeDownload Duration
+ + + {history.name} + + + + {humanFileSize(history.size)} + + + {dayjs + .duration(history.time, 's') + .format(history.time < 60 ? 's [seconds]' : 'm [minutes] s [seconds] ')} + +
+ ); +}; diff --git a/src/modules/usenet/UsenetModule.tsx b/src/modules/usenet/UsenetModule.tsx index 63ec02134..8fae8b3e3 100644 --- a/src/modules/usenet/UsenetModule.tsx +++ b/src/modules/usenet/UsenetModule.tsx @@ -1,115 +1,16 @@ -import { - Center, - Progress, - ScrollArea, - Skeleton, - Table, - Tabs, - Text, - Title, - Tooltip, -} from '@mantine/core'; -import { showNotification } from '@mantine/notifications'; -import { IconDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons'; -import axios from 'axios'; -import dayjs from 'dayjs'; -import { FunctionComponent, useEffect, useState } from 'react'; -import duration from 'dayjs/plugin/duration'; -import { humanFileSize } from '../../tools/humanFileSize'; -import { DownloadItem } from '../../tools/types'; -import { IModule } from '../ModuleTypes'; +import { Skeleton, Tabs, useMantineTheme } from '@mantine/core'; +import { IconDownload } from '@tabler/icons'; +import { FunctionComponent } from 'react'; -dayjs.extend(duration); +import { IModule } from '../ModuleTypes'; +import { useGetUsenetDownloads, useGetUsenetHistory } from '../../tools/hooks/api'; +import { UsenetQueueList } from './UsenetQueueList'; +import { UsenetHistoryList } from './UsenetHistoryList'; export const UsenetComponent: FunctionComponent = () => { - const [nzbs, setNzbs] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - setIsLoading(true); - - const getData = async () => { - try { - const response = await axios.get('/api/modules/usenet'); - setNzbs(response.data); - } catch (error) { - setNzbs([]); - 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', - }); - } finally { - setIsLoading(false); - } - }; - - const interval = setInterval(getData, 5000); - getData(); - - () => { - clearInterval(interval); - }; - }, []); - - const ths = ( - - - Name - Size - ETA - Progress - - ); - - const rows = nzbs.map((nzb) => ( - - - {nzb.state === 'paused' ? ( - - ) : ( - - )} - - - - - {nzb.name} - - - - - {humanFileSize(nzb.size * 1000 * 1000)} - - - {nzb.eta <= 0 ? ( - - Paused - - ) : ( - {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} - )} - - - {nzb.progress.toFixed(1)}% - - - - )); + const theme = useMantineTheme(); + const { isLoading, data: nzbs = [] } = useGetUsenetDownloads(); + const { data: history = [] } = useGetUsenetHistory(); if (isLoading) { return ( @@ -128,18 +29,10 @@ export const UsenetComponent: FunctionComponent = () => { History - - {rows.length > 0 ? ( - - {ths} - {rows} -
- ) : ( -
- Queue is empty -
- )} -
+ +
+ + ); diff --git a/src/modules/usenet/UsenetQueueList.tsx b/src/modules/usenet/UsenetQueueList.tsx new file mode 100644 index 000000000..3d7a4e205 --- /dev/null +++ b/src/modules/usenet/UsenetQueueList.tsx @@ -0,0 +1,87 @@ +import { Center, Progress, Table, Text, Title, Tooltip, useMantineTheme } from '@mantine/core'; +import { IconPlayerPause, IconPlayerPlay } from '@tabler/icons'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { FunctionComponent } from 'react'; +import { humanFileSize } from '../../tools/humanFileSize'; +import { UsenetQueueItem } from './types'; + +dayjs.extend(duration); +interface UsenetQueueListProps { + items: UsenetQueueItem[]; +} + +export const UsenetQueueList: FunctionComponent = ({ items }) => { + const theme = useMantineTheme(); + + if (items.length <= 0) { + return ( +
+ Queue is empty +
+ ); + } + + return ( + + + + + + + + + + + {items.map((nzb) => ( + + + + + + + + ))} + +
+ NameSizeETAProgress
+ {nzb.state === 'paused' ? ( + + ) : ( + + )} + + + + {nzb.name} + + + + {humanFileSize(nzb.size)} + + {nzb.eta <= 0 ? ( + + Paused + + ) : ( + {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} + )} + + {nzb.progress.toFixed(1)}% + 0 ? theme.primaryColor : 'lightgrey'} + value={nzb.progress} + size="lg" + style={{ width: '100%' }} + /> +
+ ); +}; diff --git a/src/modules/usenet/types.ts b/src/modules/usenet/types.ts index 88831e8ce..a4dd8c5a0 100644 --- a/src/modules/usenet/types.ts +++ b/src/modules/usenet/types.ts @@ -1,6 +1,9 @@ export interface UsenetQueueItem { name: string; progress: number; + /** + * Size in bytes + */ size: number; id: string; state: 'paused' | 'downloading' | 'queued'; @@ -8,6 +11,10 @@ export interface UsenetQueueItem { } export interface UsenetHistoryItem { name: string; + /** + * Size in bytes + */ size: number; id: string; + time: number; } diff --git a/src/pages/api/modules/usenet/history.ts b/src/pages/api/modules/usenet/history.ts index 23b50f476..4f418be68 100644 --- a/src/pages/api/modules/usenet/history.ts +++ b/src/pages/api/modules/usenet/history.ts @@ -28,7 +28,8 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { history.push({ id: slot.nzo_id, name: slot.name, - size: slot.bytes * 1000, + size: slot.bytes, + time: slot.download_time, }); }); }) diff --git a/src/pages/api/modules/usenet/index.ts b/src/pages/api/modules/usenet/index.ts index 9c3565098..cedb154e8 100644 --- a/src/pages/api/modules/usenet/index.ts +++ b/src/pages/api/modules/usenet/index.ts @@ -36,7 +36,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { eta: eta.asSeconds(), name: slot.filename, progress: parseFloat(slot.percentage), - size: parseFloat(slot.mb), + size: parseFloat(slot.mb) * 1000 * 1000, state: slot.status.toLowerCase() as any, }); }); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 604ff91a8..70ae411da 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,6 +3,8 @@ import { GetServerSidePropsContext } from 'next'; import { useEffect } from 'react'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + import AppShelf from '../components/AppShelf/AppShelf'; import LoadConfigComponent from '../components/Config/LoadConfig'; import { Config } from '../tools/types'; @@ -62,6 +64,8 @@ export async function getServerSideProps({ return getConfig(configName as string, translations); } +const queryClient = new QueryClient(); + export default function HomePage(props: any) { const { config: initialConfig }: { config: Config } = props; const { setConfig } = useConfig(); @@ -73,9 +77,11 @@ export default function HomePage(props: any) { setConfig(migratedConfig); }, [initialConfig]); return ( - - - - + + + + + + ); } diff --git a/src/tools/hooks/api.ts b/src/tools/hooks/api.ts new file mode 100644 index 000000000..305888ce0 --- /dev/null +++ b/src/tools/hooks/api.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import { UsenetHistoryItem, UsenetQueueItem } from '../../modules'; + +export const useGetUsenetDownloads = () => + useQuery( + ['usenetDownloads'], + async () => (await axios.get('/api/modules/usenet')).data, + { + refetchInterval: 1000, + } + ); + +export const useGetUsenetHistory = () => + useQuery( + ['usenetHistory'], + async () => (await axios.get('/api/modules/usenet/history')).data, + { + refetchInterval: 1000, + } + ); diff --git a/yarn.lock b/yarn.lock index 905d3b641..95c494a5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,6 +1918,33 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:^4.0.0-beta.1": + version: 4.2.1 + resolution: "@tanstack/query-core@npm:4.2.1" + checksum: f71854969e02de6c2cfbe25e8b11e275b61e1297a902e0d5c4beac580a87db99555c1c21d536d838ce5e0664bc49da7b60a3c6b8de334c7004c5005fe2a48030 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^4.2.1": + version: 4.2.1 + resolution: "@tanstack/react-query@npm:4.2.1" + dependencies: + "@tanstack/query-core": ^4.0.0-beta.1 + "@types/use-sync-external-store": ^0.0.3 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: "*" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: bbf3a808645c26c649971dc182bb9a7ed7a1d89f6456b60685c6081b8be6ae84ae83b39c7eacb96c4f3b6677ca001d8114037329951987b7a8d65de53b28c862 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -2163,6 +2190,13 @@ __metadata: languageName: node linkType: hard +"@types/use-sync-external-store@npm:^0.0.3": + version: 0.0.3 + resolution: "@types/use-sync-external-store@npm:0.0.3" + checksum: 161ddb8eec5dbe7279ac971531217e9af6b99f7783213566d2b502e2e2378ea19cf5e5ea4595039d730aa79d3d35c6567d48599f69773a02ffcff1776ec2a44e + languageName: node + linkType: hard + "@types/uuid@npm:^8.3.4": version: 8.3.4 resolution: "@types/uuid@npm:8.3.4" @@ -4775,6 +4809,7 @@ __metadata: "@nivo/core": ^0.79.0 "@nivo/line": ^0.79.1 "@tabler/icons": ^1.78.0 + "@tanstack/react-query": ^4.2.1 "@types/dockerode": ^3.3.9 "@types/node": 17.0.1 "@types/react": 17.0.1 @@ -8164,6 +8199,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2"