Skip to content

Commit 066b7dc

Browse files
committed
Extract DropDownButton
1 parent 1881ce6 commit 066b7dc

File tree

8 files changed

+154
-165
lines changed

8 files changed

+154
-165
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Button from "@material-ui/core/Button"
2+
import Popover from "@material-ui/core/Popover"
3+
import { makeStyles } from "@material-ui/core/styles"
4+
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
5+
import { DropdownContent } from "components/DropdownButton/DropdownContent/DropdownContent"
6+
import { FC, ReactNode, useRef, useState, useEffect } from "react"
7+
import { CancelButton } from "./ActionCtas"
8+
9+
export interface DropdownButtonProps {
10+
primaryAction: ReactNode
11+
secondaryActions: Array<{ action: string, button: ReactNode }>
12+
canCancel: boolean
13+
handleCancel?: () => void
14+
}
15+
16+
export const DropdownButton: FC<DropdownButtonProps> = ({ primaryAction, secondaryActions, canCancel, handleCancel }) => {
17+
const styles = useStyles()
18+
const anchorRef = useRef<HTMLButtonElement>(null)
19+
const [isOpen, setIsOpen] = useState(false)
20+
const id = isOpen ? "action-popover" : undefined
21+
22+
/**
23+
* Ensures we close the popover before calling any action handler
24+
*/
25+
useEffect(() => {
26+
setIsOpen(false)
27+
return () => {
28+
setIsOpen(false)
29+
}
30+
}, [])
31+
32+
return (
33+
<span className={styles.buttonContainer}>
34+
{/* primary workspace CTA */}
35+
<span data-testid="primary-cta" className={styles.primaryCta}>
36+
{primaryAction}
37+
</span>
38+
{canCancel && handleCancel ? (
39+
<CancelButton handleAction={handleCancel} />
40+
) : (
41+
<>
42+
{/* popover toggle button */}
43+
<Button
44+
data-testid="workspace-actions-button"
45+
aria-controls="workspace-actions-menu"
46+
aria-haspopup="true"
47+
className={styles.dropdownButton}
48+
ref={anchorRef}
49+
disabled={!secondaryActions.length}
50+
onClick={() => {
51+
setIsOpen(true)
52+
}}
53+
>
54+
{isOpen ? <CloseDropdown /> : <OpenDropdown />}
55+
</Button>
56+
<Popover
57+
classes={{ paper: styles.popoverPaper }}
58+
id={id}
59+
open={isOpen}
60+
anchorEl={anchorRef.current}
61+
onClose={() => setIsOpen(false)}
62+
anchorOrigin={{
63+
vertical: "bottom",
64+
horizontal: "right",
65+
}}
66+
transformOrigin={{
67+
vertical: "top",
68+
horizontal: "right",
69+
}}
70+
>
71+
{/* secondary workspace CTAs */}
72+
<DropdownContent secondaryActions={secondaryActions} />
73+
</Popover>
74+
</>
75+
)}
76+
</span>
77+
)
78+
}
79+
80+
const useStyles = makeStyles((theme) => ({
81+
buttonContainer: {
82+
border: `1px solid ${theme.palette.divider}`,
83+
borderRadius: `${theme.shape.borderRadius}px`,
84+
display: "inline-flex",
85+
},
86+
dropdownButton: {
87+
border: "none",
88+
borderLeft: `1px solid ${theme.palette.divider}`,
89+
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
90+
minWidth: "unset",
91+
width: "63px", // matching cancel button so button grouping doesn't grow in size
92+
"& .MuiButton-label": {
93+
marginRight: "8px",
94+
},
95+
},
96+
primaryCta: {
97+
[theme.breakpoints.down("sm")]: {
98+
width: "100%",
99+
100+
"& > *": {
101+
width: "100%",
102+
},
103+
},
104+
},
105+
popoverPaper: {
106+
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px ${theme.spacing(1)}px`,
107+
},
108+
}))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { action } from "@storybook/addon-actions"
2+
import { Story } from "@storybook/react"
3+
import { DeleteButton, UpdateButton, } from "../ActionCtas"
4+
import { DropdownContent, DropdownContentProps } from "./DropdownContent"
5+
6+
export default {
7+
title: "DropdownButton/DropdownContent",
8+
component: DropdownContent,
9+
}
10+
11+
const Template: Story<DropdownContentProps> = (args) => <DropdownContent {...args} />
12+
13+
export const Example = Template.bind({})
14+
Example.args = {
15+
secondaryActions: [
16+
{ action: "update", button: <UpdateButton handleAction={action("update")} /> },
17+
{ action: "delete", button: <DeleteButton handleAction={action("delete")} /> }
18+
]
19+
}

site/src/components/WorkspaceActions/DropdownContent/DropdownContent.tsx renamed to site/src/components/DropdownButton/DropdownContent/DropdownContent.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
11
import { makeStyles } from "@material-ui/core/styles"
2-
import { FC } from "react"
3-
import { ButtonMapping, ButtonTypesEnum } from "../constants"
2+
import { FC, ReactNode } from "react"
43

54
export interface DropdownContentProps {
6-
secondaryActions: ButtonTypesEnum[]
7-
buttonMapping: Partial<ButtonMapping>
5+
secondaryActions: Array<{ action: string, button: ReactNode }>
86
}
97

108
/* secondary workspace CTAs */
119
export const DropdownContent: FC<React.PropsWithChildren<DropdownContentProps>> = ({
1210
secondaryActions,
13-
buttonMapping,
1411
}) => {
1512
const styles = useStyles()
1613

1714
return (
1815
<span data-testid="secondary-ctas">
19-
{secondaryActions.map((action) => (
16+
{secondaryActions.map(({ action, button }) => (
2017
<div key={action} className={styles.popoverActionButton}>
21-
{buttonMapping[action]}
18+
{button}
2219
</div>
2320
))}
2421
</span>

site/src/components/WorkspaceActions/DropdownContent/DropdownContent.stories.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

site/src/components/WorkspaceActions/WorkspaceActions.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { fireEvent, screen } from "@testing-library/react"
22
import { WorkspaceStateEnum } from "util/workspace"
33
import * as Mocks from "../../testHelpers/entities"
44
import { render } from "../../testHelpers/renderHelpers"
5-
import { Language } from "./ActionCtas"
5+
import { Language } from "../DropdownButton/ActionCtas"
66
import { WorkspaceActions, WorkspaceActionsProps } from "./WorkspaceActions"
77

88
const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
Lines changed: 9 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import Button from "@material-ui/core/Button"
2-
import Popover from "@material-ui/core/Popover"
3-
import { makeStyles } from "@material-ui/core/styles"
4-
import { FC, ReactNode, useEffect, useMemo, useRef, useState } from "react"
1+
import { DropdownButton } from "components/DropdownButton/DropdownButton"
2+
import { FC, ReactNode, useMemo } from "react"
53
import { getWorkspaceStatus, WorkspaceStateEnum, WorkspaceStatus } from "util/workspace"
64
import { Workspace } from "../../api/typesGenerated"
7-
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
85
import {
96
ActionLoadingButton,
107
CancelButton,
@@ -14,9 +11,8 @@ import {
1411
StartButton,
1512
StopButton,
1613
UpdateButton,
17-
} from "./ActionCtas"
14+
} from "../DropdownButton/ActionCtas"
1815
import { ButtonMapping, ButtonTypesEnum, WorkspaceStateActions } from "./constants"
19-
import { DropdownContent } from "./DropdownContent/DropdownContent"
2016

2117
/**
2218
* Jobs submitted while another job is in progress will be discarded,
@@ -43,10 +39,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
4339
handleUpdate,
4440
handleCancel,
4541
}) => {
46-
const styles = useStyles()
47-
const anchorRef = useRef<HTMLButtonElement>(null)
48-
const [isOpen, setIsOpen] = useState(false)
49-
const id = isOpen ? "action-popover" : undefined
5042

5143
const workspaceStatus: keyof typeof WorkspaceStateEnum = getWorkspaceStatus(
5244
workspace.latest_build,
@@ -70,16 +62,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
7062
return updatedActions
7163
}, [canBeUpdated, workspaceState])
7264

73-
/**
74-
* Ensures we close the popover before calling any action handler
75-
*/
76-
useEffect(() => {
77-
setIsOpen(false)
78-
return () => {
79-
setIsOpen(false)
80-
}
81-
}, [workspaceStatus])
82-
8365
// A mapping of button type to the corresponding React component
8466
const buttonMapping: ButtonMapping = {
8567
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
@@ -98,80 +80,13 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
9880
}
9981

10082
return (
101-
<span className={styles.buttonContainer}>
102-
{/* primary workspace CTA */}
103-
<span data-testid="primary-cta" className={styles.primaryCta}>
104-
{buttonMapping[actions.primary]}
105-
</span>
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-
<DropdownContent secondaryActions={actions.secondary} buttonMapping={buttonMapping} />
142-
</Popover>
143-
</>
144-
)}
145-
</span>
83+
<DropdownButton
84+
primaryAction={buttonMapping[actions.primary]}
85+
canCancel={actions.canCancel}
86+
handleCancel={handleCancel}
87+
secondaryActions={actions.secondary.map((action) => ({ action, button: buttonMapping[action] }))}
88+
/>
14689
)
14790
}
14891

149-
const useStyles = makeStyles((theme) => ({
150-
buttonContainer: {
151-
border: `1px solid ${theme.palette.divider}`,
152-
borderRadius: `${theme.shape.borderRadius}px`,
153-
display: "inline-flex",
154-
},
155-
dropdownButton: {
156-
border: "none",
157-
borderLeft: `1px solid ${theme.palette.divider}`,
158-
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
159-
minWidth: "unset",
160-
width: "63px", // matching cancel button so button grouping doesn't grow in size
161-
"& .MuiButton-label": {
162-
marginRight: "8px",
163-
},
164-
},
165-
primaryCta: {
166-
[theme.breakpoints.down("sm")]: {
167-
width: "100%",
16892

169-
"& > *": {
170-
width: "100%",
171-
},
172-
},
173-
},
174-
popoverPaper: {
175-
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px ${theme.spacing(1)}px`,
176-
},
177-
}))

site/src/components/WorkspaceActions/constants.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@ import { WorkspaceStateEnum } from "util/workspace"
33

44
// the button types we have
55
export enum ButtonTypesEnum {
6-
start,
7-
starting,
8-
stop,
9-
stopping,
10-
delete,
11-
deleting,
12-
update,
13-
cancel,
14-
error,
6+
start = "start",
7+
starting = "starting",
8+
stop = "stop",
9+
stopping = "stopping",
10+
delete = "delete",
11+
deleting = "deleting",
12+
update = "update",
13+
cancel = "cancel",
14+
error = "error",
1515
// disabled buttons
16-
canceling,
17-
disabled,
18-
queued,
19-
loading,
16+
canceling = "canceling",
17+
disabled = "disabled",
18+
queued = "queued",
19+
loading = "loading",
2020
}
2121

2222
export type ButtonMapping = {

0 commit comments

Comments
 (0)