mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
refactor(site): Refactor workspace actions (#7124)
This commit is contained in:
@@ -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<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
className={styles.actionButton}
|
||||
startIcon={<CloudQueueIcon />}
|
||||
onClick={handleAction}
|
||||
>
|
||||
{t("actionButton.update")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingsButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
className={styles.actionButton}
|
||||
startIcon={<SettingsOutlined />}
|
||||
onClick={handleAction}
|
||||
>
|
||||
{t("actionButton.settings")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChangeVersionButton: FC<
|
||||
React.PropsWithChildren<WorkspaceAction>
|
||||
> = ({ handleAction }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
className={styles.actionButton}
|
||||
startIcon={<HistoryOutlined />}
|
||||
onClick={handleAction}
|
||||
>
|
||||
Change version
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<WorkspaceActionButton
|
||||
className={styles.actionButton}
|
||||
icon={<PlayCircleOutlineIcon />}
|
||||
onClick={handleAction}
|
||||
label={t("actionButton.start")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const StopButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<WorkspaceActionButton
|
||||
className={styles.actionButton}
|
||||
icon={<CropSquareIcon />}
|
||||
onClick={handleAction}
|
||||
label={t("actionButton.stop")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const DeleteButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<WorkspaceActionButton
|
||||
className={styles.actionButton}
|
||||
icon={<DeleteOutlineIcon />}
|
||||
onClick={handleAction}
|
||||
label={t("actionButton.delete")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const CancelButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
// this is an icon button, so it's important to include an aria label
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title="Cancel action">
|
||||
{/* We had to wrap the button to make it work with the tooltip. */}
|
||||
<div>
|
||||
<WorkspaceActionButton
|
||||
icon={<BlockIcon />}
|
||||
onClick={handleAction}
|
||||
className={styles.cancelButton}
|
||||
ariaLabel="cancel action"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface DisabledProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({
|
||||
label,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Button variant="outlined" disabled className={styles.actionButton}>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
interface LoadingProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const ActionLoadingButton: FC<React.PropsWithChildren<LoadingProps>> = ({
|
||||
label,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<LoadingButton
|
||||
loading
|
||||
variant="outlined"
|
||||
loadingLabel={label}
|
||||
className={combineClasses([styles.loadingButton, styles.actionButton])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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}`,
|
||||
},
|
||||
}))
|
||||
@@ -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<DropdownButtonProps> = (args) => (
|
||||
<DropdownButton {...args} />
|
||||
)
|
||||
|
||||
export const WithDropdown = Template.bind({})
|
||||
WithDropdown.args = {
|
||||
primaryAction: <StartButton handleAction={action("start")} />,
|
||||
secondaryActions: [
|
||||
{
|
||||
action: "update",
|
||||
button: <UpdateButton handleAction={action("update")} />,
|
||||
},
|
||||
{
|
||||
action: "delete",
|
||||
button: <DeleteButton handleAction={action("delete")} />,
|
||||
},
|
||||
],
|
||||
canCancel: false,
|
||||
}
|
||||
|
||||
export const WithCancel = Template.bind({})
|
||||
WithCancel.args = {
|
||||
primaryAction: <DisabledButton label="deleting" />,
|
||||
secondaryActions: [],
|
||||
canCancel: true,
|
||||
handleCancel: action("cancel"),
|
||||
}
|
||||
@@ -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<DropdownButtonProps> = ({
|
||||
primaryAction,
|
||||
secondaryActions,
|
||||
canCancel,
|
||||
handleCancel,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const theme = useTheme()
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const id = isOpen ? "action-popover" : undefined
|
||||
const canOpen = secondaryActions.length > 0
|
||||
|
||||
return (
|
||||
<span className={styles.buttonContainer} data-testid="workspace-actions">
|
||||
{/* primary workspace CTA */}
|
||||
<span data-testid="primary-cta" className={styles.primaryCta}>
|
||||
{primaryAction}
|
||||
</span>
|
||||
{canCancel && handleCancel ? (
|
||||
<CancelButton handleAction={handleCancel} />
|
||||
) : (
|
||||
<>
|
||||
{/* popover toggle button */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
data-testid="workspace-actions-button"
|
||||
aria-controls="workspace-actions-menu"
|
||||
aria-haspopup="true"
|
||||
className={styles.dropdownButton}
|
||||
ref={anchorRef}
|
||||
disabled={!canOpen}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
}}
|
||||
>
|
||||
{isOpen ? (
|
||||
<CloseDropdown />
|
||||
) : (
|
||||
<OpenDropdown
|
||||
color={canOpen ? undefined : theme.palette.action.disabled}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
<Popover
|
||||
classes={{ paper: styles.popoverPaper }}
|
||||
id={id}
|
||||
open={isOpen}
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={() => setIsOpen(false)}
|
||||
onBlur={() => setIsOpen(false)}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
{/* secondary workspace CTAs */}
|
||||
<DropdownContent secondaryActions={secondaryActions} />
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -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<DropdownContentProps>
|
||||
> = ({ secondaryActions }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<span data-testid="secondary-ctas">
|
||||
{secondaryActions.map(({ action, button }) => (
|
||||
<div key={action} className={styles.popoverActionButton}>
|
||||
{button}
|
||||
</div>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
popoverActionButton: {
|
||||
"& .MuiButtonBase-root": {
|
||||
backgroundColor: "unset",
|
||||
justifyContent: "start",
|
||||
padding: "0px",
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -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",
|
||||
|
||||
@@ -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<HTMLButtonElement | null>(null)
|
||||
const menuTriggerRef = useRef<HTMLButtonElement>(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}
|
||||
>
|
||||
<MoreVertOutlined />
|
||||
</Button>
|
||||
|
||||
<Menu
|
||||
id="template-options"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
anchorEl={menuTriggerRef.current}
|
||||
open={isMenuOpen}
|
||||
onClose={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={handleClose}
|
||||
component={RouterLink}
|
||||
to={`/templates/${templateName}/settings`}
|
||||
onClick={onMenuItemClick(() =>
|
||||
navigate(`/templates/${templateName}/settings`),
|
||||
)}
|
||||
>
|
||||
{Language.settingsButton}
|
||||
<SettingsOutlined />
|
||||
Settings
|
||||
</MenuItem>
|
||||
{canEditFiles && (
|
||||
<MenuItem
|
||||
onClick={handleClose}
|
||||
component={RouterLink}
|
||||
to={`/templates/new?fromTemplate=${templateName}`}
|
||||
onClick={onMenuItemClick(() =>
|
||||
navigate(`/templates/new?fromTemplate=${templateName}`),
|
||||
)}
|
||||
>
|
||||
{Language.duplicateButton}
|
||||
<FileCopyOutlined />
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
)}
|
||||
{canEditFiles && (
|
||||
<MenuItem
|
||||
component={RouterLink}
|
||||
to={`/templates/${templateName}/versions/${templateVersion}/edit`}
|
||||
onClick={handleClose}
|
||||
onClick={onMenuItemClick(() =>
|
||||
navigate(
|
||||
`/templates/${templateName}/versions/${templateVersion}/edit`,
|
||||
),
|
||||
)}
|
||||
>
|
||||
{Language.editFilesButton}
|
||||
<EditOutlined />
|
||||
Edit files
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onDelete()
|
||||
handleClose()
|
||||
}}
|
||||
>
|
||||
{Language.deleteButton}
|
||||
<MenuItem onClick={onMenuItemClick(onDelete)}>
|
||||
<DeleteOutlined />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
@@ -108,7 +109,7 @@ const CreateWorkspaceButton: FC<{
|
||||
component={RouterLink}
|
||||
to={`/templates/${templateName}/workspace`}
|
||||
>
|
||||
{Language.createButton}
|
||||
Create workspace
|
||||
</Button>
|
||||
)
|
||||
|
||||
|
||||
@@ -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<typeof WorkspaceActionButton>
|
||||
|
||||
const Template: Story<WorkspaceActionButtonProps> = (args) => (
|
||||
<WorkspaceActionButton {...args} />
|
||||
)
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
icon: <PlayArrowRoundedIcon />,
|
||||
label: "Start workspace",
|
||||
}
|
||||
@@ -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<WorkspaceActionButtonProps>
|
||||
> = ({ label, icon, onClick, className, ariaLabel }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
className={className}
|
||||
startIcon={icon}
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{Boolean(label) && label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -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<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<CloudQueueIcon />}
|
||||
onClick={handleAction}
|
||||
className={styles.fixedWidth}
|
||||
>
|
||||
{t("actionButton.update")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<PlayCircleOutlineIcon />}
|
||||
onClick={handleAction}
|
||||
className={styles.fixedWidth}
|
||||
>
|
||||
{t("actionButton.start")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const StopButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<CropSquareIcon />}
|
||||
onClick={handleAction}
|
||||
className={styles.fixedWidth}
|
||||
>
|
||||
{t("actionButton.stop")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const CancelButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
return (
|
||||
<Button variant="outlined" startIcon={<BlockIcon />} onClick={handleAction}>
|
||||
Cancel
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
interface DisabledProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({
|
||||
label,
|
||||
}) => {
|
||||
return (
|
||||
<Button variant="outlined" disabled>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
interface LoadingProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const ActionLoadingButton: FC<React.PropsWithChildren<LoadingProps>> = ({
|
||||
label,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<LoadingButton
|
||||
loading
|
||||
variant="outlined"
|
||||
loadingLabel={label}
|
||||
className={styles.fixedWidth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
fixedWidth: {
|
||||
// Make it fixed so the loading changes will not "flick" the UI
|
||||
width: theme.spacing(16),
|
||||
},
|
||||
}))
|
||||
@@ -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<WorkspaceActionsProps> = {}) => {
|
||||
render(
|
||||
<WorkspaceActions
|
||||
workspaceStatus={
|
||||
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
|
||||
}
|
||||
isOutdated={props.isOutdated ?? false}
|
||||
handleStart={jest.fn()}
|
||||
handleStop={jest.fn()}
|
||||
handleDelete={jest.fn()}
|
||||
handleUpdate={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
handleSettings={jest.fn()}
|
||||
isUpdating={false}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
||||
render(
|
||||
<WorkspaceActions
|
||||
workspaceStatus={
|
||||
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
|
||||
}
|
||||
isOutdated={props.isOutdated ?? false}
|
||||
handleStart={jest.fn()}
|
||||
handleStop={jest.fn()}
|
||||
handleDelete={jest.fn()}
|
||||
handleUpdate={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
handleSettings={jest.fn()}
|
||||
isUpdating={false}
|
||||
/>,
|
||||
)
|
||||
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" }),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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<WorkspaceActionsProps> = ({
|
||||
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<HTMLButtonElement>(null)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
|
||||
// A mapping of button type to the corresponding React component
|
||||
const buttonMapping: ButtonMapping = {
|
||||
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
|
||||
[ButtonTypesEnum.update]: (
|
||||
<UpdateButton handleAction={handleUpdate} key={ButtonTypesEnum.update} />
|
||||
),
|
||||
[ButtonTypesEnum.updating]: (
|
||||
<ActionLoadingButton label={t("actionButton.updating")} />
|
||||
<ActionLoadingButton
|
||||
label={t("actionButton.updating")}
|
||||
key={ButtonTypesEnum.updating}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.settings]: (
|
||||
<SettingsButton handleAction={handleSettings} />
|
||||
[ButtonTypesEnum.start]: (
|
||||
<StartButton handleAction={handleStart} key={ButtonTypesEnum.start} />
|
||||
),
|
||||
[ButtonTypesEnum.changeVersion]: canChangeVersions ? (
|
||||
<ChangeVersionButton handleAction={handleChangeVersion} />
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
|
||||
[ButtonTypesEnum.starting]: (
|
||||
<ActionLoadingButton label={t("actionButton.starting")} />
|
||||
<ActionLoadingButton
|
||||
label={t("actionButton.starting")}
|
||||
key={ButtonTypesEnum.starting}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.stop]: (
|
||||
<StopButton handleAction={handleStop} key={ButtonTypesEnum.stop} />
|
||||
),
|
||||
[ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />,
|
||||
[ButtonTypesEnum.stopping]: (
|
||||
<ActionLoadingButton label={t("actionButton.stopping")} />
|
||||
<ActionLoadingButton
|
||||
label={t("actionButton.stopping")}
|
||||
key={ButtonTypesEnum.stopping}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.delete]: <DeleteButton handleAction={handleDelete} />,
|
||||
[ButtonTypesEnum.deleting]: (
|
||||
<ActionLoadingButton label={t("actionButton.deleting")} />
|
||||
<ActionLoadingButton
|
||||
label={t("actionButton.deleting")}
|
||||
key={ButtonTypesEnum.deleting}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.canceling]: (
|
||||
<DisabledButton label={t("disabledButton.canceling")} />
|
||||
<DisabledButton
|
||||
label={t("disabledButton.canceling")}
|
||||
key={ButtonTypesEnum.canceling}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.deleted]: (
|
||||
<DisabledButton label={t("disabledButton.deleted")} />
|
||||
<DisabledButton
|
||||
label={t("disabledButton.deleted")}
|
||||
key={ButtonTypesEnum.deleted}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.pending]: (
|
||||
<ActionLoadingButton label={t("disabledButton.pending")} />
|
||||
<ActionLoadingButton
|
||||
label={t("disabledButton.pending")}
|
||||
key={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 (
|
||||
<DropdownButton
|
||||
primaryAction={buttonMapping[primaryAction]}
|
||||
canCancel={canCancel}
|
||||
handleCancel={handleCancel}
|
||||
secondaryActions={secondaryActions.map((action) => ({
|
||||
action,
|
||||
button: buttonMapping[action],
|
||||
}))}
|
||||
/>
|
||||
<div className={styles.actions} data-testid="workspace-actions">
|
||||
{canBeUpdated &&
|
||||
(isUpdating
|
||||
? buttonMapping[ButtonTypesEnum.updating]
|
||||
: buttonMapping[ButtonTypesEnum.update])}
|
||||
{actionsByStatus.map((action) => buttonMapping[action])}
|
||||
{canCancel && <CancelButton handleAction={handleCancel} />}
|
||||
<div>
|
||||
<Button
|
||||
data-testid="workspace-options-button"
|
||||
aria-controls="workspace-options"
|
||||
aria-haspopup="true"
|
||||
variant="outlined"
|
||||
disabled={!canAcceptJobs}
|
||||
ref={menuTriggerRef}
|
||||
onClick={() => setIsMenuOpen(true)}
|
||||
>
|
||||
<MoreVertOutlined />
|
||||
</Button>
|
||||
<Menu
|
||||
id="workspace-options"
|
||||
anchorEl={menuTriggerRef.current}
|
||||
open={isMenuOpen}
|
||||
onClose={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<MenuItem onClick={onMenuItemClick(handleSettings)}>
|
||||
<SettingsOutlined />
|
||||
Settings
|
||||
</MenuItem>
|
||||
{canChangeVersions && (
|
||||
<MenuItem onClick={onMenuItemClick(handleChangeVersion)}>
|
||||
<HistoryOutlined />
|
||||
Change version
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={onMenuItemClick(handleDelete)}>
|
||||
<DeleteOutlined />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actions: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -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<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
const statusToActions: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
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<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
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<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
deleted: {
|
||||
actions: [ButtonTypesEnum.deleted],
|
||||
canCancel: false,
|
||||
canAcceptJobs: true,
|
||||
canAcceptJobs: false,
|
||||
},
|
||||
pending: {
|
||||
actions: [ButtonTypesEnum.pending],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user