diff --git a/prisma/schema.prisma b/prisma/schema.prisma index de378532c..e270f9d71 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -74,7 +74,7 @@ model UserSettings { userId String colorScheme String @default("environment") // environment, light, dark language String @default("en") - defaultDashboard String @default("default") + defaultBoard String @default("default") searchTemplate String @default("https://google.com/search?q=%s") openSearchInNewTab Boolean @default(true) disablePingPulse Boolean @default(false) diff --git a/src/components/layout/Background.tsx b/src/components/layout/Background.tsx index f37e35429..0f714d1b3 100644 --- a/src/components/layout/Background.tsx +++ b/src/components/layout/Background.tsx @@ -5,12 +5,16 @@ import { useConfigContext } from '../../config/provider'; export function Background() { const { config } = useConfigContext(); + if (!config?.settings.customization.backgroundImageUrl) { + return null; + } + return ( ({})); diff --git a/src/components/layout/header/Meta/Head.tsx b/src/components/layout/Meta/Head.tsx similarity index 93% rename from src/components/layout/header/Meta/Head.tsx rename to src/components/layout/Meta/Head.tsx index 89a79cb70..9a1cda3e4 100644 --- a/src/components/layout/header/Meta/Head.tsx +++ b/src/components/layout/Meta/Head.tsx @@ -2,7 +2,7 @@ import NextHead from 'next/head'; import React from 'react'; -import { useConfigContext } from '../../../../config/provider'; +import { useConfigContext } from '../../../config/provider'; import { SafariStatusBarStyle } from './SafariStatusBarStyle'; export function Head() { diff --git a/src/components/layout/header/Meta/SafariStatusBarStyle.tsx b/src/components/layout/Meta/SafariStatusBarStyle.tsx similarity index 100% rename from src/components/layout/header/Meta/SafariStatusBarStyle.tsx rename to src/components/layout/Meta/SafariStatusBarStyle.tsx diff --git a/src/components/layout/main.tsx b/src/components/layout/main.tsx index 231d3d274..fae324091 100644 --- a/src/components/layout/main.tsx +++ b/src/components/layout/main.tsx @@ -1,13 +1,19 @@ -import { AppShell, useMantineTheme } from '@mantine/core'; +import { AppShell, clsx, useMantineTheme } from '@mantine/core'; +import { useConfigContext } from '~/config/provider'; +import { Background } from './Background'; +import { Head } from './Meta/Head'; import { MainHeader } from './new-header/Header'; type MainLayoutProps = { + headerActions?: React.ReactNode; children: React.ReactNode; }; -export const MainLayout = ({ children }: MainLayoutProps) => { +export const MainLayout = ({ headerActions, children }: MainLayoutProps) => { + const { config } = useConfigContext(); const theme = useMantineTheme(); + return ( { background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1], }, }} - header={} + header={} + className="dashboard-app-shell" > + + {children} + ); }; diff --git a/src/components/layout/new-header/AvatarMenu.tsx b/src/components/layout/new-header/AvatarMenu.tsx index 2f0d14b36..3b4257faf 100644 --- a/src/components/layout/new-header/AvatarMenu.tsx +++ b/src/components/layout/new-header/AvatarMenu.tsx @@ -9,25 +9,29 @@ import { IconSun, IconUserCog, } from '@tabler/icons-react'; +import { useQuery } from '@tanstack/react-query'; import { User } from 'next-auth'; import { signOut, useSession } from 'next-auth/react'; import Link from 'next/link'; import { forwardRef } from 'react'; import { AboutModal } from '~/components/Dashboard/Modals/AboutModal/AboutModal'; import { useColorScheme } from '~/hooks/use-colorscheme'; +import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore'; + +import { REPO_URL } from '../../../../data/constants'; export const AvatarMenu = () => { - const newVersionAvailable = '0.13.0'; const [aboutModalOpened, aboutModal] = useDisclosure(false); const { data: sessionData } = useSession(); const { colorScheme, toggleColorScheme } = useColorScheme(); + const newVersionAvailable = useNewVersionAvailable(); const Icon = colorScheme === 'dark' ? IconSun : IconMoonStars; return ( <> - + @@ -88,6 +92,7 @@ export const AvatarMenu = () => { type CurrentUserAvatarProps = { user: User | null; }; + const CurrentUserAvatar = forwardRef( ({ user, ...others }, ref) => { const { primaryColor } = useMantineTheme(); @@ -99,3 +104,15 @@ const CurrentUserAvatar = forwardRef( ); } ); + +const useNewVersionAvailable = () => { + const { attributes } = usePackageAttributesStore(); + const { data } = useQuery({ + queryKey: ['github/latest'], + cacheTime: 1000 * 60 * 60 * 24, + staleTime: 1000 * 60 * 60 * 5, + queryFn: () => + fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => res.json()), + }); + return data?.tag_name > `v${attributes.packageVersion}` ? data?.tag_name : undefined; +}; diff --git a/src/components/layout/new-header/Header.tsx b/src/components/layout/new-header/Header.tsx index eb0796ee2..af8259f67 100644 --- a/src/components/layout/new-header/Header.tsx +++ b/src/components/layout/new-header/Header.tsx @@ -1,24 +1,8 @@ -import { - Anchor, - Avatar, - Box, - Flex, - Group, - Header, - Menu, - Text, - TextInput, - UnstyledButton, -} from '@mantine/core'; -import { - IconAlertTriangle, - IconDashboard, - IconLogout, - IconSun, - IconUserSearch, -} from '@tabler/icons-react'; -import { signOut } from 'next-auth/react'; +import { Box, Flex, Group, Header, Text, UnstyledButton, useMantineTheme } from '@mantine/core'; +import { useMediaQuery } from '@mantine/hooks'; +import { IconAlertTriangle } from '@tabler/icons-react'; import Link from 'next/link'; +import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; import { Logo } from '../Logo'; import { AvatarMenu } from './AvatarMenu'; @@ -27,21 +11,33 @@ import { Search } from './search'; type MainHeaderProps = { logoHref?: string; showExperimental?: boolean; + headerActions?: React.ReactNode; }; -export const MainHeader = ({ showExperimental = false, logoHref = '/' }: MainHeaderProps) => { - const headerHeight = showExperimental ? 60 + 30 : 60; +export const MainHeader = ({ + showExperimental = false, + logoHref = '/', + headerActions, +}: MainHeaderProps) => { + const { breakpoints } = useMantineTheme(); + const isSmallerThanMd = useMediaQuery(`(max-width: ${breakpoints.sm})`); + const experimentalHeaderNoteHeight = isSmallerThanMd ? 50 : 30; + const headerHeight = showExperimental ? 60 + experimentalHeaderNoteHeight : 60; + return (
- + - + - + + + {headerActions} + @@ -50,16 +46,17 @@ export const MainHeader = ({ showExperimental = false, logoHref = '/' }: MainHea }; type ExperimentalHeaderNoteProps = { + height?: 30 | 50; visible?: boolean; }; -const ExperimentalHeaderNote = ({ visible = false }: ExperimentalHeaderNoteProps) => { +const ExperimentalHeaderNote = ({ visible = false, height = 30 }: ExperimentalHeaderNoteProps) => { if (!visible) return null; return ( - + - - + + This is an experimental feature of Homarr. Please report any issues to the official Homarr team. diff --git a/src/pages/board/index.tsx b/src/pages/board/index.tsx index 615f6dfed..50daefff9 100644 --- a/src/pages/board/index.tsx +++ b/src/pages/board/index.tsx @@ -1,18 +1,244 @@ -import { Title } from '@mantine/core'; -import { GetServerSideProps } from 'next'; +import { Button, ButtonProps, Text, Title, Tooltip } from '@mantine/core'; +import { useHotkeys, useWindowEvent } from '@mantine/hooks'; +import { openContextModal } from '@mantine/modals'; +import { hideNotification, showNotification } from '@mantine/notifications'; +import { IconApps, IconBrandDocker, IconEditCircle, IconEditCircleOff } from '@tabler/icons-react'; +import Consola from 'consola'; +import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import { useSession } from 'next-auth/react'; +import { SSRConfig, Trans, useTranslation } from 'next-i18next'; +import Link from 'next/link'; +import { Dashboard } from '~/components/Dashboard/Dashboard'; +import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore'; +import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/gridstack/store'; import { MainLayout } from '~/components/layout/main'; +import { useCardStyles } from '~/components/layout/useCardStyles'; +import { useInitConfig } from '~/config/init'; +import { useConfigContext } from '~/config/provider'; +import { getServerAuthSession } from '~/server/auth'; +import { prisma } from '~/server/db'; +import { getFrontendConfig } from '~/tools/config/getFrontendConfig'; +import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; +import { dashboardNamespaces } from '~/tools/server/translation-namespaces'; +import { ConfigType } from '~/types/config'; +import { api } from '~/utils/api'; + +export default function BoardPage({ + config: initialConfig, +}: InferGetServerSidePropsType) { + useInitConfig(initialConfig); -export default function BoardPage() { return ( - - BoardPage + }> + ); } -export const getServerSideProps: GetServerSideProps = async () => { - console.log('getServerSideProps'); +type BoardGetServerSideProps = { + config: ConfigType; + _nextI18Next?: SSRConfig['_nextI18Next']; +}; + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const session = await getServerAuthSession(ctx); + const currentUserSettings = await prisma.userSettings.findFirst({ + where: { + userId: session?.user?.id, + }, + }); + + const translations = await getServerSideTranslations( + dashboardNamespaces, + ctx.locale, + ctx.req, + ctx.res + ); + const boardName = currentUserSettings?.defaultBoard ?? 'default'; + const config = await getFrontendConfig(boardName as string); + return { - props: {}, + props: { + config, + ...translations, + }, }; }; + +const HeaderActions = () => { + const { data: sessionData } = useSession(); + + if (!sessionData?.user?.isAdmin) return null; + + return ( + <> + + + + ); +}; + +const DockerButton = () => { + const { t } = useTranslation('modules/docker'); + + return ( + + + + + + ); +}; + +type SpecificLinkProps = { + component: typeof Link; + href: string; +}; +type SpecificButtonProps = { + onClick: HTMLButtonElement['onclick']; +}; +type HeaderActionButtonProps = Omit & + (SpecificLinkProps | SpecificButtonProps); + +const HeaderActionButton = ({ children, ...props }: HeaderActionButtonProps) => { + const { classes } = useCardStyles(true); + + const buttonProps: ButtonProps = { + variant: 'default', + className: classes.card, + h: 38, + w: 38, + px: 0, + ...props, + }; + + if ('component' in props) { + return ( + + ); + } + + return ; +}; + +const beforeUnloadEventText = 'Exit the edit mode to save your changes'; +const editModeNotificationId = 'toggle-edit-mode'; + +const ToggleEditModeButton = () => { + const { enabled, toggleEditMode } = useEditModeStore(); + const { classes } = useCardStyles(true); + const { config, name: configName } = useConfigContext(); + const { mutateAsync: saveConfig } = api.config.save.useMutation(); + const namedWrapperColumnCount = useNamedWrapperColumnCount(); + const { t } = useTranslation(['layout/header/actions/toggle-edit-mode', 'common']); + const translatedSize = + namedWrapperColumnCount !== null + ? t(`common:breakPoints.${namedWrapperColumnCount}`) + : t('common:loading'); + + 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 save = async () => { + toggleEditMode(); + if (!config || !configName) return; + await saveConfig({ name: configName, config }); + Consola.log('Saved config to server', configName); + hideNotification(editModeNotificationId); + }; + + const enableEditMode = () => { + toggleEditMode(); + showNotification({ + styles: (theme) => ({ + root: { + backgroundColor: theme.colors.orange[7], + borderColor: theme.colors.orange[7], + + '&::before': { backgroundColor: theme.white }, + }, + title: { color: theme.white }, + description: { color: theme.white }, + closeButton: { + color: theme.white, + '&:hover': { backgroundColor: theme.colors.orange[7] }, + }, + }), + radius: 'md', + id: 'toggle-edit-mode', + autoClose: 10000, + title: ( + + <Trans + i18nKey="layout/header/actions/toggle-edit-mode:popover.title" + values={{ size: translatedSize }} + components={{ + 1: ( + <Text + component="a" + style={{ color: 'inherit', textDecoration: 'underline' }} + href="https://homarr.dev/docs/customizations/layout" + target="_blank" + /> + ), + }} + /> + + ), + message: , + }); + }; + + if (enabled) { + return ( + + + + + + + + + ); + } + return ( + + + + + + ); +}; + +const AddElementButton = () => { + const { t } = useTranslation('layout/element-selector/selector'); + const { classes } = useCardStyles(true); + + return ( + + + openContextModal({ + modal: 'selectElement', + title: t('modal.title'), + size: 'xl', + innerProps: {}, + }) + } + > + + + + ); +};