From 90388a38f32b80264763a22530f8849a61c62780 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 7 Apr 2022 13:00:40 -0300 Subject: [PATCH] feat: Add user menu (#887) --- site/src/AppRouter.tsx | 12 ++++ site/src/components/Icons/DocsIcon.tsx | 13 +++++ .../Navbar/UserDropdown.stories.tsx | 26 +++++++++ .../components/Navbar/UserDropdown.test.tsx | 55 +++++++++++++++++++ site/src/components/Navbar/UserDropdown.tsx | 40 +++++++++++++- site/src/pages/preferences/index.tsx | 15 +++++ 6 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 site/src/components/Icons/DocsIcon.tsx create mode 100644 site/src/components/Navbar/UserDropdown.stories.tsx create mode 100644 site/src/components/Navbar/UserDropdown.test.tsx create mode 100644 site/src/pages/preferences/index.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 8239d1e9fc..43cd52ba83 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -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 = () => ( /> + + + + + } + /> + + {/* Using path="*"" means "match anything", so this route acts like a catch-all for URLs that we don't have explicit routes for. */} diff --git a/site/src/components/Icons/DocsIcon.tsx b/site/src/components/Icons/DocsIcon.tsx new file mode 100644 index 0000000000..a56579fed5 --- /dev/null +++ b/site/src/components/Icons/DocsIcon.tsx @@ -0,0 +1,13 @@ +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon" +import React from "react" + +export const DocsIcon = (props: SvgIconProps): JSX.Element => ( + + + +) diff --git a/site/src/components/Navbar/UserDropdown.stories.tsx b/site/src/components/Navbar/UserDropdown.stories.tsx new file mode 100644 index 0000000000..fb83f3be70 --- /dev/null +++ b/site/src/components/Navbar/UserDropdown.stories.tsx @@ -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 = (args: UserDropdownProps) => ( + + + +) + +export const Example = Template.bind({}) +Example.args = { + user: { id: "1", username: "CathyCoder", email: "cathy@coder.com", created_at: "dawn" }, + onSignOut: () => { + return Promise.resolve() + }, +} diff --git a/site/src/components/Navbar/UserDropdown.test.tsx b/site/src/components/Navbar/UserDropdown.test.tsx new file mode 100644 index 0000000000..0a058b45b9 --- /dev/null +++ b/site/src/components/Navbar/UserDropdown.test.tsx @@ -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 = {}) => { + render() + 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") + }) +}) diff --git a/site/src/components/Navbar/UserDropdown.tsx b/site/src/components/Navbar/UserDropdown.tsx index b5f259efb7..5b9d22b2ac 100644 --- a/site/src/components/Navbar/UserDropdown.tsx +++ b/site/src/components/Navbar/UserDropdown.tsx @@ -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 = ({ user, onSignOut }: U return ( <>
- +
@@ -65,13 +73,31 @@ export const UserDropdown: React.FC = ({ user, onSignOut }: U
- + + + + + + + + + + + + + + + + + + + - +
@@ -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, }, diff --git a/site/src/pages/preferences/index.tsx b/site/src/pages/preferences/index.tsx new file mode 100644 index 0000000000..931784b94c --- /dev/null +++ b/site/src/pages/preferences/index.tsx @@ -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 ( + +
+ Preferences here! +