diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index c5b9f7f6cc06f..cd0fb83f83f70 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -8556,6 +8556,10 @@ func (q *FakeQuerier) OrganizationMembers(_ context.Context, arg database.Organi tmp = append(tmp, database.OrganizationMembersRow{ OrganizationMember: organizationMember, Username: user.Username, + AvatarURL: user.AvatarURL, + Name: user.Name, + Email: user.Email, + GlobalRoles: user.RBACRoles, }) } return tmp, nil diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 8f69b90900538..34fa03837057f 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -1,5 +1,6 @@ import { type ChildProcess, exec, spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; +import * as fs from "node:fs"; import net from "node:net"; import path from "node:path"; import { Duplex } from "node:stream"; @@ -850,6 +851,7 @@ export const fillParameters = async ( export const updateTemplate = async ( page: Page, + organization: string, templateName: string, responses?: EchoProvisionerResponses, ) => { @@ -868,6 +870,8 @@ export const updateTemplate = async ( "-y", "-d", "-", + "-O", + organization, templateName, ], { @@ -880,6 +884,7 @@ export const updateTemplate = async ( ); const uploaded = new Awaiter(); + child.on("exit", (code) => { if (code === 0) { uploaded.done(); @@ -987,3 +992,48 @@ export async function openTerminalWindow( return terminal; } + +type UserValues = { + name: string; + username: string; + email: string; + password: string; +}; + +export async function createUser( + page: Page, + userValues: Partial = {}, +): Promise { + const returnTo = page.url(); + + await page.goto("/deployment/users", { waitUntil: "domcontentloaded" }); + await expect(page).toHaveTitle("Users - Coder"); + + await page.getByRole("button", { name: "Create user" }).click(); + await expect(page).toHaveTitle("Create User - Coder"); + + const username = userValues.username ?? randomName(); + const name = userValues.name ?? username; + const email = userValues.email ?? `${username}@coder.com`; + const password = userValues.password || "s3cure&password!"; + + await page.getByLabel("Username").fill(username); + if (name) { + await page.getByLabel("Full name").fill(name); + } + await page.getByLabel("Email").fill(email); + await page.getByLabel("Login Type").click(); + await page.getByRole("option", { name: "Password", exact: false }).click(); + // Using input[name=password] due to the select element utilizing 'password' + // as the label for the currently active option. + const passwordField = page.locator("input[name=password]"); + await passwordField.fill(password); + await page.getByRole("button", { name: "Create user" }).click(); + await expect(page.getByText("Successfully created user.")).toBeVisible(); + + await expect(page).toHaveTitle("Users - Coder"); + await expect(page.locator("tr", { hasText: email })).toBeVisible(); + + await page.goto(returnTo, { waitUntil: "domcontentloaded" }); + return { name, username, email, password }; +} diff --git a/site/e2e/tests/organizationMembers.spec.ts b/site/e2e/tests/organizationMembers.spec.ts new file mode 100644 index 0000000000000..56fb2dbc4acde --- /dev/null +++ b/site/e2e/tests/organizationMembers.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from "@playwright/test"; +import { setupApiCalls } from "../api"; +import { expectUrl } from "../expectUrl"; +import { createUser, randomName, requiresLicense } from "../helpers"; +import { beforeCoderTest } from "../hooks"; + +test.beforeEach(async ({ page }) => { + await beforeCoderTest(page); + await setupApiCalls(page); +}); + +test("add and remove organization member", async ({ page }) => { + requiresLicense(); + + // Create a new organization to test + await page.goto("/organizations/new", { waitUntil: "domcontentloaded" }); + const name = randomName(); + await page.getByLabel("Slug").fill(name); + await page.getByLabel("Display name").fill(`Org ${name}`); + await page.getByLabel("Description").fill(`Org description ${name}`); + await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png"); + await page.getByRole("button", { name: "Submit" }).click(); + + // Navigate to members page + await expectUrl(page).toHavePathName(`/organizations/${name}`); + await expect(page.getByText("Organization created.")).toBeVisible(); + await page.getByText("Members").click(); + + // Add a user to the org + const personToAdd = await createUser(page); + await page.getByPlaceholder("User email or username").fill(personToAdd.email); + await page.getByRole("option", { name: personToAdd.email }).click(); + await page.getByRole("button", { name: "Add user" }).click(); + const addedRow = page.locator("tr", { hasText: personToAdd.email }); + await expect(addedRow).toBeVisible(); + + // Give them a role + await addedRow.getByLabel("Edit user roles").click(); + await page.getByText("Organization User Admin").click(); + await page.getByText("Organization Template Admin").click(); + await page.mouse.click(10, 10); // close the popover by clicking outside of it + await expect(addedRow.getByText("Organization User Admin")).toBeVisible(); + await expect(addedRow.getByText("+1 more")).toBeVisible(); + + // Remove them from the org + await addedRow.getByLabel("More options").click(); + await page.getByText("Remove").click(); // Click the "Remove" option + await page.getByRole("button", { name: "Remove" }).click(); // Click "Remove" in the confirmation dialog + await expect(addedRow).not.toBeVisible(); +}); diff --git a/site/e2e/tests/organizations.spec.ts b/site/e2e/tests/organizations.spec.ts index 625eac4762997..8d26020618643 100644 --- a/site/e2e/tests/organizations.spec.ts +++ b/site/e2e/tests/organizations.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from "@playwright/test"; import { setupApiCalls } from "../api"; import { expectUrl } from "../expectUrl"; -import { requiresLicense } from "../helpers"; +import { randomName, requiresLicense } from "../helpers"; import { beforeCoderTest } from "../hooks"; test.beforeEach(async ({ page }) => { @@ -17,27 +17,29 @@ test("create and delete organization", async ({ page }) => { waitUntil: "domcontentloaded", }); - await page.getByLabel("Slug").fill("floop"); - await page.getByLabel("Display name").fill("Floop"); - await page.getByLabel("Description").fill("Org description floop"); + const name = randomName(); + await page.getByLabel("Slug").fill(name); + await page.getByLabel("Display name").fill(`Org ${name}`); + await page.getByLabel("Description").fill(`Org description ${name}`); await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png"); await page.getByRole("button", { name: "Submit" }).click(); // Expect to be redirected to the new organization - await expectUrl(page).toHavePathName("/organizations/floop"); + await expectUrl(page).toHavePathName(`/organizations/${name}`); await expect(page.getByText("Organization created.")).toBeVisible(); - await page.getByLabel("Slug").fill("wibble"); - await page.getByLabel("Description").fill("Org description wibble"); + const newName = randomName(); + await page.getByLabel("Slug").fill(newName); + await page.getByLabel("Description").fill(`Org description ${newName}`); await page.getByRole("button", { name: "Submit" }).click(); // Expect to be redirected when renaming the organization - await expectUrl(page).toHavePathName("/organizations/wibble"); + await expectUrl(page).toHavePathName(`/organizations/${newName}`); await expect(page.getByText("Organization settings updated.")).toBeVisible(); await page.getByRole("button", { name: "Delete this organization" }).click(); const dialog = page.getByTestId("dialog"); - await dialog.getByLabel("Name").fill("wibble"); + await dialog.getByLabel("Name").fill(newName); await dialog.getByRole("button", { name: "Delete" }).click(); await expect(page.getByText("Organization deleted.")).toBeVisible(); }); diff --git a/site/e2e/tests/users/createUserWithPassword.spec.ts b/site/e2e/tests/users/createUserWithPassword.spec.ts index 85a3e47ccd87f..20396a6ac17eb 100644 --- a/site/e2e/tests/users/createUserWithPassword.spec.ts +++ b/site/e2e/tests/users/createUserWithPassword.spec.ts @@ -1,71 +1,13 @@ -import { expect, test } from "@playwright/test"; -import { randomName } from "../../helpers"; +import { test } from "@playwright/test"; +import { createUser } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; test.beforeEach(async ({ page }) => await beforeCoderTest(page)); -test("create user with password", async ({ page, baseURL }) => { - await page.goto(`${baseURL}/deployment/users`, { - waitUntil: "domcontentloaded", - }); - await expect(page).toHaveTitle("Users - Coder"); - - await page.getByRole("button", { name: "Create user" }).click(); - await expect(page).toHaveTitle("Create User - Coder"); - - const name = randomName(); - const userValues = { - username: name, - name: name, - email: `${name}@coder.com`, - loginType: "password", - password: "s3cure&password!", - }; - - await page.getByLabel("Username").fill(userValues.username); - await page.getByLabel("Full name").fill(userValues.username); - await page.getByLabel("Email").fill(userValues.email); - await page.getByLabel("Login Type").click(); - await page.getByRole("option", { name: "Password", exact: false }).click(); - // Using input[name=password] due to the select element utilizing 'password' - // as the label for the currently active option. - const passwordField = page.locator("input[name=password]"); - await passwordField.fill(userValues.password); - await page.getByRole("button", { name: "Create user" }).click(); - await expect(page.getByText("Successfully created user.")).toBeVisible(); - - await expect(page).toHaveTitle("Users - Coder"); - await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible(); +test("create user with password", async ({ page }) => { + await createUser(page); }); -test("create user without full name is optional", async ({ page, baseURL }) => { - await page.goto(`${baseURL}/deployment/users`, { - waitUntil: "domcontentloaded", - }); - await expect(page).toHaveTitle("Users - Coder"); - - await page.getByRole("button", { name: "Create user" }).click(); - await expect(page).toHaveTitle("Create User - Coder"); - - const name = randomName(); - const userValues = { - username: name, - email: `${name}@coder.com`, - loginType: "password", - password: "s3cure&password!", - }; - - await page.getByLabel("Username").fill(userValues.username); - await page.getByLabel("Email").fill(userValues.email); - await page.getByLabel("Login Type").click(); - await page.getByRole("option", { name: "Password", exact: false }).click(); - // Using input[name=password] due to the select element utilizing 'password' - // as the label for the currently active option. - const passwordField = page.locator("input[name=password]"); - await passwordField.fill(userValues.password); - await page.getByRole("button", { name: "Create user" }).click(); - await expect(page.getByText("Successfully created user.")).toBeVisible(); - - await expect(page).toHaveTitle("Users - Coder"); - await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible(); +test("create user without full name", async ({ page }) => { + await createUser(page, { name: "" }); }); diff --git a/site/e2e/tests/workspaces/updateWorkspace.spec.ts b/site/e2e/tests/workspaces/updateWorkspace.spec.ts index 8ff256f74d3e6..8018856495c40 100644 --- a/site/e2e/tests/workspaces/updateWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/updateWorkspace.spec.ts @@ -41,6 +41,7 @@ test("update workspace, new optional, immutable parameter added", async ({ const updatedRichParameters = [...richParameters, fifthParameter]; await updateTemplate( page, + "coder", template, echoResponsesWithParameters(updatedRichParameters), ); @@ -79,6 +80,7 @@ test("update workspace, new required, mutable parameter added", async ({ const updatedRichParameters = [...richParameters, sixthParameter]; await updateTemplate( page, + "coder", template, echoResponsesWithParameters(updatedRichParameters), ); diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx index de6295a838bd9..4cefc75f7e08b 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx @@ -77,7 +77,6 @@ const OrganizationMembersPage: FC = () => { isUpdatingMemberRoles={updateMemberRolesMutation.isLoading} me={me} members={members} - groupsByUserId={groupsByUserIdQuery.data} addMember={async (user: User) => { await addMemberMutation.mutateAsync(user.id); void membersQuery.refetch(); diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx index c343a2490f119..36002c605e04b 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx @@ -42,7 +42,6 @@ interface OrganizationMembersPageViewProps { isUpdatingMemberRoles: boolean; me: User; members: Array | undefined; - groupsByUserId: GroupsByUserId | undefined; addMember: (user: User) => Promise; removeMember: (member: OrganizationMemberWithUserData) => void; updateMemberRoles: ( @@ -57,17 +56,28 @@ interface OrganizationMemberTableEntry extends OrganizationMemberWithUserData { export const OrganizationMembersPageView: FC< OrganizationMembersPageViewProps -> = (props) => { +> = ({ + allAvailableRoles, + canEditMembers, + error, + isAddingMember, + isUpdatingMemberRoles, + me, + members, + addMember, + removeMember, + updateMemberRoles, +}) => { return (
- {Boolean(props.error) && } + {Boolean(error) && } - {props.canEditMembers && ( + {canEditMembers && ( )} @@ -92,7 +102,7 @@ export const OrganizationMembersPageView: FC< - {props.members?.map((member) => ( + {members?.map((member) => ( { try { - await props.updateMemberRoles(member, roles); + await updateMemberRoles(member, roles); displaySuccess("Roles updated successfully."); } catch (error) { displayError( @@ -126,7 +136,7 @@ export const OrganizationMembersPageView: FC< /> - {member.user_id !== props.me.id && props.canEditMembers && ( + {member.user_id !== me.id && canEditMembers && ( @@ -134,7 +144,7 @@ export const OrganizationMembersPageView: FC< props.removeMember(member)} + onClick={() => removeMember(member)} > Remove diff --git a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx b/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx index ed8712f4c6f73..c0a2681442f3c 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx +++ b/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx @@ -128,7 +128,7 @@ export const EditRolesButton: FC = ({ - +
= ({ roles }) => { minWidth: "auto", }, }} - anchorOrigin={{ - vertical: -4, - horizontal: "center", - }} - transformOrigin={{ - vertical: "bottom", - horizontal: "center", - }} + anchorOrigin={{ vertical: -4, horizontal: "center" }} + transformOrigin={{ vertical: "bottom", horizontal: "center" }} > {roles.map((role) => (