mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
@@ -1,17 +0,0 @@
|
||||
import Box from "@material-ui/core/Box"
|
||||
import { Story } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { AdminDropdown } from "./AdminDropdown"
|
||||
|
||||
export default {
|
||||
title: "components/AdminDropdown",
|
||||
component: AdminDropdown,
|
||||
}
|
||||
|
||||
const Template: Story = () => (
|
||||
<Box style={{ backgroundColor: "#000", width: 100 }}>
|
||||
<AdminDropdown />
|
||||
</Box>
|
||||
)
|
||||
|
||||
export const Example = Template.bind({})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import React from "react"
|
||||
import { history, render } from "../../testHelpers/renderHelpers"
|
||||
import { AdminDropdown, Language } from "./AdminDropdown"
|
||||
|
||||
const renderAndClick = async () => {
|
||||
render(<AdminDropdown />)
|
||||
const trigger = await screen.findByText(Language.menuTitle)
|
||||
trigger.click()
|
||||
}
|
||||
|
||||
describe("AdminDropdown", () => {
|
||||
describe("when the trigger is clicked", () => {
|
||||
it("opens the menu", async () => {
|
||||
await renderAndClick()
|
||||
expect(screen.getByText(Language.usersLabel)).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it("links to the users page", async () => {
|
||||
await renderAndClick()
|
||||
|
||||
const usersLink = screen.getByText(Language.usersLabel).closest("a")
|
||||
usersLink?.click()
|
||||
|
||||
expect(history.location.pathname).toEqual("/users")
|
||||
})
|
||||
})
|
||||
@@ -1,137 +0,0 @@
|
||||
import ListItem from "@material-ui/core/ListItem"
|
||||
import ListItemText from "@material-ui/core/ListItemText"
|
||||
import { fade, makeStyles, Theme } from "@material-ui/core/styles"
|
||||
import React, { useState } from "react"
|
||||
import { navHeight } from "../../theme/constants"
|
||||
import { BorderedMenu } from "../BorderedMenu/BorderedMenu"
|
||||
import { BorderedMenuRow } from "../BorderedMenuRow/BorderedMenuRow"
|
||||
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
|
||||
import { UsersOutlinedIcon } from "../Icons/UsersOutlinedIcon"
|
||||
|
||||
export const Language = {
|
||||
menuTitle: "Admin",
|
||||
usersLabel: "Users",
|
||||
usersDescription: "Manage users, roles, and permissions.",
|
||||
}
|
||||
|
||||
const entries = [
|
||||
{
|
||||
label: Language.usersLabel,
|
||||
description: Language.usersDescription,
|
||||
path: "/users",
|
||||
Icon: UsersOutlinedIcon,
|
||||
},
|
||||
]
|
||||
|
||||
export const AdminDropdown: React.FC = () => {
|
||||
const styles = useStyles()
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement>()
|
||||
const onClose = () => setAnchorEl(undefined)
|
||||
const onOpenAdminMenu = (ev: React.MouseEvent<HTMLDivElement>) => setAnchorEl(ev.currentTarget)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.link}>
|
||||
<ListItem selected={Boolean(anchorEl)} button onClick={onOpenAdminMenu}>
|
||||
<ListItemText className="no-brace" primary={Language.menuTitle} />
|
||||
{anchorEl ? <CloseDropdown /> : <OpenDropdown />}
|
||||
</ListItem>
|
||||
</div>
|
||||
|
||||
<BorderedMenu
|
||||
anchorEl={anchorEl}
|
||||
getContentAnchorEl={null}
|
||||
open={!!anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
marginThreshold={0}
|
||||
variant="admin-dropdown"
|
||||
onClose={onClose}
|
||||
>
|
||||
{entries.map((entry) => (
|
||||
<BorderedMenuRow
|
||||
description={entry.description}
|
||||
Icon={entry.Icon}
|
||||
key={entry.label}
|
||||
path={entry.path}
|
||||
title={entry.label}
|
||||
variant="narrow"
|
||||
onClick={() => {
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</BorderedMenu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
link: {
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
|
||||
"& .MuiListItem-button": {
|
||||
background: fade(theme.palette.primary.light, 0.1),
|
||||
},
|
||||
},
|
||||
|
||||
"& .MuiListItemText-root": {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
},
|
||||
"& .feature-stage-chip": {
|
||||
position: "absolute",
|
||||
bottom: theme.spacing(1),
|
||||
|
||||
"& .MuiChip-labelSmall": {
|
||||
fontSize: "10px",
|
||||
},
|
||||
},
|
||||
whiteSpace: "nowrap",
|
||||
"& .MuiListItem-button": {
|
||||
height: navHeight,
|
||||
color: "#A7A7A7",
|
||||
padding: `0 ${theme.spacing(3)}px`,
|
||||
|
||||
"&.Mui-selected": {
|
||||
background: "transparent",
|
||||
"& .MuiListItemText-root": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
|
||||
"&:not(.no-brace) .MuiTypography-root": {
|
||||
position: "relative",
|
||||
|
||||
"&::before": {
|
||||
content: `"{"`,
|
||||
left: -14,
|
||||
position: "absolute",
|
||||
},
|
||||
"&::after": {
|
||||
content: `"}"`,
|
||||
position: "absolute",
|
||||
right: -14,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"&.Mui-focusVisible, &:hover": {
|
||||
background: "#333",
|
||||
},
|
||||
|
||||
"& .MuiListItemText-primary": {
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -1,40 +0,0 @@
|
||||
import { screen, waitFor } from "@testing-library/react"
|
||||
import React from "react"
|
||||
import * as API from "../../api/api"
|
||||
import { renderWithAuth } from "../../testHelpers/renderHelpers"
|
||||
import { checks } from "../../xServices/auth/authXService"
|
||||
import { Language as AdminDropdownLanguage } from "../AdminDropdown/AdminDropdown"
|
||||
import { Navbar } from "./Navbar"
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
describe("Navbar", () => {
|
||||
describe("when user has permission to update users", () => {
|
||||
it("displays the admin menu", async () => {
|
||||
const checkUserPermissionsSpy = jest.spyOn(API, "checkUserPermissions").mockResolvedValueOnce({
|
||||
[checks.updateUsers]: true,
|
||||
})
|
||||
|
||||
renderWithAuth(<Navbar />)
|
||||
|
||||
// Wait for the request is done
|
||||
await waitFor(() => expect(checkUserPermissionsSpy).toBeCalledTimes(1))
|
||||
await screen.findByRole("button", { name: AdminDropdownLanguage.menuTitle })
|
||||
})
|
||||
})
|
||||
|
||||
describe("when user has NO permission to update users", () => {
|
||||
it("does not display the admin menu", async () => {
|
||||
const checkUserPermissionsSpy = jest.spyOn(API, "checkUserPermissions").mockResolvedValueOnce({
|
||||
[checks.updateUsers]: false,
|
||||
})
|
||||
renderWithAuth(<Navbar />)
|
||||
|
||||
// Wait for the request is done
|
||||
await waitFor(() => expect(checkUserPermissionsSpy).toBeCalledTimes(1))
|
||||
expect(screen.queryByRole("button", { name: AdminDropdownLanguage.menuTitle })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useActor, useSelector } from "@xstate/react"
|
||||
import { useActor } from "@xstate/react"
|
||||
import React, { useContext } from "react"
|
||||
import { selectPermissions } from "../../xServices/auth/authSelectors"
|
||||
import { XServiceContext } from "../../xServices/StateContext"
|
||||
import { NavbarView } from "../NavbarView/NavbarView"
|
||||
|
||||
@@ -8,11 +7,7 @@ export const Navbar: React.FC = () => {
|
||||
const xServices = useContext(XServiceContext)
|
||||
const [authState, authSend] = useActor(xServices.authXService)
|
||||
const { me } = authState.context
|
||||
const permissions = useSelector(xServices.authXService, selectPermissions)
|
||||
// When we have more options in the admin dropdown we may want to check this
|
||||
// for more permissions
|
||||
const displayAdminDropdown = !!permissions?.updateUsers
|
||||
const onSignOut = () => authSend("SIGN_OUT")
|
||||
|
||||
return <NavbarView user={me} onSignOut={onSignOut} displayAdminDropdown={displayAdminDropdown} />
|
||||
return <NavbarView user={me} onSignOut={onSignOut} />
|
||||
}
|
||||
|
||||
@@ -10,26 +10,26 @@ describe("NavbarView", () => {
|
||||
}
|
||||
it("renders content", async () => {
|
||||
// When
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} displayAdminDropdown />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
|
||||
// Then
|
||||
await screen.findAllByText("Coder", { exact: false })
|
||||
})
|
||||
|
||||
it("workspaces nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} displayAdminDropdown />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
const workspacesLink = await screen.findByText(navLanguage.workspaces)
|
||||
expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces")
|
||||
})
|
||||
|
||||
it("templates nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} displayAdminDropdown />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
const templatesLink = await screen.findByText(navLanguage.templates)
|
||||
expect((templatesLink as HTMLAnchorElement).href).toContain("/templates")
|
||||
})
|
||||
|
||||
it("users nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} displayAdminDropdown />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
const userLink = await screen.findByText(navLanguage.users)
|
||||
expect((userLink as HTMLAnchorElement).href).toContain("/users")
|
||||
})
|
||||
@@ -42,7 +42,7 @@ describe("NavbarView", () => {
|
||||
}
|
||||
|
||||
// When
|
||||
render(<NavbarView user={mockUser} onSignOut={noop} displayAdminDropdown />)
|
||||
render(<NavbarView user={mockUser} onSignOut={noop} />)
|
||||
|
||||
// Then
|
||||
// There should be a 'B' avatar!
|
||||
|
||||
@@ -5,14 +5,12 @@ import React from "react"
|
||||
import { NavLink } from "react-router-dom"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { navHeight } from "../../theme/constants"
|
||||
import { AdminDropdown } from "../AdminDropdown/AdminDropdown"
|
||||
import { Logo } from "../Icons/Logo"
|
||||
import { UserDropdown } from "../UserDropdown/UsersDropdown"
|
||||
|
||||
export interface NavbarViewProps {
|
||||
user?: TypesGen.User
|
||||
onSignOut: () => void
|
||||
displayAdminDropdown: boolean
|
||||
}
|
||||
|
||||
export const Language = {
|
||||
@@ -21,7 +19,7 @@ export const Language = {
|
||||
users: "Users",
|
||||
}
|
||||
|
||||
export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut, displayAdminDropdown }) => {
|
||||
export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut }) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<nav className={styles.root}>
|
||||
@@ -48,7 +46,6 @@ export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut, display
|
||||
</ListItem>
|
||||
</List>
|
||||
<div className={styles.fullWidth} />
|
||||
{displayAdminDropdown && <AdminDropdown />}
|
||||
<div className={styles.fixed}>{user && <UserDropdown user={user} onSignOut={onSignOut} />}</div>
|
||||
</nav>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user