Merge branch 'dev' of https://github.com/ajnart/homarr into common-troubleshoot-and-auto-handling

This commit is contained in:
Tagaishi
2023-10-30 18:55:05 +01:00
794 changed files with 17767 additions and 3519 deletions

View File

@@ -0,0 +1,17 @@
import { Stack, Switch } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useBoardCustomizationFormContext } from '~/components/Board/Customize/form';
export const AccessCustomization = () => {
const { t } = useTranslation('settings/customization/access');
const form = useBoardCustomizationFormContext();
return (
<Stack>
<Switch
label={t('allowGuests.label')}
description={t('allowGuests.description')}
{...form.getInputProps('access.allowGuests', { type: 'checkbox' })}
/>
</Stack>
);
};

View File

@@ -15,7 +15,6 @@ import {
import { useTranslation } from 'next-i18next';
import { highlight, languages } from 'prismjs';
import Editor from 'react-simple-code-editor';
import { useColorScheme } from '~/hooks/use-colorscheme';
import { useColorTheme } from '~/tools/color';
import { useBoardCustomizationFormContext } from '../form';

View File

@@ -1,4 +1,3 @@
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
import { createFormContext } from '@mantine/form';
import { z } from 'zod';
import { boardCustomizationSchema } from '~/validations/boards';

View File

@@ -1,9 +1,9 @@
import { ActionIcon, Space, createStyles } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
import { useConfigContext } from '~/config/provider';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { MobileRibbonSidebarDrawer } from './MobileRibbonSidebarDrawer';
export const MobileRibbons = () => {

View File

@@ -1,9 +1,9 @@
import { SelectItem } from '@mantine/core';
import { ContextModalProps, closeModal } from '@mantine/modals';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { AppType } from '~/types/app';
import { useGridstackStore, useWrapperColumnCount } from '../../Wrappers/gridstack/store';
import { ChangePositionModal } from './ChangePositionModal';

View File

@@ -1,7 +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 {
@@ -95,6 +94,7 @@ export const ChangePositionModal = ({
min: widthData.at(0)?.label,
max: widthData.at(-1)?.label,
})}
withinPortal
{...form.getInputProps('width')}
/>
</Grid.Col>
@@ -109,6 +109,7 @@ export const ChangePositionModal = ({
min: heightData.at(0)?.label,
max: heightData.at(-1)?.label,
})}
withinPortal
{...form.getInputProps('height')}
/>
</Grid.Col>

View File

@@ -1,8 +1,8 @@
import { SelectItem } from '@mantine/core';
import { ContextModalProps, closeModal } from '@mantine/modals';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import widgets from '../../../../widgets';
import { WidgetChangePositionModalInnerProps } from '../../Tiles/Widgets/WidgetsMenu';
import { useGridstackStore, useWrapperColumnCount } from '../../Wrappers/gridstack/store';

View File

@@ -3,9 +3,8 @@ import { UseFormReturnType } from '@mantine/form';
import { useDebouncedValue } from '@mantine/hooks';
import { useTranslation } from 'next-i18next';
import { useEffect, useRef } from 'react';
import { AppType } from '~/types/app';
import { IconSelector } from '~/components/IconSelector/IconSelector';
import { AppType } from '~/types/app';
interface AppearanceTabProps {
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
@@ -83,7 +82,8 @@ export const AppearanceTab = ({
data={[
{
value: 'column',
label: t('appearance.positionAppName.dropdown.top') as string },
label: t('appearance.positionAppName.dropdown.top') as string,
},
{
value: 'row-reverse',
label: t('appearance.positionAppName.dropdown.right') as string,
@@ -94,7 +94,8 @@ export const AppearanceTab = ({
},
{
value: 'row',
label: t('appearance.positionAppName.dropdown.left') as string },
label: t('appearance.positionAppName.dropdown.left') as string,
},
]}
{...form.getInputProps('appearance.positionAppName')}
onChange={(value) => {

View File

@@ -1,9 +1,18 @@
import { Text, TextInput, Tooltip, Stack, Switch, Tabs, Group, useMantineTheme, HoverCard } from '@mantine/core';
import {
Group,
HoverCard,
Stack,
Switch,
Tabs,
Text,
TextInput,
Tooltip,
useMantineTheme,
} from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { InfoCard } from '~/components/InfoCard/InfoCard';
import { AppType } from '~/types/app';
import { InfoCard } from '~/components/InfoCard/InfoCard'
interface BehaviourTabProps {
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
@@ -19,7 +28,7 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => {
<Switch
label={t('behaviour.isOpeningNewTab.label')}
description={t('behaviour.isOpeningNewTab.description')}
styles={{ label: { fontWeight: 500, }, description: { marginTop: 0, }, }}
styles={{ label: { fontWeight: 500 }, description: { marginTop: 0 } }}
{...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })}
/>
<Stack spacing="0.25rem">
@@ -27,13 +36,11 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => {
<Text size="0.875rem" weight={500}>
{t('behaviour.tooltipDescription.label')}
</Text>
<InfoCard message={t('behaviour.tooltipDescription.description')}/>
<InfoCard message={t('behaviour.tooltipDescription.description')} />
</Group>
<TextInput
{...form.getInputProps('behaviour.tooltipDescription')}
/>
<TextInput {...form.getInputProps('behaviour.tooltipDescription')} />
</Stack>
</Stack>
</Tabs.Panel>
);
};
};

View File

@@ -15,7 +15,6 @@ import {
import { Icon } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { AppIntegrationPropertyAccessabilityType } from '~/types/app';
interface GenericSecretInputProps {

View File

@@ -3,7 +3,6 @@ import { Group, Image, Select, SelectItem, Text } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { forwardRef } from 'react';
import {
AppIntegrationPropertyType,
AppIntegrationType,

View File

@@ -1,7 +1,6 @@
import { Stack } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { IconKey } from '@tabler/icons-react';
import {
AppIntegrationPropertyType,
AppType,
@@ -9,6 +8,7 @@ import {
integrationFieldDefinitions,
integrationFieldProperties,
} from '~/types/app';
import { GenericSecretInput } from '../InputElements/GenericSecretInput';
interface IntegrationOptionsRendererProps {

View File

@@ -2,8 +2,8 @@ import { Alert, Divider, Tabs, Text } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { IconAlertTriangle } from '@tabler/icons-react';
import { Trans, useTranslation } from 'next-i18next';
import { AppType } from '~/types/app';
import { IntegrationSelector } from './Components/InputElements/IntegrationSelector';
import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer';

View File

@@ -1,7 +1,6 @@
import { MultiSelect, Stack, Switch, Tabs } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { StatusCodes } from '~/tools/acceptableStatusCodes';
import { AppType } from '~/types/app';
@@ -20,7 +19,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
<Switch
label={t('network.statusChecker.label')}
description={t('network.statusChecker.description')}
styles={{ label: { fontWeight: 500, }, description: { marginTop: 0, }, }}
styles={{ label: { fontWeight: 500 }, description: { marginTop: 0 } }}
defaultChecked={form.values.network.enabledStatusChecker}
{...form.getInputProps('network.enabledStatusChecker')}
/>

View File

@@ -8,6 +8,7 @@ import { useStyles } from './styles';
interface GenericAvailableElementTypeProps {
name: string;
id: string;
handleAddition: () => Promise<void>;
description?: string;
image: string | Icon;
@@ -16,6 +17,7 @@ interface GenericAvailableElementTypeProps {
export const GenericAvailableElementType = ({
name,
id,
description,
image,
disabled,

View File

@@ -1,4 +1,4 @@
import { Grid, Text } from '@mantine/core';
import { Container, Grid, Text } from '@mantine/core';
import { IconCursorText } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
@@ -21,12 +21,13 @@ export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps)
</Text>
<Grid grow>
{/*
<GenericAvailableElementType
name="Static Text"
description="Display a fixed string on your dashboard"
image={IconCursorText}
handleAddition={/* TODO: add something? */ async () => {}}
/>
handleAddition={async () => {}}
/> */}
</Grid>
</>
);

View File

@@ -3,10 +3,10 @@ import { showNotification } from '@mantine/notifications';
import { Icon, IconChecks } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { v4 as uuidv4 } from 'uuid';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { IWidget, IWidgetDefinition } from '~/widgets/widgets';
import { useEditModeStore } from '../../../../Views/useEditModeStore';
import { GenericAvailableElementType } from '../Shared/GenericElementType';
@@ -97,6 +97,7 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
icon: <IconChecks stroke={1.5} />,
color: 'teal',
});
umami.track('Add widget', { id: widget.id });
};
return (
@@ -104,6 +105,7 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
name={t('descriptor.name')}
description={t('descriptor.description') ?? undefined}
image={image}
id={widget.id}
disabled={disabled}
handleAddition={handleAddition}
/>

View File

@@ -2,6 +2,7 @@ import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions';
import { AppType } from '~/types/app';
import { GenericTileMenu } from '../GenericTileMenu';
interface TileMenuProps {

View File

@@ -4,10 +4,9 @@ import Consola from 'consola';
import { TargetAndTransition, Transition, motion } from 'framer-motion';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { RouterOutputs, api } from '~/utils/api';
import { useConfigContext } from '~/config/provider';
import { AppType } from '~/types/app';
import { RouterOutputs, api } from '~/utils/api';
interface AppPingProps {
app: AppType;

View File

@@ -2,8 +2,8 @@ import { Affix, Box, Text, Tooltip, UnstyledButton } from '@mantine/core';
import { createStyles, useMantineTheme } from '@mantine/styles';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { AppType } from '~/types/app';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { HomarrCardWrapper } from '../HomarrCardWrapper';
import { BaseTileProps } from '../type';
@@ -87,7 +87,7 @@ export const AppTile = ({ className, app }: AppTileProps) => {
) : (
<UnstyledButton
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
component={Link}
component="a"
href={app.behaviour.externalUrl.length > 0 ? app.behaviour.externalUrl : app.url}
target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'}
className={`${classes.button} ${classes.base}`}
@@ -111,7 +111,7 @@ const useStyles = createStyles((theme, _params, getRef) => ({
overflow: 'visible',
flexGrow: 5,
},
appImage:{
appImage: {
maxHeight: '100%',
maxWidth: '100%',
overflow: 'auto',

View File

@@ -1,8 +1,8 @@
import { ActionIcon, Menu } from '@mantine/core';
import { IconLayoutKanban, IconPencil, IconSettings, IconTrash } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useColorTheme } from '~/tools/color';
import { useEditModeStore } from '../Views/useEditModeStore';
interface GenericTileMenuProps {
@@ -12,14 +12,12 @@ interface GenericTileMenuProps {
displayEdit: boolean;
}
export const GenericTileMenu = (
{
handleClickEdit,
handleClickChangePosition,
handleClickDelete,
displayEdit,
}: GenericTileMenuProps
) => {
export const GenericTileMenu = ({
handleClickEdit,
handleClickChangePosition,
handleClickDelete,
displayEdit,
}: GenericTileMenuProps) => {
const { t } = useTranslation('common');
const isEditMode = useEditModeStore((x) => x.enabled);

View File

@@ -3,7 +3,6 @@ import { useDisclosure } from '@mantine/hooks';
import { IconChevronDown, IconGripVertical } from '@tabler/icons-react';
import { Reorder, useDragControls } from 'framer-motion';
import { FC, useEffect, useRef } from 'react';
import { IDraggableEditableListInputValue } from '~/widgets/widgets';
interface DraggableListProps {

View File

@@ -3,7 +3,6 @@ import { useDisclosure } from '@mantine/hooks';
import { IconChevronDown, IconGripVertical } from '@tabler/icons-react';
import { Reorder, useDragControls } from 'framer-motion';
import { FC, ReactNode, useEffect, useRef } from 'react';
import { IDraggableListInputValue } from '~/widgets/widgets';
const useStyles = createStyles((theme) => ({

View File

@@ -18,13 +18,13 @@ import { ContextModalProps } from '@mantine/modals';
import { IconAlertTriangle, IconPlaylistX, IconPlus } from '@tabler/icons-react';
import { Trans, useTranslation } from 'next-i18next';
import { FC, useState } from 'react';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { mapObject } from '~/tools/client/objects';
import Widgets from '../../../../widgets';
import type { IDraggableListInputValue, IWidgetOptionValue } from '~/widgets/widgets';
import { IWidget } from '~/widgets/widgets';
import Widgets from '../../../../widgets';
import { InfoCard } from '../../../InfoCard/InfoCard';
import { DraggableList } from './Inputs/DraggableList';
import { LocationSelection } from './Inputs/LocationSelection';

View File

@@ -1,9 +1,9 @@
import { Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions';
import WidgetsDefinitions from '../../../../widgets';
import { IWidget } from '~/widgets/widgets';
import WidgetsDefinitions from '../../../../widgets';
import { useWrapperColumnCount } from '../../Wrappers/gridstack/store';
import { GenericTileMenu } from '../GenericTileMenu';
import { WidgetEditModalInnerProps } from './WidgetsEditModal';

View File

@@ -1,7 +1,6 @@
import { Button, Group, Stack, Text } from '@mantine/core';
import { ContextModalProps } from '@mantine/modals';
import { Trans, useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';

View File

@@ -1,11 +1,11 @@
import { Group, Stack } from '@mantine/core';
import { useEffect, useMemo, useRef } from 'react';
import { useConfigContext } from '~/config/provider';
import { useResize } from '~/hooks/use-resize';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { CategoryType } from '~/types/category';
import { WrapperType } from '~/types/wrapper';
import { DashboardCategory } from '../Wrappers/Category/Category';
import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar';
import { DashboardWrapper } from '../Wrappers/Wrapper/Wrapper';

View File

@@ -1,8 +1,8 @@
import { ActionIcon, Button, Text, Tooltip } from '@mantine/core';
import { IconEdit, IconEditOff } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { useEditModeStore } from './useEditModeStore';
export const ViewToggleButton = () => {

View File

@@ -13,9 +13,9 @@ import { useLocalStorage } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { IconDotsVertical, IconShare3 } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
import { CategoryType } from '~/types/category';
import { useCardStyles } from '../../../layout/Common/useCardStyles';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { WrapperContent } from '../WrapperContent';

View File

@@ -8,11 +8,11 @@ import {
IconTransitionTop,
IconTrash,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
import { CategoryType } from '~/types/category';
import { useCategoryActions } from './useCategoryActions';
import { useTranslation } from 'next-i18next';
interface CategoryEditMenuProps {
category: CategoryType;
@@ -22,7 +22,7 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
const { name: configName } = useConfigContext();
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } =
useCategoryActions(configName, category);
const { t } = useTranslation(['layout/common','common']);
const { t } = useTranslation(['layout/common', 'common']);
return (
<Menu withinPortal withArrow>
@@ -33,28 +33,24 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
</Menu.Target>
<Menu.Dropdown>
<Menu.Item icon={<IconEdit size={20} />} onClick={edit}>
{t('common:edit')}
{t('common:edit')}
</Menu.Item>
<Menu.Item icon={<IconTrash size={20} />} onClick={remove}>
{t('common:remove')}
</Menu.Item>
<Menu.Label>
{t('common:changePosition')}
</Menu.Label>
<Menu.Label>{t('common:changePosition')}</Menu.Label>
<Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}>
{t('menu.moveUp')}
</Menu.Item>
<Menu.Item icon={<IconTransitionBottom size={20} />} onClick={moveCategoryDown}>
{t('menu.moveDown')}
</Menu.Item>
<Menu.Label>
{t('menu.addCategory',{location: ''})}
</Menu.Label>
<Menu.Label>{t('menu.addCategory', { location: '' })}</Menu.Label>
<Menu.Item icon={<IconRowInsertTop size={20} />} onClick={addCategoryAbove}>
{t('menu.addCategory',{location: t('menu.addAbove')})}
{t('menu.addCategory', { location: t('menu.addAbove') })}
</Menu.Item>
<Menu.Item icon={<IconRowInsertBottom size={20} />} onClick={addCategoryBelow}>
{t('menu.addCategory',{location: t('menu.addBelow')})}
{t('menu.addCategory', { location: t('menu.addBelow') })}
</Menu.Item>
</Menu.Dropdown>
</Menu>

View File

@@ -2,7 +2,6 @@ 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';

View File

@@ -1,11 +1,11 @@
import { v4 as uuidv4 } from 'uuid';
import { useConfigStore } from '~/config/store';
import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions';
import { AppType } from '~/types/app';
import { CategoryType } from '~/types/category';
import { WrapperType } from '~/types/wrapper';
import { IWidget } from '~/widgets/widgets';
import { CategoryEditModalInnerProps } from './CategoryEditModal';
export const useCategoryActions = (configName: string | undefined, category: CategoryType) => {

View File

@@ -1,4 +1,5 @@
import { WrapperType } from '~/types/wrapper';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { WrapperContent } from '../WrapperContent';
import { useGridstack } from '../gridstack/use-gridstack';

View File

@@ -1,10 +1,10 @@
import { GridStack } from 'fily-publish-gridstack';
import { MutableRefObject, RefObject } from 'react';
import { AppType } from '~/types/app';
import Widgets from '../../../widgets';
import { WidgetWrapper } from '~/widgets/WidgetWrapper';
import { IWidget, IWidgetDefinition } from '~/widgets/widgets';
import Widgets from '../../../widgets';
import { appTileDefinition } from '../Tiles/Apps/AppTile';
import { GridstackTileWrapper } from '../Tiles/TileWrapper';
import { useGridstackStore } from './gridstack/store';

View File

@@ -1,6 +1,5 @@
import { GridItemHTMLElement, GridStack, GridStackNode } from 'fily-publish-gridstack';
import { MutableRefObject, RefObject } from 'react';
import { AppType } from '~/types/app';
import { ShapeType } from '~/types/shape';
import { IWidget } from '~/widgets/widgets';

View File

@@ -1,5 +1,4 @@
import { createWithEqualityFn } from 'zustand/traditional';
import { useConfigContext } from '~/config/provider';
import { GridstackBreakpoints } from '~/constants/gridstack-breakpoints';

View File

@@ -1,11 +1,11 @@
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
import { MutableRefObject, RefObject, createRef, useEffect, useMemo, useRef } from 'react';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { AppType } from '~/types/app';
import { AreaType } from '~/types/area';
import { IWidget } from '~/widgets/widgets';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { TileWithUnknownLocation, initializeGridstack } from './init-gridstack';
import { useGridstackStore, useWrapperColumnCount } from './store';

View File

@@ -15,9 +15,9 @@ import {
import { IconSearch } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { forwardRef, useImperativeHandle, useState } from 'react';
import { humanFileSize } from '~/tools/humanFileSize';
import { api } from '~/utils/api';
import { humanFileSize } from '~/tools/humanFileSize';
import { DebouncedImage } from './DebouncedImage';
export const IconSelector = forwardRef(
@@ -83,9 +83,7 @@ export const IconSelector = forwardRef(
}
icon={<DebouncedImage src={value ?? currentValue} width={20} height={20} />}
rightSection={
(value ?? currentValue).length > 0 ? (
<CloseButton onClick={() => onChange("")} />
) : null
(value ?? currentValue).length > 0 ? <CloseButton onClick={() => onChange('')} /> : null
}
itemComponent={AutoCompleteItem}
className={classes.textInput}

View File

@@ -23,7 +23,9 @@ interface InfoCardProps {
export const InfoCard = ({ bg, cardProp, message, link, hoverProp, position }: InfoCardProps) => {
const { colorScheme } = useMantineTheme();
const { t } = useTranslation('common');
const content = link? message + ` <a href=\"${link}\" target=\"_blank\">${t('seeMore')}</a>` : message;
const content = link
? message + ` <a href=\"${link}\" target=\"_blank\">${t('seeMore')}</a>`
: message;
const editor = useEditor({
content,
editable: false,

View File

@@ -58,7 +58,9 @@ export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
</Button>
<Button
type="submit"
onClick={async () => {}}
onClick={async () => {
umami.track('Create new board')
}}
disabled={isLoading}
variant="light"
color="green"

View File

@@ -26,7 +26,7 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps<Inn
await mutateAsync(
{
apps: innerProps.containers.map((container) => ({
name: container.Names.at(0) ?? 'App',
name: (container.Names.at(0) ?? 'App').replace('/', ''),
port: container.Ports.at(0)?.PublicPort,
})),
boardName: values.board,
@@ -117,4 +117,5 @@ export const openDockerSelectBoardModal = (innerProps: InnerProps) => {
),
innerProps,
});
umami.track('Add to homarr modal')
};

View File

@@ -23,7 +23,12 @@ export const ReviewInputStep = ({ values, prevStep, nextStep }: ReviewInputStepP
const { t } = useTranslation('manage/users/create');
const utils = api.useContext();
const { mutateAsync: createAsync, isLoading, isError, error } = api.user.create.useMutation({
const {
mutateAsync: createAsync,
isLoading,
isError,
error,
} = api.user.create.useMutation({
onSettled: () => {
void utils.user.all.invalidate();
},
@@ -106,6 +111,7 @@ export const ReviewInputStep = ({ values, prevStep, nextStep }: ReviewInputStepP
password: values.security.password,
email: values.account.eMail === '' ? undefined : values.account.eMail,
});
umami.track('Create user', { username: values.account.username});
}}
loading={isLoading}
rightIcon={<IconCheck size="1rem" />}

View File

@@ -66,6 +66,7 @@ export const CreateAccountSecurityStep = ({
onClick={async () => {
const randomPassword = await mutateAsync();
form.setFieldValue('password', randomPassword);
umami.track('Generate random password');
}}
loading={isLoading}
variant="default"

View File

@@ -29,7 +29,7 @@ export const DeleteInviteModal = ({ id, innerProps }: ContextModalProps<{ tokenI
<Button
onClick={async () => {
await deleteAsync({
tokenId: innerProps.tokenId,
id: innerProps.tokenId,
});
}}
disabled={isLoading}

View File

@@ -59,6 +59,7 @@ export const StepCreateAccount = ({
<Text>
Your administrator account <b>must be secure</b>, that's why we have so many rules surrounding it.
<br/>Try not to make it adminadmin this time...
<br/>Note: these password requirements <b>are not forced</b>, they are just recommendations.
</Text>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>

View File

@@ -41,7 +41,7 @@ export const StepOnboardingFinished = () => {
<Divider />
<NavLink
component={Link}
href="/b/default"
href="/b"
rightSection={<IconChevronRight size="0.8rem" stroke={1.5} />}
className={classes.link}
icon={<IconDashboard />}

View File

@@ -39,7 +39,7 @@ services:
const added = { color: 'green', label: '+' };
export const StepUpdatePathMappings = ({ next }: { next: () => void }) => {
const [selectedTab, setSelectedTab] = useState<TabsValue>("standard_docker");
const [selectedTab, setSelectedTab] = useState<TabsValue>('standard_docker');
return (
<OnboardingStepWrapper>
<Title order={2} align="center" mb="md">
@@ -140,7 +140,9 @@ export const StepUpdatePathMappings = ({ next }: { next: () => void }) => {
{dockerComposeCommand}
</Prism>
</List.Item>
<List.Item>Run <Code>docker compose up</Code>.</List.Item>
<List.Item>
Run <Code>docker compose up</Code>.
</List.Item>
<List.Item>Refresh this page and click on "continue"</List.Item>
</List>
</Tabs.Panel>

View File

@@ -1,24 +1,24 @@
import { Box, Text } from "@mantine/core";
import { IconCheck, IconX } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { minPasswordLength } from "~/validations/user";
import { Box, Text } from '@mantine/core';
import { IconCheck, IconX } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { minPasswordLength } from '~/validations/user';
export const PasswordRequirement = ({ meets, label }: { meets: boolean; label: string }) => {
const { t } = useTranslation('password-requirements');
return (
<Text
color={meets ? 'teal' : 'red'}
sx={{ display: 'flex', alignItems: 'center' }}
mt={7}
size="sm"
>
{meets ? <IconCheck size="0.9rem" /> : <IconX size="0.9rem" />}{' '}
<Box ml={10}>
{t(`${label}`, {
count: minPasswordLength,
})}
</Box>
</Text>
);
};
const { t } = useTranslation('password-requirements');
return (
<Text
color={meets ? 'teal' : 'red'}
sx={{ display: 'flex', alignItems: 'center' }}
mt={7}
size="sm"
>
{meets ? <IconCheck size="0.9rem" /> : <IconX size="0.9rem" />}
<Box ml={10}>
{t(`${label}`, {
count: minPasswordLength,
})}
</Box>
</Text>
);
};

View File

@@ -23,7 +23,6 @@ function getStrength(password: string) {
score += 1;
}
return (score / goal) * 100;
}
export const PasswordRequirements = ({ value }: { value: string }) => {

View File

@@ -1,23 +1,14 @@
import {
ActionIcon,
ActionIconProps,
} from '@mantine/core';
import { useColorScheme } from '~/hooks/use-colorscheme';
import { ActionIcon, ActionIconProps } from '@mantine/core';
import { IconMoonStars, IconSun } from '@tabler/icons-react';
import { useColorScheme } from '~/hooks/use-colorscheme';
export const ThemeSchemeToggle = (props : Partial<ActionIconProps>) => {
export const ThemeSchemeToggle = (props: Partial<ActionIconProps>) => {
const { colorScheme, toggleColorScheme } = useColorScheme();
const Icon = colorScheme === 'dark' ? IconSun : IconMoonStars;
return (
<ActionIcon
size={50}
variant="outline"
radius="md"
onClick={toggleColorScheme}
{...props}
>
<Icon size="66%"/>
<ActionIcon size={50} variant="outline" radius="md" onClick={toggleColorScheme} {...props}>
<Icon size="66%" />
</ActionIcon>
);
};

View File

@@ -1,133 +0,0 @@
import { Group, Select, Stack, Text } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { getCookie, setCookie } from 'cookies-next';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { forwardRef, useState } from 'react';
import { api } from '~/utils/api';
import { COOKIE_LOCALE_KEY } from '../../../../../data/constants';
import { Language, getLanguageByCode } from '~/tools/language';
export default function LanguageSelect() {
const { data: sessionData } = useSession();
const { t, i18n } = useTranslation('settings/general/internationalization');
const { changeLanguage } = i18n;
const configLocale = getCookie(COOKIE_LOCALE_KEY);
const { locale, locales, pathname, query, asPath, push } = useRouter();
const [selectedLanguage, setSelectedLanguage] = useState<string>(
sessionData?.user.language ?? (configLocale as string) ?? locale ?? 'en'
);
const { mutateAsync } = api.user.changeLanguage.useMutation();
const data = locales
? locales.map((localeItem) => ({
value: localeItem,
label: getLanguageByCode(localeItem).originalName,
icon: getLanguageByCode(localeItem).emoji,
language: getLanguageByCode(localeItem),
}))
: [];
const onChangeSelect = (value: string) => {
setSelectedLanguage(value);
const newLanguage = getLanguageByCode(value);
changeLanguage(value)
.then(async () => {
setCookie(COOKIE_LOCALE_KEY, value, {
maxAge: 60 * 60 * 24 * 30,
sameSite: 'strict',
});
if (sessionData?.user && new Date(sessionData.expires) > new Date()) {
await mutateAsync({
language: value,
});
}
push(
{
pathname,
query,
},
asPath,
{ locale: value }
);
showNotification({
title: 'Language changed',
message: `You changed the language to '${newLanguage.originalName}'`,
color: 'green',
autoClose: 5000,
});
})
.catch((err) => {
showNotification({
title: 'Failed to change language',
message: `Failed to change to '${newLanguage.originalName}', Error:'${err}`,
color: 'red',
autoClose: 5000,
});
});
};
return (
<Stack>
<Select
icon={<Text>{getLanguageByCode(selectedLanguage).emoji}</Text>}
label={t('label')}
data={data}
itemComponent={SelectItem}
nothingFound="Nothing found"
onChange={onChangeSelect}
value={selectedLanguage}
defaultValue={locale}
searchable
filter={(value, item) => {
const selectItems = item as unknown as { value: string; language: Language };
return (
selectItems.language.originalName
.toLowerCase()
.trim()
.includes(value.toLowerCase().trim()) ||
selectItems.language.translatedName
.toLowerCase()
.trim()
.includes(value.toLowerCase().trim())
);
}}
styles={{
icon: {
width: 42,
},
input: {
paddingLeft: '45px !important',
},
}}
/>
</Stack>
);
}
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
image: string;
language: Language;
}
const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
({ language, image, ...others }: ItemProps, ref) => (
<div ref={ref} {...others}>
<Group noWrap>
<Text>{language.emoji}</Text>
<div>
<Text size="sm">
{language.originalName} ({language.translatedName})
</Text>
</div>
</Group>
</div>
)
);

View File

@@ -31,6 +31,11 @@ export const SearchEngineSettings = () => {
label={t('searchEngine.newTab.label')}
{...form.getInputProps('openSearchInNewTab', { type: 'checkbox' })}
/>
<Switch
label={t('searchEngine.autoFocus.label')}
description={t('searchEngine.autoFocus.description')}
{...form.getInputProps('autoFocusSearch', { type: 'checkbox' })}
/>
<TextInput
label={t('searchEngine.template.label')}

View File

@@ -17,7 +17,7 @@ export const PolkaElement = ({
<Box
style={{
transform: `rotate(${rotation}deg)`,
pointerEvents: 'none'
pointerEvents: 'none',
}}
className="polka"
pos="absolute"

View File

@@ -1,7 +1,7 @@
import { Group, Image, Text } from '@mantine/core';
import { useConfigContext } from '~/config/provider';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { useConfigContext } from '~/config/provider';
import { usePrimaryGradient } from './useGradient';
interface LogoProps {

View File

@@ -1,5 +1,4 @@
import { createStyles } from '@mantine/core';
import { useConfigContext } from '~/config/provider';
export const useCardStyles = (isCategory: boolean) => {

View File

@@ -1,5 +1,4 @@
import { MantineGradient } from '@mantine/core';
import { useColorTheme } from '~/tools/color';
export const usePrimaryGradient = () => {

View File

@@ -1,8 +1,7 @@
import Head from 'next/head';
import React from 'react';
import { firstUpperCase } from '~/tools/shared/strings';
import { useConfigContext } from '~/config/provider';
import { firstUpperCase } from '~/tools/shared/strings';
export const BoardHeadOverride = () => {
const { config, name } = useConfigContext();

View File

@@ -19,9 +19,11 @@ import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/grid
import { BoardHeadOverride } from '~/components/layout/Meta/BoardHeadOverride';
import { HeaderActionButton } from '~/components/layout/header/ActionButton';
import { useConfigContext } from '~/config/provider';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { api } from '~/utils/api';
import { MainLayout } from './MainLayout';
import { env } from 'process';
type BoardLayoutProps = {
dockerEnabled: boolean;
@@ -30,9 +32,13 @@ type BoardLayoutProps = {
export const BoardLayout = ({ children, dockerEnabled }: BoardLayoutProps) => {
const { config } = useConfigContext();
const { data: session } = useSession();
return (
<MainLayout headerActions={<HeaderActions dockerEnabled={dockerEnabled} />}>
<MainLayout
autoFocusSearch={session?.user.autoFocusSearch}
headerActions={<HeaderActions dockerEnabled={dockerEnabled} />}
>
<BoardHeadOverride />
<BackgroundImage />
{children}
@@ -102,7 +108,7 @@ const ToggleEditModeButton = () => {
useHotkeys([['mod+E', toggleEditMode]]);
useWindowEvent('beforeunload', (event: BeforeUnloadEvent) => {
if (enabled) {
if (enabled && env.NODE_ENV === 'production') {
// eslint-disable-next-line no-param-reassign
event.returnValue = beforeUnloadEventText;
return beforeUnloadEventText;
@@ -213,7 +219,7 @@ const BackgroundImage = () => {
return (
<Global
styles={{
body: {
'.mantine-AppShell-root': {
minHeight: '100vh',
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`,
backgroundPosition: 'center center',

View File

@@ -6,9 +6,16 @@ type MainLayoutProps = {
headerActions?: React.ReactNode;
contentComponents?: React.ReactNode;
children: React.ReactNode;
autoFocusSearch?: boolean;
};
export const MainLayout = ({ showExperimental, headerActions, contentComponents, children }: MainLayoutProps) => {
export const MainLayout = ({
showExperimental,
headerActions,
contentComponents,
children,
autoFocusSearch,
}: MainLayoutProps) => {
const theme = useMantineTheme();
return (
@@ -18,7 +25,14 @@ export const MainLayout = ({ showExperimental, headerActions, contentComponents,
background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
},
}}
header={<MainHeader headerActions={headerActions} contentComponents={contentComponents} showExperimental={showExperimental} />}
header={
<MainHeader
autoFocusSearch={autoFocusSearch}
headerActions={headerActions}
contentComponents={contentComponents}
showExperimental={showExperimental}
/>
}
className="dashboard-app-shell"
>
{children}

View File

@@ -89,7 +89,7 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
</Navbar.Section>
</Navbar>
}
header={<MainHeader showExperimental logoHref="/manage" leftIcon={burgerMenu} />}
header={<MainHeader showExperimental logoHref="/b/" leftIcon={burgerMenu} />}
footer={
<Footer height={25}>
<Group position="apart" px="md">
@@ -112,7 +112,13 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
{children}
</Paper>
</AppShell>
<Drawer opened={burgerMenuOpen} onClose={closeBurgerMenu}>
<Drawer
opened={burgerMenuOpen}
onClose={closeBurgerMenu}
transitionProps={{
transition: 'slide-right',
}}
>
{navigationLinkComponents}
</Drawer>
</>

View File

@@ -7,8 +7,8 @@ import {
Grid,
Group,
HoverCard,
Kbd,
Image,
Kbd,
Modal,
Table,
Text,
@@ -31,11 +31,11 @@ import { motion } from 'framer-motion';
import { InitOptions } from 'i18next';
import { Trans, i18n, useTranslation } from 'next-i18next';
import { ReactNode } from 'react';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
import { useColorTheme } from '~/tools/color';
import { usePrimaryGradient } from '../../Common/useGradient';
import Credits from './Credits';
import Tip from './Tip';
@@ -75,13 +75,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
opened={opened}
title={
<Group spacing="sm">
<Image
alt="Homarr logo"
src="/imgs/logo/logo.png"
width={30}
height={30}
fit="contain"
/>
<Image alt="Homarr logo" src="/imgs/logo/logo.png" width={30} height={30} fit="contain" />
<Title order={3} variant="gradient" gradient={colorGradiant}>
{t('about')} Homarr
</Title>
@@ -271,13 +265,17 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
transition={{ duration: 0.8, ease: 'easeInOut' }}
>
<Badge color="green" variant="filled">
{t('version.new',{ newVersion: newVersionAvailable})}
{t('version.new', { newVersion: newVersionAvailable })}
</Badge>
</motion.div>
</HoverCard.Target>
<HoverCard.Dropdown>
<Text>
{t('version.dropdown', {currentVersion: attributes.packageVersion}).split('{{newVersion}}')[0]}
{
t('version.dropdown', { currentVersion: attributes.packageVersion }).split(
'{{newVersion}}'
)[0]
}
<b>
<Anchor
target="_blank"
@@ -286,7 +284,11 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
{newVersionAvailable}
</Anchor>
</b>
{t('version.dropdown', {currentVersion: attributes.packageVersion}).split('{{newVersion}}')[1]}
{
t('version.dropdown', { currentVersion: attributes.packageVersion }).split(
'{{newVersion}}'
)[1]
}
</Text>
</HoverCard.Dropdown>
</HoverCard>

View File

@@ -1,7 +1,6 @@
import { Anchor, Box, Collapse, Flex, Table, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'next-i18next';
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
export default function Credits() {

View File

@@ -1,4 +1,4 @@
import { Avatar, Badge, Menu, UnstyledButton, useMantineTheme } from '@mantine/core';
import { Avatar, Badge, Indicator, Menu, UnstyledButton, useMantineTheme } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import {
IconDashboard,
@@ -38,10 +38,17 @@ export const AvatarMenu = () => {
<UnstyledButton>
<Menu width={256}>
<Menu.Target>
<CurrentUserAvatar user={sessionData?.user ?? null} />
<CurrentUserAvatar
newVersionAvailable={newVersionAvailable ? true : false}
user={sessionData?.user ?? null}
/>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item closeMenuOnClick={false} icon={<Icon size="1rem" />} onClick={toggleColorScheme}>
<Menu.Item
closeMenuOnClick={false}
icon={<Icon size="1rem" />}
onClick={toggleColorScheme}
>
{t('actions.avatar.switchTheme')}
</Menu.Item>
{sessionData?.user && (
@@ -113,13 +120,24 @@ export const AvatarMenu = () => {
};
type CurrentUserAvatarProps = {
newVersionAvailable: boolean;
user: User | null;
};
const CurrentUserAvatar = forwardRef<HTMLDivElement, CurrentUserAvatarProps>(
({ user, ...others }, ref) => {
({ user, newVersionAvailable, ...others }, ref) => {
const { primaryColor } = useMantineTheme();
if (!user) return <Avatar ref={ref} {...others} />;
if (newVersionAvailable)
return (
<Indicator withBorder offset={2} color="blue" processing size={15}>
<Avatar ref={ref} color={primaryColor} {...others}>
{user.name?.slice(0, 2).toUpperCase()}
</Avatar>
</Indicator>
);
return (
<Avatar ref={ref} color={primaryColor} {...others}>
{user.name?.slice(0, 2).toUpperCase()}

View File

@@ -23,6 +23,7 @@ type MainHeaderProps = {
headerActions?: React.ReactNode;
contentComponents?: React.ReactNode;
leftIcon?: React.ReactNode;
autoFocusSearch?: boolean;
};
export const MainHeader = ({
@@ -31,6 +32,7 @@ export const MainHeader = ({
headerActions,
leftIcon,
contentComponents,
autoFocusSearch,
}: MainHeaderProps) => {
const { breakpoints } = useMantineTheme();
const isSmallerThanMd = useMediaQuery(`(max-width: ${breakpoints.sm})`);
@@ -51,7 +53,7 @@ export const MainHeader = ({
</UnstyledButton>
</Group>
{!isSmallerThanMd && <Search />}
{!isSmallerThanMd && <Search autoFocus={autoFocusSearch} />}
<Group noWrap style={{ flex: 1 }} position="right">
<Group noWrap spacing={8}>

View File

@@ -19,9 +19,10 @@ import { MovieModal } from './Search/MovieModal';
type SearchProps = {
isMobile?: boolean;
autoFocus?: boolean;
};
export const Search = ({ isMobile }: SearchProps) => {
export const Search = ({ isMobile, autoFocus }: SearchProps) => {
const { t } = useTranslation('layout/header');
const [search, setSearch] = useState('');
const ref = useRef<HTMLInputElement>(null);
@@ -62,6 +63,7 @@ export const Search = ({ isMobile }: SearchProps) => {
variant="filled"
placeholder={`${t('search.label')}...`}
hoverOnSearchChange
autoFocus={autoFocus}
rightSection={
<IconSearch
onClick={() => ref.current?.focus()}