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 * 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -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