refactor: Update users page to looks like others (#1850)

This commit is contained in:
Bruno Quaresma
2022-05-27 16:47:11 +00:00
committed by GitHub
parent e2030bba38
commit 7eacab82a2
12 changed files with 149 additions and 262 deletions
@@ -0,0 +1,23 @@
import { Story } from "@storybook/react"
import React from "react"
import { AvatarData, AvatarDataProps } from "./AvatarData"
export default {
title: "components/AvatarData",
component: AvatarData,
}
const Template: Story<AvatarDataProps> = (args: AvatarDataProps) => <AvatarData {...args} />
export const Example = Template.bind({})
Example.args = {
title: "coder",
subtitle: "coder@coder.com",
}
export const WithLink = Template.bind({})
WithLink.args = {
title: "coder",
subtitle: "coder@coder.com",
link: "/users/coder",
}
@@ -0,0 +1,67 @@
import Avatar from "@material-ui/core/Avatar"
import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { Link as RouterLink } from "react-router-dom"
import { combineClasses } from "../../util/combineClasses"
import { firstLetter } from "../../util/firstLetter"
export interface AvatarDataProps {
title: string
subtitle: string
link?: string
}
export const AvatarData: React.FC<AvatarDataProps> = ({ title, subtitle, link }) => {
const styles = useStyles()
return (
<div className={styles.root}>
<Avatar variant="square" className={styles.avatar}>
{firstLetter(title)}
</Avatar>
{link ? (
<Link component={RouterLink} to={link} className={combineClasses([styles.info, styles.link])}>
<b>{title}</b>
<span>{subtitle}</span>
</Link>
) : (
<div className={styles.info}>
<b>{title}</b>
<span>{subtitle}</span>
</div>
)}
</div>
)
}
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
alignItems: "center",
},
avatar: {
borderRadius: 2,
marginRight: theme.spacing(1),
width: 24,
height: 24,
fontSize: 16,
},
info: {
display: "flex",
flexDirection: "column",
color: theme.palette.text.primary,
"& span": {
fontSize: 12,
color: theme.palette.text.secondary,
},
},
link: {
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
},
}))
@@ -1,28 +0,0 @@
import { screen } from "@testing-library/react"
import React from "react"
import { render } from "../../testHelpers/renderHelpers"
import { Header } from "./Header"
describe("Header", () => {
it("renders title and subtitle", async () => {
// When
render(<Header title="Title Test" subTitle="Subtitle Test" />)
// Then
const titleElement = await screen.findByText("Title Test")
expect(titleElement).toBeDefined()
const subTitleElement = await screen.findByText("Subtitle Test")
expect(subTitleElement).toBeDefined()
})
it("renders button if specified", async () => {
// When
render(<Header title="Title" action={{ text: "Button Test" }} />)
// Then
const buttonElement = await screen.findByRole("button")
expect(buttonElement).toBeDefined()
expect(buttonElement.textContent).toEqual("Button Test")
})
})
-118
View File
@@ -1,118 +0,0 @@
import Box from "@material-ui/core/Box"
import { makeStyles } from "@material-ui/core/styles"
import Typography from "@material-ui/core/Typography"
import React from "react"
import { maxWidth, sidePadding } from "../../theme/constants"
import { HeaderButton } from "../HeaderButton/HeaderButton"
export interface HeaderAction {
readonly text: string
readonly onClick?: (event: MouseEvent) => void
}
export interface HeaderProps {
description?: string
title: string
subTitle?: string
action?: HeaderAction
}
export const Header: React.FC<HeaderProps> = ({ description, title, subTitle, action }) => {
const styles = useStyles()
return (
<div className={styles.root}>
<div className={styles.top}>
<div className={styles.topInner}>
<Box display="flex" flexDirection="column" minWidth={0}>
<div>
<Box display="flex" alignItems="center">
<Typography variant="h3" className={styles.title}>
<Box component="span" maxWidth="100%" overflow="hidden" textOverflow="ellipsis">
{title}
</Box>
</Typography>
{subTitle && (
<div className={styles.subtitle}>
<Typography style={{ fontSize: 16 }}>{subTitle}</Typography>
</div>
)}
</Box>
{description && (
<Typography variant="caption" className={styles.description}>
{description}
</Typography>
)}
</div>
</Box>
{action && (
<>
<div className={styles.actions}>
<HeaderButton key={action.text} {...action} />
</div>
</>
)}
</div>
</div>
</div>
)
}
const secondaryText = "#B5BFD2"
const useStyles = makeStyles((theme) => ({
root: {},
top: {
position: "relative",
display: "flex",
alignItems: "center",
height: 126,
background: theme.palette.background.default,
boxShadow: theme.shadows[3],
},
topInner: {
display: "flex",
alignItems: "center",
maxWidth,
margin: "0 auto",
flex: 1,
height: 68,
minWidth: 0,
padding: `0 ${sidePadding}`,
},
title: {
display: "flex",
alignItems: "center",
fontWeight: "bold",
whiteSpace: "nowrap",
minWidth: 0,
color: theme.palette.primary.contrastText,
},
description: {
display: "block",
marginTop: theme.spacing(1) / 2,
marginBottom: -26,
color: secondaryText,
},
subtitle: {
position: "relative",
top: 2,
display: "flex",
alignItems: "center",
borderLeft: `1px solid ${theme.palette.divider}`,
height: 28,
marginLeft: 16,
paddingLeft: 16,
color: secondaryText,
},
actions: {
paddingLeft: "50px",
paddingRight: 0,
flex: 1,
display: "flex",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
},
}))
@@ -1,35 +0,0 @@
import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
export interface HeaderButtonProps {
readonly text: string
readonly disabled?: boolean
readonly onClick?: (event: MouseEvent) => void
}
export const HeaderButton: React.FC<HeaderButtonProps> = (props) => {
const styles = useStyles()
return (
<Button
className={styles.pageButton}
variant="contained"
onClick={(event: React.MouseEvent): void => {
if (props.onClick) {
props.onClick(event.nativeEvent)
}
}}
disabled={props.disabled}
component="button"
>
{props.text}
</Button>
)
}
const useStyles = makeStyles(() => ({
pageButton: {
whiteSpace: "nowrap",
},
}))
@@ -55,5 +55,10 @@ const useStyles = makeStyles((theme: Theme) => ({
// Set a fixed width for the select. It avoids selects having different sizes
// depending on how many roles they have selected.
width: theme.spacing(25),
"& .MuiSelect-root": {
// Adjusting padding because it does not have label
paddingTop: theme.spacing(1.5),
paddingBottom: theme.spacing(1.5),
},
},
}))
@@ -6,11 +6,11 @@ import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import React from "react"
import * as TypesGen from "../../api/typesGenerated"
import { AvatarData } from "../AvatarData/AvatarData"
import { EmptyState } from "../EmptyState/EmptyState"
import { RoleSelect } from "../RoleSelect/RoleSelect"
import { TableLoader } from "../TableLoader/TableLoader"
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
import { UserCell } from "../UserCell/UserCell"
export const Language = {
pageTitle: "Users",
@@ -60,7 +60,7 @@ export const UsersTable: React.FC<UsersTableProps> = ({
users.map((u) => (
<TableRow key={u.id}>
<TableCell>
<UserCell Avatar={{ username: u.username }} primaryText={u.username} caption={u.email} />{" "}
<AvatarData title={u.username} subtitle={u.email} />
</TableCell>
<TableCell>
{canEditUsers ? (
@@ -1,5 +1,3 @@
import Avatar from "@material-ui/core/Avatar"
import Box from "@material-ui/core/Box"
import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import Table from "@material-ui/core/Table"
@@ -12,10 +10,10 @@ import relativeTime from "dayjs/plugin/relativeTime"
import React from "react"
import { Link as RouterLink } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { AvatarData } from "../../components/AvatarData/AvatarData"
import { Margins } from "../../components/Margins/Margins"
import { Stack } from "../../components/Stack/Stack"
import { TableLoader } from "../../components/TableLoader/TableLoader"
import { firstLetter } from "../../util/firstLetter"
dayjs.extend(relativeTime)
@@ -73,15 +71,11 @@ export const TemplatesPageView: React.FC<TemplatesPageViewProps> = (props) => {
{props.templates?.map((template) => (
<TableRow key={template.id}>
<TableCell>
<Box alignItems="center" display="flex">
<Avatar variant="square" className={styles.templateAvatar}>
{firstLetter(template.name)}
</Avatar>
<Link component={RouterLink} to={`/templates/${template.name}`} className={styles.templateLink}>
<b>{template.name}</b>
<span>{template.description}</span>
</Link>
</Box>
<AvatarData
title={template.name}
subtitle={template.description}
link={`/templates/${template.name}`}
/>
</TableCell>
<TableCell>{Language.developerCount(template.workspace_owner_count)}</TableCell>
@@ -114,24 +108,4 @@ const useStyles = makeStyles((theme) => ({
lineHeight: `${theme.spacing(3)}px`,
},
},
templateAvatar: {
borderRadius: 2,
marginRight: theme.spacing(1),
width: 24,
height: 24,
fontSize: 16,
},
templateLink: {
display: "flex",
flexDirection: "column",
color: theme.palette.text.primary,
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
"& span": {
fontSize: 12,
color: theme.palette.text.secondary,
},
},
}))
+6 -6
View File
@@ -103,13 +103,13 @@ describe("Users Page", () => {
expect(users.length).toEqual(2)
})
it("shows 'New user' button to an authorized user", () => {
it("shows 'Create user' button to an authorized user", () => {
render(<UsersPage />)
const newUserButton = screen.queryByText(UsersViewLanguage.newUserButton)
expect(newUserButton).toBeDefined()
const createUserButton = screen.queryByText(UsersViewLanguage.createButton)
expect(createUserButton).toBeDefined()
})
it("does not show 'New user' button to unauthorized user", () => {
it("does not show 'Create user' button to unauthorized user", () => {
server.use(
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => {
const permissions = Object.keys(permissionsToCheck)
@@ -125,8 +125,8 @@ describe("Users Page", () => {
}),
)
render(<UsersPage />)
const newUserButton = screen.queryByText(UsersViewLanguage.newUserButton)
expect(newUserButton).toBeNull()
const createUserButton = screen.queryByText(UsersViewLanguage.createButton)
expect(createUserButton).toBeNull()
})
describe("suspend user", () => {
+28 -4
View File
@@ -1,14 +1,16 @@
import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
import React from "react"
import * as TypesGen from "../../api/typesGenerated"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
import { Header } from "../../components/Header/Header"
import { Margins } from "../../components/Margins/Margins"
import { Stack } from "../../components/Stack/Stack"
import { UsersTable } from "../../components/UsersTable/UsersTable"
export const Language = {
pageTitle: "Users",
newUserButton: "New user",
createButton: "New user",
}
export interface UsersPageViewProps {
@@ -38,11 +40,20 @@ export const UsersPageView: React.FC<UsersPageViewProps> = ({
canCreateUser,
isLoading,
}) => {
const newUserAction = canCreateUser ? { text: Language.newUserButton, onClick: openUserCreationDialog } : undefined
const styles = useStyles()
return (
<Stack spacing={4}>
<Header title={Language.pageTitle} action={newUserAction} />
<Margins>
<div className={styles.actions}>
<div>
{canCreateUser && (
<Button onClick={openUserCreationDialog} startIcon={<AddCircleOutline />}>
{Language.createButton}
</Button>
)}
</div>
</div>
{error ? (
<ErrorSummary error={error} />
) : (
@@ -61,3 +72,16 @@ export const UsersPageView: React.FC<UsersPageViewProps> = ({
</Stack>
)
}
const useStyles = makeStyles((theme) => ({
actions: {
marginTop: theme.spacing(3),
marginBottom: theme.spacing(3),
display: "flex",
height: theme.spacing(6),
"& > *": {
marginLeft: "auto",
},
},
}))
@@ -139,8 +139,12 @@ describe("Workspace Page", () => {
it("shows the timeline build", async () => {
await renderWorkspacePage()
const table = await screen.findByTestId("builds-table")
const rows = table.querySelectorAll("tbody > tr")
expect(rows).toHaveLength(MockBuilds.length)
// Wait for the results to be loaded
await waitFor(async () => {
const rows = table.querySelectorAll("tbody > tr")
expect(rows).toHaveLength(MockBuilds.length)
})
})
})
@@ -1,4 +1,3 @@
import Avatar from "@material-ui/core/Avatar"
import Button from "@material-ui/core/Button"
import Link from "@material-ui/core/Link"
import { makeStyles, Theme } from "@material-ui/core/styles"
@@ -14,9 +13,9 @@ import relativeTime from "dayjs/plugin/relativeTime"
import React from "react"
import { Link as RouterLink } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { AvatarData } from "../../components/AvatarData/AvatarData"
import { Margins } from "../../components/Margins/Margins"
import { Stack } from "../../components/Stack/Stack"
import { firstLetter } from "../../util/firstLetter"
import { getDisplayStatus } from "../../util/workspace"
dayjs.extend(relativeTime)
@@ -73,15 +72,11 @@ export const WorkspacesPageView: React.FC<WorkspacesPageViewProps> = (props) =>
return (
<TableRow key={workspace.id}>
<TableCell>
<div className={styles.workspaceName}>
<Avatar variant="square" className={styles.workspaceAvatar}>
{firstLetter(workspace.name)}
</Avatar>
<Link component={RouterLink} to={`/workspaces/${workspace.id}`} className={styles.workspaceLink}>
<b>{workspace.name}</b>
<span>{workspace.owner_name}</span>
</Link>
</div>
<AvatarData
title={workspace.name}
subtitle={workspace.owner_name}
link={`/workspaces/${workspace.id}`}
/>
</TableCell>
<TableCell>{workspace.template_name}</TableCell>
<TableCell>
@@ -134,28 +129,4 @@ const useStyles = makeStyles((theme) => ({
lineHeight: `${theme.spacing(3)}px`,
},
},
workspaceAvatar: {
borderRadius: 2,
marginRight: theme.spacing(1),
width: 24,
height: 24,
fontSize: 16,
},
workspaceName: {
display: "flex",
alignItems: "center",
},
workspaceLink: {
display: "flex",
flexDirection: "column",
color: theme.palette.text.primary,
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
"& span": {
fontSize: 12,
color: theme.palette.text.secondary,
},
},
}))