mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-07 22:15:49 +01:00
Merge branch 'dev' into docker-integration
This commit is contained in:
@@ -1,29 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
Modal,
|
ActionIcon,
|
||||||
|
Anchor,
|
||||||
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Group,
|
Group,
|
||||||
TextInput,
|
|
||||||
Image,
|
Image,
|
||||||
Button,
|
|
||||||
Select,
|
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
ActionIcon,
|
Modal,
|
||||||
Tooltip,
|
|
||||||
Title,
|
|
||||||
Anchor,
|
|
||||||
Text,
|
|
||||||
Tabs,
|
|
||||||
MultiSelect,
|
MultiSelect,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
|
Select,
|
||||||
Switch,
|
Switch,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
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 { 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 { useConfig } from '../../tools/state';
|
||||||
import { ServiceTypeList, StatusCodes } from '../../tools/types';
|
import { ServiceTypeList, StatusCodes } from '../../tools/types';
|
||||||
|
import Tip from '../layout/Tip';
|
||||||
|
|
||||||
export function AddItemShelfButton(props: any) {
|
export function AddItemShelfButton(props: any) {
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
@@ -58,7 +59,8 @@ function MatchIcon(name: string, form: any) {
|
|||||||
fetch(
|
fetch(
|
||||||
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
|
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
.toLowerCase()}.png`
|
.toLowerCase()
|
||||||
|
.replace(/^dash\.$/, 'dashdot')}.png`
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
form.setFieldValue('icon', res.url);
|
form.setFieldValue('icon', res.url);
|
||||||
@@ -81,9 +83,10 @@ function MatchPort(name: string, form: any) {
|
|||||||
{ name: 'sonarr', value: '8989' },
|
{ name: 'sonarr', value: '8989' },
|
||||||
{ name: 'radarr', value: '7878' },
|
{ name: 'radarr', value: '7878' },
|
||||||
{ name: 'lidarr', value: '8686' },
|
{ name: 'lidarr', value: '8686' },
|
||||||
{ name: 'readarr', value: '8686' },
|
{ name: 'readarr', value: '8787' },
|
||||||
{ name: 'deluge', value: '8112' },
|
{ name: 'deluge', value: '8112' },
|
||||||
{ name: 'transmission', value: '9091' },
|
{ name: 'transmission', value: '9091' },
|
||||||
|
{ name: 'dash.', value: '3001' },
|
||||||
];
|
];
|
||||||
// Match name with portmap key
|
// Match name with portmap key
|
||||||
const port = portmap.find((p) => p.name === name.toLowerCase());
|
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'}
|
error={form.errors.apiKey && 'Invalid API key'}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Tip>
|
||||||
style={{
|
Get your API key{' '}
|
||||||
alignSelf: 'center',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'gray',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Tip: Get your API key{' '}
|
|
||||||
<Anchor
|
<Anchor
|
||||||
target="_blank"
|
target="_blank"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
@@ -292,7 +288,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
|||||||
>
|
>
|
||||||
here.
|
here.
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Tip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{form.values.type === 'qBittorrent' && (
|
{form.values.type === 'qBittorrent' && (
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ const AppShelf = (props: any) => {
|
|||||||
const noCategory = config.services.filter(
|
const noCategory = config.services.filter(
|
||||||
(e) => e.category === undefined || e.category === null
|
(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
|
// Create an item with 0: true, 1: true, 2: true... For each category
|
||||||
return (
|
return (
|
||||||
// Return one item for each category
|
// Return one item for each category
|
||||||
@@ -176,6 +177,7 @@ const AppShelf = (props: any) => {
|
|||||||
{item()}
|
{item()}
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
{downloadEnabled ? (
|
||||||
<Accordion.Item key="Downloads" label="Your downloads">
|
<Accordion.Item key="Downloads" label="Your downloads">
|
||||||
<Paper
|
<Paper
|
||||||
p="lg"
|
p="lg"
|
||||||
@@ -191,6 +193,7 @@ const AppShelf = (props: any) => {
|
|||||||
<DownloadComponent />
|
<DownloadComponent />
|
||||||
</Paper>
|
</Paper>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
) : null}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function TitleChanger() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group direction="column" grow>
|
<Group direction="column" grow mb="lg">
|
||||||
<form onSubmit={form.onSubmit((values) => saveChanges(values))}>
|
<form onSubmit={form.onSubmit((values) => saveChanges(values))}>
|
||||||
<Group grow direction="column">
|
<Group grow direction="column">
|
||||||
<TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} />
|
<TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} />
|
||||||
|
|||||||
@@ -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 { 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 { useConfig } from '../../tools/state';
|
||||||
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
||||||
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
|
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
|
||||||
import ConfigChanger from '../Config/ConfigChanger';
|
import ConfigChanger from '../Config/ConfigChanger';
|
||||||
import SaveConfigComponent from '../Config/SaveConfig';
|
import SaveConfigComponent from '../Config/SaveConfig';
|
||||||
import ModuleEnabler from './ModuleEnabler';
|
import ModuleEnabler from './ModuleEnabler';
|
||||||
|
import Tip from '../layout/Tip';
|
||||||
|
|
||||||
export default function CommonSettings(args: any) {
|
export default function CommonSettings(args: any) {
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
@@ -25,20 +24,16 @@ export default function CommonSettings(args: any) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group direction="column" grow>
|
<Group direction="column" grow mb="lg">
|
||||||
<Group grow direction="column" spacing={0}>
|
<Group grow direction="column" spacing={0}>
|
||||||
<Text>Search engine</Text>
|
<Text>Search engine</Text>
|
||||||
<Text
|
<Tip>
|
||||||
style={{
|
Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube or
|
||||||
fontSize: '0.75rem',
|
for a Torrent respectively.
|
||||||
color: 'gray',
|
</Tip>
|
||||||
marginBottom: '0.5rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Tip: %s can be used as a placeholder for the query.
|
|
||||||
</Text>
|
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
fullWidth
|
fullWidth
|
||||||
|
mb="sm"
|
||||||
title="Search engine"
|
title="Search engine"
|
||||||
value={
|
value={
|
||||||
// Match config.settings.searchUrl with a key in the matches array
|
// Match config.settings.searchUrl with a key in the matches array
|
||||||
@@ -60,6 +55,8 @@ export default function CommonSettings(args: any) {
|
|||||||
data={matches}
|
data={matches}
|
||||||
/>
|
/>
|
||||||
{searchUrl === 'Custom' && (
|
{searchUrl === 'Custom' && (
|
||||||
|
<>
|
||||||
|
<Tip>%s can be used as a placeholder for the query.</Tip>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Query URL"
|
label="Query URL"
|
||||||
placeholder="Custom query URL"
|
placeholder="Custom query URL"
|
||||||
@@ -75,6 +72,7 @@ export default function CommonSettings(args: any) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<ColorSchemeSwitch />
|
<ColorSchemeSwitch />
|
||||||
@@ -82,52 +80,7 @@ export default function CommonSettings(args: any) {
|
|||||||
<ModuleEnabler />
|
<ModuleEnabler />
|
||||||
<ConfigChanger />
|
<ConfigChanger />
|
||||||
<SaveConfigComponent />
|
<SaveConfigComponent />
|
||||||
<Text
|
<Tip>Upload your config file by dragging and dropping it onto the page!</Tip>
|
||||||
style={{
|
|
||||||
alignSelf: 'center',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'gray',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Tip: You can upload your config file by dragging and dropping it onto the page!
|
|
||||||
</Text>
|
|
||||||
<Group position="center" direction="row" mr="xs">
|
|
||||||
<Group spacing={0}>
|
|
||||||
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
|
||||||
<BrandGithub size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
fontSize: '0.90rem',
|
|
||||||
color: 'gray',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{CURRENT_VERSION}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group spacing={1}>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: '0.90rem',
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'gray',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Made with ❤️ by @
|
|
||||||
<Anchor
|
|
||||||
href="https://github.com/ajnart"
|
|
||||||
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
|
||||||
>
|
|
||||||
ajnart
|
|
||||||
</Anchor>
|
|
||||||
</Text>
|
|
||||||
<ActionIcon<'a'> component="a" href="https://discord.gg/aCsmEV5RgA" size="lg">
|
|
||||||
<BrandDiscord size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/components/Settings/Credits.tsx
Normal file
44
src/components/Settings/Credits.tsx
Normal file
@@ -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 (
|
||||||
|
<Group position="center" direction="row" mr="xs">
|
||||||
|
<Group spacing={0}>
|
||||||
|
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
||||||
|
<IconBrandGithub size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
fontSize: '0.90rem',
|
||||||
|
color: 'gray',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{CURRENT_VERSION}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group spacing={1}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '0.90rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'gray',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Made with ❤️ by @
|
||||||
|
<Anchor
|
||||||
|
href="https://github.com/ajnart"
|
||||||
|
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||||
|
>
|
||||||
|
ajnart
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
<ActionIcon<'a'> component="a" href="https://discord.gg/aCsmEV5RgA" size="lg">
|
||||||
|
<IconBrandDiscord size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 { useHotkeys } from '@mantine/hooks';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IconSettings } from '@tabler/icons';
|
import { IconSettings } from '@tabler/icons';
|
||||||
import AdvancedSettings from './AdvancedSettings';
|
import AdvancedSettings from './AdvancedSettings';
|
||||||
import CommonSettings from './CommonSettings';
|
import CommonSettings from './CommonSettings';
|
||||||
|
import Credits from './Credits';
|
||||||
|
|
||||||
function SettingsMenu(props: any) {
|
function SettingsMenu(props: any) {
|
||||||
return (
|
return (
|
||||||
<Tabs grow>
|
<Tabs grow>
|
||||||
<Tabs.Tab data-autofocus label="Common">
|
<Tabs.Tab data-autofocus label="Common">
|
||||||
|
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
|
||||||
<CommonSettings />
|
<CommonSettings />
|
||||||
|
</ScrollArea>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab label="Customizations">
|
<Tabs.Tab label="Customizations">
|
||||||
|
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
|
||||||
<AdvancedSettings />
|
<AdvancedSettings />
|
||||||
|
</ScrollArea>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
@@ -26,13 +31,14 @@ export function SettingsMenuButton(props: any) {
|
|||||||
<>
|
<>
|
||||||
<Drawer
|
<Drawer
|
||||||
size="xl"
|
size="xl"
|
||||||
padding="xl"
|
padding="lg"
|
||||||
position="right"
|
position="right"
|
||||||
title={<Title order={3}>Settings</Title>}
|
title={<Title order={5}>Settings</Title>}
|
||||||
opened={props.opened || opened}
|
opened={props.opened || opened}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
>
|
>
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
|
<Credits />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
createStyles,
|
ActionIcon,
|
||||||
Header as Head,
|
|
||||||
Group,
|
|
||||||
Box,
|
Box,
|
||||||
Burger,
|
Burger,
|
||||||
|
createStyles,
|
||||||
Drawer,
|
Drawer,
|
||||||
Title,
|
Group,
|
||||||
|
Header as Head,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
ActionIcon,
|
Title,
|
||||||
Transition,
|
Transition,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useBooleanToggle } from '@mantine/hooks';
|
import { useBooleanToggle } from '@mantine/hooks';
|
||||||
import { Logo } from './Logo';
|
|
||||||
import SearchBar from '../modules/search/SearchModule';
|
|
||||||
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
|
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 { ModuleWrapper } from '../modules/moduleWrapper';
|
||||||
import { CalendarModule, TotalDownloadsModule, WeatherModule, DateModule } from '../modules';
|
import { CalendarModule, TotalDownloadsModule, WeatherModule, DateModule } from '../modules';
|
||||||
import DockerDrawer from '../Docker/DockerDrawer';
|
import DockerDrawer from '../Docker/DockerDrawer';
|
||||||
|
import SearchBar from '../modules/search/SearchModule';
|
||||||
|
import { SettingsMenuButton } from '../Settings/SettingsMenu';
|
||||||
|
import { Logo } from './Logo';
|
||||||
|
|
||||||
const HEADER_HEIGHT = 60;
|
const HEADER_HEIGHT = 60;
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ export function Header(props: any) {
|
|||||||
<ModuleWrapper module={TotalDownloadsModule} />
|
<ModuleWrapper module={TotalDownloadsModule} />
|
||||||
<ModuleWrapper module={WeatherModule} />
|
<ModuleWrapper module={WeatherModule} />
|
||||||
<ModuleWrapper module={DateModule} />
|
<ModuleWrapper module={DateModule} />
|
||||||
|
<ModuleWrapper module={DashdotModule} />
|
||||||
</Group>
|
</Group>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
19
src/components/layout/Tip.tsx
Normal file
19
src/components/layout/Tip.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Text } from '@mantine/core';
|
||||||
|
|
||||||
|
interface TipProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Tip(props: TipProps) {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: 'gray',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tip: {props.children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Group } from '@mantine/core';
|
import { Group } from '@mantine/core';
|
||||||
import { useMediaQuery } from '@mantine/hooks';
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules';
|
import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules';
|
||||||
|
import { DashdotModule } from '../modules/dash.';
|
||||||
import { ModuleWrapper } from '../modules/moduleWrapper';
|
import { ModuleWrapper } from '../modules/moduleWrapper';
|
||||||
|
|
||||||
export default function Widgets(props: any) {
|
export default function Widgets(props: any) {
|
||||||
@@ -14,6 +15,7 @@ export default function Widgets(props: any) {
|
|||||||
<ModuleWrapper module={TotalDownloadsModule} />
|
<ModuleWrapper module={TotalDownloadsModule} />
|
||||||
<ModuleWrapper module={WeatherModule} />
|
<ModuleWrapper module={WeatherModule} />
|
||||||
<ModuleWrapper module={DateModule} />
|
<ModuleWrapper module={DateModule} />
|
||||||
|
<ModuleWrapper module={DashdotModule} />
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
233
src/components/modules/dash./DashdotModule.tsx
Normal file
233
src/components/modules/dash./DashdotModule.tsx
Normal file
@@ -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 extends IModule>(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<any | undefined>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<h2 className={classes.heading}>Dash.</h2>
|
||||||
|
|
||||||
|
{!dashdotService ? (
|
||||||
|
<p>No dash. service found. Please add one to your Homarr dashboard.</p>
|
||||||
|
) : !info ? (
|
||||||
|
<p>Cannot acquire information from dash. - are you running the latest version?</p>
|
||||||
|
) : (
|
||||||
|
<div className={classes.graphsContainer}>
|
||||||
|
<div className={classes.table}>
|
||||||
|
{storageEnabled && isCompact && (
|
||||||
|
<div className={classes.tableRow}>
|
||||||
|
<p className={classes.tableLabel}>Storage:</p>
|
||||||
|
<p className={classes.tableValue}>
|
||||||
|
{(totalUsed / (totalSize || 1)).toFixed(1)}%{'\n'}
|
||||||
|
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkEnabled && (
|
||||||
|
<div className={classes.tableRow}>
|
||||||
|
<p className={classes.tableLabel}>Network:</p>
|
||||||
|
<p className={classes.tableValue}>
|
||||||
|
{bpsPrettyPrint(info?.network?.speedUp)} Up{'\n'}
|
||||||
|
{bpsPrettyPrint(info?.network?.speedDown)} Down
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{graphs.map((graph) => (
|
||||||
|
<iframe
|
||||||
|
className={classes.iframe}
|
||||||
|
style={
|
||||||
|
isCompact
|
||||||
|
? {
|
||||||
|
width: graph.spanTwo ? '100%' : 'calc(50% - 5px)',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
key={graph.name}
|
||||||
|
title={graph.name}
|
||||||
|
src={`${
|
||||||
|
dashdotService.url
|
||||||
|
}?singleGraphMode=true&graph=${graph.name.toLowerCase()}&theme=${colorScheme}&surface=${(colorScheme ===
|
||||||
|
'dark'
|
||||||
|
? theme.colors.dark[7]
|
||||||
|
: theme.colors.gray[0]
|
||||||
|
).substring(1)}${isCompact ? '&gap=10' : '&gap=5'}&innerRadius=${theme.radius.lg}${
|
||||||
|
graph.params
|
||||||
|
? `&${Object.entries(graph.params)
|
||||||
|
.map(([key, value]) => `${key}=${value.toString()}`)
|
||||||
|
.join('&')}`
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
frameBorder="0"
|
||||||
|
allowTransparency
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/modules/dash./index.ts
Normal file
1
src/components/modules/dash./index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { DashdotModule } from './DashdotModule';
|
||||||
@@ -23,7 +23,7 @@ export default function DateComponent(props: any) {
|
|||||||
const [date, setDate] = useState(new Date());
|
const [date, setDate] = useState(new Date());
|
||||||
const setSafeInterval = useSetSafeInterval();
|
const setSafeInterval = useSetSafeInterval();
|
||||||
const { config } = useConfig();
|
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';
|
const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
|
||||||
// Change date on minute change
|
// Change date on minute change
|
||||||
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './date';
|
|
||||||
export * from './calendar';
|
export * from './calendar';
|
||||||
export * from './search';
|
export * from './dash.';
|
||||||
export * from './ping';
|
export * from './date';
|
||||||
export * from './weather';
|
|
||||||
export * from './downloads';
|
export * from './downloads';
|
||||||
|
export * from './ping';
|
||||||
|
export * from './search';
|
||||||
|
export * from './weather';
|
||||||
|
|||||||
@@ -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 { useConfig } from '../../tools/state';
|
||||||
import { IModule } from './modules';
|
import { IModule } from './modules';
|
||||||
|
|
||||||
function getItems(module: IModule) {
|
function getItems(module: IModule) {
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
const enabledModules = config.modules ?? {};
|
|
||||||
const items: JSX.Element[] = [];
|
const items: JSX.Element[] = [];
|
||||||
if (module.options) {
|
if (module.options) {
|
||||||
const keys = Object.keys(module.options);
|
const keys = Object.keys(module.options);
|
||||||
@@ -15,6 +23,38 @@ function getItems(module: IModule) {
|
|||||||
types.forEach((type, index) => {
|
types.forEach((type, index) => {
|
||||||
const optionName = `${module.title}.${keys[index]}`;
|
const optionName = `${module.title}.${keys[index]}`;
|
||||||
const moduleInConfig = config.modules?.[module.title];
|
const moduleInConfig = config.modules?.[module.title];
|
||||||
|
if (type === 'object') {
|
||||||
|
items.push(
|
||||||
|
<MultiSelect
|
||||||
|
label={module.options?.[keys[index]].name}
|
||||||
|
data={module.options?.[keys[index]].options ?? []}
|
||||||
|
defaultValue={
|
||||||
|
(moduleInConfig?.options?.[keys[index]]?.value as string[]) ??
|
||||||
|
(values[index].value as string[]) ??
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
searchable
|
||||||
|
onChange={(value) => {
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
modules: {
|
||||||
|
...config.modules,
|
||||||
|
[module.title]: {
|
||||||
|
...moduleInConfig,
|
||||||
|
options: {
|
||||||
|
...moduleInConfig?.options,
|
||||||
|
[keys[index]]: {
|
||||||
|
...moduleInConfig?.options?.[keys[index]],
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (type === 'string') {
|
if (type === 'string') {
|
||||||
items.push(
|
items.push(
|
||||||
<form
|
<form
|
||||||
@@ -44,7 +84,11 @@ function getItems(module: IModule) {
|
|||||||
id={optionName}
|
id={optionName}
|
||||||
name={optionName}
|
name={optionName}
|
||||||
label={values[index].name}
|
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) => {}}
|
onChange={(e) => {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -59,7 +103,9 @@ function getItems(module: IModule) {
|
|||||||
<Switch
|
<Switch
|
||||||
defaultChecked={
|
defaultChecked={
|
||||||
// Set default checked to the value of the option if it exists
|
// Set default checked to the value of the option if it exists
|
||||||
(moduleInConfig?.options?.[keys[index]]?.value as boolean) ?? false
|
(moduleInConfig?.options?.[keys[index]]?.value as boolean) ??
|
||||||
|
(values[index].value as boolean) ??
|
||||||
|
false
|
||||||
}
|
}
|
||||||
key={keys[index]}
|
key={keys[index]}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ interface Option {
|
|||||||
|
|
||||||
export interface OptionValues {
|
export interface OptionValues {
|
||||||
name: string;
|
name: string;
|
||||||
value: boolean | string;
|
value: boolean | string | string[];
|
||||||
|
options?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -107,18 +107,6 @@ export default function SearchBar(props: any) {
|
|||||||
}, 20);
|
}, 20);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Popover
|
|
||||||
opened={opened}
|
|
||||||
position="bottom"
|
|
||||||
placement="start"
|
|
||||||
width={260}
|
|
||||||
withArrow
|
|
||||||
radius="md"
|
|
||||||
trapFocus={false}
|
|
||||||
transition="pop-bottom-right"
|
|
||||||
onFocusCapture={() => setOpened(true)}
|
|
||||||
onBlurCapture={() => setOpened(false)}
|
|
||||||
target={
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
autoFocus
|
autoFocus
|
||||||
variant="filled"
|
variant="filled"
|
||||||
@@ -134,13 +122,6 @@ export default function SearchBar(props: any) {
|
|||||||
{...props}
|
{...props}
|
||||||
{...form.getInputProps('query')}
|
{...form.getInputProps('query')}
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text>
|
|
||||||
Tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube
|
|
||||||
or for a Torrent respectively.
|
|
||||||
</Text>
|
|
||||||
</Popover>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Group, Space, Title, Tooltip } from '@mantine/core';
|
import { Group, Space, Title, Tooltip, Skeleton } from '@mantine/core';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -29,7 +29,7 @@ export const WeatherModule: IModule = {
|
|||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
name: 'Current location',
|
name: 'Current location',
|
||||||
value: '',
|
value: 'Paris',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -135,7 +135,7 @@ export default function WeatherComponent(props: any) {
|
|||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const [weather, setWeather] = useState({} as WeatherResponse);
|
const [weather, setWeather] = useState({} as WeatherResponse);
|
||||||
const cityInput: string =
|
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 =
|
const isFahrenheit: boolean =
|
||||||
(config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false;
|
(config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false;
|
||||||
|
|
||||||
@@ -157,7 +157,18 @@ export default function WeatherComponent(props: any) {
|
|||||||
});
|
});
|
||||||
}, [cityInput]);
|
}, [cityInput]);
|
||||||
if (!weather.current_weather) {
|
if (!weather.current_weather) {
|
||||||
return null;
|
return (
|
||||||
|
<>
|
||||||
|
<Skeleton height={40} width={100} mb="xl" />
|
||||||
|
<Group noWrap direction="row">
|
||||||
|
<Skeleton height={50} circle />
|
||||||
|
<Group>
|
||||||
|
<Skeleton height={25} width={70} mr="lg" />
|
||||||
|
<Skeleton height={25} width={70} />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
function usePerferedUnit(value: number): string {
|
function usePerferedUnit(value: number): string {
|
||||||
return isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
return isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const Targets = [
|
|||||||
export const ServiceTypeList = [
|
export const ServiceTypeList = [
|
||||||
'Other',
|
'Other',
|
||||||
'Emby',
|
'Emby',
|
||||||
|
'Dash.',
|
||||||
'Deluge',
|
'Deluge',
|
||||||
'Lidarr',
|
'Lidarr',
|
||||||
'Plex',
|
'Plex',
|
||||||
@@ -73,6 +74,7 @@ export const ServiceTypeList = [
|
|||||||
export type ServiceType =
|
export type ServiceType =
|
||||||
| 'Other'
|
| 'Other'
|
||||||
| 'Emby'
|
| 'Emby'
|
||||||
|
| 'Dash.'
|
||||||
| 'Deluge'
|
| 'Deluge'
|
||||||
| 'Lidarr'
|
| 'Lidarr'
|
||||||
| 'Plex'
|
| 'Plex'
|
||||||
|
|||||||
Reference in New Issue
Block a user