diff --git a/public/locales/en/layout/element-selector/selector.json b/public/locales/en/layout/element-selector/selector.json new file mode 100644 index 000000000..fc0d1886a --- /dev/null +++ b/public/locales/en/layout/element-selector/selector.json @@ -0,0 +1,9 @@ +{ + "modal": { + "title": "Add a new tile to your Dashboard", + "text": "You can use tiles, to make Homarr's dashboard yours. You may choose your desired tiles from the collection below and drag them anywhere on the dashboard where you like." + }, + "actionIcon": { + "tooltip": "Add a tile" + } +} diff --git a/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx b/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx new file mode 100644 index 000000000..ba2f86161 --- /dev/null +++ b/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx @@ -0,0 +1,137 @@ +import Image from 'next/image'; +import { Button, createStyles, Group, Stack, Tabs, Text } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { closeModal, ContextModalProps } from '@mantine/modals'; +import { + IconAccessPoint, + IconAdjustments, + IconBrush, + IconClick, + IconDeviceFloppy, + IconDoorExit, + IconPlug, +} from '@tabler/icons'; +import { useTranslation } from 'next-i18next'; +import { hideNotification, showNotification } from '@mantine/notifications'; +import { ServiceType } from '../../../../types/service'; +import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab'; +import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab'; +import { GeneralTab } from './Tabs/GeneralTab/GeneralTab'; +import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab'; +import { NetworkTab } from './Tabs/NetworkTab/NetworkTab'; + +export const EditServiceModal = ({ + context, + id, + innerProps, +}: ContextModalProps<{ service: ServiceType }>) => { + const { t } = useTranslation(); + const { classes } = useStyles(); + + const form = useForm({ + initialValues: innerProps.service, + }); + + const onSubmit = (values: ServiceType) => { + console.log('form submitted'); + console.log(values); + }; + + const tryCloseModal = () => { + if (form.isDirty()) { + showNotification({ + id: 'unsaved-edit-service-modal-changes', + title: 'You have unsaved changes', + message: ( + + If you close, your changes will be discarded and not saved. + + + ), + }); + return; + } + + context.closeModal(id); + }; + + return ( + <> + + {form.values.appearance.iconUrl ? ( + // disabled because image target is too dynamic for next image cache + // eslint-disable-next-line @next/next/no-img-element + service icon + ) : ( + + )} + + + {form.values.name ?? 'New Service'} + + +
+ + + }> + General + + }> + Behaviour + + }> + Network + + }> + Appearance + + }> + Integration + + + + + + + + + + + + + + +
+ + ); +}; + +const useStyles = createStyles(() => ({ + serviceImage: { + objectFit: 'contain', + }, +})); diff --git a/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx new file mode 100644 index 000000000..f6aaee8a1 --- /dev/null +++ b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx @@ -0,0 +1,24 @@ +import { Tabs, TextInput } from '@mantine/core'; +import { UseFormReturnType } from '@mantine/form'; +import { IconPhoto } from '@tabler/icons'; +import { useTranslation } from 'next-i18next'; +import { ServiceType } from '../../../../../../types/service'; + +interface AppearanceTabProps { + form: UseFormReturnType ServiceType>; +} + +export const AppearanceTab = ({ form }: AppearanceTabProps) => { + const { t } = useTranslation(''); + return ( + + } + label="Service Icon" + variant="default" + defaultValue={form.values.appearance.iconUrl} + {...form.getInputProps('serviceIcon')} + /> + + ); +}; diff --git a/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx new file mode 100644 index 000000000..839c0fe41 --- /dev/null +++ b/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx @@ -0,0 +1,47 @@ +import { Tabs, TextInput, Switch, Text } from '@mantine/core'; +import { UseFormReturnType } from '@mantine/form'; +import { IconClick } from '@tabler/icons'; +import { useTranslation } from 'next-i18next'; +import { ServiceType } from '../../../../../../types/service'; + +interface BehaviourTabProps { + form: UseFormReturnType ServiceType>; +} + +export const BehaviourTab = ({ form }: BehaviourTabProps) => { + const { t } = useTranslation(''); + return ( + + } + label="On click url" + placeholder="Override the default service url when clicking on the service" + variant="default" + mb="md" + {...form.getInputProps('onClickUrl')} + /> + + + Disables the direct movement of the tile + + } + mb="md" + {...form.getInputProps('isEditModeMovingDisabled')} + /> + + Disables the movement of the tile when moving others + + } + {...form.getInputProps('isEditModeTileFreezed')} + /> + + ); +}; diff --git a/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx new file mode 100644 index 000000000..4971058c5 --- /dev/null +++ b/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx @@ -0,0 +1,34 @@ +import { Tabs, TextInput } from '@mantine/core'; +import { UseFormReturnType } from '@mantine/form'; +import { IconCursorText, IconLink } from '@tabler/icons'; +import { useTranslation } from 'next-i18next'; +import { ServiceType } from '../../../../../../types/service'; + +interface GeneralTabProps { + form: UseFormReturnType ServiceType>; +} + +export const GeneralTab = ({ form }: GeneralTabProps) => { + const { t } = useTranslation(''); + return ( + + } + label="Service name" + placeholder="My example service" + variant="default" + mb="md" + required + {...form.getInputProps('name')} + /> + } + label="Service url" + placeholder="https://google.com" + variant="default" + required + {...form.getInputProps('url')} + /> + + ); +}; diff --git a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx new file mode 100644 index 000000000..81196bc18 --- /dev/null +++ b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx @@ -0,0 +1,96 @@ +import { + ActionIcon, + Button, + Card, + createStyles, + Flex, + Grid, + Group, + Stack, + Text, + TextInput, + ThemeIcon, + Title, +} from '@mantine/core'; +import { IconDeviceFloppy } from '@tabler/icons'; +import { ReactNode, useState } from 'react'; + +interface GenericSecretInputProps { + label: string; + value: string; + secretIsPresent: boolean; + unsetIcon: ReactNode; + setIcon: ReactNode; +} + +export const GenericSecretInput = ({ + label, + value, + secretIsPresent, + setIcon, + unsetIcon, +}: GenericSecretInputProps) => { + const { classes } = useStyles(); + const [dirty, setDirty] = useState(false); + + return ( + + + + + + {secretIsPresent ? setIcon : unsetIcon} + + + + {label} + + + {secretIsPresent + ? 'Secret is defined in the configuration' + : 'Secret has not been defined'} + + + + + + + {secretIsPresent ? ( + <> + + + + + } + defaultValue={value} + onChange={() => setDirty(true)} + withAsterisk + /> + + ) : ( + + )} + + + + + ); +}; + +const useStyles = createStyles(() => ({ + subtitle: { + lineHeight: 1.1, + }, + alignSelfCenter: { + alignSelf: 'center', + }, +})); diff --git a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx new file mode 100644 index 000000000..0e2176d07 --- /dev/null +++ b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx @@ -0,0 +1,139 @@ +/* eslint-disable @next/next/no-img-element */ +import { + Alert, + Card, + Group, + PasswordInput, + Select, + SelectItem, + Space, + Text, + TextInput, +} from '@mantine/core'; +import { UseFormReturnType } from '@mantine/form'; +import { IconKey, IconUser } from '@tabler/icons'; +import { useTranslation } from 'next-i18next'; +import { forwardRef, useState } from 'react'; +import { ServiceType } from '../../../../../../../../../types/service'; +import { TextExplanation } from '../TextExplanation/TextExplanation'; + +interface IntegrationSelectorProps { + form: UseFormReturnType ServiceType>; +} + +export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { + const { t } = useTranslation(''); + + // TODO: read this out from integrations dynamically. + const data: SelectItem[] = [ + { + value: 'sabnzbd', + image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/sabnzbd.png', + label: 'SABnzbd', + }, + { + value: 'deluge', + image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/deluge.png', + label: 'Deluge', + }, + { + value: 'transmission', + image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/transmission.png', + label: 'Transmission', + }, + { + value: 'qbittorrent', + image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/qbittorrent.png', + label: 'qBittorrent', + }, + { + value: 'jellyseerr', + image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/jellyseerr.png', + label: 'Jellyseerr', + }, + { + value: 'overseerr', + image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/overseerr.png', + label: 'Overseerr', + }, + ]; + + const [selectedItem, setSelectedItem] = useState(); + + return ( + <> + + + +