Merge branch 'gridstack' of https://github.com/manuel-rw/homarr into gridstack
This commit is contained in:
76
src/components/Dashboard/Menu/TileMenu.tsx
Normal file
76
src/components/Dashboard/Menu/TileMenu.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { ActionIcon, Menu, Text } from '@mantine/core';
|
||||
import { openContextModal } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconCheck, IconEdit, IconMenu, IconTrash } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { useColorTheme } from '../../../tools/color';
|
||||
import { ServiceType } from '../../../types/service';
|
||||
|
||||
interface TileMenuProps {
|
||||
service: ServiceType;
|
||||
}
|
||||
|
||||
export const TileMenu = ({ service }: TileMenuProps) => {
|
||||
const { secondaryColor } = useColorTheme();
|
||||
const { t } = useTranslation('layout/app-shelf-menu');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
|
||||
return (
|
||||
<Menu withinPortal width={150} shadow="xl" withArrow radius="md" position="right">
|
||||
<Menu.Target>
|
||||
<ActionIcon>
|
||||
<IconMenu />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>{t('menu.labels.settings')}</Menu.Label>
|
||||
<Menu.Item
|
||||
color={secondaryColor}
|
||||
icon={<IconEdit />}
|
||||
onClick={() =>
|
||||
openContextModal({
|
||||
modal: 'changeTilePosition',
|
||||
innerProps: {
|
||||
type: 'service',
|
||||
tile: service,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('menu.actions.edit')}
|
||||
</Menu.Item>
|
||||
<Menu.Label>{t('menu.labels.dangerZone')}</Menu.Label>
|
||||
<Menu.Item
|
||||
color="red"
|
||||
onClick={(e: any) => {
|
||||
if (!configName) {
|
||||
return;
|
||||
}
|
||||
updateConfig(configName, (previousConfig) => ({
|
||||
...previousConfig,
|
||||
services: previousConfig.services.filter((x) => x.id !== service.id),
|
||||
})).then(() => {
|
||||
showNotification({
|
||||
autoClose: 5000,
|
||||
title: (
|
||||
<Text>
|
||||
Service <b>{service.name}</b> removed successfully!
|
||||
</Text>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <IconCheck />,
|
||||
message: undefined,
|
||||
});
|
||||
});
|
||||
}}
|
||||
icon={<IconTrash />}
|
||||
>
|
||||
{t('menu.actions.delete')}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Button, Flex, Grid, NumberInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { closeModal, ContextModalProps } from '@mantine/modals';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { ServiceType } from '../../../../types/service';
|
||||
import { TileBaseType } from '../../../../types/tile';
|
||||
|
||||
export const ChangePositionModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<{ type: 'service' | 'type'; tile: TileBaseType }>) => {
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
tile: innerProps.tile,
|
||||
},
|
||||
validateInputOnChange: true,
|
||||
validateInputOnBlur: true,
|
||||
});
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!configName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tileAsService = form.values.tile as ServiceType;
|
||||
|
||||
updateConfig(configName, (previous) => ({
|
||||
...previous,
|
||||
services: [...previous.services.filter((x) => x.id === tileAsService.id), tileAsService],
|
||||
}));
|
||||
|
||||
closeModal(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Grid>
|
||||
<Grid.Col xs={12} md={6}>
|
||||
<NumberInput
|
||||
max={99}
|
||||
min={0}
|
||||
label="X Position"
|
||||
description="0 or higher"
|
||||
{...form.getInputProps('tile.shape.location.x')}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col xs={12} md={6}>
|
||||
<NumberInput
|
||||
max={99}
|
||||
min={0}
|
||||
label="Y Position"
|
||||
description="0 or higher"
|
||||
{...form.getInputProps('tile.shape.location.y')}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col xs={12} md={6}>
|
||||
<NumberInput
|
||||
max={24}
|
||||
min={1}
|
||||
label="Width"
|
||||
description="Between 1 and 24"
|
||||
{...form.getInputProps('tile.shape.size.width')}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col xs={12} md={6}>
|
||||
<NumberInput
|
||||
max={24}
|
||||
min={1}
|
||||
label="Height"
|
||||
description="Between 1 and 24"
|
||||
{...form.getInputProps('tile.shape.size.height')}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Flex justify="end" gap="sm" mt="md">
|
||||
<Button onClick={() => closeModal(id)} variant="light" color="gray">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Change Position</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +1,30 @@
|
||||
import Image from 'next/image';
|
||||
import { Button, createStyles, Group, Stack, Tabs, Text } from '@mantine/core';
|
||||
import { Alert, Button, createStyles, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { closeModal, ContextModalProps } from '@mantine/modals';
|
||||
import { ContextModalProps } from '@mantine/modals';
|
||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconAccessPoint,
|
||||
IconAdjustments,
|
||||
IconAlertTriangle,
|
||||
IconBrush,
|
||||
IconClick,
|
||||
IconDeviceFloppy,
|
||||
IconDoorExit,
|
||||
IconPlug,
|
||||
} from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||
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 { 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';
|
||||
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 = ({
|
||||
context,
|
||||
@@ -27,16 +33,66 @@ export const EditServiceModal = ({
|
||||
}: ContextModalProps<{ service: ServiceType }>) => {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
const { name: configName, config } = useConfigContext();
|
||||
const updateConfig = useConfigStore((store) => store.updateConfig);
|
||||
|
||||
const form = useForm<ServiceType>({
|
||||
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: {
|
||||
iconUrl: (url: string) => {
|
||||
if (url.length < 1) {
|
||||
return 'This field is required';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
behaviour: {
|
||||
onClickUrl: (url: string) => {
|
||||
if (url === undefined || url.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!url.match(serviceUrlRegex)) {
|
||||
return 'Uri override is not a valid uri';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
validateInputOnChange: true,
|
||||
});
|
||||
|
||||
const onSubmit = (values: ServiceType) => {
|
||||
console.log('form submitted');
|
||||
console.log(values);
|
||||
if (!configName) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateConfig(configName, (previousConfig) => ({
|
||||
...previousConfig,
|
||||
services: [...previousConfig.services.filter((x) => x.id !== form.values.id), form.values],
|
||||
}));
|
||||
|
||||
// also close the parent modal
|
||||
context.closeAll();
|
||||
};
|
||||
|
||||
const [activeTab, setActiveTab] = useState<EditServiceModalTab>('general');
|
||||
|
||||
const tryCloseModal = () => {
|
||||
if (form.isDirty()) {
|
||||
showNotification({
|
||||
@@ -63,8 +119,27 @@ export const EditServiceModal = ({
|
||||
context.closeModal(id);
|
||||
};
|
||||
|
||||
const validationErrors = Object.keys(form.errors);
|
||||
|
||||
const ValidationErrorIndicator = ({ keys }: { keys: string[] }) => {
|
||||
const relevantErrors = validationErrors.filter((x) => keys.includes(x));
|
||||
|
||||
return (
|
||||
<ThemeIcon opacity={relevantErrors.length === 0 ? 0 : 1} color="red" variant="light">
|
||||
<IconAlertTriangle size={15} />
|
||||
</ThemeIcon>
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
{form.values.appearance.iconUrl ? (
|
||||
// disabled because image target is too dynamic for next image cache
|
||||
@@ -84,27 +159,52 @@ export const EditServiceModal = ({
|
||||
{form.values.name ?? 'New Service'}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Tabs defaultValue="general">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onTabChange={(tab) => setActiveTab(tab as EditServiceModalTab)}
|
||||
defaultValue="general"
|
||||
>
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="general" icon={<IconAdjustments size={14} />}>
|
||||
<Tabs.Tab
|
||||
rightSection={<ValidationErrorIndicator keys={['name', 'url']} />}
|
||||
icon={<IconAdjustments size={14} />}
|
||||
value="general"
|
||||
>
|
||||
General
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="behaviour" icon={<IconClick size={14} />}>
|
||||
<Tabs.Tab
|
||||
rightSection={<ValidationErrorIndicator keys={['behaviour.onClickUrl']} />}
|
||||
icon={<IconClick size={14} />}
|
||||
value="behaviour"
|
||||
>
|
||||
Behaviour
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="network" icon={<IconAccessPoint size={14} />}>
|
||||
<Tabs.Tab
|
||||
rightSection={<ValidationErrorIndicator keys={[]} />}
|
||||
icon={<IconAccessPoint size={14} />}
|
||||
value="network"
|
||||
>
|
||||
Network
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="appearance" icon={<IconBrush size={14} />}>
|
||||
<Tabs.Tab
|
||||
rightSection={<ValidationErrorIndicator keys={['appearance.iconUrl']} />}
|
||||
icon={<IconBrush size={14} />}
|
||||
value="appearance"
|
||||
>
|
||||
Appearance
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="integration" icon={<IconPlug size={14} />}>
|
||||
<Tabs.Tab
|
||||
rightSection={<ValidationErrorIndicator keys={[]} />}
|
||||
icon={<IconPlug size={14} />}
|
||||
value="integration"
|
||||
>
|
||||
Integration
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<GeneralTab form={form} />
|
||||
<GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} />
|
||||
<BehaviourTab form={form} />
|
||||
<NetworkTab form={form} />
|
||||
<AppearanceTab form={form} />
|
||||
@@ -112,16 +212,10 @@ export const EditServiceModal = ({
|
||||
</Tabs>
|
||||
|
||||
<Group position="right" mt={100}>
|
||||
<Button
|
||||
leftIcon={<IconDoorExit size={20} />}
|
||||
px={50}
|
||||
variant="light"
|
||||
color="gray"
|
||||
onClick={tryCloseModal}
|
||||
>
|
||||
<Button onClick={tryCloseModal} px={50} variant="light" color="gray">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" leftIcon={<IconDeviceFloppy size={20} />} px={50}>
|
||||
<Button disabled={!form.isValid()} px={50} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Tabs, TextInput, createStyles } from '@mantine/core';
|
||||
import Image from 'next/image';
|
||||
import { createStyles, Flex, 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';
|
||||
import { IconSelector } from './IconSelector/IconSelector';
|
||||
|
||||
interface AppearanceTabProps {
|
||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
||||
@@ -12,32 +13,56 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => {
|
||||
const { t } = useTranslation('');
|
||||
const { classes } = useStyles();
|
||||
|
||||
const Image = () => {
|
||||
if (form.values.appearance.iconUrl !== undefined) {
|
||||
const PreviewImage = () => {
|
||||
if (form.values.appearance.iconUrl !== undefined && form.values.appearance.iconUrl.length > 0) {
|
||||
// disabled due to too many dynamic targets for next image cache
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img className={classes.iconImage} src={form.values.appearance.iconUrl} alt="jife" />;
|
||||
return <img className={classes.iconImage} src={form.values.appearance.iconUrl} alt="" />;
|
||||
}
|
||||
|
||||
return <IconPhoto />;
|
||||
return (
|
||||
<Image
|
||||
src="/imgs/logo/logo.png"
|
||||
width={20}
|
||||
height={20}
|
||||
objectFit="contain"
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs.Panel value="appearance" pt="lg">
|
||||
<TextInput
|
||||
icon={<Image />}
|
||||
label="Service Icon"
|
||||
variant="default"
|
||||
defaultValue={form.values.appearance.iconUrl}
|
||||
{...form.getInputProps('appearance.iconUrl')}
|
||||
withAsterisk
|
||||
required
|
||||
/>
|
||||
<Flex gap={5}>
|
||||
<TextInput
|
||||
defaultValue={form.values.appearance.iconUrl}
|
||||
className={classes.textInput}
|
||||
icon={<PreviewImage />}
|
||||
label="Service Icon"
|
||||
description="Logo of your service displayed in your dashboard. Must return a body content containg an image"
|
||||
variant="default"
|
||||
withAsterisk
|
||||
required
|
||||
{...form.getInputProps('appearance.iconUrl')}
|
||||
/>
|
||||
<IconSelector
|
||||
onChange={(item) =>
|
||||
form.setValues({
|
||||
appearance: {
|
||||
iconUrl: item.url,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Tabs.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
textInput: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
iconImage: {
|
||||
objectFit: 'contain',
|
||||
width: 20,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
createStyles,
|
||||
Divider,
|
||||
Flex,
|
||||
Loader,
|
||||
Popover,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconSearch, IconX } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants';
|
||||
import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery';
|
||||
import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem';
|
||||
import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository';
|
||||
|
||||
interface IconSelectorProps {
|
||||
onChange: (icon: IconSelectorItem) => void;
|
||||
}
|
||||
|
||||
export const IconSelector = ({ onChange }: IconSelectorProps) => {
|
||||
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
||||
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
||||
converter: (item) => ({
|
||||
url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${item.name}`,
|
||||
}),
|
||||
});
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const { classes } = useStyles();
|
||||
|
||||
if (isLoading || !data) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
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 isTruncated =
|
||||
slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length;
|
||||
|
||||
return (
|
||||
<Popover width={310}>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
className={classes.actionIcon}
|
||||
variant="default"
|
||||
leftIcon={<IconSearch size={20} />}
|
||||
>
|
||||
Icon Picker
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Stack pt={4}>
|
||||
<TextInput
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.currentTarget.value)}
|
||||
placeholder="Search for icons..."
|
||||
variant="filled"
|
||||
rightSection={
|
||||
<ActionIcon onClick={() => setSearchTerm('')}>
|
||||
<IconX opacity={0.5} size={20} strokeWidth={2} />
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
|
||||
<ScrollArea style={{ height: 250 }} type="always">
|
||||
<Flex gap={4} wrap="wrap" pr={15}>
|
||||
{slicedFilteredItems.map((item) => (
|
||||
<ActionIcon onClick={() => onChange(item)} size={40} p={3}>
|
||||
<img className={classes.icon} src={item.url} alt="" />
|
||||
</ActionIcon>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
{isTruncated && (
|
||||
<Stack spacing="xs" pr={15}>
|
||||
<Divider mt={35} mx="xl" />
|
||||
<Title order={6} color="dimmed" align="center">
|
||||
Search is limited to {ICON_PICKER_SLICE_LIMIT} icons
|
||||
</Title>
|
||||
<Text color="dimmed" align="center" size="sm">
|
||||
To keep things snappy and fast, the search is limited to {ICON_PICKER_SLICE_LIMIT}{' '}
|
||||
icons. Use the search box to find more icons.
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
flameIcon: {
|
||||
margin: '0 auto',
|
||||
},
|
||||
icon: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
},
|
||||
actionIcon: {
|
||||
alignSelf: 'end',
|
||||
},
|
||||
}));
|
||||
@@ -15,32 +15,16 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => {
|
||||
<TextInput
|
||||
icon={<IconClick size={16} />}
|
||||
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"
|
||||
mb="md"
|
||||
{...form.getInputProps('onClickUrl')}
|
||||
{...form.getInputProps('behaviour.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')}
|
||||
label="Open in new tab"
|
||||
{...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
);
|
||||
|
||||
@@ -1,32 +1,52 @@
|
||||
import { Tabs, TextInput } from '@mantine/core';
|
||||
import { Group, Tabs, Text, 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';
|
||||
import { EditServiceModalTab } from '../type';
|
||||
|
||||
interface GeneralTabProps {
|
||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
||||
openTab: (tab: EditServiceModalTab) => void;
|
||||
}
|
||||
|
||||
export const GeneralTab = ({ form }: GeneralTabProps) => {
|
||||
export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
||||
const { t } = useTranslation('');
|
||||
return (
|
||||
<Tabs.Panel value="general" pt="lg">
|
||||
<TextInput
|
||||
icon={<IconCursorText size={16} />}
|
||||
label="Service name"
|
||||
description="Used for displaying the service on the dashboard"
|
||||
placeholder="My example service"
|
||||
variant="default"
|
||||
mb="md"
|
||||
required
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
icon={<IconLink size={16} />}
|
||||
label="Service url"
|
||||
description={
|
||||
<Text>
|
||||
URL that will be opened when clicking on the service. Can be overwritten using
|
||||
<Text
|
||||
onClick={() => openTab('behaviour')}
|
||||
variant="link"
|
||||
span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
on click URL{' '}
|
||||
</Text>
|
||||
when using external URLs to enhance security.
|
||||
</Text>
|
||||
}
|
||||
placeholder="https://google.com"
|
||||
variant="default"
|
||||
required
|
||||
withAsterisk
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
Group,
|
||||
PasswordInput,
|
||||
Select,
|
||||
SelectItem,
|
||||
Space,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { Group, Select, SelectItem, Text } 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';
|
||||
import { ServiceType } from '../../../../../../../../types/service';
|
||||
|
||||
interface IntegrationSelectorProps {
|
||||
form: UseFormReturnType<ServiceType, (item: ServiceType) => ServiceType>;
|
||||
@@ -62,11 +50,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextExplanation />
|
||||
<Space h="sm" />
|
||||
|
||||
<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"
|
||||
itemComponent={SelectItemComponent}
|
||||
data={data}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
|
||||
<Tabs.Panel value="network" pt="lg">
|
||||
<Switch
|
||||
label="Enable status checker"
|
||||
description="Sends a simple HTTP / HTTPS request to check if your service is online"
|
||||
mb="md"
|
||||
defaultChecked={form.values.network.enabledStatusChecker}
|
||||
{...form.getInputProps('network.enabledStatusChecker')}
|
||||
@@ -22,6 +23,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
|
||||
<MultiSelect
|
||||
required
|
||||
label="HTTP status codes"
|
||||
description="Determines what response codes are allowed for this service to be 'Online'"
|
||||
data={StatusCodes}
|
||||
clearable
|
||||
searchable
|
||||
|
||||
1
src/components/Dashboard/Modals/EditService/Tabs/type.ts
Normal file
1
src/components/Dashboard/Modals/EditService/Tabs/type.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type EditServiceModalTab = 'general' | 'behaviour' | 'network' | 'appereance' | 'integration';
|
||||
@@ -42,10 +42,14 @@ export const AvailableElementTypes = ({
|
||||
okStatus: [],
|
||||
},
|
||||
behaviour: {
|
||||
isOpeningNewTab: false,
|
||||
isOpeningNewTab: true,
|
||||
onClickUrl: '',
|
||||
},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
type: 'sidebar',
|
||||
properties: {
|
||||
location: 'right',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
location: {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ActionIcon, Button, Group, Text } from '@mantine/core';
|
||||
import { Button, Text } from '@mantine/core';
|
||||
import { IconArrowNarrowLeft } from '@tabler/icons';
|
||||
|
||||
interface SelectorBackArrowProps {
|
||||
onClickBack: () => void;
|
||||
}
|
||||
|
||||
export const SelectorBackArrow = ({ title, onClickBack }: SelectorBackArrowProps) => {
|
||||
return (
|
||||
export const SelectorBackArrow = ({ onClickBack }: SelectorBackArrowProps) => (
|
||||
<Button
|
||||
leftIcon={<IconArrowNarrowLeft />}
|
||||
onClick={onClickBack}
|
||||
@@ -18,4 +17,3 @@ export const SelectorBackArrow = ({ title, onClickBack }: SelectorBackArrowProps
|
||||
<Text>See all available elements</Text>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user