From 812de35149a35f8c93ca6642891ed7ac7cef9a96 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 28 Jun 2022 10:34:25 +0200 Subject: [PATCH 01/15] :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 02/15] :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 03/15] :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 04/15] :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 05/15] :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 06/15] :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 07/15] 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) => ( +