Style update for User Info Endpoint (#3055)

* style changes from mui 5

* fix spacing issue on small screen

* remove unneeded import

* add default exports

* make linter happy

* more style changes

* use startCase from lodash

Co-authored-by: Caleb Doxsey <cdoxsey@pomerium.com>
This commit is contained in:
Nathan Hayfield 2022-02-18 18:39:44 +01:00 committed by GitHub
parent f0843d6f44
commit fd8ec0099e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 400 additions and 214 deletions

View file

@ -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 ? (
<MuiAvatar alt={name} src={url} />
) : (
<User />
);
};

View file

@ -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<ClaimsTableProps> = ({ claims }) => {
const entries = Object.entries(claims || {});
entries.sort(([a], [b]) => a.localeCompare(b));
return (
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell variant="head">Claims</TableCell>
<TableCell variant="head"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{entries.length > 0 ? (
entries.map(([key, values]) => (
<TableRow key={key}>
<TableCell>{key}</TableCell>
<TableCell>
{values?.map((v, i) => (
<React.Fragment key={`${v}`}>
{i > 0 ? <br /> : <></>}
<ClaimValue claimKey={key} claimValue={v} />
</React.Fragment>
))}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={2} padding="none">
<Alert severity="warning" square={true}>
No Claims Found
</Alert>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
);
};
export default ClaimsTable;

View file

@ -8,7 +8,7 @@ type DeviceEnrolledPageProps = {
};
const DeviceEnrolledPage: FC<DeviceEnrolledPageProps> = () => {
return (
<Container>
<Container maxWidth={false}>
<HeroSection
title="Device Enrolled"
text="Device Successfully Enrolled"

View file

@ -14,7 +14,7 @@ export type ErrorPageProps = {
};
export const ErrorPage: FC<ErrorPageProps> = ({ data }) => {
return (
<Container>
<Container maxWidth={false}>
<Paper sx={{ overflow: "hidden" }}>
<Stack>
<Box sx={{ padding: "16px" }}>

View file

@ -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 (
<Container component="footer">
<AppBar
position="fixed"
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1,
top: 'auto',
bottom: 0,
}}
>
<Stack
direction="row"
spacing={2}
spacing={8}
justifyContent="center"
sx={{
fontSize: "0.85rem",
padding: "16px"
padding: "16px",
}}
>
<Box>
<a href="https://pomerium.com/">Home</a>
<FooterLink
href="https://pomerium.com/"
>
Home
</FooterLink>
</Box>
<Box>
<a href="https://pomerium.com/docs">Docs</a>
<FooterLink
href="https://pomerium.com/docs"
>
Docs
</FooterLink>
</Box>
<Box>
<a href="https://pomerium.com/docs/community/">Support</a>
</Box>
<Box>
<a href="https://github.com/pomerium">GitHub</a>
</Box>
<Box>
<a href="https://twitter.com/pomerium_io">@pomerium_io</a>
</Box>
<Box flexGrow={1} sx={{ textAlign: "right" }}>
© Pomerium, Inc.
<FooterLink
href="https://discuss.pomerium.com"
>
Support
</FooterLink>
</Box>
</Stack>
</Container>
</AppBar>
);
};
export default Footer;

View file

@ -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;

View file

@ -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<HeaderProps> = ({ csrfToken, signOutUrl }) => {
const Header: FC<HeaderProps> = ({ 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 (
<AppBar position="sticky">
<AppBar
position="fixed"
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
>
<Toolbar>
<a href="/.pomerium">
<Logo />
</a>
<Box flexGrow={1} />
{signOutUrl ? (
<form action={signOutUrl}>
<CsrfInput csrfToken={csrfToken} />
<Button variant="text" color="inherit" type="submit">
Logout
</Button>
</form>
{!mdUp && includeSidebar ? (
<>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
sx={{ mr: 2, ...(drawerOpen && { display: 'none' }) }}
>
<MenuIcon />
</IconButton>
<Drawer
sx={{
width: 256,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: 256,
boxSizing: 'border-box',
backgroundColor: 'neutral.900',
height: '100vh',
},
}}
variant="persistent"
anchor="left"
open={drawerOpen}
>
<DrawerHeader>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'ltr' ? <ChevronLeft /> : <ChevronRight />}
</IconButton>
</DrawerHeader>
<UserSidebarContent close={handleDrawerClose}/>
<ToolbarOffset />
</Drawer>
</>
) : (
<></>
<a href="/.pomerium">
<Logo />
</a>
)}
<Box flexGrow={1} />
{userName && (
<>
<IconButton color="inherit" onClick={handleMenuOpen}>
<Avatar name={userName} url={get(data, 'claims.picture', null)} />
</IconButton>
<Menu
onClose={handleMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
keepMounted
open={!!anchorEl}
anchorEl={anchorEl}
>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu>
</>
)}
</Toolbar>
</AppBar>

View file

@ -21,10 +21,10 @@ export const Section: FC<SectionProps> = ({
<Paper sx={{ overflow: "hidden" }}>
<Stack>
<Toolbar>
<Typography variant="h4" flexGrow={1}>
<Typography variant="h4">
{title}
</Typography>
{icon ? <Box>{icon}</Box> : <></>}
{!!icon && (<Box sx={{marginLeft: (theme) => theme.spacing(3)}}>{icon}</Box>)}
</Toolbar>
<Box sx={{ padding: 3, paddingTop: 0 }}>{children}</Box>
{footer ? (

View file

@ -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<SessionDetailsProps> = ({ session }) => {
return (
<Section title="Session Details">
<Section title="User Details">
<Stack spacing={3}>
<TableContainer>
<Table size="small">
<TableBody>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>
<TableCell width={'18%'} variant="head">Session ID</TableCell>
<TableCell align="left">
<IDField value={session?.id} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>User ID</TableCell>
<TableCell>
<TableCell variant="head">User ID</TableCell>
<TableCell align="left">
<IDField value={session?.userId} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Expires At</TableCell>
<TableCell>{session?.expiresAt || ""}</TableCell>
<TableCell variant="head">Expires At</TableCell>
<TableCell align="left">{session?.expiresAt || ""}</TableCell>
</TableRow>
{Object.entries(session?.claims || {}).map(
([key, values]) => (
<TableRow key={key}>
<TableCell variant="head">{startCase(key)}</TableCell>
<TableCell align="left">
{values?.map((v, i) => (
<React.Fragment key={`${v}`}>
{i > 0 ? <br /> : <></>}
<ClaimValue claimKey={key} claimValue={v} />
</React.Fragment>
))}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<ClaimsTable claims={session?.claims} />
</Stack>
</Section>
);

View file

@ -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),
}));

View file

@ -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<UserClaimsProps> = ({ user }) => {
return (
<Section title="User Claims" icon={<JwtIcon />}>
<ClaimsTable claims={user?.claims} />
</Section>
);
};
export default UserClaims;

View file

@ -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<UserInfoPageProps> = ({ 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 (
<Container>
<Stack spacing={3}>
<HeroSection
icon={
<Avatar>
<PersonIcon />
</Avatar>
}
title={<>Hi {name}!</>}
text={
<>
Welcome to the user info endpoint. Here you can view your current
session details, and authorization context.
</>
}
/>
<SessionDetails session={data?.session} />
<UserClaims user={data?.user} />
<GroupDetails groups={data?.directoryGroups} />
<SessionDeviceCredentials
csrfToken={data?.csrfToken}
session={data?.session}
user={data?.user}
webAuthnUrl={data?.webAuthnUrl}
/>
<Container maxWidth={false}>
{mdUp && (
<Drawer
anchor="left"
open
PaperProps={{
sx: {
backgroundColor: 'neutral.900',
width: 256,
height: '100vh',
}
}}
variant="persistent"
>
<ToolbarOffset />
<UserSidebarContent close={null}/>
<ToolbarOffset />
</Drawer>
)}
<Stack
spacing={3}
sx={{
marginLeft: mdUp ? '256px' : '0px',
}}>
{subpage === 'User' && (
<SessionDetails session={data?.session} />
)}
{subpage === 'Groups Info' && (
<GroupDetails groups={data?.directoryGroups} />
)}
{subpage === 'Devices Info' && (
<SessionDeviceCredentials
csrfToken={data?.csrfToken}
session={data?.session}
user={data?.user}
webAuthnUrl={data?.webAuthnUrl}
/>
)}
</Stack>
</Container>
);

View file

@ -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: <User />
},
{
title: 'Groups Info',
icon: <Users />
},
{
title: 'Devices Info',
icon: <Devices />
},
]
type UserSidebarContent = {
close: () => void | null;
};
export const UserSidebarContent:FC<UserSidebarContent> = ({close}:UserSidebarContent):JSX.Element => {
const info = useContext(SubpageContext);
return (
<List>
{sectionList.map(({title, icon}) => {
return (
<ListItemButton
key={'tab ' + title}
selected={title === info.subpage}
onClick={() => {
info.setSubpage(title)
!!close && close();
}}
>
<ListItemIcon>
{icon}
</ListItemIcon>
<ListItemText primary={title} />
</ListItemButton>
)
})}
</List>
);
}
export default UserSidebarContent;

View file

@ -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<WebAuthnRegistrationPageProps> = ({
}
return (
<Container>
<Stack spacing={3}>
<HeroSection
title={
<>
WebAuthn Registration <ExperimentalIcon />
</>
}
<Section title="WebAuthn Registration" icon={<ExperimentalIcon />}>
<Paper sx={{ padding: "16px" }}>
<Stack direction="row" justifyContent="center" spacing={3}>
<Button onClick={handleClickRegister} variant="contained">
Register New Device
</Button>
<Button
onClick={handleClickAuthenticate}
variant="contained"
disabled={!enableAuthenticate}
>
Authenticate Existing Device
</Button>
</Stack>
</Paper>
<form ref={authenticateFormRef} method="post" action={data?.selfUrl}>
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
<input type="hidden" name="action" value="authenticate" />
<input
type="hidden"
name="authenticate_response"
ref={authenticateResponseRef}
/>
<Paper sx={{ padding: "16px" }}>
<Stack direction="row" justifyContent="center" spacing={3}>
<Button onClick={handleClickRegister} variant="contained">
Register New Device
</Button>
<Button
onClick={handleClickAuthenticate}
variant="contained"
disabled={!enableAuthenticate}
>
Authenticate Existing Device
</Button>
</Stack>
</Paper>
<form ref={authenticateFormRef} method="post" action={data?.selfUrl}>
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
<input type="hidden" name="action" value="authenticate" />
<input
type="hidden"
name="authenticate_response"
ref={authenticateResponseRef}
/>
</form>
<form ref={registerFormRef} method="POST" action={data?.selfUrl}>
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
<input type="hidden" name="action" value="register" />
<input
type="hidden"
name="register_response"
ref={registerResponseRef}
/>
</form>
</Stack>
</form>
<form ref={registerFormRef} method="POST" action={data?.selfUrl}>
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
<input type="hidden" name="action" value="register" />
<input
type="hidden"
name="register_response"
ref={registerResponseRef}
/>
</form>
<AlertDialog
title="Error"
severity="error"
@ -204,7 +197,7 @@ const WebAuthnRegistrationPage: FC<WebAuthnRegistrationPageProps> = ({
>
{error}
</AlertDialog>
</Container>
</Section>
);
};
export default WebAuthnRegistrationPage;