Merge branch 'dev' into edit-mode-password

This commit is contained in:
Thomas Camlong
2023-03-22 22:22:16 +08:00
committed by GitHub
41 changed files with 501 additions and 228 deletions

View File

@@ -32,8 +32,15 @@ body:
- type: textarea
id: logs
attributes:
label: Additional info
description: Logs? Screenshots? More info?
label: Logs
description: Provide your Homarr logs so we can investigate what's going on
validations:
required: true
- type: textarea
id: context
attributes:
label: Context
description: Screenshots? More info?
validations:
required: false
- type: checkboxes
@@ -42,9 +49,11 @@ body:
label: Please tick the boxes
description: Before submitting, please ensure that
options:
- label: You've read the [docs](https://github.com/ajnart/homarr#readme)
- label: I confirm that I attached the proper logs
required: true
- label: You've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
- label: I've read the [docs](https://github.com/ajnart/homarr#readme)
required: true
- label: You've tried to debug yourself
- label: I've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
required: true
- label: I've tried to debug myself
required: true

View File

@@ -24,11 +24,14 @@ env:
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
yarn_install_and_build:
yarn_install_and_build_dev:
runs-on: ubuntu-latest
permissions:
packages: write
@@ -67,7 +70,7 @@ jobs:
- run: yarn install --immutable
- run: yarn build
- run: yarn turbo build
- name: Docker meta
if: github.event_name != 'pull_request'

1
.gitignore vendored
View File

@@ -32,6 +32,7 @@ yarn-error.log*
# vercel
.vercel
.turbo
*.tsbuildinfo
# storybook

View File

@@ -10,6 +10,7 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"turbo" : "turbo run build",
"analyze": "ANALYZE=true next build",
"start": "next start",
"typecheck": "tsc --noEmit",
@@ -53,7 +54,7 @@
"html-entities": "^2.3.3",
"i18next": "^21.9.1",
"js-file-download": "^0.4.12",
"next": "^13.1.6",
"next": "^13.2.1",
"next-i18next": "^11.3.0",
"nzbget-api": "^0.0.3",
"prismjs": "^1.29.0",
@@ -92,7 +93,7 @@
"jest": "^28.1.3",
"prettier": "^2.7.1",
"sass": "^1.56.1",
"turbo": "^1.7.4",
"turbo": "^1.8.3",
"typescript": "^4.7.4",
"video.js": "^8.0.3"
},

View File

@@ -2,6 +2,10 @@
"description": "Homarr is a <strong>sleek</strong>, <strong>modern</strong> dashboard that puts all of your apps and services at your fingertips. With Homarr, you can access and control everything in one convenient location. Homarr seamlessly integrates with the apps you've added, providing you with valuable information and giving you complete control. Installation is a breeze, and Homarr supports a wide range of deployment methods.",
"contact": "Having trouble or questions? Connect with us!",
"addToDashboard": "Add to Dashboard",
"tip": "Mod refers to your modifier key, it is Ctrl and Command/Super/Windows key",
"key": "Shortcut key",
"action": "Action",
"keybinds": "Keybinds",
"metrics": {
"configurationSchemaVersion": "Configuration schema version",
"configurationsCount": "Available configurations",

View File

@@ -0,0 +1,14 @@
{
"card": {
"title": "Oops, there was an error!",
"buttons": {
"details": "Details",
"tryAgain": "Try again"
}
},
"modal": {
"text": "We're sorry for the inconvinience! This shouln't happen - please report this issue on GitHub.",
"label": "Your error",
"reportButton": "Report this error"
}
}

View File

@@ -1,13 +1,14 @@
import {
Accordion,
ActionIcon,
Anchor,
Badge,
Button,
createStyles,
Divider,
Grid,
Group,
HoverCard,
Kbd,
Modal,
Table,
Text,
@@ -36,6 +37,7 @@ import { useConfigStore } from '../../../../config/store';
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
import { useColorTheme } from '../../../../tools/color';
import Tip from '../../../layout/Tip';
import { usePrimaryGradient } from '../../../layout/useGradient';
import Credits from '../../../Settings/Common/Credits';
@@ -51,6 +53,23 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
const informations = useInformationTableItems(newVersionAvailable);
const { t } = useTranslation(['common', 'layout/modals/about']);
const keybinds = [
{ key: 'Mod + J', shortcut: 'Toggle light/dark mode' },
{ key: 'Mod + K', shortcut: 'Focus on search bar' },
{ key: 'Mod + B', shortcut: 'Open docker widget' },
{ key: 'Mod + E', shortcut: 'Toggle Edit mode' },
];
const rows = keybinds.map((element) => (
<tr key={element.key}>
<td>
<Kbd>{element.key}</Kbd>
</td>
<td>
<Text>{element.shortcut}</Text>
</td>
</tr>
));
return (
<Modal
onClose={() => closeModal()}
@@ -77,7 +96,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
<Trans i18nKey="layout/modals/about:description" />
</Text>
<Table mb="lg" striped highlightOnHover withBorder>
<Table mb="lg" highlightOnHover withBorder>
<tbody>
{informations.map((item, index) => (
<tr key={index}>
@@ -101,8 +120,26 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
))}
</tbody>
</Table>
<Accordion mb={5} variant="contained" radius="md">
<Accordion.Item value="keybinds">
<Accordion.Control icon={<IconKey size={20} />}>
{t('layout/modals/about:keybinds')}
</Accordion.Control>
<Accordion.Panel>
<Table mb={5}>
<thead>
<tr>
<th>{t('layout/modals/about:key')}</th>
<th>{t('layout/modals/about:action')}</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
<Tip>{t('layout/modals/about:tip')}</Tip>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
<Divider variant="dashed" mb="md" />
<Title order={6} mb="xs" align="center">
{t('layout/modals/about:contact')}
</Title>

View File

@@ -90,7 +90,8 @@ export const IconSelector = ({
}
variant="default"
withAsterisk
dropdownComponent={(props: any) => <ScrollArea {...props} mah={400} />}
dropdownComponent={(props: any) => <ScrollArea {...props} mah={250} />}
dropdownPosition="bottom"
required
onChange={(event) => {
if (allowAppNamePropagation) {

View File

@@ -6,7 +6,6 @@ import {
Grid,
Group,
PasswordInput,
Stack,
ThemeIcon,
Title,
Text,
@@ -40,7 +39,7 @@ export const GenericSecretInput = ({
const Icon = setIcon;
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(false);
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(!secretIsPresent);
const { t } = useTranslation(['layout/modals/add-app', 'common']);
return (
@@ -51,26 +50,26 @@ export const GenericSecretInput = ({
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
<Icon size={18} />
</ThemeIcon>
<Stack spacing={0}>
<Flex justify="start" align="start" direction="column">
<Group spacing="xs">
<Title className={classes.subtitle} order={6}>
{t(label)}
</Title>
<Group spacing="xs">
{secretIsPresent ? (
<Badge className={classes.textTransformUnset} color="green" variant="dot">
{t('integration.type.defined')}
<Badge
className={classes.textTransformUnset}
color={secretIsPresent ? 'green' : 'red'}
variant="dot"
>
{secretIsPresent
? t('integration.type.defined')
: t('integration.type.undefined')}
</Badge>
) : (
<Badge className={classes.textTransformUnset} color="red" variant="dot">
{t('integration.type.undefined')}
</Badge>
)}
{type === 'private' ? (
<Tooltip
label={t('integration.type.explanationPrivate')}
width={200}
width={400}
multiline
withinPortal
withArrow
@@ -82,7 +81,7 @@ export const GenericSecretInput = ({
) : (
<Tooltip
label={t('integration.type.explanationPublic')}
width={200}
width={400}
multiline
withinPortal
withArrow
@@ -94,29 +93,20 @@ export const GenericSecretInput = ({
)}
</Group>
</Group>
<Text size="xs" color="dimmed">
<Text size="xs" color="dimmed" w={400}>
{type === 'private'
? 'Private: Once saved, you cannot read out this value again'
: 'Public: Can be read out repeatedly'}
</Text>
</Stack>
</Flex>
</Group>
</Grid.Col>
<Grid.Col xs={12} md={6}>
<Flex gap={10} justify="end" align="end">
<Button
onClick={() => {
setDisplayUpdateField(false);
onClickUpdateButton(undefined);
}}
variant="subtle"
color="gray"
px="xl"
>
{t('integration.secrets.clear')}
</Button>
{displayUpdateField === true ? (
<PasswordInput
required
defaultValue={value}
placeholder="new secret"
styles={{ root: { width: 200 } }}
{...props}

View File

@@ -10,6 +10,9 @@ interface NetworkTabProps {
export const NetworkTab = ({ form }: NetworkTabProps) => {
const { t } = useTranslation('layout/modals/add-app');
const acceptableStatusCodes = (form.values.network.statusCodes ?? ['200']).map((x) =>
x.toString()
);
return (
<Tabs.Panel value="network" pt="lg">
<Switch
@@ -27,7 +30,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
data={StatusCodes}
clearable
searchable
defaultValue={form.values.network.okStatus.map((x) => `${x}`)}
defaultValue={acceptableStatusCodes}
variant="default"
{...form.getInputProps('network.statusCodes')}
/>

View File

@@ -95,7 +95,7 @@ export const AvailableElementTypes = ({
},
network: {
enabledStatusChecker: true,
okStatus: [200],
statusCodes: ['200'],
},
behaviour: {
isOpeningNewTab: true,

View File

@@ -19,7 +19,7 @@ export const AppPing = ({ app }: AppPingProps) => {
queryKey: ['ping', { id: app.id, name: app.name }],
queryFn: async () => {
const response = await fetch(`/api/modules/ping?url=${encodeURI(app.url)}`);
const isOk = app.network.okStatus.includes(response.status);
const isOk = app.network.statusCodes.includes(response.status.toString());
return {
status: response.status,
state: isOk ? 'online' : 'down',
@@ -60,5 +60,3 @@ export const AppPing = ({ app }: AppPingProps) => {
</motion.div>
);
};
type PingState = 'loading' | 'down' | 'online';

View File

@@ -36,7 +36,13 @@ export const AppTile = ({ className, app }: AppTileProps) => {
className="dashboard-tile-app"
>
<Box hidden={false}>
<Title order={5} size="md" ta="center" lineClamp={1} className={cx(classes.appName, 'dashboard-tile-app-title')}>
<Title
order={5}
size="md"
ta="center"
lineClamp={1}
className={cx(classes.appName, 'dashboard-tile-app-title')}
>
{app.name}
</Title>
</Box>

View File

@@ -24,9 +24,9 @@ export const GenericTileMenu = ({
}
return (
<Menu withinPortal withArrow position="right-start">
<Menu withinPortal withArrow position="right">
<Menu.Target>
<ActionIcon pos="absolute" top={4} right={4}>
<ActionIcon size="md" radius="md" variant="light" pos="absolute" top={8} right={8}>
<IconDots />
</ActionIcon>
</Menu.Target>

View File

@@ -10,9 +10,7 @@ export default function CustomizationSettings() {
return (
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
<Stack mt="xs" mb="md" spacing="xs">
<Text color="dimmed">
{t('text')}
</Text>
<Text color="dimmed">{t('text')}</Text>
<CustomizationSettingsAccordeon />
</Stack>
</ScrollArea>

View File

@@ -8,7 +8,9 @@ export const LogoImageChanger = () => {
const { t } = useTranslation('settings/customization/page-appearance');
const updateConfig = useConfigStore((x) => x.updateConfig);
const { config, name: configName } = useConfigContext();
const [logoImageSrc, setLogoImageSrc] = useState(config?.settings.customization.logoImageUrl ?? '/imgs/logo/logo.png');
const [logoImageSrc, setLogoImageSrc] = useState(
config?.settings.customization.logoImageUrl ?? '/imgs/logo/logo.png'
);
if (!configName) return null;

View File

@@ -1,18 +1,20 @@
import { ActionIcon, Button, Group, Text, Title, Tooltip } from '@mantine/core';
import { useHotkeys, useWindowEvent } from '@mantine/hooks';
import { hideNotification, showNotification } from '@mantine/notifications';
import { IconEditCircle, IconEditCircleOff } from '@tabler/icons';
import axios from 'axios';
import Consola from 'consola';
import { ActionIcon, Button, Group, Text, Title, Tooltip } from '@mantine/core';
import { IconEditCircle, IconEditCircleOff } from '@tabler/icons';
import { getCookie } from 'cookies-next';
import { Trans, useTranslation } from 'next-i18next';
import { useHotkeys } from '@mantine/hooks';
import { hideNotification, showNotification } from '@mantine/notifications';
import { useConfigContext } from '../../../../../config/provider';
import { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan';
import { useEditModeStore } from '../../../../Dashboard/Views/useEditModeStore';
import { AddElementAction } from '../AddElementAction/AddElementAction';
import { useNamedWrapperColumnCount } from '../../../../Dashboard/Wrappers/gridstack/store';
import { useCardStyles } from '../../../useCardStyles';
import { AddElementAction } from '../AddElementAction/AddElementAction';
const beforeUnloadEventText = 'Exit the edit mode to save your changes';
export const ToggleEditModeAction = () => {
const { enabled, toggleEditMode } = useEditModeStore();
@@ -27,7 +29,17 @@ export const ToggleEditModeAction = () => {
const { config } = useConfigContext();
const { classes } = useCardStyles(true);
useHotkeys([['ctrl+E', toggleEditMode]]);
useHotkeys([['mod+E', toggleEditMode]]);
useWindowEvent('beforeunload', (event: BeforeUnloadEvent) => {
if (enabled) {
// eslint-disable-next-line no-param-reassign
event.returnValue = beforeUnloadEventText;
return beforeUnloadEventText;
}
return undefined;
});
const toggleButtonClicked = () => {
toggleEditMode();

View File

@@ -10,5 +10,7 @@ export const useGetDashboardIcons = () =>
return data as NormalizedIconRepositoryResult[];
},
refetchOnMount: false,
// Cache for infinity, refetch every so often.
cacheTime: Infinity,
refetchOnWindowFocus: false,
});

View File

@@ -1,7 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { NormalizedDownloadQueueResponse } from '../../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
export const useGetDownloadClientsQueue = () => useQuery({
export const useGetDownloadClientsQueue = () =>
useQuery({
queryKey: ['network-speed'],
queryFn: async (): Promise<NormalizedDownloadQueueResponse> => {
const response = await fetch('/api/modules/downloads');

View File

@@ -1,8 +1,8 @@
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
// eslint-disable-next-line consistent-return
export function middleware(req: NextRequest, ev: NextFetchEvent) {
export function middleware(req: NextRequest) {
const { cookies } = req;
// Don't even bother with the middleware if there is no defined password
if (!process.env.PASSWORD) return NextResponse.next();

View File

@@ -177,7 +177,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
},
network: {
enabledStatusChecker: true,
okStatus: [200],
statusCodes: ['200'],
},
behaviour: {
isOpeningNewTab: true,

View File

@@ -38,6 +38,7 @@ function App(
colorScheme: ColorScheme;
packageAttributes: ServerSidePackageAttributesType;
editModeEnabled: boolean;
defaultColorScheme: ColorScheme;
}
) {
const { Component, pageProps } = props;
@@ -55,7 +56,7 @@ function App(
// hook will return either 'dark' or 'light' on client
// and always 'light' during ssr as window.matchMedia is not available
const preferredColorScheme = useColorScheme();
const preferredColorScheme = useColorScheme(props.defaultColorScheme);
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>({
key: 'mantine-color-scheme',
defaultValue: preferredColorScheme,
@@ -144,10 +145,18 @@ App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => {
'EXPERIMENTAL: You have disabled the edit mode. Modifications are no longer possible and any requests on the API will be dropped. If you want to disable this, unset the DISABLE_EDIT_MODE environment variable. This behaviour may be removed in future versions of Homarr'
);
}
if (process.env.DEFAULT_COLOR_SCHEME !== undefined) {
Consola.debug(`Overriding the default color scheme with ${process.env.DEFAULT_COLOR_SCHEME}`);
}
const colorScheme: ColorScheme = process.env.DEFAULT_COLOR_SCHEME as ColorScheme ?? 'light';
return {
colorScheme: getCookie('color-scheme', ctx) || 'light',
packageAttributes: getServiceSidePackageAttributes(),
editModeEnabled: !disableEditMode,
defaultColorScheme: colorScheme,
};
};

View File

@@ -6,10 +6,26 @@ import { UnpkgIconsRepository } from '../../../tools/server/images/unpkg-icons-r
const Get = async (request: NextApiRequest, response: NextApiResponse) => {
const respositories = [
new LocalIconsRepository(),
new JsdelivrIconsRepository(JsdelivrIconsRepository.tablerRepository, 'Walkxcode Dashboard Icons', 'Walkxcode on Github'),
new UnpkgIconsRepository(UnpkgIconsRepository.tablerRepository, 'Tabler Icons', 'Tabler Icons - GitHub (MIT)'),
new JsdelivrIconsRepository(JsdelivrIconsRepository.papirusRepository, 'Papirus Icons', 'Papirus Development Team on GitHub (Apache 2.0)'),
new JsdelivrIconsRepository(JsdelivrIconsRepository.homelabSvgAssetsRepository, 'Homelab Svg Assets', 'loganmarchione on GitHub (MIT)'),
new JsdelivrIconsRepository(
JsdelivrIconsRepository.tablerRepository,
'Walkxcode Dashboard Icons',
'Walkxcode on Github'
),
new UnpkgIconsRepository(
UnpkgIconsRepository.tablerRepository,
'Tabler Icons',
'Tabler Icons - GitHub (MIT)'
),
new JsdelivrIconsRepository(
JsdelivrIconsRepository.papirusRepository,
'Papirus Icons',
'Papirus Development Team on GitHub (Apache 2.0)'
),
new JsdelivrIconsRepository(
JsdelivrIconsRepository.homelabSvgAssetsRepository,
'Homelab Svg Assets',
'loganmarchione on GitHub (MIT)'
),
];
const fetches = respositories.map((rep) => rep.fetch());
const data = await Promise.all(fetches);

View File

@@ -61,7 +61,9 @@ const Get = async (request: NextApiRequest, response: NextApiResponse) => {
const responseBody = { apps: data, failedApps: failedClients } as NormalizedDownloadQueueResponse;
if (failedClients.length > 0) {
Consola.warn(`${failedClients.length} download clients failed. Please check your configuration and the above log`);
Consola.warn(
`${failedClients.length} download clients failed. Please check your configuration and the above log`
);
}
return response.status(200).json(responseBody);

View File

@@ -53,6 +53,7 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) =>
title: item.title ? decode(item.title) : undefined,
content: decode(item.content),
enclosure: createEnclosure(item),
link: createLink(item),
}))
.sort((a: { pubDate: number }, b: { pubDate: number }) => {
if (!a.pubDate || !b.pubDate) {
@@ -70,6 +71,14 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) =>
});
};
const createLink = (item: any) => {
if (item.link) {
return item.link;
}
return item.guid;
};
const createEnclosure = (item: any) => {
if (item.enclosure) {
return item.enclosure;

View File

@@ -159,7 +159,7 @@ const migrateService = (oldService: serviceItem, areaType: AreaType): ConfigAppT
},
network: {
enabledStatusChecker: oldService.ping ?? true,
okStatus: oldService.status?.map((str) => parseInt(str, 10)) ?? [200],
statusCodes: oldService.status ?? ['200'],
},
appearance: {
iconUrl: migrateIcon(oldService.icon),

View File

@@ -23,7 +23,7 @@ export class JsdelivrIconsRepository extends AbstractIconRepository {
constructor(
private readonly repository: JsdelivrRepositoryUrl,
private readonly displayName: string,
copyright: string,
copyright: string
) {
super(copyright);
}

View File

@@ -36,6 +36,7 @@ export const dashboardNamespaces = [
'modules/media-server',
'modules/common-media-cards',
'modules/video-stream',
'widgets/error-boundary',
];
export const loginNamespaces = ['authentication/login'];

View File

@@ -14,18 +14,23 @@ export type GenericCurrentlyPlaying = {
episodeCount: number | undefined;
type: 'audio' | 'video' | 'tv' | 'movie' | undefined;
metadata: {
video: {
video:
| {
videoCodec: string | undefined;
videoFrameRate: string | undefined;
height: number | undefined;
width: number | undefined;
bitrate: number | undefined;
} | undefined;
audio: {
}
| undefined;
audio:
| {
audioCodec: string | undefined;
audioChannels: number | undefined;
} | undefined;
transcoding: {
}
| undefined;
transcoding:
| {
context: string | undefined;
sourceVideoCodec: string | undefined;
sourceAudioCodec: string | undefined;
@@ -41,6 +46,7 @@ export type GenericCurrentlyPlaying = {
height: number | undefined;
transcodeHwRequested: boolean | undefined;
timeStamp: number | undefined;
} | undefined;
}
| undefined;
};
};

View File

@@ -23,7 +23,7 @@ interface AppBehaviourType {
interface AppNetworkType {
enabledStatusChecker: boolean;
okStatus: number[];
statusCodes: string[];
}
interface AppAppearanceType {

View File

@@ -2,6 +2,7 @@ import { ComponentType, useMemo } from 'react';
import Widgets from '.';
import { HomarrCardWrapper } from '../components/Dashboard/Tiles/HomarrCardWrapper';
import { WidgetsMenu } from '../components/Dashboard/Tiles/Widgets/WidgetsMenu';
import ErrorBoundary from './boundary';
import { IWidget } from './widgets';
interface WidgetWrapperProps {
@@ -40,9 +41,11 @@ export const WidgetWrapper = ({
const widgetWithDefaultProps = useWidget(widget);
return (
<ErrorBoundary>
<HomarrCardWrapper className={className}>
<WidgetsMenu integration={widgetId} widget={widgetWithDefaultProps} />
<WidgetComponent widget={widgetWithDefaultProps} />
</HomarrCardWrapper>
</ErrorBoundary>
);
};

127
src/widgets/boundary.tsx Normal file
View File

@@ -0,0 +1,127 @@
import Consola from 'consola';
import React, { ReactNode } from 'react';
import { openModal } from '@mantine/modals';
import { withTranslation } from 'next-i18next';
import { Button, Card, Center, Code, Group, Stack, Text, Title } from '@mantine/core';
import { IconBrandGithub, IconBug, IconInfoCircle, IconRefresh } from '@tabler/icons';
type ErrorBoundaryState = {
hasError: boolean;
error: Error | undefined;
};
type ErrorBoundaryProps = {
t: (key: string) => string;
children: ReactNode;
};
/**
* A custom error boundary, that catches errors within widgets and renders an error component.
* The error component can be refreshed and shows a modal with error details
*/
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: any) {
super(props);
// Define a state variable to track whether is an error or not
this.state = { hasError: false, error: undefined };
}
static getDerivedStateFromError(error: Error) {
// Update state so the next render will show the fallback UI
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
Consola.error(`Error while rendering widget, ${error}: ${errorInfo}`);
}
render() {
// Check if the error is thrown
if (this.state.hasError) {
return (
<Card
m={10}
sx={(theme) => ({
backgroundColor: theme.colors.red[5],
})}
radius="lg"
shadow="sm"
withBorder
>
<Center>
<Stack align="center">
<IconBug color="white" />
<Stack spacing={0} align="center">
<Title order={4} color="white" align="center">
{this.props.t('card.title')}
</Title>
{this.state.error && (
<Text color="white" align="center" size="sm">
{this.state.error.toString()}
</Text>
)}
</Stack>
<Group>
<Button
onClick={() =>
openModal({
title: 'Your widget had an error',
children: (
<>
<Text size="sm" mb="sm">
{this.props.t('modal.text')}
</Text>
{this.state.error && (
<>
<Text weight="bold" size="sm">
{this.props.t('modal.label')}
</Text>
<Code block>{this.state.error.toString()}</Code>
</>
)}
<Button
sx={(theme) => ({
backgroundColor: theme.colors.gray[8],
'&:hover': {
backgroundColor: theme.colors.gray[9],
},
})}
leftIcon={<IconBrandGithub />}
component="a"
href="https://github.com/ajnart/homarr/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=bug.yml&title=New%20bug"
target="_blank"
mt="md"
fullWidth
>
{(this.props.t('modal.reportButton'))}
</Button>
</>
),
})
}
leftIcon={<IconInfoCircle size={16} />}
variant="light"
>
{this.props.t('card.buttons.details')}
</Button>
<Button
onClick={() => this.setState({ hasError: false })}
leftIcon={<IconRefresh size={16} />}
variant="light"
>
{this.props.t('card.buttons.tryAgain')}
</Button>
</Group>
</Stack>
</Center>
</Card>
);
}
// Return children components in case of no error
return this.props.children;
}
}
export default withTranslation('widgets/error-boundary')(ErrorBoundary);

View File

@@ -25,7 +25,7 @@ const definition = defineWidget({
component: DateTile,
});
export type IDateWidget = IWidget<typeof definition['id'], typeof definition>;
export type IDateWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface DateTileProps {
widget: IDateWidget;

View File

@@ -107,7 +107,9 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
</Group>
<Text>{session.sessionName}</Text>
</Flex>
{details.length > 0 && <Divider label="Stats for nerds" labelPosition="center" mt="lg" mb="sm" />}
{details.length > 0 && (
<Divider label="Stats for nerds" labelPosition="center" mt="lg" mb="sm" />
)}
<Grid>
{details.map((detail, index) => (
<Grid.Col xs={12} sm={6} key={index}>

View File

@@ -16,7 +16,6 @@ import {
Title,
UnstyledButton,
} from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import {
IconBulldozer,
IconCalendarTime,
@@ -65,7 +64,6 @@ function RssTile({ widget }: RssTileProps) {
);
const { classes } = useStyles();
const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false);
const { ref, height } = useElementSize();
if (!data || isLoading) {
return (
@@ -88,7 +86,7 @@ function RssTile({ widget }: RssTileProps) {
}
return (
<Stack ref={ref} h="100%">
<Stack h="100%">
<LoadingOverlay visible={loadingOverlayVisible} />
<Flex gap="md">
{data.feed.image ? (
@@ -121,7 +119,7 @@ function RssTile({ widget }: RssTileProps) {
<Card
key={index}
withBorder
component={Link}
component={Link ?? 'div'}
href={item.link}
radius="md"
target="_blank"
@@ -137,6 +135,7 @@ function RssTile({ widget }: RssTileProps) {
)}
<Flex gap="xs">
{item.enclosure && (
<MediaQuery query="(max-width: 1200px)" styles={{ display: 'none' }}>
<Image
src={item.enclosure?.url ?? undefined}
@@ -146,7 +145,8 @@ function RssTile({ widget }: RssTileProps) {
withPlaceholder
/>
</MediaQuery>
<Flex gap={2} direction="column">
)}
<Flex gap={2} direction="column" w="100%">
{item.categories && (
<Flex gap="xs" wrap="wrap" h={20} style={{ overflow: 'hidden' }}>
{item.categories.map((category: any, categoryIndex: number) => (
@@ -181,12 +181,14 @@ function RssTile({ widget }: RssTileProps) {
{data.feed.pubDate}
</Text>
</Group>
{data.feed.lastBuildDate && (
<Group>
<IconBulldozer size={14} />
<Text color="dimmed" size="sm">
{data.feed.lastBuildDate}
</Text>
</Group>
)}
{data.feed.feedUrl && (
<Group spacing="sm">
<IconSpeakerphone size={14} />

View File

@@ -46,7 +46,7 @@ const definition = defineWidget({
},
});
export type IUsenetWidget = IWidget<typeof definition['id'], typeof definition>;
export type IUsenetWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface UseNetTileProps {
widget: IUsenetWidget;

View File

@@ -28,7 +28,7 @@ const definition = defineWidget({
component: WeatherTile,
});
export type IWeatherWidget = IWidget<typeof definition['id'], typeof definition>;
export type IWeatherWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface WeatherTileProps {
widget: IWeatherWidget;

11
turbo.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"outputs": [
".next/**",
"!.next/cache/**"
]
}
}
}

185
yarn.lock
View File

@@ -1272,10 +1272,10 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:13.1.6":
version: 13.1.6
resolution: "@next/env@npm:13.1.6"
checksum: 0f911a18f0b3372007632fffa87f5d7f802c00d07b3bf757d2d09574735ae43f60000ecdf64b6f06e195971c508c2bcee82dd1e3aab27a08a4300eb0317652bb
"@next/env@npm:13.2.1":
version: 13.2.1
resolution: "@next/env@npm:13.2.1"
checksum: 16a877479348b9d6a9e69e74312546889d6419a6dec0556cf7d9ed5876b4f69a0974c804f2c5ec81526522c243d97bd2d6919d3241cd165e10e8fd6c3bb4b975
languageName: node
linkType: hard
@@ -1288,93 +1288,93 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-android-arm-eabi@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-android-arm-eabi@npm:13.1.6"
"@next/swc-android-arm-eabi@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-android-arm-eabi@npm:13.2.1"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@next/swc-android-arm64@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-android-arm64@npm:13.1.6"
"@next/swc-android-arm64@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-android-arm64@npm:13.2.1"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-darwin-arm64@npm:13.1.6"
"@next/swc-darwin-arm64@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-darwin-arm64@npm:13.2.1"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-darwin-x64@npm:13.1.6"
"@next/swc-darwin-x64@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-darwin-x64@npm:13.2.1"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-freebsd-x64@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-freebsd-x64@npm:13.1.6"
"@next/swc-freebsd-x64@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-freebsd-x64@npm:13.2.1"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm-gnueabihf@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-linux-arm-gnueabihf@npm:13.1.6"
"@next/swc-linux-arm-gnueabihf@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-linux-arm-gnueabihf@npm:13.2.1"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-linux-arm64-gnu@npm:13.1.6"
"@next/swc-linux-arm64-gnu@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-linux-arm64-gnu@npm:13.2.1"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-linux-arm64-musl@npm:13.1.6"
"@next/swc-linux-arm64-musl@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-linux-arm64-musl@npm:13.2.1"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-linux-x64-gnu@npm:13.1.6"
"@next/swc-linux-x64-gnu@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-linux-x64-gnu@npm:13.2.1"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-linux-x64-musl@npm:13.1.6"
"@next/swc-linux-x64-musl@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-linux-x64-musl@npm:13.2.1"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-win32-arm64-msvc@npm:13.1.6"
"@next/swc-win32-arm64-msvc@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-win32-arm64-msvc@npm:13.2.1"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-win32-ia32-msvc@npm:13.1.6"
"@next/swc-win32-ia32-msvc@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-win32-ia32-msvc@npm:13.2.1"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.1.6":
version: 13.1.6
resolution: "@next/swc-win32-x64-msvc@npm:13.1.6"
"@next/swc-win32-x64-msvc@npm:13.2.1":
version: 13.2.1
resolution: "@next/swc-win32-x64-msvc@npm:13.2.1"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@@ -4990,7 +4990,7 @@ __metadata:
i18next: ^21.9.1
jest: ^28.1.3
js-file-download: ^0.4.12
next: ^13.1.6
next: ^13.2.1
next-i18next: ^11.3.0
nzbget-api: ^0.0.3
prettier: ^2.7.1
@@ -5001,7 +5001,7 @@ __metadata:
rss-parser: ^3.12.0
sabnzbd-api: ^1.5.0
sass: ^1.56.1
turbo: ^1.7.4
turbo: ^1.8.3
typescript: ^4.7.4
uuid: ^8.3.2
video.js: ^8.0.3
@@ -6697,29 +6697,30 @@ __metadata:
languageName: node
linkType: hard
"next@npm:^13.1.6":
version: 13.1.6
resolution: "next@npm:13.1.6"
"next@npm:^13.2.1":
version: 13.2.1
resolution: "next@npm:13.2.1"
dependencies:
"@next/env": 13.1.6
"@next/swc-android-arm-eabi": 13.1.6
"@next/swc-android-arm64": 13.1.6
"@next/swc-darwin-arm64": 13.1.6
"@next/swc-darwin-x64": 13.1.6
"@next/swc-freebsd-x64": 13.1.6
"@next/swc-linux-arm-gnueabihf": 13.1.6
"@next/swc-linux-arm64-gnu": 13.1.6
"@next/swc-linux-arm64-musl": 13.1.6
"@next/swc-linux-x64-gnu": 13.1.6
"@next/swc-linux-x64-musl": 13.1.6
"@next/swc-win32-arm64-msvc": 13.1.6
"@next/swc-win32-ia32-msvc": 13.1.6
"@next/swc-win32-x64-msvc": 13.1.6
"@next/env": 13.2.1
"@next/swc-android-arm-eabi": 13.2.1
"@next/swc-android-arm64": 13.2.1
"@next/swc-darwin-arm64": 13.2.1
"@next/swc-darwin-x64": 13.2.1
"@next/swc-freebsd-x64": 13.2.1
"@next/swc-linux-arm-gnueabihf": 13.2.1
"@next/swc-linux-arm64-gnu": 13.2.1
"@next/swc-linux-arm64-musl": 13.2.1
"@next/swc-linux-x64-gnu": 13.2.1
"@next/swc-linux-x64-musl": 13.2.1
"@next/swc-win32-arm64-msvc": 13.2.1
"@next/swc-win32-ia32-msvc": 13.2.1
"@next/swc-win32-x64-msvc": 13.2.1
"@swc/helpers": 0.4.14
caniuse-lite: ^1.0.30001406
postcss: 8.4.14
styled-jsx: 5.1.1
peerDependencies:
"@opentelemetry/api": ^1.4.0
fibers: ">= 3.1.0"
node-sass: ^6.0.0 || ^7.0.0
react: ^18.2.0
@@ -6753,6 +6754,8 @@ __metadata:
"@next/swc-win32-x64-msvc":
optional: true
peerDependenciesMeta:
"@opentelemetry/api":
optional: true
fibers:
optional: true
node-sass:
@@ -6761,7 +6764,7 @@ __metadata:
optional: true
bin:
next: dist/bin/next
checksum: 584977e382bd826c21e7fc5f67bca50e4d95741a854b1686394d45331404479c7266569671227421975fc18e5cf70769a4ad7edede7450d4497213205bba77c8
checksum: 2dba145ef4d604cd8eadc27f9e5a537df799614d1a801b9161a997f77a432684871eae51642580972a80ef363d724789677ae7c5fe44dc3dd66e71cd43f609c8
languageName: node
linkType: hard
@@ -8363,58 +8366,58 @@ __metadata:
languageName: node
linkType: hard
"turbo-darwin-64@npm:1.8.0":
version: 1.8.0
resolution: "turbo-darwin-64@npm:1.8.0"
"turbo-darwin-64@npm:1.8.3":
version: 1.8.3
resolution: "turbo-darwin-64@npm:1.8.3"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"turbo-darwin-arm64@npm:1.8.0":
version: 1.8.0
resolution: "turbo-darwin-arm64@npm:1.8.0"
"turbo-darwin-arm64@npm:1.8.3":
version: 1.8.3
resolution: "turbo-darwin-arm64@npm:1.8.3"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"turbo-linux-64@npm:1.8.0":
version: 1.8.0
resolution: "turbo-linux-64@npm:1.8.0"
"turbo-linux-64@npm:1.8.3":
version: 1.8.3
resolution: "turbo-linux-64@npm:1.8.3"
conditions: os=linux & cpu=x64
languageName: node
linkType: hard
"turbo-linux-arm64@npm:1.8.0":
version: 1.8.0
resolution: "turbo-linux-arm64@npm:1.8.0"
"turbo-linux-arm64@npm:1.8.3":
version: 1.8.3
resolution: "turbo-linux-arm64@npm:1.8.3"
conditions: os=linux & cpu=arm64
languageName: node
linkType: hard
"turbo-windows-64@npm:1.8.0":
version: 1.8.0
resolution: "turbo-windows-64@npm:1.8.0"
"turbo-windows-64@npm:1.8.3":
version: 1.8.3
resolution: "turbo-windows-64@npm:1.8.3"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"turbo-windows-arm64@npm:1.8.0":
version: 1.8.0
resolution: "turbo-windows-arm64@npm:1.8.0"
"turbo-windows-arm64@npm:1.8.3":
version: 1.8.3
resolution: "turbo-windows-arm64@npm:1.8.3"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"turbo@npm:^1.7.4":
version: 1.8.0
resolution: "turbo@npm:1.8.0"
"turbo@npm:^1.8.3":
version: 1.8.3
resolution: "turbo@npm:1.8.3"
dependencies:
turbo-darwin-64: 1.8.0
turbo-darwin-arm64: 1.8.0
turbo-linux-64: 1.8.0
turbo-linux-arm64: 1.8.0
turbo-windows-64: 1.8.0
turbo-windows-arm64: 1.8.0
turbo-darwin-64: 1.8.3
turbo-darwin-arm64: 1.8.3
turbo-linux-64: 1.8.3
turbo-linux-arm64: 1.8.3
turbo-windows-64: 1.8.3
turbo-windows-arm64: 1.8.3
dependenciesMeta:
turbo-darwin-64:
optional: true
@@ -8430,7 +8433,7 @@ __metadata:
optional: true
bin:
turbo: bin/turbo
checksum: 7f97068d7f9a155e088d3575b1f9922e68fa3015aae0c92625238d44b4e6c275bec2a281907702dedb402fca29a6cd4690499e916cb334d7c24c98099bc3d8b0
checksum: 4a07d120ef8adf6c8e58a48abd02e075ffa215287cc6c3ef843d4fb08aeb0a566fe810ec9bfc376254468a2aa4f29bae154a60804a83af78dfa86d0e8e995476
languageName: node
linkType: hard