Add base authenticated route
This commit is contained in:
@@ -11,8 +11,8 @@ import { LoginRoute } from "./routes/auth/LoginRoute";
|
|||||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
||||||
import { HomeRoute } from "./routes/HomeRoute";
|
import { HomeRoute } from "./routes/HomeRoute";
|
||||||
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
|
||||||
import { BaseLoginPage } from "./widgets/auth/BaseLoginPage";
|
import { BaseLoginPage } from "./widgets/auth/BaseLoginPage";
|
||||||
|
import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage";
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export function BaseAuthenticatedPage(): React.ReactElement {
|
|
||||||
return <p>todo authenticated</p>;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { mdiMessageTextFast } from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
|
import DashboardHeader from "./DashboardHeader";
|
||||||
|
import DashboardSidebar from "./DashboardSidebar";
|
||||||
|
|
||||||
|
export default function BaseAuthenticatedPage(): React.ReactElement {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] =
|
||||||
|
React.useState(true);
|
||||||
|
const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
|
const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
|
||||||
|
const isNavigationExpanded = isOverMdViewport
|
||||||
|
? isDesktopNavigationExpanded
|
||||||
|
: isMobileNavigationExpanded;
|
||||||
|
|
||||||
|
const setIsNavigationExpanded = React.useCallback(
|
||||||
|
(newExpanded: boolean) => {
|
||||||
|
if (isOverMdViewport) {
|
||||||
|
setIsDesktopNavigationExpanded(newExpanded);
|
||||||
|
} else {
|
||||||
|
setIsMobileNavigationExpanded(newExpanded);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isOverMdViewport,
|
||||||
|
setIsDesktopNavigationExpanded,
|
||||||
|
setIsMobileNavigationExpanded,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleHeaderMenu = React.useCallback(
|
||||||
|
(isExpanded: boolean) => {
|
||||||
|
setIsNavigationExpanded(isExpanded);
|
||||||
|
},
|
||||||
|
[setIsNavigationExpanded]
|
||||||
|
);
|
||||||
|
|
||||||
|
const layoutRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={layoutRef}
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
overflow: "hidden",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DashboardHeader
|
||||||
|
logo={<Icon path={mdiMessageTextFast} size="2em" color={"white"} />}
|
||||||
|
title=""
|
||||||
|
menuOpen={isNavigationExpanded}
|
||||||
|
onToggleMenu={handleToggleHeaderMenu}
|
||||||
|
/>
|
||||||
|
<DashboardSidebar
|
||||||
|
expanded={isNavigationExpanded}
|
||||||
|
setExpanded={setIsNavigationExpanded}
|
||||||
|
container={layoutRef?.current ?? undefined}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar sx={{ displayPrint: "none" }} />
|
||||||
|
<Box
|
||||||
|
component="main"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
overflow: "auto",
|
||||||
|
padding: "50px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Outlet />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx
Normal file
130
matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { styled, useTheme } from "@mui/material/styles";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import MuiAppBar from "@mui/material/AppBar";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
|
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import ThemeSwitcher from "./ThemeSwitcher";
|
||||||
|
|
||||||
|
const AppBar = styled(MuiAppBar)(({ theme }) => ({
|
||||||
|
borderWidth: 0,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: (theme.vars ?? theme).palette.divider,
|
||||||
|
boxShadow: "none",
|
||||||
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const LogoContainer = styled("div")({
|
||||||
|
position: "relative",
|
||||||
|
height: 40,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
"& img": {
|
||||||
|
maxHeight: 40,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface DashboardHeaderProps {
|
||||||
|
logo?: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
menuOpen: boolean;
|
||||||
|
onToggleMenu: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardHeader({
|
||||||
|
logo,
|
||||||
|
title,
|
||||||
|
menuOpen,
|
||||||
|
onToggleMenu,
|
||||||
|
}: DashboardHeaderProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const handleMenuOpen = React.useCallback(() => {
|
||||||
|
onToggleMenu(!menuOpen);
|
||||||
|
}, [menuOpen, onToggleMenu]);
|
||||||
|
|
||||||
|
const getMenuIcon = React.useCallback(
|
||||||
|
(isExpanded: boolean) => {
|
||||||
|
const expandMenuActionText = "Expand";
|
||||||
|
const collapseMenuActionText = "Collapse";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={`${
|
||||||
|
isExpanded ? collapseMenuActionText : expandMenuActionText
|
||||||
|
} menu`}
|
||||||
|
enterDelay={1000}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label={`${
|
||||||
|
isExpanded ? collapseMenuActionText : expandMenuActionText
|
||||||
|
} navigation menu`}
|
||||||
|
onClick={handleMenuOpen}
|
||||||
|
>
|
||||||
|
{isExpanded ? <MenuOpenIcon /> : <MenuIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[handleMenuOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBar color="inherit" position="absolute" sx={{ displayPrint: "none" }}>
|
||||||
|
<Toolbar sx={{ backgroundColor: "inherit", mx: { xs: -0.75, sm: -1 } }}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
sx={{
|
||||||
|
flexWrap: "wrap",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
<Box sx={{ mr: 1 }}>{getMenuIcon(menuOpen)}</Box>
|
||||||
|
<Link to="/" style={{ textDecoration: "none" }}>
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
{logo ? <LogoContainer>{logo}</LogoContainer> : null}
|
||||||
|
{title ? (
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: (theme.vars ?? theme).palette.primary.main,
|
||||||
|
fontWeight: "700",
|
||||||
|
ml: 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={1}
|
||||||
|
sx={{ marginLeft: "auto" }}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
281
matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx
Normal file
281
matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Drawer from "@mui/material/Drawer";
|
||||||
|
import List from "@mui/material/List";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
import PersonIcon from "@mui/icons-material/Person";
|
||||||
|
import BarChartIcon from "@mui/icons-material/BarChart";
|
||||||
|
import DescriptionIcon from "@mui/icons-material/Description";
|
||||||
|
import LayersIcon from "@mui/icons-material/Layers";
|
||||||
|
import { matchPath, useLocation } from "react-router";
|
||||||
|
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||||
|
import { DRAWER_WIDTH, MINI_DRAWER_WIDTH } from "./constants";
|
||||||
|
import DashboardSidebarPageItem from "./DashboardSidebarPageItem";
|
||||||
|
import DashboardSidebarHeaderItem from "./DashboardSidebarHeaderItem";
|
||||||
|
import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem";
|
||||||
|
import {
|
||||||
|
getDrawerSxTransitionMixin,
|
||||||
|
getDrawerWidthTransitionMixin,
|
||||||
|
} from "./mixins";
|
||||||
|
|
||||||
|
export interface DashboardSidebarProps {
|
||||||
|
expanded?: boolean;
|
||||||
|
setExpanded: (expanded: boolean) => void;
|
||||||
|
disableCollapsibleSidebar?: boolean;
|
||||||
|
container?: Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardSidebar({
|
||||||
|
expanded = true,
|
||||||
|
setExpanded,
|
||||||
|
disableCollapsibleSidebar = false,
|
||||||
|
container,
|
||||||
|
}: DashboardSidebarProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const [expandedItemIds, setExpandedItemIds] = React.useState<string[]>([]);
|
||||||
|
|
||||||
|
const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm"));
|
||||||
|
const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
|
||||||
|
const [isFullyExpanded, setIsFullyExpanded] = React.useState(expanded);
|
||||||
|
const [isFullyCollapsed, setIsFullyCollapsed] = React.useState(!expanded);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (expanded) {
|
||||||
|
const drawerWidthTransitionTimeout = setTimeout(() => {
|
||||||
|
setIsFullyExpanded(true);
|
||||||
|
}, theme.transitions.duration.enteringScreen);
|
||||||
|
|
||||||
|
return () => clearTimeout(drawerWidthTransitionTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFullyExpanded(false);
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}, [expanded, theme.transitions.duration.enteringScreen]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!expanded) {
|
||||||
|
const drawerWidthTransitionTimeout = setTimeout(() => {
|
||||||
|
setIsFullyCollapsed(true);
|
||||||
|
}, theme.transitions.duration.leavingScreen);
|
||||||
|
|
||||||
|
return () => clearTimeout(drawerWidthTransitionTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFullyCollapsed(false);
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}, [expanded, theme.transitions.duration.leavingScreen]);
|
||||||
|
|
||||||
|
const mini = !disableCollapsibleSidebar && !expanded;
|
||||||
|
|
||||||
|
const handleSetSidebarExpanded = React.useCallback(
|
||||||
|
(newExpanded: boolean) => () => {
|
||||||
|
setExpanded(newExpanded);
|
||||||
|
},
|
||||||
|
[setExpanded]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePageItemClick = React.useCallback(
|
||||||
|
(itemId: string, hasNestedNavigation: boolean) => {
|
||||||
|
if (hasNestedNavigation && !mini) {
|
||||||
|
setExpandedItemIds((previousValue) =>
|
||||||
|
previousValue.includes(itemId)
|
||||||
|
? previousValue.filter(
|
||||||
|
(previousValueItemId) => previousValueItemId !== itemId
|
||||||
|
)
|
||||||
|
: [...previousValue, itemId]
|
||||||
|
);
|
||||||
|
} else if (!isOverSmViewport && !hasNestedNavigation) {
|
||||||
|
setExpanded(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mini, setExpanded, isOverSmViewport]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasDrawerTransitions =
|
||||||
|
isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport);
|
||||||
|
|
||||||
|
const getDrawerContent = React.useCallback(
|
||||||
|
(viewport: "phone" | "tablet" | "desktop") => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Toolbar />
|
||||||
|
<Box
|
||||||
|
component="nav"
|
||||||
|
aria-label={`${viewport.charAt(0).toUpperCase()}${viewport.slice(1)}`}
|
||||||
|
sx={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
overflow: "auto",
|
||||||
|
scrollbarGutter: mini ? "stable" : "auto",
|
||||||
|
overflowX: "hidden",
|
||||||
|
pt: !mini ? 0 : 2,
|
||||||
|
...(hasDrawerTransitions
|
||||||
|
? getDrawerSxTransitionMixin(isFullyExpanded, "padding")
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
dense
|
||||||
|
sx={{
|
||||||
|
padding: mini ? 0 : 0.5,
|
||||||
|
mb: 4,
|
||||||
|
width: mini ? MINI_DRAWER_WIDTH : "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DashboardSidebarHeaderItem>Main items</DashboardSidebarHeaderItem>
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
id="employees"
|
||||||
|
title="Employees"
|
||||||
|
icon={<PersonIcon />}
|
||||||
|
href="/employees"
|
||||||
|
selected={
|
||||||
|
!!matchPath("/employees/*", pathname) || pathname === "/"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DashboardSidebarDividerItem />
|
||||||
|
<DashboardSidebarHeaderItem>
|
||||||
|
Example items
|
||||||
|
</DashboardSidebarHeaderItem>
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
id="reports"
|
||||||
|
title="Reports"
|
||||||
|
icon={<BarChartIcon />}
|
||||||
|
href="/reports"
|
||||||
|
selected={!!matchPath("/reports", pathname)}
|
||||||
|
defaultExpanded={!!matchPath("/reports", pathname)}
|
||||||
|
expanded={expandedItemIds.includes("reports")}
|
||||||
|
nestedNavigation={
|
||||||
|
<List
|
||||||
|
dense
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
my: 1,
|
||||||
|
pl: mini ? 0 : 1,
|
||||||
|
minWidth: 240,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
id="sales"
|
||||||
|
title="Sales"
|
||||||
|
icon={<DescriptionIcon />}
|
||||||
|
href="/reports/sales"
|
||||||
|
selected={!!matchPath("/reports/sales", pathname)}
|
||||||
|
/>
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
id="traffic"
|
||||||
|
title="Traffic"
|
||||||
|
icon={<DescriptionIcon />}
|
||||||
|
href="/reports/traffic"
|
||||||
|
selected={!!matchPath("/reports/traffic", pathname)}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
id="integrations"
|
||||||
|
title="Integrations"
|
||||||
|
icon={<LayersIcon />}
|
||||||
|
href="/integrations"
|
||||||
|
selected={!!matchPath("/integrations", pathname)}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</React.Fragment>
|
||||||
|
),
|
||||||
|
[mini, hasDrawerTransitions, isFullyExpanded, expandedItemIds, pathname]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDrawerSharedSx = React.useCallback(
|
||||||
|
(isTemporary: boolean) => {
|
||||||
|
const drawerWidth = mini ? MINI_DRAWER_WIDTH : DRAWER_WIDTH;
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayPrint: "none",
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
...getDrawerWidthTransitionMixin(expanded),
|
||||||
|
...(isTemporary ? { position: "absolute" } : {}),
|
||||||
|
[`& .MuiDrawer-paper`]: {
|
||||||
|
position: "absolute",
|
||||||
|
width: drawerWidth,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
backgroundImage: "none",
|
||||||
|
...getDrawerWidthTransitionMixin(expanded),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[expanded, mini]
|
||||||
|
);
|
||||||
|
|
||||||
|
const sidebarContextValue = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
onPageItemClick: handlePageItemClick,
|
||||||
|
mini,
|
||||||
|
fullyExpanded: isFullyExpanded,
|
||||||
|
fullyCollapsed: isFullyCollapsed,
|
||||||
|
hasDrawerTransitions,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
handlePageItemClick,
|
||||||
|
mini,
|
||||||
|
isFullyExpanded,
|
||||||
|
isFullyCollapsed,
|
||||||
|
hasDrawerTransitions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardSidebarContext.Provider value={sidebarContextValue}>
|
||||||
|
<Drawer
|
||||||
|
container={container}
|
||||||
|
variant="temporary"
|
||||||
|
open={expanded}
|
||||||
|
onClose={handleSetSidebarExpanded(false)}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true, // Better open performance on mobile.
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: {
|
||||||
|
xs: "block",
|
||||||
|
sm: disableCollapsibleSidebar ? "block" : "none",
|
||||||
|
md: "none",
|
||||||
|
},
|
||||||
|
...getDrawerSharedSx(true),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getDrawerContent("phone")}
|
||||||
|
</Drawer>
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
display: {
|
||||||
|
xs: "none",
|
||||||
|
sm: disableCollapsibleSidebar ? "none" : "block",
|
||||||
|
md: "none",
|
||||||
|
},
|
||||||
|
...getDrawerSharedSx(false),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getDrawerContent("tablet")}
|
||||||
|
</Drawer>
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
display: { xs: "none", md: "block" },
|
||||||
|
...getDrawerSharedSx(false),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getDrawerContent("desktop")}
|
||||||
|
</Drawer>
|
||||||
|
</DashboardSidebarContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const DashboardSidebarContext = React.createContext(null);
|
||||||
|
|
||||||
|
export default DashboardSidebarContext;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||||
|
import { getDrawerSxTransitionMixin } from "./mixins";
|
||||||
|
|
||||||
|
export default function DashboardSidebarDividerItem() {
|
||||||
|
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||||
|
if (!sidebarContext) {
|
||||||
|
throw new Error("Sidebar context was used without a provider.");
|
||||||
|
}
|
||||||
|
const { fullyExpanded = true, hasDrawerTransitions } = sidebarContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Divider
|
||||||
|
sx={{
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
my: 1,
|
||||||
|
mx: -0.5,
|
||||||
|
...(hasDrawerTransitions
|
||||||
|
? getDrawerSxTransitionMixin(fullyExpanded, "margin")
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import ListSubheader from "@mui/material/ListSubheader";
|
||||||
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||||
|
import { DRAWER_WIDTH } from "./constants";
|
||||||
|
import { getDrawerSxTransitionMixin } from "./mixins";
|
||||||
|
|
||||||
|
export interface DashboardSidebarHeaderItemProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardSidebarHeaderItem({
|
||||||
|
children,
|
||||||
|
}: DashboardSidebarHeaderItemProps) {
|
||||||
|
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||||
|
if (!sidebarContext) {
|
||||||
|
throw new Error("Sidebar context was used without a provider.");
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
mini = false,
|
||||||
|
fullyExpanded = true,
|
||||||
|
hasDrawerTransitions,
|
||||||
|
} = sidebarContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
height: mini ? 0 : 36,
|
||||||
|
...(hasDrawerTransitions
|
||||||
|
? getDrawerSxTransitionMixin(fullyExpanded, "height")
|
||||||
|
: {}),
|
||||||
|
px: 1.5,
|
||||||
|
py: 0,
|
||||||
|
minWidth: DRAWER_WIDTH,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ListSubheader>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { type Theme, type SxProps } from "@mui/material/styles";
|
||||||
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Collapse from "@mui/material/Collapse";
|
||||||
|
import Grow from "@mui/material/Grow";
|
||||||
|
import ListItem from "@mui/material/ListItem";
|
||||||
|
import ListItemButton from "@mui/material/ListItemButton";
|
||||||
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||||
|
import { MINI_DRAWER_WIDTH } from "./constants";
|
||||||
|
|
||||||
|
export interface DashboardSidebarPageItemProps {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
action?: React.ReactNode;
|
||||||
|
defaultExpanded?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
nestedNavigation?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardSidebarPageItem({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
href,
|
||||||
|
action,
|
||||||
|
defaultExpanded = false,
|
||||||
|
expanded = defaultExpanded,
|
||||||
|
selected = false,
|
||||||
|
disabled = false,
|
||||||
|
nestedNavigation,
|
||||||
|
}: DashboardSidebarPageItemProps) {
|
||||||
|
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||||
|
if (!sidebarContext) {
|
||||||
|
throw new Error("Sidebar context was used without a provider.");
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
onPageItemClick,
|
||||||
|
mini = false,
|
||||||
|
fullyExpanded = true,
|
||||||
|
fullyCollapsed = false,
|
||||||
|
} = sidebarContext;
|
||||||
|
|
||||||
|
const [isHovered, setIsHovered] = React.useState(false);
|
||||||
|
|
||||||
|
const handleClick = React.useCallback(() => {
|
||||||
|
if (onPageItemClick) {
|
||||||
|
onPageItemClick(id, !!nestedNavigation);
|
||||||
|
}
|
||||||
|
}, [onPageItemClick, id, nestedNavigation]);
|
||||||
|
|
||||||
|
let nestedNavigationCollapseSx: SxProps<Theme> = { display: "none" };
|
||||||
|
if (mini && fullyCollapsed) {
|
||||||
|
nestedNavigationCollapseSx = {
|
||||||
|
fontSize: 18,
|
||||||
|
position: "absolute",
|
||||||
|
top: "41.5%",
|
||||||
|
right: "2px",
|
||||||
|
transform: "translateY(-50%) rotate(-90deg)",
|
||||||
|
};
|
||||||
|
} else if (!mini && fullyExpanded) {
|
||||||
|
nestedNavigationCollapseSx = {
|
||||||
|
ml: 0.5,
|
||||||
|
fontSize: 20,
|
||||||
|
transform: `rotate(${expanded ? 0 : -90}deg)`,
|
||||||
|
transition: (theme: Theme) =>
|
||||||
|
theme.transitions.create("transform", {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: 100,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExternalHref = href
|
||||||
|
? href.startsWith("http://") || href.startsWith("https://")
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const LinkComponent = hasExternalHref ? "a" : Link;
|
||||||
|
|
||||||
|
const miniNestedNavigationSidebarContextValue = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
onPageItemClick: onPageItemClick ?? (() => {}),
|
||||||
|
mini: false,
|
||||||
|
fullyExpanded: true,
|
||||||
|
fullyCollapsed: false,
|
||||||
|
hasDrawerTransitions: false,
|
||||||
|
};
|
||||||
|
}, [onPageItemClick]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ListItem
|
||||||
|
disablePadding
|
||||||
|
{...(nestedNavigation && mini
|
||||||
|
? {
|
||||||
|
onMouseEnter: () => {
|
||||||
|
setIsHovered(true);
|
||||||
|
},
|
||||||
|
onMouseLeave: () => {
|
||||||
|
setIsHovered(false);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
sx={{
|
||||||
|
display: "block",
|
||||||
|
py: 0,
|
||||||
|
px: 1,
|
||||||
|
overflowX: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemButton
|
||||||
|
selected={selected}
|
||||||
|
disabled={disabled}
|
||||||
|
sx={{
|
||||||
|
height: mini ? 50 : "auto",
|
||||||
|
}}
|
||||||
|
{...(nestedNavigation && !mini
|
||||||
|
? {
|
||||||
|
onClick: handleClick,
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
{...(!nestedNavigation
|
||||||
|
? {
|
||||||
|
LinkComponent,
|
||||||
|
...(hasExternalHref
|
||||||
|
? {
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener noreferrer",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
to: href,
|
||||||
|
onClick: handleClick,
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
>
|
||||||
|
{icon || mini ? (
|
||||||
|
<Box
|
||||||
|
sx={
|
||||||
|
mini
|
||||||
|
? {
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
top: "calc(50% - 6px)",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemIcon
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: mini ? "center" : "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon ?? null}
|
||||||
|
{!icon && mini ? (
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
fontSize: 10,
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title
|
||||||
|
.split(" ")
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((titleWord) => titleWord.charAt(0).toUpperCase())}
|
||||||
|
</Avatar>
|
||||||
|
) : null}
|
||||||
|
</ListItemIcon>
|
||||||
|
{mini ? (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: -18,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 500,
|
||||||
|
textAlign: "center",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
maxWidth: MINI_DRAWER_WIDTH - 28,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
{!mini ? (
|
||||||
|
<ListItemText
|
||||||
|
primary={title}
|
||||||
|
sx={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{action && !mini && fullyExpanded ? action : null}
|
||||||
|
{nestedNavigation ? (
|
||||||
|
<ExpandMoreIcon sx={nestedNavigationCollapseSx} />
|
||||||
|
) : null}
|
||||||
|
</ListItemButton>
|
||||||
|
{nestedNavigation && mini ? (
|
||||||
|
<Grow in={isHovered}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "fixed",
|
||||||
|
left: MINI_DRAWER_WIDTH - 2,
|
||||||
|
pl: "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={8}
|
||||||
|
sx={{
|
||||||
|
pt: 0.2,
|
||||||
|
pb: 0.2,
|
||||||
|
transform: "translateY(-50px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DashboardSidebarContext.Provider
|
||||||
|
value={miniNestedNavigationSidebarContextValue}
|
||||||
|
>
|
||||||
|
{nestedNavigation}
|
||||||
|
</DashboardSidebarContext.Provider>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Grow>
|
||||||
|
) : null}
|
||||||
|
</ListItem>
|
||||||
|
{nestedNavigation && !mini ? (
|
||||||
|
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||||
|
{nestedNavigation}
|
||||||
|
</Collapse>
|
||||||
|
) : null}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||||
|
import { getDrawerSxTransitionMixin } from "./mixins";
|
||||||
|
|
||||||
|
export default function DashboardSidebarDividerItem() {
|
||||||
|
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||||
|
if (!sidebarContext) {
|
||||||
|
throw new Error("Sidebar context was used without a provider.");
|
||||||
|
}
|
||||||
|
const { fullyExpanded = true, hasDrawerTransitions } = sidebarContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Divider
|
||||||
|
sx={{
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
my: 1,
|
||||||
|
mx: -0.5,
|
||||||
|
...(hasDrawerTransitions
|
||||||
|
? getDrawerSxTransitionMixin(fullyExpanded, "margin")
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx
Normal file
59
matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useTheme, useColorScheme } from "@mui/material/styles";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||||
|
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||||
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
|
||||||
|
export default function ThemeSwitcher() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||||
|
const preferredMode = prefersDarkMode ? "dark" : "light";
|
||||||
|
|
||||||
|
const { mode, setMode } = useColorScheme();
|
||||||
|
|
||||||
|
const paletteMode = !mode || mode === "system" ? preferredMode : mode;
|
||||||
|
|
||||||
|
const toggleMode = React.useCallback(() => {
|
||||||
|
setMode(paletteMode === "dark" ? "light" : "dark");
|
||||||
|
}, [setMode, paletteMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={`${paletteMode === "dark" ? "Light" : "Dark"} mode`}
|
||||||
|
enterDelay={1000}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label={`Switch to ${
|
||||||
|
paletteMode === "dark" ? "light" : "dark"
|
||||||
|
} mode`}
|
||||||
|
onClick={toggleMode}
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
<LightModeIcon
|
||||||
|
sx={{
|
||||||
|
display: "inline",
|
||||||
|
[theme.getColorSchemeSelector("dark")]: {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DarkModeIcon
|
||||||
|
sx={{
|
||||||
|
display: "none",
|
||||||
|
[theme.getColorSchemeSelector("dark")]: {
|
||||||
|
display: "inline",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
matrixgw_frontend/src/widgets/dashboard/constants.ts
Normal file
2
matrixgw_frontend/src/widgets/dashboard/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const DRAWER_WIDTH = 240; // px
|
||||||
|
export const MINI_DRAWER_WIDTH = 90; // px
|
||||||
23
matrixgw_frontend/src/widgets/dashboard/mixins.ts
Normal file
23
matrixgw_frontend/src/widgets/dashboard/mixins.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { type Theme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export function getDrawerSxTransitionMixin(
|
||||||
|
isExpanded: boolean,
|
||||||
|
property: string
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
transition: (theme: Theme) =>
|
||||||
|
theme.transitions.create(property, {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: isExpanded
|
||||||
|
? theme.transitions.duration.enteringScreen
|
||||||
|
: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDrawerWidthTransitionMixin(isExpanded: boolean) {
|
||||||
|
return {
|
||||||
|
...getDrawerSxTransitionMixin(isExpanded, "width"),
|
||||||
|
overflowX: "hidden",
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user