diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 6a23045ff9401..b486dd2696699 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1282,7 +1282,7 @@ class ApiMethods { updateUserPassword = async ( userId: TypesGen.User["id"], updatePassword: TypesGen.UpdateUserPasswordRequest, - ): Promise => { + ): Promise => { await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword); }; diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 4ec1ba39b726f..62449af12fccf 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -1,8 +1,10 @@ import { API } from "api/api"; +export const deploymentConfigQueryKey = ["deployment", "config"]; + export const deploymentConfig = () => { return { - queryKey: ["deployment", "config"], + queryKey: deploymentConfigQueryKey, queryFn: API.getDeploymentConfig, }; }; diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index 34159d263aa7e..ddc3d7e3b921c 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -8,7 +8,7 @@ import type { QueryClient, UseQueryOptions } from "react-query"; type GroupSortOrder = "asc" | "desc"; -const groupsQueryKey = ["groups"]; +export const groupsQueryKey = ["groups"]; export const groups = () => { return { @@ -49,27 +49,29 @@ export type GroupsByUserId = Readonly>; export function groupsByUserId() { return { ...groups(), - select: (allGroups) => { - // Sorting here means that nothing has to be sorted for the individual - // user arrays later - const sorted = sortGroupsByName(allGroups, "asc"); - const userIdMapper = new Map(); - - for (const group of sorted) { - for (const user of group.members) { - let groupsForUser = userIdMapper.get(user.id); - if (groupsForUser === undefined) { - groupsForUser = []; - userIdMapper.set(user.id, groupsForUser); - } - - groupsForUser.push(group); - } + select: selectGroupsByUserId, + } satisfies UseQueryOptions; +} + +export function selectGroupsByUserId(groups: Group[]): GroupsByUserId { + // Sorting here means that nothing has to be sorted for the individual + // user arrays later + const sorted = sortGroupsByName(groups, "asc"); + const userIdMapper = new Map(); + + for (const group of sorted) { + for (const user of group.members) { + let groupsForUser = userIdMapper.get(user.id); + if (groupsForUser === undefined) { + groupsForUser = []; + userIdMapper.set(user.id, groupsForUser); } - return userIdMapper as GroupsByUserId; - }, - } satisfies UseQueryOptions; + groupsForUser.push(group); + } + } + + return userIdMapper as GroupsByUserId; } export function groupsForUser(userId: string) { diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts index 97e5e29eea448..3d389237ff451 100644 --- a/site/src/api/queries/roles.ts +++ b/site/src/api/queries/roles.ts @@ -9,9 +9,11 @@ const getRoleQueryKey = (organizationId: string, roleName: string) => [ roleName, ]; +export const rolesQueryKey = ["roles"]; + export const roles = () => { return { - queryKey: ["roles"], + queryKey: rolesQueryKey, queryFn: API.getRoles, }; }; diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index d25fe3cbe9e8d..427054b3fe5e2 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -115,11 +115,13 @@ export const updateRoles = (queryClient: QueryClient) => { }; }; +export const authMethodsQueryKey = ["authMethods"]; + 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"], + queryKey: authMethodsQueryKey, queryFn: API.getAuthMethods, }; }; diff --git a/site/src/components/Filter/filter.tsx b/site/src/components/Filter/filter.tsx index 4ace1daab0bb8..c8636c3267e49 100644 --- a/site/src/components/Filter/filter.tsx +++ b/site/src/components/Filter/filter.tsx @@ -294,21 +294,21 @@ const PresetMenu: FC = ({ ))} {learnMoreLink && ( - <> - - { - setIsOpen(false); - }} - > - - View advanced filtering - - + + )} + {learnMoreLink && ( + { + setIsOpen(false); + }} + > + + View advanced filtering + )} {learnMoreLink2 && learnMoreLabel2 && ( { } // Click on the "edit icon" to display the role options - const editButton = within(userRow).getByTitle("Edit user roles"); + const editButton = within(userRow).getByLabelText("Edit user roles"); fireEvent.click(editButton); // Click on the role option diff --git a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx b/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx index 5aaf1f39eea56..ed8712f4c6f73 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx +++ b/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx @@ -2,6 +2,7 @@ import type { Interpolation, Theme } from "@emotion/react"; import UserIcon from "@mui/icons-material/PersonOutline"; import Checkbox from "@mui/material/Checkbox"; import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; import type { SlimRole } from "api/typesGenerated"; import { HelpTooltip, @@ -116,13 +117,15 @@ export const EditRolesButton: FC = ({ return ( - - - + + + + + diff --git a/site/src/pages/UsersPage/UsersPage.stories.tsx b/site/src/pages/UsersPage/UsersPage.stories.tsx new file mode 100644 index 0000000000000..fda6234668559 --- /dev/null +++ b/site/src/pages/UsersPage/UsersPage.stories.tsx @@ -0,0 +1,397 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { spyOn, userEvent, within } from "@storybook/test"; +import { API } from "api/api"; +import { deploymentConfigQueryKey } from "api/queries/deployment"; +import { groupsQueryKey } from "api/queries/groups"; +import { rolesQueryKey } from "api/queries/roles"; +import { authMethodsQueryKey, usersKey } from "api/queries/users"; +import type { User } from "api/typesGenerated"; +import { MockGroups } from "pages/UsersPage/storybookData/groups"; +import { MockRoles } from "pages/UsersPage/storybookData/roles"; +import { MockUsers } from "pages/UsersPage/storybookData/users"; +import { MockAuthMethodsAll, MockUser } from "testHelpers/entities"; +import { + withAuthProvider, + withDashboardProvider, + withGlobalSnackbar, +} from "testHelpers/storybook"; +import UsersPage from "./UsersPage"; + +const parameters = { + queries: [ + // This query loads users for the filter menu, not for the table + { + key: usersKey({ limit: 25, offset: 25, q: "" }), + data: { + users: [], + count: 60, + }, + }, + // Users for the table + { + key: usersKey({ limit: 25, offset: 0, q: "" }), + data: { + users: MockUsers, + count: 60, + }, + }, + { + key: groupsQueryKey, + data: MockGroups, + }, + { + key: authMethodsQueryKey, + data: MockAuthMethodsAll, + }, + { + key: rolesQueryKey, + data: MockRoles, + }, + { + key: deploymentConfigQueryKey, + data: { + config: { + oidc: { + user_role_field: "role", + }, + }, + options: [], + }, + }, + ], + user: MockUser, + permissions: { + createUser: true, + updateUsers: true, + viewDeploymentValues: true, + }, +}; + +const meta: Meta = { + title: "pages/UsersPage", + component: UsersPage, + parameters, + decorators: [withGlobalSnackbar, withAuthProvider, withDashboardProvider], +}; + +export default meta; +type Story = StoryObj; + +export const Loaded: Story = {}; + +export const SuspendUserSuccess: Story = { + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + + // Return the updated user in the suspended response and ensure the users + // query will return updated data. + const updatedUser: User = { ...MockUsers[0], status: "suspended" }; + spyOn(API, "suspendUser").mockResolvedValue(updatedUser); + spyOn(API, "getUsers").mockResolvedValue({ + users: replaceUser(MockUsers, 0, updatedUser), + count: 60, + }); + + await user.click(within(userRow).getByLabelText("More options")); + const suspendButton = await within(userRow).findByText("Suspend", { + exact: false, + }); + await user.click(suspendButton); + + const dialog = await within(document.body).findByRole("dialog"); + await user.click(within(dialog).getByRole("button", { name: "Suspend" })); + await within(document.body).findByText("Successfully suspended the user."); + }, +}; + +export const SuspendUserError: Story = { + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + spyOn(API, "suspendUser").mockRejectedValue(undefined); + + await user.click(within(userRow).getByLabelText("More options")); + const suspendButton = await within(userRow).findByText("Suspend", { + exact: false, + }); + await user.click(suspendButton); + + const dialog = await within(document.body).findByRole("dialog"); + await user.click(within(dialog).getByRole("button", { name: "Suspend" })); + await within(document.body).findByText("Error suspending user."); + }, +}; + +export const DeleteUserSuccess: Story = { + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + + // The delete user operation does not return a value. However, we need to + // ensure that the updated list of users, excluding the deleted one, is + // returned when the users query is refetched. + spyOn(API, "deleteUser").mockResolvedValue(); + spyOn(API, "getUsers").mockResolvedValue({ + users: MockUsers.slice(1), + count: 59, + }); + + await user.click(within(userRow).getByLabelText("More options")); + const deleteButton = await within(userRow).findByText("Delete", { + exact: false, + }); + await user.click(deleteButton); + + const dialog = await within(document.body).findByRole("dialog"); + const input = within(dialog).getByLabelText("Name of the user to delete"); + await user.type(input, MockUsers[0].username); + await user.click(within(dialog).getByRole("button", { name: "Delete" })); + await within(document.body).findByText("Successfully deleted the user."); + }, +}; + +export const DeleteUserError: Story = { + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + spyOn(API, "deleteUser").mockRejectedValue({}); + + await user.click(within(userRow).getByLabelText("More options")); + const deleteButton = await within(userRow).findByText("Delete", { + exact: false, + }); + await user.click(deleteButton); + + const dialog = await within(document.body).findByRole("dialog"); + const input = within(dialog).getByLabelText("Name of the user to delete"); + await user.type(input, MockUsers[0].username); + await user.click(within(dialog).getByRole("button", { name: "Delete" })); + await within(document.body).findByText("Error deleting user."); + }, +}; + +export const ActivateUserSuccess: Story = { + parameters: { + queries: [ + ...parameters.queries, + // To activate a user, the user must be suspended first. Since we use the + // first user in the test, we need to ensure it is suspended. + { + key: usersKey({ limit: 25, offset: 0, q: "" }), + data: { + users: replaceUser(MockUsers, 0, { + ...MockUsers[0], + status: "suspended", + }), + count: 60, + }, + }, + ], + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + + // Return the updated user in the activate response and ensure the users + // query will return updated data. + const updatedUser: User = { ...MockUsers[0], status: "active" }; + spyOn(API, "activateUser").mockResolvedValue(updatedUser); + spyOn(API, "getUsers").mockResolvedValue({ + users: replaceUser(MockUsers, 0, updatedUser), + count: 60, + }); + + await user.click(within(userRow).getByLabelText("More options")); + const activateButton = await within(userRow).findByText("Activate", { + exact: false, + }); + await user.click(activateButton); + + const dialog = await within(document.body).findByRole("dialog"); + await user.click(within(dialog).getByRole("button", { name: "Activate" })); + await within(document.body).findByText("Successfully activated the user."); + }, +}; + +export const ActivateUserError: Story = { + parameters: ActivateUserSuccess.parameters, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + spyOn(API, "activateUser").mockRejectedValue({}); + + await user.click(within(userRow).getByLabelText("More options")); + const activateButton = await within(userRow).findByText("Activate", { + exact: false, + }); + await user.click(activateButton); + + const dialog = await within(document.body).findByRole("dialog"); + await user.click(within(dialog).getByRole("button", { name: "Activate" })); + await within(document.body).findByText("Error activating user."); + }, +}; + +export const ResetUserPasswordSuccess: Story = { + parameters: { + queries: [ + ...parameters.queries, + // Ensure the first user's login type is set to 'password' to reset their + // password during the test. + { + key: usersKey({ limit: 25, offset: 0, q: "" }), + data: { + users: MockUsers.map((u, i) => { + return i === 0 ? { ...u, login_type: "password" } : u; + }), + count: 60, + }, + }, + ], + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + spyOn(API, "updateUserPassword").mockResolvedValue(); + + await user.click(within(userRow).getByLabelText("More options")); + const resetPasswordButton = await within(userRow).findByText( + "Reset password", + { exact: false }, + ); + await user.click(resetPasswordButton); + + const dialog = await within(document.body).findByRole("dialog"); + await user.click( + within(dialog).getByRole("button", { name: "Reset password" }), + ); + await within(document.body).findByText( + "Successfully updated the user password.", + ); + }, +}; + +export const ResetUserPasswordError: Story = { + parameters: ResetUserPasswordSuccess.parameters, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + spyOn(API, "updateUserPassword").mockRejectedValue({}); + + await user.click(within(userRow).getByLabelText("More options")); + const resetPasswordButton = await within(userRow).findByText( + "Reset password", + { exact: false }, + ); + await user.click(resetPasswordButton); + + const dialog = await within(document.body).findByRole("dialog"); + await user.click( + within(dialog).getByRole("button", { name: "Reset password" }), + ); + await within(document.body).findByText( + "Error on resetting the user password.", + ); + }, +}; + +export const UpdateUserRoleSuccess: Story = { + parameters: { + queries: [ + ...parameters.queries, + // Ensure the first user has the 'owner' role to test the edit functionality. + { + key: usersKey({ limit: 25, offset: 0, q: "" }), + data: { + users: replaceUser(MockUsers, 0, { + ...MockUsers[0], + roles: [ + { name: "owner", display_name: "Owner" }, + // We will update the user role to include auditor + { name: "auditor", display_name: "Auditor" }, + ], + }), + count: 60, + }, + }, + ], + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + + // Return the updated user in the update roles response and ensure the users + // query will return updated data. + const updatedUser: User = { + ...MockUsers[0], + roles: [ + { name: "owner", display_name: "Owner" }, + // We will update the user role to include auditor + { name: "auditor", display_name: "Auditor" }, + ], + }; + spyOn(API, "updateUserRoles").mockResolvedValue(updatedUser); + spyOn(API, "getUsers").mockResolvedValue({ + users: replaceUser(MockUsers, 0, updatedUser), + count: 60, + }); + + await user.click(within(userRow).getByLabelText("Edit user roles")); + await user.click( + within(userRow).getByLabelText("Auditor", { exact: false }), + ); + await within(document.body).findByText( + "Successfully updated the user roles.", + ); + }, +}; + +export const UpdateUserRoleError: Story = { + parameters: UpdateUserRoleSuccess.parameters, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const userRow = canvasElement.querySelector("tbody tr"); + if (!userRow) { + throw new Error("No user row found"); + } + spyOn(API, "updateUserRoles").mockRejectedValue({}); + + await user.click(within(userRow).getByLabelText("Edit user roles")); + await user.click( + within(userRow).getByLabelText("Auditor", { exact: false }), + ); + await within(document.body).findByText("Error on updating the user roles."); + }, +}; + +function replaceUser(users: User[], index: number, user: User) { + return users.map((u, i) => (i === index ? user : u)); +} diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx deleted file mode 100644 index c507ae77c5e71..0000000000000 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ /dev/null @@ -1,340 +0,0 @@ -import { fireEvent, screen, within } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { API } from "api/api"; -import type { SlimRole } from "api/typesGenerated"; -import { http, HttpResponse } from "msw"; -import { - MockAuditorRole, - MockUser, - MockUser2, - SuspendedMockUser, -} from "testHelpers/entities"; -import { renderWithAuth } from "testHelpers/renderHelpers"; -import { server } from "testHelpers/server"; -import { Language as ResetPasswordDialogLanguage } from "./ResetPasswordDialog"; -import UsersPage from "./UsersPage"; - -jest.spyOn(console, "error").mockImplementation(() => {}); - -const renderPage = () => { - return renderWithAuth(); -}; - -const suspendUser = async () => { - const user = userEvent.setup(); - // Get the first user in the table - const moreButtons = await screen.findAllByLabelText("More options"); - const firstMoreButton = moreButtons[0]; - await user.click(firstMoreButton); - - const suspendButton = screen.getByTestId("suspend-button"); - await user.click(suspendButton); - - // Check if the confirm message is displayed - const confirmDialog = await screen.findByRole("dialog"); - const confirmButton = await within(confirmDialog).findByRole("button", { - name: "Suspend", - }); - await user.click(confirmButton); -}; - -const deleteUser = async () => { - const user = userEvent.setup(); - // Click on the "More options" button to display the "Delete" option - // Needs to await fetching users and fetching permissions, because they're needed to see the more button - const moreButtons = await screen.findAllByLabelText("More options"); - // get MockUser2 - const selectedMoreButton = moreButtons[1]; - - await user.click(selectedMoreButton); - - const deleteButton = screen.getByText(/Delete/); - await user.click(deleteButton); - - // Confirm with text input - const textField = screen.getByLabelText("Name of the user to delete"); - const dialog = screen.getByRole("dialog"); - await user.type(textField, MockUser2.username); - - // Click on the "Confirm" button - const confirmButton = within(dialog).getByRole("button", { name: "Delete" }); - await user.click(confirmButton); -}; - -const activateUser = async () => { - const moreButtons = await screen.findAllByLabelText("More options"); - const suspendedMoreButton = moreButtons[2]; - fireEvent.click(suspendedMoreButton); - - const activateButton = screen.getByText(/Activate/); - fireEvent.click(activateButton); - - // Check if the confirm message is displayed - const confirmDialog = screen.getByRole("dialog"); - - // Click on the "Confirm" button - const confirmButton = within(confirmDialog).getByRole("button", { - name: "Activate", - }); - fireEvent.click(confirmButton); -}; - -const resetUserPassword = async (setupActionSpies: () => void) => { - const moreButtons = await screen.findAllByLabelText("More options"); - const firstMoreButton = moreButtons[0]; - fireEvent.click(firstMoreButton); - - const resetPasswordButton = screen.getByText(/Reset password/); - fireEvent.click(resetPasswordButton); - - // Check if the confirm message is displayed - const confirmDialog = screen.getByRole("dialog"); - expect(confirmDialog).toHaveTextContent( - `You will need to send ${MockUser.username} the following password:`, - ); - - // Setup spies to check the actions after - setupActionSpies(); - - // Click on the "Confirm" button - const confirmButton = within(confirmDialog).getByRole("button", { - name: ResetPasswordDialogLanguage.confirmText, - }); - - fireEvent.click(confirmButton); -}; - -const updateUserRole = async (role: SlimRole) => { - // Get the first user in the table - const users = await screen.findAllByText(/.*@coder.com/); - const userRow = users[0].closest("tr"); - if (!userRow) { - throw new Error("Error on get the first user row"); - } - - // Click on the "edit icon" to display the role options - const editButton = within(userRow).getByTitle("Edit user roles"); - fireEvent.click(editButton); - - // Click on the role option - const fieldset = await screen.findByTitle("Available roles"); - const roleOption = within(fieldset).getByText(role.display_name); - fireEvent.click(roleOption); - - return { - userRow, - }; -}; - -describe("UsersPage", () => { - describe("suspend user", () => { - describe("when it is success", () => { - it("shows a success message", async () => { - renderPage(); - - server.use( - http.put(`/api/v2/users/${MockUser.id}/status/suspend`, async () => { - return HttpResponse.json(SuspendedMockUser); - }), - ); - - await suspendUser(); - - // Check if the success message is displayed - await screen.findByText("Successfully suspended the user."); - }); - }); - - describe("when it fails", () => { - it("shows an error message", async () => { - renderPage(); - - server.use( - http.put(`/api/v2/users/${MockUser.id}/status/suspend`, async () => { - return HttpResponse.json( - { - message: "Error suspending user.", - }, - { status: 400 }, - ); - }), - ); - - await suspendUser(); - - // Check if the error message is displayed - await screen.findByText("Error suspending user."); - }); - }); - }); - - describe("delete user", () => { - describe("when it is success", () => { - it("shows a success message", async () => { - renderPage(); - - server.use( - http.delete(`/api/v2/users/${MockUser2.id}`, async () => { - return HttpResponse.json(MockUser2); - }), - ); - - await deleteUser(); - - // Check if the success message is displayed - await screen.findByText("Successfully deleted the user."); - }); - }); - describe("when it fails", () => { - it("shows an error message", async () => { - renderPage(); - - server.use( - http.delete(`/api/v2/users/${MockUser2.id}`, async () => { - return HttpResponse.json( - { - message: "Error deleting user.", - }, - { status: 400 }, - ); - }), - ); - - await deleteUser(); - - // Check if the error message is displayed - await screen.findByText("Error deleting user."); - }); - }); - }); - - describe("activate user", () => { - describe("when user is successfully activated", () => { - it("shows a success message and refreshes the page", async () => { - renderPage(); - - server.use( - http.put( - `/api/v2/users/${SuspendedMockUser.id}/status/activate`, - async () => { - return HttpResponse.json(MockUser); - }, - ), - ); - - await activateUser(); - - // Check if the success message is displayed - await screen.findByText("Successfully activated the user."); - }); - }); - describe("when activation fails", () => { - it("shows an error message", async () => { - renderPage(); - - server.use( - http.put( - `/api/v2/users/${SuspendedMockUser.id}/status/activate`, - async () => { - return HttpResponse.json( - { - message: "Error activating user.", - }, - { status: 400 }, - ); - }, - ), - ); - - await activateUser(); - - // Check if the error message is displayed - await screen.findByText("Error activating user."); - }); - }); - }); - - describe("reset user password", () => { - describe("when it is success", () => { - it("shows a success message", async () => { - renderPage(); - - await resetUserPassword(() => { - jest - .spyOn(API, "updateUserPassword") - .mockResolvedValueOnce(undefined); - }); - - // Check if the success message is displayed - await screen.findByText("Successfully updated the user password."); - - // Check if the API was called correctly - expect(API.updateUserPassword).toBeCalledTimes(1); - expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { - password: expect.any(String), - old_password: "", - }); - }); - }); - describe("when it fails", () => { - it("shows an error message", async () => { - renderPage(); - - await resetUserPassword(() => { - jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({}); - }); - - // Check if the error message is displayed - await screen.findByText("Error on resetting the user password."); - - // Check if the API was called correctly - expect(API.updateUserPassword).toBeCalledTimes(1); - expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { - password: expect.any(String), - old_password: "", - }); - }); - }); - }); - - describe("Update user role", () => { - describe("when it is success", () => { - it("updates the roles", async () => { - renderPage(); - - server.use( - http.put(`/api/v2/users/${MockUser.id}/roles`, async () => { - return HttpResponse.json({ - ...MockUser, - roles: [...MockUser.roles, MockAuditorRole], - }); - }), - ); - - await updateUserRole(MockAuditorRole); - - await screen.findByText("Successfully updated the user roles."); - }); - }); - - describe("when it fails", () => { - it("shows an error message", async () => { - renderPage(); - - server.use( - http.put(`/api/v2/users/${MockUser.id}/roles`, () => { - return HttpResponse.json( - { message: "Error on updating the user roles." }, - { status: 400 }, - ); - }), - ); - - await updateUserRole(MockAuditorRole); - - // Check if the error message is displayed - await screen.findByText("Error on updating the user roles."); - }); - }); - }); -}); diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 363bc72627794..95717909ac205 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -172,8 +172,11 @@ const UsersPage: FC = () => { entity="user" onCancel={() => setUserToDelete(undefined)} onConfirm={async () => { + if (!userToDelete) { + return; + } try { - await deleteUserMutation.mutateAsync(userToDelete!.id); + await deleteUserMutation.mutateAsync(userToDelete.id); setUserToDelete(undefined); displaySuccess("Successfully deleted the user."); } catch (e) { @@ -191,8 +194,11 @@ const UsersPage: FC = () => { confirmText="Suspend" onClose={() => setUserToSuspend(undefined)} onConfirm={async () => { + if (!userToSuspend) { + return; + } try { - await suspendUserMutation.mutateAsync(userToSuspend!.id); + await suspendUserMutation.mutateAsync(userToSuspend.id); setUserToSuspend(undefined); displaySuccess("Successfully suspended the user."); } catch (e) { @@ -216,8 +222,11 @@ const UsersPage: FC = () => { confirmText="Activate" onClose={() => setUserToActivate(undefined)} onConfirm={async () => { + if (!userToActivate) { + return; + } try { - await activateUserMutation.mutateAsync(userToActivate!.id); + await activateUserMutation.mutateAsync(userToActivate.id); setUserToActivate(undefined); displaySuccess("Successfully activated the user."); } catch (e) { @@ -242,10 +251,13 @@ const UsersPage: FC = () => { setConfirmResetPassword(undefined); }} onConfirm={async () => { + if (!confirmResetPassword) { + return; + } try { await updatePasswordMutation.mutateAsync({ - userId: confirmResetPassword!.user.id, - password: confirmResetPassword!.newPassword, + userId: confirmResetPassword.user.id, + password: confirmResetPassword.newPassword, old_password: "", }); setConfirmResetPassword(undefined); diff --git a/site/src/pages/UsersPage/UsersPageView.stories.tsx b/site/src/pages/UsersPage/UsersPageView.stories.tsx index 22db9d994f2dd..ba7bf7599da15 100644 --- a/site/src/pages/UsersPage/UsersPageView.stories.tsx +++ b/site/src/pages/UsersPage/UsersPageView.stories.tsx @@ -28,7 +28,7 @@ const defaultFilterProps = getDefaultFilterProps({ }); const meta: Meta = { - title: "pages/UsersPage", + title: "pages/UsersPageView", component: UsersPageView, args: { isNonInitialPage: false, diff --git a/site/src/pages/UsersPage/storybookData/groups.ts b/site/src/pages/UsersPage/storybookData/groups.ts new file mode 100644 index 0000000000000..f3f171e8a51bc --- /dev/null +++ b/site/src/pages/UsersPage/storybookData/groups.ts @@ -0,0 +1,544 @@ +import type { Group, GroupSource, User } from "api/typesGenerated"; +import { MockUsers } from "./users"; + +function findMockedUserById(id: string): User | undefined { + const user = MockUsers.find((user) => user.id === id); + return user; +} + +// These values were retrieved from the Coder API. Sensitive information such as +// usernames, names, and emails has been replaced with fake user data to protect +// privacy. This user data comes from the MockUsers to keep consistency. +export const MockGroups: Group[] = [ + { + id: "7621bbb4-5b04-4957-8419-cf4a683ac59a", + name: "Everyone", + display_name: "", + organization_id: "7621bbb4-5b04-4957-8419-cf4a683ac59a", + members: [findMockedUserById("5ccd3128-cbbb-4cfb-8139-5a1edbb60c71")], + total_member_count: 1, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "data-platform", + organization_display_name: "Data Platform", + }, + { + id: "d6f9d037-b9cd-4716-b424-64ef8f81e7f7", + name: "Contractors", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("c5b28cf0-f72a-4b45-b3d5-00d3989d4ed5"), + findMockedUserById("4c8ffd9b-7bc8-47db-9322-f4d5e0b51658"), + ], + total_member_count: 2, + avatar_url: "", + quota_allowance: 10, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "82773b01-c3b3-4747-a956-d824293dd857", + name: "Empty-other", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("af657bc3-6949-4b1b-bc2d-d41a40b546a4"), + findMockedUserById("a73425d1-53a7-43d3-b6ae-cae9ba59b92b"), + ], + total_member_count: 2, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "4522d478-04b2-4ced-9cf2-8f2ecbe73188", + name: "DevOps", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("3f8c0eef-6a45-4759-a4d6-d00bbffb1369"), + findMockedUserById("740bba7f-356d-4203-8f15-03ddee381998"), + findMockedUserById("12b03f43-1bb7-4fca-967a-585c97f31682"), + findMockedUserById("a73425d1-53a7-43d3-b6ae-cae9ba59b92b"), + findMockedUserById("0bac0dfd-b086-4b6d-b8ba-789e0eca7451"), + ], + total_member_count: 5, + avatar_url: "/emojis/1fa84.png", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "d00ea21b-3eec-4f63-a0cd-acb00e3a2116", + name: "ms-test-group", + display_name: "display-name", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [findMockedUserById("ef2eebf7-9708-4076-9f71-a34af71f5d24")], + total_member_count: 1, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "f7200783-62c0-4bfa-ae30-a542fc82dc8d", + name: "Tinkerers", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("15890ddb-142c-443d-8fd5-cd8307256ab1"), + findMockedUserById("ef2eebf7-9708-4076-9f71-a34af71f5d24"), + findMockedUserById("af657bc3-6949-4b1b-bc2d-d41a40b546a4"), + findMockedUserById("0bac0dfd-b086-4b6d-b8ba-789e0eca7451"), + ], + total_member_count: 4, + avatar_url: "/emojis/1f34c.png", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "e690870a-b24f-48fd-b75b-eee89098d588", + name: "underscored_group", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "fd6ea877-06b6-4c03-b7c0-ea8f0707425b", + name: "Sales", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("dd91e5f1-0e10-434e-9305-02129a6e4fdb"), + findMockedUserById("829a99f8-4c4a-4280-9500-fb21f3afb201"), + findMockedUserById("740bba7f-356d-4203-8f15-03ddee381998"), + findMockedUserById("fdc2dab9-dabd-4980-843f-2e93042db566"), + findMockedUserById("0bac0dfd-b086-4b6d-b8ba-789e0eca7451"), + ], + total_member_count: 5, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "8b1539c5-6f5c-4630-a715-53b74c444e86", + name: "eeee", + display_name: "aaaaaa", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "a26c335f-1498-48e2-bd66-c6a5783fa5f2", + name: "bruno-group", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [findMockedUserById("1c2baba2-40d6-4b9e-a788-fe393c1dbdbb")], + total_member_count: 1, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "900074db-46ce-4165-9938-3f719348fd36", + name: "F̷̡̮̩̞̺̹͔͇͖͇͊̓̈́̐̈́̃͋̀͂͂̄͘̚o̵̻̲̤̥͚͙͚̬͉͚̼̦̹̙͈̞̎̃͑̽̂͂̒͋̊̐̊̌̌͝r̶̭̖͊̊̕͘ḃ̵̢̡͍͎̝̲̼͔̩̥͎͕̪͂͜î̶̡͍̖̿̏̈̔̽͘͜d̶̼͔̜̰͛̂̊̆d̴̛̦̰̩̦̬͈͖̪͖̓̈́̊͂͝͠e̷̛̬̻͌̇́̃̌̓͠ń̴̙͖̉͐͌͌̎́͘̕͝", + display_name: "F̷̡̮̩̞̺̹͔͇͖͇͊̓̈́̐̈́̃͋̀͂͂̄͘̚o̵̻̲̤̥͚͙͚̬͉͚̼̦̹̙͈̞̎̃͑̽̂͂̒͋̊̐̊̌̌͝r̶̭̖͊̊̕͘ḃ̵̢̡͍͎̝̲̼͔̩̥͎͕̪͂͜î̶̡͍̖̿̏̈̔̽͘͜d̶̼͔̜̰͛̂̊̆d̴̛̦̰̩̦̬͈͖̪͖̓̈́̊͂͝͠e̷̛̬̻͌̇́̃̌̓͠ń̴̙͖̉͐͌͌̎́͘̕͝", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "85020b78-2a36-4bd4-b00c-7b88dab6e333", + name: "hi", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "5f8996c6-b3ba-4e39-97a1-dd44a73951de", + name: "Winners", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [findMockedUserById("c5eb8310-cf4f-444c-b223-0e991f828b40")], + total_member_count: 1, + avatar_url: "", + quota_allowance: 1000, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "0c6c1222-b65e-44c1-8bff-078d27fc10e6", + name: "Some-group", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "394222ed-7559-47a8-a57c-1bc7998c0357", + name: "kira-test", + display_name: "kira-test", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + name: "Everyone", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("31d3fcc5-618e-48ab-80b9-dab8d9aba077"), + findMockedUserById("bdbc1534-ba10-47be-a7cb-12a02de7a2c1"), + findMockedUserById("a73425d1-53a7-43d3-b6ae-cae9ba59b92b"), + findMockedUserById("9e7815af-bc48-435a-91c7-72dcbf26f036"), + findMockedUserById("f787e795-b502-41a3-ad79-fe1cb7d3403a"), + findMockedUserById("e97579fe-31c2-4e83-85d1-7241bf97ba4b"), + findMockedUserById("b10bbeed-074f-472c-b454-c2525eba6c44"), + findMockedUserById("0f452b63-64cb-4422-99ea-7391ccf7b4d5"), + findMockedUserById("bd644669-f7f4-48e6-be38-e37b5017c24f"), + findMockedUserById("f8f6345f-8751-4a37-87c7-a2f6325a248f"), + findMockedUserById("a28e6213-b339-46e8-94ec-310d39ac8acb"), + findMockedUserById("59da0bfe-9c99-47fa-a563-f9fdb18449d0"), + findMockedUserById("1355d2c6-6505-4e64-b93b-1258bbe35f02"), + findMockedUserById("4832f5d9-9773-4ea1-a65c-1b818c8cfae5"), + findMockedUserById("9be730b0-37ce-4216-84dd-6a851d21436e"), + findMockedUserById("f9c75a4e-9884-433a-80c9-2613bffbaf3f"), + findMockedUserById("49ded266-e150-4213-826c-7f721f2b9a36"), + findMockedUserById("00738e55-91cc-47bc-a494-206c8025b887"), + findMockedUserById("c0240345-f14a-4632-b713-a0f09c2ed927"), + findMockedUserById("ab872758-4f67-4b73-8d7d-460d83604990"), + findMockedUserById("2a256f0b-625d-4afc-929c-0ad687141ebd"), + findMockedUserById("4fde2562-7ec6-4ce7-b7de-359346641d6b"), + findMockedUserById("4b9a6fda-c60d-4fb2-96fd-b872d1eebee9"), + findMockedUserById("350441af-b2d3-401f-a795-39971f0a682b"), + findMockedUserById("e67fbe91-384a-41f0-9836-0b8cea0dd7c8"), + findMockedUserById("8099295b-d6bd-409b-b500-e31a12e54679"), + findMockedUserById("b81dc9eb-af70-4ccd-840f-bab9d2c1bde5"), + findMockedUserById("5bb278fe-a70f-4b5e-9eed-5df7d0e43f6b"), + findMockedUserById("c5eb8310-cf4f-444c-b223-0e991f828b40"), + findMockedUserById("8b474a55-d414-4b53-a6ba-760f3d4eed7b"), + findMockedUserById("6a973d35-8e8e-4ff4-9f95-436d62e13d6f"), + findMockedUserById("1c2baba2-40d6-4b9e-a788-fe393c1dbdbb"), + findMockedUserById("5819ca76-94ba-4009-b924-ae077fe9c615"), + findMockedUserById("1c3e3fff-6a0e-4179-9ba3-27f5443e6fce"), + findMockedUserById("5ccd3128-cbbb-4cfb-8139-5a1edbb60c71"), + findMockedUserById("bece715d-0145-45f6-97f1-42bd66b63959"), + findMockedUserById("a34ed13c-9157-426b-8029-de02292eeaa2"), + findMockedUserById("dd91e5f1-0e10-434e-9305-02129a6e4fdb"), + findMockedUserById("135e83f1-ff91-4f91-8d23-f7e7fa6627f1"), + findMockedUserById("2e1e7f76-ae77-424a-a209-f35a99731ec9"), + findMockedUserById("6ed718bc-9b36-4120-bd5c-e809163ecef1"), + findMockedUserById("d1e1792a-221d-413b-a7f4-51f33f395a9b"), + findMockedUserById("c79f306f-4816-4ca9-8990-55406a45bdb5"), + findMockedUserById("7a9f4857-e9c5-4f6c-8e1d-0827df5d753b"), + findMockedUserById("9e3e4c5a-5949-417f-9380-d0a393c78bdd"), + findMockedUserById("f2cdaba3-e0a3-447a-b186-eb10dc1d1d49"), + findMockedUserById("3f8c0eef-6a45-4759-a4d6-d00bbffb1369"), + findMockedUserById("bbaaa01d-b453-4079-b85e-74d333cc8b32"), + findMockedUserById("d9522d62-f20d-463a-b07d-0c0e2b031907"), + findMockedUserById("ae6024dc-5fe5-4d60-b19e-b8ef91e9504e"), + findMockedUserById("21fe9106-02bb-45c7-8d48-8eda6378991e"), + findMockedUserById("806e35bb-37fd-4810-b5f2-88aa47c30c84"), + findMockedUserById("b3e1b884-1a5b-44eb-b8b3-423f8eddc503"), + findMockedUserById("c57dd581-872f-4290-bb48-341554ae537b"), + findMockedUserById("81e915cb-5064-41f7-b654-c96c846424e6"), + findMockedUserById("12b03f43-1bb7-4fca-967a-585c97f31682"), + findMockedUserById("15890ddb-142c-443d-8fd5-cd8307256ab1"), + findMockedUserById("86e0067c-a4d5-4b75-9cf5-c605badeffc0"), + findMockedUserById("12708eeb-3463-4c45-b1a8-4b5fef689c33"), + findMockedUserById("8e215c92-4355-4722-bc27-cf5f06ee08e9"), + findMockedUserById("49eaf40a-bdc1-4be2-9b53-53c5b24d7016"), + findMockedUserById("9a0eee4a-ef7b-4197-b221-c9abf4d5b84a"), + findMockedUserById("c323e5c3-57cb-45e7-81c4-56d6cacb2f8c"), + findMockedUserById("78dd2361-4a5a-42b0-9ec3-3eea23af1094"), + findMockedUserById("c5b28cf0-f72a-4b45-b3d5-00d3989d4ed5"), + findMockedUserById("0f08a0c3-221e-42dc-9915-2ed85b3345dd"), + findMockedUserById("2b2e8e9f-7a13-435c-b5d4-382182b6e5bb"), + findMockedUserById("af657bc3-6949-4b1b-bc2d-d41a40b546a4"), + findMockedUserById("4c8ffd9b-7bc8-47db-9322-f4d5e0b51658"), + findMockedUserById("1da90b70-5d08-45d0-a5ee-822d1dd6397a"), + findMockedUserById("e9b24091-a633-4ba0-9746-ca325a86f0f5"), + findMockedUserById("27c64335-5e08-44ac-b93f-e454b82a9a06"), + findMockedUserById("97d00652-d374-48a2-b94f-a136bf7cde01"), + findMockedUserById("740bba7f-356d-4203-8f15-03ddee381998"), + findMockedUserById("3b0d7f28-7ec0-4d0a-b634-57af9d739e06"), + findMockedUserById("d12975ea-7a1e-4fb0-962e-0e3532f4db62"), + findMockedUserById("4670a9f0-f37f-48e2-a240-58e97a193648"), + findMockedUserById("8a98b6f7-20c1-46f1-8d58-bd2cc722b159"), + findMockedUserById("7a4319a5-0dc1-41e1-95e4-f31e312b0ecc"), + findMockedUserById("9bc756d1-5e95-4c6f-8e1b-a1bd20547151"), + findMockedUserById("b006209d-fdd2-4716-afb2-104dafb32dfb"), + findMockedUserById("c3480e4c-bdd9-4af6-9dfb-a336ae804817"), + findMockedUserById("e95fa9e6-e2c3-41d4-a238-01849dd116af"), + findMockedUserById("2374e95c-2e7b-4e54-9c6e-1cb2cb9aa4b4"), + findMockedUserById("162600c0-4900-41d4-9bb0-f53925c1d176"), + findMockedUserById("d96bf761-3f94-46b3-a1da-6316e2e4735d"), + findMockedUserById("ef2eebf7-9708-4076-9f71-a34af71f5d24"), + findMockedUserById("c915aeab-4ed1-42d6-95dc-49c50c5a288d"), + findMockedUserById("612f7a53-95df-4c4b-98c2-cebd66f1a6d6"), + findMockedUserById("da387280-f246-4329-ac3f-1c6e192b733e"), + findMockedUserById("fdc2dab9-dabd-4980-843f-2e93042db566"), + findMockedUserById("26efeb43-330f-4470-8f4e-7307b25020eb"), + findMockedUserById("1e758aaf-9dde-4b8d-9727-538f1d2264d4"), + findMockedUserById("9ed91bb9-db45-4cef-b39c-819856e98c30"), + findMockedUserById("8016c8ca-42a1-4946-8c5f-030355200130"), + findMockedUserById("13768036-8771-49b1-bf2f-0f61ff63c108"), + findMockedUserById("794b8a1b-e2d6-4590-be88-1129b0424965"), + findMockedUserById("946da1a2-9a18-4006-be65-310df64beca1"), + findMockedUserById("983dc923-5b63-4361-ae95-5b2f0db2fb6e"), + findMockedUserById("e5aef70f-f665-4358-afb2-2732dbddf7d2"), + findMockedUserById("62a3d053-6a97-4ad3-9b6d-d5999e16b800"), + findMockedUserById("58af7daf-5d08-456e-ae3b-5fba07b3df80"), + findMockedUserById("db38462b-e63d-4304-87e9-2640eea4a3b8"), + findMockedUserById("4a44ccb6-196b-4d98-a97d-8338bb751fc0"), + findMockedUserById("61099b0d-71b3-44f2-ae15-45d8af34c614"), + findMockedUserById("0bac0dfd-b086-4b6d-b8ba-789e0eca7451"), + findMockedUserById("b8d75f48-6b3a-4c33-8a99-30f8f37b720f"), + findMockedUserById("e3fef70d-f30e-43b4-b3a7-62a8e1a99ebb"), + findMockedUserById("523d5587-f473-490d-9e5d-444de9b77d15"), + findMockedUserById("b1b351ed-c489-473f-a11b-9432740f2a03"), + findMockedUserById("c76577dd-6a9f-44e5-a0d4-54d4800aeca4"), + findMockedUserById("3ab08cb8-3136-460f-a6b3-6febac00614d"), + findMockedUserById("829a99f8-4c4a-4280-9500-fb21f3afb201"), + findMockedUserById("7f5cc5e9-20ee-48ce-959d-081b3f52273e"), + findMockedUserById("2087e3c0-0d13-494a-a0e6-483d74e043e8"), + findMockedUserById("3e02d5f3-2ef1-4f52-95a8-738882d871cc"), + findMockedUserById("51492787-2823-428b-862e-c7cb83d5eac7"), + ], + total_member_count: 117, + avatar_url: "", + quota_allowance: 40, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "0b912bbd-743e-4415-9036-8217269de663", + name: "Engineers", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("3f8c0eef-6a45-4759-a4d6-d00bbffb1369"), + findMockedUserById("b006209d-fdd2-4716-afb2-104dafb32dfb"), + findMockedUserById("59da0bfe-9c99-47fa-a563-f9fdb18449d0"), + findMockedUserById("7f5cc5e9-20ee-48ce-959d-081b3f52273e"), + findMockedUserById("12b03f43-1bb7-4fca-967a-585c97f31682"), + findMockedUserById("d96bf761-3f94-46b3-a1da-6316e2e4735d"), + findMockedUserById("af657bc3-6949-4b1b-bc2d-d41a40b546a4"), + findMockedUserById("794b8a1b-e2d6-4590-be88-1129b0424965"), + findMockedUserById("c323e5c3-57cb-45e7-81c4-56d6cacb2f8c"), + findMockedUserById("78dd2361-4a5a-42b0-9ec3-3eea23af1094"), + findMockedUserById("c5eb8310-cf4f-444c-b223-0e991f828b40"), + findMockedUserById("a73425d1-53a7-43d3-b6ae-cae9ba59b92b"), + findMockedUserById("0bac0dfd-b086-4b6d-b8ba-789e0eca7451"), + findMockedUserById("b3e1b884-1a5b-44eb-b8b3-423f8eddc503"), + findMockedUserById("9ed91bb9-db45-4cef-b39c-819856e98c30"), + findMockedUserById("1c3e3fff-6a0e-4179-9ba3-27f5443e6fce"), + findMockedUserById("e9b24091-a633-4ba0-9746-ca325a86f0f5"), + ], + total_member_count: 17, + avatar_url: "/emojis/1f9d1-200d-1f4bb.png", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "4f7d2a59-3999-4d03-b346-8070a4d601dc", + name: "test-group", + display_name: "Test Group", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "125b6423-a3c4-4419-abf9-dfcbbd7e8749", + name: "bruno-group-2", + display_name: "Bruno Group 2", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "cab1a90d-15be-4efe-8ddf-4dc83f3e547e", + name: "bq-test-group", + display_name: "", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("8e215c92-4355-4722-bc27-cf5f06ee08e9"), + findMockedUserById("a73425d1-53a7-43d3-b6ae-cae9ba59b92b"), + ], + total_member_count: 2, + avatar_url: "/emojis/1f60d.png", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "1446698f-4f73-49e9-9dd2-e282a58f58e4", + name: "wowzers", + display_name: "Something else", + organization_id: "703f72a1-76f6-4f89-9de6-8a3989693fe5", + members: [ + findMockedUserById("86185021-756c-4b51-874f-a1deb00983f0"), + findMockedUserById("794b8a1b-e2d6-4590-be88-1129b0424965"), + ], + total_member_count: 2, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "coder", + organization_display_name: "Coder", + }, + { + id: "21dfa187-531e-4a77-acb8-1ac6af314703", + name: "Everyone", + display_name: "", + organization_id: "21dfa187-531e-4a77-acb8-1ac6af314703", + members: [findMockedUserById("61099b0d-71b3-44f2-ae15-45d8af34c614")], + total_member_count: 1, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "test-test", + organization_display_name: "test-test", + }, + { + id: "163a803e-1c99-44ec-927e-5a5ccb37c6aa", + name: "test", + display_name: "", + organization_id: "21dfa187-531e-4a77-acb8-1ac6af314703", + members: [], + total_member_count: 0, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "test-test", + organization_display_name: "test-test", + }, + { + id: "cbdcf774-9412-4118-8cd9-b3f502c84dfb", + name: "Everyone", + display_name: "", + organization_id: "cbdcf774-9412-4118-8cd9-b3f502c84dfb", + members: [findMockedUserById("5ccd3128-cbbb-4cfb-8139-5a1edbb60c71")], + total_member_count: 1, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "shared-compute", + organization_display_name: "Shared Compute", + }, + { + id: "e2815018-6d5a-4751-a18d-e64641dee559", + name: "Everyone", + display_name: "", + organization_id: "e2815018-6d5a-4751-a18d-e64641dee559", + members: [findMockedUserById("8b474a55-d414-4b53-a6ba-760f3d4eed7b")], + total_member_count: 1, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "docs-team", + organization_display_name: "Documentors", + }, + { + id: "d79144d9-b30a-448a-9af8-7dac83b2e4ec", + name: "Everyone", + display_name: "", + organization_id: "d79144d9-b30a-448a-9af8-7dac83b2e4ec", + members: [findMockedUserById("1c3e3fff-6a0e-4179-9ba3-27f5443e6fce")], + total_member_count: 1, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "data-science", + organization_display_name: "Data Science", + }, + { + id: "8efa9208-656a-422d-842d-b9dec0cf1bf3", + name: "Everyone", + display_name: "", + organization_id: "8efa9208-656a-422d-842d-b9dec0cf1bf3", + members: [ + findMockedUserById("162600c0-4900-41d4-9bb0-f53925c1d176"), + findMockedUserById("81e915cb-5064-41f7-b654-c96c846424e6"), + findMockedUserById("c0240345-f14a-4632-b713-a0f09c2ed927"), + findMockedUserById("c5eb8310-cf4f-444c-b223-0e991f828b40"), + findMockedUserById("9be730b0-37ce-4216-84dd-6a851d21436e"), + findMockedUserById("61099b0d-71b3-44f2-ae15-45d8af34c614"), + ], + total_member_count: 6, + avatar_url: "", + quota_allowance: 0, + source: "user", + organization_name: "rabbit", + organization_display_name: "rabbit", + }, +].map((g) => ({ + ...g, + source: g.source as GroupSource, + // The mock data from MockUsers contains only 25 users. It is possible that + // some group members are not included in these mocked users. Therefore, we + // should remove any group members that are not present in the mocked users + // during testing. + members: g.members.filter((m) => m !== undefined), +})); diff --git a/site/src/pages/UsersPage/storybookData/roles.ts b/site/src/pages/UsersPage/storybookData/roles.ts new file mode 100644 index 0000000000000..069625dbaa9ce --- /dev/null +++ b/site/src/pages/UsersPage/storybookData/roles.ts @@ -0,0 +1,474 @@ +import type { AssignableRoles, RBACAction, Role } from "api/typesGenerated"; + +// The following values were retrieved from the Coder API. +export const MockRoles: (AssignableRoles | Role)[] = [ + { + name: "owner", + display_name: "Owner", + site_permissions: [ + { + negate: false, + resource_type: "api_key", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "assign_org_role", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "assign_role", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "audit_log", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "debug_info", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "deployment_config", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "deployment_stats", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "file", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "group", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "group_member", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "license", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "notification_preference", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "notification_template", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "oauth2_app", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "oauth2_app_code_token", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "oauth2_app_secret", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "organization", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "organization_member", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "provisioner_daemon", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "provisioner_keys", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "replicas", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "system", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "tailnet_coordinator", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "template", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "user", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "workspace_proxy", + action: "*" as RBACAction, + }, + { + negate: false, + resource_type: "workspace", + action: "update", + }, + { + negate: false, + resource_type: "workspace", + action: "delete", + }, + { + negate: false, + resource_type: "workspace", + action: "start", + }, + { + negate: false, + resource_type: "workspace", + action: "stop", + }, + { + negate: false, + resource_type: "workspace", + action: "ssh", + }, + { + negate: false, + resource_type: "workspace", + action: "application_connect", + }, + { + negate: false, + resource_type: "workspace", + action: "create", + }, + { + negate: false, + resource_type: "workspace", + action: "read", + }, + { + negate: false, + resource_type: "workspace_dormant", + action: "read", + }, + { + negate: false, + resource_type: "workspace_dormant", + action: "delete", + }, + { + negate: false, + resource_type: "workspace_dormant", + action: "create", + }, + { + negate: false, + resource_type: "workspace_dormant", + action: "update", + }, + { + negate: false, + resource_type: "workspace_dormant", + action: "stop", + }, + ], + organization_permissions: [], + user_permissions: [], + assignable: true, + built_in: true, + }, + { + name: "template-admin", + display_name: "Template Admin", + site_permissions: [ + { + negate: false, + resource_type: "file", + action: "read", + }, + { + negate: false, + resource_type: "file", + action: "create", + }, + { + negate: false, + resource_type: "group", + action: "read", + }, + { + negate: false, + resource_type: "group_member", + action: "read", + }, + { + negate: false, + resource_type: "organization", + action: "read", + }, + { + negate: false, + resource_type: "organization_member", + action: "read", + }, + { + negate: false, + resource_type: "provisioner_daemon", + action: "update", + }, + { + negate: false, + resource_type: "provisioner_daemon", + action: "read", + }, + { + negate: false, + resource_type: "provisioner_daemon", + action: "delete", + }, + { + negate: false, + resource_type: "provisioner_daemon", + action: "create", + }, + { + negate: false, + resource_type: "template", + action: "create", + }, + { + negate: false, + resource_type: "template", + action: "view_insights", + }, + { + negate: false, + resource_type: "template", + action: "delete", + }, + { + negate: false, + resource_type: "template", + action: "update", + }, + { + negate: false, + resource_type: "template", + action: "read", + }, + { + negate: false, + resource_type: "user", + action: "read", + }, + { + negate: false, + resource_type: "workspace", + action: "read", + }, + ], + organization_permissions: [], + user_permissions: [], + assignable: true, + built_in: true, + }, + { + name: "user-admin", + display_name: "User Admin", + site_permissions: [ + { + negate: false, + resource_type: "assign_org_role", + action: "assign", + }, + { + negate: false, + resource_type: "assign_org_role", + action: "delete", + }, + { + negate: false, + resource_type: "assign_org_role", + action: "read", + }, + { + negate: false, + resource_type: "assign_role", + action: "assign", + }, + { + negate: false, + resource_type: "assign_role", + action: "delete", + }, + { + negate: false, + resource_type: "assign_role", + action: "read", + }, + { + negate: false, + resource_type: "group", + action: "delete", + }, + { + negate: false, + resource_type: "group", + action: "update", + }, + { + negate: false, + resource_type: "group", + action: "read", + }, + { + negate: false, + resource_type: "group", + action: "create", + }, + { + negate: false, + resource_type: "group_member", + action: "read", + }, + { + negate: false, + resource_type: "organization_member", + action: "delete", + }, + { + negate: false, + resource_type: "organization_member", + action: "create", + }, + { + negate: false, + resource_type: "organization_member", + action: "read", + }, + { + negate: false, + resource_type: "organization_member", + action: "update", + }, + { + negate: false, + resource_type: "user", + action: "read_personal", + }, + { + negate: false, + resource_type: "user", + action: "update_personal", + }, + { + negate: false, + resource_type: "user", + action: "delete", + }, + { + negate: false, + resource_type: "user", + action: "update", + }, + { + negate: false, + resource_type: "user", + action: "read", + }, + { + negate: false, + resource_type: "user", + action: "create", + }, + ], + organization_permissions: [], + user_permissions: [], + assignable: true, + built_in: true, + }, + { + name: "auditor", + display_name: "Auditor", + site_permissions: [ + { + negate: false, + resource_type: "audit_log", + action: "read", + }, + { + negate: false, + resource_type: "deployment_config", + action: "read", + }, + { + negate: false, + resource_type: "deployment_stats", + action: "read", + }, + { + negate: false, + resource_type: "group", + action: "read", + }, + { + negate: false, + resource_type: "group_member", + action: "read", + }, + { + negate: false, + resource_type: "organization_member", + action: "read", + }, + { + negate: false, + resource_type: "template", + action: "read", + }, + { + negate: false, + resource_type: "template", + action: "view_insights", + }, + { + negate: false, + resource_type: "user", + action: "read", + }, + ], + organization_permissions: [], + user_permissions: [], + built_in: true, + }, +]; diff --git a/site/src/pages/UsersPage/storybookData/users.ts b/site/src/pages/UsersPage/storybookData/users.ts new file mode 100644 index 0000000000000..5056b3e827c89 --- /dev/null +++ b/site/src/pages/UsersPage/storybookData/users.ts @@ -0,0 +1,570 @@ +import type { LoginType, User, UserStatus } from "api/typesGenerated"; + +// The following username, name, and email are generated using ChatGPT to avoid +// exposing real user data in mock values. While libraries like faker.js could +// be used, static and deterministic values are preferred for snapshot tests to +// ensure consistency. +const fakeUserData = [ + { username: "xbauer", name: "Joseph Page", email: "janet83@gmail.com" }, + { + username: "terryjessica", + name: "Deborah Gibson", + email: "christianjames@yahoo.com", + }, + { username: "stephanie39", name: "Ivan Henry", email: "reidjohn@yahoo.com" }, + { + username: "hweaver", + name: "Tina Fleming", + email: "gloriapeterson@salazar-donovan.com", + }, + { + username: "vhenderson", + name: "Melissa Woods DVM", + email: "josesalazar@jones.com", + }, + { username: "ronald64", name: "David Giles", email: "xlopez@tate.com" }, + { username: "nsteele", name: "Austin Molina", email: "webbdennis@yahoo.com" }, + { + username: "stonejonathan", + name: "Brian Parks", + email: "scott56@hotmail.com", + }, + { + username: "gary05", + name: "Jeffrey Mosley", + email: "matthewmyers@wheeler-butler.com", + }, + { + username: "oyates", + name: "Richard Gonzalez", + email: "sharon15@andrews-livingston.com", + }, + { + username: "cervantescolin", + name: "Brian Hayes", + email: "nataliemiller@clark.com", + }, + { + username: "carlosmadden", + name: "Candace Castillo", + email: "andrea62@schmitt-thomas.org", + }, + { + username: "leah46", + name: "Mrs. Susan Murillo MD", + email: "jamesschmitt@gmail.com", + }, + { + username: "eosborne", + name: "Andrew Holland", + email: "mileslauren@cruz.com", + }, + { + username: "rmorales", + name: "Madison Shaffer", + email: "wheeleralyssa@phillips.info", + }, + { + username: "haynesrachel", + name: "Samantha Torres", + email: "johnedwards@avery-diaz.com", + }, + { + username: "bakeramanda", + name: "Michael Woods", + email: "masseygabriel@hotmail.com", + }, + { + username: "gonzalesmeghan", + name: "Tiffany Jackson", + email: "psmith@yahoo.com", + }, + { + username: "xsmith", + name: "Patrick Lewis", + email: "christopher73@rivera.com", + }, + { username: "wrice", name: "Erica Smith", email: "ycisneros@hotmail.com" }, + { + username: "anthonybrady", + name: "Teresa Ward", + email: "carolmartin@simmons.com", + }, + { + username: "matthew45", + name: "Kevin Guerrero", + email: "janet91@jones-brown.com", + }, + { + username: "brandon62", + name: "Jennifer Jackson", + email: "stephaniedixon@dorsey.info", + }, + { + username: "christinesmith", + name: "Julian Torres", + email: "alvarezamy@hotmail.com", + }, + { + username: "booneandrew", + name: "Charles Johnson", + email: "xblack@gmail.com", + }, +]; + +// These values were retrieved from the Coder API. Sensitive information such as +// usernames, names, and emails has been replaced with fake user data to protect +// privacy. +export const MockUsers: User[] = [ + { + id: "a73425d1-53a7-43d3-b6ae-cae9ba59b92b", + created_at: "2022-08-10T16:57:11.04414Z", + updated_at: "2024-09-05T12:58:54.391687Z", + last_seen_at: "2024-09-05T12:58:54.391687Z", + status: "active", + login_type: "github", + theme_preference: "auto", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + { + name: "template-admin", + display_name: "Template Admin", + }, + { + name: "user-admin", + display_name: "User Admin", + }, + ], + }, + { + id: "350441af-b2d3-401f-a795-39971f0a682b", + created_at: "2024-07-23T14:40:20.205142Z", + updated_at: "2024-08-08T19:04:18.108585Z", + last_seen_at: "2024-08-08T19:04:18.108585Z", + status: "active", + login_type: "github", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [], + }, + { + id: "c0240345-f14a-4632-b713-a0f09c2ed927", + created_at: "2023-09-07T20:16:41.654648Z", + updated_at: "2024-08-27T17:34:35.171154Z", + last_seen_at: "2024-08-27T17:34:35.171153Z", + status: "active", + login_type: "password", + theme_preference: "", + organization_ids: [ + "703f72a1-76f6-4f89-9de6-8a3989693fe5", + "8efa9208-656a-422d-842d-b9dec0cf1bf3", + ], + roles: [], + }, + { + id: "d96bf761-3f94-46b3-a1da-6316e2e4735d", + created_at: "2023-08-14T15:04:56.932482Z", + updated_at: "2024-09-05T13:04:25.741671Z", + last_seen_at: "2024-09-05T13:04:25.741671Z", + status: "active", + login_type: "github", + theme_preference: "light", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "27c64335-5e08-44ac-b93f-e454b82a9a06", + created_at: "2024-06-10T18:10:33.595496Z", + updated_at: "2024-07-25T22:39:21.144268Z", + last_seen_at: "2024-07-25T22:39:21.144268Z", + status: "active", + login_type: "oidc", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [], + }, + { + id: "0f452b63-64cb-4422-99ea-7391ccf7b4d5", + created_at: "2024-06-20T15:31:39.835721Z", + updated_at: "2024-06-20T15:31:39.916055Z", + last_seen_at: "2024-06-20T15:31:39.916055Z", + status: "active", + login_type: "github", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [], + }, + { + id: "135e83f1-ff91-4f91-8d23-f7e7fa6627f1", + created_at: "2024-07-18T16:56:04.513102Z", + updated_at: "2024-09-05T02:37:40.678649Z", + last_seen_at: "2024-09-05T02:37:40.678649Z", + status: "active", + login_type: "github", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [], + }, + { + id: "9e3e4c5a-5949-417f-9380-d0a393c78bdd", + created_at: "2024-07-15T02:00:51.816307Z", + updated_at: "2024-09-05T04:32:12.923203Z", + last_seen_at: "2024-09-05T04:32:12.923203Z", + status: "active", + login_type: "oidc", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "58af7daf-5d08-456e-ae3b-5fba07b3df80", + created_at: "2024-07-22T20:31:19.143974Z", + updated_at: "2024-07-25T13:21:14.248194Z", + last_seen_at: "2024-07-25T13:21:14.248194Z", + status: "active", + login_type: "password", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [], + }, + { + id: "5ccd3128-cbbb-4cfb-8139-5a1edbb60c71", + created_at: "2022-08-10T20:33:51.324299Z", + updated_at: "2024-09-05T13:00:02.205822Z", + last_seen_at: "2024-09-05T13:00:02.205822Z", + status: "active", + login_type: "github", + theme_preference: "dark", + organization_ids: [ + "cbdcf774-9412-4118-8cd9-b3f502c84dfb", + "703f72a1-76f6-4f89-9de6-8a3989693fe5", + "7621bbb4-5b04-4957-8419-cf4a683ac59a", + ], + roles: [ + { + name: "user-admin", + display_name: "User Admin", + }, + { + name: "template-admin", + display_name: "Template Admin", + }, + { + name: "auditor", + display_name: "Auditor", + }, + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "af657bc3-6949-4b1b-bc2d-d41a40b546a4", + created_at: "2022-08-10T16:35:11.879233Z", + updated_at: "2024-09-05T13:12:27.319427Z", + last_seen_at: "2024-09-05T13:12:27.319427Z", + status: "active", + login_type: "github", + theme_preference: "dark", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + { + name: "user-admin", + display_name: "User Admin", + }, + ], + }, + { + id: "1c2baba2-40d6-4b9e-a788-fe393c1dbdbb", + created_at: "2023-01-03T13:29:52.76039Z", + updated_at: "2024-09-03T16:40:45.784352Z", + last_seen_at: "2024-09-03T16:40:45.784352Z", + status: "active", + login_type: "password", + theme_preference: "dark", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "f2cdaba3-e0a3-447a-b186-eb10dc1d1d49", + created_at: "2024-02-02T20:37:29.606054Z", + updated_at: "2024-06-10T14:31:49.820912Z", + last_seen_at: "2024-06-10T14:31:49.820912Z", + status: "active", + login_type: "password", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [], + }, + { + id: "9bc756d1-5e95-4c6f-8e1b-a1bd20547151", + created_at: "2024-09-04T11:29:24.168944Z", + updated_at: "2024-09-04T11:29:24.658926Z", + last_seen_at: "2024-09-04T11:29:24.658926Z", + status: "active", + login_type: "oidc", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "59da0bfe-9c99-47fa-a563-f9fdb18449d0", + created_at: "2022-08-15T08:30:10.343828Z", + updated_at: "2024-09-05T12:27:22.098297Z", + last_seen_at: "2024-09-05T12:27:22.098297Z", + status: "active", + login_type: "oidc", + theme_preference: "darkBlue", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "12b03f43-1bb7-4fca-967a-585c97f31682", + created_at: "2022-08-10T15:35:20.553581Z", + updated_at: "2024-09-05T13:23:46.237798Z", + last_seen_at: "2024-09-05T13:23:46.237798Z", + status: "active", + login_type: "github", + theme_preference: "dark", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + { + name: "template-admin", + display_name: "Template Admin", + }, + { + name: "user-admin", + display_name: "User Admin", + }, + ], + }, + { + id: "78dd2361-4a5a-42b0-9ec3-3eea23af1094", + created_at: "2022-08-10T17:14:30.475925Z", + updated_at: "2024-09-04T20:40:17.036986Z", + last_seen_at: "2024-09-04T20:40:17.036986Z", + status: "active", + login_type: "github", + theme_preference: "auto", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + { + name: "template-admin", + display_name: "Template Admin", + }, + { + name: "user-admin", + display_name: "User Admin", + }, + ], + }, + { + id: "4a44ccb6-196b-4d98-a97d-8338bb751fc0", + created_at: "2024-08-26T09:00:17.565927Z", + updated_at: "2024-09-05T12:45:45.987041Z", + last_seen_at: "2024-09-05T12:45:45.987041Z", + status: "active", + login_type: "github", + theme_preference: "auto", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "c323e5c3-57cb-45e7-81c4-56d6cacb2f8c", + created_at: "2024-03-04T11:12:41.201352Z", + updated_at: "2024-09-05T07:24:39.32465Z", + last_seen_at: "2024-09-05T07:24:39.324649Z", + status: "active", + login_type: "oidc", + theme_preference: "darkBlue", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "9e7815af-bc48-435a-91c7-72dcbf26f036", + created_at: "2024-05-24T14:53:53.996555Z", + updated_at: "2024-07-22T16:43:16.494533Z", + last_seen_at: "2024-07-22T16:43:16.494533Z", + status: "active", + login_type: "oidc", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [], + }, + { + id: "3f8c0eef-6a45-4759-a4d6-d00bbffb1369", + created_at: "2022-08-15T12:31:15.833843Z", + updated_at: "2024-09-05T09:21:46.442564Z", + last_seen_at: "2024-09-05T09:21:46.442564Z", + status: "active", + login_type: "github", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + { + name: "template-admin", + display_name: "Template Admin", + }, + { + name: "user-admin", + display_name: "User Admin", + }, + ], + }, + { + id: "c5eb8310-cf4f-444c-b223-0e991f828b40", + created_at: "2022-08-10T20:00:05.494466Z", + updated_at: "2024-09-04T18:33:12.702949Z", + last_seen_at: "2024-09-04T18:33:12.702948Z", + status: "active", + login_type: "github", + theme_preference: "dark", + organization_ids: [ + "703f72a1-76f6-4f89-9de6-8a3989693fe5", + "8efa9208-656a-422d-842d-b9dec0cf1bf3", + ], + roles: [ + { + name: "template-admin", + display_name: "Template Admin", + }, + { + name: "user-admin", + display_name: "User Admin", + }, + { + name: "auditor", + display_name: "Auditor", + }, + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "740bba7f-356d-4203-8f15-03ddee381998", + created_at: "2022-08-10T18:13:52.084503Z", + updated_at: "2024-09-05T11:56:07.949264Z", + last_seen_at: "2024-09-05T11:56:07.949264Z", + status: "active", + login_type: "github", + theme_preference: "dark", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "owner", + display_name: "Owner", + }, + { + name: "template-admin", + display_name: "Template Admin", + }, + { + name: "user-admin", + display_name: "User Admin", + }, + ], + }, + { + id: "806e35bb-37fd-4810-b5f2-88aa47c30c84", + created_at: "2024-06-03T03:09:52.16976Z", + updated_at: "2024-09-05T12:51:39.933117Z", + last_seen_at: "2024-09-05T12:51:39.933117Z", + status: "active", + login_type: "oidc", + theme_preference: "dark", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [ + { + name: "user-admin", + display_name: "User Admin", + }, + { + name: "template-admin", + display_name: "Template Admin", + }, + { + name: "auditor", + display_name: "Auditor", + }, + { + name: "owner", + display_name: "Owner", + }, + ], + }, + { + id: "3b0d7f28-7ec0-4d0a-b634-57af9d739e06", + created_at: "2024-07-08T12:31:40.932658Z", + updated_at: "2024-07-12T06:33:22.019268Z", + last_seen_at: "2024-07-12T06:33:22.019268Z", + status: "active", + login_type: "github", + theme_preference: "", + organization_ids: ["703f72a1-76f6-4f89-9de6-8a3989693fe5"], + roles: [], + }, +].map((u, i) => ({ + ...u, + ...fakeUserData[i], + avatar_url: "", + status: u.status as UserStatus, + login_type: u.login_type as LoginType, +}));