diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index f0230eee1..5cce3b31d 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -6,11 +6,12 @@ import UserInfoPage from "./components/UserInfoPage";
import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage";
import { createTheme } from "./theme";
import { PageData } from "./types";
-import Container from "@mui/material/Container";
import CssBaseline from "@mui/material/CssBaseline";
-import Stack from "@mui/material/Stack";
import { ThemeProvider } from "@mui/material/styles";
import React, { FC } from "react";
+import {ToolbarOffset} from "./components/ToolbarOffset";
+import Box from "@mui/material/Box";
+import {SubpageContextProvider} from "./context/Subpage";
const theme = createTheme();
@@ -34,13 +35,22 @@ const App: FC = () => {
return (
-
-
-
- {body}
-
-
-
+
+
+
+
+
+ {body}
+
+
+
+
+
);
};
diff --git a/ui/src/components/Avatar.tsx b/ui/src/components/Avatar.tsx
new file mode 100644
index 000000000..700fc6b28
--- /dev/null
+++ b/ui/src/components/Avatar.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import {User} from "react-feather";
+import MuiAvatar from "@mui/material/Avatar";
+
+type AvatarProps = {
+ name: string;
+ url?: string;
+}
+
+export const Avatar = ({url, name}:AvatarProps): JSX.Element => {
+ if (url === 'https://graph.microsoft.com/v1.0/me/photo/$value') {
+ url = null;
+ }
+
+ return url ? (
+
+ ) : (
+
+ );
+};
diff --git a/ui/src/components/ClaimsTable.tsx b/ui/src/components/ClaimsTable.tsx
deleted file mode 100644
index 96a3a0c6a..000000000
--- a/ui/src/components/ClaimsTable.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Claims } from "../types";
-import ClaimValue from "./ClaimValue";
-import Alert from "@mui/material/Alert";
-import Table from "@mui/material/Table";
-import TableBody from "@mui/material/TableBody";
-import TableCell from "@mui/material/TableCell";
-import TableContainer from "@mui/material/TableContainer";
-import TableHead from "@mui/material/TableHead";
-import TableRow from "@mui/material/TableRow";
-import React, { FC } from "react";
-
-type ClaimsTableProps = {
- claims: Claims;
-};
-const ClaimsTable: FC = ({ claims }) => {
- const entries = Object.entries(claims || {});
- entries.sort(([a], [b]) => a.localeCompare(b));
-
- return (
-
-
-
-
- Claims
-
-
-
-
- {entries.length > 0 ? (
- entries.map(([key, values]) => (
-
- {key}
-
- {values?.map((v, i) => (
-
- {i > 0 ?
: <>>}
-
-
- ))}
-
-
- ))
- ) : (
-
-
-
- No Claims Found
-
-
-
- )}
-
-
-
- );
-};
-export default ClaimsTable;
diff --git a/ui/src/components/DeviceEnrolledPage.tsx b/ui/src/components/DeviceEnrolledPage.tsx
index de938381e..108682e7f 100644
--- a/ui/src/components/DeviceEnrolledPage.tsx
+++ b/ui/src/components/DeviceEnrolledPage.tsx
@@ -8,7 +8,7 @@ type DeviceEnrolledPageProps = {
};
const DeviceEnrolledPage: FC = () => {
return (
-
+
= ({ data }) => {
return (
-
+
diff --git a/ui/src/components/Footer.tsx b/ui/src/components/Footer.tsx
index 5b75d3968..42b014520 100644
--- a/ui/src/components/Footer.tsx
+++ b/ui/src/components/Footer.tsx
@@ -1,39 +1,51 @@
import Box from "@mui/material/Box";
-import Container from "@mui/material/Container";
import Stack from "@mui/material/Stack";
import React, { FC } from "react";
+import {FooterLink} from "./FooterLink";
+import AppBar from "@mui/material/AppBar";
const Footer: FC = () => {
return (
-
+ theme.zIndex.drawer + 1,
+ top: 'auto',
+ bottom: 0,
+ }}
+ >
- Home
+
+ Home
+
- Docs
+
+ Docs
+
- Support
-
-
- GitHub
-
-
- @pomerium_io
-
-
- © Pomerium, Inc.
+
+ Support
+
-
+
);
};
export default Footer;
diff --git a/ui/src/components/FooterLink.tsx b/ui/src/components/FooterLink.tsx
new file mode 100644
index 000000000..15d3cf2d8
--- /dev/null
+++ b/ui/src/components/FooterLink.tsx
@@ -0,0 +1,10 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import {Link} from "@mui/material";
+
+export const FooterLink = styled(Link)(({ theme }) => ({
+ fontSize: '1.25rem',
+ fontWeight: `bold`,
+ color: theme.palette.background.default,
+}));
+export default FooterLink;
diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx
index 556042e56..93580c2f3 100644
--- a/ui/src/components/Header.tsx
+++ b/ui/src/components/Header.tsx
@@ -1,32 +1,124 @@
-import CsrfInput from "./CsrfInput";
import Logo from "./Logo";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
-import Button from "@mui/material/Button";
import Toolbar from "@mui/material/Toolbar";
-import React, { FC } from "react";
+import React, {FC, useState} from "react";
+import {useTheme} from "@mui/material/styles";
+import {Drawer, IconButton, Menu, MenuItem, useMediaQuery} from "@mui/material";
+import {ToolbarOffset} from "./ToolbarOffset";
+import UserSidebarContent from "./UserSidebarContent";
+import {ChevronLeft, ChevronRight, Menu as MenuIcon} from "react-feather";
+import styled from "@mui/material/styles/styled";
+import {Avatar} from "./Avatar";
+import {PageData} from "../types";
+import {get} from 'lodash';
+
+const DrawerHeader = styled('div')(({ theme }) => ({
+ display: 'flex',
+ alignItems: 'center',
+ padding: theme.spacing(0, 1),
+ justifyContent: 'flex-end',
+}));
type HeaderProps = {
- csrfToken: string;
- signOutUrl: string;
+ includeSidebar: boolean;
+ data: PageData;
};
-const Header: FC = ({ csrfToken, signOutUrl }) => {
+const Header: FC = ({ includeSidebar, data }) => {
+ const theme = useTheme();
+ const mdUp = useMediaQuery(() => theme.breakpoints.up('md'), {
+ defaultMatches: true,
+ noSsr: false
+ });
+
+ const [drawerOpen, setDrawerOpen] = useState(false);
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const handleMenuOpen = e => {
+ setAnchorEl(e.currentTarget);
+ };
+ const handleMenuClose = () => {
+ setAnchorEl(null);
+ };
+ const userName = get(data, 'user.name') || get(data, 'user.claims.given_name');
+
+ const handleDrawerOpen = () => {
+ setDrawerOpen(true);
+ };
+
+ const handleDrawerClose = ():void => {
+ setDrawerOpen(false);
+ };
+
+ const handleLogout = (evt: React.MouseEvent):void => {
+ evt.preventDefault();
+ location.href = "/.pomerium/sign_out";
+ }
+
return (
-
+ theme.zIndex.drawer + 1 }}
+ >
-
-
-
-
- {signOutUrl ? (
-
+ {!mdUp && includeSidebar ? (
+ <>
+
+
+
+
+
+
+ {theme.direction === 'ltr' ? : }
+
+
+
+
+
+ >
) : (
- <>>
+
+
+
+ )}
+
+ {userName && (
+ <>
+
+
+
+
+ >
)}
diff --git a/ui/src/components/Section.tsx b/ui/src/components/Section.tsx
index b744ceaa9..de65698f9 100644
--- a/ui/src/components/Section.tsx
+++ b/ui/src/components/Section.tsx
@@ -21,10 +21,10 @@ export const Section: FC = ({
-
+
{title}
- {icon ? {icon} : <>>}
+ {!!icon && ( theme.spacing(3)}}>{icon})}
{children}
{footer ? (
diff --git a/ui/src/components/SessionDetails.tsx b/ui/src/components/SessionDetails.tsx
index b67888c04..910d3510f 100644
--- a/ui/src/components/SessionDetails.tsx
+++ b/ui/src/components/SessionDetails.tsx
@@ -1,5 +1,4 @@
import { Session } from "../types";
-import ClaimsTable from "./ClaimsTable";
import IDField from "./IDField";
import Section from "./Section";
import Stack from "@mui/material/Stack";
@@ -9,37 +8,52 @@ import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import React, { FC } from "react";
+import ClaimValue from "./ClaimValue";
+import {startCase} from "lodash";
export type SessionDetailsProps = {
session: Session;
};
export const SessionDetails: FC = ({ session }) => {
return (
-
+
- ID
-
+ Session ID
+
- User ID
-
+ User ID
+
- Expires At
- {session?.expiresAt || ""}
+ Expires At
+ {session?.expiresAt || ""}
+ {Object.entries(session?.claims || {}).map(
+ ([key, values]) => (
+
+ {startCase(key)}
+
+ {values?.map((v, i) => (
+
+ {i > 0 ?
: <>>}
+
+
+ ))}
+
+
+ ))}
-
);
diff --git a/ui/src/components/ToolbarOffset.tsx b/ui/src/components/ToolbarOffset.tsx
new file mode 100644
index 000000000..9396cbe87
--- /dev/null
+++ b/ui/src/components/ToolbarOffset.tsx
@@ -0,0 +1,6 @@
+import styled from "@mui/material/styles/styled";
+import {BaseCSSProperties} from "@mui/material/styles/createMixins";
+
+export const ToolbarOffset = styled('div')(({ theme }) => ({
+ ...(theme.mixins.toolbar as BaseCSSProperties),
+}));
diff --git a/ui/src/components/UserClaims.tsx b/ui/src/components/UserClaims.tsx
deleted file mode 100644
index 72491b7af..000000000
--- a/ui/src/components/UserClaims.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { User } from "../types";
-import ClaimsTable from "./ClaimsTable";
-import JwtIcon from "./JwtIcon";
-import Section from "./Section";
-import React, { FC } from "react";
-
-export type UserClaimsProps = {
- user: User;
-};
-export const UserClaims: FC = ({ user }) => {
- return (
- }>
-
-
- );
-};
-export default UserClaims;
diff --git a/ui/src/components/UserInfoPage.tsx b/ui/src/components/UserInfoPage.tsx
index 785af3bb4..46372a83e 100644
--- a/ui/src/components/UserInfoPage.tsx
+++ b/ui/src/components/UserInfoPage.tsx
@@ -1,53 +1,70 @@
import GroupDetails from "./GroupDetails";
-import HeroSection from "./HeroSection";
-import PersonIcon from "./PersonIcon";
import SessionDetails from "./SessionDetails";
import SessionDeviceCredentials from "./SessionDeviceCredentials";
-import UserClaims from "./UserClaims";
-import MuiAvatar from "@mui/material/Avatar";
import Container from "@mui/material/Container";
-import Stack from "@mui/material/Stack";
-import styled from "@mui/material/styles/styled";
-import React, { FC } from "react";
+import React, {FC, useContext} from "react";
import { UserInfoPageData } from "src/types";
-
-const Avatar = styled(MuiAvatar)(({ theme }) => ({
- backgroundColor: theme.palette.primary.main,
- height: 48,
- width: 48
-}));
+import {Drawer, useMediaQuery} from "@mui/material";
+import { useTheme } from '@mui/material/styles';
+import { ToolbarOffset } from "./ToolbarOffset";
+import {UserSidebarContent} from "./UserSidebarContent";
+import {SubpageContext} from "../context/Subpage";
+import Stack from "@mui/material/Stack";
type UserInfoPageProps = {
data: UserInfoPageData;
};
const UserInfoPage: FC = ({ data }) => {
- const name = data?.user?.claims?.given_name?.[0] || data?.user?.name;
+ const theme = useTheme();
+ const mdUp = useMediaQuery(() => theme.breakpoints.up('md'), {
+ defaultMatches: true,
+ noSsr: false
+ });
+ const {subpage} = useContext(SubpageContext);
+
+
return (
-
-
-
-
-
- }
- title={<>Hi {name}!>}
- text={
- <>
- Welcome to the user info endpoint. Here you can view your current
- session details, and authorization context.
- >
- }
- />
-
-
-
-
+
+ {mdUp && (
+
+
+
+
+
+ )}
+
+
+ {subpage === 'User' && (
+
+ )}
+
+ {subpage === 'Groups Info' && (
+
+ )}
+
+ {subpage === 'Devices Info' && (
+
+ )}
);
diff --git a/ui/src/components/UserSidebarContent.tsx b/ui/src/components/UserSidebarContent.tsx
new file mode 100644
index 000000000..7dd9773d8
--- /dev/null
+++ b/ui/src/components/UserSidebarContent.tsx
@@ -0,0 +1,55 @@
+import React, {FC, ReactNode, useContext} from "react";
+import {SubpageContext} from "../context/Subpage";
+import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material";
+import {User, Users} from "react-feather";
+import {Devices} from "@mui/icons-material";
+
+export interface Subpage {
+ icon: ReactNode;
+ title: string;
+}
+
+export const sectionList: Subpage[] = [
+ {
+ title: 'User',
+ icon:
+ },
+ {
+ title: 'Groups Info',
+ icon:
+ },
+ {
+ title: 'Devices Info',
+ icon:
+ },
+]
+type UserSidebarContent = {
+ close: () => void | null;
+};
+export const UserSidebarContent:FC = ({close}:UserSidebarContent):JSX.Element => {
+
+ const info = useContext(SubpageContext);
+
+ return (
+
+ {sectionList.map(({title, icon}) => {
+ return (
+ {
+ info.setSubpage(title)
+ !!close && close();
+ }}
+ >
+
+ {icon}
+
+
+
+ )
+ })}
+
+ );
+}
+export default UserSidebarContent;
diff --git a/ui/src/components/WebAuthnRegistrationPage.tsx b/ui/src/components/WebAuthnRegistrationPage.tsx
index a65680734..e092affb4 100644
--- a/ui/src/components/WebAuthnRegistrationPage.tsx
+++ b/ui/src/components/WebAuthnRegistrationPage.tsx
@@ -6,13 +6,15 @@ import Button from "@mui/material/Button";
import Container from "@mui/material/Container";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
-import Typography from "@mui/material/Typography";
import React, { FC, useRef, useState } from "react";
import {
WebAuthnCreationOptions,
WebAuthnRegistrationPageData,
WebAuthnRequestOptions
} from "src/types";
+import JwtIcon from "./JwtIcon";
+import ClaimsTable from "./ClaimsTable";
+import Section from "./Section";
type CredentialForAuthenticate = {
id: string;
@@ -154,48 +156,39 @@ const WebAuthnRegistrationPage: FC = ({
}
return (
-
-
-
- WebAuthn Registration
- >
- }
+ }>
+
+
+
+
+
+
+
-
-
+
+
= ({
>
{error}
-
+
);
};
export default WebAuthnRegistrationPage;
diff --git a/ui/src/context/Subpage.tsx b/ui/src/context/Subpage.tsx
new file mode 100644
index 000000000..ff2d7cfb6
--- /dev/null
+++ b/ui/src/context/Subpage.tsx
@@ -0,0 +1,31 @@
+import React, {createContext, FC, useState} from 'react'
+
+export interface SubpageContextValue {
+ subpage: string;
+ setSubpage: (subpage: string) => void;
+}
+
+export const SubpageContext = createContext({
+ subpage: "User",
+ setSubpage: (_: string) => {},
+});
+
+export const SubpageContextProvider:FC = ({children}) => {
+
+ const setSubpage = (subpage: string) => {
+ setState({...state, subpage})
+ }
+
+ const initState = {
+ subpage: "User",
+ setSubpage
+ }
+
+ const [state, setState] = useState(initState)
+
+ return (
+
+ {children}
+
+ )
+}