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) => ( +