feat: improve design in edit service modal

This commit is contained in:
Manuel Ruwe
2022-12-06 20:48:35 +01:00
parent d7bec26ee2
commit b28547777f
11 changed files with 112 additions and 49 deletions

View File

@@ -1,24 +1,23 @@
import { Button, createStyles, Group, Stack, Tabs, Text } from '@mantine/core'; import { Alert, Button, createStyles, Group, Stack, Tabs, Text } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { ContextModalProps } from '@mantine/modals'; import { ContextModalProps } from '@mantine/modals';
import { hideNotification, showNotification } from '@mantine/notifications'; import { hideNotification, showNotification } from '@mantine/notifications';
import { import { IconAccessPoint, IconAdjustments, IconBrush, IconClick, IconPlug } from '@tabler/icons';
IconAccessPoint,
IconAdjustments,
IconBrush,
IconClick,
IconDeviceFloppy,
IconDoorExit,
IconPlug,
} from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { ServiceType } from '../../../../types/service'; import { ServiceType } from '../../../../types/service';
import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab'; import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab';
import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab'; import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab';
import { GeneralTab } from './Tabs/GeneralTab/GeneralTab'; import { GeneralTab } from './Tabs/GeneralTab/GeneralTab';
import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab'; import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab';
import { NetworkTab } from './Tabs/NetworkTab/NetworkTab'; import { NetworkTab } from './Tabs/NetworkTab/NetworkTab';
import { EditServiceModalTab } from './Tabs/type';
const serviceUrlRegex =
'(https?://(?:www.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|https?://(?:www.|(?!www))[a-zA-Z0-9]+.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})';
export const EditServiceModal = ({ export const EditServiceModal = ({
context, context,
@@ -27,16 +26,55 @@ export const EditServiceModal = ({
}: ContextModalProps<{ service: ServiceType }>) => { }: ContextModalProps<{ service: ServiceType }>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { classes } = useStyles(); const { classes } = useStyles();
const { name: configName, config } = useConfigContext();
const updateConfig = useConfigStore((store) => store.updateConfig);
const form = useForm<ServiceType>({ const form = useForm<ServiceType>({
initialValues: innerProps.service, initialValues: innerProps.service,
validate: {
name: (name) => (!name ? 'Name is required' : null),
url: (url) => {
if (!url) {
return 'Url is required';
}
if (!url.match(serviceUrlRegex)) {
return 'Value is not a valid url';
}
return null;
},
appearance: (appearance) => (!appearance.iconUrl ? 'Icon is required' : null),
behaviour: (behaviour) => {
if (behaviour.onClickUrl === undefined || behaviour.onClickUrl.length < 1) {
return null;
}
if (!behaviour.onClickUrl?.match(serviceUrlRegex)) {
return 'Uri override is not a valid uri';
}
return null;
},
},
validateInputOnChange: true,
}); });
const onSubmit = (values: ServiceType) => { const onSubmit = (values: ServiceType) => {
console.log('form submitted');
console.log(values); console.log(values);
if (!configName) {
return;
}
updateConfig(configName, (previousConfig) => ({
...previousConfig,
services: [...previousConfig.services.filter((x) => x.id !== form.values.id), form.values],
}));
}; };
const [activeTab, setActiveTab] = useState<EditServiceModalTab>('general');
const tryCloseModal = () => { const tryCloseModal = () => {
if (form.isDirty()) { if (form.isDirty()) {
showNotification({ showNotification({
@@ -65,6 +103,13 @@ export const EditServiceModal = ({
return ( return (
<> <>
{configName === undefined ||
(config === undefined && (
<Alert color="red">
There was an unexpected problem loading the configuration. Functionality might be
restricted. Please report this incident.
</Alert>
))}
<Stack spacing={0} align="center" my="lg"> <Stack spacing={0} align="center" my="lg">
{form.values.appearance.iconUrl ? ( {form.values.appearance.iconUrl ? (
// disabled because image target is too dynamic for next image cache // disabled because image target is too dynamic for next image cache
@@ -85,7 +130,11 @@ export const EditServiceModal = ({
</Text> </Text>
</Stack> </Stack>
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Tabs defaultValue="general"> <Tabs
value={activeTab}
onTabChange={(tab) => setActiveTab(tab as EditServiceModalTab)}
defaultValue="general"
>
<Tabs.List grow> <Tabs.List grow>
<Tabs.Tab value="general" icon={<IconAdjustments size={14} />}> <Tabs.Tab value="general" icon={<IconAdjustments size={14} />}>
General General
@@ -104,7 +153,7 @@ export const EditServiceModal = ({
</Tabs.Tab> </Tabs.Tab>
</Tabs.List> </Tabs.List>
<GeneralTab form={form} /> <GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} />
<BehaviourTab form={form} /> <BehaviourTab form={form} />
<NetworkTab form={form} /> <NetworkTab form={form} />
<AppearanceTab form={form} /> <AppearanceTab form={form} />
@@ -112,16 +161,10 @@ export const EditServiceModal = ({
</Tabs> </Tabs>
<Group position="right" mt={100}> <Group position="right" mt={100}>
<Button <Button px={50} variant="light" color="gray" onClick={tryCloseModal}>
leftIcon={<IconDoorExit size={20} />}
px={50}
variant="light"
color="gray"
onClick={tryCloseModal}
>
Cancel Cancel
</Button> </Button>
<Button type="submit" leftIcon={<IconDeviceFloppy size={20} />} px={50}> <Button type="submit" px={50}>
Save Save
</Button> </Button>
</Group> </Group>

View File

@@ -31,6 +31,7 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => {
className={classes.textInput} className={classes.textInput}
icon={<Image />} icon={<Image />}
label="Service Icon" label="Service Icon"
description="Logo of your service displayed in your dashboard. Must return a body content containg an image"
variant="default" variant="default"
withAsterisk withAsterisk
required required

View File

@@ -1,6 +1,7 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { import {
ActionIcon, ActionIcon,
Button,
createStyles, createStyles,
Divider, Divider,
Flex, Flex,
@@ -12,7 +13,7 @@ import {
TextInput, TextInput,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { IconFlame, IconSearch, IconX } from '@tabler/icons'; import { IconSearch, IconX } from '@tabler/icons';
import { useState } from 'react'; import { useState } from 'react';
import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants'; import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants';
import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery'; import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery';
@@ -38,7 +39,12 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => {
return <Loader />; return <Loader />;
} }
const filteredItems = searchTerm ? data.filter((x) => x.url.includes(searchTerm)) : data; const replaceCharacters = (value: string) =>
value.toLowerCase().replaceAll(' ', '').replaceAll('-', '');
const filteredItems = searchTerm
? data.filter((x) => replaceCharacters(x.url).includes(replaceCharacters(searchTerm)))
: data;
const slicedFilteredItems = filteredItems.slice(0, ICON_PICKER_SLICE_LIMIT); const slicedFilteredItems = filteredItems.slice(0, ICON_PICKER_SLICE_LIMIT);
const isTruncated = const isTruncated =
slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length; slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length;
@@ -46,9 +52,13 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => {
return ( return (
<Popover width={310}> <Popover width={310}>
<Popover.Target> <Popover.Target>
<ActionIcon className={classes.actionIcon} size={36} variant="default"> <Button
<IconSearch size={20} /> className={classes.actionIcon}
</ActionIcon> variant="default"
leftIcon={<IconSearch size={20} />}
>
Icon Picker
</Button>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<Stack pt={4}> <Stack pt={4}>
@@ -76,7 +86,6 @@ export const IconSelector = ({ onChange }: IconSelectorProps) => {
{isTruncated && ( {isTruncated && (
<Stack spacing="xs" pr={15}> <Stack spacing="xs" pr={15}>
<Divider mt={35} mx="xl" /> <Divider mt={35} mx="xl" />
<IconFlame className={classes.flameIcon} size={40} opacity={0.6} strokeWidth={1} />
<Title order={6} color="dimmed" align="center"> <Title order={6} color="dimmed" align="center">
Search is limited to {ICON_PICKER_SLICE_LIMIT} icons Search is limited to {ICON_PICKER_SLICE_LIMIT} icons
</Title> </Title>

View File

@@ -15,13 +15,17 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => {
<TextInput <TextInput
icon={<IconClick size={16} />} icon={<IconClick size={16} />}
label="On click url" label="On click url"
placeholder="Override the default service url when clicking on the service" description="Overrides the service URL when clicking on the service"
placeholder="URL that should be opened instead when clicking on the service"
variant="default" variant="default"
mb="md" mb="md"
{...form.getInputProps('behaviour.onClickUrl')} {...form.getInputProps('behaviour.onClickUrl')}
/> />
<Switch label="Open in new tab" {...form.getInputProps('behaviour.isOpeningNewTab')} /> <Switch
label="Open in new tab"
{...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })}
/>
</Tabs.Panel> </Tabs.Panel>
); );
}; };

View File

@@ -1,32 +1,46 @@
import { Tabs, TextInput } from '@mantine/core'; import { Group, Tabs, Text, TextInput } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { IconCursorText, IconLink } from '@tabler/icons'; import { IconCursorText, IconLink } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { ServiceType } from '../../../../../../types/service'; import { ServiceType } from '../../../../../../types/service';
import { EditServiceModalTab } from '../type';
interface GeneralTabProps { interface GeneralTabProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
openTab: (tab: EditServiceModalTab) => void;
} }
export const GeneralTab = ({ form }: GeneralTabProps) => { export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
const { t } = useTranslation(''); const { t } = useTranslation('');
return ( return (
<Tabs.Panel value="general" pt="lg"> <Tabs.Panel value="general" pt="lg">
<TextInput <TextInput
icon={<IconCursorText size={16} />} icon={<IconCursorText size={16} />}
label="Service name" label="Service name"
description="Used for displaying the service on the dashboard"
placeholder="My example service" placeholder="My example service"
variant="default" variant="default"
mb="md" mb="md"
required withAsterisk
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<TextInput <TextInput
icon={<IconLink size={16} />} icon={<IconLink size={16} />}
label="Service url" label="Service url"
description={
<Group spacing={3}>
<Text>
URL that will be opened when clicking on the service. Can be overwritten using
</Text>
<Text onClick={() => openTab('behaviour')} variant="link">
on click URL
</Text>
<Text>when using external URLs to enhance security.</Text>
</Group>
}
placeholder="https://google.com" placeholder="https://google.com"
variant="default" variant="default"
required withAsterisk
{...form.getInputProps('url')} {...form.getInputProps('url')}
/> />
</Tabs.Panel> </Tabs.Panel>

View File

@@ -62,11 +62,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
return ( return (
<> <>
<TextExplanation />
<Space h="sm" />
<Select <Select
label="Configure this service for the following integration" label="Integration configuration"
description="Treats this service as the selected integration and provides you with per-service configuration"
placeholder="Select your desired configuration" placeholder="Select your desired configuration"
itemComponent={SelectItemComponent} itemComponent={SelectItemComponent}
data={data} data={data}

View File

@@ -1,10 +0,0 @@
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

@@ -14,6 +14,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
<Tabs.Panel value="network" pt="lg"> <Tabs.Panel value="network" pt="lg">
<Switch <Switch
label="Enable status checker" label="Enable status checker"
description="Sends a simple HTTP / HTTPS request to check if your service is online"
mb="md" mb="md"
defaultChecked={form.values.network.enabledStatusChecker} defaultChecked={form.values.network.enabledStatusChecker}
{...form.getInputProps('network.enabledStatusChecker')} {...form.getInputProps('network.enabledStatusChecker')}
@@ -22,6 +23,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
<MultiSelect <MultiSelect
required required
label="HTTP status codes" label="HTTP status codes"
description="Determines what response codes are allowed for this service to be 'Online'"
data={StatusCodes} data={StatusCodes}
clearable clearable
searchable searchable

View File

@@ -0,0 +1 @@
export type EditServiceModalTab = 'general' | 'behaviour' | 'network' | 'appereance' | 'integration';

View File

@@ -42,7 +42,8 @@ export const AvailableElementTypes = ({
okStatus: [], okStatus: [],
}, },
behaviour: { behaviour: {
isOpeningNewTab: false, isOpeningNewTab: true,
onClickUrl: '',
}, },
area: { area: {
type: 'wrapper', type: 'wrapper',

View File

@@ -11,7 +11,7 @@ export interface ServiceType extends TileBaseType {
} }
interface ServiceBehaviourType { interface ServiceBehaviourType {
onClickUrl?: string; onClickUrl: string;
isOpeningNewTab: boolean; isOpeningNewTab: boolean;
} }