mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
✨ Add mantine context modals
This commit is contained in:
9
public/locales/en/layout/element-selector/selector.json
Normal file
9
public/locales/en/layout/element-selector/selector.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/components/Dashboard/Modals/EditService/EditServiceModal.tsx
Normal file
137
src/components/Dashboard/Modals/EditService/EditServiceModal.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -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'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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import { Logo } from '../Logo';
|
|||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
import { SettingsMenu } from './SettingsMenu';
|
import { SettingsMenu } from './SettingsMenu';
|
||||||
import { ToolsMenu } from './ToolsMenu';
|
import { ToolsMenu } from './ToolsMenu';
|
||||||
|
import { AddElementAction } from './Actions/AddElementAction/AddElementAction';
|
||||||
|
|
||||||
export const HeaderHeight = 64;
|
export const HeaderHeight = 64;
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export function Header(props: any) {
|
|||||||
<Group position="right" noWrap>
|
<Group position="right" noWrap>
|
||||||
<Search />
|
<Search />
|
||||||
<AddItemShelfButton />
|
<AddItemShelfButton />
|
||||||
|
<AddElementAction />
|
||||||
<ToolsMenu />
|
<ToolsMenu />
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ 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 { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal';
|
||||||
|
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
|
||||||
import { ConfigProvider } from '../config/provider';
|
import { ConfigProvider } from '../config/provider';
|
||||||
import { ColorTheme } from '../tools/color';
|
import { ColorTheme } from '../tools/color';
|
||||||
import { queryClient } from '../tools/queryClient';
|
import { queryClient } from '../tools/queryClient';
|
||||||
@@ -75,7 +77,9 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
withNormalizeCSS
|
withNormalizeCSS
|
||||||
>
|
>
|
||||||
<NotificationsProvider limit={4} position="bottom-left">
|
<NotificationsProvider limit={4} position="bottom-left">
|
||||||
<ModalsProvider>
|
<ModalsProvider
|
||||||
|
modals={{ editService: EditServiceModal, selectElement: SelectElementModal }}
|
||||||
|
>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
21
src/tools/acceptableStatusCodes.ts
Normal file
21
src/tools/acceptableStatusCodes.ts
Normal 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' },
|
||||||
|
];
|
||||||
6
src/tools/mantineModalManagerExtensions.ts
Normal file
6
src/tools/mantineModalManagerExtensions.ts
Normal 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);
|
||||||
@@ -4,6 +4,7 @@ export const dashboardNamespaces = [
|
|||||||
'layout/add-service-app-shelf',
|
'layout/add-service-app-shelf',
|
||||||
'layout/app-shelf-menu',
|
'layout/app-shelf-menu',
|
||||||
'layout/tools',
|
'layout/tools',
|
||||||
|
'layout/element-selector/selector',
|
||||||
'settings/common',
|
'settings/common',
|
||||||
'settings/general/theme-selector',
|
'settings/general/theme-selector',
|
||||||
'settings/general/config-changer',
|
'settings/general/config-changer',
|
||||||
|
|||||||
Reference in New Issue
Block a user