feat: Add user menu (#887)

This commit is contained in:
Bruno Quaresma
2022-04-07 13:00:40 -03:00
committed by GitHub
parent 2ca725386f
commit 90388a38f3
6 changed files with 158 additions and 3 deletions

View File

@ -6,6 +6,7 @@ import { NotFoundPage } from "./pages/404"
import { CliAuthenticationPage } from "./pages/cli-auth"
import { HealthzPage } from "./pages/healthz"
import { SignInPage } from "./pages/login"
import { PreferencesPage } from "./pages/preferences"
import { TemplatesPage } from "./pages/templates"
import { TemplatePage } from "./pages/templates/[organization]/[template]"
import { CreateWorkspacePage } from "./pages/templates/[organization]/[template]/create"
@ -67,6 +68,17 @@ export const AppRouter: React.FC = () => (
/>
</Route>
<Route path="preferences">
<Route
index
element={
<AuthAndNav>
<PreferencesPage />
</AuthAndNav>
}
/>
</Route>
{/* Using path="*"" means "match anything", so this route
acts like a catch-all for URLs that we don't have explicit
routes for. */}

View File

@ -0,0 +1,13 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
import React from "react"
export const DocsIcon = (props: SvgIconProps): JSX.Element => (
<SvgIcon {...props} viewBox="0 0 24 24">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.53846 3.75C4.67698 3.75 2.86058 4.50721 2.86058 4.50721L2.5 4.66947V16.4423H9.00841C9.20898 16.7871 9.57407 17.0192 10 17.0192C10.4259 17.0192 10.791 16.7871 10.9916 16.4423H17.5V4.66947L17.1394 4.50721C17.1394 4.50721 15.323 3.75 13.4615 3.75C11.7781 3.75 10.2997 4.31566 10 4.4351C9.70027 4.31566 8.22191 3.75 6.53846 3.75ZM6.53846 4.90385C7.654 4.90385 8.84615 5.26442 9.42308 5.46274V14.7656C8.7808 14.5538 7.72611 14.2608 6.53846 14.2608C5.32602 14.2608 4.33894 14.5403 3.65385 14.7656V5.46274C4.09781 5.30273 5.26968 4.90385 6.53846 4.90385ZM13.4615 4.90385C14.7303 4.90385 15.9022 5.30273 16.3462 5.46274V14.7656C15.6611 14.5403 14.674 14.2608 13.4615 14.2608C12.2739 14.2608 11.2192 14.5538 10.5769 14.7656V5.46274C11.1538 5.26442 12.346 4.90385 13.4615 4.90385Z"
fill="currentColor"
/>
</SvgIcon>
)

View File

@ -0,0 +1,26 @@
import Box from "@material-ui/core/Box"
import { Story } from "@storybook/react"
import React from "react"
import { UserDropdown, UserDropdownProps } from "./UserDropdown"
export default {
title: "Page/UserDropdown",
component: UserDropdown,
argTypes: {
onSignOut: { action: "Sign Out" },
},
}
const Template: Story<UserDropdownProps> = (args: UserDropdownProps) => (
<Box style={{ backgroundColor: "#000", width: 88 }}>
<UserDropdown {...args} />
</Box>
)
export const Example = Template.bind({})
Example.args = {
user: { id: "1", username: "CathyCoder", email: "cathy@coder.com", created_at: "dawn" },
onSignOut: () => {
return Promise.resolve()
},
}

View File

@ -0,0 +1,55 @@
import { screen } from "@testing-library/react"
import React from "react"
import { render } from "../../test_helpers"
import { MockUser } from "../../test_helpers/entities"
import { Language, UserDropdown, UserDropdownProps } from "./UserDropdown"
const renderAndClick = async (props: Partial<UserDropdownProps> = {}) => {
render(<UserDropdown user={props.user ?? MockUser} onSignOut={props.onSignOut ?? jest.fn()} />)
const trigger = await screen.findByTestId("user-dropdown-trigger")
trigger.click()
}
describe("UserDropdown", () => {
describe("when the trigger is clicked", () => {
it("opens the menu", async () => {
await renderAndClick()
expect(screen.getByText(Language.accountLabel)).toBeDefined()
expect(screen.getByText(Language.docsLabel)).toBeDefined()
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
})
})
describe("when the menu is open", () => {
describe("and sign out is clicked", () => {
it("calls the onSignOut function", async () => {
const onSignOut = jest.fn()
await renderAndClick({ onSignOut })
screen.getByText(Language.signOutLabel).click()
expect(onSignOut).toBeCalledTimes(1)
})
})
})
it("has the correct link for the documentation item", async () => {
await renderAndClick()
const link = screen.getByText(Language.docsLabel).closest("a")
if (!link) {
throw new Error("Anchor tag not found for the documentation menu item")
}
expect(link.getAttribute("href")).toBe("https://coder.com/docs")
})
it("has the correct link for the account item", async () => {
await renderAndClick()
const link = screen.getByText(Language.accountLabel).closest("a")
if (!link) {
throw new Error("Anchor tag not found for the account menu item")
}
expect(link.getAttribute("href")).toBe("/preferences")
})
})

View File

@ -4,15 +4,23 @@ import ListItemIcon from "@material-ui/core/ListItemIcon"
import ListItemText from "@material-ui/core/ListItemText"
import MenuItem from "@material-ui/core/MenuItem"
import { fade, makeStyles } from "@material-ui/core/styles"
import AccountIcon from "@material-ui/icons/AccountCircleOutlined"
import KeyboardArrowDown from "@material-ui/icons/KeyboardArrowDown"
import KeyboardArrowUp from "@material-ui/icons/KeyboardArrowUp"
import React, { useState } from "react"
import { Link } from "react-router-dom"
import { UserResponse } from "../../api/types"
import { LogoutIcon } from "../Icons"
import { DocsIcon } from "../Icons/DocsIcon"
import { UserAvatar } from "../User"
import { UserProfileCard } from "../User/UserProfileCard"
import { BorderedMenu } from "./BorderedMenu"
export const Language = {
accountLabel: "Account",
docsLabel: "Documentation",
signOutLabel: "Sign Out",
}
export interface UserDropdownProps {
user: UserResponse
onSignOut: () => void
@ -32,7 +40,7 @@ export const UserDropdown: React.FC<UserDropdownProps> = ({ user, onSignOut }: U
return (
<>
<div>
<MenuItem onClick={handleDropdownClick}>
<MenuItem onClick={handleDropdownClick} data-testid="user-dropdown-trigger">
<div className={styles.inner}>
<Badge overlap="circle">
<UserAvatar username={user.username} />
@ -65,13 +73,31 @@ export const UserDropdown: React.FC<UserDropdownProps> = ({ user, onSignOut }: U
<div className={styles.userInfo}>
<UserProfileCard user={user} />
<Divider className={styles.divider} />
<Divider />
<Link to="/preferences" className={styles.link}>
<MenuItem className={styles.menuItem} onClick={handleDropdownClick}>
<ListItemIcon className={styles.icon}>
<AccountIcon />
</ListItemIcon>
<ListItemText primary={Language.accountLabel} />
</MenuItem>
</Link>
<a href="https://coder.com/docs" target="_blank" rel="noreferrer" className={styles.link}>
<MenuItem className={styles.menuItem} onClick={handleDropdownClick}>
<ListItemIcon className={styles.icon}>
<DocsIcon />
</ListItemIcon>
<ListItemText primary={Language.docsLabel} />
</MenuItem>
</a>
<MenuItem className={styles.menuItem} onClick={onSignOut}>
<ListItemIcon className={styles.icon}>
<LogoutIcon />
</ListItemIcon>
<ListItemText primary="Sign Out" />
<ListItemText primary={Language.signOutLabel} />
</MenuItem>
</div>
</BorderedMenu>
@ -84,6 +110,7 @@ export const useStyles = makeStyles((theme) => ({
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
inner: {
display: "flex",
alignItems: "center",
@ -94,12 +121,14 @@ export const useStyles = makeStyles((theme) => ({
userInfo: {
marginBottom: theme.spacing(1),
},
arrowIcon: {
color: fade(theme.palette.primary.contrastText, 0.7),
marginLeft: theme.spacing(1),
width: 16,
height: 16,
},
arrowIconUp: {
color: theme.palette.primary.contrastText,
},
@ -114,6 +143,11 @@ export const useStyles = makeStyles((theme) => ({
},
},
link: {
textDecoration: "none",
color: "inherit",
},
icon: {
color: theme.palette.text.secondary,
},

View File

@ -0,0 +1,15 @@
import Box from "@material-ui/core/Box"
import Paper from "@material-ui/core/Paper"
import React from "react"
import { Header } from "../../components/Header"
import { Footer } from "../../components/Page"
export const PreferencesPage: React.FC = () => {
return (
<Box display="flex" flexDirection="column">
<Header title="Preferences" />
<Paper style={{ maxWidth: "1380px", margin: "1em auto", width: "100%" }}>Preferences here!</Paper>
<Footer />
</Box>
)
}