diff --git a/site/src/components/UserDropdown/UserDropdown.stories.tsx b/site/src/components/UserDropdown/UserDropdown.stories.tsx index 1ba9ee38b7c1a..6b76fa11e4b64 100644 --- a/site/src/components/UserDropdown/UserDropdown.stories.tsx +++ b/site/src/components/UserDropdown/UserDropdown.stories.tsx @@ -17,8 +17,8 @@ const Template: Story = (args: UserDropdownProps) => ( ) -export const ExampleNoRoles = Template.bind({}) -ExampleNoRoles.args = { +export const Example = Template.bind({}) +Example.args = { user: MockUser, onSignOut: () => { return Promise.resolve() diff --git a/site/src/components/UserDropdown/UserDropdown.test.tsx b/site/src/components/UserDropdown/UserDropdown.test.tsx index 6ef615182cff8..6772100e97685 100644 --- a/site/src/components/UserDropdown/UserDropdown.test.tsx +++ b/site/src/components/UserDropdown/UserDropdown.test.tsx @@ -1,7 +1,8 @@ import { screen } from "@testing-library/react" -import { MockAdminRole, MockUser } from "../../testHelpers/entities" +import { MockUser } from "../../testHelpers/entities" import { render } from "../../testHelpers/renderHelpers" -import { Language, UserDropdown, UserDropdownProps } from "./UsersDropdown" +import { Language } from "../UserDropdownContent/UserDropdownContent" +import { UserDropdown, UserDropdownProps } from "./UsersDropdown" const renderAndClick = async (props: Partial = {}) => { render() @@ -10,18 +11,6 @@ const renderAndClick = async (props: Partial = {}) => { } describe("UserDropdown", () => { - const env = process.env - - // REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests - beforeEach(() => { - process.env = { ...env } - }) - - // REMARK: restoring process.env - afterEach(() => { - process.env = env - }) - describe("when the trigger is clicked", () => { it("opens the menu", async () => { await renderAndClick() @@ -30,44 +19,4 @@ describe("UserDropdown", () => { expect(screen.getByText(Language.signOutLabel)).toBeDefined() }) }) - - describe("when the menu is open", () => { - it("displays the user's roles", async () => { - await renderAndClick() - - expect(screen.getByText(MockAdminRole.display_name)).toBeDefined() - }) - - it("has the correct link for the documentation item", async () => { - process.env.CODER_VERSION = "v0.5.4" - 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://github.com/coder/coder/tree/${process.env.CODER_VERSION}/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("/settings/account") - }) - - 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) - }) - }) - }) }) diff --git a/site/src/components/UserDropdown/UsersDropdown.tsx b/site/src/components/UserDropdown/UsersDropdown.tsx index 3e1de2c41162a..7cca88e117c8a 100644 --- a/site/src/components/UserDropdown/UsersDropdown.tsx +++ b/site/src/components/UserDropdown/UsersDropdown.tsx @@ -1,26 +1,14 @@ import Badge from "@material-ui/core/Badge" -import Divider from "@material-ui/core/Divider" -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 React, { useState } from "react" -import { Link } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { navHeight } from "../../theme/constants" import { BorderedMenu } from "../BorderedMenu/BorderedMenu" import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows" -import { DocsIcon } from "../Icons/DocsIcon" -import { LogoutIcon } from "../Icons/LogoutIcon" import { UserAvatar } from "../UserAvatar/UserAvatar" -import { UserProfileCard } from "../UserProfileCard/UserProfileCard" +import { UserDropdownContent } from "../UserDropdownContent/UserDropdownContent" -export const Language = { - accountLabel: "Account", - docsLabel: "Documentation", - signOutLabel: "Sign Out", -} export interface UserDropdownProps { user: TypesGen.User onSignOut: () => void @@ -64,41 +52,7 @@ export const UserDropdown: React.FC = ({ user, onSignOut }: U variant="user-dropdown" onClose={onPopoverClose} > -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ ) @@ -117,10 +71,6 @@ export const useStyles = makeStyles((theme) => ({ maxWidth: 300, }, - userInfo: { - marginBottom: theme.spacing(1), - }, - menuItem: { height: navHeight, padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`, @@ -130,13 +80,4 @@ export const useStyles = makeStyles((theme) => ({ transition: "background-color 0.3s ease", }, }, - - link: { - textDecoration: "none", - color: "inherit", - }, - - icon: { - color: theme.palette.text.secondary, - }, })) diff --git a/site/src/components/UserProfileCard/UserProfileCard.stories.tsx b/site/src/components/UserDropdownContent/UserDropdownContent.stories.tsx similarity index 69% rename from site/src/components/UserProfileCard/UserProfileCard.stories.tsx rename to site/src/components/UserDropdownContent/UserDropdownContent.stories.tsx index 92754f0709fcf..bc09586d17ffa 100644 --- a/site/src/components/UserProfileCard/UserProfileCard.stories.tsx +++ b/site/src/components/UserDropdownContent/UserDropdownContent.stories.tsx @@ -1,17 +1,14 @@ import { Story } from "@storybook/react" import React from "react" import { MockUser } from "../../testHelpers/entities" -import { UserProfileCard, UserProfileCardProps } from "./UserProfileCard" +import { UserDropdownContent, UserDropdownContentProps } from "./UserDropdownContent" export default { - title: "components/UserDropdown", - component: UserProfileCard, - argTypes: { - onSignOut: { action: "Sign Out" }, - }, + title: "components/UserDropdownContent", + component: UserDropdownContent, } -const Template: Story = (args: UserProfileCardProps) => +const Template: Story = (args) => export const ExampleNoRoles = Template.bind({}) ExampleNoRoles.args = { diff --git a/site/src/components/UserDropdownContent/UserDropdownContent.test.tsx b/site/src/components/UserDropdownContent/UserDropdownContent.test.tsx new file mode 100644 index 0000000000000..53b6c2bb7b905 --- /dev/null +++ b/site/src/components/UserDropdownContent/UserDropdownContent.test.tsx @@ -0,0 +1,61 @@ +import { screen } from "@testing-library/react" +import { MockAdminRole, MockUser } from "../../testHelpers/entities" +import { render } from "../../testHelpers/renderHelpers" +import { Language, UserDropdownContent } from "./UserDropdownContent" + +describe("UserDropdownContent", () => { + const env = process.env + + // REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests + beforeEach(() => { + process.env = { ...env } + }) + + // REMARK: restoring process.env + afterEach(() => { + process.env = env + }) + + it("displays the menu items", () => { + render() + expect(screen.getByText(Language.accountLabel)).toBeDefined() + expect(screen.getByText(Language.docsLabel)).toBeDefined() + expect(screen.getByText(Language.signOutLabel)).toBeDefined() + }) + + it("displays the user's roles", () => { + render() + + expect(screen.getByText(MockAdminRole.display_name)).toBeDefined() + }) + + it("has the correct link for the documentation item", () => { + process.env.CODER_VERSION = "v0.5.4" + render() + + 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://github.com/coder/coder/tree/${process.env.CODER_VERSION}/docs`) + }) + + it("has the correct link for the account item", () => { + render() + + 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("/settings/account") + }) + + it("calls the onSignOut function", () => { + const onSignOut = jest.fn() + render() + screen.getByText(Language.signOutLabel).click() + expect(onSignOut).toBeCalledTimes(1) + }) +}) diff --git a/site/src/components/UserDropdownContent/UserDropdownContent.tsx b/site/src/components/UserDropdownContent/UserDropdownContent.tsx new file mode 100644 index 0000000000000..4325631aecc19 --- /dev/null +++ b/site/src/components/UserDropdownContent/UserDropdownContent.tsx @@ -0,0 +1,151 @@ +import Chip from "@material-ui/core/Chip" +import Divider from "@material-ui/core/Divider" +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 Typography from "@material-ui/core/Typography" +import AccountIcon from "@material-ui/icons/AccountCircleOutlined" +import { FC } from "react" +import { Link } from "react-router-dom" +import * as TypesGen from "../../api/typesGenerated" +import { Role } from "../../api/typesGenerated" +import { navHeight } from "../../theme/constants" +import { DocsIcon } from "../Icons/DocsIcon" +import { LogoutIcon } from "../Icons/LogoutIcon" +import { UserAvatar } from "../UserAvatar/UserAvatar" + +export const Language = { + accountLabel: "Account", + docsLabel: "Documentation", + signOutLabel: "Sign Out", +} + +export interface UserDropdownContentProps { + user: TypesGen.User + onPopoverClose: () => void + onSignOut: () => void +} + +export const UserDropdownContent: FC = ({ user, onPopoverClose, onSignOut }) => { + const styles = useStyles() + + return ( +
+
+
+ +
+ {user.username} + {user.email} +
    + {user.roles.map((role: Role) => ( +
  • + +
  • + ))} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + root: { + paddingTop: theme.spacing(3), + textAlign: "center", + }, + avatarContainer: { + width: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + avatar: { + width: 48, + height: 48, + borderRadius: "50%", + marginBottom: theme.spacing(1), + transition: `transform .2s`, + + "&:hover": { + transform: `scale(1.1)`, + }, + }, + userName: { + fontSize: 16, + marginBottom: theme.spacing(0.5), + }, + userEmail: { + fontSize: 14, + letterSpacing: 0.2, + color: theme.palette.text.secondary, + }, + chipContainer: { + display: "flex", + justifyContent: "center", + flexWrap: "wrap", + listStyle: "none", + margin: "0", + padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`, + }, + chipStyles: { + margin: theme.spacing(0.5), + }, + chipRoot: { + backgroundColor: "#7057FF", + }, + link: { + textDecoration: "none", + color: "inherit", + }, + menuItem: { + height: navHeight, + padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`, + + "&:hover": { + backgroundColor: fade(theme.palette.primary.light, 0.1), + transition: "background-color 0.3s ease", + }, + }, + userInfo: { + marginBottom: theme.spacing(1), + }, + icon: { + color: theme.palette.text.secondary, + }, +})) diff --git a/site/src/components/UserProfileCard/UserProfileCard.tsx b/site/src/components/UserProfileCard/UserProfileCard.tsx deleted file mode 100644 index d171ab4b3951d..0000000000000 --- a/site/src/components/UserProfileCard/UserProfileCard.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import Chip from "@material-ui/core/Chip" -import { makeStyles } from "@material-ui/core/styles" -import Typography from "@material-ui/core/Typography" -import { FC } from "react" -import * as TypesGen from "../../api/typesGenerated" -import { Role } from "../../api/typesGenerated" -import { UserAvatar } from "../UserAvatar/UserAvatar" - -export interface UserProfileCardProps { - user: TypesGen.User -} - -export const UserProfileCard: FC = ({ user }) => { - const styles = useStyles() - - return ( -
-
- -
- {user.username} - {user.email} -
    - {user.roles.map((role: Role) => ( -
  • - -
  • - ))} -
-
- ) -} - -const useStyles = makeStyles((theme) => ({ - root: { - paddingTop: theme.spacing(3), - textAlign: "center", - }, - avatarContainer: { - width: "100%", - display: "flex", - alignItems: "center", - justifyContent: "center", - }, - avatar: { - width: 48, - height: 48, - borderRadius: "50%", - marginBottom: theme.spacing(1), - transition: `transform .2s`, - - "&:hover": { - transform: `scale(1.1)`, - }, - }, - userName: { - fontSize: 16, - marginBottom: theme.spacing(0.5), - }, - userEmail: { - fontSize: 14, - letterSpacing: 0.2, - color: theme.palette.text.secondary, - }, - chipContainer: { - display: "flex", - justifyContent: "center", - flexWrap: "wrap", - listStyle: "none", - margin: "0", - padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`, - }, - chipStyles: { - margin: theme.spacing(0.5), - }, - chipRoot: { - backgroundColor: "#7057FF", - }, -}))