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,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"
}
}

View File

@@ -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<ServiceType>({
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: (
<Stack>
<Text color="dimmed">If you close, your changes will be discarded and not saved.</Text>
<Button
onClick={() => {
context.closeModal(id);
hideNotification('unsaved-edit-service-modal-changes');
}}
variant="light"
>
Close anyway
</Button>
</Stack>
),
});
return;
}
context.closeModal(id);
};
return (
<>
<Stack spacing={0} align="center" my="lg">
{form.values.appearance.iconUrl ? (
// disabled because image target is too dynamic for next image cache
// eslint-disable-next-line @next/next/no-img-element
<img
className={classes.serviceImage}
src={form.values.appearance.iconUrl}
width={120}
height={120}
alt="service icon"
/>
) : (
<Image src="/favicon-squared.png" width={120} height={120} />
)}
<Text align="center" weight="bold" size="lg" mt="md">
{form.values.name ?? 'New Service'}
</Text>
</Stack>
<form onSubmit={form.onSubmit(onSubmit)}>
<Tabs defaultValue="general">
<Tabs.List grow>
<Tabs.Tab value="general" icon={<IconAdjustments size={14} />}>
General
</Tabs.Tab>
<Tabs.Tab value="behaviour" icon={<IconClick size={14} />}>
Behaviour
</Tabs.Tab>
<Tabs.Tab value="network" icon={<IconAccessPoint size={14} />}>
Network
</Tabs.Tab>
<Tabs.Tab value="appearance" icon={<IconBrush size={14} />}>
Appearance
</Tabs.Tab>
<Tabs.Tab value="integration" icon={<IconPlug size={14} />}>
Integration
</Tabs.Tab>
</Tabs.List>
<GeneralTab form={form} />
<BehaviourTab form={form} />
<NetworkTab form={form} />
<AppearanceTab form={form} />
<IntegrationTab form={form} />
</Tabs>
<Group position="right" mt={100}>
<Button
leftIcon={<IconDoorExit size={20} />}
px={50}
variant="light"
color="gray"
onClick={tryCloseModal}
>
Cancel
</Button>
<Button type="submit" leftIcon={<IconDeviceFloppy size={20} />} px={50}>
Save
</Button>
</Group>
</form>
</>
);
};
const useStyles = createStyles(() => ({
serviceImage: {
objectFit: 'contain',
},
}));

View File

@@ -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, (values: ServiceType) => ServiceType>;
}
export const AppearanceTab = ({ form }: AppearanceTabProps) => {
const { t } = useTranslation('');
return (
<Tabs.Panel value="appearance" pt="lg">
<TextInput
icon={<IconPhoto />}
label="Service Icon"
variant="default"
defaultValue={form.values.appearance.iconUrl}
{...form.getInputProps('serviceIcon')}
/>
</Tabs.Panel>
);
};

View File

@@ -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, (values: ServiceType) => ServiceType>;
}
export const BehaviourTab = ({ form }: BehaviourTabProps) => {
const { t } = useTranslation('');
return (
<Tabs.Panel value="behaviour" pt="xs">
<TextInput
icon={<IconClick size={16} />}
label="On click url"
placeholder="Override the default service url when clicking on the service"
variant="default"
mb="md"
{...form.getInputProps('onClickUrl')}
/>
<Switch
value="disable_handle"
label="Disable direct moving in edit modus"
description={
<Text color="dimmed" size="sm">
Disables the direct movement of the tile
</Text>
}
mb="md"
{...form.getInputProps('isEditModeMovingDisabled')}
/>
<Switch
value="freze"
label="Freeze tile within edit modus"
description={
<Text color="dimmed" size="sm">
Disables the movement of the tile when moving others
</Text>
}
{...form.getInputProps('isEditModeTileFreezed')}
/>
</Tabs.Panel>
);
};

View File

@@ -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, (values: ServiceType) => ServiceType>;
}
export const GeneralTab = ({ form }: GeneralTabProps) => {
const { t } = useTranslation('');
return (
<Tabs.Panel value="general" pt="lg">
<TextInput
icon={<IconCursorText size={16} />}
label="Service name"
placeholder="My example service"
variant="default"
mb="md"
required
{...form.getInputProps('name')}
/>
<TextInput
icon={<IconLink size={16} />}
label="Service url"
placeholder="https://google.com"
variant="default"
required
{...form.getInputProps('url')}
/>
</Tabs.Panel>
);
};

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>
);
};

View File

@@ -0,0 +1,35 @@
import { Tabs, Switch, MultiSelect } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { StatusCodes } from '../../../../../../tools/acceptableStatusCodes';
import { ServiceType } from '../../../../../../types/service';
interface NetworkTabProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
}
export const NetworkTab = ({ form }: NetworkTabProps) => {
const { t } = useTranslation('');
return (
<Tabs.Panel value="network" pt="lg">
<Switch
label="Enable status checker"
mb="md"
defaultChecked={form.values.network.enabledStatusChecker}
{...form.getInputProps('network.enabledStatusChecker')}
/>
{form.values.network.enabledStatusChecker && (
<MultiSelect
required
label="HTTP status codes"
data={StatusCodes}
clearable
searchable
defaultValue={form.values.network.okStatus}
variant="default"
{...form.getInputProps('network.statusCodes')}
/>
)}
</Tabs.Panel>
);
};

View File

@@ -0,0 +1,69 @@
import { Stack, Text, Title } from '@mantine/core';
import {
IconArrowsUpDown,
IconCalendarTime,
IconClock,
IconCloudRain,
IconFileDownload,
} from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { GenericAvailableElementType } from '../Shared/GenericElementType';
import { SelectorBackArrow } from '../Shared/SelectorBackArrow';
interface AvailableIntegrationElementsProps {
onClickBack: () => void;
}
export const AvailableIntegrationElements = ({
onClickBack,
}: AvailableIntegrationElementsProps) => {
const { t } = useTranslation('layout/element-selector/selector');
return (
<>
<SelectorBackArrow onClickBack={onClickBack} />
<Text mb="md" color="dimmed">
Integrations interact with your services, to provide you with more control over your
applications. They usually require a few configurations before use.
</Text>
<Stack spacing="xs">
<GenericAvailableElementType
name="Usenet downloads"
description="Display and manage your Usenet downloads directly from Homarr"
image={<IconFileDownload />}
/>
<GenericAvailableElementType
name="BitTorrent downloads"
description="Display and manage your Torrent downloads directly from Homarr"
image={<IconFileDownload />}
/>
<GenericAvailableElementType
name="Calendar"
description="Integrate your Sonarr, Radarr, Lidarr and Readarr releases in a calendar"
image={<IconCalendarTime />}
/>
<GenericAvailableElementType
name="Date & Time"
description="Display the current date & time"
image={<IconClock />}
/>
<GenericAvailableElementType
name="Weather"
description="Display the current weather of a specified location"
image={<IconCloudRain />}
/>
<GenericAvailableElementType
name="Dash."
description="Display hardware data in realtime on your dashboard"
image="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/dashdot.png"
/>
<GenericAvailableElementType
name="Torrrent network traffic"
description="Display the current download & upload of your torrent clients"
image={<IconArrowsUpDown />}
/>
</Stack>
</>
);
};

View File

@@ -0,0 +1,91 @@
import { Group, Space, Stack, Text, UnstyledButton } from '@mantine/core';
import { IconBox, IconPlug, IconTextResize } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { ReactNode } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { openContextModalGeneric } from '../../../../../../tools/mantineModalManagerExtensions';
import { ServiceType } from '../../../../../../types/service';
import { useStyles } from '../Shared/styles';
interface AvailableElementTypesProps {
onOpenIntegrations: () => void;
onOpenStaticElements: () => void;
}
export const AvailableElementTypes = ({
onOpenIntegrations,
onOpenStaticElements,
}: AvailableElementTypesProps) => {
const { t } = useTranslation('layout/element-selector/selector');
return (
<>
<Text color="dimmed">{t('modal.text')}</Text>
<Space h="lg" />
<Group spacing="md" grow>
<ElementItem
name="Service"
icon={<IconBox size={40} strokeWidth={1.3} />}
onClick={() => {
openContextModalGeneric<{ service: ServiceType }>({
modal: 'editService',
innerProps: {
service: {
id: uuidv4(),
name: 'Your service',
url: 'https://homarr.dev',
appearance: {
iconUrl: '/imgs/logo/logo.png',
},
network: {
enabledStatusChecker: false,
okStatus: [],
},
integration: {
type: 'deluge',
properties: {},
},
},
},
size: 'xl',
});
}}
/>
<ElementItem
name="Integration"
icon={<IconPlug size={40} strokeWidth={1.3} />}
onClick={onOpenIntegrations}
/>
<ElementItem
name="Static Element"
icon={<IconTextResize size={40} strokeWidth={1.3} />}
onClick={onOpenStaticElements}
/>
</Group>
</>
);
};
interface ElementItemProps {
icon: ReactNode;
name: string;
onClick: () => void;
}
const ElementItem = ({ name, icon, onClick }: ElementItemProps) => {
const { classes, cx } = useStyles();
return (
<UnstyledButton
className={cx(classes.elementButton, classes.styledButton)}
onClick={onClick}
py="md"
>
<Stack className={classes.elementStack} align="center" spacing={5}>
{icon}
<Text className={classes.elementName} weight={500} size="sm">
{name}
</Text>
</Stack>
</UnstyledButton>
);
};

View File

@@ -0,0 +1,61 @@
import { Button, Stack, Text } from '@mantine/core';
import { IconChevronRight } from '@tabler/icons';
import Image from 'next/image';
import React, { ReactNode } from 'react';
import { useStyles } from './styles';
interface GenericAvailableElementTypeProps {
name: string;
description?: string;
image: string | ReactNode;
disabled?: boolean;
}
export const GenericAvailableElementType = ({
name,
description,
image,
disabled,
}: GenericAvailableElementTypeProps) => {
const { classes } = useStyles();
const Icon = () => {
if (React.isValidElement(image)) {
return <>{image}</>;
}
return <Image src={image as string} width={24} height={24} />;
};
return (
<Button
className={classes.styledButton}
leftIcon={<Icon />}
rightIcon={<IconChevronRight opacity={0.5} />}
styles={{
root: {
height: 'auto',
padding: '8px 12px',
},
inner: {
justifyContent: 'left',
},
label: {
justifyContent: 'space-between',
width: '100%',
},
}}
disabled={disabled}
fullWidth
>
<Stack spacing={0}>
<Text className={classes.elementText}>{name}</Text>
{description && (
<Text className={classes.elementText} size="xs" color="dimmed">
{description}
</Text>
)}
</Stack>
</Button>
);
};

View File

@@ -0,0 +1,21 @@
import { ActionIcon, Button, Group, Text } from '@mantine/core';
import { IconArrowNarrowLeft } from '@tabler/icons';
interface SelectorBackArrowProps {
onClickBack: () => void;
}
export const SelectorBackArrow = ({ title, onClickBack }: SelectorBackArrowProps) => {
return (
<Button
leftIcon={<IconArrowNarrowLeft />}
onClick={onClickBack}
styles={{ inner: { width: 'fit-content' } }}
fullWidth
variant='default'
mb="md"
>
<Text>See all available elements</Text>
</Button>
);
};

View File

@@ -0,0 +1,28 @@
import { createStyles } from '@mantine/core';
export const useStyles = createStyles((theme) => ({
styledButton: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.gray[9] : theme.colors.gray[2],
color: theme.colorScheme === 'dark' ? theme.colors.gray[0] : theme.colors.dark[9],
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.gray[8] : theme.colors.gray[3],
},
},
elementButton: {
width: '100%',
height: '100%',
borderRadius: theme.radius.sm
},
elementStack: {
width: '100%',
},
elementName: {
whiteSpace: 'normal',
textAlign: 'center',
lineHeight: 1.2,
},
elementText: {
lineHeight: 1.2,
whiteSpace: 'normal',
},
}));

View File

@@ -0,0 +1,31 @@
import { Stack, Text, Title } from '@mantine/core';
import { IconCursorText } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { GenericAvailableElementType } from '../Shared/GenericElementType';
import { SelectorBackArrow } from '../Shared/SelectorBackArrow';
interface AvailableStaticTypesProps {
onClickBack: () => void;
}
export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps) => {
const { t } = useTranslation('layout/element-selector/selector');
return (
<>
<SelectorBackArrow onClickBack={onClickBack} />
<Text mb="md" color="dimmed">
Static elements provide you additional control over your dashboard. They are static, because
they don&apos;t integrate with any services and their content never changes.
</Text>
<Stack>
<GenericAvailableElementType
name="Static Text"
description="Display a fixed string on your dashboard"
image={<IconCursorText />}
/>
</Stack>
</>
);
};

View File

@@ -0,0 +1,27 @@
import { ContextModalProps } from '@mantine/modals';
import { useState } from 'react';
import { AvailableIntegrationElements } from './Components/IntegrationsTab/AvailableIntegrationsTab';
import { AvailableElementTypes } from './Components/Overview/AvailableElementsOverview';
import { AvailableStaticTypes } from './Components/StaticElementsTab/AvailableStaticElementsTab';
export const SelectElementModal = ({ context, id }: ContextModalProps) => {
const [activeTab, setActiveTab] = useState<undefined | 'integrations' | 'static_elements'>();
switch (activeTab) {
case undefined:
return (
<AvailableElementTypes
onOpenIntegrations={() => setActiveTab('integrations')}
onOpenStaticElements={() => setActiveTab('static_elements')}
/>
);
case 'integrations':
return <AvailableIntegrationElements onClickBack={() => setActiveTab(undefined)} />;
case 'static_elements':
return <AvailableStaticTypes onClickBack={() => setActiveTab(undefined)} />;
default:
/* default to the main selection tab */
setActiveTab(undefined);
return <></>;
}
};

View File

@@ -0,0 +1,31 @@
import { ActionIcon, Tooltip } from '@mantine/core';
import { openContextModal } from '@mantine/modals';
import { IconApps } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { v4 as uuidv4 } from 'uuid';
import { openContextModalGeneric } from '../../../../../tools/mantineModalManagerExtensions';
import { ServiceType } from '../../../../../types/service';
export const AddElementAction = () => {
const { t } = useTranslation('layout/add-service-app-shelf');
return (
<Tooltip withinPortal label={t('actionIcon.tooltip')}>
<ActionIcon
variant="default"
radius="md"
size="xl"
color="blue"
onClick={() =>
openContextModal({
modal: 'selectElement',
title: 'Add an element to your dashboard',
innerProps: {},
})
}
>
<IconApps />
</ActionIcon>
</Tooltip>
);
};

View File

@@ -8,6 +8,7 @@ import { Logo } from '../Logo';
import { useCardStyles } from '../useCardStyles';
import { SettingsMenu } from './SettingsMenu';
import { ToolsMenu } from './ToolsMenu';
import { AddElementAction } from './Actions/AddElementAction/AddElementAction';
export const HeaderHeight = 64;
@@ -24,6 +25,7 @@ export function Header(props: any) {
<Group position="right" noWrap>
<Search />
<AddItemShelfButton />
<AddElementAction />
<ToolsMenu />
<SettingsMenu />
</Group>

View File

@@ -9,6 +9,8 @@ import { appWithTranslation } from 'next-i18next';
import { AppProps } from 'next/app';
import Head from 'next/head';
import { useState } from 'react';
import { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal';
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
import { ConfigProvider } from '../config/provider';
import { ColorTheme } from '../tools/color';
import { queryClient } from '../tools/queryClient';
@@ -75,7 +77,9 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
withNormalizeCSS
>
<NotificationsProvider limit={4} position="bottom-left">
<ModalsProvider>
<ModalsProvider
modals={{ editService: EditServiceModal, selectElement: SelectElementModal }}
>
<ConfigProvider>
<Component {...pageProps} />
</ConfigProvider>

View File

@@ -0,0 +1,21 @@
export const StatusCodes = [
{ value: '200', label: '200 - OK', group: 'Sucessful responses' },
{ value: '204', label: '204 - No Content', group: 'Sucessful responses' },
{ value: '301', label: '301 - Moved Permanently', group: 'Redirection responses' },
{ value: '302', label: '302 - Found / Moved Temporarily', group: 'Redirection responses' },
{ value: '304', label: '304 - Not Modified', group: 'Redirection responses' },
{ value: '307', label: '307 - Temporary Redirect', group: 'Redirection responses' },
{ value: '308', label: '308 - Permanent Redirect', group: 'Redirection responses' },
{ value: '400', label: '400 - Bad Request', group: 'Client error responses' },
{ value: '401', label: '401 - Unauthorized', group: 'Client error responses' },
{ value: '403', label: '403 - Forbidden', group: 'Client error responses' },
{ value: '404', label: '404 - Not Found', group: 'Client error responses' },
{ value: '405', label: '405 - Method Not Allowed', group: 'Client error responses' },
{ value: '408', label: '408 - Request Timeout', group: 'Client error responses' },
{ value: '410', label: '410 - Gone', group: 'Client error responses' },
{ value: '429', label: '429 - Too Many Requests', group: 'Client error responses' },
{ value: '500', label: '500 - Internal Server Error', group: 'Server error responses' },
{ value: '502', label: '502 - Bad Gateway', group: 'Server error responses' },
{ value: '503', label: '503 - Service Unavailable', group: 'Server error responses' },
{ value: '054', label: '504 - Gateway Timeout Error', group: 'Server error responses' },
];

View File

@@ -0,0 +1,6 @@
import { openContextModal } from '@mantine/modals';
import { OpenContextModal } from '@mantine/modals/lib/context';
export const openContextModalGeneric = <T extends Record<string, unknown>>(
payload: OpenContextModal<T> & { modal: string }
) => openContextModal(payload);

View File

@@ -4,6 +4,7 @@ export const dashboardNamespaces = [
'layout/add-service-app-shelf',
'layout/app-shelf-menu',
'layout/tools',
'layout/element-selector/selector',
'settings/common',
'settings/general/theme-selector',
'settings/general/config-changer',