mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: add "on this page" to empty table message when you're past page 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
This commit is contained in:
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import {
|
||||
MockSiteRoles,
|
||||
MockAssignableSiteRoles,
|
||||
MockUser,
|
||||
MockUser2,
|
||||
} from "../../testHelpers/renderHelpers"
|
||||
@ -9,6 +9,11 @@ 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} />
|
||||
@ -16,27 +21,27 @@ 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 = {
|
||||
|
@ -15,6 +15,7 @@ describe("AuditPage", () => {
|
||||
onActivateUser={() => jest.fn()}
|
||||
onResetUserPassword={() => jest.fn()}
|
||||
onUpdateUserRoles={() => jest.fn()}
|
||||
isNonInitialPage={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
|
@ -32,6 +32,7 @@ export interface UsersTableProps {
|
||||
user: TypesGen.User,
|
||||
roles: TypesGen.Role["name"][],
|
||||
) => void
|
||||
isNonInitialPage: boolean
|
||||
}
|
||||
|
||||
export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
||||
@ -46,6 +47,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
||||
isUpdatingUserRoles,
|
||||
canEditUsers,
|
||||
isLoading,
|
||||
isNonInitialPage,
|
||||
}) => {
|
||||
return (
|
||||
<TableContainer>
|
||||
@ -78,6 +80,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
||||
onResetUserPassword={onResetUserPassword}
|
||||
onSuspendUser={onSuspendUser}
|
||||
onUpdateUserRoles={onUpdateUserRoles}
|
||||
isNonInitialPage={isNonInitialPage}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
@ -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"
|
||||
@ -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[]
|
||||
@ -36,6 +29,7 @@ interface UsersTableBodyProps {
|
||||
user: TypesGen.User,
|
||||
roles: TypesGen.Role["name"][],
|
||||
) => void
|
||||
isNonInitialPage: boolean
|
||||
}
|
||||
|
||||
export const UsersTableBody: FC<
|
||||
@ -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
|
||||
|
||||
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
|
||||
}
|
||||
/>
|
||||
</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,
|
||||
},
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</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}
|
||||
/>
|
||||
) : 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: 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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -21,11 +21,12 @@ export interface WorkspacesTableProps {
|
||||
isLoading?: boolean
|
||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||
filter?: string
|
||||
isNonInitialPage: boolean
|
||||
}
|
||||
|
||||
export const WorkspacesTable: FC<
|
||||
React.PropsWithChildren<WorkspacesTableProps>
|
||||
> = ({ isLoading, workspaceRefs, filter }) => {
|
||||
> = ({ isLoading, workspaceRefs, filter, isNonInitialPage }) => {
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@ -44,6 +45,7 @@ export const WorkspacesTable: FC<
|
||||
isLoading={isLoading}
|
||||
workspaceRefs={workspaceRefs}
|
||||
filter={filter}
|
||||
isNonInitialPage={isNonInitialPage}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
@ -3,7 +3,9 @@ import Link from "@material-ui/core/Link"
|
||||
import TableCell from "@material-ui/core/TableCell"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||
import { FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import { workspaceFilterQuery } from "../../util/filters"
|
||||
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
||||
@ -11,63 +13,65 @@ import { EmptyState } from "../EmptyState/EmptyState"
|
||||
import { TableLoader } from "../TableLoader/TableLoader"
|
||||
import { WorkspacesRow } from "./WorkspacesRow"
|
||||
|
||||
export const Language = {
|
||||
emptyCreateWorkspaceMessage: "Create your first workspace",
|
||||
emptyCreateWorkspaceDescription:
|
||||
"Start editing your source code and building your software.",
|
||||
createFromTemplateButton: "Create from template",
|
||||
emptyResultsMessage: "No results matched your search",
|
||||
}
|
||||
|
||||
interface TableBodyProps {
|
||||
isLoading?: boolean
|
||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||
filter?: string
|
||||
isNonInitialPage: boolean
|
||||
}
|
||||
|
||||
export const WorkspacesTableBody: FC<
|
||||
React.PropsWithChildren<TableBodyProps>
|
||||
> = ({ isLoading, workspaceRefs, filter }) => {
|
||||
if (isLoading) {
|
||||
return <TableLoader />
|
||||
}
|
||||
|
||||
if (!workspaceRefs || workspaceRefs.length === 0) {
|
||||
return (
|
||||
<>
|
||||
{filter === workspaceFilterQuery.me ||
|
||||
filter === workspaceFilterQuery.all ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message={Language.emptyCreateWorkspaceMessage}
|
||||
description={Language.emptyCreateWorkspaceDescription}
|
||||
cta={
|
||||
<Link underline="none" component={RouterLink} to="/templates">
|
||||
<Button startIcon={<AddCircleOutline />}>
|
||||
{Language.createFromTemplateButton}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState message={Language.emptyResultsMessage} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
> = ({ isLoading, workspaceRefs, filter, isNonInitialPage }) => {
|
||||
const { t } = useTranslation("workspacesPage")
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceRefs.map((workspaceRef) => (
|
||||
<WorkspacesRow workspaceRef={workspaceRef} key={workspaceRef.id} />
|
||||
))}
|
||||
</>
|
||||
<ChooseOne>
|
||||
<Cond condition={Boolean(isLoading)}>
|
||||
<TableLoader />
|
||||
</Cond>
|
||||
<Cond condition={!workspaceRefs || workspaceRefs.length === 0}>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<ChooseOne>
|
||||
<Cond condition={isNonInitialPage}>
|
||||
<EmptyState message={t("emptyPageMessage")} />
|
||||
</Cond>
|
||||
<Cond
|
||||
condition={
|
||||
filter === workspaceFilterQuery.me ||
|
||||
filter === workspaceFilterQuery.all
|
||||
}
|
||||
>
|
||||
<EmptyState
|
||||
message={t("emptyCreateWorkspaceMessage")}
|
||||
description={t("emptyCreateWorkspaceDescription")}
|
||||
cta={
|
||||
<Link
|
||||
underline="none"
|
||||
component={RouterLink}
|
||||
to="/templates"
|
||||
>
|
||||
<Button startIcon={<AddCircleOutline />}>
|
||||
{t("createFromTemplateButton")}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Cond>
|
||||
<Cond>
|
||||
<EmptyState message={t("emptyResultsMessage")} />
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Cond>
|
||||
<Cond>
|
||||
{workspaceRefs &&
|
||||
workspaceRefs.map((workspaceRef) => (
|
||||
<WorkspacesRow workspaceRef={workspaceRef} key={workspaceRef.id} />
|
||||
))}
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
}
|
||||
|
@ -3,5 +3,9 @@
|
||||
"create": "created a new",
|
||||
"write": "updated",
|
||||
"delete": "deleted"
|
||||
},
|
||||
"table": {
|
||||
"emptyPage": "No audit logs available on this page",
|
||||
"noLogs": "No audit logs available"
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import templatesPage from "./templatesPage.json"
|
||||
import workspacePage from "./workspacePage.json"
|
||||
import agent from "./agent.json"
|
||||
import buildPage from "./buildPage.json"
|
||||
import workspacesPage from "./workspacesPage.json"
|
||||
import usersPage from "./usersPage.json"
|
||||
|
||||
export const en = {
|
||||
common,
|
||||
@ -16,4 +18,6 @@ export const en = {
|
||||
createWorkspacePage,
|
||||
agent,
|
||||
buildPage,
|
||||
workspacesPage,
|
||||
usersPage,
|
||||
}
|
||||
|
9
site/src/i18n/en/usersPage.json
Normal file
9
site/src/i18n/en/usersPage.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"emptyMessage": "No users found",
|
||||
"emptyPageMessage": "No users found on this page",
|
||||
"suspendMenuItem": "Suspend",
|
||||
"deleteMenuItem": "Delete",
|
||||
"listWorkspacesMenuItem": "View workspaces",
|
||||
"activateMenuItem": "Activate",
|
||||
"resetPasswordMenuItem": "Reset password"
|
||||
}
|
7
site/src/i18n/en/workspacesPage.json
Normal file
7
site/src/i18n/en/workspacesPage.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"emptyCreateWorkspaceMessage": "Create your first workspace",
|
||||
"emptyCreateWorkspaceDescription": "Start editing your source code and building your software.",
|
||||
"createFromTemplateButton": "Create from template",
|
||||
"emptyResultsMessage": "No results matched your search",
|
||||
"emptyPageMessage": "No results on this page"
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { getPaginationContext } from "components/PaginationWidget/utils"
|
||||
import {
|
||||
getPaginationContext,
|
||||
nonInitialPage,
|
||||
} from "components/PaginationWidget/utils"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
@ -38,6 +41,7 @@ const AuditPage: FC = () => {
|
||||
auditSend("FILTER", { filter })
|
||||
}}
|
||||
paginationRef={paginationRef}
|
||||
isNonInitialPage={nonInitialPage(searchParams)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
@ -25,6 +25,26 @@ const Template: Story<AuditPageViewProps> = (args) => (
|
||||
|
||||
export const AuditPage = Template.bind({})
|
||||
|
||||
export const Loading = Template.bind({})
|
||||
Loading.args = {
|
||||
auditLogs: undefined,
|
||||
count: undefined,
|
||||
isNonInitialPage: false,
|
||||
}
|
||||
|
||||
export const EmptyPage = Template.bind({})
|
||||
EmptyPage.args = {
|
||||
auditLogs: [],
|
||||
isNonInitialPage: true,
|
||||
}
|
||||
|
||||
export const NoLogs = Template.bind({})
|
||||
NoLogs.args = {
|
||||
auditLogs: [],
|
||||
count: 0,
|
||||
isNonInitialPage: false,
|
||||
}
|
||||
|
||||
export const AuditPageSmallViewport = Template.bind({})
|
||||
AuditPageSmallViewport.parameters = {
|
||||
chromatic: { viewports: [600] },
|
||||
|
@ -5,6 +5,7 @@ import TableContainer from "@material-ui/core/TableContainer"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import { AuditLog } from "api/typesGenerated"
|
||||
import { AuditLogRow } from "components/AuditLogRow/AuditLogRow"
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||
import { EmptyState } from "components/EmptyState/EmptyState"
|
||||
import { Margins } from "components/Margins/Margins"
|
||||
import {
|
||||
@ -19,6 +20,7 @@ import { TableLoader } from "components/TableLoader/TableLoader"
|
||||
import { Timeline } from "components/Timeline/Timeline"
|
||||
import { AuditHelpTooltip } from "components/Tooltips"
|
||||
import { FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
||||
|
||||
export const Language = {
|
||||
@ -43,6 +45,7 @@ export interface AuditPageViewProps {
|
||||
filter: string
|
||||
onFilter: (filter: string) => void
|
||||
paginationRef: PaginationMachineRef
|
||||
isNonInitialPage: boolean
|
||||
}
|
||||
|
||||
export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
@ -51,7 +54,9 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
filter,
|
||||
onFilter,
|
||||
paginationRef,
|
||||
isNonInitialPage,
|
||||
}) => {
|
||||
const { t } = useTranslation("auditLog")
|
||||
const isLoading = auditLogs === undefined || count === undefined
|
||||
const isEmpty = !isLoading && auditLogs.length === 0
|
||||
|
||||
@ -77,23 +82,38 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{isLoading && <TableLoader />}
|
||||
|
||||
{auditLogs && (
|
||||
<Timeline
|
||||
items={auditLogs}
|
||||
getDate={(log) => new Date(log.time)}
|
||||
row={(log) => <AuditLogRow key={log.id} auditLog={log} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isEmpty && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState message="No audit logs available" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<ChooseOne>
|
||||
<Cond condition={isLoading}>
|
||||
<TableLoader />
|
||||
</Cond>
|
||||
<Cond condition={isEmpty}>
|
||||
<ChooseOne>
|
||||
<Cond condition={isNonInitialPage}>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState message={t("table.emptyPage")} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Cond>
|
||||
<Cond>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState message={t("table.noLogs")} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</Cond>
|
||||
<Cond>
|
||||
{auditLogs && (
|
||||
<Timeline
|
||||
items={auditLogs}
|
||||
getDate={(log) => new Date(log.time)}
|
||||
row={(log) => <AuditLogRow key={log.id} auditLog={log} />}
|
||||
/>
|
||||
)}
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
@ -8,7 +8,6 @@ import { Role } from "../../api/typesGenerated"
|
||||
import { Language as ResetPasswordDialogLanguage } from "../../components/Dialogs/ResetPasswordDialog/ResetPasswordDialog"
|
||||
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
|
||||
import { Language as RoleSelectLanguage } from "../../components/RoleSelect/RoleSelect"
|
||||
import { Language as UsersTableBodyLanguage } from "../../components/UsersTable/UsersTableBody"
|
||||
import {
|
||||
MockAuditorRole,
|
||||
MockUser,
|
||||
@ -39,9 +38,8 @@ const suspendUser = async (setupActionSpies: () => void) => {
|
||||
await user.click(firstMoreButton)
|
||||
|
||||
const menu = await screen.findByRole("menu")
|
||||
const suspendButton = within(menu).getByText(
|
||||
UsersTableBodyLanguage.suspendMenuItem,
|
||||
)
|
||||
const text = t("suspendMenuItem", { ns: "usersPage" })
|
||||
const suspendButton = within(menu).getByText(text)
|
||||
|
||||
await user.click(suspendButton)
|
||||
|
||||
@ -72,9 +70,8 @@ const deleteUser = async (setupActionSpies: () => void) => {
|
||||
await user.click(selectedMoreButton)
|
||||
|
||||
const menu = await screen.findByRole("menu")
|
||||
const deleteButton = within(menu).getByText(
|
||||
UsersTableBodyLanguage.deleteMenuItem,
|
||||
)
|
||||
const text = t("deleteMenuItem", { ns: "usersPage" })
|
||||
const deleteButton = within(menu).getByText(text)
|
||||
|
||||
await user.click(deleteButton)
|
||||
|
||||
@ -107,9 +104,8 @@ const activateUser = async (setupActionSpies: () => void) => {
|
||||
fireEvent.click(suspendedMoreButton)
|
||||
|
||||
const menu = screen.getByRole("menu")
|
||||
const activateButton = within(menu).getByText(
|
||||
UsersTableBodyLanguage.activateMenuItem,
|
||||
)
|
||||
const text = t("activateMenuItem", { ns: "usersPage" })
|
||||
const activateButton = within(menu).getByText(text)
|
||||
fireEvent.click(activateButton)
|
||||
|
||||
// Check if the confirm message is displayed
|
||||
@ -135,9 +131,8 @@ const resetUserPassword = async (setupActionSpies: () => void) => {
|
||||
fireEvent.click(firstMoreButton)
|
||||
|
||||
const menu = screen.getByRole("menu")
|
||||
const resetPasswordButton = within(menu).getByText(
|
||||
UsersTableBodyLanguage.resetPasswordMenuItem,
|
||||
)
|
||||
const text = t("resetPasswordMenuItem", { ns: "usersPage" })
|
||||
const resetPasswordButton = within(menu).getByText(text)
|
||||
|
||||
fireEvent.click(resetPasswordButton)
|
||||
|
||||
|
@ -2,7 +2,10 @@ import { useActor, useMachine } from "@xstate/react"
|
||||
import { getErrorDetail } from "api/errors"
|
||||
import { User } from "api/typesGenerated"
|
||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
|
||||
import { getPaginationContext } from "components/PaginationWidget/utils"
|
||||
import {
|
||||
getPaginationContext,
|
||||
nonInitialPage,
|
||||
} from "components/PaginationWidget/utils"
|
||||
import { usePermissions } from "hooks/usePermissions"
|
||||
import { FC, ReactNode, useContext, useEffect } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
@ -135,6 +138,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
usersSend({ type: "UPDATE_FILTER", query })
|
||||
}}
|
||||
paginationRef={paginationRef}
|
||||
isNonInitialPage={nonInitialPage(searchParams)}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
|
@ -14,6 +14,18 @@ export default {
|
||||
paginationRef: {
|
||||
defaultValue: createPaginationRef({ page: 1, limit: 25 }),
|
||||
},
|
||||
isNonInitialPage: {
|
||||
defaultValue: false,
|
||||
},
|
||||
users: {
|
||||
defaultValue: [MockUser, MockUser2],
|
||||
},
|
||||
roles: {
|
||||
defaultValue: MockAssignableSiteRoles,
|
||||
},
|
||||
canEditUsers: {
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof UsersPageView>
|
||||
|
||||
@ -22,29 +34,23 @@ const Template: Story<UsersPageViewProps> = (args) => (
|
||||
)
|
||||
|
||||
export const Admin = Template.bind({})
|
||||
Admin.args = {
|
||||
users: [MockUser, MockUser2],
|
||||
roles: MockAssignableSiteRoles,
|
||||
canEditUsers: true,
|
||||
}
|
||||
|
||||
export const SmallViewport = Template.bind({})
|
||||
SmallViewport.args = {
|
||||
...Admin.args,
|
||||
}
|
||||
SmallViewport.parameters = {
|
||||
chromatic: { viewports: [600] },
|
||||
}
|
||||
|
||||
export const Member = Template.bind({})
|
||||
Member.args = { ...Admin.args, canEditUsers: false }
|
||||
Member.args = { canEditUsers: false }
|
||||
|
||||
export const Empty = Template.bind({})
|
||||
Empty.args = { ...Admin.args, users: [] }
|
||||
Empty.args = { users: [] }
|
||||
|
||||
export const EmptyPage = Template.bind({})
|
||||
EmptyPage.args = { users: [], isNonInitialPage: true }
|
||||
|
||||
export const Error = Template.bind({})
|
||||
Error.args = {
|
||||
...Admin.args,
|
||||
users: undefined,
|
||||
error: {
|
||||
response: {
|
||||
|
@ -30,6 +30,7 @@ export interface UsersPageViewProps {
|
||||
) => void
|
||||
onFilter: (query: string) => void
|
||||
paginationRef: PaginationMachineRef
|
||||
isNonInitialPage: boolean
|
||||
}
|
||||
|
||||
export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
||||
@ -49,6 +50,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
||||
filter,
|
||||
onFilter,
|
||||
paginationRef,
|
||||
isNonInitialPage,
|
||||
}) => {
|
||||
const presetFilters = [
|
||||
{ query: userFilterQuery.active, name: Language.activeUsersFilterName },
|
||||
@ -76,6 +78,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
||||
isUpdatingUserRoles={isUpdatingUserRoles}
|
||||
canEditUsers={canEditUsers}
|
||||
isLoading={isLoading}
|
||||
isNonInitialPage={isNonInitialPage}
|
||||
/>
|
||||
|
||||
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { screen, waitFor } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import * as CreateDayString from "util/createDayString"
|
||||
import { Language as WorkspacesTableBodyLanguage } from "../../components/WorkspacesTable/WorkspacesTableBody"
|
||||
import { MockWorkspace } from "../../testHelpers/entities"
|
||||
import { history, render } from "../../testHelpers/renderHelpers"
|
||||
import { server } from "../../testHelpers/server"
|
||||
import WorkspacesPage from "./WorkspacesPage"
|
||||
import { i18n } from "i18n"
|
||||
|
||||
const { t } = i18n
|
||||
|
||||
describe("WorkspacesPage", () => {
|
||||
beforeEach(() => {
|
||||
@ -27,9 +29,8 @@ describe("WorkspacesPage", () => {
|
||||
render(<WorkspacesPage />)
|
||||
|
||||
// Then
|
||||
await screen.findByText(
|
||||
WorkspacesTableBodyLanguage.emptyCreateWorkspaceMessage,
|
||||
)
|
||||
const text = t("emptyCreateWorkspaceMessage", { ns: "workspacesPage" })
|
||||
await screen.findByText(text)
|
||||
})
|
||||
|
||||
it("renders a filled workspaces page", async () => {
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { getPaginationContext } from "components/PaginationWidget/utils"
|
||||
import {
|
||||
getPaginationContext,
|
||||
nonInitialPage,
|
||||
} from "components/PaginationWidget/utils"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
@ -49,6 +52,7 @@ const WorkspacesPage: FC = () => {
|
||||
})
|
||||
}}
|
||||
paginationRef={paginationRef}
|
||||
isNonInitialPage={nonInitialPage(searchParams)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
@ -112,6 +112,7 @@ AllStates.args = {
|
||||
...Object.values(additionalWorkspaces),
|
||||
],
|
||||
count: 14,
|
||||
isNonInitialPage: false,
|
||||
}
|
||||
|
||||
export const OwnerHasNoWorkspaces = Template.bind({})
|
||||
@ -119,6 +120,7 @@ OwnerHasNoWorkspaces.args = {
|
||||
workspaceRefs: [],
|
||||
filter: workspaceFilterQuery.me,
|
||||
count: 0,
|
||||
isNonInitialPage: false,
|
||||
}
|
||||
|
||||
export const NoResults = Template.bind({})
|
||||
@ -126,4 +128,13 @@ NoResults.args = {
|
||||
workspaceRefs: [],
|
||||
filter: "searchtearmwithnoresults",
|
||||
count: 0,
|
||||
isNonInitialPage: false,
|
||||
}
|
||||
|
||||
export const EmptyPage = Template.bind({})
|
||||
EmptyPage.args = {
|
||||
workspaceRefs: [],
|
||||
filter: workspaceFilterQuery.me,
|
||||
count: 0,
|
||||
isNonInitialPage: true,
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ export interface WorkspacesPageViewProps {
|
||||
filter?: string
|
||||
onFilter: (query: string) => void
|
||||
paginationRef: PaginationMachineRef
|
||||
isNonInitialPage: boolean
|
||||
}
|
||||
|
||||
export const WorkspacesPageView: FC<
|
||||
@ -49,6 +50,7 @@ export const WorkspacesPageView: FC<
|
||||
filter,
|
||||
onFilter,
|
||||
paginationRef,
|
||||
isNonInitialPage,
|
||||
}) => {
|
||||
const presetFilters = [
|
||||
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
|
||||
@ -105,6 +107,7 @@ export const WorkspacesPageView: FC<
|
||||
isLoading={isLoading}
|
||||
workspaceRefs={workspaceRefs}
|
||||
filter={filter}
|
||||
isNonInitialPage={isNonInitialPage}
|
||||
/>
|
||||
|
||||
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
||||
|
Reference in New Issue
Block a user