diff --git a/site/src/components/Table/Table.tsx b/site/src/components/Table/Table.tsx index a854d1ecdb313..e8445464c9378 100644 --- a/site/src/components/Table/Table.tsx +++ b/site/src/components/Table/Table.tsx @@ -40,17 +40,21 @@ export interface TableProps { * 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 = ({ columns, data, emptyState, title }: TableProps): React.ReactElement => { +export const Table = ({ columns, data, emptyState, title, rowMenu }: TableProps): React.ReactElement => { const columnNames = columns.map(({ name }) => name) - const body = renderTableBody(data, columns, emptyState) + const body = renderTableBody(data, columns, emptyState, rowMenu) return ( {title && } - + {body} @@ -60,7 +64,12 @@ export const Table = ({ columns, data, emptyState, title }: TableProps): /** * Helper function to render the table data, falling back to an empty state if available */ -const renderTableBody = (data: T[], columns: Column[], emptyState?: React.ReactElement) => { +const renderTableBody = ( + data: T[], + columns: Column[], + emptyState?: React.ReactElement, + rowMenu?: (data: T) => React.ReactElement, +) => { if (data.length > 0) { const rows = data.map((item: T, index) => { const cells = columns.map((column) => { @@ -70,7 +79,12 @@ const renderTableBody = (data: T[], columns: Column[], emptyState?: React return {String(item[column.key]).toString()} } }) - return {cells} + return ( + + {cells} + {rowMenu && {rowMenu(item)}} + + ) }) return {rows} } else { diff --git a/site/src/components/TableHeaders/TableHeaders.tsx b/site/src/components/TableHeaders/TableHeaders.tsx index f91aa578df37a..6004939e449ba 100644 --- a/site/src/components/TableHeaders/TableHeaders.tsx +++ b/site/src/components/TableHeaders/TableHeaders.tsx @@ -5,9 +5,10 @@ import React from "react" export interface TableHeadersProps { columns: string[] + hasMenu?: boolean } -export const TableHeaders: React.FC = ({ columns }) => { +export const TableHeaders: React.FC = ({ columns, hasMenu }) => { const styles = useStyles() return ( @@ -16,6 +17,8 @@ export const TableHeaders: React.FC = ({ columns }) => { {c} ))} + {/* 1% is a trick to make the table cell width fit the content */} + {hasMenu && } ) } diff --git a/site/src/components/TableRowMenu/TableRowMenu.stories.tsx b/site/src/components/TableRowMenu/TableRowMenu.stories.tsx new file mode 100644 index 0000000000000..39b2aec38c300 --- /dev/null +++ b/site/src/components/TableRowMenu/TableRowMenu.stories.tsx @@ -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 + +type DataType = { + id: string +} + +const Template: Story> = (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) }, + ], +} diff --git a/site/src/components/TableRowMenu/TableRowMenu.tsx b/site/src/components/TableRowMenu/TableRowMenu.tsx new file mode 100644 index 0000000000000..8e10df4e88ba7 --- /dev/null +++ b/site/src/components/TableRowMenu/TableRowMenu.tsx @@ -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 { + data: TData + menuItems: Array<{ + label: string + onClick: (data: TData) => void + }> +} + +export const TableRowMenu = ({ data, menuItems }: TableRowMenuProps): JSX.Element => { + const [anchorEl, setAnchorEl] = React.useState(null) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + return ( + <> + + + + + {menuItems.map((item) => ( + { + handleClose() + item.onClick(data) + }} + > + {item.label} + + ))} + + + ) +} diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx index 764f90dad63d5..3b497e161243a 100644 --- a/site/src/components/UsersTable/UsersTable.tsx +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -2,6 +2,7 @@ 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 = { @@ -9,6 +10,7 @@ const Language = { usersTitle: "All users", emptyMessage: "No users found", usernameLabel: "User", + suspendMenuItem: "Suspend", } const emptyState = @@ -28,5 +30,25 @@ export interface UsersTableProps { } export const UsersTable: React.FC = ({ users }) => { - return + return ( +
( + { + // TO-DO: Add suspend action here + }, + }, + ]} + /> + )} + /> + ) }