Merge branch 'gridstack' of https://github.com/manuel-rw/homarr into gridstack

This commit is contained in:
Meierschlumpf
2022-12-20 07:50:08 +01:00
62 changed files with 453 additions and 574 deletions

View File

@@ -1,9 +1,11 @@
import {
ActionIcon,
Anchor,
Badge,
Button,
createStyles,
Group,
HoverCard,
Modal,
Table,
Text,
@@ -17,6 +19,7 @@ import {
IconVocabulary,
IconWorldWww,
} from '@tabler/icons';
import { motion } from 'framer-motion';
import { InitOptions } from 'i18next';
import { i18n } from 'next-i18next';
import Image from 'next/image';
@@ -27,12 +30,13 @@ import { usePrimaryGradient } from '../layout/useGradient';
interface AboutModalProps {
opened: boolean;
closeModal: () => void;
newVersionAvailable?: string;
}
export const AboutModal = ({ opened, closeModal }: AboutModalProps) => {
export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutModalProps) => {
const { classes } = useStyles();
const colorGradiant = usePrimaryGradient();
const informations = useInformationTableItems();
const informations = useInformationTableItems(newVersionAvailable);
return (
<Modal
@@ -50,8 +54,8 @@ export const AboutModal = ({ opened, closeModal }: AboutModalProps) => {
>
<Text mb="lg">
Homarr is a simple and modern homepage for your server that helps you access all of your
apps in one place. It integrates with the apps you use to display useful information
or control them. It&apos;s easy to install and supports many different devices.
apps in one place. It integrates with the apps you use to display useful information or
control them. It&apos;s easy to install and supports many different devices.
</Text>
<Title order={6} mb="xs" align="center">
@@ -123,7 +127,8 @@ interface ExtendedInitOptions extends InitOptions {
locales: string[];
}
const useInformationTableItems = (): InformationTableItem[] => {
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
// TODO: Fix this to not request. Pass it as a prop.
const colorGradiant = usePrimaryGradient();
let items: InformationTableItem[] = [];
@@ -161,9 +166,41 @@ const useInformationTableItems = (): InformationTableItem[] => {
icon: <IconVersions size={20} />,
label: 'Homarr version',
content: (
<Badge variant="gradient" gradient={colorGradiant}>
{CURRENT_VERSION}
</Badge>
<Group position="right">
<Badge variant="gradient" gradient={colorGradiant}>
{CURRENT_VERSION}
</Badge>
{newVersionAvailable && (
<HoverCard shadow="md" position="top" withArrow>
<HoverCard.Target>
<motion.div
initial={{ scale: 1.2 }}
animate={{
scale: [0.8, 1.10, 1],
rotate: [0, 10, 0],
}}
transition={{ duration: 0.8, ease: 'easeInOut' }}
>
<Badge color="green" variant="filled">
new: {newVersionAvailable}
</Badge>
</motion.div>
</HoverCard.Target>
<HoverCard.Dropdown>
Version{' '}
<b>
<Anchor
target="_blank"
href={`https://github.com/ajnart/homarr/releases/tag/${newVersionAvailable}`}
>
{newVersionAvailable}
</Anchor>
</b>{' '}
is available ! Current version: {CURRENT_VERSION}
</HoverCard.Dropdown>
</HoverCard>
)}
</Group>
),
},
];

View File

@@ -12,7 +12,6 @@ import {
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
import { useLocalStorage } from '@mantine/hooks';
import { useTranslation } from 'next-i18next';
import * as Modules from '../../modules';
import { useConfig } from '../../tools/state';
@@ -38,8 +37,6 @@ const AppShelf = (props: any) => {
const [activeId, setActiveId] = useState(null);
const { colorScheme } = useMantineColorScheme();
const { t } = useTranslation('layout/app-shelf');
const sensors = useSensors(
useSensor(TouchSensor, {
activationConstraint: {

View File

@@ -47,11 +47,9 @@ export default function ConfigChanger() {
);
}
const useConfigsQuery = () => {
return useQuery({
const useConfigsQuery = () => useQuery({
queryKey: ['config/get-all'],
queryFn: fetchConfigs,
});
};
const fetchConfigs = async () => (await (await fetch('/api/configs')).json()) as string[];

View File

@@ -3,8 +3,6 @@ import { DashboardDetailView } from './Views/DetailView';
import { DashboardEditView } from './Views/EditView';
import { useEditModeStore } from './Views/useEditModeStore';
interface DashboardProps {}
export const Dashboard = () => {
const isEditMode = useEditModeStore((x) => x.enabled);

View File

@@ -10,8 +10,7 @@ interface MobileRibbonSidebarDrawerProps {
export const MobileRibbonSidebarDrawer = ({
location,
...props
}: MobileRibbonSidebarDrawerProps) => {
return (
}: MobileRibbonSidebarDrawerProps) => (
<Drawer
position={location}
title={<Title order={4}>{location} sidebar</Title>}
@@ -24,4 +23,3 @@ export const MobileRibbonSidebarDrawer = ({
<DashboardSidebar location={location} />
</Drawer>
);
};

View File

@@ -1,5 +1,6 @@
import { Button, Flex, Grid, NumberInput, Select, SelectItem } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../config/provider';
interface ChangePositionModalProps {
@@ -44,6 +45,8 @@ export const ChangePositionModal = ({
onSubmit(form.values.x, form.values.y, form.values.width, form.values.height);
};
const { t } = useTranslation('common');
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Grid>
@@ -94,9 +97,9 @@ export const ChangePositionModal = ({
<Flex justify="end" gap="sm" mt="md">
<Button onClick={() => onCancel()} variant="light" color="gray">
Cancel
{t('cancel')}
</Button>
<Button type="submit">Save</Button>
<Button type="submit">{t('save')}</Button>
</Flex>
</form>
);

View File

@@ -22,7 +22,7 @@ export const ChangeWidgetPositionModal = ({
updateConfig(
configName,
(prev) => {
let currentWidget = prev.widgets.find((x) => x.id === innerProps.widgetId);
const currentWidget = prev.widgets.find((x) => x.id === innerProps.widgetId);
currentWidget!.shape = {
location: {
x,

View File

@@ -1,4 +1,4 @@
import { Alert, Button, createStyles, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core';
import { Alert, Button, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core';
import { useForm } from '@mantine/form';
import { ContextModalProps } from '@mantine/modals';
import {
@@ -30,7 +30,7 @@ export const EditAppModal = ({
id,
innerProps,
}: ContextModalProps<{ app: AppType; allowAppNamePropagation: boolean }>) => {
const { t } = useTranslation();
const { t } = useTranslation(['layout/modals/add-app', 'common']);
const { name: configName, config } = useConfigContext();
const updateConfig = useConfigStore((store) => store.updateConfig);
const [allowAppNamePropagation, setAllowAppNamePropagation] = useState<boolean>(
@@ -137,68 +137,76 @@ export const EditAppModal = ({
</Stack>
<form onSubmit={form.onSubmit(onSubmit)}>
<Tabs
value={activeTab}
onTabChange={(tab) => setActiveTab(tab as EditAppModalTab)}
defaultValue="general"
<Stack
justify="space-between"
style={{
minHeight: 300,
}}
>
<Tabs.List grow>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={['name', 'url']} />}
icon={<IconAdjustments size={14} />}
value="general"
>
General
</Tabs.Tab>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={['behaviour.onClickUrl']} />}
icon={<IconClick size={14} />}
value="behaviour"
>
Behaviour
</Tabs.Tab>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={[]} />}
icon={<IconAccessPoint size={14} />}
value="network"
>
Network
</Tabs.Tab>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={['appearance.iconUrl']} />}
icon={<IconBrush size={14} />}
value="appearance"
>
Appearance
</Tabs.Tab>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={[]} />}
icon={<IconPlug size={14} />}
value="integration"
>
Integration
</Tabs.Tab>
</Tabs.List>
<Tabs
value={activeTab}
onTabChange={(tab) => setActiveTab(tab as EditAppModalTab)}
defaultValue="general"
radius="md"
>
<Tabs.List grow>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={['name', 'url']} />}
icon={<IconAdjustments size={14} />}
value="general"
>
{t('tabs.general')}
</Tabs.Tab>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={['behaviour.onClickUrl']} />}
icon={<IconClick size={14} />}
value="behaviour"
>
{t('tabs.behaviour')}
</Tabs.Tab>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={[]} />}
icon={<IconAccessPoint size={14} />}
value="network"
>
{t('tabs.network')}
</Tabs.Tab>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={['appearance.iconUrl']} />}
icon={<IconBrush size={14} />}
value="appearance"
>
{t('tabs.appearance')}
</Tabs.Tab>
<Tabs.Tab
rightSection={<ValidationErrorIndicator keys={[]} />}
icon={<IconPlug size={14} />}
value="integration"
>
{t('tabs.integration')}
</Tabs.Tab>
</Tabs.List>
<GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} />
<BehaviourTab form={form} />
<NetworkTab form={form} />
<AppearanceTab
form={form}
disallowAppNameProgagation={() => setAllowAppNamePropagation(false)}
allowAppNamePropagation={allowAppNamePropagation}
/>
<IntegrationTab form={form} />
</Tabs>
<GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} />
<BehaviourTab form={form} />
<NetworkTab form={form} />
<AppearanceTab
form={form}
disallowAppNameProgagation={() => setAllowAppNamePropagation(false)}
allowAppNamePropagation={allowAppNamePropagation}
/>
<IntegrationTab form={form} />
</Tabs>
<Group position="right" mt={100}>
<Button onClick={closeModal} px={50} variant="light" color="gray">
Cancel
</Button>
<Button disabled={!form.isValid()} px={50} type="submit">
Save
</Button>
</Group>
<Group position="right" mt="md">
<Button onClick={closeModal} px={50} variant="light" color="gray">
{t('common:actions.cancel')}
</Button>
<Button disabled={!form.isValid()} px={50} type="submit">
{t('common:actions.save')}
</Button>
</Group>
</Stack>
</form>
</>
);

View File

@@ -16,7 +16,7 @@ export const AppearanceTab = ({
disallowAppNameProgagation,
allowAppNamePropagation,
}: AppearanceTabProps) => {
const { t } = useTranslation('');
const { t } = useTranslation('layout/modals/add-app');
const { classes } = useStyles();
return (
@@ -26,8 +26,8 @@ export const AppearanceTab = ({
defaultValue={form.values.appearance.iconUrl}
className={classes.textInput}
icon={<DebouncedAppIcon form={form} width={20} height={20} />}
label="App Icon"
description="Logo of your app displayed in your dashboard. Must return a body content containg an image"
label={t('appearance.icon.label')}
description={t('appearance.icon.description')}
variant="default"
withAsterisk
required

View File

@@ -29,11 +29,7 @@ interface IconSelectorProps {
allowAppNamePropagation: boolean;
}
export const IconSelector = ({
onChange,
allowAppNamePropagation,
form,
}: IconSelectorProps) => {
export const IconSelector = ({ onChange, allowAppNamePropagation, form }: IconSelectorProps) => {
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
converter: (item) => ({

View File

@@ -1,6 +1,5 @@
import { Tabs, TextInput, Switch, Text } from '@mantine/core';
import { Tabs, Switch } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { IconClick } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { AppType } from '../../../../../../types/app';
@@ -9,21 +8,13 @@ interface BehaviourTabProps {
}
export const BehaviourTab = ({ form }: BehaviourTabProps) => {
const { t } = useTranslation('');
const { t } = useTranslation('layout/modals/add-app');
return (
<Tabs.Panel value="behaviour" pt="xs">
<TextInput
icon={<IconClick size={16} />}
label="On click url"
description="Overrides the app URL when clicking on the app"
placeholder="URL that should be opened instead when clicking on the app"
variant="default"
mb="md"
{...form.getInputProps('behaviour.onClickUrl')}
/>
<Switch
label="Open in new tab"
label={t('behaviour.isOpeningNewTab.label')}
description={t('behaviour.isOpeningNewTab.description')}
{...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })}
/>
</Tabs.Panel>

View File

@@ -1,6 +1,6 @@
import { Tabs, Text, TextInput } from '@mantine/core';
import { Tabs, TextInput } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { IconCursorText, IconLink } from '@tabler/icons';
import { IconClick, IconCursorText, IconLink } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { AppType } from '../../../../../../types/app';
import { EditAppModalTab } from '../type';
@@ -11,43 +11,42 @@ interface GeneralTabProps {
}
export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
const { t } = useTranslation('');
const { t } = useTranslation('layout/modals/add-app');
return (
<Tabs.Panel value="general" pt="lg">
<Tabs.Panel value="general" pt="sm">
<TextInput
icon={<IconCursorText size={16} />}
label="App name"
description="Used for displaying the app on the dashboard"
label={t('general.appname.label')}
description={t('general.appname.description')}
placeholder="My example app"
variant="default"
mb="md"
withAsterisk
required
{...form.getInputProps('name')}
/>
<TextInput
icon={<IconLink size={16} />}
label="App url"
description={
<Text>
URL that will be opened when clicking on the app. 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>
}
label={t('general.internalAddress.label')}
description={t('general.internalAddress.description')}
placeholder="https://google.com"
variant="default"
withAsterisk
required
{...form.getInputProps('url')}
onChange={(e) => {
form.setFieldValue('behaviour.onClickUrl', e.target.value);
form.setFieldValue('url', e.target.value);
}}
/>
<TextInput
icon={<IconClick size={16} />}
label={t('general.externalAddress.label')}
description={t('general.externalAddress.description')}
placeholder="https://homarr.mywebsite.com/"
variant="default"
mb="md"
{...form.getInputProps('behaviour.onClickUrl')}
/>
</Tabs.Panel>
);

View File

@@ -11,6 +11,7 @@ import {
Title,
} from '@mantine/core';
import { TablerIcon } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
interface GenericSecretInputProps {
@@ -32,6 +33,7 @@ export const GenericSecretInput = ({
const Icon = setIcon;
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(false);
const { t } = useTranslation(['layout/modals/add-app', 'common']);
return (
<Card withBorder>
@@ -43,7 +45,7 @@ export const GenericSecretInput = ({
</ThemeIcon>
<Stack spacing={0}>
<Title className={classes.subtitle} order={6}>
{label}
{t(label)}
</Title>
</Stack>
</Group>
@@ -51,13 +53,13 @@ export const GenericSecretInput = ({
<Grid.Col xs={12} md={6}>
<Flex gap={10} justify="end" align="end">
<Button variant="subtle" color="gray" px="xl">
Clear Secret
{t('integration.secrets.clear')}
</Button>
{displayUpdateField === true ? (
<PasswordInput placeholder="new secret" width={200} {...props} />
) : (
<Button onClick={() => setDisplayUpdateField(true)} variant="light">
Set Secret
{t('integration.secrets.update')}
</Button>
)}
</Flex>

View File

@@ -17,7 +17,7 @@ interface IntegrationSelectorProps {
}
export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
const { t } = useTranslation('');
const { t } = useTranslation('layout/modals/add-app');
// TODO: read this out from integrations dynamically.
const data: SelectItem[] = [
@@ -76,9 +76,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
return (
<Select
label="Integration configuration"
description="Treats this app as the selected integration and provides you with per-app configuration"
placeholder="Select your desired configuration"
label={t('integration.type.label')}
description={t('integration.type.description')}
placeholder={t('integration.type.placeholder')}
itemComponent={SelectItemComponent}
data={data}
maxDropdownHeight={400}

View File

@@ -31,9 +31,9 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
let indexInFormValue =
form.values.integration?.properties.findIndex((p) => p.field === property) ?? -1;
if (indexInFormValue === -1) {
const type = Object.entries(integrationFieldDefinitions).find(
const { type } = Object.entries(integrationFieldDefinitions).find(
([k, v]) => k === property
)![1].type;
)![1];
const newProperty: AppIntegrationPropertyType = {
type,
field: property as IntegrationField,

View File

@@ -1,7 +1,7 @@
import { Alert, Divider, Tabs, Text } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { IconAlertTriangle } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { Trans, useTranslation } from 'next-i18next';
import { AppType } from '../../../../../../types/app';
import { IntegrationSelector } from './Components/InputElements/IntegrationSelector';
import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer';
@@ -11,7 +11,7 @@ interface IntegrationTabProps {
}
export const IntegrationTab = ({ form }: IntegrationTabProps) => {
const { t } = useTranslation('');
const { t } = useTranslation('layout/modals/add-app');
const hasIntegrationSelected = form.values.integration?.type;
return (
@@ -20,18 +20,14 @@ export const IntegrationTab = ({ form }: IntegrationTabProps) => {
{hasIntegrationSelected && (
<>
<Divider label="Integration Configuration" labelPosition="center" mt="xl" mb="md" />
<Divider label={t('integration.type.label')} labelPosition="center" mt="xl" mb="md" />
<Text size="sm" color="dimmed" mb="lg">
To update a secret, enter a value and click the save button. To remove a secret, use the
clear button.
{t('integration.secrets.description')}
</Text>
<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>.
<Trans i18nKey="integration.secrets.warning" />
</Text>
</Alert>
</>

View File

@@ -9,12 +9,12 @@ interface NetworkTabProps {
}
export const NetworkTab = ({ form }: NetworkTabProps) => {
const { t } = useTranslation('');
const { t } = useTranslation('layout/modals/add-app');
return (
<Tabs.Panel value="network" pt="lg">
<Switch
label="Enable status checker"
description="Sends a simple HTTP / HTTPS request to check if your app is online"
label={t('network.statusChecker.label')}
description={t('network.statusChecker.description')}
mb="md"
defaultChecked={form.values.network.enabledStatusChecker}
{...form.getInputProps('network.enabledStatusChecker')}
@@ -22,8 +22,8 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
{form.values.network.enabledStatusChecker && (
<MultiSelect
required
label="HTTP status codes"
description="Determines what response codes are allowed for this app to be 'Online'"
label={t('network.statusCodes.label')}
description={t('network.statusCodes.description')}
data={StatusCodes}
clearable
searchable

View File

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

View File

@@ -21,9 +21,7 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
if (!configName) return null;
const getLowestWrapper = () => {
return config?.wrappers.sort((a, b) => a.position - b.position)[0];
};
const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0];
const handleAddition = async () => {
updateConfig(

View File

@@ -1,4 +1,4 @@
import { Card, Center, Text, UnstyledButton } from '@mantine/core';
import { Center, Text, UnstyledButton } from '@mantine/core';
import { NextLink } from '@mantine/next';
import { createStyles } from '@mantine/styles';
import { AppType } from '../../../../types/app';

View File

@@ -1,6 +1,6 @@
import { HomarrCardWrapper } from './HomarrCardWrapper';
import { BaseTileProps } from './type';
export const EmptyTile = ({ className }: BaseTileProps) => {
return <HomarrCardWrapper className={className}>Empty</HomarrCardWrapper>;
};
export const EmptyTile = ({ className }: BaseTileProps) => (
<HomarrCardWrapper className={className}>Empty</HomarrCardWrapper>
);

View File

@@ -1,6 +1,7 @@
import { Card, CardProps } from '@mantine/core';
import { ReactNode } from 'react';
import { useCardStyles } from '../../layout/useCardStyles';
import { useEditModeStore } from '../Views/useEditModeStore';
interface HomarrCardWrapperProps extends CardProps {
children: ReactNode;
@@ -11,11 +12,13 @@ export const HomarrCardWrapper = ({ ...props }: HomarrCardWrapperProps) => {
cx,
classes: { card: cardClass },
} = useCardStyles();
const isEditMode = useEditModeStore((x) => x.enabled);
return (
<Card
{...props}
className={cx(props.className, cardClass)}
withBorder
style={{ cursor: isEditMode ? 'move' : 'default' }}
radius="lg"
shadow="md"
/>

View File

@@ -1,8 +1,8 @@
import Widgets from '../../../../widgets';
import { Button, Group, MultiSelect, Stack, Switch, TextInput } from '@mantine/core';
import { ContextModalProps } from '@mantine/modals';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import Widgets from '../../../../widgets';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { IWidget } from '../../../../widgets/widgets';
@@ -48,7 +48,7 @@ export const WidgetsEditModal = ({
updateConfig(
configName,
(prev) => {
let currentWidget = prev.widgets.find((x) => x.id === innerProps.widgetId);
const currentWidget = prev.widgets.find((x) => x.id === innerProps.widgetId);
currentWidget!.properties = moduleProperties;
return {

View File

@@ -38,7 +38,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
title: null,
innerProps: {
widgetId: integration,
widget: widget,
widget,
},
});
};

View File

@@ -1,7 +1,6 @@
import { Group, Stack } from '@mantine/core';
import { useMemo } from 'react';
import { useConfigContext } from '../../../config/provider';
import { useScreenLargerThan } from '../../../tools/hooks/useScreenLargerThan';
import { useScreenSmallerThan } from '../../../tools/hooks/useScreenSmallerThan';
import { CategoryType } from '../../../types/category';
import { WrapperType } from '../../../types/wrapper';

View File

@@ -1,5 +1,3 @@
import { DashboardView } from './DashboardView';
export const DashboardDetailView = () => {
return <DashboardView />;
};
export const DashboardDetailView = () => <DashboardView />;

View File

@@ -1,5 +1,3 @@
import { DashboardView } from './DashboardView';
export const DashboardEditView = () => {
return <DashboardView />;
};
export const DashboardEditView = () => <DashboardView />;

View File

@@ -1,10 +1,6 @@
import { Card, Group, Title } from '@mantine/core';
import { Group, Title } from '@mantine/core';
import { CategoryType } from '../../../../types/category';
import { IWidgetDefinition } from '../../../../widgets/widgets';
import { HomarrCardWrapper } from '../../Tiles/HomarrCardWrapper';
import { Tiles } from '../../Tiles/tilesDefinitions';
import Widgets from '../../../../widgets';
import { GridstackTileWrapper } from '../../Tiles/TileWrapper';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { useGridstack } from '../gridstack/use-gridstack';
import { CategoryEditMenu } from './CategoryEditMenu';

View File

@@ -1,6 +1,7 @@
import { Button, Group, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { ContextModalProps } from '@mantine/modals';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { CategoryType } from '../../../../types/category';
@@ -31,15 +32,17 @@ export const CategoryEditModal = ({
context.closeModal(id);
};
const { t } = useTranslation('common');
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput data-autoFocus {...form.getInputProps('name')} label="Name of category" />
<Group mt="md" grow>
<Button onClick={() => context.closeModal(id)} variant="light" color="gray">
Cancel
{t('cancel')}
</Button>
<Button type="submit">Save</Button>
<Button type="submit">{t('save')}</Button>
</Group>
</form>
);

View File

@@ -1,9 +1,5 @@
import { Card } from '@mantine/core';
import { RefObject } from 'react';
import { IWidgetDefinition } from '../../../../widgets/widgets';
import { Tiles } from '../../Tiles/tilesDefinitions';
import Widgets from '../../../../widgets';
import { GridstackTileWrapper } from '../../Tiles/TileWrapper';
import { useGridstack } from '../gridstack/use-gridstack';
import { WrapperContent } from '../WrapperContent';
@@ -38,6 +34,5 @@ export const DashboardSidebar = ({ location }: DashboardSidebarProps) => {
);
};
const useMinRowForFullHeight = (wrapperRef: RefObject<HTMLDivElement>) => {
return wrapperRef.current ? Math.floor(wrapperRef.current!.offsetHeight / 64) : 2;
};
const useMinRowForFullHeight = (wrapperRef: RefObject<HTMLDivElement>) =>
wrapperRef.current ? Math.floor(wrapperRef.current!.offsetHeight / 64) : 2;

View File

@@ -18,47 +18,45 @@ interface WrapperContentProps {
};
}
export const WrapperContent = ({ apps, refs, widgets }: WrapperContentProps) => {
return (
<>
{apps?.map((app) => {
const { component: TileComponent, ...tile } = Tiles['app'];
return (
<GridstackTileWrapper
id={app.id}
type="app"
key={app.id}
itemRef={refs.items.current[app.id]}
{...tile}
{...app.shape.location}
{...app.shape.size}
>
<TileComponent className="grid-stack-item-content" app={app} />
</GridstackTileWrapper>
);
})}
{widgets.map((widget) => {
const definition = Widgets[widget.id as keyof typeof Widgets] as
| IWidgetDefinition
| undefined;
if (!definition) return null;
export const WrapperContent = ({ apps, refs, widgets }: WrapperContentProps) => (
<>
{apps?.map((app) => {
const { component: TileComponent, ...tile } = Tiles.app;
return (
<GridstackTileWrapper
id={app.id}
type="app"
key={app.id}
itemRef={refs.items.current[app.id]}
{...tile}
{...app.shape.location}
{...app.shape.size}
>
<TileComponent className="grid-stack-item-content" app={app} />
</GridstackTileWrapper>
);
})}
{widgets.map((widget) => {
const definition = Widgets[widget.id as keyof typeof Widgets] as
| IWidgetDefinition
| undefined;
if (!definition) return null;
return (
<GridstackTileWrapper
type="widget"
key={widget.id}
itemRef={refs.items.current[widget.id]}
id={definition.id}
{...definition.gridstack}
{...widget.shape.location}
{...widget.shape.size}
>
<WidgetWrapper className="grid-stack-item-content" widget={widget} widgetId={widget.id}>
<definition.component className="grid-stack-item-content" widget={widget} />
</WidgetWrapper>
</GridstackTileWrapper>
);
})}
</>
);
};
return (
<GridstackTileWrapper
type="widget"
key={widget.id}
itemRef={refs.items.current[widget.id]}
id={definition.id}
{...definition.gridstack}
{...widget.shape.location}
{...widget.shape.size}
>
<WidgetWrapper className="grid-stack-item-content" widget={widget} widgetId={widget.id}>
<definition.component className="grid-stack-item-content" widget={widget} />
</WidgetWrapper>
</GridstackTileWrapper>
);
})}
</>
);

View File

@@ -4,7 +4,6 @@ import ConfigChanger from '../../Config/ConfigChanger';
import ConfigActions from './Config/ConfigActions';
import LanguageSelect from './Language/LanguageSelect';
import { SearchEngineSelector } from './SearchEngine/SearchEngineSelector';
import { SearchNewTabSwitch } from './SearchNewTabSwitch';
export default function CommonSettings() {
const { config } = useConfigContext();

View File

@@ -39,7 +39,7 @@ export default function ConfigActions() {
<Flex gap="xs" justify="stretch">
<ActionIcon className={classes.actionIcon} onClick={handleDownload} variant="default">
<IconDownload size={20} />
<Text>{t('buttons.download')}</Text>
<Text size="sm">{t('buttons.download')}</Text>
</ActionIcon>
<ActionIcon
className={classes.actionIcon}
@@ -48,11 +48,11 @@ export default function ConfigActions() {
variant="light"
>
<IconTrash color={colors.red[2]} size={20} />
<Text>{t('buttons.delete.text')}</Text>
<Text size="sm">{t('buttons.delete.text')}</Text>
</ActionIcon>
<ActionIcon className={classes.actionIcon} onClick={createCopyModal.open} variant="default">
<IconCopy size={20} />
<Text>{t('buttons.saveCopy')}</Text>
<Text size="sm">{t('buttons.saveCopy')}</Text>
</ActionIcon>
</Flex>

View File

@@ -1,6 +1,5 @@
import {
ActionIcon,
Center,
Checkbox,
createStyles,
Flex,
@@ -11,6 +10,7 @@ import {
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';
@@ -35,6 +35,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
const { colors, colorScheme } = useMantineTheme();
const { t } = useTranslation('settings/common');
if (!configName) return null;
@@ -69,7 +70,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
return (
<Stack spacing="xs">
<Title order={6}>Dashboard layout</Title>
<Title order={6}>{t('layout.title')}</Title>
<Paper px="xs" py={4} withBorder>
<Group position="apart">
@@ -94,7 +95,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
{leftSidebar && (
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
<Flex align="center" justify="center" direction="column">
<Text align="center">Sidebar</Text>
<Text align="center">{t('layout.sidebar')}</Text>
<Text color="dimmed" size="xs" align="center">
Only for
<br />
@@ -106,16 +107,16 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
)}
<Paper className={classes.primaryWrapper} p="xs" withBorder>
<Text align="center">Main</Text>
<Text align="center">{t('layout.main')}</Text>
<Text color="dimmed" size="xs" align="center">
Cannot be turned of.
{t('layout.cannotturnoff')}
</Text>
</Paper>
{rightSidebar && (
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
<Flex align="center" justify="center" direction="column">
<Text align="center">Sidebar</Text>
<Text align="center">{t('layout.sidebar')}</Text>
<Text color="dimmed" size="xs" align="center">
Only for
<br />
@@ -129,29 +130,29 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
<Stack spacing="xs">
<Checkbox
label="Enable left sidebar"
description="Optional. Can be used for apps and integrations only"
label={t('layout.enablelsidebar')}
description={t('layout.enablelsidebardesc')}
checked={leftSidebar}
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
/>
<Checkbox
label="Enable right sidebar"
description="Optional. Can be used for apps and integrations only"
label={t('layout.enablersidebar')}
description={t('layout.enablersidebardesc')}
checked={rightSidebar}
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
/>
<Checkbox
label="Enable search bar"
label={t('layout.enablesearchbar')}
checked={searchBar}
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
/>
<Checkbox
label="Enable docker"
label={t('layout.enabledocker')}
checked={docker}
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
/>
<Checkbox
label="Enable pings"
label={t('layout.enableping')}
checked={ping}
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
/>

View File

@@ -1,14 +1,11 @@
import { ActionIcon, Title, Tooltip, Drawer, Tabs, ScrollArea } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { useState } from 'react';
import { IconSettings } from '@tabler/icons';
import { Title, Drawer, Tabs, ScrollArea } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import CustomizationSettings from './Customization/CustomizationSettings';
import CommonSettings from './Common/CommonSettings';
import Credits from './Common/Credits';
function SettingsMenu() {
function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
const { t } = useTranslation('settings/common');
return (
@@ -36,7 +33,11 @@ interface SettingsDrawerProps {
closeDrawer: () => void;
}
export function SettingsDrawer({ opened, closeDrawer }: SettingsDrawerProps) {
export function SettingsDrawer({
opened,
closeDrawer,
newVersionAvailable,
}: SettingsDrawerProps & { newVersionAvailable: string }) {
const { t } = useTranslation('settings/common');
return (
@@ -48,7 +49,7 @@ export function SettingsDrawer({ opened, closeDrawer }: SettingsDrawerProps) {
opened={opened}
onClose={closeDrawer}
>
<SettingsMenu />
<SettingsMenu newVersionAvailable={newVersionAvailable} />
<Credits />
</Drawer>
);

View File

@@ -1,74 +0,0 @@
import React, { useEffect } from 'react';
import { createStyles, Footer as FooterComponent } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconAlertCircle as AlertCircle } from '@tabler/icons';
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
const useStyles = createStyles((theme) => ({
footer: {
borderTop: `1px solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
}`,
},
inner: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: `${theme.spacing.md}px ${theme.spacing.md}px`,
[theme.fn.smallerThan('sm')]: {
flexDirection: 'column',
},
},
links: {
[theme.fn.smallerThan('sm')]: {
marginTop: theme.spacing.lg,
marginBottom: theme.spacing.sm,
},
},
}));
interface FooterCenteredProps {
links: { link: string; label: string }[];
}
export function Footer({ links }: FooterCenteredProps) {
useEffect(() => {
// Fetch Data here when component first mounted
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
res.json().then((data) => {
if (data.tag_name > CURRENT_VERSION) {
showNotification({
color: 'yellow',
autoClose: false,
title: 'New version available',
icon: <AlertCircle />,
message: `Version ${data.tag_name} is available, update now!`,
});
} else if (data.tag_name < CURRENT_VERSION) {
showNotification({
color: 'orange',
autoClose: 5000,
title: 'You are using a development version',
icon: <AlertCircle />,
message: 'This version of Homarr is still in development! Bugs are expected 🐛',
});
}
});
});
}, []);
return (
<FooterComponent
height="auto"
style={{
background: 'none',
border: 'none',
clear: 'both',
}}
children={undefined}
/>
);
}

View File

@@ -1,9 +1,8 @@
import { AppShell, createStyles } from '@mantine/core';
import { useConfigContext } from '../../config/provider';
import { Background } from './Background';
import { Footer } from './Footer';
import { Header } from './Header/Header';
import { Head } from './Header/Meta/Head';
import { Header } from './header/Header';
import { Head } from './header/Meta/Head';
const useStyles = createStyles(() => ({}));
@@ -15,7 +14,6 @@ export default function Layout({ children }: any) {
<AppShell
fixed={false}
header={<Header />}
footer={<Footer links={[]} />}
styles={{
main: {
minHeight: 'calc(100vh - var(--mantine-header-height))',

View File

@@ -4,7 +4,7 @@ import { IconApps } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
export const AddElementAction = () => {
const { t } = useTranslation('layout/add-service-app-shelf');
const { t } = useTranslation('layout/element-selector/selector');
return (
<Tooltip withinPortal label={t('actionIcon.tooltip')}>
@@ -16,7 +16,7 @@ export const AddElementAction = () => {
onClick={() =>
openContextModal({
modal: 'selectElement',
title: 'Add an element to your dashboard',
title: t('modal.title'),
size: 'xl',
innerProps: {},
})

View File

@@ -1,4 +1,6 @@
import { Box, createStyles, Group, Header as MantineHeader } from '@mantine/core';
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
import { useState, useEffect } from 'react';
import { REPO_URL, CURRENT_VERSION } from '../../../../data/constants';
import { useConfigContext } from '../../../config/provider';
import { Logo } from '../Logo';
import { useCardStyles } from '../useCardStyles';
@@ -15,6 +17,18 @@ export function Header(props: any) {
const { config } = useConfigContext();
const [newVersionAvailable, setNewVersionAvailable] = useState<string>('');
useEffect(() => {
// Fetch Data here when component first mounted
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
res.json().then((data) => {
if (data.tag_name > CURRENT_VERSION) {
setNewVersionAvailable(data.tag_name);
}
});
});
}, [CURRENT_VERSION]);
return (
<MantineHeader height={HeaderHeight} className={cardClasses.card}>
<Group p="xs" noWrap grow>
@@ -25,7 +39,9 @@ export function Header(props: any) {
<Search />
<AddElementAction />
<ToggleEditModeAction />
<SettingsMenu />
<Indicator size={15} color="blue" withBorder processing disabled={!newVersionAvailable}>
<SettingsMenu newVersionAvailable={newVersionAvailable} />
</Indicator>
</Group>
</Group>
</MantineHeader>

View File

@@ -60,8 +60,7 @@ export function Search() {
// Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results
const isOverseerrEnabled = false; //config?.settings.common.enabledModules.overseerr;
const overseerrApp = config?.apps.find(
(app) =>
app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
(app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
);
const searchEngineSettings = config?.settings.common.searchEngine;
const searchEngineUrl = !searchEngineSettings

View File

@@ -1,12 +1,14 @@
import { ActionIcon, Menu, Tooltip } from '@mantine/core';
import { ActionIcon, Badge, Menu, Tooltip } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconInfoCircle, IconMenu2, IconSettings } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { AboutModal } from '../../About/AboutModal';
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch';
export const SettingsMenu = () => {
export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
const [drawerOpened, drawer] = useDisclosure(false);
const { t } = useTranslation('common');
const [aboutModalOpened, aboutModal] = useDisclosure(false);
return (
@@ -22,19 +24,34 @@ export const SettingsMenu = () => {
<ColorSchemeSwitch />
<Menu.Divider />
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
Homarr Settings
{t('sections.settings')}
</Menu.Item>
<Menu.Item
icon={<IconInfoCircle strokeWidth={1.2} size={18} />}
onClick={aboutModal.open}
rightSection={
newVersionAvailable && (
<Badge variant="light" color="blue">
New
</Badge>
)
}
onClick={() => aboutModal.open()}
>
About
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Tooltip>
<SettingsDrawer opened={drawerOpened} closeDrawer={drawer.close} />
<AboutModal opened={aboutModalOpened} closeModal={aboutModal.close} />
<SettingsDrawer
opened={drawerOpened}
closeDrawer={drawer.close}
newVersionAvailable={newVersionAvailable}
/>
<AboutModal
opened={aboutModalOpened}
closeModal={aboutModal.close}
newVersionAvailable={newVersionAvailable}
/>
</>
);
};
}

View File

@@ -1,4 +1,4 @@
import { Menu, useMantineColorScheme, useMantineTheme } from '@mantine/core';
import { Menu, useMantineColorScheme } from '@mantine/core';
import { IconMoonStars, IconSun } from '@tabler/icons';
import { useTranslation } from 'next-i18next';