diff --git a/site/src/components/DropdownButton/ActionCtas.tsx b/site/src/components/DropdownButton/ActionCtas.tsx deleted file mode 100644 index 3b072bb691986..0000000000000 --- a/site/src/components/DropdownButton/ActionCtas.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import Tooltip from "@material-ui/core/Tooltip" -import Button from "@material-ui/core/Button" -import { makeStyles } from "@material-ui/core/styles" -import BlockIcon from "@material-ui/icons/Block" -import CloudQueueIcon from "@material-ui/icons/CloudQueue" -import SettingsOutlined from "@material-ui/icons/SettingsOutlined" -import HistoryOutlined from "@material-ui/icons/HistoryOutlined" -import CropSquareIcon from "@material-ui/icons/CropSquare" -import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline" -import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" -import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { FC } from "react" -import { useTranslation } from "react-i18next" -import { combineClasses } from "utils/combineClasses" -import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton" - -interface WorkspaceAction { - handleAction: () => void -} - -export const UpdateButton: FC> = ({ - handleAction, -}) => { - const styles = useStyles() - const { t } = useTranslation("workspacePage") - - return ( - - ) -} - -export const SettingsButton: FC> = ({ - handleAction, -}) => { - const styles = useStyles() - const { t } = useTranslation("workspacePage") - - return ( - - ) -} - -export const ChangeVersionButton: FC< - React.PropsWithChildren -> = ({ handleAction }) => { - const styles = useStyles() - - return ( - - ) -} - -export const StartButton: FC> = ({ - handleAction, -}) => { - const styles = useStyles() - const { t } = useTranslation("workspacePage") - - return ( - } - onClick={handleAction} - label={t("actionButton.start")} - /> - ) -} - -export const StopButton: FC> = ({ - handleAction, -}) => { - const styles = useStyles() - const { t } = useTranslation("workspacePage") - - return ( - } - onClick={handleAction} - label={t("actionButton.stop")} - /> - ) -} - -export const DeleteButton: FC> = ({ - handleAction, -}) => { - const styles = useStyles() - const { t } = useTranslation("workspacePage") - - return ( - } - onClick={handleAction} - label={t("actionButton.delete")} - /> - ) -} - -export const CancelButton: FC> = ({ - handleAction, -}) => { - const styles = useStyles() - - // this is an icon button, so it's important to include an aria label - return ( -
- - {/* We had to wrap the button to make it work with the tooltip. */} -
- } - onClick={handleAction} - className={styles.cancelButton} - ariaLabel="cancel action" - /> -
-
-
- ) -} - -interface DisabledProps { - label: string -} - -export const DisabledButton: FC> = ({ - label, -}) => { - const styles = useStyles() - - return ( - - ) -} - -interface LoadingProps { - label: string -} - -export const ActionLoadingButton: FC> = ({ - label, -}) => { - const styles = useStyles() - return ( - - ) -} - -const useStyles = makeStyles((theme) => ({ - actionButton: { - // Set fixed width for the action buttons so they will not change the size - // during the transitions - width: theme.spacing(20), - borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`, - // This is used to show the hover effect - marginRight: -1, - position: "relative", - "&:hover": { - zIndex: 1, - }, - }, - cancelButton: { - "&.MuiButton-root": { - padding: "0px 0px !important", - borderLeft: `1px solid ${theme.palette.divider}`, - borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`, - width: "63px", // matching dropdown button so button grouping doesn't grow in size - }, - "& .MuiButton-label": { - marginLeft: "10px", - }, - }, - // this is all custom to work with our button wrapper - loadingButton: { - border: "none", - borderRadius: `${theme.shape.borderRadius} 0px 0px ${theme.shape.borderRadius}`, - }, -})) diff --git a/site/src/components/DropdownButton/DropdownButton.stories.tsx b/site/src/components/DropdownButton/DropdownButton.stories.tsx deleted file mode 100644 index ab2815a236dbc..0000000000000 --- a/site/src/components/DropdownButton/DropdownButton.stories.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { action } from "@storybook/addon-actions" -import { Story } from "@storybook/react" -import { - DeleteButton, - DisabledButton, - StartButton, - UpdateButton, -} from "./ActionCtas" -import { DropdownButton, DropdownButtonProps } from "./DropdownButton" - -export default { - title: "Components/DropdownButton", - component: DropdownButton, -} - -const Template: Story = (args) => ( - -) - -export const WithDropdown = Template.bind({}) -WithDropdown.args = { - primaryAction: , - secondaryActions: [ - { - action: "update", - button: , - }, - { - action: "delete", - button: , - }, - ], - canCancel: false, -} - -export const WithCancel = Template.bind({}) -WithCancel.args = { - primaryAction: , - secondaryActions: [], - canCancel: true, - handleCancel: action("cancel"), -} diff --git a/site/src/components/DropdownButton/DropdownButton.tsx b/site/src/components/DropdownButton/DropdownButton.tsx deleted file mode 100644 index c7788941e6851..0000000000000 --- a/site/src/components/DropdownButton/DropdownButton.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import Button from "@material-ui/core/Button" -import Popover from "@material-ui/core/Popover" -import { makeStyles, useTheme } from "@material-ui/core/styles" -import { - CloseDropdown, - OpenDropdown, -} from "components/DropdownArrows/DropdownArrows" -import { DropdownContent } from "components/DropdownButton/DropdownContent/DropdownContent" -import { FC, ReactNode, useRef, useState } from "react" -import { CancelButton } from "./ActionCtas" - -export interface DropdownButtonProps { - primaryAction: ReactNode - secondaryActions: Array<{ action: string; button: ReactNode }> - canCancel: boolean - handleCancel?: () => void -} - -export const DropdownButton: FC = ({ - primaryAction, - secondaryActions, - canCancel, - handleCancel, -}) => { - const styles = useStyles() - const theme = useTheme() - const anchorRef = useRef(null) - const [isOpen, setIsOpen] = useState(false) - const id = isOpen ? "action-popover" : undefined - const canOpen = secondaryActions.length > 0 - - return ( - - {/* primary workspace CTA */} - - {primaryAction} - - {canCancel && handleCancel ? ( - - ) : ( - <> - {/* popover toggle button */} - - setIsOpen(false)} - onBlur={() => setIsOpen(false)} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", - }} - > - {/* secondary workspace CTAs */} - - - - )} - - ) -} - -const useStyles = makeStyles((theme) => ({ - buttonContainer: { - display: "inline-flex", - }, - dropdownButton: { - borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`, - minWidth: "unset", - width: "64px", // matching cancel button so button grouping doesn't grow in size - "& .MuiButton-label": { - marginRight: "8px", - }, - }, - primaryCta: { - [theme.breakpoints.down("sm")]: { - width: "100%", - - "& > *": { - width: "100%", - }, - }, - }, - popoverPaper: { - padding: 0, - width: theme.spacing(28), - - "& .MuiButton-root": { - padding: theme.spacing(1, 2), - borderRadius: 0, - width: "100%", - border: 0, - - "&:hover": { - background: theme.palette.action.hover, - }, - }, - }, -})) diff --git a/site/src/components/DropdownButton/DropdownContent/DropdownContent.tsx b/site/src/components/DropdownButton/DropdownContent/DropdownContent.tsx deleted file mode 100644 index 3ed91531e795f..0000000000000 --- a/site/src/components/DropdownButton/DropdownContent/DropdownContent.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { makeStyles } from "@material-ui/core/styles" -import { FC, ReactNode } from "react" - -export interface DropdownContentProps { - secondaryActions: Array<{ action: string; button: ReactNode }> -} - -/* secondary workspace CTAs */ -export const DropdownContent: FC< - React.PropsWithChildren -> = ({ secondaryActions }) => { - const styles = useStyles() - - return ( - - {secondaryActions.map(({ action, button }) => ( -
- {button} -
- ))} -
- ) -} - -const useStyles = makeStyles(() => ({ - popoverActionButton: { - "& .MuiButtonBase-root": { - backgroundColor: "unset", - justifyContent: "start", - padding: "0px", - }, - }, -})) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index d1b78341971c5..115b341e71617 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -15,8 +15,11 @@ import { } from "api/api" import { useQuery } from "@tanstack/react-query" import { useDashboard } from "components/Dashboard/DashboardProvider" +import { AuthorizationRequest } from "api/typesGenerated" -const templatePermissions = (templateId: string) => ({ +const templatePermissions = ( + templateId: string, +): AuthorizationRequest["checks"] => ({ canUpdateTemplate: { object: { resource_type: "template", diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index 636d3dcb9db77..1776377339a95 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -14,22 +14,17 @@ import { PageHeaderSubtitle, } from "components/PageHeader/PageHeader" import { Stack } from "components/Stack/Stack" -import { FC, useState } from "react" -import { Link as RouterLink } from "react-router-dom" +import { FC, useRef, useState } from "react" +import { Link as RouterLink, useNavigate } from "react-router-dom" import { useDeleteTemplate } from "./deleteTemplate" import { Margins } from "components/Margins/Margins" import MoreVertOutlined from "@material-ui/icons/MoreVertOutlined" import Menu from "@material-ui/core/Menu" import MenuItem from "@material-ui/core/MenuItem" - -const Language = { - variablesButton: "Variables", - settingsButton: "Settings", - createButton: "Create workspace", - deleteButton: "Delete", - editFilesButton: "Edit files", - duplicateButton: "Duplicate", -} +import SettingsOutlined from "@material-ui/icons/SettingsOutlined" +import DeleteOutlined from "@material-ui/icons/DeleteOutlined" +import EditOutlined from "@material-ui/icons/EditOutlined" +import FileCopyOutlined from "@material-ui/icons/FileCopyOutlined" const TemplateMenu: FC<{ templateName: string @@ -37,10 +32,15 @@ const TemplateMenu: FC<{ canEditFiles: boolean onDelete: () => void }> = ({ templateName, templateVersion, canEditFiles, onDelete }) => { - const [anchorEl, setAnchorEl] = useState(null) + const menuTriggerRef = useRef(null) + const [isMenuOpen, setIsMenuOpen] = useState(false) + const navigate = useNavigate() - const handleClose = () => { - setAnchorEl(null) + // Returns a function that will execute the action and close the menu + const onMenuItemClick = (actionFn: () => void) => () => { + setIsMenuOpen(false) + + actionFn() } return ( @@ -49,50 +49,51 @@ const TemplateMenu: FC<{ variant="outlined" aria-controls="template-options" aria-haspopup="true" - onClick={(e) => setAnchorEl(e.currentTarget)} + onClick={() => setIsMenuOpen(true)} + ref={menuTriggerRef} > setIsMenuOpen(false)} > + navigate(`/templates/${templateName}/settings`), + )} > - {Language.settingsButton} + + Settings {canEditFiles && ( + navigate(`/templates/new?fromTemplate=${templateName}`), + )} > - {Language.duplicateButton} + + Duplicate )} {canEditFiles && ( + navigate( + `/templates/${templateName}/versions/${templateVersion}/edit`, + ), + )} > - {Language.editFilesButton} + + Edit files )} - { - onDelete() - handleClose() - }} - > - {Language.deleteButton} + + + Delete @@ -108,7 +109,7 @@ const CreateWorkspaceButton: FC<{ component={RouterLink} to={`/templates/${templateName}/workspace`} > - {Language.createButton} + Create workspace ) diff --git a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx deleted file mode 100644 index 6a8045d0efda9..0000000000000 --- a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded" -import { ComponentMeta, Story } from "@storybook/react" -import { - WorkspaceActionButton, - WorkspaceActionButtonProps, -} from "./WorkspaceActionButton" - -export default { - title: "components/WorkspaceActionButton", - component: WorkspaceActionButton, -} as ComponentMeta - -const Template: Story = (args) => ( - -) - -export const Example = Template.bind({}) -Example.args = { - icon: , - label: "Start workspace", -} diff --git a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx deleted file mode 100644 index 594cd23b75adf..0000000000000 --- a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import Button from "@material-ui/core/Button" -import { FC } from "react" - -export interface WorkspaceActionButtonProps { - label?: string - icon: JSX.Element - onClick: () => void - className?: string - ariaLabel?: string -} - -export const WorkspaceActionButton: FC< - React.PropsWithChildren -> = ({ label, icon, onClick, className, ariaLabel }) => { - return ( - - ) -} diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx new file mode 100644 index 0000000000000..b8c38469df68f --- /dev/null +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -0,0 +1,116 @@ +import Button from "@material-ui/core/Button" +import BlockIcon from "@material-ui/icons/Block" +import CloudQueueIcon from "@material-ui/icons/CloudQueue" +import CropSquareIcon from "@material-ui/icons/CropSquare" +import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" +import { LoadingButton } from "components/LoadingButton/LoadingButton" +import { FC } from "react" +import { useTranslation } from "react-i18next" +import { makeStyles } from "@material-ui/core/styles" + +interface WorkspaceAction { + handleAction: () => void +} + +export const UpdateButton: FC> = ({ + handleAction, +}) => { + const { t } = useTranslation("workspacePage") + const styles = useStyles() + + return ( + + ) +} + +export const StartButton: FC> = ({ + handleAction, +}) => { + const { t } = useTranslation("workspacePage") + const styles = useStyles() + + return ( + + ) +} + +export const StopButton: FC> = ({ + handleAction, +}) => { + const { t } = useTranslation("workspacePage") + const styles = useStyles() + + return ( + + ) +} + +export const CancelButton: FC> = ({ + handleAction, +}) => { + return ( + + ) +} + +interface DisabledProps { + label: string +} + +export const DisabledButton: FC> = ({ + label, +}) => { + return ( + + ) +} + +interface LoadingProps { + label: string +} + +export const ActionLoadingButton: FC> = ({ + label, +}) => { + const styles = useStyles() + return ( + + ) +} + +const useStyles = makeStyles((theme) => ({ + fixedWidth: { + // Make it fixed so the loading changes will not "flick" the UI + width: theme.spacing(16), + }, +})) diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx deleted file mode 100644 index 98c8c3e7fd1af..0000000000000 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { fireEvent, screen } from "@testing-library/react" -import i18next from "i18next" -import * as Mocks from "../../testHelpers/entities" -import { render } from "../../testHelpers/renderHelpers" -import { WorkspaceActions, WorkspaceActionsProps } from "./WorkspaceActions" - -const { t } = i18next - -const renderComponent = async (props: Partial = {}) => { - render( - , - ) -} - -const renderAndClick = async (props: Partial = {}) => { - render( - , - ) - const trigger = await screen.findByTestId("workspace-actions-button") - fireEvent.click(trigger) -} - -describe("WorkspaceActions", () => { - describe("when the workspace is starting", () => { - it("primary is starting; cancel is available; no secondary", async () => { - await renderComponent({ - workspaceStatus: Mocks.MockStartingWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("actionButton.starting", { ns: "workspacePage" }), - ) - expect( - screen.getByRole("button", { - name: "cancel action", - }), - ).toBeInTheDocument() - expect(screen.queryByTestId("secondary-ctas")).toBeNull() - }) - }) - describe("when the workspace is started", () => { - it("primary is stop; secondary is delete", async () => { - await renderAndClick({ - workspaceStatus: Mocks.MockWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("actionButton.stop", { ns: "workspacePage" }), - ) - expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( - t("actionButton.delete", { ns: "workspacePage" }), - ) - }) - }) - describe("when the workspace is started", () => { - it("primary is stop; secondary is delete", async () => { - await renderAndClick({ - workspaceStatus: Mocks.MockWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("actionButton.stop", { ns: "workspacePage" }), - ) - expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( - t("actionButton.delete", { ns: "workspacePage" }), - ) - }) - }) - describe("when the workspace is stopping", () => { - it("primary is stopping; cancel is available; no secondary", async () => { - await renderComponent({ - workspaceStatus: Mocks.MockStoppingWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("actionButton.stopping", { ns: "workspacePage" }), - ) - expect( - screen.getByRole("button", { - name: "cancel action", - }), - ).toBeInTheDocument() - expect(screen.queryByTestId("secondary-ctas")).toBeNull() - }) - }) - describe("when the workspace is canceling", () => { - it("primary is canceling; no secondary", async () => { - await renderAndClick({ - workspaceStatus: Mocks.MockCancelingWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("disabledButton.canceling", { ns: "workspacePage" }), - ) - expect(screen.queryByTestId("secondary-ctas")).toBeNull() - }) - }) - describe("when the workspace is canceled", () => { - it("primary is start; secondary are stop, delete", async () => { - await renderAndClick({ - workspaceStatus: Mocks.MockCanceledWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("actionButton.start", { ns: "workspacePage" }), - ) - expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( - t("actionButton.stop", { ns: "workspacePage" }), - ) - expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( - t("actionButton.delete", { ns: "workspacePage" }), - ) - }) - }) - describe("when the workspace is errored", () => { - it("primary is start; secondary is delete", async () => { - await renderAndClick({ - workspaceStatus: Mocks.MockFailedWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("actionButton.start", { ns: "workspacePage" }), - ) - expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( - t("actionButton.delete", { ns: "workspacePage" }), - ) - }) - }) - describe("when the workspace is deleting", () => { - it("primary is deleting; cancel is available; no secondary", async () => { - await renderComponent({ - workspaceStatus: Mocks.MockDeletingWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("actionButton.deleting", { ns: "workspacePage" }), - ) - expect( - screen.getByRole("button", { - name: "cancel action", - }), - ).toBeInTheDocument() - expect(screen.queryByTestId("secondary-ctas")).toBeNull() - }) - }) - describe("when the workspace is deleted", () => { - it("primary is deleted; no secondary", async () => { - await renderAndClick({ - workspaceStatus: Mocks.MockDeletedWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("disabledButton.deleted", { ns: "workspacePage" }), - ) - expect(screen.queryByTestId("secondary-ctas")).toBeNull() - }) - }) - describe("when the workspace is outdated", () => { - it("primary is update; secondary are start, delete", async () => { - await renderAndClick({ - isOutdated: true, - workspaceStatus: Mocks.MockOutdatedWorkspace.latest_build.status, - }) - expect(screen.getByTestId("primary-cta")).toHaveTextContent( - t("actionButton.update", { ns: "workspacePage" }), - ) - expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( - t("actionButton.start", { ns: "workspacePage" }), - ) - expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( - t("actionButton.delete", { ns: "workspacePage" }), - ) - }) - }) -}) diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 7a1c4e3aba14d..d7508ed64405b 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -1,18 +1,27 @@ -import { DropdownButton } from "components/DropdownButton/DropdownButton" -import { FC, ReactNode, useMemo } from "react" +import MenuItem from "@material-ui/core/MenuItem" +import Button from "@material-ui/core/Button" +import Menu from "@material-ui/core/Menu" +import { makeStyles } from "@material-ui/core/styles" +import MoreVertOutlined from "@material-ui/icons/MoreVertOutlined" +import { FC, ReactNode, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { WorkspaceStatus } from "../../api/typesGenerated" import { ActionLoadingButton, - ChangeVersionButton, - DeleteButton, + CancelButton, DisabledButton, - SettingsButton, StartButton, StopButton, UpdateButton, -} from "../DropdownButton/ActionCtas" -import { ButtonMapping, ButtonTypesEnum, buttonAbilities } from "./constants" +} from "./Buttons" +import { + ButtonMapping, + ButtonTypesEnum, + actionsByWorkspaceStatus, +} from "./constants" +import SettingsOutlined from "@material-ui/icons/SettingsOutlined" +import HistoryOutlined from "@material-ui/icons/HistoryOutlined" +import DeleteOutlined from "@material-ui/icons/DeleteOutlined" export interface WorkspaceActionsProps { workspaceStatus: WorkspaceStatus @@ -42,67 +51,128 @@ export const WorkspaceActions: FC = ({ isUpdating, canChangeVersions, }) => { + const styles = useStyles() const { t } = useTranslation("workspacePage") - const { canCancel, canAcceptJobs, actions } = buttonAbilities(workspaceStatus) + const { + canCancel, + canAcceptJobs, + actions: actionsByStatus, + } = actionsByWorkspaceStatus(workspaceStatus) const canBeUpdated = isOutdated && canAcceptJobs + const menuTriggerRef = useRef(null) + const [isMenuOpen, setIsMenuOpen] = useState(false) // A mapping of button type to the corresponding React component const buttonMapping: ButtonMapping = { - [ButtonTypesEnum.update]: , - [ButtonTypesEnum.updating]: ( - + [ButtonTypesEnum.update]: ( + ), - [ButtonTypesEnum.settings]: ( - + [ButtonTypesEnum.updating]: ( + ), - [ButtonTypesEnum.changeVersion]: canChangeVersions ? ( - - ) : ( - <> + [ButtonTypesEnum.start]: ( + ), - [ButtonTypesEnum.start]: , [ButtonTypesEnum.starting]: ( - + + ), + [ButtonTypesEnum.stop]: ( + ), - [ButtonTypesEnum.stop]: , [ButtonTypesEnum.stopping]: ( - + ), - [ButtonTypesEnum.delete]: , [ButtonTypesEnum.deleting]: ( - + ), [ButtonTypesEnum.canceling]: ( - + ), [ButtonTypesEnum.deleted]: ( - + ), [ButtonTypesEnum.pending]: ( - + ), } - // memoize so this isn't recalculated every time we fetch the workspace - const [primaryAction, ...secondaryActions] = useMemo( - () => - isUpdating - ? [ButtonTypesEnum.updating, ...actions] - : canBeUpdated - ? [ButtonTypesEnum.update, ...actions] - : actions, - [actions, canBeUpdated, isUpdating], - ) + // Returns a function that will execute the action and close the menu + const onMenuItemClick = (actionFn: () => void) => () => { + setIsMenuOpen(false) + actionFn() + } return ( - ({ - action, - button: buttonMapping[action], - }))} - /> +
+ {canBeUpdated && + (isUpdating + ? buttonMapping[ButtonTypesEnum.updating] + : buttonMapping[ButtonTypesEnum.update])} + {actionsByStatus.map((action) => buttonMapping[action])} + {canCancel && } +
+ + setIsMenuOpen(false)} + > + + + Settings + + {canChangeVersions && ( + + + Change version + + )} + + + Delete + + +
+
) } + +const useStyles = makeStyles((theme) => ({ + actions: { + display: "flex", + alignItems: "center", + gap: theme.spacing(2), + }, +})) diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index ada352871f2c3..18923c3f38295 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -7,12 +7,9 @@ export enum ButtonTypesEnum { starting = "starting", stop = "stop", stopping = "stopping", - delete = "delete", deleting = "deleting", update = "update", updating = "updating", - settings = "settings", - changeVersion = "changeVersion", // disabled buttons canceling = "canceling", deleted = "deleted", @@ -29,25 +26,20 @@ interface WorkspaceAbilities { canAcceptJobs: boolean } -export const buttonAbilities = ( +export const actionsByWorkspaceStatus = ( status: WorkspaceStatus, ): WorkspaceAbilities => { - return statusToAbilities[status] + return statusToActions[status] } -const statusToAbilities: Record = { +const statusToActions: Record = { starting: { actions: [ButtonTypesEnum.starting], canCancel: true, canAcceptJobs: false, }, running: { - actions: [ - ButtonTypesEnum.stop, - ButtonTypesEnum.settings, - ButtonTypesEnum.changeVersion, - ButtonTypesEnum.delete, - ], + actions: [ButtonTypesEnum.stop], canCancel: false, canAcceptJobs: true, }, @@ -57,35 +49,18 @@ const statusToAbilities: Record = { canAcceptJobs: false, }, stopped: { - actions: [ - ButtonTypesEnum.start, - ButtonTypesEnum.settings, - ButtonTypesEnum.changeVersion, - ButtonTypesEnum.delete, - ], + actions: [ButtonTypesEnum.start], canCancel: false, canAcceptJobs: true, }, canceled: { - actions: [ - ButtonTypesEnum.start, - ButtonTypesEnum.stop, - ButtonTypesEnum.settings, - ButtonTypesEnum.changeVersion, - ButtonTypesEnum.delete, - ], + actions: [ButtonTypesEnum.start, ButtonTypesEnum.stop], canCancel: false, canAcceptJobs: true, }, // in the case of an error failed: { - actions: [ - ButtonTypesEnum.start, - ButtonTypesEnum.stop, - ButtonTypesEnum.settings, - ButtonTypesEnum.changeVersion, - ButtonTypesEnum.delete, - ], + actions: [ButtonTypesEnum.start, ButtonTypesEnum.stop], canCancel: false, canAcceptJobs: true, }, @@ -105,7 +80,7 @@ const statusToAbilities: Record = { deleted: { actions: [ButtonTypesEnum.deleted], canCancel: false, - canAcceptJobs: true, + canAcceptJobs: false, }, pending: { actions: [ButtonTypesEnum.pending], diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 68335cbabd9bc..ac0383ce34b4d 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -98,7 +98,7 @@ describe("WorkspacePage", () => { await renderWorkspacePage() // open the workspace action popover so we have access to all available ctas - const trigger = screen.getByTestId("workspace-actions-button") + const trigger = screen.getByTestId("workspace-options-button") await user.click(trigger) const buttonText = t("actionButton.delete", { ns: "workspacePage" }) @@ -168,7 +168,7 @@ describe("WorkspacePage", () => { const workspaceActions = screen.getByTestId("workspace-actions") const cancelButton = within(workspaceActions).getByRole("button", { - name: "cancel action", + name: "Cancel", }) await userEvent.setup().click(cancelButton) diff --git a/site/src/theme/overrides.ts b/site/src/theme/overrides.ts index e9ded0bf46218..a6484d2d34371 100644 --- a/site/src/theme/overrides.ts +++ b/site/src/theme/overrides.ts @@ -242,6 +242,15 @@ export const getOverrides = ({ minWidth: 120, }, }, + MuiMenuItem: { + root: { + gap: 12, + + "& .MuiSvgIcon-root": { + fontSize: 20, + }, + }, + }, MuiSnackbar: { anchorOriginBottomRight: { bottom: `${24 + 36}px !important`, // 36 is the bottom bar height