From f677c4470ba44fd49d547c3102ee9af1d98a1b27 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Oct 2023 09:13:21 -0300 Subject: [PATCH] chore(site): add custom popover component (#10319) --- site/.eslintrc.yaml | 4 + .../Navbar/UserDropdown/BorderedMenu.tsx | 30 -- .../UserDropdown/UserDropdown.stories.tsx | 9 +- .../Navbar/UserDropdown/UserDropdown.tsx | 137 +++++---- .../UserDropdownContent.stories.tsx | 42 --- .../UserDropdown/UserDropdownContent.test.tsx | 17 +- .../UserDropdown/UserDropdownContent.tsx | 9 +- .../components/HelpTooltip/HelpTooltip.tsx | 2 + site/src/components/IconField/IconField.tsx | 66 +++-- site/src/components/Popover/Popover.tsx | 178 ++++++++++++ .../PopoverContainer.stories.tsx | 20 -- .../PopoverContainer/PopoverContainer.tsx | 244 ---------------- .../Resources/PortForwardButton.tsx | 91 +++--- .../Resources/SSHButton/SSHButton.stories.tsx | 2 +- .../Resources/SSHButton/SSHButton.tsx | 51 ++-- .../TemplateInsightsPage/DateRange.tsx | 184 ++++++------ site/src/pages/TerminalPage/TerminalPage.tsx | 158 +++++----- .../UsersTable/EditRolesButton.stories.tsx | 2 +- .../UsersPage/UsersTable/EditRolesButton.tsx | 61 ++-- .../BuildParametersPopover.tsx | 178 ++++++------ .../pages/WorkspacePage/WorkspaceStats.tsx | 269 +++++++++--------- .../pages/WorkspacesPage/WorkspacesButton.tsx | 58 +--- 22 files changed, 793 insertions(+), 1019 deletions(-) delete mode 100644 site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu.tsx delete mode 100644 site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.stories.tsx create mode 100644 site/src/components/Popover/Popover.tsx delete mode 100644 site/src/components/PopoverContainer/PopoverContainer.stories.tsx delete mode 100644 site/src/components/PopoverContainer/PopoverContainer.tsx diff --git a/site/.eslintrc.yaml b/site/.eslintrc.yaml index 478c1e0704..018bf9043d 100644 --- a/site/.eslintrc.yaml +++ b/site/.eslintrc.yaml @@ -135,6 +135,10 @@ rules: message: "You should use the Alert component provided on components/Alert/Alert" + - name: "@mui/material/Popover" + message: + "You should use the Popover component provided on + components/Popover/Popover" no-unused-vars: "off" "object-curly-spacing": "off" react-hooks/exhaustive-deps: warn diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu.tsx deleted file mode 100644 index b57984aa6d..0000000000 --- a/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { css } from "@emotion/css"; -import { useTheme } from "@emotion/react"; -import Popover, { type PopoverProps } from "@mui/material/Popover"; -import type { FC, PropsWithChildren } from "react"; - -type BorderedMenuVariant = "user-dropdown"; - -export type BorderedMenuProps = Omit & { - variant?: BorderedMenuVariant; -}; - -export const BorderedMenu: FC> = ({ - children, - variant, - ...rest -}) => { - const theme = useTheme(); - - const paper = css` - width: 260px; - border-radius: ${theme.shape.borderRadius}px; - box-shadow: ${theme.shadows[6]}; - `; - - return ( - - {children} - - ); -}; diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx index 7ebffa89a0..394d4846e2 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx @@ -1,4 +1,4 @@ -import { MockUser } from "testHelpers/entities"; +import { MockBuildInfo, MockUser } from "testHelpers/entities"; import { UserDropdown } from "./UserDropdown"; import type { Meta, StoryObj } from "@storybook/react"; @@ -7,6 +7,13 @@ const meta: Meta = { component: UserDropdown, args: { user: MockUser, + isDefaultOpen: true, + buildInfo: MockBuildInfo, + supportLinks: [ + { icon: "docs", name: "Documentation", target: "" }, + { icon: "bug", name: "Report a bug", target: "" }, + { icon: "chat", name: "Join the Coder Discord", target: "" }, + ], }, }; diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx index 50143eeecf..d8194ac8c5 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx @@ -1,21 +1,25 @@ import Badge from "@mui/material/Badge"; -import MenuItem from "@mui/material/MenuItem"; -import { useState, FC, PropsWithChildren, MouseEvent } from "react"; +import { FC, PropsWithChildren } from "react"; import { colors } from "theme/colors"; import * as TypesGen from "api/typesGenerated"; import { navHeight } from "theme/constants"; -import { BorderedMenu } from "./BorderedMenu"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { UserDropdownContent } from "./UserDropdownContent"; import { BUTTON_SM_HEIGHT } from "theme/theme"; import { css } from "@emotion/react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; export interface UserDropdownProps { user: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; supportLinks?: TypesGen.LinkConfig[]; onSignOut: () => void; + isDefaultOpen?: boolean; } export const UserDropdown: FC> = ({ @@ -23,76 +27,69 @@ export const UserDropdown: FC> = ({ user, supportLinks, onSignOut, + isDefaultOpen, }: UserDropdownProps) => { - const [anchorEl, setAnchorEl] = useState(); - - const handleDropdownClick = (ev: MouseEvent): void => { - setAnchorEl(ev.currentTarget); - }; - const onPopoverClose = () => { - setAnchorEl(undefined); - }; - return ( - <> - css` - height: ${navHeight}px; - padding: ${theme.spacing(1.5, 0)}; + + {(popover) => ( + <> + + + + + ({ + ".MuiPaper-root": { + width: 260, + boxShadow: theme.shadows[6], + }, + })} + > + - - - - - - - - - + + + )} + ); }; diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.stories.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.stories.tsx deleted file mode 100644 index f9f8c5a8a5..0000000000 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.stories.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { MockUser } from "testHelpers/entities"; -import { UserDropdownContent } from "./UserDropdownContent"; -import type { Meta, StoryObj } from "@storybook/react"; - -const meta: Meta = { - title: "components/UserDropdownContent", - component: UserDropdownContent, -}; - -export default meta; -type Story = StoryObj; - -export const ExampleNoRoles: Story = { - args: { - user: { - ...MockUser, - roles: [], - }, - }, -}; - -export const ExampleOneRole: Story = { - args: { - user: { - ...MockUser, - roles: [{ name: "member", display_name: "Member" }], - }, - }, -}; - -export const ExampleThreeRoles: Story = { - args: { - user: { - ...MockUser, - roles: [ - { name: "admin", display_name: "Admin" }, - { name: "member", display_name: "Member" }, - { name: "auditor", display_name: "Auditor" }, - ], - }, - }, -}; diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.test.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.test.tsx index 4e488f04d0..f9650a60d0 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.test.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.test.tsx @@ -2,15 +2,14 @@ import { screen } from "@testing-library/react"; import { MockUser } from "testHelpers/entities"; import { render, waitForLoaderToBeRemoved } from "testHelpers/renderHelpers"; import { Language, UserDropdownContent } from "./UserDropdownContent"; +import { Popover } from "components/Popover/Popover"; describe("UserDropdownContent", () => { it("has the correct link for the account item", async () => { render( - , + + + , ); await waitForLoaderToBeRemoved(); @@ -25,11 +24,9 @@ describe("UserDropdownContent", () => { it("calls the onSignOut function", async () => { const onSignOut = jest.fn(); render( - , + + + , ); await waitForLoaderToBeRemoved(); screen.getByText(Language.signOutLabel).click(); diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index ebc103da57..99d2d65a48 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -16,6 +16,7 @@ import { type Interpolation, type Theme, } from "@emotion/react"; +import { usePopover } from "components/Popover/Popover"; export const Language = { accountLabel: "Account", @@ -82,7 +83,6 @@ export interface UserDropdownContentProps { user: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; supportLinks?: TypesGen.LinkConfig[]; - onPopoverClose: () => void; onSignOut: () => void; } @@ -90,9 +90,14 @@ export const UserDropdownContent: FC = ({ buildInfo, user, supportLinks, - onPopoverClose, onSignOut, }) => { + const popover = usePopover(); + + const onPopoverClose = () => { + popover.setIsOpen(false); + }; + return (
diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx index 92990c318f..a9c01acb44 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -1,4 +1,6 @@ import Link from "@mui/material/Link"; +// This is used as base for the main HelpTooltip component +// eslint-disable-next-line no-restricted-imports -- Read above import Popover, { PopoverProps } from "@mui/material/Popover"; import HelpIcon from "@mui/icons-material/HelpOutline"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index ab6fd044a8..6e8bf11506 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -1,15 +1,19 @@ import Button from "@mui/material/Button"; import InputAdornment from "@mui/material/InputAdornment"; -import Popover from "@mui/material/Popover"; import TextField, { TextFieldProps } from "@mui/material/TextField"; import { makeStyles } from "@mui/styles"; import Picker from "@emoji-mart/react"; -import { useRef, FC, useState } from "react"; +import { FC } from "react"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { Stack } from "components/Stack/Stack"; import { colors } from "theme/colors"; import data from "@emoji-mart/data/sets/14/twitter.json"; import icons from "theme/icons.json"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; // See: https://github.com/missive/emoji-mart/issues/51#issuecomment-287353222 const urlFromUnifiedCode = (unified: string) => @@ -45,8 +49,6 @@ const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { } const styles = useStyles(); - const emojiButtonRef = useRef(null); - const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); const hasIcon = textFieldProps.value && textFieldProps.value !== ""; return ( @@ -71,36 +73,32 @@ const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { }} /> - - - { - setIsEmojiPickerOpen(false); - }} - > - { - const value = emoji.src ?? urlFromUnifiedCode(emoji.unified); - onPickEmoji(value); - setIsEmojiPickerOpen(false); - }} - /> + + {(popover) => ( + <> + + + + + { + const value = emoji.src ?? urlFromUnifiedCode(emoji.unified); + onPickEmoji(value); + popover.setIsOpen(false); + }} + /> + + + )} ); diff --git a/site/src/components/Popover/Popover.tsx b/site/src/components/Popover/Popover.tsx new file mode 100644 index 0000000000..aa9b39183e --- /dev/null +++ b/site/src/components/Popover/Popover.tsx @@ -0,0 +1,178 @@ +import { + ReactElement, + ReactNode, + cloneElement, + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; +// This is used as base for the main Popover component +// eslint-disable-next-line no-restricted-imports -- Read above +import MuiPopover, { + type PopoverProps as MuiPopoverProps, +} from "@mui/material/Popover"; + +type TriggerMode = "hover" | "click"; + +type TriggerRef = React.RefObject; + +type TriggerElement = ReactElement<{ + onClick?: () => void; + ref: TriggerRef; +}>; + +type PopoverContextValue = { + isOpen: boolean; + setIsOpen: React.Dispatch>; + triggerRef: TriggerRef; + mode: TriggerMode; +}; + +const PopoverContext = createContext( + undefined, +); + +export const Popover = (props: { + children: ReactNode | ((popover: PopoverContextValue) => ReactNode); // Allows inline usage + mode?: TriggerMode; + isDefaultOpen?: boolean; +}) => { + const [isOpen, setIsOpen] = useState(props.isDefaultOpen ?? false); + const triggerRef = useRef(null); + const value = { isOpen, setIsOpen, triggerRef, mode: props.mode ?? "click" }; + + return ( + + {typeof props.children === "function" + ? props.children(value) + : props.children} + + ); +}; + +export const usePopover = () => { + const context = useContext(PopoverContext); + if (!context) { + throw new Error( + "Popover compound components cannot be rendered outside the Popover component", + ); + } + return context; +}; + +export const PopoverTrigger = (props: { + children: TriggerElement; + hover?: boolean; +}) => { + const popover = usePopover(); + + const clickProps = { + onClick: () => { + popover.setIsOpen((isOpen) => !isOpen); + }, + }; + + const hoverProps = { + onPointerEnter: () => { + popover.setIsOpen(true); + }, + onPointerLeave: () => { + popover.setIsOpen(false); + }, + }; + + return cloneElement(props.children, { + ...(popover.mode === "click" ? clickProps : hoverProps), + ref: popover.triggerRef, + }); +}; + +type Horizontal = "left" | "right"; + +export const PopoverContent = ( + props: Omit & { + horizontal?: Horizontal; + }, +) => { + const popover = usePopover(); + const [isReady, setIsReady] = useState(false); + const horizontal = props.horizontal ?? "left"; + const hoverMode = popover.mode === "hover"; + + // This is a hack to make sure the popover is not rendered until the trigger + // is ready. This is a limitation on MUI that does not support defaultIsOpen + // on Popover but we need it to storybook the component. + useEffect(() => { + if (!isReady && popover.triggerRef.current !== null) { + setIsReady(true); + } + }, [isReady, popover.triggerRef]); + + if (!popover.triggerRef.current) { + return null; + } + + return ( + ({ + // When it is on hover mode, and the moude is moving from the trigger to + // the popover, if there is any space, the popover will be closed. I + // found this is a limitation on how MUI structured the component. It is + // not a big issue for now but we can reavaluate it in the future. + marginTop: hoverMode ? undefined : theme.spacing(1), + pointerEvents: hoverMode ? "none" : undefined, + "& .MuiPaper-root": { + minWidth: theme.spacing(40), + fontSize: 14, + pointerEvents: hoverMode ? "auto" : undefined, + }, + })} + {...horizontalProps(horizontal)} + {...modeProps(popover)} + {...props} + open={popover.isOpen} + onClose={() => popover.setIsOpen(false)} + anchorEl={popover.triggerRef.current} + /> + ); +}; + +const modeProps = (popover: PopoverContextValue) => { + if (popover.mode === "hover") { + return { + onMouseEnter: () => { + popover.setIsOpen(true); + }, + onMouseLeave: () => { + popover.setIsOpen(false); + }, + }; + } + + return {}; +}; + +const horizontalProps = (horizontal: Horizontal) => { + if (horizontal === "right") { + return { + anchorOrigin: { + vertical: "bottom", + horizontal: "right", + }, + transformOrigin: { + vertical: "top", + horizontal: "right", + }, + } as const; + } + + return { + anchorOrigin: { + vertical: "bottom", + horizontal: "left", + }, + } as const; +}; diff --git a/site/src/components/PopoverContainer/PopoverContainer.stories.tsx b/site/src/components/PopoverContainer/PopoverContainer.stories.tsx deleted file mode 100644 index 879faf7d38..0000000000 --- a/site/src/components/PopoverContainer/PopoverContainer.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import { PopoverContainer } from "./PopoverContainer"; -import Button from "@mui/material/Button"; - -const meta: Meta = { - title: "components/PopoverContainer", - component: PopoverContainer, - args: { - anchorButton: , - children:

Hiya!

, - originY: "bottom", - }, -}; - -export default meta; - -type Story = StoryObj; -const Example: Story = {}; - -export { Example as PopoverContainer }; diff --git a/site/src/components/PopoverContainer/PopoverContainer.tsx b/site/src/components/PopoverContainer/PopoverContainer.tsx deleted file mode 100644 index 833d8b267d..0000000000 --- a/site/src/components/PopoverContainer/PopoverContainer.tsx +++ /dev/null @@ -1,244 +0,0 @@ -/** - * @file Abstracts over MUI's Popover component to simplify using it (and hide - * some of the wonkier parts of the API). - * - * Just place a button and some content in the component, and things just work. - * No setup needed with hooks or refs. - */ -import { - type KeyboardEvent, - type MouseEvent, - type PropsWithChildren, - type ReactElement, - createContext, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from "react"; - -import { type Theme, type SystemStyleObject, Box } from "@mui/system"; -import Popover, { type PopoverOrigin } from "@mui/material/Popover"; -import { useNavigate, type LinkProps } from "react-router-dom"; -import { useTheme } from "@emotion/react"; - -function getButton(container: HTMLElement) { - return ( - container.querySelector("button") ?? - container.querySelector('[aria-role="button"]') - ); -} - -const ClosePopoverContext = createContext<(() => void) | null>(null); - -type PopoverLinkProps = LinkProps & { - to: string; - sx?: SystemStyleObject; -}; - -/** - * A custom version of a React Router Link that makes sure to close the popover - * before starting a navigation. - * - * This is necessary because React Router's navigation logic doesn't work well - * with modals (including MUI's base Popover component). - * - * --- - * If the page being navigated to has lazy loading and isn't available yet, the - * previous components are supposed to be hidden during the transition, but - * because most React modals use React.createPortal to put content outside of - * the main DOM tree, React Router has no way of knowing about them. So open - * modals have a high risk of not disappearing until the page transition - * finishes and the previous components fully unmount. - */ -export function PopoverLink({ - children, - to, - sx, - ...linkProps -}: PopoverLinkProps) { - const closePopover = useContext(ClosePopoverContext); - if (closePopover === null) { - throw new Error("PopoverLink is not located inside of a PopoverContainer"); - } - - // Luckily, useNavigate and Link are designed to be imperative/declarative - // mirrors of each other, so their inputs should never get out of sync - const navigate = useNavigate(); - const theme = useTheme(); - - const onClick = (event: MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - closePopover(); - - // Hacky, but by using a promise to push the navigation to resolve via the - // micro-task queue, there's guaranteed to be a period for the popover to - // close. Tried React DOM's flushSync function, but it was unreliable. - void Promise.resolve().then(() => { - navigate(to, linkProps); - }); - }; - - return ( - - {children} - - ); -} - -type PopoverContainerProps = PropsWithChildren<{ - /** - * Does not require any hooks or refs to work. Also does not override any refs - * or event handlers attached to the button. - */ - anchorButton: ReactElement; - - width?: number; - originX?: PopoverOrigin["horizontal"]; - originY?: PopoverOrigin["vertical"]; - sx?: SystemStyleObject; -}>; - -export function PopoverContainer({ - children, - anchorButton, - originX = 0, - originY = 0, - width = 320, - sx = {}, -}: PopoverContainerProps) { - const parentClosePopover = useContext(ClosePopoverContext); - if (parentClosePopover !== null) { - throw new Error( - "Popover detected inside of Popover - this will always be a bad user experience", - ); - } - - const buttonContainerRef = useRef(null); - - // Ref value is for effects and event listeners; state value is for React - // renders. Have to duplicate state because after the initial render, it's - // never safe to reference ref contents inside a render path, especially with - // React 18 concurrency. Duplication is a necessary evil because of MUI's - // weird, clunky APIs - const anchorButtonRef = useRef(null); - const [loadedButton, setLoadedButton] = useState(); - - // Makes container listen to changes in button. If this approach becomes - // untenable in the future, it can be replaced with React.cloneElement, but - // the trade-off there is that every single anchorButton will need to be - // wrapped inside React.forwardRef, making the abstraction leak a little more - useEffect(() => { - const buttonContainer = buttonContainerRef.current; - if (buttonContainer === null) { - throw new Error("Please attach container ref to button container"); - } - - const initialButton = getButton(buttonContainer); - if (initialButton === null) { - throw new Error("Initial ref query failed"); - } - anchorButtonRef.current = initialButton; - - const onContainerMutation: MutationCallback = () => { - const newButton = getButton(buttonContainer); - if (newButton === null) { - throw new Error("Semantic button removed after DOM update"); - } - - anchorButtonRef.current = newButton; - setLoadedButton((current) => { - return current === undefined ? undefined : newButton; - }); - }; - - const observer = new MutationObserver(onContainerMutation); - observer.observe(buttonContainer, { - childList: true, - subtree: true, - }); - - return () => observer.disconnect(); - }, []); - - // Not using useInteractive because the container element is just meant to - // catch events from the inner button, not act as a button itself - const onInnerButtonInteraction = () => { - if (anchorButtonRef.current === null) { - throw new Error("Usable ref value is unavailable"); - } - - setLoadedButton(anchorButtonRef.current); - }; - - const onInnerButtonKeydown = (event: KeyboardEvent) => { - if (event.key === "Enter" || event.key === " ") { - onInnerButtonInteraction(); - } - }; - - const closePopover = useCallback(() => { - setLoadedButton(undefined); - }, []); - - return ( - <> - {/* Cannot switch with Box component; breaks implementation */} -
- {anchorButton} -
- - - - {children} - - - - ); -} diff --git a/site/src/components/Resources/PortForwardButton.tsx b/site/src/components/Resources/PortForwardButton.tsx index f22e0e2082..83f54aeebb 100644 --- a/site/src/components/Resources/PortForwardButton.tsx +++ b/site/src/components/Resources/PortForwardButton.tsx @@ -1,11 +1,9 @@ import Box from "@mui/material/Box"; import Link from "@mui/material/Link"; -import Popover from "@mui/material/Popover"; import CircularProgress from "@mui/material/CircularProgress"; import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import { css } from "@emotion/css"; import { useTheme } from "@emotion/react"; -import { useRef, useState } from "react"; import { useQuery } from "react-query"; import { colors } from "theme/colors"; import { @@ -22,6 +20,11 @@ import type { WorkspaceAgentListeningPort, } from "api/typesGenerated"; import { portForwardURL } from "utils/portForward"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; export interface PortForwardButtonProps { host: string; @@ -32,9 +35,6 @@ export interface PortForwardButtonProps { export const PortForwardButton: React.FC = (props) => { const theme = useTheme(); - const anchorRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - const id = isOpen ? "schedule-popover" : undefined; const portsQuery = useQuery({ queryKey: ["portForward", props.agent.id], queryFn: () => getAgentListeningPorts(props.agent.id), @@ -42,43 +42,36 @@ export const PortForwardButton: React.FC = (props) => { refetchInterval: 5_000, }); - const onClose = () => { - setIsOpen(false); - }; - return ( - <> - { - setIsOpen(true); - }} - > - Ports - {portsQuery.data ? ( - theme.spacing(0, 0.5), - borderRadius: "50%", - display: "flex", - alignItems: "center", - justifyContent: "center", - backgroundColor: colors.gray[11], - ml: 1, - }} - > - {portsQuery.data.ports.length} - - ) : ( - - )} - - + + + Ports + {portsQuery.data ? ( + theme.spacing(0, 0.5), + borderRadius: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.gray[11], + ml: 1, + }} + > + {portsQuery.data.ports.length} + + ) : ( + + )} + + + = (props) => { margin-top: ${theme.spacing(0.5)}; `, }} - id={id} - open={isOpen} - anchorEl={anchorRef.current} - onClose={onClose} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", - }} > - - + + ); }; diff --git a/site/src/components/Resources/SSHButton/SSHButton.stories.tsx b/site/src/components/Resources/SSHButton/SSHButton.stories.tsx index 5829af000d..b5e451160f 100644 --- a/site/src/components/Resources/SSHButton/SSHButton.stories.tsx +++ b/site/src/components/Resources/SSHButton/SSHButton.stories.tsx @@ -22,7 +22,7 @@ export const Opened: Story = { args: { workspaceName: MockWorkspace.name, agentName: MockWorkspaceAgent.name, - defaultIsOpen: true, + isDefaultOpen: true, sshPrefix: "coder.", }, }; diff --git a/site/src/components/Resources/SSHButton/SSHButton.tsx b/site/src/components/Resources/SSHButton/SSHButton.tsx index 846ba5baaa..d9827d0795 100644 --- a/site/src/components/Resources/SSHButton/SSHButton.tsx +++ b/site/src/components/Resources/SSHButton/SSHButton.tsx @@ -1,7 +1,6 @@ -import Popover from "@mui/material/Popover"; import { css } from "@emotion/css"; import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import { type FC, type PropsWithChildren, useRef, useState } from "react"; +import { type FC, type PropsWithChildren } from "react"; import { HelpTooltipLink, HelpTooltipLinksGroup, @@ -11,41 +10,35 @@ import { docs } from "utils/docs"; import { CodeExample } from "../../CodeExample/CodeExample"; import { Stack } from "../../Stack/Stack"; import { SecondaryAgentButton } from "../AgentButton"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; export interface SSHButtonProps { workspaceName: string; agentName: string; - defaultIsOpen?: boolean; + isDefaultOpen?: boolean; sshPrefix?: string; } export const SSHButton: FC> = ({ workspaceName, agentName, - defaultIsOpen = false, + isDefaultOpen = false, sshPrefix, }) => { const theme = useTheme(); - const anchorRef = useRef(null); - const [isOpen, setIsOpen] = useState(defaultIsOpen); - const id = isOpen ? "schedule-popover" : undefined; - - const onClose = () => { - setIsOpen(false); - }; return ( - <> - { - setIsOpen(true); - }} - > - SSH - + + + SSH + - > = ({ margin-top: ${theme.spacing(0.25)}; `, }} - id={id} - open={isOpen} - anchorEl={anchorRef.current} - onClose={onClose} - anchorOrigin={{ - vertical: "bottom", - horizontal: "left", - }} - transformOrigin={{ - vertical: "top", - horizontal: "left", - }} > Run the following commands to connect with SSH: @@ -107,8 +88,8 @@ export const SSHButton: FC> = ({ SSH configuration - - + + ); }; diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx index 9e0194e98d..aa6d43bc9a 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx @@ -5,7 +5,6 @@ import "react-date-range/dist/styles.css"; import "react-date-range/dist/theme/default.css"; import Button from "@mui/material/Button"; import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined"; -import Popover from "@mui/material/Popover"; import { DateRangePicker, createStaticRanges } from "react-date-range"; import { addDays, @@ -16,6 +15,11 @@ import { startOfHour, subDays, } from "date-fns"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; // The type definition from @types is wrong declare module "react-date-range" { @@ -41,8 +45,6 @@ export const DateRange = ({ onChange: (value: DateRangeValue) => void; }) => { const selectionStatusRef = useRef<"idle" | "selecting">("idle"); - const anchorRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); const [ranges, setRanges] = useState([ { ...value, @@ -53,105 +55,91 @@ export const DateRange = ({ startDate: ranges[0].startDate as Date, endDate: ranges[0].endDate as Date, }; - const handleClose = () => { - const now = new Date(); - onChange({ - startDate: startOfDay(currentRange.startDate), - endDate: isToday(currentRange.endDate) - ? startOfHour(addHours(now, 1)) - : startOfDay(addDays(currentRange.endDate, 1)), - }); - setIsOpen(false); - }; return ( - <> - - - { - const range = item.selection; - setRanges([range]); + + {(popover) => ( + <> + + + + + { + const range = item.selection; + setRanges([range]); - // When it is the first selection, we don't want to close the popover - // We have to do that ourselves because the library doesn't provide a way to do it - if (selectionStatusRef.current === "idle") { - selectionStatusRef.current = "selecting"; - return; - } + // When it is the first selection, we don't want to close the popover + // We have to do that ourselves because the library doesn't provide a way to do it + if (selectionStatusRef.current === "idle") { + selectionStatusRef.current = "selecting"; + return; + } - selectionStatusRef.current = "idle"; - const startDate = range.startDate as Date; - const endDate = range.endDate as Date; - onChange({ - startDate, - endDate, - }); - setIsOpen(false); - }} - moveRangeOnFirstSelection={false} - months={2} - ranges={ranges} - maxDate={new Date()} - direction="horizontal" - staticRanges={createStaticRanges([ - { - label: "Today", - range: () => ({ - startDate: new Date(), - endDate: new Date(), - }), - }, - { - label: "Yesterday", - range: () => ({ - startDate: subDays(new Date(), 1), - endDate: subDays(new Date(), 1), - }), - }, - { - label: "Last 7 days", - range: () => ({ - startDate: subDays(new Date(), 6), - endDate: new Date(), - }), - }, - { - label: "Last 14 days", - range: () => ({ - startDate: subDays(new Date(), 13), - endDate: new Date(), - }), - }, - { - label: "Last 30 days", - range: () => ({ - startDate: subDays(new Date(), 29), - endDate: new Date(), - }), - }, - ])} - /> - - + selectionStatusRef.current = "idle"; + const startDate = range.startDate as Date; + const endDate = range.endDate as Date; + const now = new Date(); + onChange({ + startDate: startOfDay(startDate), + endDate: isToday(endDate) + ? startOfHour(addHours(now, 1)) + : startOfDay(addDays(endDate, 1)), + }); + popover.setIsOpen(false); + }} + moveRangeOnFirstSelection={false} + months={2} + ranges={ranges} + maxDate={new Date()} + direction="horizontal" + staticRanges={createStaticRanges([ + { + label: "Today", + range: () => ({ + startDate: new Date(), + endDate: new Date(), + }), + }, + { + label: "Yesterday", + range: () => ({ + startDate: subDays(new Date(), 1), + endDate: subDays(new Date(), 1), + }), + }, + { + label: "Last 7 days", + range: () => ({ + startDate: subDays(new Date(), 6), + endDate: new Date(), + }), + }, + { + label: "Last 14 days", + range: () => ({ + startDate: subDays(new Date(), 13), + endDate: new Date(), + }), + }, + { + label: "Last 30 days", + range: () => ({ + startDate: subDays(new Date(), 29), + endDate: new Date(), + }), + }, + ])} + /> + + + )} + ); }; diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index dfc52c8a91..1761cd3a98 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -20,7 +20,6 @@ import Box from "@mui/material/Box"; import { useDashboard } from "components/Dashboard/DashboardProvider"; import { Region } from "api/typesGenerated"; import { getLatencyColor } from "utils/latency"; -import Popover from "@mui/material/Popover"; import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"; import { portForwardURL } from "utils/portForward"; import { @@ -31,6 +30,11 @@ import { } from "./TerminalAlerts"; import { useQuery } from "react-query"; import { deploymentConfig } from "api/queries/deployment"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; export const Language = { workspaceErrorMessagePrefix: "Unable to fetch workspace: ", @@ -332,13 +336,11 @@ const TerminalPage: FC = () => { const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => { const theme = useTheme(); const color = getLatencyColor(theme, latency); - const anchorRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); return ( theme.spacing(1, 2), + padding: theme.spacing(0, 2), background: (theme) => theme.palette.background.paper, display: "flex", alignItems: "center", @@ -347,82 +349,80 @@ const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => { borderTop: (theme) => `1px solid ${theme.palette.divider}`, }} > - setIsOpen(true)} - onMouseLeave={() => setIsOpen(false)} - sx={{ - background: "none", - cursor: "pointer", - display: "flex", - alignItems: "center", - gap: 1, - border: 0, - }} - > - - - - setIsOpen(false)} - sx={{ - pointerEvents: "none", - "& .MuiPaper-root": { - padding: (theme) => theme.spacing(1, 2), - marginTop: -1, - }, - }} - anchorOrigin={{ - vertical: "top", - horizontal: "right", - }} - transformOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - > - theme.palette.text.secondary, - fontWeight: 500, - }} - > - Selected proxy - - - - - - - {proxy.display_name} + + + + + - - + + theme.spacing(1, 2), + }, + }} + anchorOrigin={{ + vertical: "top", + horizontal: "right", + }} + transformOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + > + theme.palette.text.secondary, + fontWeight: 500, + }} + > + Selected proxy + + + + + + + {proxy.display_name} + + + + ); diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx b/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx index 422faabb66..2d53044cfd 100644 --- a/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx +++ b/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx @@ -10,7 +10,7 @@ const meta: Meta = { title: "pages/UsersPage/EditRolesButton", component: EditRolesButton, args: { - defaultIsOpen: true, + isDefaultOpen: true, }, }; diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx index be5e38f930..980122ba5f 100644 --- a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx +++ b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx @@ -1,8 +1,7 @@ import IconButton from "@mui/material/IconButton"; import { EditSquare } from "components/Icons/EditSquare"; -import { useRef, useState, FC } from "react"; +import { FC } from "react"; import { makeStyles } from "@mui/styles"; -import Popover from "@mui/material/Popover"; import { Stack } from "components/Stack/Stack"; import Checkbox from "@mui/material/Checkbox"; import UserIcon from "@mui/icons-material/PersonOutline"; @@ -12,6 +11,11 @@ import { HelpTooltipText, HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; const roleDescriptions: Record = { owner: @@ -59,7 +63,7 @@ export interface EditRolesButtonProps { roles: Role[]; selectedRoles: Role[]; onChange: (roles: Role["name"][]) => void; - defaultIsOpen?: boolean; + isDefaultOpen?: boolean; oidcRoleSync: boolean; userLoginType: string; } @@ -69,14 +73,11 @@ export const EditRolesButton: FC = ({ selectedRoles, onChange, isLoading, - defaultIsOpen = false, + isDefaultOpen = false, userLoginType, oidcRoleSync, }) => { const styles = useStyles(); - const anchorRef = useRef(null); - const [isOpen, setIsOpen] = useState(defaultIsOpen); - const id = isOpen ? "edit-roles-popover" : undefined; const selectedRoleNames = selectedRoles.map((role) => role.name); const handleChange = (roleName: string) => { @@ -91,42 +92,30 @@ export const EditRolesButton: FC = ({ const canSetRoles = userLoginType !== "oidc" || (userLoginType === "oidc" && !oidcRoleSync); + if (!canSetRoles) { + return ( + + Externally controlled + + Roles for this user are controlled by the OIDC identity provider. + + + ); + } + return ( - <> - {canSetRoles ? ( + + setIsOpen(true)} > - ) : ( - - Externally controlled - - Roles for this user are controlled by the OIDC identity provider. - - - )} + - setIsOpen(false)} - anchorOrigin={{ - vertical: "bottom", - horizontal: "left", - }} - transformOrigin={{ - vertical: "top", - horizontal: "left", - }} - classes={{ paper: styles.popoverPaper }} - > +
= ({
- - + + ); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx index 0b106af089..333d7999e7 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx @@ -1,7 +1,6 @@ import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; -import Popover from "@mui/material/Popover"; import { useQuery } from "react-query"; import { getWorkspaceParameters } from "api/api"; import { @@ -19,10 +18,15 @@ import { HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; import { useFormik } from "formik"; -import { useRef, useState } from "react"; import { docs } from "utils/docs"; import { getFormHelpers } from "utils/formUtils"; import { getInitialRichParameterValues } from "utils/richParameters"; +import { + Popover, + PopoverContent, + PopoverTrigger, + usePopover, +} from "components/Popover/Popover"; export const BuildParametersPopover = ({ workspace, @@ -33,12 +37,43 @@ export const BuildParametersPopover = ({ disabled?: boolean; onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void; }) => { - const anchorRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); + return ( + + + + + ({ ".MuiPaper-root": { width: theme.spacing(38) } })} + > + + + + ); +}; + +const BuildParametersPopoverContent = ({ + workspace, + onSubmit, +}: { + workspace: Workspace; + onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void; +}) => { + const popover = usePopover(); const { data: parameters } = useQuery({ queryKey: ["workspace", workspace.id, "parameters"], queryFn: () => getWorkspaceParameters(workspace), - enabled: isOpen, + enabled: popover.isOpen, }); const ephemeralParameters = parameters ? parameters.templateVersionRichParameters.filter((p) => p.ephemeral) @@ -46,93 +81,56 @@ export const BuildParametersPopover = ({ return ( <> - - { - setIsOpen(false); - }} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", - }} - sx={{ - ".MuiPaper-root": { - width: (theme) => theme.spacing(38), - marginTop: 1, - }, - }} - > - - {parameters && parameters.buildParameters && ephemeralParameters ? ( - ephemeralParameters.length > 0 ? ( - <> - theme.palette.text.secondary, - p: 2.5, - borderBottom: (theme) => - `1px solid ${theme.palette.divider}`, - }} - > - Build Options - - These parameters only apply for a single workspace start. - - - -
{ - onSubmit(buildParameters); - setIsOpen(false); - }} - ephemeralParameters={ephemeralParameters} - buildParameters={parameters.buildParameters} - /> - - - ) : ( - theme.palette.text.secondary, - p: 2.5, - borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + {parameters && parameters.buildParameters && ephemeralParameters ? ( + ephemeralParameters.length > 0 ? ( + <> + theme.palette.text.secondary, + p: 2.5, + borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + }} + > + Build Options + + These parameters only apply for a single workspace start. + + + + { + onSubmit(buildParameters); + popover.setIsOpen(false); }} + ephemeralParameters={ephemeralParameters} + buildParameters={parameters.buildParameters} + /> + + + ) : ( + theme.palette.text.secondary, + p: 2.5, + borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + }} + > + Build Options + + This template has no ephemeral build options. + + + - Build Options - - This template has no ephemeral build options. - - - - Read the docs - - - - ) - ) : ( - - )} - - + Read the docs + + + + ) + ) : ( + + )} ); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx index c55f09d34f..773be0baa5 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.tsx @@ -1,6 +1,6 @@ import Link from "@mui/material/Link"; import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; -import { FC, useRef, useState } from "react"; +import { FC } from "react"; import { Link as RouterLink } from "react-router-dom"; import { createDayString } from "utils/createDayString"; import { @@ -16,11 +16,16 @@ import IconButton from "@mui/material/IconButton"; import RemoveIcon from "@mui/icons-material/RemoveOutlined"; import { makeStyles } from "@mui/styles"; import AddIcon from "@mui/icons-material/AddOutlined"; -import Popover from "@mui/material/Popover"; import TextField from "@mui/material/TextField"; import Button from "@mui/material/Button"; import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; import { DormantDeletionStat } from "components/WorkspaceDeletion"; +import { + Popover, + PopoverContent, + PopoverTrigger, + usePopover, +} from "components/Popover/Popover"; const Language = { workspaceDetails: "Workspace Details", @@ -62,10 +67,6 @@ export const WorkspaceStats: FC = ({ const styles = useStyles(); const deadlinePlusEnabled = maxDeadlineIncrease >= 1; const deadlineMinusEnabled = maxDeadlineDecrease >= 1; - const addButtonRef = useRef(null); - const subButtonRef = useRef(null); - const [isAddingTime, setIsAddingTime] = useState(false); - const [isSubTime, setIsSubTime] = useState(false); return ( <> @@ -138,26 +139,50 @@ export const WorkspaceStats: FC = ({ {canUpdateWorkspace && canEditDeadline(workspace) && ( - setIsSubTime(true)} - > - - - setIsAddingTime(true)} - > - - + + + + + + + + + + + + + + + + + + + + )} @@ -174,118 +199,106 @@ export const WorkspaceStats: FC = ({ /> )} + + ); +}; - setIsAddingTime(false)} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", +const AddTimeContent = (props: { + maxDeadlineIncrease: number; + onDeadlinePlus: (value: number) => void; +}) => { + const styles = useStyles(); + const popover = usePopover(); + + return ( + <> + Add hours to deadline + + Delay the shutdown of this workspace for a few more hours. This is only + applied once. + + { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const hours = Number(formData.get("hours")); + props.onDeadlinePlus(hours); + popover.setIsOpen(false); }} > - Add hours to deadline - - Delay the shutdown of this workspace for a few more hours. This is - only applied once. - - { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const hours = Number(formData.get("hours")); - onDeadlinePlus(hours); - setIsAddingTime(false); + - + inputProps={{ + min: 0, + max: props.maxDeadlineIncrease, + step: 1, + defaultValue: 1, + }} + /> - - - + + + + ); +}; - setIsSubTime(false)} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", +export const DecreaseTimeContent = (props: { + onDeadlineMinus: (hours: number) => void; + maxDeadlineDecrease: number; +}) => { + const styles = useStyles(); + const popover = usePopover(); + + return ( + <> + + Subtract hours to deadline + + + Anticipate the shutdown of this workspace for a few more hours. This is + only applied once. + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const hours = Number(formData.get("hours")); + props.onDeadlineMinus(hours); + popover.setIsOpen(false); }} > - - Subtract hours to deadline - - - Anticipate the shutdown of this workspace for a few more hours. This - is only applied once. - - { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const hours = Number(formData.get("hours")); - onDeadlineMinus(hours); - setIsSubTime(false); + - + inputProps={{ + min: 0, + max: props.maxDeadlineDecrease, + step: 1, + defaultValue: 1, + }} + /> - - -
+ + ); }; diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index dfb6e05be3..2e3d656360 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,9 +1,4 @@ -import { - type PropsWithChildren, - type ReactNode, - useState, - useRef, -} from "react"; +import { type PropsWithChildren, type ReactNode, useState } from "react"; import { type Template } from "api/typesGenerated"; import { type UseQueryResult } from "react-query"; import { @@ -20,7 +15,11 @@ import { OverflowY } from "components/OverflowY/OverflowY"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Avatar } from "components/Avatar/Avatar"; import { SearchBox } from "./WorkspacesSearchBox"; -import Popover from "@mui/material/Popover"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; const ICON_SIZE = 18; const COLUMN_GAP = 1.5; @@ -42,9 +41,6 @@ export function WorkspacesButton({ const [searchTerm, setSearchTerm] = useState(""); const processed = sortTemplatesByUsersDesc(templates ?? [], searchTerm); - const anchorRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - let emptyState: ReactNode = undefined; if (templates?.length === 0) { emptyState = ( @@ -62,37 +58,13 @@ export function WorkspacesButton({ } return ( - <> - - setIsOpen(false)} - anchorEl={anchorRef.current} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", - }} - css={(theme) => ({ - marginTop: theme.spacing(1), - "& .MuiPaper-root": { - width: theme.spacing(40), - }, - })} - > + + + + + setSearchTerm(newValue)} @@ -142,8 +114,8 @@ export function WorkspacesButton({ See all templates
-
- + + ); }