Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3f21ea4

Browse files
authored
feat(site): Add Admin Dropdown menu (#885)
* Start porting components for Admin menu * More porting, wip * Add icons * Extract arrow components, navHeight * Add Admin Dropdown * Format * Delete types * Fix styles * Lint * Add stub pages * Use navHeight constant * Move files * Add and organize stories * Storybook and organize text stories * Add test * Lint * Lint * Fix double navigation * Lint * Wrap new routes in AuthAndNav * Undo unrelated storybook changes * Refactor according to conventions
1 parent 4c1ef38 commit 3f21ea4

28 files changed

+674
-45
lines changed

site/src/AppRouter.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import { NotFoundPage } from "./pages/404"
77
import { CliAuthenticationPage } from "./pages/cli-auth"
88
import { HealthzPage } from "./pages/healthz"
99
import { SignInPage } from "./pages/login"
10+
import { OrganizationsPage } from "./pages/orgs"
1011
import { PreferencesAccountPage } from "./pages/preferences/account"
1112
import { PreferencesLinkedAccountsPage } from "./pages/preferences/linked-accounts"
1213
import { PreferencesSecurityPage } from "./pages/preferences/security"
1314
import { PreferencesSSHKeysPage } from "./pages/preferences/ssh-keys"
15+
import { SettingsPage } from "./pages/settings"
1416
import { TemplatesPage } from "./pages/templates"
1517
import { TemplatePage } from "./pages/templates/[organization]/[template]"
1618
import { CreateWorkspacePage } from "./pages/templates/[organization]/[template]/create"
19+
import { UsersPage } from "./pages/users"
1720
import { WorkspacePage } from "./pages/workspaces/[workspace]"
1821

1922
export const AppRouter: React.FC = () => (
@@ -72,6 +75,31 @@ export const AppRouter: React.FC = () => (
7275
/>
7376
</Route>
7477

78+
<Route
79+
path="users"
80+
element={
81+
<AuthAndNav>
82+
<UsersPage />
83+
</AuthAndNav>
84+
}
85+
/>
86+
<Route
87+
path="orgs"
88+
element={
89+
<AuthAndNav>
90+
<OrganizationsPage />
91+
</AuthAndNav>
92+
}
93+
/>
94+
<Route
95+
path="settings"
96+
element={
97+
<AuthAndNav>
98+
<SettingsPage />
99+
</AuthAndNav>
100+
}
101+
/>
102+
75103
<Route path="preferences" element={<PreferencesLayout />}>
76104
<Route path="account" element={<PreferencesAccountPage />} />
77105
<Route path="security" element={<PreferencesSecurityPage />} />
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Box from "@material-ui/core/Box"
2+
import { Story } from "@storybook/react"
3+
import React from "react"
4+
import { AdminDropdown } from "./AdminDropdown"
5+
6+
export default {
7+
title: "components/AdminDropdown",
8+
component: AdminDropdown,
9+
}
10+
11+
const Template: Story = () => (
12+
<Box style={{ backgroundColor: "#000", width: 100 }}>
13+
<AdminDropdown />
14+
</Box>
15+
)
16+
17+
export const Example = Template.bind({})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { screen } from "@testing-library/react"
2+
import React from "react"
3+
import { history, render } from "../../test_helpers"
4+
import { AdminDropdown, Language } from "./AdminDropdown"
5+
6+
const renderAndClick = async () => {
7+
render(<AdminDropdown />)
8+
const trigger = await screen.findByText(Language.menuTitle)
9+
trigger.click()
10+
}
11+
12+
describe("AdminDropdown", () => {
13+
describe("when the trigger is clicked", () => {
14+
it("opens the menu", async () => {
15+
await renderAndClick()
16+
expect(screen.getByText(Language.usersLabel)).toBeDefined()
17+
expect(screen.getByText(Language.orgsLabel)).toBeDefined()
18+
expect(screen.getByText(Language.settingsLabel)).toBeDefined()
19+
})
20+
})
21+
22+
it("links to the users page", async () => {
23+
await renderAndClick()
24+
25+
const usersLink = screen.getByText(Language.usersLabel).closest("a")
26+
usersLink?.click()
27+
28+
expect(history.location.pathname).toEqual("/users")
29+
})
30+
31+
it("links to the orgs page", async () => {
32+
await renderAndClick()
33+
34+
const usersLink = screen.getByText(Language.orgsLabel).closest("a")
35+
usersLink?.click()
36+
37+
expect(history.location.pathname).toEqual("/orgs")
38+
})
39+
40+
it("links to the settings page", async () => {
41+
await renderAndClick()
42+
43+
const usersLink = screen.getByText(Language.settingsLabel).closest("a")
44+
usersLink?.click()
45+
46+
expect(history.location.pathname).toEqual("/settings")
47+
})
48+
})
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import ListItem from "@material-ui/core/ListItem"
2+
import ListItemText from "@material-ui/core/ListItemText"
3+
import { fade, makeStyles, Theme } from "@material-ui/core/styles"
4+
import AdminIcon from "@material-ui/icons/SettingsOutlined"
5+
import React, { useState } from "react"
6+
import { navHeight } from "../../theme/constants"
7+
import { BorderedMenu } from "../BorderedMenu/BorderedMenu"
8+
import { BorderedMenuRow } from "../BorderedMenuRow/BorderedMenuRow"
9+
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
10+
import { BuildingIcon } from "../Icons/BuildingIcon"
11+
import { UsersOutlinedIcon } from "../Icons/UsersOutlinedIcon"
12+
13+
export const Language = {
14+
menuTitle: "Admin",
15+
usersLabel: "Users",
16+
usersDescription: "Manage users, roles, and permissions.",
17+
orgsLabel: "Organizations",
18+
orgsDescription: "Manage organizations.",
19+
settingsLabel: "Settings",
20+
settingsDescription: "Configure authentication and more.",
21+
}
22+
23+
const entries = [
24+
{
25+
label: Language.usersLabel,
26+
description: Language.usersDescription,
27+
path: "/users",
28+
Icon: UsersOutlinedIcon,
29+
},
30+
{
31+
label: Language.orgsLabel,
32+
description: Language.orgsDescription,
33+
path: "/orgs",
34+
Icon: BuildingIcon,
35+
},
36+
{
37+
label: Language.settingsLabel,
38+
description: Language.settingsDescription,
39+
path: "/settings",
40+
Icon: AdminIcon,
41+
},
42+
]
43+
44+
export const AdminDropdown: React.FC = () => {
45+
const styles = useStyles()
46+
const [anchorEl, setAnchorEl] = useState<HTMLElement>()
47+
const onClose = () => setAnchorEl(undefined)
48+
const onOpenAdminMenu = (ev: React.MouseEvent<HTMLDivElement>) => setAnchorEl(ev.currentTarget)
49+
50+
return (
51+
<>
52+
<div className={styles.link}>
53+
<ListItem selected={Boolean(anchorEl)} button onClick={onOpenAdminMenu}>
54+
<ListItemText className="no-brace" color="primary" primary={Language.menuTitle} />
55+
{anchorEl ? <CloseDropdown /> : <OpenDropdown />}
56+
</ListItem>
57+
</div>
58+
59+
<BorderedMenu
60+
anchorEl={anchorEl}
61+
getContentAnchorEl={null}
62+
open={!!anchorEl}
63+
anchorOrigin={{
64+
vertical: "bottom",
65+
horizontal: "center",
66+
}}
67+
transformOrigin={{
68+
vertical: "top",
69+
horizontal: "center",
70+
}}
71+
marginThreshold={0}
72+
variant="admin-dropdown"
73+
onClose={onClose}
74+
>
75+
{entries.map((entry) => (
76+
<BorderedMenuRow
77+
description={entry.description}
78+
Icon={entry.Icon}
79+
key={entry.label}
80+
path={entry.path}
81+
title={entry.label}
82+
variant="narrow"
83+
onClick={() => {
84+
onClose()
85+
}}
86+
/>
87+
))}
88+
</BorderedMenu>
89+
</>
90+
)
91+
}
92+
93+
const useStyles = makeStyles((theme: Theme) => ({
94+
link: {
95+
"&:focus": {
96+
outline: "none",
97+
98+
"& .MuiListItem-button": {
99+
background: fade(theme.palette.primary.light, 0.1),
100+
},
101+
},
102+
103+
"& .MuiListItemText-root": {
104+
display: "flex",
105+
flexDirection: "column",
106+
alignItems: "center",
107+
},
108+
"& .feature-stage-chip": {
109+
position: "absolute",
110+
bottom: theme.spacing(1),
111+
112+
"& .MuiChip-labelSmall": {
113+
fontSize: "10px",
114+
},
115+
},
116+
whiteSpace: "nowrap",
117+
"& .MuiListItem-button": {
118+
height: navHeight,
119+
color: "#A7A7A7",
120+
padding: `0 ${theme.spacing(3)}px`,
121+
122+
"&.Mui-selected": {
123+
background: "transparent",
124+
"& .MuiListItemText-root": {
125+
color: theme.palette.primary.contrastText,
126+
127+
"&:not(.no-brace) .MuiTypography-root": {
128+
position: "relative",
129+
130+
"&::before": {
131+
content: `"{"`,
132+
left: -14,
133+
position: "absolute",
134+
},
135+
"&::after": {
136+
content: `"}"`,
137+
position: "absolute",
138+
right: -14,
139+
},
140+
},
141+
},
142+
},
143+
144+
"&.Mui-focusVisible, &:hover": {
145+
background: "#333",
146+
},
147+
148+
"& .MuiListItemText-primary": {
149+
fontFamily: theme.typography.fontFamily,
150+
fontSize: 16,
151+
fontWeight: 500,
152+
},
153+
},
154+
},
155+
}))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Story } from "@storybook/react"
2+
import React from "react"
3+
import { BorderedMenuRow } from "../BorderedMenuRow/BorderedMenuRow"
4+
import { BuildingIcon } from "../Icons/BuildingIcon"
5+
import { UsersOutlinedIcon } from "../Icons/UsersOutlinedIcon"
6+
import { BorderedMenu, BorderedMenuProps } from "./BorderedMenu"
7+
8+
export default {
9+
title: "components/BorderedMenu",
10+
component: BorderedMenu,
11+
}
12+
13+
const Template: Story<BorderedMenuProps> = (args: BorderedMenuProps) => (
14+
<BorderedMenu {...args}>
15+
<BorderedMenuRow title="Item 1" description="Here's a description" Icon={BuildingIcon} />
16+
<BorderedMenuRow active title="Item 2" description="This BorderedMenuRow is active" Icon={UsersOutlinedIcon} />
17+
</BorderedMenu>
18+
)
19+
20+
export const AdminVariant = Template.bind({})
21+
AdminVariant.args = {
22+
variant: "admin-dropdown",
23+
open: true,
24+
}
25+
26+
export const UserVariant = Template.bind({})
27+
UserVariant.args = {
28+
variant: "user-dropdown",
29+
open: true,
30+
}

site/src/components/Navbar/BorderedMenu.tsx renamed to site/src/components/BorderedMenu/BorderedMenu.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import Popover, { PopoverProps } from "@material-ui/core/Popover"
22
import { fade, makeStyles } from "@material-ui/core/styles"
33
import React from "react"
44

5-
type BorderedMenuVariant = "manage-dropdown" | "user-dropdown"
5+
type BorderedMenuVariant = "admin-dropdown" | "user-dropdown"
66

7-
type BorderedMenuProps = Omit<PopoverProps, "variant"> & {
7+
export type BorderedMenuProps = Omit<PopoverProps, "variant"> & {
88
variant?: BorderedMenuVariant
99
}
1010

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

2121
const useStyles = makeStyles((theme) => ({
2222
root: {
23-
paddingBottom: theme.spacing(1),
23+
"&[data-variant='admin-dropdown'] $paperRoot": {
24+
padding: `${theme.spacing(3)}px 0`,
25+
},
26+
27+
"&[data-variant='user-dropdown'] $paperRoot": {
28+
paddingBottom: theme.spacing(1),
29+
width: 292,
30+
},
2431
},
2532
paperRoot: {
2633
width: "292px",

0 commit comments

Comments
 (0)