Skip to content

Commit 6d95145

Browse files
authored
Feat: delete template button (#3781)
* Add api call * Extract DropDownButton * Start adding DropdownButton to Template page * Move stories to dropdown button * Format * Update xservice to delete * Deletion flow * Format * Move ErrorSummary for consistency * RBAC (unfinished) and style tweak * Format * Test rbac * Format * Move ErrorSummary under PageHeader in workspace and template * Format * Replace hook with onBlur * Make style arg optional * Format
1 parent 6826b97 commit 6d95145

File tree

20 files changed

+603
-393
lines changed

20 files changed

+603
-393
lines changed

site/src/api/api.ts

+5
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ export const updateTemplateMeta = async (
153153
return response.data
154154
}
155155

156+
export const deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => {
157+
const response = await axios.delete<TypesGen.Template>(`/api/v2/templates/${templateId}`)
158+
return response.data
159+
}
160+
156161
export const getWorkspace = async (
157162
workspaceId: string,
158163
params?: TypesGen.WorkspaceOptions,

site/src/components/DropdownArrows/DropdownArrows.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ interface ArrowProps {
2121

2222
export const OpenDropdown: FC<ArrowProps> = ({ margin = true }) => {
2323
const styles = useStyles({ margin })
24-
return <KeyboardArrowDown className={styles.arrowIcon} />
24+
return <KeyboardArrowDown aria-label="open-dropdown" className={styles.arrowIcon} />
2525
}
2626

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

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

+4-7
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/Workspace/Workspace.tsx

+37-40
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,19 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
6868
const styles = useStyles()
6969
const navigate = useNavigate()
7070

71+
const buildError = workspaceErrors[WorkspaceErrors.BUILD_ERROR] ? (
72+
<ErrorSummary error={workspaceErrors[WorkspaceErrors.BUILD_ERROR]} dismissible />
73+
) : (
74+
<></>
75+
)
76+
const cancellationError = workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR] ? (
77+
<ErrorSummary error={workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR]} dismissible />
78+
) : (
79+
<></>
80+
)
81+
7182
return (
7283
<Margins>
73-
<Stack spacing={1}>
74-
{workspaceErrors[WorkspaceErrors.BUILD_ERROR] ? (
75-
<ErrorSummary error={workspaceErrors[WorkspaceErrors.BUILD_ERROR]} dismissible />
76-
) : (
77-
<></>
78-
)}
79-
{workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR] ? (
80-
<ErrorSummary error={workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR]} dismissible />
81-
) : (
82-
<></>
83-
)}
84-
</Stack>
8584
<PageHeader
8685
actions={
8786
<Stack direction="row" spacing={1} className={styles.actions}>
@@ -109,39 +108,37 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
109108
<PageHeaderSubtitle>{workspace.owner_name}</PageHeaderSubtitle>
110109
</PageHeader>
111110

112-
<Stack direction="row" spacing={3}>
113-
<Stack direction="column" className={styles.firstColumnSpacer} spacing={3}>
114-
<WorkspaceScheduleBanner
115-
isLoading={bannerProps.isLoading}
116-
onExtend={bannerProps.onExtend}
117-
workspace={workspace}
118-
/>
111+
<Stack direction="column" className={styles.firstColumnSpacer} spacing={2.5}>
112+
{buildError}
113+
{cancellationError}
114+
115+
<WorkspaceScheduleBanner
116+
isLoading={bannerProps.isLoading}
117+
onExtend={bannerProps.onExtend}
118+
workspace={workspace}
119+
/>
119120

120-
<WorkspaceDeletedBanner
121+
<WorkspaceDeletedBanner workspace={workspace} handleClick={() => navigate(`/templates`)} />
122+
123+
<WorkspaceStats workspace={workspace} handleUpdate={handleUpdate} />
124+
125+
{!!resources && !!resources.length && (
126+
<Resources
127+
resources={resources}
128+
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
121129
workspace={workspace}
122-
handleClick={() => navigate(`/templates`)}
130+
canUpdateWorkspace={canUpdateWorkspace}
131+
buildInfo={buildInfo}
123132
/>
133+
)}
124134

125-
<WorkspaceStats workspace={workspace} handleUpdate={handleUpdate} />
126-
127-
{!!resources && !!resources.length && (
128-
<Resources
129-
resources={resources}
130-
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
131-
workspace={workspace}
132-
canUpdateWorkspace={canUpdateWorkspace}
133-
buildInfo={buildInfo}
134-
/>
135+
<WorkspaceSection title="Logs" contentsProps={{ className: styles.timelineContents }}>
136+
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
137+
<ErrorSummary error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]} />
138+
) : (
139+
<BuildsTable builds={builds} className={styles.timelineTable} />
135140
)}
136-
137-
<WorkspaceSection title="Logs" contentsProps={{ className: styles.timelineContents }}>
138-
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
139-
<ErrorSummary error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]} />
140-
) : (
141-
<BuildsTable builds={builds} className={styles.timelineTable} />
142-
)}
143-
</WorkspaceSection>
144-
</Stack>
141+
</WorkspaceSection>
145142
</Stack>
146143
</Margins>
147144
)

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

-50
This file was deleted.

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

+1-1
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> = {}) => {

0 commit comments

Comments
 (0)