mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
chore: replace MoreMenu with DropdownMenu (#17615)
Replace MoreMenu with DropDownMenu component to match update design patterns. Note: This was the result of experimentation using Cursor to make the changes and Claude Code for fixing tests. One key takeaway is that verbose e2e logging, especially benign warnings/errors can confuse Claude Code in running playwright and confirming its work. <img width="201" alt="Screenshot 2025-05-01 at 00 00 52" src="https://github.com/user-attachments/assets/4905582e-902e-4b61-adc8-14cab6bd006b" /> <img width="257" alt="Screenshot 2025-05-01 at 00 01 07" src="https://github.com/user-attachments/assets/5befc420-724a-4c57-9a9d-330a39867fae" /> <img width="270" alt="Screenshot 2025-05-01 at 00 01 20" src="https://github.com/user-attachments/assets/9cbf07cb-7d44-4228-ae6f-216e9f2faed0" /> <img width="224" alt="Screenshot 2025-05-01 at 00 01 30" src="https://github.com/user-attachments/assets/9fe95916-3d9d-4600-9b1f-8a620e152a53" />
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -196,7 +196,7 @@ export const DropdownMenuSeparator = forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn(["-mx-1 my-3 h-px bg-border"], className)}
|
||||
className={cn(["-mx-1 my-2 h-px bg-border"], className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -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<typeof MoreMenu> = {
|
||||
title: "components/MoreMenu",
|
||||
component: MoreMenu,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof MoreMenu>;
|
||||
|
||||
const Example: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem onClick={action("grass")}>
|
||||
<GrassIcon />
|
||||
Touch grass
|
||||
</MoreMenuItem>
|
||||
<MoreMenuItem onClick={action("water")}>
|
||||
<KitesurfingIcon />
|
||||
Touch water
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</>
|
||||
),
|
||||
},
|
||||
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 };
|
||||
@@ -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<HTMLButtonElement>;
|
||||
close: () => void;
|
||||
open: () => void;
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
const MoreMenuContext = createContext<MoreMenuContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const MoreMenu: FC<PropsWithChildren> = ({ children }) => {
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const close = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<MoreMenuContext.Provider value={{ close, open, triggerRef, isOpen }}>
|
||||
{children}
|
||||
</MoreMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useMoreMenuContext = () => {
|
||||
const ctx = useContext(MoreMenuContext);
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error("useMoreMenuContext must be used inside of MoreMenu");
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const MoreMenuTrigger: FC<HTMLProps<HTMLButtonElement>> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const menu = useMoreMenuContext();
|
||||
|
||||
return cloneElement(children as ReactElement, {
|
||||
"aria-haspopup": "true",
|
||||
...props,
|
||||
ref: menu.triggerRef,
|
||||
onClick: menu.open,
|
||||
});
|
||||
};
|
||||
|
||||
export const ThreeDotsButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<IconButton
|
||||
aria-controls="more-options"
|
||||
aria-label="More options"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<MoreVertOutlined />
|
||||
</IconButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const MoreMenuContent: FC<Omit<MenuProps, "open" | "onClose">> = (
|
||||
props,
|
||||
) => {
|
||||
const menu = useMoreMenuContext();
|
||||
|
||||
return (
|
||||
<Menu
|
||||
id="more-options"
|
||||
anchorEl={menu.triggerRef.current}
|
||||
open={menu.isOpen}
|
||||
onClose={menu.close}
|
||||
disablePortal
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface MoreMenuItemProps extends MenuItemProps {
|
||||
closeOnClick?: boolean;
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
export const MoreMenuItem: FC<MoreMenuItemProps> = ({
|
||||
closeOnClick = true,
|
||||
danger = false,
|
||||
...menuItemProps
|
||||
}) => {
|
||||
const menu = useMoreMenuContext();
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
{...menuItemProps}
|
||||
css={(theme) => ({
|
||||
fontSize: 14,
|
||||
color: danger ? theme.palette.warning.light : undefined,
|
||||
"& .MuiSvgIcon-root": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
})}
|
||||
onClick={(e) => {
|
||||
menuItemProps.onClick?.(e);
|
||||
if (closeOnClick) {
|
||||
menu.close();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+25
-16
@@ -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<AnnouncementBannerItemProps> = ({
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem onClick={() => onEdit()}>Edit…</MoreMenuItem>
|
||||
<MoreMenuItem onClick={() => onDelete()} danger>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon-lg" variant="subtle" aria-label="Open menu">
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onEdit()}>
|
||||
Edit…
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
Delete…
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -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<GroupMemberRowProps> = ({
|
||||
</TableCell>
|
||||
<TableCell width="1%">
|
||||
{canUpdate && (
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem
|
||||
danger
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ShadcnButton
|
||||
size="icon-lg"
|
||||
variant="subtle"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</ShadcnButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={onRemove}
|
||||
disabled={group.id === group.organization_id}
|
||||
>
|
||||
Remove
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -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<RoleRowProps> = ({
|
||||
|
||||
<TableCell>
|
||||
{!role.built_in && (canUpdateOrgRole || canDeleteOrgRole) && (
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ShadcnButton
|
||||
size="icon-lg"
|
||||
variant="subtle"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</ShadcnButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{canUpdateOrgRole && (
|
||||
<MoreMenuItem
|
||||
onClick={() => {
|
||||
navigate(role.name);
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => navigate(role.name)}>
|
||||
Edit
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canDeleteOrgRole && (
|
||||
<MoreMenuItem danger onClick={onDelete}>
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
Delete…
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
@@ -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<
|
||||
<UserGroupsCell userGroups={member.groups} />
|
||||
<TableCell>
|
||||
{member.user_id !== me.id && canEditMembers && (
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem
|
||||
danger
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon-lg"
|
||||
variant="subtle"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={() => removeMember(member)}
|
||||
>
|
||||
Remove
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -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<TemplateMenuProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem
|
||||
onClick={() => {
|
||||
navigate(`${templateLink}/settings`);
|
||||
}}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ShadcnButton size="icon-lg" variant="subtle" aria-label="Open menu">
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</ShadcnButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate(`${templateLink}/settings`)}
|
||||
>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<MoreMenuItem
|
||||
onClick={() => {
|
||||
navigate(`${templateLink}/versions/${templateVersion}/edit`);
|
||||
}}
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
navigate(`${templateLink}/versions/${templateVersion}/edit`)
|
||||
}
|
||||
>
|
||||
<EditIcon />
|
||||
Edit files
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<MoreMenuItem
|
||||
onClick={() => {
|
||||
navigate(`/templates/new?fromTemplate=${templateId}`);
|
||||
}}
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
navigate(`/templates/new?fromTemplate=${templateId}`)
|
||||
}
|
||||
>
|
||||
<CopyIcon />
|
||||
Duplicate…
|
||||
</MoreMenuItem>
|
||||
<Divider />
|
||||
<MoreMenuItem onClick={dialogState.openDeleteConfirmation} danger>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={dialogState.openDeleteConfirmation}
|
||||
>
|
||||
<DeleteIcon />
|
||||
Delete…
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{safeToDeleteTemplate ? (
|
||||
<DeleteDialog
|
||||
|
||||
+42
-27
@@ -19,18 +19,19 @@ import type {
|
||||
} from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { Button } 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 { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { TableLoader } from "components/TableLoader/TableLoader";
|
||||
import { EllipsisVertical } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { getGroupSubtitle } from "utils/groups";
|
||||
import {
|
||||
@@ -289,19 +290,26 @@ export const TemplatePermissionsPageView: FC<
|
||||
|
||||
<TableCell>
|
||||
{canUpdatePermissions && (
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem
|
||||
danger
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon-lg"
|
||||
variant="subtle"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={() => onRemoveGroup(group)}
|
||||
>
|
||||
Remove
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -338,19 +346,26 @@ export const TemplatePermissionsPageView: FC<
|
||||
|
||||
<TableCell>
|
||||
{canUpdatePermissions && (
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem
|
||||
danger
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon-lg"
|
||||
variant="subtle"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={() => onRemoveUser(user)}
|
||||
>
|
||||
Remove
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -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<ExternalAuthRowProps> = ({
|
||||
</LoadingButton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton size="small" disabled={!authenticated} />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon-lg" variant="subtle" aria-label="Open menu">
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
onValidateExternalAuth();
|
||||
// This is kinda jank. It does a refetch of the thing
|
||||
@@ -194,19 +197,18 @@ const ExternalAuthRow: FC<ExternalAuthRowProps> = ({
|
||||
}}
|
||||
>
|
||||
Test Validate…
|
||||
</MoreMenuItem>
|
||||
<Divider />
|
||||
<MoreMenuItem
|
||||
danger
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={async () => {
|
||||
onUnlinkExternalAuth();
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
Unlink…
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<UsersTableBodyProps> = ({
|
||||
|
||||
{canEditUsers && (
|
||||
<TableCell>
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon-lg"
|
||||
variant="subtle"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{user.status === "active" || user.status === "dormant" ? (
|
||||
<MoreMenuItem
|
||||
<DropdownMenuItem
|
||||
data-testid="suspend-button"
|
||||
onClick={() => {
|
||||
onSuspendUser(user);
|
||||
}}
|
||||
onClick={() => onSuspendUser(user)}
|
||||
>
|
||||
Suspend…
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<MoreMenuItem onClick={() => onActivateUser(user)}>
|
||||
<DropdownMenuItem onClick={() => onActivateUser(user)}>
|
||||
Activate…
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<MoreMenuItem onClick={() => onListWorkspaces(user)}>
|
||||
|
||||
<DropdownMenuItem onClick={() => onListWorkspaces(user)}>
|
||||
View workspaces
|
||||
</MoreMenuItem>
|
||||
<MoreMenuItem
|
||||
onClick={() => onViewActivity(user)}
|
||||
disabled={!canViewActivity}
|
||||
>
|
||||
View activity
|
||||
{!canViewActivity && <PremiumBadge />}
|
||||
</MoreMenuItem>
|
||||
<MoreMenuItem
|
||||
onClick={() => onResetUserPassword(user)}
|
||||
disabled={user.login_type !== "password"}
|
||||
>
|
||||
Reset password…
|
||||
</MoreMenuItem>
|
||||
<Divider />
|
||||
<MoreMenuItem
|
||||
</DropdownMenuItem>
|
||||
|
||||
{canViewActivity && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onViewActivity(user)}
|
||||
disabled={!canViewActivity}
|
||||
>
|
||||
View activity {!canViewActivity && <PremiumBadge />}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{user.login_type === "password" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onResetUserPassword(user)}
|
||||
disabled={user.login_type !== "password"}
|
||||
>
|
||||
Reset password…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={() => onDeleteUser(user)}
|
||||
disabled={user.id === actorID}
|
||||
danger
|
||||
>
|
||||
<DeleteIcon />
|
||||
Delete…
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<WorkspaceActionsProps> = ({
|
||||
onToggle={handleToggleFavorite}
|
||||
/>
|
||||
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<TopbarIconButton
|
||||
title="More options"
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon-lg"
|
||||
variant="subtle"
|
||||
aria-label="Workspace actions"
|
||||
data-testid="workspace-options-button"
|
||||
aria-controls="workspace-options"
|
||||
disabled={!canAcceptJobs}
|
||||
>
|
||||
<MoreVertOutlined />
|
||||
</TopbarIconButton>
|
||||
</MoreMenuTrigger>
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
<span className="sr-only">Workspace actions</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<MoreMenuContent id="workspace-options">
|
||||
<MoreMenuItem onClick={handleSettings}>
|
||||
<DropdownMenuContent id="workspace-options" align="end">
|
||||
<DropdownMenuItem onClick={handleSettings}>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{canChangeVersions && (
|
||||
<MoreMenuItem onClick={handleChangeVersion}>
|
||||
<DropdownMenuItem onClick={handleChangeVersion}>
|
||||
<HistoryIcon />
|
||||
Change version…
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<MoreMenuItem
|
||||
<DropdownMenuItem
|
||||
onClick={duplicateWorkspace}
|
||||
disabled={!isDuplicationReady}
|
||||
>
|
||||
<DuplicateIcon />
|
||||
Duplicate…
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<MoreMenuItem onClick={() => setIsDownloadDialogOpen(true)}>
|
||||
<DropdownMenuItem onClick={() => setIsDownloadDialogOpen(true)}>
|
||||
<DownloadOutlined />
|
||||
Download logs…
|
||||
</MoreMenuItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<Divider />
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<MoreMenuItem
|
||||
danger
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={handleDelete}
|
||||
data-testid="delete-button"
|
||||
>
|
||||
<DeleteIcon />
|
||||
Delete…
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DownloadLogsDialog
|
||||
workspace={workspace}
|
||||
|
||||
@@ -4,19 +4,19 @@ import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutl
|
||||
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined";
|
||||
import StopOutlined from "@mui/icons-material/StopOutlined";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import { hasError, isApiValidationError } from "api/errors";
|
||||
import type { Template, Workspace } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "components/DropdownMenu/DropdownMenu";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import {
|
||||
MoreMenu,
|
||||
MoreMenuContent,
|
||||
MoreMenuItem,
|
||||
MoreMenuTrigger,
|
||||
} from "components/MoreMenu/MoreMenu";
|
||||
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
|
||||
import { PaginationHeader } from "components/PaginationWidget/PaginationHeader";
|
||||
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase";
|
||||
@@ -134,8 +134,8 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
{workspaces?.length === 1 ? "workspace" : "workspaces"}
|
||||
</div>
|
||||
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<LoadingButton
|
||||
loading={isRunningBatchAction}
|
||||
loadingPosition="end"
|
||||
@@ -146,10 +146,9 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
>
|
||||
Actions
|
||||
</LoadingButton>
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem
|
||||
onClick={onStartAll}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
disabled={
|
||||
!checkedWorkspaces?.every(
|
||||
(w) =>
|
||||
@@ -157,28 +156,32 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
!mustUpdateWorkspace(w, canChangeVersions),
|
||||
)
|
||||
}
|
||||
onClick={onStartAll}
|
||||
>
|
||||
<PlayArrowOutlined /> Start
|
||||
</MoreMenuItem>
|
||||
<MoreMenuItem
|
||||
onClick={onStopAll}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={
|
||||
!checkedWorkspaces?.every(
|
||||
(w) => w.latest_build.status === "running",
|
||||
)
|
||||
}
|
||||
onClick={onStopAll}
|
||||
>
|
||||
<StopOutlined /> Stop
|
||||
</MoreMenuItem>
|
||||
<Divider />
|
||||
<MoreMenuItem onClick={onUpdateAll}>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onUpdateAll}>
|
||||
<CloudQueue /> Update…
|
||||
</MoreMenuItem>
|
||||
<MoreMenuItem danger onClick={onDeleteAll}>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={onDeleteAll}
|
||||
>
|
||||
<DeleteOutlined /> Delete…
|
||||
</MoreMenuItem>
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : (
|
||||
!invalidPageNumber && (
|
||||
|
||||
Reference in New Issue
Block a user