chore(site): remove users and pagination services (#9932)

This commit is contained in:
Bruno Quaresma
2023-10-02 15:10:51 -03:00
committed by GitHub
parent 885b2502ed
commit 42e25740eb
20 changed files with 433 additions and 1251 deletions
+59 -2
View File
@@ -1,5 +1,13 @@
import { QueryClient } from "@tanstack/react-query";
import * as API from "api/api"; import * as API from "api/api";
import { UpdateUserPasswordRequest } from "api/typesGenerated"; import { UpdateUserPasswordRequest, UsersRequest } from "api/typesGenerated";
export const users = (req: UsersRequest) => {
return {
queryKey: ["users", req],
queryFn: () => API.getUsers(req),
};
};
export const updatePassword = () => { export const updatePassword = () => {
return { return {
@@ -11,9 +19,12 @@ export const updatePassword = () => {
}; };
}; };
export const createUser = () => { export const createUser = (queryClient: QueryClient) => {
return { return {
mutationFn: API.createUser, mutationFn: API.createUser,
onSuccess: async () => {
await queryClient.invalidateQueries(["users"]);
},
}; };
}; };
@@ -22,3 +33,49 @@ export const createFirstUser = () => {
mutationFn: API.createFirstUser, mutationFn: API.createFirstUser,
}; };
}; };
export const suspendUser = (queryClient: QueryClient) => {
return {
mutationFn: API.suspendUser,
onSuccess: async () => {
await queryClient.invalidateQueries(["users"]);
},
};
};
export const activateUser = (queryClient: QueryClient) => {
return {
mutationFn: API.activateUser,
onSuccess: async () => {
await queryClient.invalidateQueries(["users"]);
},
};
};
export const deleteUser = (queryClient: QueryClient) => {
return {
mutationFn: API.deleteUser,
onSuccess: async () => {
await queryClient.invalidateQueries(["users"]);
},
};
};
export const updateRoles = (queryClient: QueryClient) => {
return {
mutationFn: ({ userId, roles }: { userId: string; roles: string[] }) =>
API.updateUserRoles(roles, userId),
onSuccess: async () => {
await queryClient.invalidateQueries(["users"]);
},
};
};
export const authMethods = () => {
return {
// Even the endpoint being /users/authmethods we don't want to revalidate it
// when users change so its better add a unique query key
queryKey: ["authMethods"],
queryFn: API.getAuthMethods,
};
};
+12 -14
View File
@@ -353,20 +353,18 @@ const PresetMenu = ({
View advanced filtering View advanced filtering
</MenuItem> </MenuItem>
{learnMoreLink2 && learnMoreLabel2 && ( {learnMoreLink2 && learnMoreLabel2 && (
<> <MenuItem
<MenuItem component="a"
component="a" href={learnMoreLink2}
href={learnMoreLink2} target="_blank"
target="_blank" sx={{ fontSize: 13, fontWeight: 500 }}
sx={{ fontSize: 13, fontWeight: 500 }} onClick={() => {
onClick={() => { setIsOpen(false);
setIsOpen(false); }}
}} >
> <OpenInNewOutlined sx={{ fontSize: "14px !important" }} />
<OpenInNewOutlined sx={{ fontSize: "14px !important" }} /> {learnMoreLabel2}
{learnMoreLabel2} </MenuItem>
</MenuItem>
</>
)} )}
</Menu> </Menu>
</> </>
@@ -1,43 +0,0 @@
import { PaginationWidget } from "./PaginationWidget";
import { createPaginationRef } from "./utils";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof PaginationWidget> = {
title: "components/PaginationWidget",
component: PaginationWidget,
args: {
prevLabel: "Previous",
nextLabel: "Next",
paginationRef: createPaginationRef({ page: 1, limit: 12 }),
numRecords: 200,
},
};
export default meta;
type Story = StoryObj<typeof PaginationWidget>;
export const MoreThan8Pages: Story = {};
export const LessThan8Pages: Story = {
args: {
numRecords: 84,
},
};
export const MoreThan7PagesWithActivePageCloseToStart: Story = {
args: {
paginationRef: createPaginationRef({ page: 2, limit: 12 }),
},
};
export const MoreThan7PagesWithActivePageFarFromBoundaries: Story = {
args: {
paginationRef: createPaginationRef({ page: 4, limit: 12 }),
},
};
export const MoreThan7PagesWithActivePageCloseToEnd: Story = {
args: {
paginationRef: createPaginationRef({ page: 17, limit: 12 }),
},
};
@@ -1,28 +0,0 @@
import { screen } from "@testing-library/react";
import { render } from "testHelpers/renderHelpers";
import { PaginationWidget } from "./PaginationWidget";
import { createPaginationRef } from "./utils";
describe("PaginatedList", () => {
it("disables the previous button on the first page", () => {
render(
<PaginationWidget
numRecords={100}
paginationRef={createPaginationRef({ page: 1, limit: 25 })}
/>,
);
const prevButton = screen.getByLabelText("Previous page");
expect(prevButton).toBeDisabled();
});
it("disables the next button on the last page", () => {
render(
<PaginationWidget
numRecords={100}
paginationRef={createPaginationRef({ page: 4, limit: 25 })}
/>,
);
const nextButton = screen.getByLabelText("Next page");
expect(nextButton).toBeDisabled();
});
});
@@ -1,106 +0,0 @@
import Button from "@mui/material/Button";
import { makeStyles, useTheme } from "@mui/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import { useActor } from "@xstate/react";
import { CSSProperties } from "react";
import { PaginationMachineRef } from "xServices/pagination/paginationXService";
import { PageButton } from "./PageButton";
import { buildPagedList } from "./utils";
export type PaginationWidgetProps = {
prevLabel?: string;
nextLabel?: string;
numRecords?: number;
containerStyle?: CSSProperties;
paginationRef: PaginationMachineRef;
};
export const PaginationWidget = ({
prevLabel = "",
nextLabel = "",
numRecords,
containerStyle,
paginationRef,
}: PaginationWidgetProps): JSX.Element | null => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const styles = useStyles();
const [paginationState, send] = useActor(paginationRef);
const currentPage = paginationState.context.page;
const numRecordsPerPage = paginationState.context.limit;
const numPages = numRecords ? Math.ceil(numRecords / numRecordsPerPage) : 0;
const firstPageActive = currentPage === 1 && numPages !== 0;
const lastPageActive = currentPage === numPages && numPages !== 0;
// if beyond page 1, show pagination widget even if there's only one true page, so user can navigate back
const showWidget = numPages > 1 || currentPage > 1;
if (!showWidget) {
return null;
}
return (
<div style={containerStyle} className={styles.defaultContainerStyles}>
<Button
className={styles.prevLabelStyles}
aria-label="Previous page"
disabled={firstPageActive}
onClick={() => send({ type: "PREVIOUS_PAGE" })}
>
<KeyboardArrowLeft />
<div>{prevLabel}</div>
</Button>
{isMobile ? (
<PageButton
activePage={currentPage}
page={currentPage}
numPages={numPages}
/>
) : (
buildPagedList(numPages, currentPage).map((page) =>
typeof page !== "number" ? (
<PageButton
key={`Page${page}`}
activePage={currentPage}
placeholder="..."
disabled
/>
) : (
<PageButton
key={`Page${page}`}
activePage={currentPage}
page={page}
numPages={numPages}
onPageClick={() => send({ type: "GO_TO_PAGE", page })}
/>
),
)
)}
<Button
aria-label="Next page"
disabled={lastPageActive}
onClick={() => send({ type: "NEXT_PAGE" })}
>
<div>{nextLabel}</div>
<KeyboardArrowRight />
</Button>
</div>
);
};
const useStyles = makeStyles((theme) => ({
defaultContainerStyles: {
justifyContent: "center",
alignItems: "center",
display: "flex",
flexDirection: "row",
padding: "20px",
},
prevLabelStyles: {
marginRight: theme.spacing(0.5),
},
}));
@@ -0,0 +1,35 @@
import { PaginationWidgetBase } from "./PaginationWidgetBase";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof PaginationWidgetBase> = {
title: "components/PaginationWidgetBase",
component: PaginationWidgetBase,
args: {
page: 1,
limit: 12,
count: 200,
},
};
export default meta;
type Story = StoryObj<typeof PaginationWidgetBase>;
export const MoreThan8Pages: Story = {};
export const LessThan8Pages: Story = {
args: {
count: 84,
},
};
export const MoreThan7PagesWithActivePageCloseToStart: Story = {
args: { page: 2, limit: 12 },
};
export const MoreThan7PagesWithActivePageFarFromBoundaries: Story = {
args: { page: 4, limit: 12 },
};
export const MoreThan7PagesWithActivePageCloseToEnd: Story = {
args: { page: 17, limit: 12 },
};
@@ -1,31 +1,29 @@
import { buildPagedList, getOffset } from "./utils"; import { buildPagedList, getOffset } from "./utils";
describe("unit/PaginationWidget", () => { describe("buildPagedList", () => {
describe("buildPagedList", () => { it.each<{
it.each<{ numPages: number;
numPages: number; activePage: number;
activePage: number; expected: (string | number)[];
expected: (string | number)[]; }>([
}>([ { numPages: 7, activePage: 1, expected: [1, 2, 3, 4, 5, 6, 7] },
{ numPages: 7, activePage: 1, expected: [1, 2, 3, 4, 5, 6, 7] }, { numPages: 17, activePage: 1, expected: [1, 2, 3, 4, 5, "right", 17] },
{ numPages: 17, activePage: 1, expected: [1, 2, 3, 4, 5, "right", 17] }, {
{ numPages: 17,
numPages: 17, activePage: 9,
activePage: 9, expected: [1, "left", 8, 9, 10, "right", 17],
expected: [1, "left", 8, 9, 10, "right", 17], },
}, {
{ numPages: 17,
numPages: 17, activePage: 17,
activePage: 17, expected: [1, "left", 13, 14, 15, 16, 17],
expected: [1, "left", 13, 14, 15, 16, 17], },
}, ])(
])( `buildPagedList($numPages, $activePage)`,
`buildPagedList($numPages, $activePage)`, ({ numPages, activePage, expected }) => {
({ numPages, activePage, expected }) => { expect(buildPagedList(numPages, activePage)).toEqual(expected);
expect(buildPagedList(numPages, activePage)).toEqual(expected); },
}, );
);
});
}); });
describe("getOffset", () => { describe("getOffset", () => {
@@ -1,10 +1,3 @@
import {
PaginationContext,
paginationMachine,
PaginationMachineRef,
} from "xServices/pagination/paginationXService";
import { spawn } from "xstate";
/** /**
* Generates a ranged array with an option to step over values. * Generates a ranged array with an option to step over values.
* Shamelessly stolen from: * Shamelessly stolen from:
@@ -63,46 +56,10 @@ export const buildPagedList = (
return range(1, numPages); return range(1, numPages);
}; };
const getInitialPage = (page: string | null): number =>
page ? Number(page) : 1;
// pages count from 1 // pages count from 1
export const getOffset = (page: number, limit: number): number => export const getOffset = (page: number, limit: number): number =>
(page - 1) * limit; (page - 1) * limit;
interface PaginationData {
offset: number;
limit: number;
}
export const getPaginationData = (
ref: PaginationMachineRef,
): PaginationData => {
const snapshot = ref.getSnapshot();
if (snapshot) {
const { page, limit } = snapshot.context;
const offset = getOffset(page, limit);
return { offset, limit };
} else {
throw new Error("No pagination data");
}
};
export const getPaginationContext = (
searchParams: URLSearchParams,
limit: number = DEFAULT_RECORDS_PER_PAGE,
): PaginationContext => ({
page: getInitialPage(searchParams.get("page")),
limit,
});
// for storybook
export const createPaginationRef = (
context: PaginationContext,
): PaginationMachineRef => {
return spawn(paginationMachine.withContext(context));
};
export const nonInitialPage = (searchParams: URLSearchParams): boolean => { export const nonInitialPage = (searchParams: URLSearchParams): boolean => {
const page = searchParams.get("page"); const page = searchParams.get("page");
const numberPage = page ? Number(page) : 1; const numberPage = page ? Number(page) : 1;
+2
View File
@@ -9,6 +9,7 @@ export const usePagination = ({
const [searchParams, setSearchParams] = searchParamsResult; const [searchParams, setSearchParams] = searchParamsResult;
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
const limit = DEFAULT_RECORDS_PER_PAGE; const limit = DEFAULT_RECORDS_PER_PAGE;
const offset = page <= 0 ? 0 : (page - 1) * limit;
const goToPage = (page: number) => { const goToPage = (page: number) => {
searchParams.set("page", page.toString()); searchParams.set("page", page.toString());
@@ -19,5 +20,6 @@ export const usePagination = ({
page, page,
limit, limit,
goToPage, goToPage,
offset,
}; };
}; };
@@ -5,9 +5,8 @@ import { useNavigate } from "react-router-dom";
import { CreateUserForm } from "./CreateUserForm"; import { CreateUserForm } from "./CreateUserForm";
import { Margins } from "components/Margins/Margins"; import { Margins } from "components/Margins/Margins";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { getAuthMethods } from "api/api"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query"; import { authMethods, createUser } from "api/queries/users";
import { createUser } from "api/queries/users";
import { displaySuccess } from "components/GlobalSnackbar/utils"; import { displaySuccess } from "components/GlobalSnackbar/utils";
export const Language = { export const Language = {
@@ -17,13 +16,9 @@ export const Language = {
export const CreateUserPage: FC = () => { export const CreateUserPage: FC = () => {
const myOrgId = useOrganizationId(); const myOrgId = useOrganizationId();
const navigate = useNavigate(); const navigate = useNavigate();
const createUserMutation = useMutation(createUser()); const queryClient = useQueryClient();
// TODO: We should probably place this somewhere else to reduce the number of calls. const createUserMutation = useMutation(createUser(queryClient));
// This would be called each time this page is loaded. const authMethodsQuery = useQuery(authMethods());
const { data: authMethods } = useQuery({
queryKey: ["authMethods"],
queryFn: getAuthMethods,
});
return ( return (
<Margins> <Margins>
@@ -33,7 +28,7 @@ export const CreateUserPage: FC = () => {
<CreateUserForm <CreateUserForm
error={createUserMutation.error} error={createUserMutation.error}
authMethods={authMethods} authMethods={authMethodsQuery.data}
onSubmit={async (user) => { onSubmit={async (user) => {
await createUserMutation.mutateAsync(user); await createUserMutation.mutateAsync(user);
displaySuccess("Successfully created user."); displaySuccess("Successfully created user.");
@@ -3,30 +3,27 @@ import { ComponentProps, FC } from "react";
import { Section } from "components/SettingsLayout/Section"; import { Section } from "components/SettingsLayout/Section";
import { SecurityForm } from "./SettingsSecurityForm"; import { SecurityForm } from "./SettingsSecurityForm";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { getAuthMethods, getUserLoginType } from "api/api"; import { getUserLoginType } from "api/api";
import { import {
SingleSignOnSection, SingleSignOnSection,
useSingleSignOnSection, useSingleSignOnSection,
} from "./SingleSignOnSection"; } from "./SingleSignOnSection";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { updatePassword } from "api/queries/users"; import { authMethods, updatePassword } from "api/queries/users";
import { displaySuccess } from "components/GlobalSnackbar/utils"; import { displaySuccess } from "components/GlobalSnackbar/utils";
export const SecurityPage: FC = () => { export const SecurityPage: FC = () => {
const me = useMe(); const me = useMe();
const updatePasswordMutation = useMutation(updatePassword()); const updatePasswordMutation = useMutation(updatePassword());
const { data: authMethods } = useQuery({ const authMethodsQuery = useQuery(authMethods());
queryKey: ["authMethods"],
queryFn: getAuthMethods,
});
const { data: userLoginType } = useQuery({ const { data: userLoginType } = useQuery({
queryKey: ["loginType"], queryKey: ["loginType"],
queryFn: getUserLoginType, queryFn: getUserLoginType,
}); });
const singleSignOnSection = useSingleSignOnSection(); const singleSignOnSection = useSingleSignOnSection();
if (!authMethods || !userLoginType) { if (!authMethodsQuery.data || !userLoginType) {
return <Loader />; return <Loader />;
} }
@@ -51,7 +48,7 @@ export const SecurityPage: FC = () => {
}} }}
oidc={{ oidc={{
section: { section: {
authMethods, authMethods: authMethodsQuery.data,
userLoginType, userLoginType,
...singleSignOnSection, ...singleSignOnSection,
}, },
+118 -194
View File
@@ -1,4 +1,4 @@
import { fireEvent, screen, waitFor, within } from "@testing-library/react"; import { fireEvent, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { rest } from "msw"; import { rest } from "msw";
import { import {
@@ -6,50 +6,38 @@ import {
MockUser2, MockUser2,
SuspendedMockUser, SuspendedMockUser,
MockAuditorRole, MockAuditorRole,
MockOwnerRole,
} from "testHelpers/entities"; } from "testHelpers/entities";
import { Language as usersXServiceLanguage } from "xServices/users/usersXService";
import * as API from "api/api"; import * as API from "api/api";
import { Role } from "api/typesGenerated"; import { Role } from "api/typesGenerated";
import { Language as ResetPasswordDialogLanguage } from "./ResetPasswordDialog"; import { Language as ResetPasswordDialogLanguage } from "./ResetPasswordDialog";
import { renderWithAuth } from "testHelpers/renderHelpers"; import { renderWithAuth } from "testHelpers/renderHelpers";
import { server } from "testHelpers/server"; import { server } from "testHelpers/server";
import { Language as UsersPageLanguage, UsersPage } from "./UsersPage"; import { UsersPage } from "./UsersPage";
const renderPage = () => { const renderPage = () => {
return renderWithAuth(<UsersPage />); return renderWithAuth(<UsersPage />);
}; };
const suspendUser = async (setupActionSpies: () => void) => { const suspendUser = async () => {
const user = userEvent.setup(); const user = userEvent.setup();
// Get the first user in the table // Get the first user in the table
const moreButtons = await screen.findAllByLabelText("more"); const moreButtons = await screen.findAllByLabelText("more");
const firstMoreButton = moreButtons[0]; const firstMoreButton = moreButtons[0];
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(/Suspend/); const suspendButton = within(menu).getByText(/Suspend/);
await user.click(suspendButton); await user.click(suspendButton);
// Check if the confirm message is displayed // Check if the confirm message is displayed
const confirmDialog = await screen.findByRole("dialog"); const confirmDialog = await screen.findByRole("dialog");
expect(confirmDialog).toHaveTextContent( const confirmButton = await within(confirmDialog).findByRole("button", {
`${UsersPageLanguage.suspendDialogMessagePrefix} ${MockUser.username}?`, name: "Suspend",
); });
// Setup spies to check the actions after
setupActionSpies();
// Click on the "Confirm" button
const confirmButton = await within(confirmDialog).findByText(
UsersPageLanguage.suspendDialogAction,
);
await user.click(confirmButton); await user.click(confirmButton);
}; };
const deleteUser = async (setupActionSpies: () => void) => { const deleteUser = async () => {
const user = userEvent.setup(); const user = userEvent.setup();
// Click on the "more" button to display the "Delete" option // Click on the "more" button to display the "Delete" option
// Needs to await fetching users and fetching permissions, because they're needed to see the more button // Needs to await fetching users and fetching permissions, because they're needed to see the more button
@@ -73,15 +61,12 @@ const deleteUser = async (setupActionSpies: () => void) => {
const dialog = screen.getByRole("dialog"); const dialog = screen.getByRole("dialog");
await user.type(textField, MockUser2.username); await user.type(textField, MockUser2.username);
// Setup spies to check the actions after
setupActionSpies();
// Click on the "Confirm" button // Click on the "Confirm" button
const confirmButton = within(dialog).getByRole("button", { name: "Delete" }); const confirmButton = within(dialog).getByRole("button", { name: "Delete" });
await user.click(confirmButton); await user.click(confirmButton);
}; };
const activateUser = async (setupActionSpies: () => void) => { const activateUser = async () => {
const moreButtons = await screen.findAllByLabelText("more"); const moreButtons = await screen.findAllByLabelText("more");
const suspendedMoreButton = moreButtons[2]; const suspendedMoreButton = moreButtons[2];
fireEvent.click(suspendedMoreButton); fireEvent.click(suspendedMoreButton);
@@ -92,17 +77,11 @@ const activateUser = async (setupActionSpies: () => void) => {
// Check if the confirm message is displayed // Check if the confirm message is displayed
const confirmDialog = screen.getByRole("dialog"); const confirmDialog = screen.getByRole("dialog");
expect(confirmDialog).toHaveTextContent(
`${UsersPageLanguage.activateDialogMessagePrefix} ${SuspendedMockUser.username}?`,
);
// Setup spies to check the actions after
setupActionSpies();
// Click on the "Confirm" button // Click on the "Confirm" button
const confirmButton = within(confirmDialog).getByText( const confirmButton = within(confirmDialog).getByRole("button", {
UsersPageLanguage.activateDialogAction, name: "Activate",
); });
fireEvent.click(confirmButton); fireEvent.click(confirmButton);
}; };
@@ -134,7 +113,7 @@ const resetUserPassword = async (setupActionSpies: () => void) => {
fireEvent.click(confirmButton); fireEvent.click(confirmButton);
}; };
const updateUserRole = async (setupActionSpies: () => void, role: Role) => { const updateUserRole = async (role: Role) => {
// Get the first user in the table // Get the first user in the table
const users = await screen.findAllByText(/.*@coder.com/); const users = await screen.findAllByText(/.*@coder.com/);
const userRow = users[0].closest("tr"); const userRow = users[0].closest("tr");
@@ -146,9 +125,6 @@ const updateUserRole = async (setupActionSpies: () => void, role: Role) => {
const editButton = within(userRow).getByTitle("Edit user roles"); const editButton = within(userRow).getByTitle("Edit user roles");
fireEvent.click(editButton); fireEvent.click(editButton);
// Setup spies to check the actions after
setupActionSpies();
// Click on the role option // Click on the role option
const fieldset = await screen.findByTitle("Available roles"); const fieldset = await screen.findByTitle("Available roles");
const auditorOption = within(fieldset).getByText(role.display_name); const auditorOption = within(fieldset).getByText(role.display_name);
@@ -162,121 +138,93 @@ const updateUserRole = async (setupActionSpies: () => void, role: Role) => {
describe("UsersPage", () => { describe("UsersPage", () => {
describe("suspend user", () => { describe("suspend user", () => {
describe("when it is success", () => { describe("when it is success", () => {
it("shows a success message and refresh the page", async () => { it("shows a success message", async () => {
renderPage(); renderPage();
await suspendUser(() => { server.use(
jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser); rest.put(
jest.spyOn(API, "getUsers").mockResolvedValueOnce({ `/api/v2/users/${MockUser.id}/status/suspend`,
users: [SuspendedMockUser, MockUser2], async (req, res, ctx) => {
count: 2, return res(ctx.status(200), ctx.json(SuspendedMockUser));
}); },
}); ),
);
await suspendUser();
// Check if the success message is displayed // Check if the success message is displayed
await screen.findByText(usersXServiceLanguage.suspendUserSuccess); await screen.findByText("Successfully suspended the user.");
// Check if the API was called correctly
expect(API.suspendUser).toBeCalledTimes(1);
expect(API.suspendUser).toBeCalledWith(MockUser.id);
// Check if the users list was reload
await waitFor(() => expect(API.getUsers).toBeCalledTimes(1));
}); });
}); });
describe("when it fails", () => { describe("when it fails", () => {
it("shows an error message", async () => { it("shows an error message", async () => {
renderPage(); renderPage();
await suspendUser(() => { server.use(
jest.spyOn(API, "suspendUser").mockRejectedValueOnce({}); rest.put(
}); `/api/v2/users/${MockUser.id}/status/suspend`,
async (req, res, ctx) => {
return res(
ctx.status(400),
ctx.json({
message: "Error suspending user.",
}),
);
},
),
);
await suspendUser();
// Check if the error message is displayed // Check if the error message is displayed
await screen.findByText(usersXServiceLanguage.suspendUserError); await screen.findByText("Error suspending user.");
// Check if the API was called correctly
expect(API.suspendUser).toBeCalledTimes(1);
expect(API.suspendUser).toBeCalledWith(MockUser.id);
}); });
}); });
}); });
describe("pagination", () => {
it("goes to next and previous page", async () => {
const { container } = renderPage();
const user = userEvent.setup();
const mock = jest
.spyOn(API, "getUsers")
.mockResolvedValueOnce({ users: [MockUser, MockUser2], count: 26 });
const nextButton = await screen.findByLabelText("Next page");
expect(nextButton).toBeEnabled();
const previousButton = await screen.findByLabelText("Previous page");
expect(previousButton).toBeDisabled();
await user.click(nextButton);
await waitFor(() =>
expect(API.getUsers).toBeCalledWith({ offset: 25, limit: 25, q: "" }),
);
mock.mockClear();
await user.click(previousButton);
await waitFor(() =>
expect(API.getUsers).toBeCalledWith({ offset: 0, limit: 25, q: "" }),
);
const pageButtons = container.querySelectorAll(
`button[name="Page button"]`,
);
// count handler says there are 2 pages of results
expect(pageButtons.length).toBe(2);
});
});
describe("delete user", () => { describe("delete user", () => {
describe("when it is success", () => { describe("when it is success", () => {
it("shows a success message and refresh the page", async () => { it("shows a success message", async () => {
renderPage(); renderPage();
await deleteUser(() => { server.use(
jest.spyOn(API, "deleteUser").mockResolvedValueOnce(undefined); rest.delete(
jest.spyOn(API, "getUsers").mockResolvedValueOnce({ `/api/v2/users/${MockUser2.id}`,
users: [MockUser, SuspendedMockUser], async (req, res, ctx) => {
count: 2, return res(ctx.status(200), ctx.json(MockUser2));
}); },
}); ),
);
await deleteUser();
// Check if the success message is displayed // Check if the success message is displayed
await screen.findByText(usersXServiceLanguage.deleteUserSuccess); await screen.findByText("Successfully deleted the user.");
// Check if the API was called correctly
expect(API.deleteUser).toBeCalledTimes(1);
expect(API.deleteUser).toBeCalledWith(MockUser2.id);
// Check if the users list was reloaded
await waitFor(() => {
const users = screen.getAllByLabelText("more");
expect(users.length).toEqual(2);
});
}); });
}); });
describe("when it fails", () => { describe("when it fails", () => {
it("shows an error message", async () => { it("shows an error message", async () => {
renderPage(); renderPage();
await deleteUser(() => { server.use(
jest.spyOn(API, "deleteUser").mockRejectedValueOnce({}); rest.delete(
}); `/api/v2/users/${MockUser2.id}`,
async (req, res, ctx) => {
return res(
ctx.status(400),
ctx.json({
message: "Error deleting user.",
}),
);
},
),
);
await deleteUser();
// Check if the error message is displayed // Check if the error message is displayed
await screen.findByText(usersXServiceLanguage.deleteUserError); await screen.findByText("Error deleting user.");
// Check if the API was called correctly
expect(API.deleteUser).toBeCalledTimes(1);
expect(API.deleteUser).toBeCalledWith(MockUser2.id);
}); });
}); });
}); });
@@ -286,40 +234,43 @@ describe("UsersPage", () => {
it("shows a success message and refreshes the page", async () => { it("shows a success message and refreshes the page", async () => {
renderPage(); renderPage();
await activateUser(() => { server.use(
jest rest.put(
.spyOn(API, "activateUser") `/api/v2/users/${SuspendedMockUser.id}/status/activate`,
.mockResolvedValueOnce(SuspendedMockUser); async (req, res, ctx) => {
jest.spyOn(API, "getUsers").mockImplementationOnce(() => return res(ctx.status(200), ctx.json(MockUser));
Promise.resolve({ },
users: [MockUser, MockUser2, SuspendedMockUser], ),
count: 3, );
}),
); await activateUser();
});
// Check if the success message is displayed // Check if the success message is displayed
await screen.findByText(usersXServiceLanguage.activateUserSuccess); await screen.findByText("Successfully activated the user.");
// Check if the API was called correctly
expect(API.activateUser).toBeCalledTimes(1);
expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id);
}); });
}); });
describe("when activation fails", () => { describe("when activation fails", () => {
it("shows an error message", async () => { it("shows an error message", async () => {
renderPage(); renderPage();
await activateUser(() => { server.use(
jest.spyOn(API, "activateUser").mockRejectedValueOnce({}); rest.put(
}); `/api/v2/users/${SuspendedMockUser.id}/status/activate`,
async (req, res, ctx) => {
return res(
ctx.status(400),
ctx.json({
message: "Error activating user.",
}),
);
},
),
);
await activateUser();
// Check if the error message is displayed // Check if the error message is displayed
await screen.findByText(usersXServiceLanguage.activateUserError); await screen.findByText("Error activating user.");
// Check if the API was called correctly
expect(API.activateUser).toBeCalledTimes(1);
expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id);
}); });
}); });
}); });
@@ -336,7 +287,7 @@ describe("UsersPage", () => {
}); });
// Check if the success message is displayed // Check if the success message is displayed
await screen.findByText(usersXServiceLanguage.resetUserPasswordSuccess); await screen.findByText("Successfully updated the user password.");
// Check if the API was called correctly // Check if the API was called correctly
expect(API.updateUserPassword).toBeCalledTimes(1); expect(API.updateUserPassword).toBeCalledTimes(1);
@@ -355,7 +306,7 @@ describe("UsersPage", () => {
}); });
// Check if the error message is displayed // Check if the error message is displayed
await screen.findByText(usersXServiceLanguage.resetUserPasswordError); await screen.findByText("Error on resetting the user password.");
// Check if the API was called correctly // Check if the API was called correctly
expect(API.updateUserPassword).toBeCalledTimes(1); expect(API.updateUserPassword).toBeCalledTimes(1);
@@ -372,26 +323,24 @@ describe("UsersPage", () => {
it("updates the roles", async () => { it("updates the roles", async () => {
renderPage(); renderPage();
const { userRow } = await updateUserRole(() => { server.use(
jest.spyOn(API, "updateUserRoles").mockResolvedValueOnce({ rest.put(
...MockUser, `/api/v2/users/${MockUser.id}/roles`,
roles: [...MockUser.roles, MockAuditorRole], async (req, res, ctx) => {
}); return res(
}, MockAuditorRole); ctx.status(200),
ctx.json({
// Check if the select text was updated with the Auditor role ...MockUser,
await waitFor(() => { roles: [...MockUser.roles, MockAuditorRole],
expect(userRow).toHaveTextContent(MockOwnerRole.display_name); }),
}); );
expect(userRow).toHaveTextContent(MockAuditorRole.display_name); },
),
// Check if the API was called correctly
const currentRoles = MockUser.roles.map((r) => r.name);
expect(API.updateUserRoles).toBeCalledTimes(1);
expect(API.updateUserRoles).toBeCalledWith(
[...currentRoles, MockAuditorRole.name],
MockUser.id,
); );
await updateUserRole(MockAuditorRole);
await screen.findByText("Successfully updated the user roles.");
}); });
}); });
@@ -399,44 +348,19 @@ describe("UsersPage", () => {
it("shows an error message", async () => { it("shows an error message", async () => {
renderPage(); renderPage();
await updateUserRole(() => {
jest.spyOn(API, "updateUserRoles").mockRejectedValueOnce({});
}, MockAuditorRole);
// Check if the error message is displayed
const errorMessage = await screen.findByText(
usersXServiceLanguage.updateUserRolesError,
);
await waitFor(() => expect(errorMessage).toBeDefined());
// Check if the API was called correctly
const currentRoles = MockUser.roles.map((r) => r.name);
expect(API.updateUserRoles).toBeCalledTimes(1);
expect(API.updateUserRoles).toBeCalledWith(
[...currentRoles, MockAuditorRole.name],
MockUser.id,
);
});
it("shows an error from the backend", async () => {
renderPage();
server.use( server.use(
rest.put(`/api/v2/users/${MockUser.id}/roles`, (req, res, ctx) => { rest.put(`/api/v2/users/${MockUser.id}/roles`, (req, res, ctx) => {
return res( return res(
ctx.status(400), ctx.status(400),
ctx.json({ message: "message from the backend" }), ctx.json({ message: "Error on updating the user roles." }),
); );
}), }),
); );
await updateUserRole(() => null, MockAuditorRole); await updateUserRole(MockAuditorRole);
// Check if the error message is displayed // Check if the error message is displayed
const errorMessage = await screen.findByText( await screen.findByText("Error on updating the user roles.");
"message from the backend",
);
expect(errorMessage).toBeDefined();
}); });
}); });
}); });
+142 -144
View File
@@ -1,16 +1,11 @@
import { useMachine } from "@xstate/react";
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 { import { nonInitialPage } from "components/PaginationWidget/utils";
getPaginationContext,
nonInitialPage,
} from "components/PaginationWidget/utils";
import { useMe } from "hooks/useMe"; import { useMe } from "hooks/useMe";
import { usePermissions } from "hooks/usePermissions"; import { usePermissions } from "hooks/usePermissions";
import { FC, ReactNode, useEffect } from "react"; import { FC, ReactNode, useState } from "react";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useSearchParams, useNavigate } from "react-router-dom"; import { useSearchParams, useNavigate } from "react-router-dom";
import { usersMachine } from "xServices/users/usersXService";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ResetPasswordDialog } from "./ResetPasswordDialog"; import { ResetPasswordDialog } from "./ResetPasswordDialog";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
@@ -18,50 +13,41 @@ import { UsersPageView } from "./UsersPageView";
import { useStatusFilterMenu } from "./UsersFilter"; import { useStatusFilterMenu } from "./UsersFilter";
import { useFilter } from "components/Filter/filter"; import { useFilter } from "components/Filter/filter";
import { useDashboard } from "components/Dashboard/DashboardProvider"; import { useDashboard } from "components/Dashboard/DashboardProvider";
import { useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getAuthMethods } from "api/api";
import { roles } from "api/queries/roles"; import { roles } from "api/queries/roles";
import { deploymentConfig } from "api/queries/deployment"; import { deploymentConfig } from "api/queries/deployment";
import { prepareQuery } from "utils/filters";
export const Language = { import { usePagination } from "hooks";
suspendDialogTitle: "Suspend user", import {
suspendDialogAction: "Suspend", users,
suspendDialogMessagePrefix: "Do you want to suspend the user", suspendUser,
activateDialogTitle: "Activate user", activateUser,
activateDialogAction: "Activate", deleteUser,
activateDialogMessagePrefix: "Do you want to activate the user", updatePassword,
}; updateRoles,
authMethods,
const getSelectedUser = (id: string, users?: User[]) => } from "api/queries/users";
users?.find((u) => u.id === id); import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { getErrorMessage } from "api/errors";
import { generateRandomString } from "utils/random";
export const UsersPage: FC<{ children?: ReactNode }> = () => { export const UsersPage: FC<{ children?: ReactNode }> = () => {
const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const searchParamsResult = useSearchParams(); const searchParamsResult = useSearchParams();
const { entitlements } = useDashboard(); const { entitlements } = useDashboard();
const [searchParams, setSearchParams] = searchParamsResult; const [searchParams] = searchParamsResult;
const filter = searchParams.get("filter") ?? ""; const filter = searchParams.get("filter") ?? "";
const [usersState, usersSend] = useMachine(usersMachine, { const pagination = usePagination({
context: { searchParamsResult,
filter,
paginationContext: getPaginationContext(searchParams),
},
actions: {
updateURL: (context, event) =>
setSearchParams({ page: event.page, filter: context.filter }),
},
}); });
const { const usersQuery = useQuery(
users, users({
getUsersError, q: prepareQuery(filter),
usernameToDelete, limit: pagination.limit,
usernameToSuspend, offset: pagination.offset,
usernameToActivate, }),
userIdToResetPassword, );
newUserPassword,
paginationRef,
count,
} = usersState.context;
const { updateUsers: canEditUsers, viewDeploymentValues } = usePermissions(); const { updateUsers: canEditUsers, viewDeploymentValues } = usePermissions();
const rolesQuery = useQuery({ ...roles(), enabled: canEditUsers }); const rolesQuery = useQuery({ ...roles(), enabled: canEditUsers });
const { data: deploymentValues } = useQuery({ const { data: deploymentValues } = useQuery({
@@ -77,12 +63,9 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
const useFilterResult = useFilter({ const useFilterResult = useFilter({
searchParamsResult, searchParamsResult,
onUpdate: () => { onUpdate: () => {
usersSend({ type: "UPDATE_PAGE", page: "1" }); pagination.goToPage(1);
}, },
}); });
useEffect(() => {
usersSend({ type: "UPDATE_FILTER", query: useFilterResult.query });
}, [useFilterResult.query, usersSend]);
const statusMenu = useStatusFilterMenu({ const statusMenu = useStatusFilterMenu({
value: useFilterResult.values.status, value: useFilterResult.values.status,
onChange: (option) => onChange: (option) =>
@@ -91,19 +74,26 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
status: option?.value, status: option?.value,
}), }),
}); });
const authMethods = useQuery({ const authMethodsQuery = useQuery(authMethods());
queryKey: ["authMethods"],
queryFn: () => {
return getAuthMethods();
},
});
// Is loading if
// - users are loading or
// - the user can edit the users but the roles are loading
const isLoading = const isLoading =
usersState.matches("gettingUsers") || usersQuery.isLoading || rolesQuery.isLoading || authMethodsQuery.isLoading;
rolesQuery.isLoading ||
authMethods.isLoading; const [confirmSuspendUser, setConfirmSuspendUser] = useState<User>();
const suspendUserMutation = useMutation(suspendUser(queryClient));
const [confirmActivateUser, setConfirmActivateUser] = useState<User>();
const activateUserMutation = useMutation(activateUser(queryClient));
const [confirmDeleteUser, setConfirmDeleteUser] = useState<User>();
const deleteUserMutation = useMutation(deleteUser(queryClient));
const [confirmResetPassword, setConfirmResetPassword] = useState<{
user: User;
newPassword: string;
}>();
const updatePasswordMutation = useMutation(updatePassword());
const updateRolesMutation = useMutation(updateRoles(queryClient));
return ( return (
<> <>
@@ -113,9 +103,8 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
<UsersPageView <UsersPageView
oidcRoleSyncEnabled={oidcRoleSyncEnabled} oidcRoleSyncEnabled={oidcRoleSyncEnabled}
roles={rolesQuery.data} roles={rolesQuery.data}
users={users} users={usersQuery.data?.users}
authMethods={authMethods.data} authMethods={authMethodsQuery.data}
count={count}
onListWorkspaces={(user) => { onListWorkspaces={(user) => {
navigate( navigate(
"/workspaces?filter=" + "/workspaces?filter=" +
@@ -127,91 +116,90 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
"/audit?filter=" + encodeURIComponent(`username:${user.username}`), "/audit?filter=" + encodeURIComponent(`username:${user.username}`),
); );
}} }}
onDeleteUser={(user) => { onDeleteUser={setConfirmDeleteUser}
usersSend({ onSuspendUser={setConfirmSuspendUser}
type: "DELETE_USER", onActivateUser={setConfirmActivateUser}
userId: user.id,
username: user.username,
});
}}
onSuspendUser={(user) => {
usersSend({
type: "SUSPEND_USER",
userId: user.id,
username: user.username,
});
}}
onActivateUser={(user) => {
usersSend({
type: "ACTIVATE_USER",
userId: user.id,
username: user.username,
});
}}
onResetUserPassword={(user) => { onResetUserPassword={(user) => {
usersSend({ type: "RESET_USER_PASSWORD", userId: user.id }); setConfirmResetPassword({
}} user,
onUpdateUserRoles={(user, roles) => { newPassword: generateRandomString(12),
usersSend({
type: "UPDATE_USER_ROLES",
userId: user.id,
roles,
}); });
}} }}
isUpdatingUserRoles={usersState.matches("updatingUserRoles")} onUpdateUserRoles={async (user, roles) => {
try {
await updateRolesMutation.mutateAsync({
userId: user.id,
roles,
});
displaySuccess("Successfully updated the user roles.");
} catch (e) {
displayError(
getErrorMessage(e, "Error on updating the user roles."),
);
}
}}
isUpdatingUserRoles={updateRolesMutation.isLoading}
isLoading={isLoading} isLoading={isLoading}
canEditUsers={canEditUsers} canEditUsers={canEditUsers}
canViewActivity={entitlements.features.audit_log.enabled} canViewActivity={entitlements.features.audit_log.enabled}
paginationRef={paginationRef}
isNonInitialPage={nonInitialPage(searchParams)} isNonInitialPage={nonInitialPage(searchParams)}
actorID={me.id} actorID={me.id}
filterProps={{ filterProps={{
filter: useFilterResult, filter: useFilterResult,
error: getUsersError, error: usersQuery.error,
menus: { menus: {
status: statusMenu, status: statusMenu,
}, },
}} }}
count={usersQuery.data?.count}
page={pagination.page}
limit={pagination.limit}
onPageChange={pagination.goToPage}
/> />
<DeleteDialog <DeleteDialog
key={usernameToDelete} key={confirmDeleteUser?.username}
isOpen={ isOpen={confirmDeleteUser !== undefined}
usersState.matches("confirmUserDeletion") || confirmLoading={deleteUserMutation.isLoading}
usersState.matches("deletingUser") name={confirmDeleteUser?.username ?? ""}
}
confirmLoading={usersState.matches("deletingUser")}
name={usernameToDelete ?? ""}
entity="user" entity="user"
onConfirm={() => { onConfirm={async () => {
usersSend("CONFIRM_USER_DELETE"); try {
await deleteUserMutation.mutateAsync(confirmDeleteUser!.id);
setConfirmDeleteUser(undefined);
displaySuccess("Successfully deleted the user.");
} catch (e) {
displayError(getErrorMessage(e, "Error deleting user."));
}
}} }}
onCancel={() => { onCancel={() => {
usersSend("CANCEL_USER_DELETE"); setConfirmDeleteUser(undefined);
}} }}
/> />
<ConfirmDialog <ConfirmDialog
type="delete" type="delete"
hideCancel={false} hideCancel={false}
open={ open={confirmSuspendUser !== undefined}
usersState.matches("confirmUserSuspension") || confirmLoading={suspendUserMutation.isLoading}
usersState.matches("suspendingUser") title="Suspend user"
} confirmText="Suspend"
confirmLoading={usersState.matches("suspendingUser")} onConfirm={async () => {
title={Language.suspendDialogTitle} try {
confirmText={Language.suspendDialogAction} await suspendUserMutation.mutateAsync(confirmSuspendUser!.id);
onConfirm={() => { setConfirmSuspendUser(undefined);
usersSend("CONFIRM_USER_SUSPENSION"); displaySuccess("Successfully suspended the user.");
} catch (e) {
displayError(getErrorMessage(e, "Error suspending user."));
}
}} }}
onClose={() => { onClose={() => {
usersSend("CANCEL_USER_SUSPENSION"); setConfirmSuspendUser(undefined);
}} }}
description={ description={
<> <>
{Language.suspendDialogMessagePrefix} Do you want to suspend the user{" "}
{usernameToSuspend && " "} <strong>{confirmSuspendUser?.username ?? ""}</strong>?
<strong>{usernameToSuspend ?? ""}</strong>?
</> </>
} }
/> />
@@ -219,45 +207,55 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
<ConfirmDialog <ConfirmDialog
type="success" type="success"
hideCancel={false} hideCancel={false}
open={ open={confirmActivateUser !== undefined}
usersState.matches("confirmUserActivation") || confirmLoading={activateUserMutation.isLoading}
usersState.matches("activatingUser") title="Activate user"
} confirmText="Activate"
confirmLoading={usersState.matches("activatingUser")} onConfirm={async () => {
title={Language.activateDialogTitle} try {
confirmText={Language.activateDialogAction} await activateUserMutation.mutateAsync(confirmActivateUser!.id);
onConfirm={() => { setConfirmActivateUser(undefined);
usersSend("CONFIRM_USER_ACTIVATION"); displaySuccess("Successfully activated the user.");
} catch (e) {
displayError(getErrorMessage(e, "Error activating user."));
}
}} }}
onClose={() => { onClose={() => {
usersSend("CANCEL_USER_ACTIVATION"); setConfirmActivateUser(undefined);
}} }}
description={ description={
<> <>
{Language.activateDialogMessagePrefix} Do you want to activate{" "}
{usernameToActivate && " "} <strong>{confirmActivateUser?.username ?? ""}</strong>?
<strong>{usernameToActivate ?? ""}</strong>?
</> </>
} }
/> />
{userIdToResetPassword && ( <ResetPasswordDialog
<ResetPasswordDialog key={confirmResetPassword?.user.username}
open={ open={confirmResetPassword !== undefined}
usersState.matches("confirmUserPasswordReset") || loading={updatePasswordMutation.isLoading}
usersState.matches("resettingUserPassword") user={confirmResetPassword?.user}
newPassword={confirmResetPassword?.newPassword}
onClose={() => {
setConfirmResetPassword(undefined);
}}
onConfirm={async () => {
try {
await updatePasswordMutation.mutateAsync({
userId: confirmResetPassword!.user.id,
password: confirmResetPassword!.newPassword,
old_password: "",
});
setConfirmResetPassword(undefined);
displaySuccess("Successfully updated the user password.");
} catch (e) {
displayError(
getErrorMessage(e, "Error on resetting the user password."),
);
} }
loading={usersState.matches("resettingUserPassword")} }}
user={getSelectedUser(userIdToResetPassword, users)} />
newPassword={newUserPassword}
onClose={() => {
usersSend("CANCEL_USER_PASSWORD_RESET");
}}
onConfirm={() => {
usersSend("CONFIRM_USER_PASSWORD_RESET");
}}
/>
)}
</> </>
); );
}; };
@@ -1,5 +1,4 @@
import { Meta, StoryObj } from "@storybook/react"; import { Meta, StoryObj } from "@storybook/react";
import { createPaginationRef } from "components/PaginationWidget/utils";
import { import {
MockUser, MockUser,
MockUser2, MockUser2,
@@ -30,7 +29,8 @@ const meta: Meta<typeof UsersPageView> = {
title: "pages/UsersPageView", title: "pages/UsersPageView",
component: UsersPageView, component: UsersPageView,
args: { args: {
paginationRef: createPaginationRef({ page: 1, limit: 25 }), page: 1,
limit: 25,
isNonInitialPage: false, isNonInitialPage: false,
users: [MockUser, MockUser2], users: [MockUser, MockUser2],
roles: MockAssignableSiteRoles, roles: MockAssignableSiteRoles,
+18 -7
View File
@@ -1,6 +1,4 @@
import { PaginationWidget } from "components/PaginationWidget/PaginationWidget";
import { ComponentProps, FC } from "react"; import { ComponentProps, FC } from "react";
import { PaginationMachineRef } from "xServices/pagination/paginationXService";
import * as TypesGen from "api/typesGenerated"; import * as TypesGen from "api/typesGenerated";
import { UsersTable } from "./UsersTable/UsersTable"; import { UsersTable } from "./UsersTable/UsersTable";
import { UsersFilter } from "./UsersFilter"; import { UsersFilter } from "./UsersFilter";
@@ -8,10 +6,10 @@ import {
PaginationStatus, PaginationStatus,
TableToolbar, TableToolbar,
} from "components/TableToolbar/TableToolbar"; } from "components/TableToolbar/TableToolbar";
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase";
export interface UsersPageViewProps { export interface UsersPageViewProps {
users?: TypesGen.User[]; users?: TypesGen.User[];
count?: number;
roles?: TypesGen.AssignableRoles[]; roles?: TypesGen.AssignableRoles[];
isUpdatingUserRoles?: boolean; isUpdatingUserRoles?: boolean;
canEditUsers?: boolean; canEditUsers?: boolean;
@@ -30,14 +28,17 @@ export interface UsersPageViewProps {
roles: TypesGen.Role["name"][], roles: TypesGen.Role["name"][],
) => void; ) => void;
filterProps: ComponentProps<typeof UsersFilter>; filterProps: ComponentProps<typeof UsersFilter>;
paginationRef: PaginationMachineRef;
isNonInitialPage: boolean; isNonInitialPage: boolean;
actorID: string; actorID: string;
// Pagination
count?: number;
page: number;
limit: number;
onPageChange: (page: number) => void;
} }
export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
users, users,
count,
roles, roles,
onSuspendUser, onSuspendUser,
onDeleteUser, onDeleteUser,
@@ -52,10 +53,13 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
canViewActivity, canViewActivity,
isLoading, isLoading,
filterProps, filterProps,
paginationRef,
isNonInitialPage, isNonInitialPage,
actorID, actorID,
authMethods, authMethods,
count,
limit,
onPageChange,
page,
}) => { }) => {
return ( return (
<> <>
@@ -90,7 +94,14 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
authMethods={authMethods} authMethods={authMethods}
/> />
<PaginationWidget numRecords={count} paginationRef={paginationRef} /> {count && (
<PaginationWidgetBase
count={count}
limit={limit}
onChange={onPageChange}
page={page}
/>
)}
</> </>
); );
}; };
@@ -188,7 +188,7 @@ const useStyles = makeStyles((theme) => ({
padding: 0, padding: 0,
"&:disabled": { "&:disabled": {
opacity: 0, opacity: 0.5,
}, },
}, },
options: { options: {
+4
View File
@@ -341,6 +341,10 @@ export const handlers = [
return res(ctx.status(200), ctx.json(M.MockDeploymentStats)); return res(ctx.status(200), ctx.json(M.MockDeploymentStats));
}), }),
rest.get("/api/v2/deployment/config", (_, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockDeploymentConfig));
}),
rest.get( rest.get(
"/api/v2/workspacebuilds/:workspaceBuildId/parameters", "/api/v2/workspacebuilds/:workspaceBuildId/parameters",
(_, res, ctx) => { (_, res, ctx) => {
+5 -2
View File
@@ -3,12 +3,15 @@ import * as TypesGen from "api/typesGenerated";
export const queryToFilter = ( export const queryToFilter = (
query?: string, query?: string,
): TypesGen.WorkspaceFilter | TypesGen.UsersRequest => { ): TypesGen.WorkspaceFilter | TypesGen.UsersRequest => {
const preparedQuery = query?.trim().replace(/ +/g, " ");
return { return {
q: preparedQuery, q: prepareQuery(query),
}; };
}; };
export const prepareQuery = (query?: string) => {
return query?.trim().replace(/ +/g, " ");
};
export const workspaceFilterQuery = { export const workspaceFilterQuery = {
me: "owner:me", me: "owner:me",
all: "", all: "",
@@ -1,66 +0,0 @@
import { ActorRefFrom, createMachine, sendParent, assign } from "xstate";
export interface PaginationContext {
page: number;
limit: number;
}
export type PaginationEvent =
| { type: "NEXT_PAGE" }
| { type: "PREVIOUS_PAGE" }
| { type: "GO_TO_PAGE"; page: number }
| { type: "RESET_PAGE" };
export type PaginationMachineRef = ActorRefFrom<typeof paginationMachine>;
export const paginationMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QAcCGUCWA7VAXDA9lgLKoDGAFtmAMQByAogBoAqA+gAoCCA4gwNoAGALqIUBWBnxExIAB6IATADZBAOgAcGgCwBWDYMEHBAdm2KANCACeiEwEZ7axboCcbkw8-3XGgL5+VmiYONIk5FRYtBwASgwAagCSAPIAqgDKnLwCIrLIElKEWLIKCAC09hrKarq6AMwarsraGormuiYaVrYIDk4u7q7e3r4BQejYeEWklNQ0PMlsLIvcfEKiSCD5kmElduq69vrKyiaCjqfu3YgauupatYp1eia+o4FbE6HTEXNx6Qx2KschtxDsintyvZqlVzmdGs0tIpbtcENplIoanV7M8jrpFCZntoAh8sAQIHA8l8pkQZpEwGoAE5gVAQHpgwoyTalZSuNTuV6CFzabSCFqdVFmdTPMU+dHnZrKMafEI08KzKJ5Aq7blKJwC1xC3QisUaCU2RDKQ5qGXaOWqaHokl+IA */
createMachine(
{
tsTypes: {} as import("./paginationXService.typegen").Typegen0,
schema: {
context: {} as PaginationContext,
events: {} as PaginationEvent,
},
predictableActionArguments: true,
id: "paginationMachine",
initial: "ready",
on: {
NEXT_PAGE: {
actions: ["assignNextPage", "sendUpdatePage"],
},
PREVIOUS_PAGE: {
actions: ["assignPreviousPage", "sendUpdatePage"],
},
GO_TO_PAGE: {
actions: ["assignPage", "sendUpdatePage"],
},
RESET_PAGE: {
actions: ["resetPage", "sendUpdatePage"],
},
},
states: {
ready: {},
},
},
{
actions: {
sendUpdatePage: sendParent((context) => ({
type: "UPDATE_PAGE",
page: context.page.toString(),
})),
assignNextPage: assign({
page: (context) => context.page + 1,
}),
assignPreviousPage: assign({
page: (context) => context.page - 1,
}),
assignPage: assign({
page: (_, event) => event.page,
}),
resetPage: assign({
page: (_) => 1,
}),
},
},
);
-554
View File
@@ -1,554 +0,0 @@
import { getPaginationData } from "components/PaginationWidget/utils";
import {
PaginationContext,
paginationMachine,
PaginationMachineRef,
} from "xServices/pagination/paginationXService";
import { assign, createMachine, send, spawn } from "xstate";
import * as API from "api/api";
import { getErrorMessage } from "api/errors";
import * as TypesGen from "api/typesGenerated";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { queryToFilter } from "utils/filters";
import { generateRandomString } from "utils/random";
const usersPaginationId = "usersPagination";
export const Language = {
getUsersError: "Error getting users.",
suspendUserSuccess: "Successfully suspended the user.",
suspendUserError: "Error suspending user.",
deleteUserSuccess: "Successfully deleted the user.",
deleteUserError: "Error deleting user.",
activateUserSuccess: "Successfully activated the user.",
activateUserError: "Error activating user.",
resetUserPasswordSuccess: "Successfully updated the user password.",
resetUserPasswordError: "Error on resetting the user password.",
updateUserRolesSuccess: "Successfully updated the user roles.",
updateUserRolesError: "Error on updating the user roles.",
};
export interface UsersContext {
// Get users
users?: TypesGen.User[];
filter: string;
getUsersError?: unknown;
// Suspend user
userIdToSuspend?: TypesGen.User["id"];
usernameToSuspend?: TypesGen.User["username"];
suspendUserError?: unknown;
// Delete user
userIdToDelete?: TypesGen.User["id"];
usernameToDelete?: TypesGen.User["username"];
deleteUserError?: unknown;
// Activate user
userIdToActivate?: TypesGen.User["id"];
usernameToActivate?: TypesGen.User["username"];
activateUserError?: unknown;
// Reset user password
userIdToResetPassword?: TypesGen.User["id"];
resetUserPasswordError?: unknown;
newUserPassword?: string;
// Update user roles
userIdToUpdateRoles?: TypesGen.User["id"];
updateUserRolesError?: unknown;
paginationContext: PaginationContext;
paginationRef: PaginationMachineRef;
count: number;
}
export type UsersEvent =
| { type: "GET_USERS"; query?: string }
// Suspend events
| {
type: "SUSPEND_USER";
userId: TypesGen.User["id"];
username: TypesGen.User["username"];
}
| { type: "CONFIRM_USER_SUSPENSION" }
| { type: "CANCEL_USER_SUSPENSION" }
// Delete events
| {
type: "DELETE_USER";
userId: TypesGen.User["id"];
username: TypesGen.User["username"];
}
| { type: "CONFIRM_USER_DELETE" }
| { type: "CANCEL_USER_DELETE" }
// Activate events
| {
type: "ACTIVATE_USER";
userId: TypesGen.User["id"];
username: TypesGen.User["username"];
}
| { type: "CONFIRM_USER_ACTIVATION" }
| { type: "CANCEL_USER_ACTIVATION" }
// Reset password events
| { type: "RESET_USER_PASSWORD"; userId: TypesGen.User["id"] }
| { type: "CONFIRM_USER_PASSWORD_RESET" }
| { type: "CANCEL_USER_PASSWORD_RESET" }
// Update roles events
| {
type: "UPDATE_USER_ROLES";
userId: TypesGen.User["id"];
roles: TypesGen.Role["name"][];
}
// Filter
| { type: "UPDATE_FILTER"; query: string }
// Pagination
| { type: "UPDATE_PAGE"; page: string };
export const usersMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QFdZgE6wMoBcCGOYAdLPujgJYB2UACnlNQRQPZUDEA2gAwC6ioAA4tYFSmwEgAHogC0ARgBMigBxEAnCoCsAdhWrdK9QBYdAGhABPRPO7ruRAMzdj3HYuNbFANm5b1AL4BFqgY2PiERDA4lDQAqmiY7BBsxNQAbiwA1sTRCWE8-EggwqLiVJIyCArc8upOxt6K-o7qjloq3ioW1ghKPkQu6m2KOraaw0EhieEEuWAx1FD5SRjoLOhEggA2BABmGwC2UQsrsIWSpWKsFcVVCqqOGoqOKnUqKrW15lY2ivI6IiNRqOHR6bhdXRTEChTC4OZECgQbZgdhYOJYWgAUQAcgARAD6GKxACULsUruVKnIdLoiPJjPItF42p0fI4ejYtMZjE4ASoxi4dI4lCpobDZpEkSj2HisQAZLEAFSxRKwpPJQhE1wkdxp3nqAJM3i0zm8tPknL63gNRB0TQBTNBHRM4pm8KlyNRAEEAMJKgCSADVvSq1Rq+JdtVS9dUdMYnkN1C9uM0Teofr15CpXEQPt95IXC952m6wh60l72CSseqleGSQTaN6sFgAOoAeRJeM1JWjN2pca8RH+bmar3tdUUVve3me8kcIqU3G0NrLcIilZlcVoeNDquJjZJHcVWF7lIHsdk8d5As6ybeHxLxitym4Dh5b1pYx0LhUjnXSUt1RHc9zDZsAHEsXPftdVAe4GT0IEMz0UYtAhV51BnZwHDsTQDQXZNNEAitESrUD9wJAAxAN5RVMlIwpWDbnguR5BNXlEPUbNmk+Ixp1+PpFHULQgUcbxTH8YTCy8EjNyIABjNg9godBDhWLBUEEMAqFENh2F9DscRokkAFkGwJdFMVxLAAyMmCykvVjqg8eQ82UdQJJ5DxaWZGdmUUDRWi8VM+LGbw5IRJSqBUtSNK0nS9I4X1vRxX0FQsqzsRxWz7MYrVHLg6Q5GMbQgWZbiS3tbhWktQT2P+PMBR0DMM0dLQIuCGF3Xk6LYvUxI8TAFFygMoyTPMw8CTlRUVQcnUWOKlzlEGBlHATPRhn0JkZy6XlSuMUZamEox7UiyI+tUgaMCGkabgM1L0vlCyZuVaD8r7QrFvuYwM3pewRUhTxPkzGx7XqYTGTayTtEUc7iEuuLEm9BTKHSZh9MM4yAzMiy-UDENAzyooCoWwdZBeNQfBccT0NqbkOhnHNAXaPxVHsJpTB0eHFOUq6VhRtGMeSx6Mqm-Hg1DOycXmmNnNkdC51KlqlHaYTRgErM2lvZkDUO37-ELHnYASqgICWFZklSREqEyHISFNiAVllpyltaJ5fscXivd0UqsPq0q3MLFrzRzV5MONx2LcSdg1g2LZdhwA41Id2BtLN52PovIqqhNUTOgI2xF3tUEZy5kcXXNQ6Vx8TrpnLeSIGGhZo4wK2qDSW3smIJuRrATOSc+snY1cD83hC1lPG8OqswkiHGghX8Du0Ywed7lv4hjuPNh2fYjiIdfCAHqMvsHc03PfdpPI6xkF26eqS1E5pUNUbQM1GHm8FRih0diZYY5SB3G2dtiBfyFkfRILsc6IGCiOX8vgmTsTGJ5F89UwQOBDtoU0pgPCry6hKUiYCf7ME3m3beCc94pyIb-fukCs7MTPtPIgCDhJuE+O7bwM49BPBFCYXQoxhjsTFPgnqUU+ZIwwPQWAsAADuGwIAkjgAsMa2NcZTWbK2Ts3YCQ1jrFA76bEPiDFaP+G0UkXhaFfLUNQWhsx6xeM4dweD64bjETFfmiQpGyPkYotAOAHppTFuqRsGj2xdkJLo5U+jya2MBDTWkyhaQGk6K+WwolGjuH+KVaeDIeboCUYsUh6AvFyPQBAduncQFEHyX4lYJT5HRNjKCD2RYRIgyaIoPwM5uJuXEsKDqjwVZ5IKX-OpeBpGlPKeQ3eSd941NOJ48Z3iymNOchJJ4bRFyXy6MmLo3TPIaAFJ0xcug3CMh5sgQQEASH-wwCSFgKJYAVOAd3IglzrkQLuQ8uAqy3adAaHYdiSgOivBnogf4tpWhbQBK4ZQuSRENwRO8m5Kx7mPNjugdYO9E7J2OMiz56A0U-PoafWMJo3I+AZLXcc7FLGCThXObiYxRie2DvDdgFEww0TohGQe2cDHVFsAmekvgTnsV+mkq09gHDOHWs0TpU5OpdSoCwJu8BigEPkqQPA5Alj0EYFQYWJ9h7ywXAyPMdgEzV02bSKVC47SfHfLSX65pSwItcZEaIoyZjGrlktBQSh6guAhI0aejJhQzjcKJA0Xt2g8i8Aqnm0owC+tdghO+eYTRoSLo0DM2F4xEGwZ00wbg1bc3dUBXm7iJHoE0mnRKrt+XkwlcY0wrI6jZlqP5YcbRjQeDsLoM6FbSKI2uugW6LcipNqvNk5hph1rTw6HUPZD8cxAhzEdQNp1nHdURRdcRY7BbEL9dO+WpVo2lSjQKEUjJ2hM25KtNoisvaHXNJHetZtW7oFTdAhAbxmF2HjKCIwIl1b+SZGJES75RimIBGvZu3qMA-oFQKO0edxJVW8kYXazI7ToT9nTAUrph3yWoSixIyHBxlQkrYaDnwOqaFBn0N4c5bGsm0MdcYPNR1jImT4gplGZ0SXpKaAdYJQRtHNFYjwThWoLg8JVESwy-GIeKUsyZgnnIMjcuGuVEIl2mCsamJwXsBR2BeOKuuu6PXEHxV+ol6rSZ+qqCKQYyYMNoRBsKVBvR-j-ITHPcS7DXhWc1XMTT-qATGa4jxDoK5kxWjeL0mqIJ5NczwUEIAA */
createMachine(
{
tsTypes: {} as import("./usersXService.typegen").Typegen0,
schema: {
context: {} as UsersContext,
events: {} as UsersEvent,
services: {} as {
getUsers: {
data: TypesGen.GetUsersResponse;
};
createUser: {
data: TypesGen.User;
};
suspendUser: {
data: TypesGen.User;
};
deleteUser: {
data: undefined;
};
activateUser: {
data: TypesGen.User;
};
updateUserPassword: {
data: undefined;
};
updateUserRoles: {
data: TypesGen.User;
};
},
},
predictableActionArguments: true,
id: "usersState",
on: {
UPDATE_FILTER: {
actions: ["assignFilter", "sendResetPage"],
},
UPDATE_PAGE: {
target: "gettingUsers",
actions: "updateURL",
},
},
initial: "startingPagination",
states: {
startingPagination: {
entry: "assignPaginationRef",
always: {
target: "gettingUsers",
},
},
gettingUsers: {
entry: "clearGetUsersError",
invoke: {
src: "getUsers",
id: "getUsers",
onDone: [
{
target: "idle",
actions: "assignUsers",
},
],
onError: [
{
target: "idle",
actions: ["clearUsers", "assignGetUsersError"],
},
],
},
tags: "loading",
},
idle: {
entry: "clearSelectedUser",
on: {
SUSPEND_USER: {
target: "confirmUserSuspension",
actions: "assignUserToSuspend",
},
DELETE_USER: {
target: "confirmUserDeletion",
actions: "assignUserToDelete",
},
ACTIVATE_USER: {
target: "confirmUserActivation",
actions: "assignUserToActivate",
},
RESET_USER_PASSWORD: {
target: "confirmUserPasswordReset",
actions: [
"assignUserIdToResetPassword",
"generateRandomPassword",
],
},
UPDATE_USER_ROLES: {
target: "updatingUserRoles",
actions: "assignUserIdToUpdateRoles",
},
},
},
confirmUserSuspension: {
on: {
CONFIRM_USER_SUSPENSION: {
target: "suspendingUser",
},
CANCEL_USER_SUSPENSION: {
target: "idle",
},
},
},
confirmUserDeletion: {
on: {
CONFIRM_USER_DELETE: {
target: "deletingUser",
},
CANCEL_USER_DELETE: {
target: "idle",
},
},
},
confirmUserActivation: {
on: {
CONFIRM_USER_ACTIVATION: {
target: "activatingUser",
},
CANCEL_USER_ACTIVATION: {
target: "idle",
},
},
},
suspendingUser: {
entry: "clearSuspendUserError",
invoke: {
src: "suspendUser",
id: "suspendUser",
onDone: [
{
target: "gettingUsers",
actions: "displaySuspendSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignSuspendUserError",
"displaySuspendedErrorMessage",
],
},
],
},
},
deletingUser: {
entry: "clearDeleteUserError",
invoke: {
src: "deleteUser",
id: "deleteUser",
onDone: [
{
target: "gettingUsers",
actions: "displayDeleteSuccess",
},
],
onError: [
{
target: "idle",
actions: ["assignDeleteUserError", "displayDeleteErrorMessage"],
},
],
},
},
activatingUser: {
entry: "clearActivateUserError",
invoke: {
src: "activateUser",
id: "activateUser",
onDone: [
{
target: "gettingUsers",
actions: "displayActivateSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignActivateUserError",
"displayActivatedErrorMessage",
],
},
],
},
},
confirmUserPasswordReset: {
on: {
CONFIRM_USER_PASSWORD_RESET: {
target: "resettingUserPassword",
},
CANCEL_USER_PASSWORD_RESET: {
target: "idle",
},
},
},
resettingUserPassword: {
entry: "clearResetUserPasswordError",
invoke: {
src: "resetUserPassword",
id: "resetUserPassword",
onDone: [
{
target: "idle",
actions: "displayResetPasswordSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignResetUserPasswordError",
"displayResetPasswordErrorMessage",
],
},
],
},
},
updatingUserRoles: {
entry: "clearUpdateUserRolesError",
invoke: {
src: "updateUserRoles",
id: "updateUserRoles",
onDone: [
{
target: "idle",
actions: "updateUserRolesInTheList",
},
],
onError: [
{
target: "idle",
actions: [
"assignUpdateRolesError",
"displayUpdateRolesErrorMessage",
],
},
],
},
},
},
},
{
services: {
// Passing API.getUsers directly does not invoke the function properly
// when it is mocked. This happen in the UsersPage tests inside of the
// "shows a success message and refresh the page" test case.
getUsers: (context) => {
const { offset, limit } = getPaginationData(context.paginationRef);
return API.getUsers({
...queryToFilter(context.filter),
offset,
limit,
});
},
suspendUser: (context) => {
if (!context.userIdToSuspend) {
throw new Error("userIdToSuspend is undefined");
}
return API.suspendUser(context.userIdToSuspend);
},
deleteUser: (context) => {
if (!context.userIdToDelete) {
throw new Error("userIdToDelete is undefined");
}
return API.deleteUser(context.userIdToDelete);
},
activateUser: (context) => {
if (!context.userIdToActivate) {
throw new Error("userIdToActivate is undefined");
}
return API.activateUser(context.userIdToActivate);
},
resetUserPassword: (context) => {
if (!context.userIdToResetPassword) {
throw new Error("userIdToResetPassword is undefined");
}
if (!context.newUserPassword) {
throw new Error("newUserPassword not generated");
}
return API.updateUserPassword(context.userIdToResetPassword, {
password: context.newUserPassword,
old_password: "",
});
},
updateUserRoles: (context, event) => {
if (!context.userIdToUpdateRoles) {
throw new Error("userIdToUpdateRoles is undefined");
}
return API.updateUserRoles(event.roles, context.userIdToUpdateRoles);
},
},
actions: {
clearSelectedUser: assign({
userIdToSuspend: (_) => undefined,
usernameToSuspend: (_) => undefined,
userIdToDelete: (_) => undefined,
usernameToDelete: (_) => undefined,
userIdToActivate: (_) => undefined,
usernameToActivate: (_) => undefined,
userIdToResetPassword: (_) => undefined,
userIdToUpdateRoles: (_) => undefined,
}),
assignUsers: assign({
users: (_, event) => event.data.users,
count: (_, event) => event.data.count,
}),
assignFilter: assign({
filter: (_, event) => event.query,
}),
assignGetUsersError: assign({
getUsersError: (_, event) => event.data,
}),
assignUserToSuspend: assign({
userIdToSuspend: (_, event) => event.userId,
usernameToSuspend: (_, event) => event.username,
}),
assignUserToDelete: assign({
userIdToDelete: (_, event) => event.userId,
usernameToDelete: (_, event) => event.username,
}),
assignUserToActivate: assign({
userIdToActivate: (_, event) => event.userId,
usernameToActivate: (_, event) => event.username,
}),
assignUserIdToResetPassword: assign({
userIdToResetPassword: (_, event) => event.userId,
}),
assignUserIdToUpdateRoles: assign({
userIdToUpdateRoles: (_, event) => event.userId,
}),
clearGetUsersError: assign((context: UsersContext) => ({
...context,
getUsersError: undefined,
})),
assignSuspendUserError: assign({
suspendUserError: (_, event) => event.data,
}),
assignDeleteUserError: assign({
deleteUserError: (_, event) => event.data,
}),
assignActivateUserError: assign({
activateUserError: (_, event) => event.data,
}),
assignResetUserPasswordError: assign({
resetUserPasswordError: (_, event) => event.data,
}),
assignUpdateRolesError: assign({
updateUserRolesError: (_, event) => event.data,
}),
clearUsers: assign((context: UsersContext) => ({
...context,
users: undefined,
count: undefined,
})),
clearSuspendUserError: assign({
suspendUserError: (_) => undefined,
}),
clearDeleteUserError: assign({
deleteUserError: (_) => undefined,
}),
clearActivateUserError: assign({
activateUserError: (_) => undefined,
}),
clearResetUserPasswordError: assign({
resetUserPasswordError: (_) => undefined,
}),
clearUpdateUserRolesError: assign({
updateUserRolesError: (_) => undefined,
}),
displaySuspendSuccess: () => {
displaySuccess(Language.suspendUserSuccess);
},
displaySuspendedErrorMessage: (context) => {
const message = getErrorMessage(
context.suspendUserError,
Language.suspendUserError,
);
displayError(message);
},
displayDeleteSuccess: () => {
displaySuccess(Language.deleteUserSuccess);
},
displayDeleteErrorMessage: (context) => {
const message = getErrorMessage(
context.deleteUserError,
Language.deleteUserError,
);
displayError(message);
},
displayActivateSuccess: () => {
displaySuccess(Language.activateUserSuccess);
},
displayActivatedErrorMessage: (context) => {
const message = getErrorMessage(
context.activateUserError,
Language.activateUserError,
);
displayError(message);
},
displayResetPasswordSuccess: () => {
displaySuccess(Language.resetUserPasswordSuccess);
},
displayResetPasswordErrorMessage: (context) => {
const message = getErrorMessage(
context.resetUserPasswordError,
Language.resetUserPasswordError,
);
displayError(message);
},
displayUpdateRolesErrorMessage: (context) => {
const message = getErrorMessage(
context.updateUserRolesError,
Language.updateUserRolesError,
);
displayError(message);
},
generateRandomPassword: assign({
newUserPassword: (_) => generateRandomString(12),
}),
updateUserRolesInTheList: assign({
users: ({ users }, event) => {
if (!users) {
return users;
}
return users.map((u) => {
return u.id === event.data.id ? event.data : u;
});
},
}),
assignPaginationRef: assign({
paginationRef: (context) =>
spawn(
paginationMachine.withContext(context.paginationContext),
usersPaginationId,
),
}),
sendResetPage: send({ type: "RESET_PAGE" }, { to: usersPaginationId }),
},
},
);