Skip to content

Commit a7f14f8

Browse files
fix(site): Fix loading buttons (coder#7549)
1 parent 119098a commit a7f14f8

File tree

5 files changed

+94
-158
lines changed

5 files changed

+94
-158
lines changed

site/src/components/LoadingButton/LoadingButton.stories.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ const Template: Story<LoadingButtonProps> = (args) => (
1919

2020
export const Loading = Template.bind({})
2121
Loading.args = {
22-
variant: "contained",
2322
loading: true,
2423
}
2524

2625
export const NotLoading = Template.bind({})
2726
NotLoading.args = {
28-
variant: "contained",
2927
loading: false,
3028
}
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,21 @@
1-
import Button, { ButtonProps } from "@mui/material/Button"
2-
import CircularProgress from "@mui/material/CircularProgress"
3-
import { makeStyles } from "@mui/styles"
4-
import { Theme } from "@mui/material/styles"
51
import { FC } from "react"
2+
import MuiLoadingButton, {
3+
LoadingButtonProps as MuiLoadingButtonProps,
4+
} from "@mui/lab/LoadingButton"
65

7-
export interface LoadingButtonProps extends ButtonProps {
8-
/** Whether or not to disable the button and show a spinner */
9-
loading?: boolean
10-
/** An optional label to display with the loading spinner */
11-
loadingLabel?: string
12-
}
6+
export type LoadingButtonProps = MuiLoadingButtonProps
137

14-
/**
15-
* LoadingButton is a small wrapper around Material-UI's button to show a loading spinner
16-
*
17-
* In Material-UI 5+ - this is built-in, but since we're on an earlier version,
18-
* we have to roll our own.
19-
*/
20-
export const LoadingButton: FC<React.PropsWithChildren<LoadingButtonProps>> = ({
21-
loading = false,
22-
loadingLabel,
8+
export const LoadingButton: FC<LoadingButtonProps> = ({
239
children,
24-
...rest
10+
loadingIndicator,
11+
...buttonProps
2512
}) => {
26-
const styles = useStyles({ hasLoadingLabel: Boolean(loadingLabel) })
27-
const hidden = loading ? { opacity: 0 } : undefined
28-
2913
return (
30-
<Button {...rest} disabled={rest.disabled || loading}>
31-
<span style={hidden}>{children}</span>
32-
{loading && (
33-
<div className={styles.loader}>
34-
<CircularProgress size={16} className={styles.spinner} />
35-
</div>
36-
)}
37-
{Boolean(loadingLabel) && loadingLabel}
38-
</Button>
14+
<MuiLoadingButton variant="outlined" color="neutral" {...buttonProps}>
15+
{/* known issue: https://github.com/mui/material-ui/issues/27853 */}
16+
<span>
17+
{buttonProps.loading && loadingIndicator ? loadingIndicator : children}
18+
</span>
19+
</MuiLoadingButton>
3920
)
4021
}
41-
42-
interface StyleProps {
43-
hasLoadingLabel?: boolean
44-
}
45-
46-
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
47-
loader: {
48-
position: (props) => {
49-
if (!props.hasLoadingLabel) {
50-
return "absolute"
51-
}
52-
},
53-
transform: (props) => {
54-
if (!props.hasLoadingLabel) {
55-
return "translate(-50%, -50%)"
56-
}
57-
},
58-
marginRight: (props) => {
59-
if (props.hasLoadingLabel) {
60-
return "10px"
61-
}
62-
},
63-
top: "50%",
64-
left: "50%",
65-
height: 22, // centering loading icon
66-
width: 16,
67-
},
68-
spinner: {
69-
color: theme.palette.text.disabled,
70-
},
71-
}))

site/src/components/WorkspaceActions/Buttons.tsx

+56-42
Original file line numberDiff line numberDiff line change
@@ -5,76 +5,85 @@ import CropSquareIcon from "@mui/icons-material/CropSquare"
55
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline"
66
import ReplayIcon from "@mui/icons-material/Replay"
77
import { LoadingButton } from "components/LoadingButton/LoadingButton"
8-
import { FC, PropsWithChildren } from "react"
9-
import { useTranslation } from "react-i18next"
8+
import { FC } from "react"
9+
import BlockOutlined from "@mui/icons-material/BlockOutlined"
1010

1111
interface WorkspaceAction {
12+
loading?: boolean
1213
handleAction: () => void
1314
}
1415

15-
export const UpdateButton: FC<PropsWithChildren<WorkspaceAction>> = ({
16+
export const UpdateButton: FC<WorkspaceAction> = ({
1617
handleAction,
18+
loading,
1719
}) => {
18-
const { t } = useTranslation("workspacePage")
19-
2020
return (
21-
<Button
21+
<LoadingButton
22+
loading={loading}
23+
loadingIndicator="Updating..."
24+
loadingPosition="start"
2225
size="small"
2326
data-testid="workspace-update-button"
2427
startIcon={<CloudQueueIcon />}
2528
onClick={handleAction}
2629
>
27-
{t("actionButton.update")}
28-
</Button>
30+
Update
31+
</LoadingButton>
2932
)
3033
}
3134

32-
export const StartButton: FC<PropsWithChildren<WorkspaceAction>> = ({
33-
handleAction,
34-
}) => {
35-
const { t } = useTranslation("workspacePage")
36-
35+
export const StartButton: FC<WorkspaceAction> = ({ handleAction, loading }) => {
3736
return (
38-
<Button startIcon={<PlayCircleOutlineIcon />} onClick={handleAction}>
39-
{t("actionButton.start")}
40-
</Button>
37+
<LoadingButton
38+
size="small"
39+
loading={loading}
40+
loadingIndicator="Starting..."
41+
loadingPosition="start"
42+
startIcon={<PlayCircleOutlineIcon />}
43+
onClick={handleAction}
44+
>
45+
Start
46+
</LoadingButton>
4147
)
4248
}
4349

44-
export const StopButton: FC<PropsWithChildren<WorkspaceAction>> = ({
45-
handleAction,
46-
}) => {
47-
const { t } = useTranslation("workspacePage")
48-
50+
export const StopButton: FC<WorkspaceAction> = ({ handleAction, loading }) => {
4951
return (
50-
<Button size="small" startIcon={<CropSquareIcon />} onClick={handleAction}>
51-
{t("actionButton.stop")}
52-
</Button>
52+
<LoadingButton
53+
size="small"
54+
loading={loading}
55+
loadingIndicator="Stopping..."
56+
loadingPosition="start"
57+
startIcon={<CropSquareIcon />}
58+
onClick={handleAction}
59+
>
60+
Stop
61+
</LoadingButton>
5362
)
5463
}
5564

56-
export const RestartButton: FC<PropsWithChildren<WorkspaceAction>> = ({
65+
export const RestartButton: FC<WorkspaceAction> = ({
5766
handleAction,
67+
loading,
5868
}) => {
59-
const { t } = useTranslation("workspacePage")
60-
6169
return (
62-
<Button
70+
<LoadingButton
71+
loading={loading}
72+
loadingIndicator="Restarting..."
73+
loadingPosition="start"
6374
size="small"
6475
startIcon={<ReplayIcon />}
6576
onClick={handleAction}
6677
data-testid="workspace-restart-button"
6778
>
68-
{t("actionButton.restart")}
69-
</Button>
79+
Restart
80+
</LoadingButton>
7081
)
7182
}
7283

73-
export const CancelButton: FC<PropsWithChildren<WorkspaceAction>> = ({
74-
handleAction,
75-
}) => {
84+
export const CancelButton: FC<WorkspaceAction> = ({ handleAction }) => {
7685
return (
77-
<Button startIcon={<BlockIcon />} onClick={handleAction}>
86+
<Button size="small" startIcon={<BlockIcon />} onClick={handleAction}>
7887
Cancel
7988
</Button>
8089
)
@@ -84,11 +93,9 @@ interface DisabledProps {
8493
label: string
8594
}
8695

87-
export const DisabledButton: FC<PropsWithChildren<DisabledProps>> = ({
88-
label,
89-
}) => {
96+
export const DisabledButton: FC<DisabledProps> = ({ label }) => {
9097
return (
91-
<Button size="small" disabled>
98+
<Button size="small" startIcon={<BlockOutlined />} disabled>
9299
{label}
93100
</Button>
94101
)
@@ -98,8 +105,15 @@ interface LoadingProps {
98105
label: string
99106
}
100107

101-
export const ActionLoadingButton: FC<PropsWithChildren<LoadingProps>> = ({
102-
label,
103-
}) => {
104-
return <LoadingButton loading size="small" loadingLabel={label} />
108+
export const ActionLoadingButton: FC<LoadingProps> = ({ label }) => {
109+
return (
110+
<LoadingButton
111+
loading
112+
size="small"
113+
loadingPosition="start"
114+
loadingIndicator={label}
115+
// This icon can be anything
116+
startIcon={<ReplayIcon />}
117+
/>
118+
)
105119
}

site/src/components/WorkspaceActions/WorkspaceActions.tsx

+11-51
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Menu from "@mui/material/Menu"
33
import { makeStyles } from "@mui/styles"
44
import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"
55
import { FC, ReactNode, useRef, useState } from "react"
6-
import { useTranslation } from "react-i18next"
76
import { WorkspaceStatus } from "api/typesGenerated"
87
import {
98
ActionLoadingButton,
@@ -57,7 +56,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
5756
canChangeVersions,
5857
}) => {
5958
const styles = useStyles()
60-
const { t } = useTranslation("workspacePage")
6159
const {
6260
canCancel,
6361
canAcceptJobs,
@@ -69,64 +67,26 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
6967

7068
// A mapping of button type to the corresponding React component
7169
const buttonMapping: ButtonMapping = {
72-
[ButtonTypesEnum.update]: (
73-
<UpdateButton handleAction={handleUpdate} key={ButtonTypesEnum.update} />
74-
),
70+
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
7571
[ButtonTypesEnum.updating]: (
76-
<ActionLoadingButton
77-
label={t("actionButton.updating")}
78-
key={ButtonTypesEnum.updating}
79-
/>
80-
),
81-
[ButtonTypesEnum.start]: (
82-
<StartButton handleAction={handleStart} key={ButtonTypesEnum.start} />
72+
<UpdateButton loading handleAction={handleUpdate} />
8373
),
74+
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
8475
[ButtonTypesEnum.starting]: (
85-
<ActionLoadingButton
86-
label={t("actionButton.starting")}
87-
key={ButtonTypesEnum.starting}
88-
/>
89-
),
90-
[ButtonTypesEnum.stop]: (
91-
<StopButton handleAction={handleStop} key={ButtonTypesEnum.stop} />
76+
<StartButton loading handleAction={handleStart} />
9277
),
78+
[ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />,
9379
[ButtonTypesEnum.stopping]: (
94-
<ActionLoadingButton
95-
label={t("actionButton.stopping")}
96-
key={ButtonTypesEnum.stopping}
97-
/>
80+
<StopButton loading handleAction={handleStop} />
9881
),
9982
[ButtonTypesEnum.restart]: <RestartButton handleAction={handleRestart} />,
10083
[ButtonTypesEnum.restarting]: (
101-
<ActionLoadingButton
102-
label="Restarting"
103-
key={ButtonTypesEnum.restarting}
104-
/>
105-
),
106-
[ButtonTypesEnum.deleting]: (
107-
<ActionLoadingButton
108-
label={t("actionButton.deleting")}
109-
key={ButtonTypesEnum.deleting}
110-
/>
111-
),
112-
[ButtonTypesEnum.canceling]: (
113-
<DisabledButton
114-
label={t("disabledButton.canceling")}
115-
key={ButtonTypesEnum.canceling}
116-
/>
117-
),
118-
[ButtonTypesEnum.deleted]: (
119-
<DisabledButton
120-
label={t("disabledButton.deleted")}
121-
key={ButtonTypesEnum.deleted}
122-
/>
123-
),
124-
[ButtonTypesEnum.pending]: (
125-
<ActionLoadingButton
126-
label={t("disabledButton.pending")}
127-
key={ButtonTypesEnum.pending}
128-
/>
84+
<RestartButton loading handleAction={handleRestart} />
12985
),
86+
[ButtonTypesEnum.deleting]: <ActionLoadingButton label="Deleting" />,
87+
[ButtonTypesEnum.canceling]: <DisabledButton label="Canceling..." />,
88+
[ButtonTypesEnum.deleted]: <DisabledButton label="Deleted" />,
89+
[ButtonTypesEnum.pending]: <ActionLoadingButton label="Pending..." />,
13090
}
13191

13292
// Returns a function that will execute the action and close the menu

site/src/theme/theme.ts

+14
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,26 @@ dark = createTheme(dark, {
133133
sizeSmall: {
134134
borderRadius: 6,
135135
height: BUTTON_SM_HEIGHT,
136+
137+
"& .MuiCircularProgress-root": {
138+
width: "14px !important",
139+
height: "14px !important",
140+
},
136141
},
137142
sizeLarge: {
138143
height: BUTTON_LG_HEIGHT,
139144
},
140145
outlinedNeutral: {
141146
borderColor: colors.gray[12],
147+
148+
"&.Mui-disabled": {
149+
borderColor: colors.gray[13],
150+
color: colors.gray[11],
151+
152+
"& > .MuiLoadingButton-loadingIndicator": {
153+
color: colors.gray[11],
154+
},
155+
},
142156
},
143157
containedNeutral: {
144158
borderColor: colors.gray[12],

0 commit comments

Comments
 (0)