diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 55879bb7a..2e8c262ca 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -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 diff --git a/.github/workflows/docker_dev.yml b/.github/workflows/docker_dev.yml index db5961ed4..d65cbea87 100644 --- a/.github/workflows/docker_dev.yml +++ b/.github/workflows/docker_dev.yml @@ -24,11 +24,14 @@ env: REGISTRY: ghcr.io # github.repository as / 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' diff --git a/.gitignore b/.gitignore index 1b7d1e587..66a0673f5 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # vercel .vercel +.turbo *.tsbuildinfo # storybook diff --git a/package.json b/package.json index c398dc5cc..6d904f4b1 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/public/locales/en/layout/modals/about.json b/public/locales/en/layout/modals/about.json index aa99318db..83109b624 100644 --- a/public/locales/en/layout/modals/about.json +++ b/public/locales/en/layout/modals/about.json @@ -2,6 +2,10 @@ "description": "Homarr is a sleek, modern 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", diff --git a/public/locales/en/widgets/error-boundary.json b/public/locales/en/widgets/error-boundary.json new file mode 100644 index 000000000..9b75f4080 --- /dev/null +++ b/public/locales/en/widgets/error-boundary.json @@ -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" + } +} \ No newline at end of file diff --git a/src/components/Dashboard/Modals/AboutModal/AboutModal.tsx b/src/components/Dashboard/Modals/AboutModal/AboutModal.tsx index 4346f0124..cd175fa99 100644 --- a/src/components/Dashboard/Modals/AboutModal/AboutModal.tsx +++ b/src/components/Dashboard/Modals/AboutModal/AboutModal.tsx @@ -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) => ( + + + {element.key} + + + {element.shortcut} + + + )); + return ( closeModal()} @@ -77,7 +96,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod - +
{informations.map((item, index) => ( @@ -101,8 +120,26 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod ))}
+ + + }> + {t('layout/modals/about:keybinds')} + + + + + + + + + + {rows} +
{t('layout/modals/about:key')}{t('layout/modals/about:action')}
+ {t('layout/modals/about:tip')} +
+
+
- {t('layout/modals/about:contact')} diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector.tsx index 0d4fd4249..c9c01035d 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector.tsx @@ -90,7 +90,8 @@ export const IconSelector = ({ } variant="default" withAsterisk - dropdownComponent={(props: any) => } + dropdownComponent={(props: any) => } + dropdownPosition="bottom" required onChange={(event) => { if (allowAppNamePropagation) { diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx index 895dbf015..b65b0f5b7 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/GenericSecretInput.tsx @@ -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(false); + const [displayUpdateField, setDisplayUpdateField] = useState(!secretIsPresent); const { t } = useTranslation(['layout/modals/add-app', 'common']); return ( @@ -51,26 +50,26 @@ export const GenericSecretInput = ({ - + {t(label)} - {secretIsPresent ? ( - - {t('integration.type.defined')} - - ) : ( - - {t('integration.type.undefined')} - - )} + + {secretIsPresent + ? t('integration.type.defined') + : t('integration.type.undefined')} + {type === 'private' ? ( - + {type === 'private' ? 'Private: Once saved, you cannot read out this value again' : 'Public: Can be read out repeatedly'} - + - {displayUpdateField === true ? ( { const { t } = useTranslation('layout/modals/add-app'); + const acceptableStatusCodes = (form.values.network.statusCodes ?? ['200']).map((x) => + x.toString() + ); return ( { data={StatusCodes} clearable searchable - defaultValue={form.values.network.okStatus.map((x) => `${x}`)} + defaultValue={acceptableStatusCodes} variant="default" {...form.getInputProps('network.statusCodes')} /> diff --git a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx index d746ae334..0ec7e091d 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx @@ -95,7 +95,7 @@ export const AvailableElementTypes = ({ }, network: { enabledStatusChecker: true, - okStatus: [200], + statusCodes: ['200'], }, behaviour: { isOpeningNewTab: true, diff --git a/src/components/Dashboard/Tiles/Apps/AppPing.tsx b/src/components/Dashboard/Tiles/Apps/AppPing.tsx index d94cdc676..0a14bb9f2 100644 --- a/src/components/Dashboard/Tiles/Apps/AppPing.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppPing.tsx @@ -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) => { ); }; - -type PingState = 'loading' | 'down' | 'online'; diff --git a/src/components/Dashboard/Tiles/Apps/AppTile.tsx b/src/components/Dashboard/Tiles/Apps/AppTile.tsx index 1c9edd328..42dacd806 100644 --- a/src/components/Dashboard/Tiles/Apps/AppTile.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppTile.tsx @@ -36,7 +36,13 @@ export const AppTile = ({ className, app }: AppTileProps) => { className="dashboard-tile-app" > diff --git a/src/components/Dashboard/Tiles/GenericTileMenu.tsx b/src/components/Dashboard/Tiles/GenericTileMenu.tsx index fba87edfa..d414d0613 100644 --- a/src/components/Dashboard/Tiles/GenericTileMenu.tsx +++ b/src/components/Dashboard/Tiles/GenericTileMenu.tsx @@ -24,9 +24,9 @@ export const GenericTileMenu = ({ } return ( - + - + diff --git a/src/components/Settings/Customization/CustomizationSettings.tsx b/src/components/Settings/Customization/CustomizationSettings.tsx index f7dbc327d..30bbe4149 100644 --- a/src/components/Settings/Customization/CustomizationSettings.tsx +++ b/src/components/Settings/Customization/CustomizationSettings.tsx @@ -10,9 +10,7 @@ export default function CustomizationSettings() { return ( - - {t('text')} - + {t('text')} diff --git a/src/components/Settings/Customization/Meta/LogoImageChanger.tsx b/src/components/Settings/Customization/Meta/LogoImageChanger.tsx index 3d5563fe1..18fc8246a 100644 --- a/src/components/Settings/Customization/Meta/LogoImageChanger.tsx +++ b/src/components/Settings/Customization/Meta/LogoImageChanger.tsx @@ -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; diff --git a/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx b/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx index 794d992be..c1f4445ff 100644 --- a/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx +++ b/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx @@ -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(); diff --git a/src/hooks/icons/useGetDashboardIcons.tsx b/src/hooks/icons/useGetDashboardIcons.tsx index 854322693..707461eec 100644 --- a/src/hooks/icons/useGetDashboardIcons.tsx +++ b/src/hooks/icons/useGetDashboardIcons.tsx @@ -10,5 +10,7 @@ export const useGetDashboardIcons = () => return data as NormalizedIconRepositoryResult[]; }, refetchOnMount: false, + // Cache for infinity, refetch every so often. + cacheTime: Infinity, refetchOnWindowFocus: false, }); diff --git a/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx b/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx index 965607e1c..2af50a67e 100644 --- a/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx +++ b/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx @@ -1,11 +1,12 @@ import { useQuery } from '@tanstack/react-query'; import { NormalizedDownloadQueueResponse } from '../../../types/api/downloads/queue/NormalizedDownloadQueueResponse'; -export const useGetDownloadClientsQueue = () => useQuery({ - queryKey: ['network-speed'], - queryFn: async (): Promise => { - const response = await fetch('/api/modules/downloads'); - return response.json(); - }, - refetchInterval: 3000, -}); +export const useGetDownloadClientsQueue = () => + useQuery({ + queryKey: ['network-speed'], + queryFn: async (): Promise => { + const response = await fetch('/api/modules/downloads'); + return response.json(); + }, + refetchInterval: 3000, + }); diff --git a/src/middleware.ts b/src/middleware.ts index 79f85b693..6652e8d41 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -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(); diff --git a/src/modules/Docker/ContainerActionBar.tsx b/src/modules/Docker/ContainerActionBar.tsx index 3b8943908..427b877ea 100644 --- a/src/modules/Docker/ContainerActionBar.tsx +++ b/src/modules/Docker/ContainerActionBar.tsx @@ -177,7 +177,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction }, network: { enabledStatusChecker: true, - okStatus: [200], + statusCodes: ['200'], }, behaviour: { isOpeningNewTab: true, diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index e55dd6bef..b8178c006 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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({ 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, }; }; diff --git a/src/pages/api/icons/index.ts b/src/pages/api/icons/index.ts index 7b1c6a4f9..9649f9cbe 100644 --- a/src/pages/api/icons/index.ts +++ b/src/pages/api/icons/index.ts @@ -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); diff --git a/src/pages/api/modules/calendar.ts b/src/pages/api/modules/calendar.ts index ab3694eda..275555656 100644 --- a/src/pages/api/modules/calendar.ts +++ b/src/pages/api/modules/calendar.ts @@ -53,7 +53,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { ); const IntegrationTypeEndpointMap = new Map([ - ['sonarr', useSonarrv4 ? '/api/v3/calendar' : '/api/calendar'], + ['sonarr', useSonarrv4 ? '/api/v3/calendar' : '/api/calendar'], ['radarr', '/api/v3/calendar'], ['lidarr', '/api/v1/calendar'], ['readarr', '/api/v1/calendar'], diff --git a/src/pages/api/modules/downloads/index.ts b/src/pages/api/modules/downloads/index.ts index d7d5dc086..6d80db458 100644 --- a/src/pages/api/modules/downloads/index.ts +++ b/src/pages/api/modules/downloads/index.ts @@ -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); diff --git a/src/pages/api/modules/rss/index.ts b/src/pages/api/modules/rss/index.ts index 0a5623158..0ee993be3 100644 --- a/src/pages/api/modules/rss/index.ts +++ b/src/pages/api/modules/rss/index.ts @@ -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; diff --git a/src/tools/config/migrateConfig.ts b/src/tools/config/migrateConfig.ts index 7e8586524..f83734a57 100644 --- a/src/tools/config/migrateConfig.ts +++ b/src/tools/config/migrateConfig.ts @@ -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), diff --git a/src/tools/server/images/jsdelivr-icons-repository.ts b/src/tools/server/images/jsdelivr-icons-repository.ts index 734f49853..acce25790 100644 --- a/src/tools/server/images/jsdelivr-icons-repository.ts +++ b/src/tools/server/images/jsdelivr-icons-repository.ts @@ -23,7 +23,7 @@ export class JsdelivrIconsRepository extends AbstractIconRepository { constructor( private readonly repository: JsdelivrRepositoryUrl, private readonly displayName: string, - copyright: string, + copyright: string ) { super(copyright); } diff --git a/src/tools/server/translation-namespaces.ts b/src/tools/server/translation-namespaces.ts index 5fe821c3f..90aedada9 100644 --- a/src/tools/server/translation-namespaces.ts +++ b/src/tools/server/translation-namespaces.ts @@ -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']; diff --git a/src/types/api/media-server/session-info.ts b/src/types/api/media-server/session-info.ts index 5b8c25ab7..d9ee0cece 100644 --- a/src/types/api/media-server/session-info.ts +++ b/src/types/api/media-server/session-info.ts @@ -14,33 +14,39 @@ export type GenericCurrentlyPlaying = { episodeCount: number | undefined; type: 'audio' | 'video' | 'tv' | 'movie' | undefined; metadata: { - video: { - videoCodec: string | undefined; - videoFrameRate: string | undefined; - height: number | undefined; - width: number | undefined; - bitrate: number | undefined; - } | undefined; - audio: { - audioCodec: string | undefined; - audioChannels: number | undefined; - } | undefined; - transcoding: { - context: string | undefined; - sourceVideoCodec: string | undefined; - sourceAudioCodec: string | undefined; - videoDecision: string | undefined; - audioDecision: string | undefined; - container: string | undefined; - videoCodec: string | undefined; - audioCodec: string | undefined; - error: boolean | undefined; - duration: number | undefined; - audioChannels: number | undefined; - width: number | undefined; - height: number | undefined; - transcodeHwRequested: boolean | undefined; - timeStamp: number | undefined; - } | undefined; + video: + | { + videoCodec: string | undefined; + videoFrameRate: string | undefined; + height: number | undefined; + width: number | undefined; + bitrate: number | undefined; + } + | undefined; + audio: + | { + audioCodec: string | undefined; + audioChannels: number | undefined; + } + | undefined; + transcoding: + | { + context: string | undefined; + sourceVideoCodec: string | undefined; + sourceAudioCodec: string | undefined; + videoDecision: string | undefined; + audioDecision: string | undefined; + container: string | undefined; + videoCodec: string | undefined; + audioCodec: string | undefined; + error: boolean | undefined; + duration: number | undefined; + audioChannels: number | undefined; + width: number | undefined; + height: number | undefined; + transcodeHwRequested: boolean | undefined; + timeStamp: number | undefined; + } + | undefined; }; }; diff --git a/src/types/app.ts b/src/types/app.ts index 2e611c031..ca933d2d4 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -23,7 +23,7 @@ interface AppBehaviourType { interface AppNetworkType { enabledStatusChecker: boolean; - okStatus: number[]; + statusCodes: string[]; } interface AppAppearanceType { diff --git a/src/widgets/WidgetWrapper.tsx b/src/widgets/WidgetWrapper.tsx index bb7323073..457890537 100644 --- a/src/widgets/WidgetWrapper.tsx +++ b/src/widgets/WidgetWrapper.tsx @@ -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 ( - - - - + + + + + + ); }; diff --git a/src/widgets/boundary.tsx b/src/widgets/boundary.tsx new file mode 100644 index 000000000..21530f9f3 --- /dev/null +++ b/src/widgets/boundary.tsx @@ -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 { + 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 ( + ({ + backgroundColor: theme.colors.red[5], + })} + radius="lg" + shadow="sm" + withBorder + > +
+ + + + + {this.props.t('card.title')} + + {this.state.error && ( + + {this.state.error.toString()} + + )} + + + + + ), + }) + } + leftIcon={} + variant="light" + > + {this.props.t('card.buttons.details')} + + + + +
+
+ ); + } + + // Return children components in case of no error + return this.props.children; + } +} + +export default withTranslation('widgets/error-boundary')(ErrorBoundary); diff --git a/src/widgets/date/DateTile.tsx b/src/widgets/date/DateTile.tsx index b9f005873..07c26cb5c 100644 --- a/src/widgets/date/DateTile.tsx +++ b/src/widgets/date/DateTile.tsx @@ -25,7 +25,7 @@ const definition = defineWidget({ component: DateTile, }); -export type IDateWidget = IWidget; +export type IDateWidget = IWidget<(typeof definition)['id'], typeof definition>; interface DateTileProps { widget: IDateWidget; diff --git a/src/widgets/iframe/IFrameTile.tsx b/src/widgets/iframe/IFrameTile.tsx index 44b17d089..8cc2efe1f 100644 --- a/src/widgets/iframe/IFrameTile.tsx +++ b/src/widgets/iframe/IFrameTile.tsx @@ -43,7 +43,7 @@ function IFrameTile({ widget }: IFrameTileProps) { - {t('card.errors.noUrl.title')} + {t('card.errors.noUrl.title')} {t('card.errors.noUrl.text')} diff --git a/src/widgets/media-server/DetailCollapseable.tsx b/src/widgets/media-server/DetailCollapseable.tsx index 536e3d0e7..2517962bf 100644 --- a/src/widgets/media-server/DetailCollapseable.tsx +++ b/src/widgets/media-server/DetailCollapseable.tsx @@ -107,7 +107,9 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo }) {session.sessionName} - {details.length > 0 && } + {details.length > 0 && ( + + )} {details.map((detail, index) => ( diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index 39b03db68..6c6e53b4e 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -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 ( - + {data.feed.image ? ( @@ -121,7 +119,7 @@ function RssTile({ widget }: RssTileProps) { - - - - + {item.enclosure && ( + + + + )} + {item.categories && ( {item.categories.map((category: any, categoryIndex: number) => ( @@ -181,12 +181,14 @@ function RssTile({ widget }: RssTileProps) { {data.feed.pubDate} - - - - {data.feed.lastBuildDate} - - + {data.feed.lastBuildDate && ( + + + + {data.feed.lastBuildDate} + + + )} {data.feed.feedUrl && ( diff --git a/src/widgets/useNet/UseNetTile.tsx b/src/widgets/useNet/UseNetTile.tsx index 832b8d2d7..bfe1345cb 100644 --- a/src/widgets/useNet/UseNetTile.tsx +++ b/src/widgets/useNet/UseNetTile.tsx @@ -46,7 +46,7 @@ const definition = defineWidget({ }, }); -export type IUsenetWidget = IWidget; +export type IUsenetWidget = IWidget<(typeof definition)['id'], typeof definition>; interface UseNetTileProps { widget: IUsenetWidget; diff --git a/src/widgets/weather/WeatherTile.tsx b/src/widgets/weather/WeatherTile.tsx index ba7b9eff7..b23731aa1 100644 --- a/src/widgets/weather/WeatherTile.tsx +++ b/src/widgets/weather/WeatherTile.tsx @@ -28,7 +28,7 @@ const definition = defineWidget({ component: WeatherTile, }); -export type IWeatherWidget = IWidget; +export type IWeatherWidget = IWidget<(typeof definition)['id'], typeof definition>; interface WeatherTileProps { widget: IWeatherWidget; diff --git a/turbo.json b/turbo.json new file mode 100644 index 000000000..833e96950 --- /dev/null +++ b/turbo.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "outputs": [ + ".next/**", + "!.next/cache/**" + ] + } + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2d3338ba5..1b4d1f91d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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