diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a136d41efa..41058f45ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,7 +52,8 @@ repos: (?x)^( docs/docs/stylesheets/.*| docs/docs/javascripts/.*| - docs/docs/webfonts/.* + docs/docs/webfonts/.* | + src/frontend/src/locales/.* | )$ - repo: https://github.com/pre-commit/mirrors-prettier rev: "v3.0.0-alpha.9-for-vscode" diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 662f68208a..95f04a0206 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -33,6 +33,7 @@ from report.api import report_api_urls from stock.api import stock_api_urls from stock.urls import stock_urls from users.api import user_urls +from web.urls import spa_view from web.urls import urlpatterns as platform_urls from .api import APISearchView, InfoView, NotFoundView @@ -210,7 +211,7 @@ classic_frontendpatterns = [ new_frontendpatterns = [ # Platform urls re_path(r'^platform/', include(platform_urls)), - + re_path(r'^platform', spa_view, name='platform'), ] # Load patterns for frontend according to settings diff --git a/InvenTree/web/urls.py b/InvenTree/web/urls.py index b025d1028a..7124e165cb 100644 --- a/InvenTree/web/urls.py +++ b/InvenTree/web/urls.py @@ -22,5 +22,6 @@ spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html urlpatterns = [ path("assets/", RedirectAssetView.as_view()), re_path(r"^(?P.*)/$", spa_view), + path("set-password?uid=&token=", spa_view, name="password_reset_confirm"), path("", spa_view), ] diff --git a/src/frontend/.linguirc b/src/frontend/.linguirc index 9235d3bb76..e1f99865c2 100644 --- a/src/frontend/.linguirc +++ b/src/frontend/.linguirc @@ -12,8 +12,5 @@ "fallbackLocales": { "default": "en", "pseudo-LOCALE": "en" - }, - "extractBabelOptions": { - "presets": ["@babel/preset-typescript"] } } diff --git a/src/frontend/index.html b/src/frontend/index.html index 350af83ef7..3e7fd76816 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -3,7 +3,6 @@ - InvenTree diff --git a/src/frontend/package.json b/src/frontend/package.json index 1c3e16bfae..f34c90ca37 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -11,10 +11,29 @@ "compile": "lingui compile --typescript" }, "dependencies": { + "@emotion/react": "^11.10.5", + "@fortawesome/fontawesome-svg-core": "^6.2.1", + "@fortawesome/free-regular-svg-icons": "^6.2.1", + "@fortawesome/free-solid-svg-icons": "^6.2.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@lingui/core": "^4.2.1", "@lingui/react": "^4.2.1", + "@mantine/core": "^6.0.15", + "@mantine/dates": "^6.0.15", + "@mantine/dropzone": "^6.0.15", + "@mantine/form": "^6.0.15", + "@mantine/hooks": "^6.0.15", + "@mantine/modals": "^6.0.15", + "@mantine/notifications": "^6.0.15", + "@tabler/icons-react": "^2.23.0", + "@tanstack/react-query": "^4.16.1", + "axios": "^1.1.3", + "dayjs": "^1.11.6", + "html5-qrcode": "^2.3.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.4.3", + "zustand": "^4.2.0" }, "devDependencies": { "@babel/core": "^7.20.5", diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index d757519679..2f5b895ef2 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1,10 +1,38 @@ +import { useViewportSize } from '@mantine/hooks'; +import { QueryClient } from '@tanstack/react-query'; +import axios from 'axios'; +import { lazy } from 'react'; + +import { Loadable } from './functions/loading'; +import { useLocalState } from './states/LocalState'; +import { useSessionState } from './states/SessionState'; + +// API +export const api = axios.create({}); +export function setApiDefaults() { + const host = useLocalState.getState().host; + const token = useSessionState.getState().token; + + api.defaults.baseURL = host; + api.defaults.headers.common['Authorization'] = `Token ${token}`; +} +export const queryClient = new QueryClient(); + +function checkMobile() { + const { height, width } = useViewportSize(); + if (width < 425 || height < 425) return true; + return false; +} + // Main App export default function App() { + // Check if mobile + if (checkMobile()) { + const MobileAppView = Loadable(lazy(() => import('./views/MobileAppView'))); + return ; + } + // Main App component - return ( - <> -

Welcome to the new frontend!

-

This is a placeholder site

- - ); + const DesktopAppView = Loadable(lazy(() => import('./views/DesktopAppView'))); + return ; } diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx new file mode 100644 index 0000000000..ed652fd5d1 --- /dev/null +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -0,0 +1,154 @@ +import { Trans, t } from '@lingui/macro'; +import { + Anchor, + Button, + Group, + Paper, + PasswordInput, + Stack, + Text, + TextInput +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useDisclosure } from '@mantine/hooks'; +import { notifications } from '@mantine/notifications'; +import { IconCheck } from '@tabler/icons-react'; +import { useNavigate } from 'react-router-dom'; + +import { doClassicLogin, doSimpleLogin } from '../../functions/auth'; +import { EditButton } from '../items/EditButton'; + +export function AuthenticationForm({ + hostname, + editing, + setEditing, + selectElement +}: { + hostname: string; + editing: boolean; + setEditing: (value?: React.SetStateAction | undefined) => void; + selectElement: JSX.Element; +}) { + const classicForm = useForm({ + initialValues: { username: '', password: '' } + }); + const simpleForm = useForm({ initialValues: { email: '' } }); + const [classicLoginMode, setMode] = useDisclosure(true); + const navigate = useNavigate(); + + function handleLogin() { + if (classicLoginMode === true) { + doClassicLogin( + classicForm.values.username, + classicForm.values.password + ).then((ret) => { + if (ret === false) { + notifications.show({ + title: t`Login failed`, + message: t`Check your your input and try again.`, + color: 'red' + }); + } else { + notifications.show({ + title: t`Login successfull`, + message: t`Welcome back!`, + color: 'green', + icon: + }); + navigate('/home'); + } + }); + } else { + doSimpleLogin(simpleForm.values.email).then((ret) => { + if (ret?.status === 'ok') { + notifications.show({ + title: t`Mail delivery successfull`, + message: t`Check your inbox for the login link. If you have an account, you will receive a login link. Check in spam too.`, + color: 'green', + icon: , + autoClose: false + }); + } else { + notifications.show({ + title: t`Input error`, + message: t`Check your your input and try again.`, + color: 'red' + }); + } + }); + } + } + + return ( + + + + {!editing ? hostname : selectElement} + + + +
{})}> + {classicLoginMode ? ( + + + + + navigate('/reset-password')} + > + Reset password + + + + ) : ( + + + + )} + + + setMode.toggle()} + > + {classicLoginMode ? ( + Send me an email + ) : ( + I will use username and password + )} + + + +
+
+ ); +} diff --git a/src/frontend/src/components/forms/HostOptionsForm.tsx b/src/frontend/src/components/forms/HostOptionsForm.tsx new file mode 100644 index 0000000000..c8ee15bc7a --- /dev/null +++ b/src/frontend/src/components/forms/HostOptionsForm.tsx @@ -0,0 +1,95 @@ +import { Trans, t } from '@lingui/macro'; +import { + ActionIcon, + Box, + Button, + Group, + Space, + Text, + TextInput +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { randomId } from '@mantine/hooks'; +import { IconSquarePlus, IconTrash } from '@tabler/icons-react'; + +import { HostList } from '../../states/states'; + +export function HostOptionsForm({ + data, + saveOptions +}: { + data: HostList; + saveOptions: (newData: HostList) => void; +}) { + const form = useForm({ initialValues: data }); + function deleteItem(key: string) { + const newData = form.values; + delete newData[key]; + form.setValues(newData); + } + + const fields = Object.entries(form.values).map(([key]) => ( + + {form.values[key] !== undefined && ( + <> + + + { + deleteItem(key); + }} + > + + + + )} + + )); + + return ( +
+ + {fields.length > 0 ? ( + + + Host + + + Name + + + ) : ( + + No one here... + + )} + {fields} + + + + + + +
+ ); +} diff --git a/src/frontend/src/components/items/ColorToggle.tsx b/src/frontend/src/components/items/ColorToggle.tsx new file mode 100644 index 0000000000..6d998bfd72 --- /dev/null +++ b/src/frontend/src/components/items/ColorToggle.tsx @@ -0,0 +1,27 @@ +import { ActionIcon, Group, useMantineColorScheme } from '@mantine/core'; +import { IconMoonStars, IconSun } from '@tabler/icons-react'; + +export function ColorToggle() { + const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + + return ( + + toggleColorScheme()} + size="lg" + sx={(theme) => ({ + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[6] + : theme.colors.gray[0], + color: + theme.colorScheme === 'dark' + ? theme.colors.yellow[4] + : theme.colors.blue[6] + })} + > + {colorScheme === 'dark' ? : } + + + ); +} diff --git a/src/frontend/src/components/items/DocTooltip.tsx b/src/frontend/src/components/items/DocTooltip.tsx new file mode 100644 index 0000000000..c09e370dc9 --- /dev/null +++ b/src/frontend/src/components/items/DocTooltip.tsx @@ -0,0 +1,95 @@ +import { Trans } from '@lingui/macro'; +import { Anchor, Container, HoverCard, ScrollArea, Text } from '@mantine/core'; +import { useEffect, useRef, useState } from 'react'; + +import { InvenTreeStyle } from '../../globalStyle'; + +export function DocTooltip({ + children, + text, + detail, + link, + docchildren +}: { + children: React.ReactNode; + text: string | JSX.Element; + detail?: string | JSX.Element; + link?: string; + docchildren?: React.ReactNode; +}) { + const { classes } = InvenTreeStyle(); + + return ( + + +
{children}
+
+ + + +
+ ); +} + +function ConstBody({ + text, + detail, + docchildren, + link +}: { + text: string | JSX.Element; + detail?: string | JSX.Element; + docchildren?: React.ReactNode; + link?: string; +}) { + const [height, setHeight] = useState(0); + const ref = useRef(null); + + // dynamically set height of scroll area based on content to remove unnecessary scroll bar + useEffect(() => { + if (ref.current == null) return; + + let height = ref.current['clientHeight']; + if (height > 250) { + setHeight(250); + } else { + setHeight(height + 1); + } + }); + + return ( + + {text} + {(detail || docchildren) && ( + +
+ {detail && ( + + {detail} + + )} + {docchildren} +
+
+ )} + {link && ( + + + Read More + + + )} +
+ ); +} diff --git a/src/frontend/src/components/items/DocumentationLinks.tsx b/src/frontend/src/components/items/DocumentationLinks.tsx new file mode 100644 index 0000000000..6d8b18a5b7 --- /dev/null +++ b/src/frontend/src/components/items/DocumentationLinks.tsx @@ -0,0 +1,41 @@ +import { Anchor, Group, SimpleGrid, Text } from '@mantine/core'; + +import { DocTooltip } from './DocTooltip'; +import { PlaceholderPill } from './Placeholder'; + +export interface DocumentationLinkItem { + id: string; + title: string | JSX.Element; + description: string | JSX.Element; + link: string; + placeholder?: boolean; +} + +export function DocumentationLinks({ + links +}: { + links: DocumentationLinkItem[]; +}) { + return ( + + {links.map((link) => ( + + + {link.placeholder ? ( + + + {link.title} + + + + ) : ( + + {link.title} + + )} + + + ))} + + ); +} diff --git a/src/frontend/src/components/items/EditButton.tsx b/src/frontend/src/components/items/EditButton.tsx new file mode 100644 index 0000000000..6d68cf62b4 --- /dev/null +++ b/src/frontend/src/components/items/EditButton.tsx @@ -0,0 +1,18 @@ +import { ActionIcon } from '@mantine/core'; +import { IconDeviceFloppy, IconEdit } from '@tabler/icons-react'; + +export function EditButton({ + setEditing, + editing, + disabled +}: { + setEditing: (value?: React.SetStateAction | undefined) => void; + editing: boolean; + disabled?: boolean; +}) { + return ( + setEditing()} disabled={disabled}> + {editing ? : } + + ); +} diff --git a/src/frontend/src/components/items/ErrorItem.tsx b/src/frontend/src/components/items/ErrorItem.tsx new file mode 100644 index 0000000000..fb82b1b342 --- /dev/null +++ b/src/frontend/src/components/items/ErrorItem.tsx @@ -0,0 +1,15 @@ +import { Trans } from '@lingui/macro'; + +export function ErrorItem({ id, error }: { id: string; error?: any }) { + const error_message = error?.message || error?.toString() || ( + Unknown error + ); + return ( + <> +

+ An error occurred: +

+ {error_message} + + ); +} diff --git a/src/frontend/src/components/items/InvenTreeLogo.tsx b/src/frontend/src/components/items/InvenTreeLogo.tsx new file mode 100644 index 0000000000..29862818a3 --- /dev/null +++ b/src/frontend/src/components/items/InvenTreeLogo.tsx @@ -0,0 +1,18 @@ +import { t } from '@lingui/macro'; +import { ActionIcon } from '@mantine/core'; +import { forwardRef } from 'react'; +import { NavLink } from 'react-router-dom'; + +import InvenTreeIcon from './inventree.svg'; + +export const InvenTreeLogo = forwardRef((props, ref) => { + return ( +
+ + + {t`InvenTree + + +
+ ); +}); diff --git a/src/frontend/src/components/items/MenuLinks.tsx b/src/frontend/src/components/items/MenuLinks.tsx new file mode 100644 index 0000000000..b6341f40ba --- /dev/null +++ b/src/frontend/src/components/items/MenuLinks.tsx @@ -0,0 +1,73 @@ +import { Anchor, SimpleGrid, Text, UnstyledButton } from '@mantine/core'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { InvenTreeStyle } from '../../globalStyle'; +import { DocTooltip } from './DocTooltip'; + +export interface MenuLinkItem { + id: string; + text: string | JSX.Element; + link: string; + highlight?: boolean; + doctext?: string | JSX.Element; + docdetail?: string | JSX.Element; + doclink?: string; + docchildren?: React.ReactNode; +} + +function ConditionalDocTooltip({ + item, + children +}: { + item: MenuLinkItem; + children: React.ReactNode; +}) { + if (item.doctext !== undefined) { + return ( + + {children} + + ); + } + return <>{children}; +} + +export function MenuLinks({ + links, + highlighted +}: { + links: MenuLinkItem[]; + highlighted?: boolean; +}) { + const { classes } = InvenTreeStyle(); + highlighted = highlighted || false; + + const filteredLinks = links.filter( + (item) => !highlighted || item.highlight === true + ); + return ( + + {filteredLinks.map((item) => ( + + + + {item.text} + + + + ))} + + ); +} diff --git a/src/frontend/src/components/items/Placeholder.tsx b/src/frontend/src/components/items/Placeholder.tsx new file mode 100644 index 0000000000..0a6d47cba4 --- /dev/null +++ b/src/frontend/src/components/items/Placeholder.tsx @@ -0,0 +1,17 @@ +import { Trans, t } from '@lingui/macro'; +import { Badge, Tooltip } from '@mantine/core'; + +export function PlaceholderPill() { + return ( + + + PLH + + + ); +} diff --git a/src/frontend/src/components/items/ScanButton.tsx b/src/frontend/src/components/items/ScanButton.tsx new file mode 100644 index 0000000000..65c30caef6 --- /dev/null +++ b/src/frontend/src/components/items/ScanButton.tsx @@ -0,0 +1,20 @@ +import { t } from '@lingui/macro'; +import { ActionIcon } from '@mantine/core'; +import { openContextModal } from '@mantine/modals'; +import { IconQrcode } from '@tabler/icons-react'; + +export function ScanButton() { + return ( + + openContextModal({ + modal: 'qr', + title: t`Scan QR code`, + innerProps: {} + }) + } + > + + + ); +} diff --git a/src/frontend/src/components/items/StylishText.tsx b/src/frontend/src/components/items/StylishText.tsx new file mode 100644 index 0000000000..cbd63f019c --- /dev/null +++ b/src/frontend/src/components/items/StylishText.tsx @@ -0,0 +1,12 @@ +import { Text } from '@mantine/core'; + +import { InvenTreeStyle } from '../../globalStyle'; + +export function StylishText({ children }: { children: JSX.Element | string }) { + const { classes } = InvenTreeStyle(); + return ( + + {children} + + ); +} diff --git a/src/frontend/src/components/items/inventree.svg b/src/frontend/src/components/items/inventree.svg new file mode 100644 index 0000000000..1404d2cdaa --- /dev/null +++ b/src/frontend/src/components/items/inventree.svg @@ -0,0 +1 @@ + diff --git a/src/frontend/src/components/modals/QrCodeModal.tsx b/src/frontend/src/components/modals/QrCodeModal.tsx new file mode 100644 index 0000000000..afc9eac3cc --- /dev/null +++ b/src/frontend/src/components/modals/QrCodeModal.tsx @@ -0,0 +1,201 @@ +import { Trans, t } from '@lingui/macro'; +import { + Badge, + Button, + Container, + Group, + ScrollArea, + Space, + Stack, + Text +} from '@mantine/core'; +import { + useDocumentVisibility, + useListState, + useLocalStorage +} from '@mantine/hooks'; +import { ContextModalProps } from '@mantine/modals'; +import { showNotification } from '@mantine/notifications'; +import { IconX } from '@tabler/icons-react'; +import { Html5Qrcode } from 'html5-qrcode'; +import { CameraDevice } from 'html5-qrcode/camera/core'; +import { Html5QrcodeResult } from 'html5-qrcode/core'; +import { useEffect, useState } from 'react'; + +import { api } from '../../App'; + +export function QrCodeModal({ + context, + id +}: ContextModalProps<{ modalBody: string }>) { + const [qrCodeScanner, setQrCodeScanner] = useState(null); + const [camId, setCamId] = useLocalStorage({ + key: 'camId', + defaultValue: null + }); + const [ScanningEnabled, setIsScanning] = useState(false); + const [wasAutoPaused, setWasAutoPaused] = useState(false); + const documentState = useDocumentVisibility(); + + const [values, handlers] = useListState([]); + + // Mount QR code once we are loaded + useEffect(() => { + setQrCodeScanner(new Html5Qrcode('reader')); + }, []); + + // Stop/star when leaving or reentering page + useEffect(() => { + if (ScanningEnabled && documentState === 'hidden') { + stopScanning(); + setWasAutoPaused(true); + } else if (wasAutoPaused && documentState === 'visible') { + startScanning(); + setWasAutoPaused(false); + } + }, [documentState]); + + // Scanner functions + function onScanSuccess( + decodedText: string, + decodedResult: Html5QrcodeResult + ) { + qrCodeScanner?.pause(); + + handlers.append(decodedText); + api.post('/barcode/', { barcode: decodedText }).then((response) => { + showNotification({ + title: response.data?.success || t`Unknown response`, + message: JSON.stringify(response.data), + color: response.data?.success ? 'teal' : 'red' + }); + if (response.data?.url) { + window.location.href = response.data.url; + } + }); + + qrCodeScanner?.resume(); + } + + function onScanFailure(error: string) { + if ( + error != + 'QR code parse error, error = NotFoundException: No MultiFormat Readers were able to detect the code.' + ) { + console.warn(`Code scan error = ${error}`); + } + } + + function selectCamera() { + Html5Qrcode.getCameras() + .then((devices) => { + if (devices?.length) { + setCamId(devices[0]); + } + }) + .catch((err) => { + showNotification({ + title: t`Error while getting camera`, + message: err, + color: 'red', + icon: + }); + }); + } + + function startScanning() { + if (camId && qrCodeScanner) { + qrCodeScanner + .start( + camId.id, + { fps: 10, qrbox: { width: 250, height: 250 } }, + (decodedText, decodedResult) => { + onScanSuccess(decodedText, decodedResult); + }, + (errorMessage) => { + onScanFailure(errorMessage); + } + ) + .catch((err: string) => { + showNotification({ + title: t`Error while scanning`, + message: err, + color: 'red', + icon: + }); + }); + setIsScanning(true); + } + } + + function stopScanning() { + if (qrCodeScanner && ScanningEnabled) { + qrCodeScanner.stop().catch((err: string) => { + showNotification({ + title: t`Error while stopping`, + message: err, + color: 'red', + icon: + }); + }); + setIsScanning(false); + } + } + + return ( + + + {camId?.label} + + {ScanningEnabled ? t`Scanning` : t`Not scanning`} + + + {!camId ? ( + + ) : ( + <> + + + + + {values.length == 0 ? ( + + No scans yet! + + ) : ( + + {values.map((value, index) => ( +
{value}
+ ))} +
+ )} + + )} + +
+ ); +} diff --git a/src/frontend/src/components/nav/Footer.tsx b/src/frontend/src/components/nav/Footer.tsx new file mode 100644 index 0000000000..06af2b2962 --- /dev/null +++ b/src/frontend/src/components/nav/Footer.tsx @@ -0,0 +1,29 @@ +import { Anchor, Container, Group } from '@mantine/core'; + +import { footerLinks } from '../../defaults/links'; +import { InvenTreeStyle } from '../../globalStyle'; +import { InvenTreeLogo } from '../items/InvenTreeLogo'; + +export function Footer() { + const { classes } = InvenTreeStyle(); + const items = footerLinks.map((link) => ( + + color="dimmed" + key={link.key} + href={link.link} + onClick={(event) => event.preventDefault()} + size="sm" + > + {link.label} + + )); + + return ( +
+ + + {items} + +
+ ); +} diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx new file mode 100644 index 0000000000..3fc014e9db --- /dev/null +++ b/src/frontend/src/components/nav/Header.tsx @@ -0,0 +1,64 @@ +import { Container, Group, Tabs } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { navTabs as mainNavTabs } from '../../defaults/links'; +import { InvenTreeStyle } from '../../globalStyle'; +import { ColorToggle } from '../items/ColorToggle'; +import { ScanButton } from '../items/ScanButton'; +import { MainMenu } from './MainMenu'; +import { NavHoverMenu } from './NavHoverMenu'; +import { NavigationDrawer } from './NavigationDrawer'; + +export function Header() { + const { classes } = InvenTreeStyle(); + const [drawerOpened, { open: openDrawer, close }] = useDisclosure(false); + + return ( +
+ + + + + + + + + + + + + + +
+ ); +} + +function NavTabs() { + const { classes } = InvenTreeStyle(); + const { tabValue } = useParams(); + const navigate = useNavigate(); + + return ( + + value == '/' ? navigate('/') : navigate(`/${value}`) + } + > + + {mainNavTabs.map((tab) => ( + + {tab.text} + + ))} + + + ); +} diff --git a/src/frontend/src/components/nav/Layout.tsx b/src/frontend/src/components/nav/Layout.tsx new file mode 100644 index 0000000000..98c78d8fce --- /dev/null +++ b/src/frontend/src/components/nav/Layout.tsx @@ -0,0 +1,34 @@ +import { Container, Flex, Space } from '@mantine/core'; +import { Navigate, Outlet } from 'react-router-dom'; + +import { InvenTreeStyle } from '../../globalStyle'; +import { useSessionState } from '../../states/SessionState'; +import { Footer } from './Footer'; +import { Header } from './Header'; + +export const ProtectedRoute = ({ children }: { children: JSX.Element }) => { + const [token] = useSessionState((state) => [state.token]); + + if (!token) { + return ; + } + + return children; +}; + +export default function LayoutComponent() { + const { classes } = InvenTreeStyle(); + + return ( + + +
+ + + + +