mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +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)) {
|
if (conditionedOptions.some((cond) => cond.props.condition === undefined)) {
|
||||||
throw new Error(
|
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)
|
const chosen = conditionedOptions.find((child) => child.props.condition)
|
||||||
|
@ -102,3 +102,9 @@ export const createPaginationRef = (
|
|||||||
): PaginationMachineRef => {
|
): PaginationMachineRef => {
|
||||||
return spawn(paginationMachine.withContext(context))
|
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 { ComponentMeta, Story } from "@storybook/react"
|
||||||
import {
|
import {
|
||||||
MockSiteRoles,
|
MockAssignableSiteRoles,
|
||||||
MockUser,
|
MockUser,
|
||||||
MockUser2,
|
MockUser2,
|
||||||
} from "../../testHelpers/renderHelpers"
|
} from "../../testHelpers/renderHelpers"
|
||||||
@ -9,6 +9,11 @@ import { UsersTable, UsersTableProps } from "./UsersTable"
|
|||||||
export default {
|
export default {
|
||||||
title: "components/UsersTable",
|
title: "components/UsersTable",
|
||||||
component: UsersTable,
|
component: UsersTable,
|
||||||
|
argTypes: {
|
||||||
|
isNonInitialPage: {
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
} as ComponentMeta<typeof UsersTable>
|
} as ComponentMeta<typeof UsersTable>
|
||||||
|
|
||||||
const Template: Story<UsersTableProps> = (args) => <UsersTable {...args} />
|
const Template: Story<UsersTableProps> = (args) => <UsersTable {...args} />
|
||||||
@ -16,27 +21,27 @@ const Template: Story<UsersTableProps> = (args) => <UsersTable {...args} />
|
|||||||
export const Example = Template.bind({})
|
export const Example = Template.bind({})
|
||||||
Example.args = {
|
Example.args = {
|
||||||
users: [MockUser, MockUser2],
|
users: [MockUser, MockUser2],
|
||||||
roles: MockSiteRoles,
|
roles: MockAssignableSiteRoles,
|
||||||
canEditUsers: false,
|
canEditUsers: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editable = Template.bind({})
|
export const Editable = Template.bind({})
|
||||||
Editable.args = {
|
Editable.args = {
|
||||||
users: [MockUser, MockUser2],
|
users: [MockUser, MockUser2],
|
||||||
roles: MockSiteRoles,
|
roles: MockAssignableSiteRoles,
|
||||||
canEditUsers: true,
|
canEditUsers: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Empty = Template.bind({})
|
export const Empty = Template.bind({})
|
||||||
Empty.args = {
|
Empty.args = {
|
||||||
users: [],
|
users: [],
|
||||||
roles: MockSiteRoles,
|
roles: MockAssignableSiteRoles,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Loading = Template.bind({})
|
export const Loading = Template.bind({})
|
||||||
Loading.args = {
|
Loading.args = {
|
||||||
users: [],
|
users: [],
|
||||||
roles: MockSiteRoles,
|
roles: MockAssignableSiteRoles,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
}
|
}
|
||||||
Loading.parameters = {
|
Loading.parameters = {
|
||||||
|
@ -15,6 +15,7 @@ describe("AuditPage", () => {
|
|||||||
onActivateUser={() => jest.fn()}
|
onActivateUser={() => jest.fn()}
|
||||||
onResetUserPassword={() => jest.fn()}
|
onResetUserPassword={() => jest.fn()}
|
||||||
onUpdateUserRoles={() => jest.fn()}
|
onUpdateUserRoles={() => jest.fn()}
|
||||||
|
isNonInitialPage={false}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ export interface UsersTableProps {
|
|||||||
user: TypesGen.User,
|
user: TypesGen.User,
|
||||||
roles: TypesGen.Role["name"][],
|
roles: TypesGen.Role["name"][],
|
||||||
) => void
|
) => void
|
||||||
|
isNonInitialPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
||||||
@ -46,6 +47,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
|||||||
isUpdatingUserRoles,
|
isUpdatingUserRoles,
|
||||||
canEditUsers,
|
canEditUsers,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isNonInitialPage,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
@ -78,6 +80,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
|||||||
onResetUserPassword={onResetUserPassword}
|
onResetUserPassword={onResetUserPassword}
|
||||||
onSuspendUser={onSuspendUser}
|
onSuspendUser={onSuspendUser}
|
||||||
onUpdateUserRoles={onUpdateUserRoles}
|
onUpdateUserRoles={onUpdateUserRoles}
|
||||||
|
isNonInitialPage={isNonInitialPage}
|
||||||
/>
|
/>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
@ -2,8 +2,10 @@ import Box from "@material-ui/core/Box"
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import TableCell from "@material-ui/core/TableCell"
|
import TableCell from "@material-ui/core/TableCell"
|
||||||
import TableRow from "@material-ui/core/TableRow"
|
import TableRow from "@material-ui/core/TableRow"
|
||||||
|
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||||
import { LastUsed } from "components/LastUsed/LastUsed"
|
import { LastUsed } from "components/LastUsed/LastUsed"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
import { combineClasses } from "../../util/combineClasses"
|
import { combineClasses } from "../../util/combineClasses"
|
||||||
import { AvatarData } from "../AvatarData/AvatarData"
|
import { AvatarData } from "../AvatarData/AvatarData"
|
||||||
@ -12,15 +14,6 @@ import { RoleSelect } from "../RoleSelect/RoleSelect"
|
|||||||
import { TableLoader } from "../TableLoader/TableLoader"
|
import { TableLoader } from "../TableLoader/TableLoader"
|
||||||
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
|
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 {
|
interface UsersTableBodyProps {
|
||||||
users?: TypesGen.User[]
|
users?: TypesGen.User[]
|
||||||
roles?: TypesGen.AssignableRoles[]
|
roles?: TypesGen.AssignableRoles[]
|
||||||
@ -36,6 +29,7 @@ interface UsersTableBodyProps {
|
|||||||
user: TypesGen.User,
|
user: TypesGen.User,
|
||||||
roles: TypesGen.Role["name"][],
|
roles: TypesGen.Role["name"][],
|
||||||
) => void
|
) => void
|
||||||
|
isNonInitialPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsersTableBody: FC<
|
export const UsersTableBody: FC<
|
||||||
@ -52,121 +46,144 @@ export const UsersTableBody: FC<
|
|||||||
isUpdatingUserRoles,
|
isUpdatingUserRoles,
|
||||||
canEditUsers,
|
canEditUsers,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isNonInitialPage,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
const { t } = useTranslation("usersPage")
|
||||||
if (isLoading) {
|
|
||||||
return <TableLoader />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!users || users.length === 0) {
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={999}>
|
|
||||||
<Box p={4}>
|
|
||||||
<EmptyState message={Language.emptyMessage} />
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ChooseOne>
|
||||||
{users.map((user) => {
|
<Cond condition={Boolean(isLoading)}>
|
||||||
// When the user has no role we want to show they are a Member
|
<TableLoader />
|
||||||
const fallbackRole: TypesGen.Role = {
|
</Cond>
|
||||||
name: "member",
|
<Cond condition={!users || users.length === 0}>
|
||||||
display_name: "Member",
|
<ChooseOne>
|
||||||
}
|
<Cond condition={isNonInitialPage}>
|
||||||
const userRoles = user.roles.length === 0 ? [fallbackRole] : user.roles
|
<TableRow>
|
||||||
|
<TableCell colSpan={999}>
|
||||||
return (
|
<Box p={4}>
|
||||||
<TableRow key={user.id}>
|
<EmptyState message={t("emptyPageMessage")} />
|
||||||
<TableCell>
|
</Box>
|
||||||
<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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</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
|
isLoading?: boolean
|
||||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||||
filter?: string
|
filter?: string
|
||||||
|
isNonInitialPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspacesTable: FC<
|
export const WorkspacesTable: FC<
|
||||||
React.PropsWithChildren<WorkspacesTableProps>
|
React.PropsWithChildren<WorkspacesTableProps>
|
||||||
> = ({ isLoading, workspaceRefs, filter }) => {
|
> = ({ isLoading, workspaceRefs, filter, isNonInitialPage }) => {
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@ -44,6 +45,7 @@ export const WorkspacesTable: FC<
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
workspaceRefs={workspaceRefs}
|
workspaceRefs={workspaceRefs}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
isNonInitialPage={isNonInitialPage}
|
||||||
/>
|
/>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
@ -3,7 +3,9 @@ import Link from "@material-ui/core/Link"
|
|||||||
import TableCell from "@material-ui/core/TableCell"
|
import TableCell from "@material-ui/core/TableCell"
|
||||||
import TableRow from "@material-ui/core/TableRow"
|
import TableRow from "@material-ui/core/TableRow"
|
||||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||||
|
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link as RouterLink } from "react-router-dom"
|
import { Link as RouterLink } from "react-router-dom"
|
||||||
import { workspaceFilterQuery } from "../../util/filters"
|
import { workspaceFilterQuery } from "../../util/filters"
|
||||||
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
||||||
@ -11,63 +13,65 @@ import { EmptyState } from "../EmptyState/EmptyState"
|
|||||||
import { TableLoader } from "../TableLoader/TableLoader"
|
import { TableLoader } from "../TableLoader/TableLoader"
|
||||||
import { WorkspacesRow } from "./WorkspacesRow"
|
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 {
|
interface TableBodyProps {
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||||
filter?: string
|
filter?: string
|
||||||
|
isNonInitialPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspacesTableBody: FC<
|
export const WorkspacesTableBody: FC<
|
||||||
React.PropsWithChildren<TableBodyProps>
|
React.PropsWithChildren<TableBodyProps>
|
||||||
> = ({ isLoading, workspaceRefs, filter }) => {
|
> = ({ isLoading, workspaceRefs, filter, isNonInitialPage }) => {
|
||||||
if (isLoading) {
|
const { t } = useTranslation("workspacesPage")
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ChooseOne>
|
||||||
{workspaceRefs.map((workspaceRef) => (
|
<Cond condition={Boolean(isLoading)}>
|
||||||
<WorkspacesRow workspaceRef={workspaceRef} key={workspaceRef.id} />
|
<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",
|
"create": "created a new",
|
||||||
"write": "updated",
|
"write": "updated",
|
||||||
"delete": "deleted"
|
"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 workspacePage from "./workspacePage.json"
|
||||||
import agent from "./agent.json"
|
import agent from "./agent.json"
|
||||||
import buildPage from "./buildPage.json"
|
import buildPage from "./buildPage.json"
|
||||||
|
import workspacesPage from "./workspacesPage.json"
|
||||||
|
import usersPage from "./usersPage.json"
|
||||||
|
|
||||||
export const en = {
|
export const en = {
|
||||||
common,
|
common,
|
||||||
@ -16,4 +18,6 @@ export const en = {
|
|||||||
createWorkspacePage,
|
createWorkspacePage,
|
||||||
agent,
|
agent,
|
||||||
buildPage,
|
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 { useMachine } from "@xstate/react"
|
||||||
import { getPaginationContext } from "components/PaginationWidget/utils"
|
import {
|
||||||
|
getPaginationContext,
|
||||||
|
nonInitialPage,
|
||||||
|
} from "components/PaginationWidget/utils"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { useSearchParams } from "react-router-dom"
|
||||||
@ -38,6 +41,7 @@ const AuditPage: FC = () => {
|
|||||||
auditSend("FILTER", { filter })
|
auditSend("FILTER", { filter })
|
||||||
}}
|
}}
|
||||||
paginationRef={paginationRef}
|
paginationRef={paginationRef}
|
||||||
|
isNonInitialPage={nonInitialPage(searchParams)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -25,6 +25,26 @@ const Template: Story<AuditPageViewProps> = (args) => (
|
|||||||
|
|
||||||
export const AuditPage = Template.bind({})
|
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({})
|
export const AuditPageSmallViewport = Template.bind({})
|
||||||
AuditPageSmallViewport.parameters = {
|
AuditPageSmallViewport.parameters = {
|
||||||
chromatic: { viewports: [600] },
|
chromatic: { viewports: [600] },
|
||||||
|
@ -5,6 +5,7 @@ import TableContainer from "@material-ui/core/TableContainer"
|
|||||||
import TableRow from "@material-ui/core/TableRow"
|
import TableRow from "@material-ui/core/TableRow"
|
||||||
import { AuditLog } from "api/typesGenerated"
|
import { AuditLog } from "api/typesGenerated"
|
||||||
import { AuditLogRow } from "components/AuditLogRow/AuditLogRow"
|
import { AuditLogRow } from "components/AuditLogRow/AuditLogRow"
|
||||||
|
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||||
import { EmptyState } from "components/EmptyState/EmptyState"
|
import { EmptyState } from "components/EmptyState/EmptyState"
|
||||||
import { Margins } from "components/Margins/Margins"
|
import { Margins } from "components/Margins/Margins"
|
||||||
import {
|
import {
|
||||||
@ -19,6 +20,7 @@ import { TableLoader } from "components/TableLoader/TableLoader"
|
|||||||
import { Timeline } from "components/Timeline/Timeline"
|
import { Timeline } from "components/Timeline/Timeline"
|
||||||
import { AuditHelpTooltip } from "components/Tooltips"
|
import { AuditHelpTooltip } from "components/Tooltips"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
@ -43,6 +45,7 @@ export interface AuditPageViewProps {
|
|||||||
filter: string
|
filter: string
|
||||||
onFilter: (filter: string) => void
|
onFilter: (filter: string) => void
|
||||||
paginationRef: PaginationMachineRef
|
paginationRef: PaginationMachineRef
|
||||||
|
isNonInitialPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuditPageView: FC<AuditPageViewProps> = ({
|
export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||||
@ -51,7 +54,9 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
|||||||
filter,
|
filter,
|
||||||
onFilter,
|
onFilter,
|
||||||
paginationRef,
|
paginationRef,
|
||||||
|
isNonInitialPage,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation("auditLog")
|
||||||
const isLoading = auditLogs === undefined || count === undefined
|
const isLoading = auditLogs === undefined || count === undefined
|
||||||
const isEmpty = !isLoading && auditLogs.length === 0
|
const isEmpty = !isLoading && auditLogs.length === 0
|
||||||
|
|
||||||
@ -77,23 +82,38 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
|||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading && <TableLoader />}
|
<ChooseOne>
|
||||||
|
<Cond condition={isLoading}>
|
||||||
{auditLogs && (
|
<TableLoader />
|
||||||
<Timeline
|
</Cond>
|
||||||
items={auditLogs}
|
<Cond condition={isEmpty}>
|
||||||
getDate={(log) => new Date(log.time)}
|
<ChooseOne>
|
||||||
row={(log) => <AuditLogRow key={log.id} auditLog={log} />}
|
<Cond condition={isNonInitialPage}>
|
||||||
/>
|
<TableRow>
|
||||||
)}
|
<TableCell colSpan={999}>
|
||||||
|
<EmptyState message={t("table.emptyPage")} />
|
||||||
{isEmpty && (
|
</TableCell>
|
||||||
<TableRow>
|
</TableRow>
|
||||||
<TableCell colSpan={999}>
|
</Cond>
|
||||||
<EmptyState message="No audit logs available" />
|
<Cond>
|
||||||
</TableCell>
|
<TableRow>
|
||||||
</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>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
@ -8,7 +8,6 @@ import { Role } from "../../api/typesGenerated"
|
|||||||
import { Language as ResetPasswordDialogLanguage } from "../../components/Dialogs/ResetPasswordDialog/ResetPasswordDialog"
|
import { Language as ResetPasswordDialogLanguage } from "../../components/Dialogs/ResetPasswordDialog/ResetPasswordDialog"
|
||||||
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
|
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
|
||||||
import { Language as RoleSelectLanguage } from "../../components/RoleSelect/RoleSelect"
|
import { Language as RoleSelectLanguage } from "../../components/RoleSelect/RoleSelect"
|
||||||
import { Language as UsersTableBodyLanguage } from "../../components/UsersTable/UsersTableBody"
|
|
||||||
import {
|
import {
|
||||||
MockAuditorRole,
|
MockAuditorRole,
|
||||||
MockUser,
|
MockUser,
|
||||||
@ -39,9 +38,8 @@ const suspendUser = async (setupActionSpies: () => void) => {
|
|||||||
await user.click(firstMoreButton)
|
await user.click(firstMoreButton)
|
||||||
|
|
||||||
const menu = await screen.findByRole("menu")
|
const menu = await screen.findByRole("menu")
|
||||||
const suspendButton = within(menu).getByText(
|
const text = t("suspendMenuItem", { ns: "usersPage" })
|
||||||
UsersTableBodyLanguage.suspendMenuItem,
|
const suspendButton = within(menu).getByText(text)
|
||||||
)
|
|
||||||
|
|
||||||
await user.click(suspendButton)
|
await user.click(suspendButton)
|
||||||
|
|
||||||
@ -72,9 +70,8 @@ const deleteUser = async (setupActionSpies: () => void) => {
|
|||||||
await user.click(selectedMoreButton)
|
await user.click(selectedMoreButton)
|
||||||
|
|
||||||
const menu = await screen.findByRole("menu")
|
const menu = await screen.findByRole("menu")
|
||||||
const deleteButton = within(menu).getByText(
|
const text = t("deleteMenuItem", { ns: "usersPage" })
|
||||||
UsersTableBodyLanguage.deleteMenuItem,
|
const deleteButton = within(menu).getByText(text)
|
||||||
)
|
|
||||||
|
|
||||||
await user.click(deleteButton)
|
await user.click(deleteButton)
|
||||||
|
|
||||||
@ -107,9 +104,8 @@ const activateUser = async (setupActionSpies: () => void) => {
|
|||||||
fireEvent.click(suspendedMoreButton)
|
fireEvent.click(suspendedMoreButton)
|
||||||
|
|
||||||
const menu = screen.getByRole("menu")
|
const menu = screen.getByRole("menu")
|
||||||
const activateButton = within(menu).getByText(
|
const text = t("activateMenuItem", { ns: "usersPage" })
|
||||||
UsersTableBodyLanguage.activateMenuItem,
|
const activateButton = within(menu).getByText(text)
|
||||||
)
|
|
||||||
fireEvent.click(activateButton)
|
fireEvent.click(activateButton)
|
||||||
|
|
||||||
// Check if the confirm message is displayed
|
// Check if the confirm message is displayed
|
||||||
@ -135,9 +131,8 @@ const resetUserPassword = async (setupActionSpies: () => void) => {
|
|||||||
fireEvent.click(firstMoreButton)
|
fireEvent.click(firstMoreButton)
|
||||||
|
|
||||||
const menu = screen.getByRole("menu")
|
const menu = screen.getByRole("menu")
|
||||||
const resetPasswordButton = within(menu).getByText(
|
const text = t("resetPasswordMenuItem", { ns: "usersPage" })
|
||||||
UsersTableBodyLanguage.resetPasswordMenuItem,
|
const resetPasswordButton = within(menu).getByText(text)
|
||||||
)
|
|
||||||
|
|
||||||
fireEvent.click(resetPasswordButton)
|
fireEvent.click(resetPasswordButton)
|
||||||
|
|
||||||
|
@ -2,7 +2,10 @@ import { useActor, useMachine } from "@xstate/react"
|
|||||||
import { getErrorDetail } from "api/errors"
|
import { getErrorDetail } from "api/errors"
|
||||||
import { User } from "api/typesGenerated"
|
import { User } from "api/typesGenerated"
|
||||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
|
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 { usePermissions } from "hooks/usePermissions"
|
||||||
import { FC, ReactNode, useContext, useEffect } from "react"
|
import { FC, ReactNode, useContext, useEffect } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
@ -135,6 +138,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
|||||||
usersSend({ type: "UPDATE_FILTER", query })
|
usersSend({ type: "UPDATE_FILTER", query })
|
||||||
}}
|
}}
|
||||||
paginationRef={paginationRef}
|
paginationRef={paginationRef}
|
||||||
|
isNonInitialPage={nonInitialPage(searchParams)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
|
@ -14,6 +14,18 @@ export default {
|
|||||||
paginationRef: {
|
paginationRef: {
|
||||||
defaultValue: createPaginationRef({ page: 1, limit: 25 }),
|
defaultValue: createPaginationRef({ page: 1, limit: 25 }),
|
||||||
},
|
},
|
||||||
|
isNonInitialPage: {
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
defaultValue: [MockUser, MockUser2],
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
defaultValue: MockAssignableSiteRoles,
|
||||||
|
},
|
||||||
|
canEditUsers: {
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as ComponentMeta<typeof UsersPageView>
|
} as ComponentMeta<typeof UsersPageView>
|
||||||
|
|
||||||
@ -22,29 +34,23 @@ const Template: Story<UsersPageViewProps> = (args) => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const Admin = Template.bind({})
|
export const Admin = Template.bind({})
|
||||||
Admin.args = {
|
|
||||||
users: [MockUser, MockUser2],
|
|
||||||
roles: MockAssignableSiteRoles,
|
|
||||||
canEditUsers: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SmallViewport = Template.bind({})
|
export const SmallViewport = Template.bind({})
|
||||||
SmallViewport.args = {
|
|
||||||
...Admin.args,
|
|
||||||
}
|
|
||||||
SmallViewport.parameters = {
|
SmallViewport.parameters = {
|
||||||
chromatic: { viewports: [600] },
|
chromatic: { viewports: [600] },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Member = Template.bind({})
|
export const Member = Template.bind({})
|
||||||
Member.args = { ...Admin.args, canEditUsers: false }
|
Member.args = { canEditUsers: false }
|
||||||
|
|
||||||
export const Empty = Template.bind({})
|
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({})
|
export const Error = Template.bind({})
|
||||||
Error.args = {
|
Error.args = {
|
||||||
...Admin.args,
|
|
||||||
users: undefined,
|
users: undefined,
|
||||||
error: {
|
error: {
|
||||||
response: {
|
response: {
|
||||||
|
@ -30,6 +30,7 @@ export interface UsersPageViewProps {
|
|||||||
) => void
|
) => void
|
||||||
onFilter: (query: string) => void
|
onFilter: (query: string) => void
|
||||||
paginationRef: PaginationMachineRef
|
paginationRef: PaginationMachineRef
|
||||||
|
isNonInitialPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
||||||
@ -49,6 +50,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
|||||||
filter,
|
filter,
|
||||||
onFilter,
|
onFilter,
|
||||||
paginationRef,
|
paginationRef,
|
||||||
|
isNonInitialPage,
|
||||||
}) => {
|
}) => {
|
||||||
const presetFilters = [
|
const presetFilters = [
|
||||||
{ query: userFilterQuery.active, name: Language.activeUsersFilterName },
|
{ query: userFilterQuery.active, name: Language.activeUsersFilterName },
|
||||||
@ -76,6 +78,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
|||||||
isUpdatingUserRoles={isUpdatingUserRoles}
|
isUpdatingUserRoles={isUpdatingUserRoles}
|
||||||
canEditUsers={canEditUsers}
|
canEditUsers={canEditUsers}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
isNonInitialPage={isNonInitialPage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { screen, waitFor } from "@testing-library/react"
|
import { screen, waitFor } from "@testing-library/react"
|
||||||
import { rest } from "msw"
|
import { rest } from "msw"
|
||||||
import * as CreateDayString from "util/createDayString"
|
import * as CreateDayString from "util/createDayString"
|
||||||
import { Language as WorkspacesTableBodyLanguage } from "../../components/WorkspacesTable/WorkspacesTableBody"
|
|
||||||
import { MockWorkspace } from "../../testHelpers/entities"
|
import { MockWorkspace } from "../../testHelpers/entities"
|
||||||
import { history, render } from "../../testHelpers/renderHelpers"
|
import { history, render } from "../../testHelpers/renderHelpers"
|
||||||
import { server } from "../../testHelpers/server"
|
import { server } from "../../testHelpers/server"
|
||||||
import WorkspacesPage from "./WorkspacesPage"
|
import WorkspacesPage from "./WorkspacesPage"
|
||||||
|
import { i18n } from "i18n"
|
||||||
|
|
||||||
|
const { t } = i18n
|
||||||
|
|
||||||
describe("WorkspacesPage", () => {
|
describe("WorkspacesPage", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -27,9 +29,8 @@ describe("WorkspacesPage", () => {
|
|||||||
render(<WorkspacesPage />)
|
render(<WorkspacesPage />)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
await screen.findByText(
|
const text = t("emptyCreateWorkspaceMessage", { ns: "workspacesPage" })
|
||||||
WorkspacesTableBodyLanguage.emptyCreateWorkspaceMessage,
|
await screen.findByText(text)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("renders a filled workspaces page", async () => {
|
it("renders a filled workspaces page", async () => {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { useMachine } from "@xstate/react"
|
import { useMachine } from "@xstate/react"
|
||||||
import { getPaginationContext } from "components/PaginationWidget/utils"
|
import {
|
||||||
|
getPaginationContext,
|
||||||
|
nonInitialPage,
|
||||||
|
} from "components/PaginationWidget/utils"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { useSearchParams } from "react-router-dom"
|
||||||
@ -49,6 +52,7 @@ const WorkspacesPage: FC = () => {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
paginationRef={paginationRef}
|
paginationRef={paginationRef}
|
||||||
|
isNonInitialPage={nonInitialPage(searchParams)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -112,6 +112,7 @@ AllStates.args = {
|
|||||||
...Object.values(additionalWorkspaces),
|
...Object.values(additionalWorkspaces),
|
||||||
],
|
],
|
||||||
count: 14,
|
count: 14,
|
||||||
|
isNonInitialPage: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OwnerHasNoWorkspaces = Template.bind({})
|
export const OwnerHasNoWorkspaces = Template.bind({})
|
||||||
@ -119,6 +120,7 @@ OwnerHasNoWorkspaces.args = {
|
|||||||
workspaceRefs: [],
|
workspaceRefs: [],
|
||||||
filter: workspaceFilterQuery.me,
|
filter: workspaceFilterQuery.me,
|
||||||
count: 0,
|
count: 0,
|
||||||
|
isNonInitialPage: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NoResults = Template.bind({})
|
export const NoResults = Template.bind({})
|
||||||
@ -126,4 +128,13 @@ NoResults.args = {
|
|||||||
workspaceRefs: [],
|
workspaceRefs: [],
|
||||||
filter: "searchtearmwithnoresults",
|
filter: "searchtearmwithnoresults",
|
||||||
count: 0,
|
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
|
filter?: string
|
||||||
onFilter: (query: string) => void
|
onFilter: (query: string) => void
|
||||||
paginationRef: PaginationMachineRef
|
paginationRef: PaginationMachineRef
|
||||||
|
isNonInitialPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspacesPageView: FC<
|
export const WorkspacesPageView: FC<
|
||||||
@ -49,6 +50,7 @@ export const WorkspacesPageView: FC<
|
|||||||
filter,
|
filter,
|
||||||
onFilter,
|
onFilter,
|
||||||
paginationRef,
|
paginationRef,
|
||||||
|
isNonInitialPage,
|
||||||
}) => {
|
}) => {
|
||||||
const presetFilters = [
|
const presetFilters = [
|
||||||
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
|
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
|
||||||
@ -105,6 +107,7 @@ export const WorkspacesPageView: FC<
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
workspaceRefs={workspaceRefs}
|
workspaceRefs={workspaceRefs}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
isNonInitialPage={isNonInitialPage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
||||||
|
Reference in New Issue
Block a user