Files
Homarr/src/components/AppShelf/AddAppShelfItem.tsx

380 lines
12 KiB
TypeScript
Raw Normal View History

2022-04-25 23:33:32 +02:00
import {
ActionIcon,
Anchor,
Button,
2022-04-25 23:33:32 +02:00
Center,
Group,
Image,
LoadingOverlay,
Modal,
MultiSelect,
2022-08-01 14:13:35 +02:00
PasswordInput,
Select,
Stack,
Switch,
Tabs,
TextInput,
Title,
Tooltip,
2022-04-25 23:33:32 +02:00
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconApps } from '@tabler/icons';
2022-06-27 17:27:59 +02:00
import { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useDebouncedValue } from '@mantine/hooks';
2022-05-03 19:52:09 +02:00
import { useConfig } from '../../tools/state';
2022-07-22 16:19:07 +02:00
import { tryMatchPort, ServiceTypeList, StatusCodes } from '../../tools/types';
import Tip from '../layout/Tip';
2022-04-25 23:33:32 +02:00
export function AddItemShelfButton(props: any) {
const [opened, setOpened] = useState(false);
return (
<>
<Modal
size="xl"
radius="md"
title={<Title order={3}>Add service</Title>}
opened={props.opened || opened}
onClose={() => setOpened(false)}
>
<AddAppShelfItemForm setOpened={setOpened} />
</Modal>
<Tooltip withinPortal label="Add a service">
<ActionIcon
variant="default"
radius="md"
size="xl"
color="blue"
style={props.style}
onClick={() => setOpened(true)}
>
<IconApps />
</ActionIcon>
</Tooltip>
</>
);
}
2022-07-22 13:13:41 +02:00
function MatchIcon(name: string | undefined, form: any) {
if (name === undefined || name === '') return null;
2022-05-12 14:24:15 +02:00
fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-')
.toLowerCase()
.replace(/^dash\.$/, 'dashdot')}.png`
).then((res) => {
if (res.ok) {
form.setFieldValue('icon', res.url);
}
});
return false;
}
2022-05-12 14:24:15 +02:00
function MatchService(name: string, form: any) {
const service = ServiceTypeList.find((s) => s.toLowerCase() === name.toLowerCase());
if (service) {
form.setFieldValue('type', service);
}
}
2022-08-10 13:59:46 +02:00
const DEFAULT_ICON = '/favicon.png';
export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) {
const { setOpened } = props;
const { config, setConfig } = useConfig();
const [isLoading, setLoading] = useState(false);
2022-05-29 10:45:49 +02:00
// Extract all the categories from the services in config
const InitialCategories = config.services.reduce((acc, cur) => {
2022-05-29 10:45:49 +02:00
if (cur.category && !acc.includes(cur.category)) {
acc.push(cur.category);
}
return acc;
}, [] as string[]);
const [categories, setCategories] = useState<string[]>(InitialCategories);
2022-05-29 10:45:49 +02:00
const form = useForm({
initialValues: {
2022-05-23 10:23:10 +02:00
id: props.id ?? uuidv4(),
type: props.type ?? 'Other',
category: props.category ?? null,
name: props.name ?? '',
icon: props.icon ?? DEFAULT_ICON,
url: props.url ?? '',
apiKey: props.apiKey ?? undefined,
username: props.username ?? undefined,
password: props.password ?? undefined,
openedUrl: props.openedUrl ?? undefined,
status: props.status ?? ['200'],
newTab: props.newTab ?? true,
},
validate: {
apiKey: () => null,
// Validate icon with a regex
icon: (value: string) =>
// Disable matching to allow any values
null,
// Validate url with a regex http/https
url: (value: string) => {
try {
const _isValid = new URL(value);
} catch (e) {
return 'Please enter a valid URL';
}
return null;
},
status: (value: string[]) => {
if (!value.length) {
return 'Please select a status code';
}
return null;
},
},
});
const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
useEffect(() => {
if (
form.values.name !== debounced ||
form.values.icon !== DEFAULT_ICON ||
form.values.type !== 'Other'
) {
return;
}
MatchIcon(form.values.name, form);
MatchService(form.values.name, form);
2022-07-22 16:19:07 +02:00
tryMatchPort(form.values.name, form);
}, [debounced]);
2022-05-29 10:45:49 +02:00
// Try to set const hostname to new URL(form.values.url).hostname)
// If it fails, set it to the form.values.url
let hostname = form.values.url;
try {
hostname = new URL(form.values.url).origin;
} catch (e) {
// Do nothing
}
return (
<>
2022-08-08 15:17:51 +02:00
<Center mb="lg">
2022-05-17 21:36:07 +02:00
<Image
height={120}
width={120}
fit="contain"
src={form.values.icon}
alt="Placeholder"
withPlaceholder
/>
</Center>
<form
onSubmit={form.onSubmit(() => {
const newForm = { ...form.values };
if (newForm.newTab === true) newForm.newTab = undefined;
if (newForm.openedUrl === '') newForm.openedUrl = undefined;
if (newForm.category === null) newForm.category = undefined;
if (newForm.status.length === 1 && newForm.status[0] === '200') {
delete newForm.status;
2022-06-16 16:54:07 -04:00
}
// If service already exists, update it.
if (config.services && config.services.find((s) => s.id === newForm.id)) {
setConfig({
...config,
2022-05-19 22:29:35 +02:00
// replace the found item by matching ID
services: config.services.map((s) => {
if (s.id === newForm.id) {
return {
...newForm,
};
}
return s;
}),
});
} else {
setConfig({
...config,
services: [...config.services, newForm],
});
}
setOpened(false);
form.reset();
})}
>
<Tabs defaultValue="Options">
<Tabs.List grow>
<Tabs.Tab value="Options">Options</Tabs.Tab>
<Tabs.Tab value="Advanced Options">Advanced options</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="Options">
<Stack>
<TextInput
required
label="Service name"
placeholder="Plex"
{...form.getInputProps('name')}
/>
<TextInput
required
label="Icon URL"
placeholder={DEFAULT_ICON}
{...form.getInputProps('icon')}
/>
<TextInput
required
label="Service URL"
placeholder="http://localhost:7575"
{...form.getInputProps('url')}
/>
<TextInput
label="On Click URL"
placeholder="http://sonarr.example.com"
{...form.getInputProps('openedUrl')}
/>
<Select
label="Service type"
defaultValue="Other"
placeholder="Pick one"
required
searchable
data={ServiceTypeList}
{...form.getInputProps('type')}
/>
<Select
label="Category"
data={categories}
placeholder="Select a category or create a new one"
nothingFound="Nothing found"
searchable
clearable
creatable
onCreate={(query) => {
const item = { value: query, label: query };
setCategories([...InitialCategories, query]);
return item;
}}
getCreateLabel={(query) => `+ Create "${query}"`}
{...form.getInputProps('category')}
/>
<LoadingOverlay visible={isLoading} />
{(form.values.type === 'Sonarr' ||
form.values.type === 'Radarr' ||
form.values.type === 'Lidarr' ||
2022-08-08 15:17:51 +02:00
form.values.type === 'Overseerr' ||
2022-08-09 13:26:55 +02:00
form.values.type === 'Jellyseerr' ||
form.values.type === 'Readarr') && (
<>
<TextInput
required
label="API key"
placeholder="Your API key"
value={form.values.apiKey}
onChange={(event) => {
form.setFieldValue('apiKey', event.currentTarget.value);
}}
error={form.errors.apiKey && 'Invalid API key'}
/>
<Tip>
Get your API key{' '}
<Anchor
target="_blank"
weight="bold"
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
href={`${hostname}/settings/general`}
>
here.
</Anchor>
</Tip>
</>
)}
{form.values.type === 'qBittorrent' && (
<>
<TextInput
required
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/>
2022-08-01 14:13:35 +02:00
<PasswordInput
required
label="Password"
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{form.values.type === 'Deluge' && (
<>
2022-08-01 14:13:35 +02:00
<PasswordInput
label="Password"
placeholder="password"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{form.values.type === 'Transmission' && (
<>
<TextInput
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/>
2022-08-01 14:13:35 +02:00
<PasswordInput
label="Password"
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
</Stack>
</Tabs.Panel>
<Tabs.Panel value="Advanced Options">
<Stack>
<MultiSelect
required
label="HTTP Status Codes"
data={StatusCodes}
placeholder="Select valid status codes"
clearButtonLabel="Clear selection"
nothingFound="Nothing found"
defaultValue={['200']}
clearable
searchable
{...form.getInputProps('status')}
/>
<Switch
label="Open service in new tab"
defaultChecked={form.values.newTab}
{...form.getInputProps('newTab')}
2022-05-26 21:07:01 +02:00
/>
</Stack>
</Tabs.Panel>
</Tabs>
<Group grow position="center" mt="xl">
<Button type="submit">{props.message ?? 'Add service'}</Button>
</Group>
</form>
</>
);
}