diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index 6f598b96a..c9024d47c 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -1,29 +1,30 @@ import { - Modal, + ActionIcon, + Anchor, + Button, Center, Group, - TextInput, Image, - Button, - Select, LoadingOverlay, - ActionIcon, - Tooltip, - Title, - Anchor, - Text, - Tabs, + 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'; export function AddItemShelfButton(props: any) { const [opened, setOpened] = useState(false); @@ -58,7 +59,8 @@ function MatchIcon(name: string, form: any) { fetch( `https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name .replace(/\s+/g, '-') - .toLowerCase()}.png` + .toLowerCase() + .replace(/^dash\.$/, 'dashdot')}.png` ).then((res) => { if (res.ok) { form.setFieldValue('icon', res.url); @@ -81,9 +83,10 @@ function MatchPort(name: string, form: any) { { name: 'sonarr', value: '8989' }, { name: 'radarr', value: '7878' }, { name: 'lidarr', value: '8686' }, - { name: 'readarr', 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()); @@ -275,15 +278,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/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} ); 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..4d55eee18 100644 --- a/src/components/Settings/CommonSettings.tsx +++ b/src/components/Settings/CommonSettings.tsx @@ -1,13 +1,12 @@ -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'; 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(); @@ -25,20 +24,16 @@ export default function CommonSettings(args: any) { ); return ( - + 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, + }, + }); + }} + /> + )} @@ -82,52 +80,7 @@ export default function CommonSettings(args: any) { - - 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"> - - - - + Upload your config file by dragging and dropping it onto the page! ); } 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)} > + + 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} + + ); +} 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..ff6175629 --- /dev/null +++ b/src/components/modules/dash./DashdotModule.tsx @@ -0,0 +1,233 @@ +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, + }, + graphs: { + name: 'Graphs', + value: ['CPU', 'RAM', 'Storage', 'Network'], + options: ['CPU', 'RAM', 'Storage', 'Network', 'GPU'], + }, + }, +}); + +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: 10, + 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 enabledGraphs = dashConfig?.graphs?.value ?? ['CPU', 'RAM', 'Storage', 'Network']; + const cpuEnabled = enabledGraphs.includes('CPU'); + const storageEnabled = enabledGraphs.includes('Storage'); + const ramEnabled = enabledGraphs.includes('RAM'); + const networkEnabled = enabledGraphs.includes('Network'); + const gpuEnabled = enabledGraphs.includes('GPU'); + + 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)} +

+
+ )} + {networkEnabled && ( +
+

Network:

+

+ {bpsPrettyPrint(info?.network?.speedUp)} Up{'\n'} + {bpsPrettyPrint(info?.network?.speedDown)} Down +

+
+ )} +
+ + {graphs.map((graph) => ( +