mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore(site): remove users and pagination services (#9932)
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
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 = () => {
|
||||
return {
|
||||
@@ -11,9 +19,12 @@ export const updatePassword = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createUser = () => {
|
||||
export const createUser = (queryClient: QueryClient) => {
|
||||
return {
|
||||
mutationFn: API.createUser,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries(["users"]);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,3 +33,49 @@ export const 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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -353,20 +353,18 @@ const PresetMenu = ({
|
||||
View advanced filtering
|
||||
</MenuItem>
|
||||
{learnMoreLink2 && learnMoreLabel2 && (
|
||||
<>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={learnMoreLink2}
|
||||
target="_blank"
|
||||
sx={{ fontSize: 13, fontWeight: 500 }}
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<OpenInNewOutlined sx={{ fontSize: "14px !important" }} />
|
||||
{learnMoreLabel2}
|
||||
</MenuItem>
|
||||
</>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={learnMoreLink2}
|
||||
target="_blank"
|
||||
sx={{ fontSize: 13, fontWeight: 500 }}
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<OpenInNewOutlined sx={{ fontSize: "14px !important" }} />
|
||||
{learnMoreLabel2}
|
||||
</MenuItem>
|
||||
)}
|
||||
</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";
|
||||
|
||||
describe("unit/PaginationWidget", () => {
|
||||
describe("buildPagedList", () => {
|
||||
it.each<{
|
||||
numPages: number;
|
||||
activePage: number;
|
||||
expected: (string | number)[];
|
||||
}>([
|
||||
{ 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: 9,
|
||||
expected: [1, "left", 8, 9, 10, "right", 17],
|
||||
},
|
||||
{
|
||||
numPages: 17,
|
||||
activePage: 17,
|
||||
expected: [1, "left", 13, 14, 15, 16, 17],
|
||||
},
|
||||
])(
|
||||
`buildPagedList($numPages, $activePage)`,
|
||||
({ numPages, activePage, expected }) => {
|
||||
expect(buildPagedList(numPages, activePage)).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
describe("buildPagedList", () => {
|
||||
it.each<{
|
||||
numPages: number;
|
||||
activePage: number;
|
||||
expected: (string | number)[];
|
||||
}>([
|
||||
{ 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: 9,
|
||||
expected: [1, "left", 8, 9, 10, "right", 17],
|
||||
},
|
||||
{
|
||||
numPages: 17,
|
||||
activePage: 17,
|
||||
expected: [1, "left", 13, 14, 15, 16, 17],
|
||||
},
|
||||
])(
|
||||
`buildPagedList($numPages, $activePage)`,
|
||||
({ numPages, activePage, expected }) => {
|
||||
expect(buildPagedList(numPages, activePage)).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
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.
|
||||
* Shamelessly stolen from:
|
||||
@@ -63,46 +56,10 @@ export const buildPagedList = (
|
||||
return range(1, numPages);
|
||||
};
|
||||
|
||||
const getInitialPage = (page: string | null): number =>
|
||||
page ? Number(page) : 1;
|
||||
|
||||
// pages count from 1
|
||||
export const getOffset = (page: number, limit: number): number =>
|
||||
(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 => {
|
||||
const page = searchParams.get("page");
|
||||
const numberPage = page ? Number(page) : 1;
|
||||
|
||||
@@ -9,6 +9,7 @@ export const usePagination = ({
|
||||
const [searchParams, setSearchParams] = searchParamsResult;
|
||||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
|
||||
const limit = DEFAULT_RECORDS_PER_PAGE;
|
||||
const offset = page <= 0 ? 0 : (page - 1) * limit;
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
searchParams.set("page", page.toString());
|
||||
@@ -19,5 +20,6 @@ export const usePagination = ({
|
||||
page,
|
||||
limit,
|
||||
goToPage,
|
||||
offset,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,9 +5,8 @@ import { useNavigate } from "react-router-dom";
|
||||
import { CreateUserForm } from "./CreateUserForm";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { getAuthMethods } from "api/api";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createUser } from "api/queries/users";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { authMethods, createUser } from "api/queries/users";
|
||||
import { displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
|
||||
export const Language = {
|
||||
@@ -17,13 +16,9 @@ export const Language = {
|
||||
export const CreateUserPage: FC = () => {
|
||||
const myOrgId = useOrganizationId();
|
||||
const navigate = useNavigate();
|
||||
const createUserMutation = useMutation(createUser());
|
||||
// TODO: We should probably place this somewhere else to reduce the number of calls.
|
||||
// This would be called each time this page is loaded.
|
||||
const { data: authMethods } = useQuery({
|
||||
queryKey: ["authMethods"],
|
||||
queryFn: getAuthMethods,
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const createUserMutation = useMutation(createUser(queryClient));
|
||||
const authMethodsQuery = useQuery(authMethods());
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
@@ -33,7 +28,7 @@ export const CreateUserPage: FC = () => {
|
||||
|
||||
<CreateUserForm
|
||||
error={createUserMutation.error}
|
||||
authMethods={authMethods}
|
||||
authMethods={authMethodsQuery.data}
|
||||
onSubmit={async (user) => {
|
||||
await createUserMutation.mutateAsync(user);
|
||||
displaySuccess("Successfully created user.");
|
||||
|
||||
@@ -3,30 +3,27 @@ import { ComponentProps, FC } from "react";
|
||||
import { Section } from "components/SettingsLayout/Section";
|
||||
import { SecurityForm } from "./SettingsSecurityForm";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { getAuthMethods, getUserLoginType } from "api/api";
|
||||
import { getUserLoginType } from "api/api";
|
||||
import {
|
||||
SingleSignOnSection,
|
||||
useSingleSignOnSection,
|
||||
} from "./SingleSignOnSection";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
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";
|
||||
|
||||
export const SecurityPage: FC = () => {
|
||||
const me = useMe();
|
||||
const updatePasswordMutation = useMutation(updatePassword());
|
||||
const { data: authMethods } = useQuery({
|
||||
queryKey: ["authMethods"],
|
||||
queryFn: getAuthMethods,
|
||||
});
|
||||
const authMethodsQuery = useQuery(authMethods());
|
||||
const { data: userLoginType } = useQuery({
|
||||
queryKey: ["loginType"],
|
||||
queryFn: getUserLoginType,
|
||||
});
|
||||
const singleSignOnSection = useSingleSignOnSection();
|
||||
|
||||
if (!authMethods || !userLoginType) {
|
||||
if (!authMethodsQuery.data || !userLoginType) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
@@ -51,7 +48,7 @@ export const SecurityPage: FC = () => {
|
||||
}}
|
||||
oidc={{
|
||||
section: {
|
||||
authMethods,
|
||||
authMethods: authMethodsQuery.data,
|
||||
userLoginType,
|
||||
...singleSignOnSection,
|
||||
},
|
||||
|
||||
@@ -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 { rest } from "msw";
|
||||
import {
|
||||
@@ -6,50 +6,38 @@ import {
|
||||
MockUser2,
|
||||
SuspendedMockUser,
|
||||
MockAuditorRole,
|
||||
MockOwnerRole,
|
||||
} from "testHelpers/entities";
|
||||
import { Language as usersXServiceLanguage } from "xServices/users/usersXService";
|
||||
import * as API from "api/api";
|
||||
import { Role } from "api/typesGenerated";
|
||||
import { Language as ResetPasswordDialogLanguage } from "./ResetPasswordDialog";
|
||||
import { renderWithAuth } from "testHelpers/renderHelpers";
|
||||
import { server } from "testHelpers/server";
|
||||
import { Language as UsersPageLanguage, UsersPage } from "./UsersPage";
|
||||
import { UsersPage } from "./UsersPage";
|
||||
|
||||
const renderPage = () => {
|
||||
return renderWithAuth(<UsersPage />);
|
||||
};
|
||||
|
||||
const suspendUser = async (setupActionSpies: () => void) => {
|
||||
const suspendUser = async () => {
|
||||
const user = userEvent.setup();
|
||||
// Get the first user in the table
|
||||
const moreButtons = await screen.findAllByLabelText("more");
|
||||
const firstMoreButton = moreButtons[0];
|
||||
|
||||
await user.click(firstMoreButton);
|
||||
|
||||
const menu = await screen.findByRole("menu");
|
||||
const suspendButton = within(menu).getByText(/Suspend/);
|
||||
|
||||
await user.click(suspendButton);
|
||||
|
||||
// Check if the confirm message is displayed
|
||||
const confirmDialog = await screen.findByRole("dialog");
|
||||
expect(confirmDialog).toHaveTextContent(
|
||||
`${UsersPageLanguage.suspendDialogMessagePrefix} ${MockUser.username}?`,
|
||||
);
|
||||
|
||||
// Setup spies to check the actions after
|
||||
setupActionSpies();
|
||||
|
||||
// Click on the "Confirm" button
|
||||
const confirmButton = await within(confirmDialog).findByText(
|
||||
UsersPageLanguage.suspendDialogAction,
|
||||
);
|
||||
const confirmButton = await within(confirmDialog).findByRole("button", {
|
||||
name: "Suspend",
|
||||
});
|
||||
await user.click(confirmButton);
|
||||
};
|
||||
|
||||
const deleteUser = async (setupActionSpies: () => void) => {
|
||||
const deleteUser = async () => {
|
||||
const user = userEvent.setup();
|
||||
// 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
|
||||
@@ -73,15 +61,12 @@ const deleteUser = async (setupActionSpies: () => void) => {
|
||||
const dialog = screen.getByRole("dialog");
|
||||
await user.type(textField, MockUser2.username);
|
||||
|
||||
// Setup spies to check the actions after
|
||||
setupActionSpies();
|
||||
|
||||
// Click on the "Confirm" button
|
||||
const confirmButton = within(dialog).getByRole("button", { name: "Delete" });
|
||||
await user.click(confirmButton);
|
||||
};
|
||||
|
||||
const activateUser = async (setupActionSpies: () => void) => {
|
||||
const activateUser = async () => {
|
||||
const moreButtons = await screen.findAllByLabelText("more");
|
||||
const suspendedMoreButton = moreButtons[2];
|
||||
fireEvent.click(suspendedMoreButton);
|
||||
@@ -92,17 +77,11 @@ const activateUser = async (setupActionSpies: () => void) => {
|
||||
|
||||
// Check if the confirm message is displayed
|
||||
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
|
||||
const confirmButton = within(confirmDialog).getByText(
|
||||
UsersPageLanguage.activateDialogAction,
|
||||
);
|
||||
const confirmButton = within(confirmDialog).getByRole("button", {
|
||||
name: "Activate",
|
||||
});
|
||||
fireEvent.click(confirmButton);
|
||||
};
|
||||
|
||||
@@ -134,7 +113,7 @@ const resetUserPassword = async (setupActionSpies: () => void) => {
|
||||
fireEvent.click(confirmButton);
|
||||
};
|
||||
|
||||
const updateUserRole = async (setupActionSpies: () => void, role: Role) => {
|
||||
const updateUserRole = async (role: Role) => {
|
||||
// Get the first user in the table
|
||||
const users = await screen.findAllByText(/.*@coder.com/);
|
||||
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");
|
||||
fireEvent.click(editButton);
|
||||
|
||||
// Setup spies to check the actions after
|
||||
setupActionSpies();
|
||||
|
||||
// Click on the role option
|
||||
const fieldset = await screen.findByTitle("Available roles");
|
||||
const auditorOption = within(fieldset).getByText(role.display_name);
|
||||
@@ -162,121 +138,93 @@ const updateUserRole = async (setupActionSpies: () => void, role: Role) => {
|
||||
describe("UsersPage", () => {
|
||||
describe("suspend user", () => {
|
||||
describe("when it is success", () => {
|
||||
it("shows a success message and refresh the page", async () => {
|
||||
it("shows a success message", async () => {
|
||||
renderPage();
|
||||
|
||||
await suspendUser(() => {
|
||||
jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser);
|
||||
jest.spyOn(API, "getUsers").mockResolvedValueOnce({
|
||||
users: [SuspendedMockUser, MockUser2],
|
||||
count: 2,
|
||||
});
|
||||
});
|
||||
server.use(
|
||||
rest.put(
|
||||
`/api/v2/users/${MockUser.id}/status/suspend`,
|
||||
async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(SuspendedMockUser));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await suspendUser();
|
||||
|
||||
// Check if the success message is displayed
|
||||
await screen.findByText(usersXServiceLanguage.suspendUserSuccess);
|
||||
|
||||
// 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));
|
||||
await screen.findByText("Successfully suspended the user.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when it fails", () => {
|
||||
it("shows an error message", async () => {
|
||||
renderPage();
|
||||
|
||||
await suspendUser(() => {
|
||||
jest.spyOn(API, "suspendUser").mockRejectedValueOnce({});
|
||||
});
|
||||
server.use(
|
||||
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
|
||||
await screen.findByText(usersXServiceLanguage.suspendUserError);
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.suspendUser).toBeCalledTimes(1);
|
||||
expect(API.suspendUser).toBeCalledWith(MockUser.id);
|
||||
await screen.findByText("Error suspending user.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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("when it is success", () => {
|
||||
it("shows a success message and refresh the page", async () => {
|
||||
it("shows a success message", async () => {
|
||||
renderPage();
|
||||
|
||||
await deleteUser(() => {
|
||||
jest.spyOn(API, "deleteUser").mockResolvedValueOnce(undefined);
|
||||
jest.spyOn(API, "getUsers").mockResolvedValueOnce({
|
||||
users: [MockUser, SuspendedMockUser],
|
||||
count: 2,
|
||||
});
|
||||
});
|
||||
server.use(
|
||||
rest.delete(
|
||||
`/api/v2/users/${MockUser2.id}`,
|
||||
async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(MockUser2));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await deleteUser();
|
||||
|
||||
// Check if the success message is displayed
|
||||
await screen.findByText(usersXServiceLanguage.deleteUserSuccess);
|
||||
|
||||
// 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);
|
||||
});
|
||||
await screen.findByText("Successfully deleted the user.");
|
||||
});
|
||||
});
|
||||
describe("when it fails", () => {
|
||||
it("shows an error message", async () => {
|
||||
renderPage();
|
||||
|
||||
await deleteUser(() => {
|
||||
jest.spyOn(API, "deleteUser").mockRejectedValueOnce({});
|
||||
});
|
||||
server.use(
|
||||
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
|
||||
await screen.findByText(usersXServiceLanguage.deleteUserError);
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.deleteUser).toBeCalledTimes(1);
|
||||
expect(API.deleteUser).toBeCalledWith(MockUser2.id);
|
||||
await screen.findByText("Error deleting user.");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -286,40 +234,43 @@ describe("UsersPage", () => {
|
||||
it("shows a success message and refreshes the page", async () => {
|
||||
renderPage();
|
||||
|
||||
await activateUser(() => {
|
||||
jest
|
||||
.spyOn(API, "activateUser")
|
||||
.mockResolvedValueOnce(SuspendedMockUser);
|
||||
jest.spyOn(API, "getUsers").mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
users: [MockUser, MockUser2, SuspendedMockUser],
|
||||
count: 3,
|
||||
}),
|
||||
);
|
||||
});
|
||||
server.use(
|
||||
rest.put(
|
||||
`/api/v2/users/${SuspendedMockUser.id}/status/activate`,
|
||||
async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(MockUser));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await activateUser();
|
||||
|
||||
// Check if the success message is displayed
|
||||
await screen.findByText(usersXServiceLanguage.activateUserSuccess);
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.activateUser).toBeCalledTimes(1);
|
||||
expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id);
|
||||
await screen.findByText("Successfully activated the user.");
|
||||
});
|
||||
});
|
||||
describe("when activation fails", () => {
|
||||
it("shows an error message", async () => {
|
||||
renderPage();
|
||||
|
||||
await activateUser(() => {
|
||||
jest.spyOn(API, "activateUser").mockRejectedValueOnce({});
|
||||
});
|
||||
server.use(
|
||||
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
|
||||
await screen.findByText(usersXServiceLanguage.activateUserError);
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.activateUser).toBeCalledTimes(1);
|
||||
expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id);
|
||||
await screen.findByText("Error activating user.");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -336,7 +287,7 @@ describe("UsersPage", () => {
|
||||
});
|
||||
|
||||
// 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
|
||||
expect(API.updateUserPassword).toBeCalledTimes(1);
|
||||
@@ -355,7 +306,7 @@ describe("UsersPage", () => {
|
||||
});
|
||||
|
||||
// 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
|
||||
expect(API.updateUserPassword).toBeCalledTimes(1);
|
||||
@@ -372,26 +323,24 @@ describe("UsersPage", () => {
|
||||
it("updates the roles", async () => {
|
||||
renderPage();
|
||||
|
||||
const { userRow } = await updateUserRole(() => {
|
||||
jest.spyOn(API, "updateUserRoles").mockResolvedValueOnce({
|
||||
...MockUser,
|
||||
roles: [...MockUser.roles, MockAuditorRole],
|
||||
});
|
||||
}, MockAuditorRole);
|
||||
|
||||
// Check if the select text was updated with the Auditor role
|
||||
await waitFor(() => {
|
||||
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,
|
||||
server.use(
|
||||
rest.put(
|
||||
`/api/v2/users/${MockUser.id}/roles`,
|
||||
async (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...MockUser,
|
||||
roles: [...MockUser.roles, MockAuditorRole],
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await updateUserRole(MockAuditorRole);
|
||||
|
||||
await screen.findByText("Successfully updated the user roles.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -399,44 +348,19 @@ describe("UsersPage", () => {
|
||||
it("shows an error message", async () => {
|
||||
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(
|
||||
rest.put(`/api/v2/users/${MockUser.id}/roles`, (req, res, ctx) => {
|
||||
return res(
|
||||
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
|
||||
const errorMessage = await screen.findByText(
|
||||
"message from the backend",
|
||||
);
|
||||
expect(errorMessage).toBeDefined();
|
||||
await screen.findByText("Error on updating the user roles.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { useMachine } from "@xstate/react";
|
||||
import { User } from "api/typesGenerated";
|
||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import {
|
||||
getPaginationContext,
|
||||
nonInitialPage,
|
||||
} from "components/PaginationWidget/utils";
|
||||
import { nonInitialPage } from "components/PaginationWidget/utils";
|
||||
import { useMe } from "hooks/useMe";
|
||||
import { usePermissions } from "hooks/usePermissions";
|
||||
import { FC, ReactNode, useEffect } from "react";
|
||||
import { FC, ReactNode, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import { usersMachine } from "xServices/users/usersXService";
|
||||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ResetPasswordDialog } from "./ResetPasswordDialog";
|
||||
import { pageTitle } from "utils/page";
|
||||
@@ -18,50 +13,41 @@ import { UsersPageView } from "./UsersPageView";
|
||||
import { useStatusFilterMenu } from "./UsersFilter";
|
||||
import { useFilter } from "components/Filter/filter";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getAuthMethods } from "api/api";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { roles } from "api/queries/roles";
|
||||
import { deploymentConfig } from "api/queries/deployment";
|
||||
|
||||
export const Language = {
|
||||
suspendDialogTitle: "Suspend user",
|
||||
suspendDialogAction: "Suspend",
|
||||
suspendDialogMessagePrefix: "Do you want to suspend the user",
|
||||
activateDialogTitle: "Activate user",
|
||||
activateDialogAction: "Activate",
|
||||
activateDialogMessagePrefix: "Do you want to activate the user",
|
||||
};
|
||||
|
||||
const getSelectedUser = (id: string, users?: User[]) =>
|
||||
users?.find((u) => u.id === id);
|
||||
import { prepareQuery } from "utils/filters";
|
||||
import { usePagination } from "hooks";
|
||||
import {
|
||||
users,
|
||||
suspendUser,
|
||||
activateUser,
|
||||
deleteUser,
|
||||
updatePassword,
|
||||
updateRoles,
|
||||
authMethods,
|
||||
} from "api/queries/users";
|
||||
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import { generateRandomString } from "utils/random";
|
||||
|
||||
export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const searchParamsResult = useSearchParams();
|
||||
const { entitlements } = useDashboard();
|
||||
const [searchParams, setSearchParams] = searchParamsResult;
|
||||
const [searchParams] = searchParamsResult;
|
||||
const filter = searchParams.get("filter") ?? "";
|
||||
const [usersState, usersSend] = useMachine(usersMachine, {
|
||||
context: {
|
||||
filter,
|
||||
paginationContext: getPaginationContext(searchParams),
|
||||
},
|
||||
actions: {
|
||||
updateURL: (context, event) =>
|
||||
setSearchParams({ page: event.page, filter: context.filter }),
|
||||
},
|
||||
const pagination = usePagination({
|
||||
searchParamsResult,
|
||||
});
|
||||
const {
|
||||
users,
|
||||
getUsersError,
|
||||
usernameToDelete,
|
||||
usernameToSuspend,
|
||||
usernameToActivate,
|
||||
userIdToResetPassword,
|
||||
newUserPassword,
|
||||
paginationRef,
|
||||
count,
|
||||
} = usersState.context;
|
||||
const usersQuery = useQuery(
|
||||
users({
|
||||
q: prepareQuery(filter),
|
||||
limit: pagination.limit,
|
||||
offset: pagination.offset,
|
||||
}),
|
||||
);
|
||||
const { updateUsers: canEditUsers, viewDeploymentValues } = usePermissions();
|
||||
const rolesQuery = useQuery({ ...roles(), enabled: canEditUsers });
|
||||
const { data: deploymentValues } = useQuery({
|
||||
@@ -77,12 +63,9 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
const useFilterResult = useFilter({
|
||||
searchParamsResult,
|
||||
onUpdate: () => {
|
||||
usersSend({ type: "UPDATE_PAGE", page: "1" });
|
||||
pagination.goToPage(1);
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
usersSend({ type: "UPDATE_FILTER", query: useFilterResult.query });
|
||||
}, [useFilterResult.query, usersSend]);
|
||||
const statusMenu = useStatusFilterMenu({
|
||||
value: useFilterResult.values.status,
|
||||
onChange: (option) =>
|
||||
@@ -91,19 +74,26 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
status: option?.value,
|
||||
}),
|
||||
});
|
||||
const authMethods = useQuery({
|
||||
queryKey: ["authMethods"],
|
||||
queryFn: () => {
|
||||
return getAuthMethods();
|
||||
},
|
||||
});
|
||||
// Is loading if
|
||||
// - users are loading or
|
||||
// - the user can edit the users but the roles are loading
|
||||
const authMethodsQuery = useQuery(authMethods());
|
||||
const isLoading =
|
||||
usersState.matches("gettingUsers") ||
|
||||
rolesQuery.isLoading ||
|
||||
authMethods.isLoading;
|
||||
usersQuery.isLoading || rolesQuery.isLoading || authMethodsQuery.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 (
|
||||
<>
|
||||
@@ -113,9 +103,8 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
<UsersPageView
|
||||
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
|
||||
roles={rolesQuery.data}
|
||||
users={users}
|
||||
authMethods={authMethods.data}
|
||||
count={count}
|
||||
users={usersQuery.data?.users}
|
||||
authMethods={authMethodsQuery.data}
|
||||
onListWorkspaces={(user) => {
|
||||
navigate(
|
||||
"/workspaces?filter=" +
|
||||
@@ -127,91 +116,90 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
"/audit?filter=" + encodeURIComponent(`username:${user.username}`),
|
||||
);
|
||||
}}
|
||||
onDeleteUser={(user) => {
|
||||
usersSend({
|
||||
type: "DELETE_USER",
|
||||
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,
|
||||
});
|
||||
}}
|
||||
onDeleteUser={setConfirmDeleteUser}
|
||||
onSuspendUser={setConfirmSuspendUser}
|
||||
onActivateUser={setConfirmActivateUser}
|
||||
onResetUserPassword={(user) => {
|
||||
usersSend({ type: "RESET_USER_PASSWORD", userId: user.id });
|
||||
}}
|
||||
onUpdateUserRoles={(user, roles) => {
|
||||
usersSend({
|
||||
type: "UPDATE_USER_ROLES",
|
||||
userId: user.id,
|
||||
roles,
|
||||
setConfirmResetPassword({
|
||||
user,
|
||||
newPassword: generateRandomString(12),
|
||||
});
|
||||
}}
|
||||
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}
|
||||
canEditUsers={canEditUsers}
|
||||
canViewActivity={entitlements.features.audit_log.enabled}
|
||||
paginationRef={paginationRef}
|
||||
isNonInitialPage={nonInitialPage(searchParams)}
|
||||
actorID={me.id}
|
||||
filterProps={{
|
||||
filter: useFilterResult,
|
||||
error: getUsersError,
|
||||
error: usersQuery.error,
|
||||
menus: {
|
||||
status: statusMenu,
|
||||
},
|
||||
}}
|
||||
count={usersQuery.data?.count}
|
||||
page={pagination.page}
|
||||
limit={pagination.limit}
|
||||
onPageChange={pagination.goToPage}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
key={usernameToDelete}
|
||||
isOpen={
|
||||
usersState.matches("confirmUserDeletion") ||
|
||||
usersState.matches("deletingUser")
|
||||
}
|
||||
confirmLoading={usersState.matches("deletingUser")}
|
||||
name={usernameToDelete ?? ""}
|
||||
key={confirmDeleteUser?.username}
|
||||
isOpen={confirmDeleteUser !== undefined}
|
||||
confirmLoading={deleteUserMutation.isLoading}
|
||||
name={confirmDeleteUser?.username ?? ""}
|
||||
entity="user"
|
||||
onConfirm={() => {
|
||||
usersSend("CONFIRM_USER_DELETE");
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await deleteUserMutation.mutateAsync(confirmDeleteUser!.id);
|
||||
setConfirmDeleteUser(undefined);
|
||||
displaySuccess("Successfully deleted the user.");
|
||||
} catch (e) {
|
||||
displayError(getErrorMessage(e, "Error deleting user."));
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
usersSend("CANCEL_USER_DELETE");
|
||||
setConfirmDeleteUser(undefined);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={
|
||||
usersState.matches("confirmUserSuspension") ||
|
||||
usersState.matches("suspendingUser")
|
||||
}
|
||||
confirmLoading={usersState.matches("suspendingUser")}
|
||||
title={Language.suspendDialogTitle}
|
||||
confirmText={Language.suspendDialogAction}
|
||||
onConfirm={() => {
|
||||
usersSend("CONFIRM_USER_SUSPENSION");
|
||||
open={confirmSuspendUser !== undefined}
|
||||
confirmLoading={suspendUserMutation.isLoading}
|
||||
title="Suspend user"
|
||||
confirmText="Suspend"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await suspendUserMutation.mutateAsync(confirmSuspendUser!.id);
|
||||
setConfirmSuspendUser(undefined);
|
||||
displaySuccess("Successfully suspended the user.");
|
||||
} catch (e) {
|
||||
displayError(getErrorMessage(e, "Error suspending user."));
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
usersSend("CANCEL_USER_SUSPENSION");
|
||||
setConfirmSuspendUser(undefined);
|
||||
}}
|
||||
description={
|
||||
<>
|
||||
{Language.suspendDialogMessagePrefix}
|
||||
{usernameToSuspend && " "}
|
||||
<strong>{usernameToSuspend ?? ""}</strong>?
|
||||
Do you want to suspend the user{" "}
|
||||
<strong>{confirmSuspendUser?.username ?? ""}</strong>?
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -219,45 +207,55 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
<ConfirmDialog
|
||||
type="success"
|
||||
hideCancel={false}
|
||||
open={
|
||||
usersState.matches("confirmUserActivation") ||
|
||||
usersState.matches("activatingUser")
|
||||
}
|
||||
confirmLoading={usersState.matches("activatingUser")}
|
||||
title={Language.activateDialogTitle}
|
||||
confirmText={Language.activateDialogAction}
|
||||
onConfirm={() => {
|
||||
usersSend("CONFIRM_USER_ACTIVATION");
|
||||
open={confirmActivateUser !== undefined}
|
||||
confirmLoading={activateUserMutation.isLoading}
|
||||
title="Activate user"
|
||||
confirmText="Activate"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await activateUserMutation.mutateAsync(confirmActivateUser!.id);
|
||||
setConfirmActivateUser(undefined);
|
||||
displaySuccess("Successfully activated the user.");
|
||||
} catch (e) {
|
||||
displayError(getErrorMessage(e, "Error activating user."));
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
usersSend("CANCEL_USER_ACTIVATION");
|
||||
setConfirmActivateUser(undefined);
|
||||
}}
|
||||
description={
|
||||
<>
|
||||
{Language.activateDialogMessagePrefix}
|
||||
{usernameToActivate && " "}
|
||||
<strong>{usernameToActivate ?? ""}</strong>?
|
||||
Do you want to activate{" "}
|
||||
<strong>{confirmActivateUser?.username ?? ""}</strong>?
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{userIdToResetPassword && (
|
||||
<ResetPasswordDialog
|
||||
open={
|
||||
usersState.matches("confirmUserPasswordReset") ||
|
||||
usersState.matches("resettingUserPassword")
|
||||
<ResetPasswordDialog
|
||||
key={confirmResetPassword?.user.username}
|
||||
open={confirmResetPassword !== undefined}
|
||||
loading={updatePasswordMutation.isLoading}
|
||||
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 { createPaginationRef } from "components/PaginationWidget/utils";
|
||||
import {
|
||||
MockUser,
|
||||
MockUser2,
|
||||
@@ -30,7 +29,8 @@ const meta: Meta<typeof UsersPageView> = {
|
||||
title: "pages/UsersPageView",
|
||||
component: UsersPageView,
|
||||
args: {
|
||||
paginationRef: createPaginationRef({ page: 1, limit: 25 }),
|
||||
page: 1,
|
||||
limit: 25,
|
||||
isNonInitialPage: false,
|
||||
users: [MockUser, MockUser2],
|
||||
roles: MockAssignableSiteRoles,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { PaginationWidget } from "components/PaginationWidget/PaginationWidget";
|
||||
import { ComponentProps, FC } from "react";
|
||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService";
|
||||
import * as TypesGen from "api/typesGenerated";
|
||||
import { UsersTable } from "./UsersTable/UsersTable";
|
||||
import { UsersFilter } from "./UsersFilter";
|
||||
@@ -8,10 +6,10 @@ import {
|
||||
PaginationStatus,
|
||||
TableToolbar,
|
||||
} from "components/TableToolbar/TableToolbar";
|
||||
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase";
|
||||
|
||||
export interface UsersPageViewProps {
|
||||
users?: TypesGen.User[];
|
||||
count?: number;
|
||||
roles?: TypesGen.AssignableRoles[];
|
||||
isUpdatingUserRoles?: boolean;
|
||||
canEditUsers?: boolean;
|
||||
@@ -30,14 +28,17 @@ export interface UsersPageViewProps {
|
||||
roles: TypesGen.Role["name"][],
|
||||
) => void;
|
||||
filterProps: ComponentProps<typeof UsersFilter>;
|
||||
paginationRef: PaginationMachineRef;
|
||||
isNonInitialPage: boolean;
|
||||
actorID: string;
|
||||
// Pagination
|
||||
count?: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
||||
users,
|
||||
count,
|
||||
roles,
|
||||
onSuspendUser,
|
||||
onDeleteUser,
|
||||
@@ -52,10 +53,13 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
||||
canViewActivity,
|
||||
isLoading,
|
||||
filterProps,
|
||||
paginationRef,
|
||||
isNonInitialPage,
|
||||
actorID,
|
||||
authMethods,
|
||||
count,
|
||||
limit,
|
||||
onPageChange,
|
||||
page,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@@ -90,7 +94,14 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
||||
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,
|
||||
|
||||
"&:disabled": {
|
||||
opacity: 0,
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
|
||||
@@ -341,6 +341,10 @@ export const handlers = [
|
||||
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(
|
||||
"/api/v2/workspacebuilds/:workspaceBuildId/parameters",
|
||||
(_, res, ctx) => {
|
||||
|
||||
@@ -3,12 +3,15 @@ import * as TypesGen from "api/typesGenerated";
|
||||
export const queryToFilter = (
|
||||
query?: string,
|
||||
): TypesGen.WorkspaceFilter | TypesGen.UsersRequest => {
|
||||
const preparedQuery = query?.trim().replace(/ +/g, " ");
|
||||
return {
|
||||
q: preparedQuery,
|
||||
q: prepareQuery(query),
|
||||
};
|
||||
};
|
||||
|
||||
export const prepareQuery = (query?: string) => {
|
||||
return query?.trim().replace(/ +/g, " ");
|
||||
};
|
||||
|
||||
export const workspaceFilterQuery = {
|
||||
me: "owner:me",
|
||||
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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -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 }),
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user