mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): Fix loading buttons (#7549)
This commit is contained in:
@@ -19,12 +19,10 @@ const Template: Story<LoadingButtonProps> = (args) => (
|
||||
|
||||
export const Loading = Template.bind({})
|
||||
Loading.args = {
|
||||
variant: "contained",
|
||||
loading: true,
|
||||
}
|
||||
|
||||
export const NotLoading = Template.bind({})
|
||||
NotLoading.args = {
|
||||
variant: "contained",
|
||||
loading: false,
|
||||
}
|
||||
|
||||
@@ -1,71 +1,21 @@
|
||||
import Button, { ButtonProps } from "@mui/material/Button"
|
||||
import CircularProgress from "@mui/material/CircularProgress"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import { Theme } from "@mui/material/styles"
|
||||
import { FC } from "react"
|
||||
import MuiLoadingButton, {
|
||||
LoadingButtonProps as MuiLoadingButtonProps,
|
||||
} from "@mui/lab/LoadingButton"
|
||||
|
||||
export interface LoadingButtonProps extends ButtonProps {
|
||||
/** Whether or not to disable the button and show a spinner */
|
||||
loading?: boolean
|
||||
/** An optional label to display with the loading spinner */
|
||||
loadingLabel?: string
|
||||
}
|
||||
export type LoadingButtonProps = MuiLoadingButtonProps
|
||||
|
||||
/**
|
||||
* LoadingButton is a small wrapper around Material-UI's button to show a loading spinner
|
||||
*
|
||||
* In Material-UI 5+ - this is built-in, but since we're on an earlier version,
|
||||
* we have to roll our own.
|
||||
*/
|
||||
export const LoadingButton: FC<React.PropsWithChildren<LoadingButtonProps>> = ({
|
||||
loading = false,
|
||||
loadingLabel,
|
||||
export const LoadingButton: FC<LoadingButtonProps> = ({
|
||||
children,
|
||||
...rest
|
||||
loadingIndicator,
|
||||
...buttonProps
|
||||
}) => {
|
||||
const styles = useStyles({ hasLoadingLabel: Boolean(loadingLabel) })
|
||||
const hidden = loading ? { opacity: 0 } : undefined
|
||||
|
||||
return (
|
||||
<Button {...rest} disabled={rest.disabled || loading}>
|
||||
<span style={hidden}>{children}</span>
|
||||
{loading && (
|
||||
<div className={styles.loader}>
|
||||
<CircularProgress size={16} className={styles.spinner} />
|
||||
</div>
|
||||
)}
|
||||
{Boolean(loadingLabel) && loadingLabel}
|
||||
</Button>
|
||||
<MuiLoadingButton variant="outlined" color="neutral" {...buttonProps}>
|
||||
{/* known issue: https://github.com/mui/material-ui/issues/27853 */}
|
||||
<span>
|
||||
{buttonProps.loading && loadingIndicator ? loadingIndicator : children}
|
||||
</span>
|
||||
</MuiLoadingButton>
|
||||
)
|
||||
}
|
||||
|
||||
interface StyleProps {
|
||||
hasLoadingLabel?: boolean
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
|
||||
loader: {
|
||||
position: (props) => {
|
||||
if (!props.hasLoadingLabel) {
|
||||
return "absolute"
|
||||
}
|
||||
},
|
||||
transform: (props) => {
|
||||
if (!props.hasLoadingLabel) {
|
||||
return "translate(-50%, -50%)"
|
||||
}
|
||||
},
|
||||
marginRight: (props) => {
|
||||
if (props.hasLoadingLabel) {
|
||||
return "10px"
|
||||
}
|
||||
},
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
height: 22, // centering loading icon
|
||||
width: 16,
|
||||
},
|
||||
spinner: {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -5,76 +5,85 @@ import CropSquareIcon from "@mui/icons-material/CropSquare"
|
||||
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline"
|
||||
import ReplayIcon from "@mui/icons-material/Replay"
|
||||
import { LoadingButton } from "components/LoadingButton/LoadingButton"
|
||||
import { FC, PropsWithChildren } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { FC } from "react"
|
||||
import BlockOutlined from "@mui/icons-material/BlockOutlined"
|
||||
|
||||
interface WorkspaceAction {
|
||||
loading?: boolean
|
||||
handleAction: () => void
|
||||
}
|
||||
|
||||
export const UpdateButton: FC<PropsWithChildren<WorkspaceAction>> = ({
|
||||
export const UpdateButton: FC<WorkspaceAction> = ({
|
||||
handleAction,
|
||||
loading,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingIndicator="Updating..."
|
||||
loadingPosition="start"
|
||||
size="small"
|
||||
data-testid="workspace-update-button"
|
||||
startIcon={<CloudQueueIcon />}
|
||||
onClick={handleAction}
|
||||
>
|
||||
{t("actionButton.update")}
|
||||
</Button>
|
||||
Update
|
||||
</LoadingButton>
|
||||
)
|
||||
}
|
||||
|
||||
export const StartButton: FC<PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
export const StartButton: FC<WorkspaceAction> = ({ handleAction, loading }) => {
|
||||
return (
|
||||
<Button startIcon={<PlayCircleOutlineIcon />} onClick={handleAction}>
|
||||
{t("actionButton.start")}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
size="small"
|
||||
loading={loading}
|
||||
loadingIndicator="Starting..."
|
||||
loadingPosition="start"
|
||||
startIcon={<PlayCircleOutlineIcon />}
|
||||
onClick={handleAction}
|
||||
>
|
||||
Start
|
||||
</LoadingButton>
|
||||
)
|
||||
}
|
||||
|
||||
export const StopButton: FC<PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
export const StopButton: FC<WorkspaceAction> = ({ handleAction, loading }) => {
|
||||
return (
|
||||
<Button size="small" startIcon={<CropSquareIcon />} onClick={handleAction}>
|
||||
{t("actionButton.stop")}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
size="small"
|
||||
loading={loading}
|
||||
loadingIndicator="Stopping..."
|
||||
loadingPosition="start"
|
||||
startIcon={<CropSquareIcon />}
|
||||
onClick={handleAction}
|
||||
>
|
||||
Stop
|
||||
</LoadingButton>
|
||||
)
|
||||
}
|
||||
|
||||
export const RestartButton: FC<PropsWithChildren<WorkspaceAction>> = ({
|
||||
export const RestartButton: FC<WorkspaceAction> = ({
|
||||
handleAction,
|
||||
loading,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingIndicator="Restarting..."
|
||||
loadingPosition="start"
|
||||
size="small"
|
||||
startIcon={<ReplayIcon />}
|
||||
onClick={handleAction}
|
||||
data-testid="workspace-restart-button"
|
||||
>
|
||||
{t("actionButton.restart")}
|
||||
</Button>
|
||||
Restart
|
||||
</LoadingButton>
|
||||
)
|
||||
}
|
||||
|
||||
export const CancelButton: FC<PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
export const CancelButton: FC<WorkspaceAction> = ({ handleAction }) => {
|
||||
return (
|
||||
<Button startIcon={<BlockIcon />} onClick={handleAction}>
|
||||
<Button size="small" startIcon={<BlockIcon />} onClick={handleAction}>
|
||||
Cancel
|
||||
</Button>
|
||||
)
|
||||
@@ -84,11 +93,9 @@ interface DisabledProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const DisabledButton: FC<PropsWithChildren<DisabledProps>> = ({
|
||||
label,
|
||||
}) => {
|
||||
export const DisabledButton: FC<DisabledProps> = ({ label }) => {
|
||||
return (
|
||||
<Button size="small" disabled>
|
||||
<Button size="small" startIcon={<BlockOutlined />} disabled>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
@@ -98,8 +105,15 @@ interface LoadingProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const ActionLoadingButton: FC<PropsWithChildren<LoadingProps>> = ({
|
||||
label,
|
||||
}) => {
|
||||
return <LoadingButton loading size="small" loadingLabel={label} />
|
||||
export const ActionLoadingButton: FC<LoadingProps> = ({ label }) => {
|
||||
return (
|
||||
<LoadingButton
|
||||
loading
|
||||
size="small"
|
||||
loadingPosition="start"
|
||||
loadingIndicator={label}
|
||||
// This icon can be anything
|
||||
startIcon={<ReplayIcon />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import Menu from "@mui/material/Menu"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"
|
||||
import { FC, ReactNode, useRef, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { WorkspaceStatus } from "api/typesGenerated"
|
||||
import {
|
||||
ActionLoadingButton,
|
||||
@@ -57,7 +56,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
canChangeVersions,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const {
|
||||
canCancel,
|
||||
canAcceptJobs,
|
||||
@@ -69,64 +67,26 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
|
||||
// A mapping of button type to the corresponding React component
|
||||
const buttonMapping: ButtonMapping = {
|
||||
[ButtonTypesEnum.update]: (
|
||||
<UpdateButton handleAction={handleUpdate} key={ButtonTypesEnum.update} />
|
||||
),
|
||||
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
|
||||
[ButtonTypesEnum.updating]: (
|
||||
<ActionLoadingButton
|
||||
label={t("actionButton.updating")}
|
||||
key={ButtonTypesEnum.updating}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.start]: (
|
||||
<StartButton handleAction={handleStart} key={ButtonTypesEnum.start} />
|
||||
<UpdateButton loading handleAction={handleUpdate} />
|
||||
),
|
||||
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
|
||||
[ButtonTypesEnum.starting]: (
|
||||
<ActionLoadingButton
|
||||
label={t("actionButton.starting")}
|
||||
key={ButtonTypesEnum.starting}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.stop]: (
|
||||
<StopButton handleAction={handleStop} key={ButtonTypesEnum.stop} />
|
||||
<StartButton loading handleAction={handleStart} />
|
||||
),
|
||||
[ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />,
|
||||
[ButtonTypesEnum.stopping]: (
|
||||
<ActionLoadingButton
|
||||
label={t("actionButton.stopping")}
|
||||
key={ButtonTypesEnum.stopping}
|
||||
/>
|
||||
<StopButton loading handleAction={handleStop} />
|
||||
),
|
||||
[ButtonTypesEnum.restart]: <RestartButton handleAction={handleRestart} />,
|
||||
[ButtonTypesEnum.restarting]: (
|
||||
<ActionLoadingButton
|
||||
label="Restarting"
|
||||
key={ButtonTypesEnum.restarting}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.deleting]: (
|
||||
<ActionLoadingButton
|
||||
label={t("actionButton.deleting")}
|
||||
key={ButtonTypesEnum.deleting}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.canceling]: (
|
||||
<DisabledButton
|
||||
label={t("disabledButton.canceling")}
|
||||
key={ButtonTypesEnum.canceling}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.deleted]: (
|
||||
<DisabledButton
|
||||
label={t("disabledButton.deleted")}
|
||||
key={ButtonTypesEnum.deleted}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.pending]: (
|
||||
<ActionLoadingButton
|
||||
label={t("disabledButton.pending")}
|
||||
key={ButtonTypesEnum.pending}
|
||||
/>
|
||||
<RestartButton loading handleAction={handleRestart} />
|
||||
),
|
||||
[ButtonTypesEnum.deleting]: <ActionLoadingButton label="Deleting" />,
|
||||
[ButtonTypesEnum.canceling]: <DisabledButton label="Canceling..." />,
|
||||
[ButtonTypesEnum.deleted]: <DisabledButton label="Deleted" />,
|
||||
[ButtonTypesEnum.pending]: <ActionLoadingButton label="Pending..." />,
|
||||
}
|
||||
|
||||
// Returns a function that will execute the action and close the menu
|
||||
|
||||
@@ -133,12 +133,26 @@ dark = createTheme(dark, {
|
||||
sizeSmall: {
|
||||
borderRadius: 6,
|
||||
height: BUTTON_SM_HEIGHT,
|
||||
|
||||
"& .MuiCircularProgress-root": {
|
||||
width: "14px !important",
|
||||
height: "14px !important",
|
||||
},
|
||||
},
|
||||
sizeLarge: {
|
||||
height: BUTTON_LG_HEIGHT,
|
||||
},
|
||||
outlinedNeutral: {
|
||||
borderColor: colors.gray[12],
|
||||
|
||||
"&.Mui-disabled": {
|
||||
borderColor: colors.gray[13],
|
||||
color: colors.gray[11],
|
||||
|
||||
"& > .MuiLoadingButton-loadingIndicator": {
|
||||
color: colors.gray[11],
|
||||
},
|
||||
},
|
||||
},
|
||||
containedNeutral: {
|
||||
borderColor: colors.gray[12],
|
||||
|
||||
Reference in New Issue
Block a user