mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
feat: remove unused appshelf components
This commit is contained in:
@@ -1,460 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
Anchor,
|
|
||||||
Button,
|
|
||||||
Center,
|
|
||||||
Group,
|
|
||||||
Image,
|
|
||||||
LoadingOverlay,
|
|
||||||
Modal,
|
|
||||||
PasswordInput,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Stack,
|
|
||||||
Switch,
|
|
||||||
Tabs,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
|
||||||
import { IconApps } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { tryMatchPort, ServiceTypeList, Config } from '../../tools/types';
|
|
||||||
import apiKeyPaths from './apiKeyPaths.json';
|
|
||||||
import Tip from '../layout/Tip';
|
|
||||||
|
|
||||||
export function AddItemShelfButton(props: any) {
|
|
||||||
const { config, setConfig } = useConfig();
|
|
||||||
const [opened, setOpened] = useState(false);
|
|
||||||
const { t } = useTranslation('layout/add-service-app-shelf');
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
size="xl"
|
|
||||||
radius="md"
|
|
||||||
title={<Title order={3}>{t('modal.title')}</Title>}
|
|
||||||
opened={props.opened || opened}
|
|
||||||
onClose={() => setOpened(false)}
|
|
||||||
>
|
|
||||||
<AddAppShelfItemForm config={config} setConfig={setConfig} setOpened={setOpened} />
|
|
||||||
</Modal>
|
|
||||||
<Tooltip withinPortal label={t('actionIcon.tooltip')}>
|
|
||||||
<ActionIcon
|
|
||||||
variant="default"
|
|
||||||
radius="md"
|
|
||||||
size="xl"
|
|
||||||
color="blue"
|
|
||||||
style={props.style}
|
|
||||||
onClick={() => setOpened(true)}
|
|
||||||
>
|
|
||||||
<IconApps />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MatchIcon(name: string | undefined, form: any) {
|
|
||||||
if (name === undefined || name === '') return null;
|
|
||||||
fetch(
|
|
||||||
`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${name
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/^dash\.$/, 'dashdot')}.png`
|
|
||||||
).then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
form.setFieldValue('icon', res.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MatchService(name: string, form: any) {
|
|
||||||
const service = ServiceTypeList.find((s) => s.toLowerCase() === name.toLowerCase());
|
|
||||||
if (service) {
|
|
||||||
form.setFieldValue('type', service);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_ICON = '/imgs/favicon/favicon.png';
|
|
||||||
|
|
||||||
interface AddAppShelfItemFormProps {
|
|
||||||
setOpened: (b: boolean) => void;
|
|
||||||
config: Config;
|
|
||||||
setConfig: (config: Config) => void;
|
|
||||||
// Any other props you want to pass to the form
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) {
|
|
||||||
const { setOpened, config, setConfig } = props;
|
|
||||||
// Only get config and setConfig from useCOnfig if they are not present in props
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
const { t } = useTranslation('layout/add-service-app-shelf');
|
|
||||||
|
|
||||||
// Extract all the categories from the services in config
|
|
||||||
const InitialCategories = config.services.reduce((acc, cur) => {
|
|
||||||
if (cur.category && !acc.includes(cur.category)) {
|
|
||||||
acc.push(cur.category);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as string[]);
|
|
||||||
const [categories, setCategories] = useState<string[]>(InitialCategories);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: {
|
|
||||||
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,
|
|
||||||
ping: props.ping ?? true,
|
|
||||||
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 t('modal.form.validation.invalidUrl');
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
tryMatchPort(form.values.name, form);
|
|
||||||
}, [debounced]);
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<>
|
|
||||||
<Center mb="lg">
|
|
||||||
<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.ping === true) newForm.ping = undefined;
|
|
||||||
// If service already exists, update it.
|
|
||||||
if (config.services && config.services.find((s) => s.id === newForm.id)) {
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
// 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">{t('modal.tabs.options.title')}</Tabs.Tab>
|
|
||||||
<Tabs.Tab value="Advanced Options">{t('modal.tabs.advancedOptions.title')}</Tabs.Tab>
|
|
||||||
</Tabs.List>
|
|
||||||
<Tabs.Panel value="Options">
|
|
||||||
<Space h="sm" />
|
|
||||||
<Stack>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={t('modal.tabs.options.form.serviceName.label')}
|
|
||||||
placeholder={t('modal.tabs.options.form.serviceName.placeholder')}
|
|
||||||
{...form.getInputProps('name')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={t('modal.tabs.options.form.iconUrl.label')}
|
|
||||||
placeholder={DEFAULT_ICON}
|
|
||||||
{...form.getInputProps('icon')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={t('modal.tabs.options.form.serviceUrl.label')}
|
|
||||||
placeholder="http://localhost:7575"
|
|
||||||
{...form.getInputProps('url')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.tabs.options.form.onClickUrl.label')}
|
|
||||||
placeholder="http://sonarr.example.com"
|
|
||||||
{...form.getInputProps('openedUrl')}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label={t('modal.tabs.options.form.serviceType.label')}
|
|
||||||
defaultValue={t('modal.tabs.options.form.serviceType.defaultValue')}
|
|
||||||
placeholder={t('modal.tabs.options.form.serviceType.placeholder')}
|
|
||||||
required
|
|
||||||
searchable
|
|
||||||
data={ServiceTypeList}
|
|
||||||
{...form.getInputProps('type')}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label={t('modal.tabs.options.form.category.label')}
|
|
||||||
data={categories}
|
|
||||||
placeholder={t('modal.tabs.options.form.category.placeholder')}
|
|
||||||
nothingFound={t('modal.tabs.options.form.category.nothingFound')}
|
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
creatable
|
|
||||||
onCreate={(query) => {
|
|
||||||
const item = { value: query, label: query };
|
|
||||||
setCategories([...InitialCategories, query]);
|
|
||||||
return item;
|
|
||||||
}}
|
|
||||||
getCreateLabel={(query) =>
|
|
||||||
t('modal.tabs.options.form.category.createLabel', {
|
|
||||||
query,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{...form.getInputProps('category')}
|
|
||||||
/>
|
|
||||||
<LoadingOverlay visible={isLoading} />
|
|
||||||
{(form.values.type === 'Sonarr' ||
|
|
||||||
form.values.type === 'Radarr' ||
|
|
||||||
form.values.type === 'Lidarr' ||
|
|
||||||
form.values.type === 'Overseerr' ||
|
|
||||||
form.values.type === 'Jellyseerr' ||
|
|
||||||
form.values.type === 'Readarr' ||
|
|
||||||
form.values.type === 'Sabnzbd') && (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={t('modal.tabs.options.form.integrations.apiKey.label')}
|
|
||||||
placeholder={t('modal.tabs.options.form.integrations.apiKey.placeholder')}
|
|
||||||
value={form.values.apiKey}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('apiKey', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.apiKey &&
|
|
||||||
t('modal.tabs.options.form.integrations.apiKey.validation.noKey')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Tip>
|
|
||||||
{t('modal.tabs.options.form.integrations.apiKey.tip.text')}{' '}
|
|
||||||
<Anchor
|
|
||||||
target="_blank"
|
|
||||||
weight="bold"
|
|
||||||
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
|
|
||||||
href={`${hostname}/${
|
|
||||||
apiKeyPaths[form.values.type as keyof typeof apiKeyPaths]
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t('modal.tabs.options.form.integrations.apiKey.tip.link')}
|
|
||||||
</Anchor>
|
|
||||||
</Tip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{form.values.type === 'qBittorrent' && (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.qBittorrent.username.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.qBittorrent.username.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.username}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('username', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.username &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.qBittorrent.username.validation.invalidUsername'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.qBittorrent.password.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.qBittorrent.password.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.password}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('password', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.password &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.qBittorrent.password.validation.invalidPassword'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{form.values.type === 'Deluge' && (
|
|
||||||
<>
|
|
||||||
<PasswordInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.deluge.password.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.deluge.password.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.password}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('password', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.password &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.deluge.password.validation.invalidPassword'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{form.values.type === 'Transmission' && (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.transmission.username.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.transmission.username.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.username}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('username', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.username &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.transmission.username.validation.invalidUsername'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.transmission.password.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.transmission.password.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.password}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('password', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.password &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.transmission.password.validation.invalidPassword'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{form.values.type === 'NZBGet' && (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.nzbget.username.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.nzbget.username.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.username}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('username', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.username &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.nzbget.username.validation.invalidUsername'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.nzbget.password.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.nzbget.password.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.password}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('password', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.password &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.nzbget.password.validation.invalidPassword'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Tabs.Panel>
|
|
||||||
<Tabs.Panel value="Advanced Options">
|
|
||||||
<Space h="sm" />
|
|
||||||
<Stack>
|
|
||||||
<Switch
|
|
||||||
label="Ping service"
|
|
||||||
defaultChecked={form.values.ping}
|
|
||||||
{...form.getInputProps('ping')}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
label={t('modal.tabs.advancedOptions.form.openServiceInNewTab.label')}
|
|
||||||
defaultChecked={form.values.newTab}
|
|
||||||
{...form.getInputProps('newTab')}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Tabs.Panel>
|
|
||||||
</Tabs>
|
|
||||||
<Group grow position="center" mt="xl">
|
|
||||||
<Button type="submit">
|
|
||||||
{props.message ?? t('modal.tabs.advancedOptions.form.buttons.submit.content')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@ import { useState } from 'react';
|
|||||||
import PingComponent from '../../modules/ping/PingModule';
|
import PingComponent from '../../modules/ping/PingModule';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { serviceItem } from '../../tools/types';
|
import { serviceItem } from '../../tools/types';
|
||||||
import AppShelfMenu from './AppShelfMenu';
|
import { TileMenu } from '../Dashboard/Menu/TileMenu';
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
item: {
|
item: {
|
||||||
@@ -104,7 +104,7 @@ export function AppShelfItem(props: any) {
|
|||||||
opacity: hovering ? 1 : 0,
|
opacity: hovering ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppShelfMenu service={service} />
|
<TileMenu service={service} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { ActionIcon, Menu, Modal, Text } from '@mantine/core';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { IconCheck as Check, IconEdit as Edit, IconMenu, IconTrash as Trash } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { serviceItem } from '../../tools/types';
|
|
||||||
import { AddAppShelfItemForm } from './AddAppShelfItem';
|
|
||||||
import { useColorTheme } from '../../tools/color';
|
|
||||||
|
|
||||||
export default function AppShelfMenu(props: any) {
|
|
||||||
const { service }: { service: serviceItem } = props;
|
|
||||||
const { config, setConfig } = useConfig();
|
|
||||||
const { secondaryColor } = useColorTheme();
|
|
||||||
const { t } = useTranslation('layout/app-shelf-menu');
|
|
||||||
const [opened, setOpened] = useState(false);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
size="xl"
|
|
||||||
radius="md"
|
|
||||||
opened={props.opened || opened}
|
|
||||||
onClose={() => setOpened(false)}
|
|
||||||
title={t('modal.title')}
|
|
||||||
>
|
|
||||||
<AddAppShelfItemForm
|
|
||||||
config={config}
|
|
||||||
setConfig={setConfig}
|
|
||||||
setOpened={setOpened}
|
|
||||||
{...service}
|
|
||||||
message={t('modal.buttons.save')}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
<Menu
|
|
||||||
withinPortal
|
|
||||||
width={150}
|
|
||||||
shadow="xl"
|
|
||||||
withArrow
|
|
||||||
radius="md"
|
|
||||||
position="right"
|
|
||||||
styles={{
|
|
||||||
dropdown: {
|
|
||||||
// Add shadow and elevation to the body
|
|
||||||
boxShadow: '0 0 14px 14px rgba(0, 0, 0, 0.05)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon style={{}}>
|
|
||||||
<IconMenu />
|
|
||||||
</ActionIcon>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<Menu.Label>{t('menu.labels.settings')}</Menu.Label>
|
|
||||||
<Menu.Item color={secondaryColor} icon={<Edit />} onClick={() => setOpened(true)}>
|
|
||||||
{t('menu.actions.edit')}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Label>{t('menu.labels.dangerZone')}</Menu.Label>
|
|
||||||
<Menu.Item
|
|
||||||
color="red"
|
|
||||||
onClick={(e: any) => {
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
services: config.services.filter((s) => s.id !== service.id),
|
|
||||||
});
|
|
||||||
showNotification({
|
|
||||||
autoClose: 5000,
|
|
||||||
title: (
|
|
||||||
<Text>
|
|
||||||
Service <b>{service.name}</b> removed successfully!
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
color: 'green',
|
|
||||||
icon: <Check />,
|
|
||||||
message: undefined,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon={<Trash />}
|
|
||||||
>
|
|
||||||
{t('menu.actions.delete')}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
76
src/components/Dashboard/Menu/TileMenu.tsx
Normal file
76
src/components/Dashboard/Menu/TileMenu.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { ActionIcon, Menu, Text } from '@mantine/core';
|
||||||
|
import { openContextModal } from '@mantine/modals';
|
||||||
|
import { showNotification } from '@mantine/notifications';
|
||||||
|
import { IconCheck, IconEdit, IconMenu, IconTrash } from '@tabler/icons';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useConfigContext } from '../../../config/provider';
|
||||||
|
import { useConfigStore } from '../../../config/store';
|
||||||
|
import { useColorTheme } from '../../../tools/color';
|
||||||
|
import { ServiceType } from '../../../types/service';
|
||||||
|
|
||||||
|
interface TileMenuProps {
|
||||||
|
service: ServiceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TileMenu = ({ service }: TileMenuProps) => {
|
||||||
|
const { secondaryColor } = useColorTheme();
|
||||||
|
const { t } = useTranslation('layout/app-shelf-menu');
|
||||||
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
const { name: configName } = useConfigContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu withinPortal width={150} shadow="xl" withArrow radius="md" position="right">
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon>
|
||||||
|
<IconMenu />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Label>{t('menu.labels.settings')}</Menu.Label>
|
||||||
|
<Menu.Item
|
||||||
|
color={secondaryColor}
|
||||||
|
icon={<IconEdit />}
|
||||||
|
onClick={() =>
|
||||||
|
openContextModal({
|
||||||
|
modal: 'changeTilePosition',
|
||||||
|
innerProps: {
|
||||||
|
type: 'service',
|
||||||
|
tile: service,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('menu.actions.edit')}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Label>{t('menu.labels.dangerZone')}</Menu.Label>
|
||||||
|
<Menu.Item
|
||||||
|
color="red"
|
||||||
|
onClick={(e: any) => {
|
||||||
|
if (!configName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateConfig(configName, (previousConfig) => ({
|
||||||
|
...previousConfig,
|
||||||
|
services: previousConfig.services.filter((x) => x.id !== service.id),
|
||||||
|
})).then(() => {
|
||||||
|
showNotification({
|
||||||
|
autoClose: 5000,
|
||||||
|
title: (
|
||||||
|
<Text>
|
||||||
|
Service <b>{service.name}</b> removed successfully!
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
color: 'green',
|
||||||
|
icon: <IconCheck />,
|
||||||
|
message: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={<IconTrash />}
|
||||||
|
>
|
||||||
|
{t('menu.actions.delete')}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Grid, NumberInput } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { ContextModalProps } from '@mantine/modals';
|
||||||
|
import { TileBaseType } from '../../../../types/tile';
|
||||||
|
|
||||||
|
export const ChangePositionModal = ({
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<{ type: 'service' | 'type'; tile: TileBaseType }>) => {
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
area: innerProps.tile.area,
|
||||||
|
shape: innerProps.tile.shape,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col xs={12} md={6}>
|
||||||
|
<NumberInput label="X Position" {...form.getInputProps('area.tile.shape.location.x')} />
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col xs={12} md={6}>
|
||||||
|
<NumberInput label="Y Position" {...form.getInputProps('area.tile.shape.location.y')} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,9 +2,6 @@ import { ActionIcon, Tooltip } from '@mantine/core';
|
|||||||
import { openContextModal } from '@mantine/modals';
|
import { openContextModal } from '@mantine/modals';
|
||||||
import { IconApps } from '@tabler/icons';
|
import { IconApps } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { openContextModalGeneric } from '../../../../../tools/mantineModalManagerExtensions';
|
|
||||||
import { ServiceType } from '../../../../../types/service';
|
|
||||||
|
|
||||||
export const AddElementAction = () => {
|
export const AddElementAction = () => {
|
||||||
const { t } = useTranslation('layout/add-service-app-shelf');
|
const { t } = useTranslation('layout/add-service-app-shelf');
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { Box, createStyles, Group, Header as MantineHeader } from '@mantine/core';
|
import { Box, createStyles, Group, Header as MantineHeader } from '@mantine/core';
|
||||||
import { AddItemShelfButton } from '../../AppShelf/AddAppShelfItem';
|
|
||||||
|
|
||||||
import { Logo } from '../Logo';
|
import { Logo } from '../Logo';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
import { AddElementAction } from './Actions/AddElementAction/AddElementAction';
|
import { AddElementAction } from './Actions/AddElementAction/AddElementAction';
|
||||||
@@ -22,7 +20,6 @@ export function Header(props: any) {
|
|||||||
</Box>
|
</Box>
|
||||||
<Group position="right" noWrap>
|
<Group position="right" noWrap>
|
||||||
<Search />
|
<Search />
|
||||||
<AddItemShelfButton />
|
|
||||||
<AddElementAction />
|
<AddElementAction />
|
||||||
<ToolsMenu />
|
<ToolsMenu />
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import Dockerode from 'dockerode';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TFunction } from 'react-i18next';
|
import { TFunction } from 'react-i18next';
|
||||||
import { AddAppShelfItemForm } from '../../components/AppShelf/AddAppShelfItem';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { tryMatchService } from '../../tools/addToHomarr';
|
import { tryMatchService } from '../../tools/addToHomarr';
|
||||||
|
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
|
import { ServiceType } from '../../types/service';
|
||||||
|
|
||||||
let t: TFunction<'modules/docker', undefined>;
|
let t: TFunction<'modules/docker', undefined>;
|
||||||
|
|
||||||
@@ -160,21 +162,40 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
radius="md"
|
radius="md"
|
||||||
disabled={selected.length === 0 || selected.length > 1}
|
disabled={selected.length === 0 || selected.length > 1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openModal({
|
const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`;
|
||||||
size: 'xl',
|
openContextModalGeneric<{ service: ServiceType }>({
|
||||||
modalId: selected.at(0)!.Id,
|
modal: 'editService',
|
||||||
radius: 'md',
|
innerProps: {
|
||||||
title: t('actionBar.addService.title'),
|
service: {
|
||||||
zIndex: 500,
|
id: uuidv4(),
|
||||||
children: (
|
name: selected[0].Names[0],
|
||||||
<AddAppShelfItemForm
|
url: containerUrl,
|
||||||
setConfig={setConfig}
|
appearance: {
|
||||||
config={config}
|
iconUrl: '/imgs/logo/logo.png', // TODO: find icon automatically
|
||||||
setOpened={() => closeModal(selected.at(0)!.Id)}
|
},
|
||||||
message={t('actionBar.addService.message')}
|
network: {
|
||||||
{...tryMatchService(selected.at(0)!)}
|
enabledStatusChecker: false,
|
||||||
/>
|
okStatus: [],
|
||||||
),
|
},
|
||||||
|
behaviour: {
|
||||||
|
isOpeningNewTab: true,
|
||||||
|
onClickUrl: '',
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
type: 'wrapper', // TODO: Set the wrapper automatically
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
location: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { IModule } from '../ModuleTypes';
|
import { IModule } from '../ModuleTypes';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
|
|
||||||
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
|
|
||||||
@@ -93,7 +92,6 @@ export default function TorrentsComponent() {
|
|||||||
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
||||||
<AddItemShelfButton />
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { Datum, ResponsiveLine } from '@nivo/line';
|
import { Datum, ResponsiveLine } from '@nivo/line';
|
||||||
import { useListState } from '@mantine/hooks';
|
import { useListState } from '@mantine/hooks';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
|
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
import { IModule } from '../ModuleTypes';
|
import { IModule } from '../ModuleTypes';
|
||||||
@@ -84,11 +83,6 @@ export default function TotalDownloadsComponent() {
|
|||||||
<Group>
|
<Group>
|
||||||
<Title order={4}>{t('card.errors.noDownloadClients.title')}</Title>
|
<Title order={4}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||||
<div>
|
<div>
|
||||||
<AddItemShelfButton
|
|
||||||
style={{
|
|
||||||
float: 'inline-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{t('card.errors.noDownloadClients.text')}
|
{t('card.errors.noDownloadClients.text')}
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { UsenetHistoryList } from './UsenetHistoryList';
|
|||||||
import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType';
|
import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType';
|
||||||
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
|
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
@@ -49,7 +48,6 @@ export const UsenetComponent: FunctionComponent = () => {
|
|||||||
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
||||||
<AddItemShelfButton />
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { appWithTranslation } from 'next-i18next';
|
|||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { ChangePositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangePositionModal';
|
||||||
import { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal';
|
import { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal';
|
||||||
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
|
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
|
||||||
import { ConfigProvider } from '../config/provider';
|
import { ConfigProvider } from '../config/provider';
|
||||||
@@ -79,7 +80,11 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
<NotificationsProvider limit={4} position="bottom-left">
|
<NotificationsProvider limit={4} position="bottom-left">
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<ModalsProvider
|
<ModalsProvider
|
||||||
modals={{ editService: EditServiceModal, selectElement: SelectElementModal }}
|
modals={{
|
||||||
|
editService: EditServiceModal,
|
||||||
|
selectElement: SelectElementModal,
|
||||||
|
changeTilePosition: ChangePositionModal,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user