Skip to content

Commit

Permalink
feat: add "on this page" to empty table message when you're past page…
Browse files Browse the repository at this point in the history
… 1 (#4886)

* Use empty page message on workspaces page

* Add prop for story

* AuditPage

* UsersPage

* Lint and format

* Fix tests

* Remove log

* Try to fix story

* Fix the right story
  • Loading branch information
presleyp committed Nov 8, 2022
1 parent f496b14 commit 87b3fe1
Show file tree
Hide file tree
Showing 23 changed files with 354 additions and 221 deletions.
2 changes: 1 addition & 1 deletion site/src/components/Conditionals/ChooseOne.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const ChooseOne = ({
}
if (conditionedOptions.some((cond) => cond.props.condition === undefined)) {
throw new Error(
"A non-final Cond in a ChooseOne does not have a condition prop.",
"A non-final Cond in a ChooseOne does not have a condition prop or the prop is undefined.",
)
}
const chosen = conditionedOptions.find((child) => child.props.condition)
Expand Down
6 changes: 6 additions & 0 deletions site/src/components/PaginationWidget/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,9 @@ export const createPaginationRef = (
): PaginationMachineRef => {
return spawn(paginationMachine.withContext(context))
}

export const nonInitialPage = (searchParams: URLSearchParams): boolean => {
const page = searchParams.get("page")
const numberPage = page ? Number(page) : 1
return numberPage > 1
}
15 changes: 10 additions & 5 deletions site/src/components/UsersTable/UsersTable.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ComponentMeta, Story } from "@storybook/react"
import {
MockSiteRoles,
MockAssignableSiteRoles,
MockUser,
MockUser2,
} from "../../testHelpers/renderHelpers"
Expand All @@ -9,34 +9,39 @@ import { UsersTable, UsersTableProps } from "./UsersTable"
export default {
title: "components/UsersTable",
component: UsersTable,
argTypes: {
isNonInitialPage: {
defaultValue: false,
},
},
} as ComponentMeta<typeof UsersTable>

const Template: Story<UsersTableProps> = (args) => <UsersTable {...args} />

export const Example = Template.bind({})
Example.args = {
users: [MockUser, MockUser2],
roles: MockSiteRoles,
roles: MockAssignableSiteRoles,
canEditUsers: false,
}

export const Editable = Template.bind({})
Editable.args = {
users: [MockUser, MockUser2],
roles: MockSiteRoles,
roles: MockAssignableSiteRoles,
canEditUsers: true,
}

export const Empty = Template.bind({})
Empty.args = {
users: [],
roles: MockSiteRoles,
roles: MockAssignableSiteRoles,
}

export const Loading = Template.bind({})
Loading.args = {
users: [],
roles: MockSiteRoles,
roles: MockAssignableSiteRoles,
isLoading: true,
}
Loading.parameters = {
Expand Down
1 change: 1 addition & 0 deletions site/src/components/UsersTable/UsersTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe("AuditPage", () => {
onActivateUser={() => jest.fn()}
onResetUserPassword={() => jest.fn()}
onUpdateUserRoles={() => jest.fn()}
isNonInitialPage={false}
/>,
)

Expand Down
3 changes: 3 additions & 0 deletions site/src/components/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface UsersTableProps {
user: TypesGen.User,
roles: TypesGen.Role["name"][],
) => void
isNonInitialPage: boolean
}

export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
Expand All @@ -46,6 +47,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
isUpdatingUserRoles,
canEditUsers,
isLoading,
isNonInitialPage,
}) => {
return (
<TableContainer>
Expand Down Expand Up @@ -78,6 +80,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
onResetUserPassword={onResetUserPassword}
onSuspendUser={onSuspendUser}
onUpdateUserRoles={onUpdateUserRoles}
isNonInitialPage={isNonInitialPage}
/>
</TableBody>
</Table>
Expand Down
253 changes: 135 additions & 118 deletions site/src/components/UsersTable/UsersTableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import Box from "@material-ui/core/Box"
import { makeStyles } from "@material-ui/core/styles"
import TableCell from "@material-ui/core/TableCell"
import TableRow from "@material-ui/core/TableRow"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { LastUsed } from "components/LastUsed/LastUsed"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import * as TypesGen from "../../api/typesGenerated"
import { combineClasses } from "../../util/combineClasses"
import { AvatarData } from "../AvatarData/AvatarData"
Expand All @@ -12,15 +14,6 @@ import { RoleSelect } from "../RoleSelect/RoleSelect"
import { TableLoader } from "../TableLoader/TableLoader"
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"

export const Language = {
emptyMessage: "No users found",
suspendMenuItem: "Suspend",
deleteMenuItem: "Delete",
listWorkspacesMenuItem: "View workspaces",
activateMenuItem: "Activate",
resetPasswordMenuItem: "Reset password",
}

interface UsersTableBodyProps {
users?: TypesGen.User[]
roles?: TypesGen.AssignableRoles[]
Expand All @@ -36,6 +29,7 @@ interface UsersTableBodyProps {
user: TypesGen.User,
roles: TypesGen.Role["name"][],
) => void
isNonInitialPage: boolean
}

export const UsersTableBody: FC<
Expand All @@ -52,121 +46,144 @@ export const UsersTableBody: FC<
isUpdatingUserRoles,
canEditUsers,
isLoading,
isNonInitialPage,
}) => {
const styles = useStyles()

if (isLoading) {
return <TableLoader />
}

if (!users || users.length === 0) {
return (
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<EmptyState message={Language.emptyMessage} />
</Box>
</TableCell>
</TableRow>
)
}
const { t } = useTranslation("usersPage")

return (
<>
{users.map((user) => {
// When the user has no role we want to show they are a Member
const fallbackRole: TypesGen.Role = {
name: "member",
display_name: "Member",
}
const userRoles = user.roles.length === 0 ? [fallbackRole] : user.roles
<ChooseOne>
<Cond condition={Boolean(isLoading)}>
<TableLoader />
</Cond>
<Cond condition={!users || users.length === 0}>
<ChooseOne>
<Cond condition={isNonInitialPage}>
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<EmptyState message={t("emptyPageMessage")} />
</Box>
</TableCell>
</TableRow>
</Cond>
<Cond>
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<EmptyState message={t("emptyMessage")} />
</Box>
</TableCell>
</TableRow>
</Cond>
</ChooseOne>
</Cond>
<Cond>
<>
{users &&
users.map((user) => {
// When the user has no role we want to show they are a Member
const fallbackRole: TypesGen.Role = {
name: "member",
display_name: "Member",
}
const userRoles =
user.roles.length === 0 ? [fallbackRole] : user.roles

return (
<TableRow key={user.id}>
<TableCell>
<AvatarData
title={user.username}
subtitle={user.email}
highlightTitle
avatar={
user.avatar_url ? (
<img
className={styles.avatar}
alt={`${user.username}'s Avatar`}
src={user.avatar_url}
return (
<TableRow key={user.id}>
<TableCell>
<AvatarData
title={user.username}
subtitle={user.email}
highlightTitle
avatar={
user.avatar_url ? (
<img
className={styles.avatar}
alt={`${user.username}'s Avatar`}
src={user.avatar_url}
/>
) : null
}
/>
) : null
}
/>
</TableCell>
<TableCell
className={combineClasses([
styles.status,
user.status === "suspended" ? styles.suspended : undefined,
])}
>
{user.status}
</TableCell>
<TableCell>
<LastUsed lastUsedAt={user.last_seen_at} />
</TableCell>
<TableCell>
{canEditUsers ? (
<RoleSelect
roles={roles ?? []}
selectedRoles={userRoles}
loading={isUpdatingUserRoles}
onChange={(roles) => {
// Remove the fallback role because it is only for the UI
roles = roles.filter((role) => role !== fallbackRole.name)
onUpdateUserRoles(user, roles)
}}
/>
) : (
<>{userRoles.map((role) => role.display_name).join(", ")}</>
)}
</TableCell>
{canEditUsers && (
<TableCell>
<TableRowMenu
data={user}
menuItems={
// Return either suspend or activate depending on status
(user.status === "active"
? [
{
label: Language.suspendMenuItem,
onClick: onSuspendUser,
},
]
: [
{
label: Language.activateMenuItem,
onClick: onActivateUser,
},
]
).concat(
{
label: Language.deleteMenuItem,
onClick: onDeleteUser,
},
{
label: Language.listWorkspacesMenuItem,
onClick: onListWorkspaces,
},
{
label: Language.resetPasswordMenuItem,
onClick: onResetUserPassword,
},
)
}
/>
</TableCell>
)}
</TableRow>
)
})}
</>
</TableCell>
<TableCell
className={combineClasses([
styles.status,
user.status === "suspended"
? styles.suspended
: undefined,
])}
>
{user.status}
</TableCell>
<TableCell>
<LastUsed lastUsedAt={user.last_seen_at} />
</TableCell>
<TableCell>
{canEditUsers ? (
<RoleSelect
roles={roles ?? []}
selectedRoles={userRoles}
loading={isUpdatingUserRoles}
onChange={(roles) => {
// Remove the fallback role because it is only for the UI
roles = roles.filter(
(role) => role !== fallbackRole.name,
)
onUpdateUserRoles(user, roles)
}}
/>
) : (
<>
{userRoles.map((role) => role.display_name).join(", ")}
</>
)}
</TableCell>
{canEditUsers && (
<TableCell>
<TableRowMenu
data={user}
menuItems={
// Return either suspend or activate depending on status
(user.status === "active"
? [
{
label: t("suspendMenuItem"),
onClick: onSuspendUser,
},
]
: [
{
label: t("activateMenuItem"),
onClick: onActivateUser,
},
]
).concat(
{
label: t("deleteMenuItem"),
onClick: onDeleteUser,
},
{
label: t("listWorkspacesMenuItem"),
onClick: onListWorkspaces,
},
{
label: t("resetPasswordMenuItem"),
onClick: onResetUserPassword,
},
)
}
/>
</TableCell>
)}
</TableRow>
)
})}
</>
</Cond>
</ChooseOne>
)
}

Expand Down

0 comments on commit 87b3fe1

Please sign in to comment.