🔀 Merge branch 'dev' into feature/add-basic-authentication

This commit is contained in:
Manuel
2023-08-13 15:12:20 +02:00
202 changed files with 3334 additions and 1502 deletions

View File

@@ -205,7 +205,7 @@ export const EditAppModal = ({
<NetworkTab form={form} />
<AppearanceTab
form={form}
disallowAppNameProgagation={() => setAllowAppNamePropagation(false)}
disallowAppNamePropagation={() => setAllowAppNamePropagation(false)}
allowAppNamePropagation={allowAppNamePropagation}
/>
<IntegrationTab form={form} />

View File

@@ -1,4 +1,4 @@
import { Flex, Select, Stack, Switch, Tabs } from '@mantine/core';
import { Flex, NumberInput, Select, Stack, Switch, Tabs } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { useDebouncedValue } from '@mantine/hooks';
import { useTranslation } from 'next-i18next';
@@ -9,13 +9,13 @@ import { IconSelector } from '../../../../../IconSelector/IconSelector';
interface AppearanceTabProps {
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
disallowAppNameProgagation: () => void;
disallowAppNamePropagation: () => void;
allowAppNamePropagation: boolean;
}
export const AppearanceTab = ({
form,
disallowAppNameProgagation,
disallowAppNamePropagation,
allowAppNamePropagation,
}: AppearanceTabProps) => {
const iconSelectorRef = useRef();
@@ -46,7 +46,7 @@ export const AppearanceTab = ({
defaultValue={form.values.appearance.iconUrl}
onChange={(value) => {
form.setFieldValue('appearance.iconUrl', value);
disallowAppNameProgagation();
disallowAppNamePropagation();
}}
value={form.values.appearance.iconUrl}
ref={iconSelectorRef}
@@ -66,26 +66,41 @@ export const AppearanceTab = ({
}}
/>
{form.values.appearance.appNameStatus === 'normal' && (
<Select
label={t('appearance.positionAppName.label')}
description={t('appearance.positionAppName.description')}
data={[
{ value: 'column', label: t('appearance.positionAppName.dropdown.top') as string },
{
value: 'row-reverse',
label: t('appearance.positionAppName.dropdown.right') as string,
},
{
value: 'column-reverse',
label: t('appearance.positionAppName.dropdown.bottom') as string,
},
{ value: 'row', label: t('appearance.positionAppName.dropdown.left') as string },
]}
{...form.getInputProps('appearance.positionAppName')}
onChange={(value) => {
form.setFieldValue('appearance.positionAppName', value);
}}
/>
<>
<Select
label={t('appearance.positionAppName.label')}
description={t('appearance.positionAppName.description')}
data={[
{
value: 'column',
label: t('appearance.positionAppName.dropdown.top') as string },
{
value: 'row-reverse',
label: t('appearance.positionAppName.dropdown.right') as string,
},
{
value: 'column-reverse',
label: t('appearance.positionAppName.dropdown.bottom') as string,
},
{
value: 'row',
label: t('appearance.positionAppName.dropdown.left') as string },
]}
{...form.getInputProps('appearance.positionAppName')}
onChange={(value) => {
form.setFieldValue('appearance.positionAppName', value);
}}
/>
<NumberInput
label={t('appearance.lineClampAppName.label')}
description={t('appearance.lineClampAppName.description')}
min={0}
{...form.getInputProps('appearance.lineClampAppName')}
onChange={(value) => {
form.setFieldValue('appearance.lineClampAppName', value);
}}
/>
</>
)}
</Stack>
</Tabs.Panel>

View File

@@ -95,6 +95,7 @@ export const AvailableElementTypes = ({
iconUrl: '/imgs/logo/logo.png',
appNameStatus: 'normal',
positionAppName: 'column',
lineClampAppName: 1,
},
network: {
enabledStatusChecker: true,

View File

@@ -22,6 +22,7 @@ export const AppPing = ({ app }: AppPingProps) => {
const { data, isFetching, isError, error, isActive } = usePing(app);
const tooltipLabel = useTooltipLabel({ isFetching, isError, data, errorMessage: error?.message });
const isOnline = isError ? false : data?.state === 'online';
const pulse = usePingPulse({ isOnline, settings: userWithSettings?.settings });
if (!isActive) return null;
@@ -32,7 +33,7 @@ export const AppPing = ({ app }: AppPingProps) => {
<motion.div
style={{
position: 'absolute',
bottom: replaceDotWithIcon ? 5 : 20,
bottom: replaceDotWithIcon ? 0 : 20,
right: replaceDotWithIcon ? 8 : 20,
zIndex: 2,
}}
@@ -110,6 +111,16 @@ const usePing = (app: AppType) => {
{
retry: false,
enabled: isActive,
refetchOnWindowFocus: false,
retryDelay(failureCount, error) {
// TODO: Add logic to retry on timeout
return 3000;
},
// 5 minutes of cache
cacheTime: 1000 * 60 * 5,
staleTime: 1000 * 60 * 5,
retryOnMount: true,
select: (data) => {
const isOk = isStatusOk(app, data.status);
if (isOk)

View File

@@ -1,10 +1,9 @@
import { Box, Flex, Text, Tooltip, UnstyledButton } from '@mantine/core';
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 { useCardStyles } from '../../../layout/Common/useCardStyles';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { HomarrCardWrapper } from '../HomarrCardWrapper';
import { BaseTileProps } from '../type';
@@ -17,89 +16,77 @@ interface AppTileProps extends BaseTileProps {
export const AppTile = ({ className, app }: AppTileProps) => {
const isEditMode = useEditModeStore((x) => x.enabled);
const { cx, classes } = useStyles();
const { colorScheme } = useMantineTheme();
const tooltipContent = [
app.appearance.appNameStatus === "hover" ? app.name : undefined,
app.behaviour.tooltipDescription
].filter( e => e ).join( ': ' );
app.appearance.appNameStatus === 'hover' ? app.name : undefined,
app.behaviour.tooltipDescription,
]
.filter((e) => e)
.join(': ');
const {
classes: { card: cardClass },
} = useCardStyles(false);
const isRow = app.appearance.positionAppName.includes('row');
function Inner() {
return (
<Tooltip.Floating
label={tooltipContent}
position="right-start"
c={ colorScheme === 'light' ? "black" : "dark.0" }
color={ colorScheme === 'light' ? "gray.2" : "dark.4" }
c={colorScheme === 'light' ? 'black' : 'dark.0'}
color={colorScheme === 'light' ? 'gray.2' : 'dark.4'}
multiline
disabled={tooltipContent === ''}
styles={{ tooltip: { '&': { maxWidth: 300, }, }, }}
disabled={!tooltipContent}
styles={{ tooltip: { maxWidth: 300 } }}
>
<Flex
m={0}
p={0}
justify="space-around"
align="center"
<Box
className={`${classes.base} ${cx(classes.appContent, 'dashboard-tile-app')}`}
h="100%"
w="100%"
className="dashboard-tile-app"
direction={app.appearance.positionAppName ?? 'column'}
sx={{
flexFlow: app.appearance.positionAppName ?? 'column',
}}
>
<Box w="100%" hidden={["hover", "hidden"].includes(app.appearance.appNameStatus)}>
{app.appearance.appNameStatus === 'normal' && (
<Text
w="100%"
className={cx(classes.appName, 'dashboard-tile-app-title')}
fw={700}
size="md"
ta="center"
weight={700}
className={cx(classes.appName, 'dashboard-tile-app-title')}
lineClamp={2}
sx={{
flex: isRow ? '1' : undefined,
}}
lineClamp={app.appearance.lineClampAppName}
>
{app.name}
</Text>
</Box>
<Box
w="100%"
h="100%"
display="flex"
sx={{
alignContent: 'center',
justifyContent: 'center',
flex: '1 1 auto',
flexWrap: 'wrap',
)}
<motion.img
className={cx('dashboard-tile-app-image')}
src={app.appearance.iconUrl}
height="85%"
width="85%"
alt={app.name}
whileHover={{ scale: 0.9 }}
initial={{ scale: 0.8 }}
style={{
maxHeight: '90%',
maxWidth: '90%',
flex: 1,
overflow: 'auto',
objectFit: 'contain',
width: isRow ? 0 : undefined,
}}
>
<motion.img
className={classes.image}
height="85%"
style={{
objectFit: 'contain',
}}
src={app.appearance.iconUrl}
alt={app.name}
whileHover={{
scale: 1.2,
transition: { duration: 0.2 },
}}
/>
</Box>
</Flex>
/>
</Box>
</Tooltip.Floating>
);
}
return (
<HomarrCardWrapper className={className}>
<HomarrCardWrapper className={className} p={10}>
<AppMenu app={app} />
{!app.url || isEditMode ? (
<UnstyledButton
className={classes.button}
className={`${classes.button} ${classes.base}`}
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
>
<Inner />
@@ -110,7 +97,7 @@ export const AppTile = ({ className, app }: AppTileProps) => {
component={Link}
href={app.behaviour.externalUrl.length > 0 ? app.behaviour.externalUrl : app.url}
target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'}
className={cx(classes.button)}
className={`${classes.button} ${classes.base}`}
>
<Inner />
</UnstyledButton>
@@ -121,9 +108,15 @@ export const AppTile = ({ className, app }: AppTileProps) => {
};
const useStyles = createStyles((theme, _params, getRef) => ({
image: {
maxHeight: '90%',
maxWidth: '90%',
base: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
appContent: {
gap: 0,
overflow: 'visible',
flexGrow: 5,
},
appName: {
wordBreak: 'break-word',
@@ -131,9 +124,6 @@ const useStyles = createStyles((theme, _params, getRef) => ({
button: {
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 4,
},
}));

View File

@@ -2,6 +2,7 @@ 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 {
@@ -11,12 +12,14 @@ 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);
@@ -28,13 +31,13 @@ export const GenericTileMenu = ({
<Menu withinPortal withArrow position="right">
<Menu.Target>
<ActionIcon
style={{ zIndex: 1 }}
size="md"
radius="md"
variant="light"
pos="absolute"
top={8}
right={8}
style={{ zIndex: 1 }}
>
<IconSettings />
</ActionIcon>

View File

@@ -19,13 +19,13 @@ import { IconAlertTriangle, IconPlaylistX, IconPlus } from '@tabler/icons-react'
import { Trans, useTranslation } from 'next-i18next';
import { FC, useState } from 'react';
import { InfoCard } from '../../../InfoCard/InfoCard';
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 { InfoCard } from '../../../InfoCard/InfoCard';
import { DraggableList } from './Inputs/DraggableList';
import { LocationSelection } from './Inputs/LocationSelection';
import { StaticDraggableList } from './Inputs/StaticDraggableList';
@@ -138,6 +138,8 @@ const WidgetOptionTypeSwitch: FC<{
const info = option.info ?? false;
const link = option.infoLink ?? undefined;
if (option.hide) return null;
switch (option.type) {
case 'switch':
return (
@@ -148,15 +150,17 @@ const WidgetOptionTypeSwitch: FC<{
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
{...option.inputProps}
/>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
);
case 'text':
return (
<Stack spacing={0}>
<Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
<Text size="0.875rem" weight="500">
{t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<TextInput
value={value as string}
@@ -169,8 +173,10 @@ const WidgetOptionTypeSwitch: FC<{
return (
<Stack spacing={0}>
<Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
<Text size="0.875rem" weight="500">
{t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<MultiSelect
data={option.data}
@@ -183,15 +189,26 @@ const WidgetOptionTypeSwitch: FC<{
</Stack>
);
case 'select':
const items = typeof option.data === 'function' ? option.data() : option.data;
const data = items.map((dataType) => {
return !dataType.label
? {
value: dataType.value,
label: t(`descriptor.settings.${key}.data.${dataType.value}`),
}
: dataType;
});
return (
<Stack spacing={0}>
<Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
<Text size="0.875rem" weight="500">
{t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<Select
defaultValue={option.defaultValue}
data={option.data}
data={data}
value={value as string}
onChange={(v) => handleChange(key, v ?? option.defaultValue)}
withinPortal
@@ -203,8 +220,10 @@ const WidgetOptionTypeSwitch: FC<{
return (
<Stack spacing={0}>
<Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
<Text size="0.875rem" weight="500">
{t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<NumberInput
value={value as number}
@@ -217,8 +236,10 @@ const WidgetOptionTypeSwitch: FC<{
return (
<Stack spacing={0}>
<Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
<Text size="0.875rem" weight="500">
{t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<Slider
label={value}
@@ -270,7 +291,7 @@ const WidgetOptionTypeSwitch: FC<{
<Stack spacing="xs">
<Group align="center" spacing="sm">
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<StaticDraggableList
value={typedVal}
@@ -298,8 +319,10 @@ const WidgetOptionTypeSwitch: FC<{
return (
<Stack spacing={0}>
<Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
<Text size="0.875rem" weight="500">
{t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<MultiSelect
data={value.map((name: any) => ({ value: name, label: name }))}
@@ -324,7 +347,7 @@ const WidgetOptionTypeSwitch: FC<{
<Stack spacing="xs">
<Group align="center" spacing="sm">
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<DraggableList
items={Array.from(value).map((v: any) => ({