diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx index 402826090..ced815e2f 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx @@ -59,7 +59,9 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => { handleClickEdit={handleEditClick} handleClickChangePosition={handleChangeSizeClick} handleClickDelete={handleDeleteClick} - displayEdit={widget.properties !== undefined} + displayEdit={ + typeof widget.properties !== 'undefined' && Object.keys(widget.properties).length !== 0 + } /> ); }; diff --git a/src/widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile.tsx b/src/widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile.tsx index e87324baa..bdecf9a29 100644 --- a/src/widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile.tsx +++ b/src/widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile.tsx @@ -1,4 +1,16 @@ +import { NormalizedTorrent } from '@ctrl/shared-torrent'; +import { Text, Title, Group, useMantineTheme, Box, Card, ColorSwatch, Stack } from '@mantine/core'; +import { useListState } from '@mantine/hooks'; +import { showNotification } from '@mantine/notifications'; +import { linearGradientDef } from '@nivo/core'; +import { Datum, ResponsiveLine } from '@nivo/line'; import { IconArrowsUpDown } from '@tabler/icons'; +import axios from 'axios'; +import { useTranslation } from 'next-i18next'; +import { useEffect, useState } from 'react'; +import { useConfigContext } from '../../config/provider'; +import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval'; +import { humanFileSize } from '../../tools/humanFileSize'; import { defineWidget } from '../helper'; import { IWidget } from '../widgets'; @@ -8,10 +20,10 @@ const definition = defineWidget({ options: {}, gridstack: { - minWidth: 2, - minHeight: 2, - maxWidth: 2, - maxHeight: 2, + minWidth: 4, + minHeight: 4, + maxWidth: 12, + maxHeight: 12, }, component: TorrentNetworkTrafficTile, }); @@ -23,7 +35,161 @@ interface TorrentNetworkTrafficTileProps { } function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTrafficTileProps) { - return null; + const { t } = useTranslation(`modules/${definition.id}`); + const { colors } = useMantineTheme(); + const setSafeInterval = useSetSafeInterval(); + const { config } = useConfigContext(); + + const [torrentHistory, torrentHistoryHandlers] = useListState([]); + const [torrents, setTorrents] = useState([]); + + const downloadServices = + config?.apps.filter( + (app) => + app.integration.type === 'qBittorrent' || + app.integration.type === 'transmission' || + app.integration.type === 'deluge' + ) ?? []; + 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; + const interval = setSafeInterval(() => { + // Send one request with each download service inside + axios + .post('/api/modules/torrents') + .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?.apps]); + + useEffect(() => { + torrentHistoryHandlers.append({ + x: Date.now(), + down: totalDownloadSpeed, + up: totalUploadSpeed, + }); + }, [totalDownloadSpeed, totalUploadSpeed]); + + const history = torrentHistory.slice(-10); + const chartDataUp = history.map((load, i) => ({ + x: load.x, + y: load.up, + })) as Datum[]; + const chartDataDown = history.map((load, i) => ({ + x: load.x, + y: load.down, + })) as Datum[]; + + return ( + + {t('card.lineChart.title')} + + + + + {t('card.lineChart.totalDownload', { download: humanFileSize(totalDownloadSpeed) })} + + + + + + {t('card.lineChart.totalUpload', { upload: humanFileSize(totalUploadSpeed) })} + + + + + { + const Download = slice.points[0].data.y as number; + const Upload = slice.points[1].data.y as number; + // Get the number of seconds since the last update. + const seconds = (Date.now() - (slice.points[0].data.x as number)) / 1000; + // Round to the nearest second. + const roundedSeconds = Math.round(seconds); + return ( + + {t('card.lineChart.timeSpan', { seconds: roundedSeconds })} + + + + + + {t('card.lineChart.download', { download: humanFileSize(Download) })} + + + + + + {t('card.lineChart.upload', { upload: humanFileSize(Upload) })} + + + + + + ); + }} + data={[ + { + id: 'downloads', + data: chartDataUp, + }, + { + id: 'uploads', + data: chartDataDown, + }, + ]} + curve="monotoneX" + yFormat=" >-.2f" + axisTop={null} + axisRight={null} + enablePoints={false} + animate={false} + enableGridX={false} + enableGridY={false} + enableArea + defs={[ + linearGradientDef('gradientA', [ + { offset: 0, color: 'inherit' }, + { offset: 100, color: 'inherit', opacity: 0 }, + ]), + ]} + fill={[{ match: '*', id: 'gradientA' }]} + colors={[colors.blue[5], colors.green[5]]} + /> + + + ); } export default definition; + +interface TorrentHistory { + x: number; + up: number; + down: number; +}