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 92b9ea013..1731b2f41 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 156f93dc6..88e93644f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev": "next dev", "build": "vitest run && next build", "analyze": "vitest run && ANALYZE=true next build", + "turbo" : "turbo run build", "start": "next start", "typecheck": "tsc --noEmit", "export": "vitest run && next build && next export", @@ -96,7 +97,7 @@ "jsdom": "^21.1.1", "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", "vitest": "^0.29.3", 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/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx b/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx index 794d992be..365431379 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(); @@ -29,6 +31,16 @@ export const ToggleEditModeAction = () => { useHotkeys([['ctrl+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(); if (enabled || config === undefined || config?.schemaVersion === undefined) { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 7e2c34429..19af3479e 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/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/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/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 9af9f32dd..21ca2518b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4936,7 +4936,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 @@ -7936,58 +7936,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 @@ -8003,7 +8003,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 7f97068d7f9a155e088d3575b1f9922e68fa3015aae0c92625238d44b4e6c275bec2a281907702dedb402fca29a6cd4690499e916cb334d7c24c98099bc3d8b0 + checksum: 4a07d120ef8adf6c8e58a48abd02e075ffa215287cc6c3ef843d4fb08aeb0a566fe810ec9bfc376254468a2aa4f29bae154a60804a83af78dfa86d0e8e995476 languageName: node linkType: hard