Skip to content

feat(site): Add Admin Dropdown menu #885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import { NotFoundPage } from "./pages/404"
import { CliAuthenticationPage } from "./pages/cli-auth"
import { HealthzPage } from "./pages/healthz"
import { SignInPage } from "./pages/login"
import { OrganizationsPage } from "./pages/orgs"
import { PreferencesAccountPage } from "./pages/preferences/account"
import { PreferencesLinkedAccountsPage } from "./pages/preferences/linked-accounts"
import { PreferencesSecurityPage } from "./pages/preferences/security"
import { PreferencesSSHKeysPage } from "./pages/preferences/ssh-keys"
import { SettingsPage } from "./pages/settings"
import { TemplatesPage } from "./pages/templates"
import { TemplatePage } from "./pages/templates/[organization]/[template]"
import { CreateWorkspacePage } from "./pages/templates/[organization]/[template]/create"
import { UsersPage } from "./pages/users"
import { WorkspacePage } from "./pages/workspaces/[workspace]"

export const AppRouter: React.FC = () => (
Expand Down Expand Up @@ -72,6 +75,10 @@ export const AppRouter: React.FC = () => (
/>
</Route>

<Route path="users" element={<UsersPage />} />
<Route path="orgs" element={<OrganizationsPage />} />
<Route path="settings" element={<SettingsPage />} />

<Route path="preferences" element={<PreferencesLayout />}>
<Route index element={<Navigate to="account" />} />
<Route path="account" element={<PreferencesAccountPage />} />
Expand Down
30 changes: 30 additions & 0 deletions site/src/components/BorderedMenu/BorderedMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Story } from "@storybook/react"
import React from "react"
import { BorderedMenu, BorderedMenuProps } from "."
import { BuildingIcon } from "../Icons/BuildingIcon"
import { UsersOutlinedIcon } from "../Icons/UsersOutlinedIcon"
import { BorderedMenuRow } from "./BorderedMenuRow"

export default {
title: "Navbar/BorderedMenu",
component: BorderedMenu,
}

const Template: Story<BorderedMenuProps> = (args: BorderedMenuProps) => (
<BorderedMenu {...args}>
<BorderedMenuRow title="Item 1" description="Here's a description" Icon={BuildingIcon} />
<BorderedMenuRow active title="Item 2" description="This BorderedMenuRow is active" Icon={UsersOutlinedIcon} />
</BorderedMenu>
)

export const AdminVariant = Template.bind({})
AdminVariant.args = {
variant: "admin-dropdown",
open: true,
}

export const UserVariant = Template.bind({})
UserVariant.args = {
variant: "user-dropdown",
open: true,
}
144 changes: 144 additions & 0 deletions site/src/components/BorderedMenu/BorderedMenuRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import ListItem from "@material-ui/core/ListItem"
import { makeStyles } from "@material-ui/core/styles"
import SvgIcon from "@material-ui/core/SvgIcon"
import CheckIcon from "@material-ui/icons/Check"
import React from "react"
import { NavLink } from "react-router-dom"
import { ellipsizeText } from "../../util/ellipsizeText"
import { Typography } from "../Typography"

type BorderedMenuRowVariant = "narrow" | "wide"

interface BorderedMenuRowProps {
/** `true` indicates this row is currently selected */
active?: boolean
/** Optional description that appears beneath the title */
description?: string
/** An SvgIcon that will be rendered to the left of the title */
Icon: typeof SvgIcon
/** URL path */
path?: string
/** Required title of this row */
title: string
/** Defaults to `"wide"` */
variant?: BorderedMenuRowVariant
/** Callback fired when this row is clicked */
onClick?: () => void
}

export const BorderedMenuRow: React.FC<BorderedMenuRowProps> = ({
active,
description,
Icon,
path,
title,
variant,
onClick,
}) => {
const styles = useStyles()

const Component = () => (
<ListItem
classes={{ gutters: styles.rootGutters }}
className={styles.root}
onClick={onClick}
data-status={active ? "active" : "inactive"}
>
<div className={styles.content} data-variant={variant}>
<div className={styles.contentTop}>
<Icon className={styles.icon} />
<Typography className={styles.title}>{title}</Typography>
{active && <CheckIcon className={styles.checkMark} />}
</div>

{description && (
<Typography className={styles.description} color="textSecondary" variant="caption">
{ellipsizeText(description)}
</Typography>
)}
</div>
</ListItem>
)

if (path) {
return (
<NavLink to={path} className={styles.link}>
<Component />
</NavLink>
)
}

return <Component />
}

const iconSize = 20

const useStyles = makeStyles((theme) => ({
root: {
cursor: "pointer",
padding: `0 ${theme.spacing(1)}px`,

"&:hover": {
backgroundColor: "unset",
"& $content": {
backgroundColor: theme.palette.background.default,
},
},

"&[data-status='active']": {
color: theme.palette.primary.main,
"& .BorderedMenuRow-description": {
color: theme.palette.text.primary,
},
"& .BorderedMenuRow-icon": {
color: theme.palette.primary.main,
},
},
},
rootGutters: {
padding: `0 ${theme.spacing(1.5)}px`,
},
content: {
borderRadius: 7,
display: "flex",
flexDirection: "column",
padding: theme.spacing(2),
width: 320,

"&[data-variant='narrow']": {
width: 268,
},
},
contentTop: {
alignItems: "center",
display: "flex",
},
icon: {
color: theme.palette.text.secondary,
height: iconSize,
width: iconSize,

"& path": {
fill: theme.palette.text.secondary,
},
},
link: {
textDecoration: "none",
color: "inherit",
},
title: {
fontSize: 16,
fontWeight: 500,
lineHeight: 1.5,
marginLeft: theme.spacing(2),
},
checkMark: {
height: iconSize,
marginLeft: "auto",
width: iconSize,
},
description: {
marginLeft: theme.spacing(4.5),
marginTop: theme.spacing(0.5),
},
}))
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import Popover, { PopoverProps } from "@material-ui/core/Popover"
import { fade, makeStyles } from "@material-ui/core/styles"
import React from "react"

type BorderedMenuVariant = "manage-dropdown" | "user-dropdown"
type BorderedMenuVariant = "admin-dropdown" | "user-dropdown"

type BorderedMenuProps = Omit<PopoverProps, "variant"> & {
export type BorderedMenuProps = Omit<PopoverProps, "variant"> & {
variant?: BorderedMenuVariant
}

Expand All @@ -20,7 +20,14 @@ export const BorderedMenu: React.FC<BorderedMenuProps> = ({ children, variant, .

const useStyles = makeStyles((theme) => ({
root: {
paddingBottom: theme.spacing(1),
"&[data-variant='admin-dropdown'] $paperRoot": {
padding: `${theme.spacing(3)}px 0`,
},

"&[data-variant='user-dropdown'] $paperRoot": {
paddingBottom: theme.spacing(1),
width: 292,
},
},
paperRoot: {
width: "292px",
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/CodeBlock/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Started container user
Using user 'coder' with shell '/bin/bash'`.split("\n")

export default {
title: "CodeBlock/CodeBlock",
title: "Text/CodeBlock",
component: CodeBlock,
argTypes: {
lines: { control: "text", defaultValue: sampleLines },
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/CodeExample/CodeExample.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CodeExample, CodeExampleProps } from "./CodeExample"
const sampleCode = `echo "Hello, world"`

export default {
title: "CodeBlock/CodeExample",
title: "Text/CodeExample",
component: CodeExample,
argTypes: {
code: { control: "string", defaultValue: sampleCode },
Expand Down
13 changes: 13 additions & 0 deletions site/src/components/Icons/BuildingIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
import React from "react"

export const BuildingIcon = (props: SvgIconProps): JSX.Element => (
<SvgIcon {...props} viewBox="0 0 24 24">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.96387 1.15222H17.3405V11.2377H21V22.8479L17.3405 22.8479L10.9364 22.8479H9.36801L2.96387 22.8479V1.15222ZM10.9364 21.2795V19.4498H9.36801V21.2795H4.53223V2.72058H15.7722V11.2377H15.7721V12.806H15.7722V21.2795H10.9364ZM17.3405 12.806V21.2795H19.4317V12.806H17.3405ZM9.10661 4.81173H6.62337V6.38009H9.10661V4.81173ZM11.1978 4.81173H13.681V6.38009H11.1978V4.81173ZM9.10661 8.47124H6.62337V10.0396H9.10661V8.47124ZM11.1978 8.47124H13.681V10.0396H11.1978V8.47124ZM9.10661 12.1307H6.62337V13.6991H9.10661V12.1307ZM11.1978 12.1307H13.681V13.6991H11.1978V12.1307ZM9.10661 15.7903H6.62337V17.3586H9.10661V15.7903ZM11.1978 15.7903H13.681V17.3586H11.1978V15.7903Z"
fill="currentColor"
/>
</SvgIcon>
)
25 changes: 25 additions & 0 deletions site/src/components/Icons/UsersOutlinedIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SvgIcon from "@material-ui/core/SvgIcon"
import React from "react"

export const UsersOutlinedIcon: typeof SvgIcon = (props) => (
<SvgIcon {...props} viewBox="0 0 20 20">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M18.75 18.75H17.5V15.625V15.625C17.498 13.8999 16.1001 12.502 14.375 12.5V11.25C16.7901 11.2527 18.7473 13.2099 18.75 15.625L18.75 18.75Z"
fill="#677693"
/>
<path
d="M13.75 18.75H12.5V15.625V15.6251C12.498 13.9001 11.1002 12.5021 9.3751 12.5H5.625C3.89995 12.502 2.50203 13.9 2.5 15.625V18.75H1.25V15.625L1.25 15.6251C1.25277 13.21 3.20982 11.2529 5.62489 11.25H9.37489C11.79 11.2528 13.7471 13.2099 13.7499 15.625L13.75 18.75Z"
fill="#677693"
/>
<path
d="M12.5 1.25V2.5C14.2259 2.5 15.625 3.89911 15.625 5.625C15.625 7.35089 14.2259 8.75 12.5 8.75V10C14.9162 10 16.875 8.04124 16.875 5.625C16.875 3.20876 14.9162 1.25 12.5 1.25Z"
fill="#677693"
/>
<path
d="M7.5 2.5C9.22589 2.5 10.625 3.89911 10.625 5.625C10.625 7.35089 9.22589 8.75 7.5 8.75C5.77411 8.75 4.375 7.35089 4.375 5.625V5.625C4.375 3.89911 5.77411 2.5 7.5 2.5V2.5ZM7.5 1.25H7.5C5.08376 1.25 3.125 3.20876 3.125 5.625C3.125 8.04124 5.08376 10 7.5 10C9.91624 10 11.875 8.04124 11.875 5.625C11.875 3.20876 9.91624 1.25 7.5 1.25L7.5 1.25Z"
fill="#677693"
/>
</svg>
</SvgIcon>
)
17 changes: 17 additions & 0 deletions site/src/components/Navbar/AdminDropdown/AdminDropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Box from "@material-ui/core/Box"
import { Story } from "@storybook/react"
import React from "react"
import { AdminDropdown } from "."

export default {
title: "Navbar/AdminDropdown",
component: AdminDropdown,
}

const Template: Story = () => (
<Box style={{ backgroundColor: "#000", width: 100 }}>
<AdminDropdown />
</Box>
)

export const Example = Template.bind({})
48 changes: 48 additions & 0 deletions site/src/components/Navbar/AdminDropdown/AdminDropdown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { screen } from "@testing-library/react"
import React from "react"
import { AdminDropdown, Language } from "."
import { history, render } from "../../../test_helpers"

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()
expect(screen.getByText(Language.orgsLabel)).toBeDefined()
expect(screen.getByText(Language.settingsLabel)).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")
})

it("links to the orgs page", async () => {
await renderAndClick()

const usersLink = screen.getByText(Language.orgsLabel).closest("a")
usersLink?.click()

expect(history.location.pathname).toEqual("/orgs")
})

it("links to the settings page", async () => {
await renderAndClick()

const usersLink = screen.getByText(Language.settingsLabel).closest("a")
usersLink?.click()

expect(history.location.pathname).toEqual("/settings")
})
})
Loading