Skip to content

feat: Add user menu on users table #1222

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 3 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 19 additions & 5 deletions site/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,21 @@ export interface TableProps<T> {
* Optional empty state UI when the data is empty
*/
emptyState?: React.ReactElement
/**
* Optional element to render row actions like delete, update, etc
*/
rowMenu?: (data: T) => React.ReactElement
}

export const Table = <T,>({ columns, data, emptyState, title }: TableProps<T>): React.ReactElement => {
export const Table = <T,>({ columns, data, emptyState, title, rowMenu }: TableProps<T>): React.ReactElement => {
const columnNames = columns.map(({ name }) => name)
const body = renderTableBody(data, columns, emptyState)
const body = renderTableBody(data, columns, emptyState, rowMenu)

return (
<MuiTable>
<TableHead>
{title && <TableTitle title={title} />}
<TableHeaders columns={columnNames} />
<TableHeaders columns={columnNames} hasMenu={!!rowMenu} />
</TableHead>
{body}
</MuiTable>
Expand All @@ -60,7 +64,12 @@ export const Table = <T,>({ columns, data, emptyState, title }: TableProps<T>):
/**
* Helper function to render the table data, falling back to an empty state if available
*/
const renderTableBody = <T,>(data: T[], columns: Column<T>[], emptyState?: React.ReactElement) => {
const renderTableBody = <T,>(
data: T[],
columns: Column<T>[],
emptyState?: React.ReactElement,
rowMenu?: (data: T) => React.ReactElement,
) => {
if (data.length > 0) {
const rows = data.map((item: T, index) => {
const cells = columns.map((column) => {
Expand All @@ -70,7 +79,12 @@ const renderTableBody = <T,>(data: T[], columns: Column<T>[], emptyState?: React
return <TableCell key={String(column.key)}>{String(item[column.key]).toString()}</TableCell>
}
})
return <TableRow key={index}>{cells}</TableRow>
return (
<TableRow key={index}>
{cells}
{rowMenu && <TableCell>{rowMenu(item)}</TableCell>}
</TableRow>
)
})
return <TableBody>{rows}</TableBody>
} else {
Expand Down
5 changes: 4 additions & 1 deletion site/src/components/TableHeaders/TableHeaders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import React from "react"

export interface TableHeadersProps {
columns: string[]
hasMenu?: boolean
}

export const TableHeaders: React.FC<TableHeadersProps> = ({ columns }) => {
export const TableHeaders: React.FC<TableHeadersProps> = ({ columns, hasMenu }) => {
const styles = useStyles()
return (
<TableRow className={styles.root}>
Expand All @@ -16,6 +17,8 @@ export const TableHeaders: React.FC<TableHeadersProps> = ({ columns }) => {
{c}
</TableCell>
))}
{/* 1% is a trick to make the table cell width fit the content */}
{hasMenu && <TableCell width="1%" />}
Copy link
Contributor

@greyscaled greyscaled Apr 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside: Added ticket for storybook here: #1224

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to have a storybook for each table component, I think one for "Table" is good enough since it will use all the others. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BrunoQuaresma In general I agree, but I can think of a reason it might be useful to storybook subcomponents: if they take props that make them render differently, a story for the subcomponent will allow you to play with those props and make different stories for them, whereas it's a little harder to do that in a story for just the composite component.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... I think that is ok when the component can be used outside of the "main" component but in this case, the TableHeader should be used inside of the Table component and it also does not have states or multiple behaviors 🤔. But I'm ok with doing the storybook for this component, we can discuss it better in a FE Variety meeting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide against it and close the ticket, no harm is done. Wasn't intending to add context or weight here, just noticed it and made a ticket and moved on.

</TableRow>
)
}
Expand Down
24 changes: 24 additions & 0 deletions site/src/components/TableRowMenu/TableRowMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentMeta, Story } from "@storybook/react"
import React from "react"
import { TableRowMenu, TableRowMenuProps } from "./TableRowMenu"

export default {
title: "components/TableRowMenu",
component: TableRowMenu,
} as ComponentMeta<typeof TableRowMenu>

type DataType = {
id: string
}

const Template: Story<TableRowMenuProps<DataType>> = (args) => <TableRowMenu {...args} />

export const Example = Template.bind({})
Example.args = {
data: { id: "123" },
menuItems: [
{ label: "Suspend", onClick: (data) => alert(data.id) },
{ label: "Update", onClick: (data) => alert(data.id) },
{ label: "Delete", onClick: (data) => alert(data.id) },
],
}
46 changes: 46 additions & 0 deletions site/src/components/TableRowMenu/TableRowMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import IconButton from "@material-ui/core/IconButton"
import Menu, { MenuProps } from "@material-ui/core/Menu"
import MenuItem from "@material-ui/core/MenuItem"
import MoreVertIcon from "@material-ui/icons/MoreVert"
import React from "react"

export interface TableRowMenuProps<TData> {
data: TData
menuItems: Array<{
label: string
onClick: (data: TData) => void
}>
}

export const TableRowMenu = <T,>({ data, menuItems }: TableRowMenuProps<T>): JSX.Element => {
const [anchorEl, setAnchorEl] = React.useState<MenuProps["anchorEl"]>(null)

const handleClick = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget)
}

const handleClose = () => {
setAnchorEl(null)
}

return (
<>
<IconButton size="small" aria-label="more" aria-controls="long-menu" aria-haspopup="true" onClick={handleClick}>
<MoreVertIcon />
</IconButton>
<Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
{menuItems.map((item) => (
<MenuItem
key={item.label}
onClick={() => {
handleClose()
item.onClick(data)
}}
>
{item.label}
</MenuItem>
))}
</Menu>
</>
)
}
24 changes: 23 additions & 1 deletion site/src/components/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import React from "react"
import { UserResponse } from "../../api/types"
import { EmptyState } from "../EmptyState/EmptyState"
import { Column, Table } from "../Table/Table"
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
import { UserCell } from "../UserCell/UserCell"

const Language = {
pageTitle: "Users",
usersTitle: "All users",
emptyMessage: "No users found",
usernameLabel: "User",
suspendMenuItem: "Suspend",
}

const emptyState = <EmptyState message={Language.emptyMessage} />
Expand All @@ -28,5 +30,25 @@ export interface UsersTableProps {
}

export const UsersTable: React.FC<UsersTableProps> = ({ users }) => {
return <Table columns={columns} data={users} title={Language.usersTitle} emptyState={emptyState} />
return (
<Table
columns={columns}
data={users}
title={Language.usersTitle}
emptyState={emptyState}
rowMenu={(user) => (
<TableRowMenu
data={user}
menuItems={[
{
label: Language.suspendMenuItem,
onClick: () => {
// TO-DO: Add suspend action here
},
},
]}
/>
)}
/>
)
}