Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ac32b7f

Browse files
committed
show progress indicator within workspace dropdown
resolves #2020
1 parent 471564d commit ac32b7f

File tree

4 files changed

+176
-78
lines changed

4 files changed

+176
-78
lines changed

site/src/components/LoadingButton/LoadingButton.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import Button, { ButtonProps } from "@material-ui/core/Button"
22
import CircularProgress from "@material-ui/core/CircularProgress"
33
import { makeStyles } from "@material-ui/core/styles"
4-
import * as React from "react"
4+
import { Theme } from "@material-ui/core/styles/createMuiTheme"
5+
import { FC } from "react"
56

67
export interface LoadingButtonProps extends ButtonProps {
78
/** Whether or not to disable the button and show a spinner */
89
loading?: boolean
10+
/** An optional label to display with the loading spinner */
11+
loadingLabel?: string
12+
classProp?: string
913
}
1014

1115
/**
@@ -14,33 +18,53 @@ export interface LoadingButtonProps extends ButtonProps {
1418
* In Material-UI 5+ - this is built-in, but since we're on an earlier version,
1519
* we have to roll our own.
1620
*/
17-
export const LoadingButton: React.FC<LoadingButtonProps> = ({
21+
export const LoadingButton: FC<LoadingButtonProps> = ({
1822
loading = false,
23+
loadingLabel,
1924
children,
25+
classProp,
2026
...rest
2127
}) => {
22-
const styles = useStyles()
28+
const styles = useStyles({ hasLoadingLabel: !!loadingLabel })
2329
const hidden = loading ? { opacity: 0 } : undefined
2430

2531
return (
26-
<Button {...rest} disabled={rest.disabled || loading}>
32+
<Button {...rest} disabled={rest.disabled || loading} className={classProp}>
2733
<span style={hidden}>{children}</span>
2834
{loading && (
2935
<div className={styles.loader}>
3036
<CircularProgress size={18} className={styles.spinner} />
3137
</div>
3238
)}
39+
{!!loadingLabel && loadingLabel}
3340
</Button>
3441
)
3542
}
3643

37-
const useStyles = makeStyles((theme) => ({
44+
interface StyleProps {
45+
hasLoadingLabel?: boolean
46+
}
47+
48+
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
3849
loader: {
39-
position: "absolute",
50+
position: (props) => {
51+
if (!props.hasLoadingLabel) {
52+
return "absolute"
53+
}
54+
},
55+
transform: (props) => {
56+
if (!props.hasLoadingLabel) {
57+
return "translate(-50%, -50%)"
58+
}
59+
},
60+
marginRight: (props) => {
61+
if (props.hasLoadingLabel) {
62+
return "10px"
63+
}
64+
},
4065
top: "50%",
4166
left: "50%",
42-
transform: "translate(-50%, -50%)",
43-
height: 18,
67+
height: 22, // centering loading icon
4468
width: 18,
4569
},
4670
spinner: {

site/src/components/WorkspaceActions/ActionCtas.tsx

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import Button from "@material-ui/core/Button"
22
import { makeStyles } from "@material-ui/core/styles"
3+
import BlockIcon from "@material-ui/icons/Block"
34
import CloudQueueIcon from "@material-ui/icons/CloudQueue"
45
import CropSquareIcon from "@material-ui/icons/CropSquare"
56
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
6-
import HighlightOffIcon from "@material-ui/icons/HighlightOff"
77
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
8+
import { LoadingButton } from "components/LoadingButton/LoadingButton"
89
import { FC } from "react"
10+
import { combineClasses } from "util/combineClasses"
911
import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton"
12+
import { WorkspaceStateEnum } from "./constants"
1013

1114
export const Language = {
1215
start: "Start",
1316
stop: "Stop",
1417
delete: "Delete",
1518
cancel: "Cancel",
1619
update: "Update",
20+
// these labels are used in WorkspaceActions.tsx
21+
starting: "Starting...",
22+
stopping: "Stopping...",
23+
deleting: "Deleting...",
1724
}
1825

1926
interface WorkspaceAction {
@@ -72,12 +79,42 @@ export const DeleteButton: FC<WorkspaceAction> = ({ handleAction }) => {
7279
export const CancelButton: FC<WorkspaceAction> = ({ handleAction }) => {
7380
const styles = useStyles()
7481

82+
// this is an icon button, so it's important to include an aria label
7583
return (
76-
<WorkspaceActionButton
77-
className={styles.actionButton}
78-
icon={<HighlightOffIcon />}
84+
<Button
85+
startIcon={<BlockIcon />}
7986
onClick={handleAction}
80-
label={Language.cancel}
87+
className={styles.cancelButton}
88+
aria-label="cancel action"
89+
/>
90+
)
91+
}
92+
93+
interface DisabledProps {
94+
workspaceState: WorkspaceStateEnum
95+
}
96+
97+
export const DisabledButton: FC<DisabledProps> = ({ workspaceState }) => {
98+
const styles = useStyles()
99+
100+
return (
101+
<Button disabled className={styles.actionButton}>
102+
{workspaceState}
103+
</Button>
104+
)
105+
}
106+
107+
interface LoadingProps {
108+
label: string
109+
}
110+
111+
export const ActionLoadingButton: FC<LoadingProps> = ({ label }) => {
112+
const styles = useStyles()
113+
return (
114+
<LoadingButton
115+
loading
116+
loadingLabel={label}
117+
classProp={combineClasses([styles.loadingButton, styles.actionButton])}
81118
/>
82119
)
83120
}
@@ -86,8 +123,26 @@ const useStyles = makeStyles((theme) => ({
86123
actionButton: {
87124
// Set fixed width for the action buttons so they will not change the size
88125
// during the transitions
89-
width: theme.spacing(16),
126+
width: "160px",
90127
border: "none",
91128
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
92129
},
130+
cancelButton: {
131+
"&.MuiButton-root": {
132+
padding: "0px 0px !important",
133+
border: "none",
134+
borderLeft: `1px solid ${theme.palette.divider}`,
135+
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
136+
width: "63px", // matching dropdown button so button grouping doesn't grow in size
137+
},
138+
"& .MuiButton-label": {
139+
marginLeft: "10px",
140+
},
141+
},
142+
// this is all custom to work with our button wrapper
143+
loadingButton: {
144+
border: "none",
145+
borderLeft: "1px solid #333740", // MUI disabled button
146+
borderRadius: "3px 0px 0px 3px",
147+
},
93148
}))

site/src/components/WorkspaceActions/WorkspaceActions.tsx

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ import { FC, ReactNode, useEffect, useMemo, useRef, useState } from "react"
55
import { Workspace } from "../../api/typesGenerated"
66
import { getWorkspaceStatus, WorkspaceStatus } from "../../util/workspace"
77
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
8-
import { CancelButton, DeleteButton, StartButton, StopButton, UpdateButton } from "./ActionCtas"
8+
import {
9+
ActionLoadingButton,
10+
CancelButton,
11+
DeleteButton,
12+
DisabledButton,
13+
Language,
14+
StartButton,
15+
StopButton,
16+
UpdateButton,
17+
} from "./ActionCtas"
918
import { ButtonTypesEnum, WorkspaceStateActions, WorkspaceStateEnum } from "./constants"
1019

1120
/**
@@ -69,12 +78,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
6978
}
7079
}, [workspaceStatus])
7180

72-
const disabledButton = (
73-
<Button disabled className={styles.actionButton}>
74-
{workspaceState}
75-
</Button>
76-
)
77-
7881
type ButtonMapping = {
7982
[key in ButtonTypesEnum]: ReactNode
8083
}
@@ -83,60 +86,68 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
8386
const buttonMapping: ButtonMapping = {
8487
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
8588
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
89+
[ButtonTypesEnum.starting]: <ActionLoadingButton label={Language.starting} />,
8690
[ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />,
91+
[ButtonTypesEnum.stopping]: <ActionLoadingButton label={Language.stopping} />,
8792
[ButtonTypesEnum.delete]: <DeleteButton handleAction={handleDelete} />,
93+
[ButtonTypesEnum.deleting]: <ActionLoadingButton label={Language.deleting} />,
8894
[ButtonTypesEnum.cancel]: <CancelButton handleAction={handleCancel} />,
89-
[ButtonTypesEnum.canceling]: disabledButton,
90-
[ButtonTypesEnum.disabled]: disabledButton,
91-
[ButtonTypesEnum.queued]: disabledButton,
92-
[ButtonTypesEnum.error]: disabledButton,
93-
[ButtonTypesEnum.loading]: disabledButton,
95+
[ButtonTypesEnum.canceling]: <DisabledButton workspaceState={workspaceState} />,
96+
[ButtonTypesEnum.disabled]: <DisabledButton workspaceState={workspaceState} />,
97+
[ButtonTypesEnum.queued]: <DisabledButton workspaceState={workspaceState} />,
98+
[ButtonTypesEnum.error]: <DisabledButton workspaceState={workspaceState} />,
99+
[ButtonTypesEnum.loading]: <DisabledButton workspaceState={workspaceState} />,
94100
}
95101

96102
return (
97103
<span className={styles.buttonContainer}>
98104
{/* primary workspace CTA */}
99105
<span data-testid="primary-cta">{buttonMapping[actions.primary]}</span>
100-
101-
{/* popover toggle button */}
102-
<Button
103-
data-testid="workspace-actions-button"
104-
aria-controls="workspace-actions-menu"
105-
aria-haspopup="true"
106-
className={styles.dropdownButton}
107-
ref={anchorRef}
108-
disabled={!actions.secondary.length}
109-
onClick={() => {
110-
setIsOpen(true)
111-
}}
112-
>
113-
{isOpen ? <CloseDropdown /> : <OpenDropdown />}
114-
</Button>
115-
116-
<Popover
117-
classes={{ paper: styles.popoverPaper }}
118-
id={id}
119-
open={isOpen}
120-
anchorEl={anchorRef.current}
121-
onClose={() => setIsOpen(false)}
122-
anchorOrigin={{
123-
vertical: "bottom",
124-
horizontal: "right",
125-
}}
126-
transformOrigin={{
127-
vertical: "top",
128-
horizontal: "right",
129-
}}
130-
>
131-
{/* secondary workspace CTAs */}
132-
<span data-testid="secondary-ctas">
133-
{actions.secondary.map((action) => (
134-
<div key={action} className={styles.popoverActionButton}>
135-
{buttonMapping[action]}
136-
</div>
137-
))}
138-
</span>
139-
</Popover>
106+
{actions.canCancel ? (
107+
// cancel CTA
108+
<>{buttonMapping[ButtonTypesEnum.cancel]}</>
109+
) : (
110+
<>
111+
{/* popover toggle button */}
112+
<Button
113+
data-testid="workspace-actions-button"
114+
aria-controls="workspace-actions-menu"
115+
aria-haspopup="true"
116+
className={styles.dropdownButton}
117+
ref={anchorRef}
118+
disabled={!actions.secondary.length}
119+
onClick={() => {
120+
setIsOpen(true)
121+
}}
122+
>
123+
{isOpen ? <CloseDropdown /> : <OpenDropdown />}
124+
</Button>
125+
<Popover
126+
classes={{ paper: styles.popoverPaper }}
127+
id={id}
128+
open={isOpen}
129+
anchorEl={anchorRef.current}
130+
onClose={() => setIsOpen(false)}
131+
anchorOrigin={{
132+
vertical: "bottom",
133+
horizontal: "right",
134+
}}
135+
transformOrigin={{
136+
vertical: "top",
137+
horizontal: "right",
138+
}}
139+
>
140+
{/* secondary workspace CTAs */}
141+
<span data-testid="secondary-ctas">
142+
{actions.secondary.map((action) => (
143+
<div key={action} className={styles.popoverActionButton}>
144+
{buttonMapping[action]}
145+
</div>
146+
))}
147+
</span>
148+
</Popover>
149+
</>
150+
)}
140151
</span>
141152
)
142153
}
@@ -152,18 +163,11 @@ const useStyles = makeStyles((theme) => ({
152163
borderLeft: `1px solid ${theme.palette.divider}`,
153164
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
154165
minWidth: "unset",
155-
width: "35px",
166+
width: "63px", // matching cancel button so button grouping doesn't grow in size
156167
"& .MuiButton-label": {
157168
marginRight: "8px",
158169
},
159170
},
160-
actionButton: {
161-
// Set fixed width for the action buttons so they will not change the size
162-
// during the transitions
163-
width: theme.spacing(16),
164-
border: "none",
165-
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
166-
},
167171
popoverActionButton: {
168172
"& .MuiButtonBase-root": {
169173
backgroundColor: "unset",

0 commit comments

Comments
 (0)