mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 09:25:47 +01:00
@@ -0,0 +1,112 @@
|
||||
import { Alert, Button, Grid, Input, LoadingOverlay, Slider } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconCheck, IconReload } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { GridstackBreakpoints } from '../../../../constants/gridstack-breakpoints';
|
||||
import { sleep } from '../../../../tools/client/time';
|
||||
import { GridstackSettingsType } from '../../../../types/settings';
|
||||
|
||||
export const GridstackConfiguration = () => {
|
||||
const { t } = useTranslation(['settings/customization/gridstack', 'common']);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
if (!config || !configName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValue = config.settings.customization?.gridstack ?? {
|
||||
columnCountSmall: 3,
|
||||
columnCountMedium: 6,
|
||||
columnCountLarge: 12,
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValue,
|
||||
});
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSubmit = async (values: GridstackSettingsType) => {
|
||||
setIsSaving(true);
|
||||
|
||||
await sleep(250);
|
||||
await updateConfig(
|
||||
configName,
|
||||
(previousConfig) => ({
|
||||
...previousConfig,
|
||||
settings: {
|
||||
...previousConfig.settings,
|
||||
customization: {
|
||||
...previousConfig.settings.customization,
|
||||
gridstack: values,
|
||||
},
|
||||
},
|
||||
}),
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
form.resetDirty();
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} style={{ position: 'relative' }}>
|
||||
<LoadingOverlay overlayBlur={2} visible={isSaving} radius="md" />
|
||||
<Input.Wrapper
|
||||
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.small') })}
|
||||
description={t('columnsCount.descriptionPreset', { pixels: GridstackBreakpoints.medium })}
|
||||
mb="md"
|
||||
>
|
||||
<Slider min={1} max={8} mt="xs" {...form.getInputProps('columnCountSmall')} />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.medium') })}
|
||||
description={t('columnsCount.descriptionPreset', { pixels: GridstackBreakpoints.large })}
|
||||
mb="md"
|
||||
>
|
||||
<Slider min={3} max={16} mt="xs" {...form.getInputProps('columnCountMedium')} />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.large') })}
|
||||
description={t('columnsCount.descriptionExceedsPreset', {
|
||||
pixels: GridstackBreakpoints.large,
|
||||
})}
|
||||
>
|
||||
<Slider min={5} max={20} mt="xs" {...form.getInputProps('columnCountLarge')} />
|
||||
</Input.Wrapper>
|
||||
{form.isDirty() && (
|
||||
<Alert variant="light" color="yellow" title="Unsaved changes" my="md">
|
||||
{t('unsavedChanges')}
|
||||
</Alert>
|
||||
)}
|
||||
<Grid mt="md">
|
||||
<Grid.Col md={6} xs={12}>
|
||||
<Button variant="light" leftIcon={<IconCheck size={18} />} type="submit" fullWidth>
|
||||
{t('applyChanges')}
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
<Grid.Col md={6} xs={12}>
|
||||
<Button
|
||||
variant="light"
|
||||
leftIcon={<IconReload size={18} />}
|
||||
onClick={() =>
|
||||
form.setValues({
|
||||
columnCountSmall: 3,
|
||||
columnCountMedium: 6,
|
||||
columnCountLarge: 12,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
{t('defaultValues')}
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +1,39 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Checkbox,
|
||||
createStyles,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Indicator,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { createDummyArray } from '../../../../tools/client/arrays';
|
||||
import { CustomizationSettingsType } from '../../../../types/settings';
|
||||
import { Logo } from '../../../layout/Logo';
|
||||
|
||||
interface LayoutSelectorProps {
|
||||
defaultLayout: CustomizationSettingsType['layout'] | undefined;
|
||||
}
|
||||
|
||||
// TODO: add translations
|
||||
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
||||
export const LayoutSelector = () => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
|
||||
const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true);
|
||||
const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false);
|
||||
const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false);
|
||||
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
|
||||
const layoutSettings = config?.settings.customization.layout;
|
||||
|
||||
const { colors, colorScheme } = useMantineTheme();
|
||||
const [leftSidebar, setLeftSidebar] = useState(layoutSettings?.enabledLeftSidebar ?? true);
|
||||
const [rightSidebar, setRightSidebar] = useState(layoutSettings?.enabledRightSidebar ?? true);
|
||||
const [docker, setDocker] = useState(layoutSettings?.enabledDocker ?? false);
|
||||
const [ping, setPing] = useState(layoutSettings?.enabledPing ?? false);
|
||||
const [searchBar, setSearchBar] = useState(layoutSettings?.enabledSearchbar ?? false);
|
||||
const { t } = useTranslation('settings/common');
|
||||
|
||||
if (!configName) return null;
|
||||
if (!configName || !config) return null;
|
||||
|
||||
const handleChange = (
|
||||
key: keyof CustomizationSettingsType['layout'],
|
||||
@@ -68,99 +64,140 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const enabledPing = layoutSettings?.enabledPing ?? false;
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Title order={6}>{t('layout.title')}</Title>
|
||||
|
||||
<Paper px="xs" py={4} withBorder>
|
||||
<Group position="apart">
|
||||
<Logo size="xs" />
|
||||
<Group spacing={5}>
|
||||
{searchBar ? (
|
||||
<Paper
|
||||
style={{
|
||||
height: 10,
|
||||
backgroundColor: colorScheme === 'dark' ? colors.gray[8] : colors.gray[1],
|
||||
}}
|
||||
p={2}
|
||||
w={60}
|
||||
/>
|
||||
) : null}
|
||||
{docker ? <ActionIcon size={10} disabled /> : null}
|
||||
<>
|
||||
<Stack spacing={0} mb="md">
|
||||
<Title order={6}>{t('layout.preview.title')}</Title>
|
||||
<Text color="dimmed" size="xs">
|
||||
{t('layout.preview.subtitle')}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack spacing="xs">
|
||||
<Paper px="xs" py={4} withBorder>
|
||||
<Group position="apart">
|
||||
<Logo size="xs" />
|
||||
<Group spacing={5}>
|
||||
{searchBar && <PlaceholderElement width={60} height={10} />}
|
||||
{docker && <PlaceholderElement width={10} height={10} />}
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Group align="stretch">
|
||||
{leftSidebar && (
|
||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||
<Flex align="center" justify="center" direction="column">
|
||||
<Text align="center">{t('layout.sidebar')}</Text>
|
||||
<Text color="dimmed" size="xs" align="center">
|
||||
Only for
|
||||
<br />
|
||||
apps &<br />
|
||||
integrations
|
||||
</Text>
|
||||
</Flex>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Paper className={classes.primaryWrapper} p="xs" withBorder>
|
||||
<Text align="center">{t('layout.main')}</Text>
|
||||
<Text color="dimmed" size="xs" align="center">
|
||||
{t('layout.cannotturnoff')}
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
{rightSidebar && (
|
||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||
<Flex align="center" justify="center" direction="column">
|
||||
<Text align="center">{t('layout.sidebar')}</Text>
|
||||
<Text color="dimmed" size="xs" align="center">
|
||||
Only for
|
||||
<br />
|
||||
apps &<br />
|
||||
integrations
|
||||
</Text>
|
||||
<Flex gap={6}>
|
||||
{leftSidebar && (
|
||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||
<Flex gap={5} wrap="wrap">
|
||||
{createDummyArray(5).map((item, index) => (
|
||||
<PlaceholderElement
|
||||
height={index % 4 === 0 ? 60 + 5 : 30}
|
||||
width={30}
|
||||
key={`example-item-right-sidebard-${index}`}
|
||||
index={index}
|
||||
hasPing={enabledPing}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Paper className={classes.primaryWrapper} p="xs" withBorder>
|
||||
<Flex gap={5} wrap="wrap">
|
||||
{createDummyArray(10).map((item, index) => (
|
||||
<PlaceholderElement
|
||||
height={30}
|
||||
width={index % 5 === 0 ? 60 : 30}
|
||||
key={`example-item-main-${index}`}
|
||||
index={index}
|
||||
hasPing={enabledPing}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Paper>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Stack spacing="xs">
|
||||
<Checkbox
|
||||
label={t('layout.enablelsidebar')}
|
||||
description={t('layout.enablelsidebardesc')}
|
||||
checked={leftSidebar}
|
||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enablersidebar')}
|
||||
description={t('layout.enablersidebardesc')}
|
||||
checked={rightSidebar}
|
||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enablesearchbar')}
|
||||
checked={searchBar}
|
||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enabledocker')}
|
||||
checked={docker}
|
||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enableping')}
|
||||
checked={ping}
|
||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||
/>
|
||||
{rightSidebar && (
|
||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||
<Flex gap={5} align="start" wrap="wrap">
|
||||
{createDummyArray(5).map((item, index) => (
|
||||
<PlaceholderElement
|
||||
height={30}
|
||||
width={index % 4 === 0 ? 60 + 5 : 30}
|
||||
key={`example-item-right-sidebard-${index}`}
|
||||
index={index}
|
||||
hasPing={enabledPing}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Paper>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Divider label={t('layout.divider')} labelPosition="center" mt="md" mb="xs" />
|
||||
<Stack spacing="xs">
|
||||
<Checkbox
|
||||
label={t('layout.enablelsidebar')}
|
||||
description={t('layout.enablelsidebardesc')}
|
||||
checked={leftSidebar}
|
||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enablersidebar')}
|
||||
description={t('layout.enablersidebardesc')}
|
||||
checked={rightSidebar}
|
||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enablesearchbar')}
|
||||
checked={searchBar}
|
||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enabledocker')}
|
||||
checked={docker}
|
||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enableping')}
|
||||
checked={ping}
|
||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BaseElement = ({ height, width }: { height: number; width: number }) => (
|
||||
<Paper
|
||||
sx={(theme) => ({
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.gray[8] : theme.colors.gray[1],
|
||||
})}
|
||||
h={height}
|
||||
p={2}
|
||||
w={width}
|
||||
/>
|
||||
);
|
||||
|
||||
const PlaceholderElement = (props: any) => {
|
||||
const { height, width, hasPing, index } = props;
|
||||
|
||||
if (hasPing) {
|
||||
return (
|
||||
<Indicator
|
||||
position="bottom-end"
|
||||
size={5}
|
||||
offset={10}
|
||||
color={index % 4 === 0 ? 'red' : 'green'}
|
||||
>
|
||||
<BaseElement width={width} height={height} />
|
||||
</Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseElement width={width} height={height} />;
|
||||
};
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
primaryWrapper: {
|
||||
flexGrow: 2,
|
||||
|
||||
Reference in New Issue
Block a user