diff --git a/site/e2e/tests/groups/removeMember.spec.ts b/site/e2e/tests/groups/removeMember.spec.ts index 856ece95c0b02..c69925589221a 100644 --- a/site/e2e/tests/groups/removeMember.spec.ts +++ b/site/e2e/tests/groups/removeMember.spec.ts @@ -33,9 +33,8 @@ test("remove member", async ({ page, baseURL }) => { await expect(page).toHaveTitle(`${group.display_name} - Coder`); const userRow = page.getByRole("row", { name: member.username }); - await userRow.getByRole("button", { name: "More options" }).click(); - - const menu = page.locator("#more-options"); + await userRow.getByRole("button", { name: "Open menu" }).click(); + const menu = page.getByRole("menu"); await menu.getByText("Remove").click({ timeout: 1_000 }); await expect(page.getByText("Member removed successfully.")).toBeVisible(); diff --git a/site/e2e/tests/organizationGroups.spec.ts b/site/e2e/tests/organizationGroups.spec.ts index 08768d4bbae11..14741bdf38e00 100644 --- a/site/e2e/tests/organizationGroups.spec.ts +++ b/site/e2e/tests/organizationGroups.spec.ts @@ -79,8 +79,10 @@ test("create group", async ({ page }) => { await expect(page.getByText("No users found")).toBeVisible(); // Remove someone from the group - await addedRow.getByLabel("More options").click(); - await page.getByText("Remove").click(); + await addedRow.getByRole("button", { name: "Open menu" }).click(); + const menu = page.getByRole("menu"); + await menu.getByText("Remove").click(); + await expect(addedRow).not.toBeVisible(); // Delete the group diff --git a/site/e2e/tests/organizationMembers.spec.ts b/site/e2e/tests/organizationMembers.spec.ts index 51c3491ae3d62..639e6428edfb5 100644 --- a/site/e2e/tests/organizationMembers.spec.ts +++ b/site/e2e/tests/organizationMembers.spec.ts @@ -39,8 +39,9 @@ test("add and remove organization member", async ({ page }) => { 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 addedRow.getByRole("button", { name: "Open menu" }).click(); + const menu = page.getByRole("menu"); + await menu.getByText("Remove").click(); 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/customRoles/customRoles.spec.ts b/site/e2e/tests/organizations/customRoles/customRoles.spec.ts index 1e1e518e96399..1f55e87de8bab 100644 --- a/site/e2e/tests/organizations/customRoles/customRoles.spec.ts +++ b/site/e2e/tests/organizations/customRoles/customRoles.spec.ts @@ -37,8 +37,8 @@ test.describe("CustomRolesPage", () => { await expect(roleRow.getByText(customRole.display_name)).toBeVisible(); await expect(roleRow.getByText("organization_member")).toBeVisible(); - await roleRow.getByRole("button", { name: "More options" }).click(); - const menu = page.locator("#more-options"); + await roleRow.getByRole("button", { name: "Open menu" }).click(); + const menu = page.getByRole("menu"); await menu.getByText("Edit").click(); await expect(page).toHaveURL( @@ -118,7 +118,7 @@ test.describe("CustomRolesPage", () => { // Verify that the more menu (three dots) is not present for built-in roles await expect( - roleRow.getByRole("button", { name: "More options" }), + roleRow.getByRole("button", { name: "Open menu" }), ).not.toBeVisible(); await deleteOrganization(org.name); @@ -175,9 +175,9 @@ test.describe("CustomRolesPage", () => { await page.goto(`/organizations/${org.name}/roles`); const roleRow = page.getByTestId(`role-${customRole.name}`); - await roleRow.getByRole("button", { name: "More options" }).click(); + await roleRow.getByRole("button", { name: "Open menu" }).click(); - const menu = page.locator("#more-options"); + const menu = page.getByRole("menu"); await menu.getByText("Delete…").click(); const input = page.getByRole("textbox"); diff --git a/site/e2e/tests/updateTemplate.spec.ts b/site/e2e/tests/updateTemplate.spec.ts index e0bfac03cf036..43dd392443ea2 100644 --- a/site/e2e/tests/updateTemplate.spec.ts +++ b/site/e2e/tests/updateTemplate.spec.ts @@ -53,8 +53,10 @@ test("add and remove a group", async ({ page }) => { await expect(row).toBeVisible(); // Now remove the group - await row.getByLabel("More options").click(); - await page.getByText("Remove").click(); + await row.getByRole("button", { name: "Open menu" }).click(); + const menu = page.getByRole("menu"); + await menu.getByText("Remove").click(); + await expect(page.getByText("Group removed successfully!")).toBeVisible(); await expect(row).not.toBeVisible(); }); diff --git a/site/e2e/tests/users/removeUser.spec.ts b/site/e2e/tests/users/removeUser.spec.ts index c44d64b39c13c..92aa3efaa803a 100644 --- a/site/e2e/tests/users/removeUser.spec.ts +++ b/site/e2e/tests/users/removeUser.spec.ts @@ -17,9 +17,9 @@ test("remove user", async ({ page, baseURL }) => { await expect(page).toHaveTitle("Users - Coder"); const userRow = page.getByRole("row", { name: user.email }); - await userRow.getByRole("button", { name: "More options" }).click(); - const menu = page.locator("#more-options"); - await menu.getByText("Delete").click(); + await userRow.getByRole("button", { name: "Open menu" }).click(); + const menu = page.getByRole("menu"); + await menu.getByText("Delete…").click(); const dialog = page.getByTestId("dialog"); await dialog.getByLabel("Name of the user to delete").fill(user.username); diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx index c37f9f0146047..e56fd7cbe4343 100644 --- a/site/src/components/DropdownMenu/DropdownMenu.tsx +++ b/site/src/components/DropdownMenu/DropdownMenu.tsx @@ -196,7 +196,7 @@ export const DropdownMenuSeparator = forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/site/src/components/MoreMenu/MoreMenu.stories.tsx b/site/src/components/MoreMenu/MoreMenu.stories.tsx deleted file mode 100644 index e7a9968b01414..0000000000000 --- a/site/src/components/MoreMenu/MoreMenu.stories.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import GrassIcon from "@mui/icons-material/Grass"; -import KitesurfingIcon from "@mui/icons-material/Kitesurfing"; -import { action } from "@storybook/addon-actions"; -import type { Meta, StoryObj } from "@storybook/react"; -import { expect, screen, userEvent, waitFor, within } from "@storybook/test"; -import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "./MoreMenu"; - -const meta: Meta = { - title: "components/MoreMenu", - component: MoreMenu, -}; - -export default meta; -type Story = StoryObj; - -const Example: Story = { - args: { - children: ( - <> - - - - - - - Touch grass - - - - Touch water - - - - ), - }, - play: async ({ canvasElement, step }) => { - const canvas = within(canvasElement); - - await step("Open menu", async () => { - await userEvent.click( - canvas.getByRole("button", { name: "More options" }), - ); - await waitFor(() => - Promise.all([ - expect(screen.getByText(/touch grass/i)).toBeInTheDocument(), - expect(screen.getByText(/touch water/i)).toBeInTheDocument(), - ]), - ); - }); - }, -}; - -export { Example as MoreMenu }; diff --git a/site/src/components/MoreMenu/MoreMenu.tsx b/site/src/components/MoreMenu/MoreMenu.tsx deleted file mode 100644 index 8ba7864fc5e5d..0000000000000 --- a/site/src/components/MoreMenu/MoreMenu.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"; -import IconButton, { type IconButtonProps } from "@mui/material/IconButton"; -import Menu, { type MenuProps } from "@mui/material/Menu"; -import MenuItem, { type MenuItemProps } from "@mui/material/MenuItem"; -import { - type FC, - type HTMLProps, - type PropsWithChildren, - type ReactElement, - cloneElement, - createContext, - forwardRef, - useContext, - useRef, - useState, -} from "react"; - -type MoreMenuContextValue = { - triggerRef: React.RefObject; - close: () => void; - open: () => void; - isOpen: boolean; -}; - -const MoreMenuContext = createContext( - undefined, -); - -export const MoreMenu: FC = ({ children }) => { - const triggerRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - - const close = () => { - setIsOpen(false); - }; - - const open = () => { - setIsOpen(true); - }; - - return ( - - {children} - - ); -}; - -const useMoreMenuContext = () => { - const ctx = useContext(MoreMenuContext); - - if (!ctx) { - throw new Error("useMoreMenuContext must be used inside of MoreMenu"); - } - - return ctx; -}; - -export const MoreMenuTrigger: FC> = ({ - children, - ...props -}) => { - const menu = useMoreMenuContext(); - - return cloneElement(children as ReactElement, { - "aria-haspopup": "true", - ...props, - ref: menu.triggerRef, - onClick: menu.open, - }); -}; - -export const ThreeDotsButton = forwardRef( - (props, ref) => { - return ( - - - - ); - }, -); - -export const MoreMenuContent: FC> = ( - props, -) => { - const menu = useMoreMenuContext(); - - return ( - - ); -}; - -interface MoreMenuItemProps extends MenuItemProps { - closeOnClick?: boolean; - danger?: boolean; -} - -export const MoreMenuItem: FC = ({ - closeOnClick = true, - danger = false, - ...menuItemProps -}) => { - const menu = useMoreMenuContext(); - - return ( - ({ - fontSize: 14, - color: danger ? theme.palette.warning.light : undefined, - "& .MuiSvgIcon-root": { - width: 16, - height: 16, - }, - })} - onClick={(e) => { - menuItemProps.onClick?.(e); - if (closeOnClick) { - menu.close(); - } - }} - /> - ); -}; diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerItem.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerItem.tsx index 9df726681a555..9f72601ea9607 100644 --- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerItem.tsx +++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerItem.tsx @@ -3,13 +3,14 @@ import Checkbox from "@mui/material/Checkbox"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; import type { BannerConfig } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { EllipsisVertical } from "lucide-react"; import type { FC } from "react"; interface AnnouncementBannerItemProps { @@ -48,17 +49,25 @@ export const AnnouncementBannerItem: FC = ({ - - - - - - onEdit()}>Edit… - onDelete()} danger> + + + + + + onEdit()}> + Edit… + + onDelete()} + > Delete… - - - + + + ); diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index db439550f2f81..2d1469ed696dd 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -20,18 +20,18 @@ import type { import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; +import { Button as ShadcnButton } from "components/Button/Button"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { LastSeen } from "components/LastSeen/LastSeen"; import { Loader } from "components/Loader/Loader"; -import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; import { SettingsHeader, SettingsHeaderDescription, @@ -51,6 +51,7 @@ import { TableToolbar, } from "components/TableToolbar/TableToolbar"; import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { EllipsisVertical } from "lucide-react"; import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; @@ -330,20 +331,27 @@ const GroupMemberRow: FC = ({ {canUpdate && ( - - - - - - + + + + + + Remove - - - + + + )} diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index 3fb04fe483271..2c360a8dd4e45 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -4,15 +4,15 @@ import AddOutlined from "@mui/icons-material/AddOutlined"; import Button from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; import type { AssignableRoles, Role } from "api/typesGenerated"; +import { Button as ShadcnButton } from "components/Button/Button"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; -import { EmptyState } from "components/EmptyState/EmptyState"; import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { Paywall } from "components/Paywall/Paywall"; import { Stack } from "components/Stack/Stack"; import { @@ -27,6 +27,7 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { EllipsisVertical } from "lucide-react"; import type { FC } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { docs } from "utils/docs"; @@ -213,27 +214,33 @@ const RoleRow: FC = ({ {!role.built_in && (canUpdateOrgRole || canDeleteOrgRole) && ( - - - - - + + + + + + {canUpdateOrgRole && ( - { - navigate(role.name); - }} - > + navigate(role.name)}> Edit - + )} {canDeleteOrgRole && ( - + Delete… - + )} - - + + )} diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx index f828969238cec..660e66ca0ccb2 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx @@ -46,15 +46,19 @@ const renderPage = async () => { const removeMember = async () => { const user = userEvent.setup(); - // Click on the "More options" button to display the "Remove" option - const moreButtons = await screen.findAllByLabelText("More options"); - // get MockUser2 - const selectedMoreButton = moreButtons[0]; - await user.click(selectedMoreButton); + const users = await screen.findAllByText(/.*@coder.com/); + const userRow = users[1].closest("tr"); + if (!userRow) { + throw new Error("Error on get the first user row"); + } + const menuButton = await within(userRow).findByRole("button", { + name: "Open menu", + }); + await user.click(menuButton); - const removeButton = screen.getByText(/Remove/); - await user.click(removeButton); + const removeOption = await screen.findByRole("menuitem", { name: "Remove" }); + await user.click(removeOption); const dialog = await within(document.body).findByRole("dialog"); await user.click(within(dialog).getByRole("button", { name: "Remove" })); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx index 296ea9a8c658a..686842b196b0b 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx @@ -10,15 +10,15 @@ import type { import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; +import { Button } from "components/Button/Button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; -import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; import { PaginationContainer } from "components/PaginationWidget/PaginationContainer"; import { SettingsHeader, @@ -35,7 +35,7 @@ import { } from "components/Table/Table"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import type { PaginationResultInfo } from "hooks/usePaginatedQuery"; -import { TriangleAlert } from "lucide-react"; +import { EllipsisVertical, TriangleAlert } from "lucide-react"; import { UserGroupsCell } from "pages/UsersPage/UsersTable/UserGroupsCell"; import { type FC, useState } from "react"; import { TableColumnHelpTooltip } from "./UserTable/TableColumnHelpTooltip"; @@ -163,19 +163,26 @@ export const OrganizationMembersPageView: FC< {member.user_id !== me.id && canEditMembers && ( - - - - - - + + + + + removeMember(member)} > Remove - - - + + + )} diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index e9970df30c174..83c5b55019715 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -4,7 +4,6 @@ import EditIcon from "@mui/icons-material/EditOutlined"; import CopyIcon from "@mui/icons-material/FileCopyOutlined"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import Button from "@mui/material/Button"; -import Divider from "@mui/material/Divider"; import { workspaces } from "api/queries/workspaces"; import type { AuthorizationResponse, @@ -12,17 +11,18 @@ import type { TemplateVersion, } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; +import { Button as ShadcnButton } from "components/Button/Button"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; import { Margins } from "components/Margins/Margins"; import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; -import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; import { PageHeader, PageHeaderSubtitle, @@ -30,6 +30,7 @@ import { } from "components/PageHeader/PageHeader"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; +import { EllipsisVertical } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { WorkspacePermissions } from "modules/permissions/workspaces"; import type { FC } from "react"; @@ -67,44 +68,48 @@ const TemplateMenu: FC = ({ return ( <> - - - - - - { - navigate(`${templateLink}/settings`); - }} + + + + + + + navigate(`${templateLink}/settings`)} > Settings - + - { - navigate(`${templateLink}/versions/${templateVersion}/edit`); - }} + + navigate(`${templateLink}/versions/${templateVersion}/edit`) + } > Edit files - + - { - navigate(`/templates/new?fromTemplate=${templateId}`); - }} + + navigate(`/templates/new?fromTemplate=${templateId}`) + } > Duplicate… - - - + + + Delete… - - - + + + {safeToDeleteTemplate ? ( {canUpdatePermissions && ( - - - - - - + + + + + onRemoveGroup(group)} > Remove - - - + + + )} @@ -338,19 +346,26 @@ export const TemplatePermissionsPageView: FC< {canUpdatePermissions && ( - - - - - - + + + + + onRemoveUser(user)} > Remove - - - + + + )} diff --git a/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx b/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx index 6cf9204b70540..e44e26fa5aeeb 100644 --- a/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx +++ b/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx @@ -1,7 +1,6 @@ import { useTheme } from "@emotion/react"; import AutorenewIcon from "@mui/icons-material/Autorenew"; import LoadingButton from "@mui/lab/LoadingButton"; -import Divider from "@mui/material/Divider"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; @@ -18,16 +17,17 @@ import type { } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; -import { Loader } from "components/Loader/Loader"; +import { Button } from "components/Button/Button"; import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { Loader } from "components/Loader/Loader"; import { Stack } from "components/Stack/Stack"; import { TableEmpty } from "components/TableEmpty/TableEmpty"; +import { EllipsisVertical } from "lucide-react"; import type { ExternalAuthPollingState } from "pages/CreateWorkspacePage/CreateWorkspacePage"; import { type FC, useCallback, useEffect, useState } from "react"; import { useQuery } from "react-query"; @@ -178,12 +178,15 @@ const ExternalAuthRow: FC = ({ - - - - - - + + + + + { onValidateExternalAuth(); // This is kinda jank. It does a refetch of the thing @@ -194,19 +197,18 @@ const ExternalAuthRow: FC = ({ }} > Test Validate… - - - + { onUnlinkExternalAuth(); await refetch(); }} > Unlink… - - - + + + ); diff --git a/site/src/pages/UsersPage/UsersPage.stories.tsx b/site/src/pages/UsersPage/UsersPage.stories.tsx index 8a3c9bea5d013..88059c35e3096 100644 --- a/site/src/pages/UsersPage/UsersPage.stories.tsx +++ b/site/src/pages/UsersPage/UsersPage.stories.tsx @@ -99,10 +99,8 @@ export const SuspendUserSuccess: Story = { count: 60, }); - await user.click(within(userRow).getByLabelText("More options")); - const suspendButton = await within(userRow).findByText("Suspend", { - exact: false, - }); + await user.click(within(userRow).getByLabelText("Open menu")); + const suspendButton = await within(document.body).findByText("Suspend…"); await user.click(suspendButton); const dialog = await within(document.body).findByRole("dialog"); @@ -120,10 +118,8 @@ export const SuspendUserError: Story = { } 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(within(userRow).getByLabelText("Open menu")); + const suspendButton = await within(document.body).findByText("Suspend…"); await user.click(suspendButton); const dialog = await within(document.body).findByRole("dialog"); @@ -149,10 +145,8 @@ export const DeleteUserSuccess: Story = { count: 59, }); - await user.click(within(userRow).getByLabelText("More options")); - const deleteButton = await within(userRow).findByText("Delete", { - exact: false, - }); + await user.click(within(userRow).getByLabelText("Open menu")); + const deleteButton = await within(document.body).findByText("Delete…"); await user.click(deleteButton); const dialog = await within(document.body).findByRole("dialog"); @@ -172,10 +166,8 @@ export const DeleteUserError: Story = { } spyOn(API, "deleteUser").mockRejectedValue({}); - await user.click(within(userRow).getByLabelText("More options")); - const deleteButton = await within(userRow).findByText("Delete", { - exact: false, - }); + await user.click(within(userRow).getByLabelText("Open menu")); + const deleteButton = await within(document.body).findByText("Delete…"); await user.click(deleteButton); const dialog = await within(document.body).findByRole("dialog"); @@ -220,10 +212,8 @@ export const ActivateUserSuccess: Story = { count: 60, }); - await user.click(within(userRow).getByLabelText("More options")); - const activateButton = await within(userRow).findByText("Activate", { - exact: false, - }); + await user.click(within(userRow).getByLabelText("Open menu")); + const activateButton = await within(document.body).findByText("Activate…"); await user.click(activateButton); const dialog = await within(document.body).findByRole("dialog"); @@ -242,10 +232,8 @@ export const ActivateUserError: Story = { } spyOn(API, "activateUser").mockRejectedValue({}); - await user.click(within(userRow).getByLabelText("More options")); - const activateButton = await within(userRow).findByText("Activate", { - exact: false, - }); + await user.click(within(userRow).getByLabelText("Open menu")); + const activateButton = await within(document.body).findByText("Activate…"); await user.click(activateButton); const dialog = await within(document.body).findByRole("dialog"); @@ -279,10 +267,9 @@ export const ResetUserPasswordSuccess: Story = { } 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(within(userRow).getByLabelText("Open menu")); + const resetPasswordButton = await within(document.body).findByText( + "Reset password…", ); await user.click(resetPasswordButton); @@ -306,10 +293,9 @@ export const ResetUserPasswordError: Story = { } 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(within(userRow).getByLabelText("Open menu")); + const resetPasswordButton = await within(document.body).findByText( + "Reset password…", ); await user.click(resetPasswordButton); diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index f746b35aba75f..d473e2be95fe6 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -1,26 +1,27 @@ import type { Interpolation, Theme } from "@emotion/react"; +import DeleteIcon from "@mui/icons-material/Delete"; import GitHub from "@mui/icons-material/GitHub"; import HideSourceOutlined from "@mui/icons-material/HideSourceOutlined"; import KeyOutlined from "@mui/icons-material/KeyOutlined"; import PasswordOutlined from "@mui/icons-material/PasswordOutlined"; import ShieldOutlined from "@mui/icons-material/ShieldOutlined"; -import Divider from "@mui/material/Divider"; import Skeleton from "@mui/material/Skeleton"; import type { GroupsByUserId } from "api/queries/groups"; import type * as TypesGen from "api/typesGenerated"; import { AvatarData } from "components/Avatar/AvatarData"; import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; import { PremiumBadge } from "components/Badges/Badges"; +import { Button } from "components/Button/Button"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; import { EmptyState } from "components/EmptyState/EmptyState"; import { LastSeen } from "components/LastSeen/LastSeen"; -import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; import { TableCell, TableRow } from "components/Table/Table"; import { TableLoaderSkeleton, @@ -28,6 +29,7 @@ import { } from "components/TableLoader/TableLoader"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import { EllipsisVertical } from "lucide-react"; import type { FC } from "react"; import { UserRoleCell } from "../../OrganizationSettingsPage/UserTable/UserRoleCell"; import { UserGroupsCell } from "./UserGroupsCell"; @@ -180,51 +182,65 @@ export const UsersTableBody: FC = ({ {canEditUsers && ( - - - - - + + + + + {user.status === "active" || user.status === "dormant" ? ( - { - onSuspendUser(user); - }} + onClick={() => onSuspendUser(user)} > Suspend… - + ) : ( - onActivateUser(user)}> + onActivateUser(user)}> Activate… - + )} - onListWorkspaces(user)}> + + onListWorkspaces(user)}> View workspaces - - onViewActivity(user)} - disabled={!canViewActivity} - > - View activity - {!canViewActivity && } - - onResetUserPassword(user)} - disabled={user.login_type !== "password"} - > - Reset password… - - - + + {canViewActivity && ( + onViewActivity(user)} + disabled={!canViewActivity} + > + View activity {!canViewActivity && } + + )} + + {user.login_type === "password" && ( + onResetUserPassword(user)} + disabled={user.login_type !== "password"} + > + Reset password… + + )} + + + + onDeleteUser(user)} disabled={user.id === actorID} - danger > + Delete… - - - + + + )} diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx index 700797c886030..d726a047b7c57 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx @@ -202,9 +202,11 @@ export const OpenDownloadLogs: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole("button", { name: "More options" })); - await userEvent.click(canvas.getByText("Download logs", { exact: false })); + await userEvent.click( + canvas.getByRole("button", { name: "Workspace actions" }), + ); const screen = within(document.body); + await userEvent.click(screen.getByText("Download logs…")); await expect(screen.getByTestId("dialog")).toBeInTheDocument(); }, }; @@ -215,8 +217,11 @@ export const CanDeleteDormantWorkspace: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole("button", { name: "More options" })); - const deleteButton = canvas.getByText("Delete…"); + await userEvent.click( + canvas.getByRole("button", { name: "Workspace actions" }), + ); + const screen = within(document.body); + const deleteButton = screen.getByText("Delete…"); await expect(deleteButton).toBeEnabled(); }, }; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index b187167bb4631..71f890cff3a5b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -2,17 +2,17 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import DownloadOutlined from "@mui/icons-material/DownloadOutlined"; import DuplicateIcon from "@mui/icons-material/FileCopyOutlined"; import HistoryIcon from "@mui/icons-material/HistoryOutlined"; -import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; -import Divider from "@mui/material/Divider"; import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; -import { TopbarIconButton } from "components/FullPageLayout/Topbar"; +import { Button } from "components/Button/Button"; import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, -} from "components/MoreMenu/MoreMenu"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { EllipsisVertical } from "lucide-react"; import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication"; import { type FC, Fragment, type ReactNode, useState } from "react"; import { mustUpdateWorkspace } from "utils/workspace"; @@ -177,56 +177,59 @@ export const WorkspaceActions: FC = ({ onToggle={handleToggleFavorite} /> - - - + + + - - + + Settings - + {canChangeVersions && ( - + Change version… - + )} - Duplicate… - + - setIsDownloadDialogOpen(true)}> + setIsDownloadDialogOpen(true)}> Download logs… - + - + - Delete… - - - + + + = ({ {workspaces?.length === 1 ? "workspace" : "workspaces"} - - + + = ({ > Actions - - - + + @@ -157,28 +156,32 @@ export const WorkspacesPageView: FC = ({ !mustUpdateWorkspace(w, canChangeVersions), ) } + onClick={onStartAll} > Start - - + w.latest_build.status === "running", ) } + onClick={onStopAll} > Stop - - - + + + Update… - - + + Delete… - - - + + + ) : ( !invalidPageNumber && (