Add mantine context modals

This commit is contained in:
Manuel Ruwe
2022-12-04 21:19:40 +01:00
parent 99a3a4936e
commit 57d76d223f
24 changed files with 1023 additions and 1 deletions

View File

@@ -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 (
<Card withBorder>
<Grid>
<Grid.Col className={classes.alignSelfCenter} xs={12} md={6}>
<Group spacing="sm">
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light">
{secretIsPresent ? setIcon : unsetIcon}
</ThemeIcon>
<Stack spacing={0}>
<Title className={classes.subtitle} order={6}>
{label}
</Title>
<Text size="xs" color="dimmed">
{secretIsPresent
? 'Secret is defined in the configuration'
: 'Secret has not been defined'}
</Text>
</Stack>
</Group>
</Grid.Col>
<Grid.Col xs={12} md={6}>
<Flex gap={10} justify="end" align="end">
{secretIsPresent ? (
<>
<Button variant="subtle" color="gray" px="xl">
Clear Secret
</Button>
<TextInput
type="password"
placeholder="Leave empty"
description={`Update secret${dirty ? ' (unsaved)' : ''}`}
rightSection={
<ActionIcon disabled={!dirty}>
<IconDeviceFloppy size={18} />
</ActionIcon>
}
defaultValue={value}
onChange={() => setDirty(true)}
withAsterisk
/>
</>
) : (
<Button variant="light" px="xl">
Define secret
</Button>
)}
</Flex>
</Grid.Col>
</Grid>
</Card>
);
};
const useStyles = createStyles(() => ({
subtitle: {
lineHeight: 1.1,
},
alignSelfCenter: {
alignSelf: 'center',
},
}));

View File

@@ -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, (item: ServiceType) => 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<SelectItem>();
return (
<>
<TextExplanation />
<Space h="sm" />
<Select
label="Configure this service for the following integration"
placeholder="Select your desired configuration"
itemComponent={SelectItemComponent}
data={data}
maxDropdownHeight={400}
clearable
onSelect={(e) => {
const item = data.find((x) => x.label === e.currentTarget.value);
if (item === undefined) {
setSelectedItem(undefined);
return;
}
setSelectedItem(item);
}}
variant="default"
mb="md"
icon={selectedItem && <img src={selectedItem.image} alt="test" width={20} height={20} />}
/>
{/*
{selectedItem && (
<Card p="md" pt="sm" radius="sm">
<Text weight={500} mb="lg">
Integration Configuration
</Text>
<Group grow>
<TextInput
icon={<IconUser size={16} />}
label="Username"
description="Optional"
placeholder="deluge"
variant="default"
{...form.getInputProps('username')}
/>
<PasswordInput
icon={<IconKey />}
label="Password"
description="Optional, never share this with anybody else"
variant="default"
{...form.getInputProps('password')}
/>
</Group>
</Card>
)}
*/}
</>
);
};
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
image: string;
label: string;
}
const SelectItemComponent = forwardRef<HTMLDivElement, ItemProps>(
({ image, label, ...others }: ItemProps, ref) => (
<div ref={ref} {...others}>
<Group noWrap>
<img src={image} alt="integration icon" width={20} height={20} />
<div>
<Text size="sm">{label}</Text>
</div>
</Group>
</div>
)
);

View File

@@ -0,0 +1,61 @@
import { Stack } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { IconKey, IconKeyOff, IconLock, IconLockOff, IconUser, IconUserOff } from '@tabler/icons';
import { ServiceType } from '../../../../../../../../../types/service';
import { GenericSecretInput } from '../InputElements/GenericSecretInput';
interface IntegrationOptionsRendererProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
}
const secretMappings = [
{
label: 'username',
prettyName: 'Username',
icon: <IconUser size={18} />,
iconUnset: <IconUserOff size={18} />,
},
{
label: 'password',
prettyName: 'Password',
icon: <IconLock size={18} />,
iconUnset: <IconLockOff size={18} />,
},
{
label: 'apiKey',
prettyName: 'API Key',
icon: <IconKey size={18} />,
iconUnset: <IconKeyOff size={18} />,
},
];
export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererProps) => (
<Stack spacing="xs" mb="md">
{Object.entries(form.values.integration.properties).map((entry) => {
const mapping = secretMappings.find((item) => item.label === entry[0]);
const isPresent = entry[1] !== undefined;
if (!mapping) {
return (
<GenericSecretInput
label={`${entry[0]} (potentionally unmapped)`}
value={entry[1]}
secretIsPresent={isPresent}
setIcon={<IconKey size={18} />}
unsetIcon={<IconKeyOff size={18} />}
/>
);
}
return (
<GenericSecretInput
label={mapping.prettyName}
value={entry[1]}
secretIsPresent={isPresent}
setIcon={mapping.icon}
unsetIcon={mapping.iconUnset}
/>
);
})}
</Stack>
);

View File

@@ -0,0 +1,10 @@
import { Text } from '@mantine/core';
export const TextExplanation = () => {
return (
<Text color="dimmed">
You can optionally connect your services using integrations. Integration elements on your
dashboard will communicate to your services using the configuration below.
</Text>
);
};

View File

@@ -0,0 +1,37 @@
import { Alert, Divider, Tabs, Text } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { IconAlertTriangle } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { ServiceType } from '../../../../../../types/service';
import { IntegrationSelector } from './Components/InputElements/IntegrationSelector';
import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer';
interface IntegrationTabProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
}
export const IntegrationTab = ({ form }: IntegrationTabProps) => {
const { t } = useTranslation('');
const hasIntegrationSelected = form.values.integration !== null;
return (
<Tabs.Panel value="integration" pt="lg">
<IntegrationSelector form={form} />
{hasIntegrationSelected && (
<>
<Divider label="Integration Configuration" labelPosition="center" mt="xl" mb="md" />
<IntegrationOptionsRenderer form={form} />
<Alert icon={<IconAlertTriangle />} color="yellow">
<Text>
Please note that Homarr removes secrets from the configuration for security reasons.
Thus, you can only either define or unset any credentials. Your credentials act as the
main access for your integrations and you should <b>never</b> share them with anybody
else. Make sure to <b>store and manage your secrets safely</b>.
</Text>
</Alert>
</>
)}
</Tabs.Panel>
);
};