Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitef70165

Browse files
authored
feat: addorphan option to workspace delete in UI (#10654)
* added workspace delete dialog* added stories and tests* PR review* fix flake* fixed stories
1 parent4f08330 commitef70165

File tree

10 files changed

+319
-28
lines changed

10 files changed

+319
-28
lines changed

‎docs/workspaces.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,12 @@ though the exact behavior depends on the template. For more information, see
115115
>You can use`coder show <workspace-name>` to see which resources are
116116
>persistent and which are ephemeral.
117117
118-
When a workspace is deleted, all of the workspace's resources are deleted.
118+
Typically, when a workspace is deleted, all of the workspace's resources are
119+
deleted along with it. Rarely, one may wish to delete a workspace without
120+
deleting its resources, e.g. a workspace in a broken state. Users with the
121+
Template Admin role have the option to do so both in the UI, and also in the CLI
122+
by running the`delete` command with the`--orphan` flag. This option should be
123+
considered cautiously as orphaning may lead to unaccounted cloud resources.
119124

120125
##Repairing workspaces
121126

‎site/src/api/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -545,11 +545,11 @@ export const stopWorkspace = (
545545

546546
exportconstdeleteWorkspace=(
547547
workspaceId:string,
548-
logLevel?:TypesGen.CreateWorkspaceBuildRequest["log_level"],
548+
options?:Pick<TypesGen.CreateWorkspaceBuildRequest,"log_level"&"orphan">,
549549
)=>
550550
postWorkspaceBuild(workspaceId,{
551551
transition:"delete",
552-
log_level:logLevel,
552+
...options,
553553
});
554554

555555
exportconstcancelWorkspaceBuild=async(

‎site/src/components/Dialogs/Dialog.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const DialogActionButtons: React.FC<DialogActionButtonsProps> = ({
5959
disabled={disabled}
6060
type="submit"
6161
css={[
62-
type==="delete"&&styles.errorButton,
62+
type==="delete"&&styles.warningButton,
6363
type==="success"&&styles.successButton,
6464
]}
6565
>
@@ -71,26 +71,26 @@ export const DialogActionButtons: React.FC<DialogActionButtonsProps> = ({
7171
};
7272

7373
conststyles={
74-
errorButton:(theme)=>({
74+
warningButton:(theme)=>({
7575
"&.MuiButton-contained":{
76-
backgroundColor:colors.red[10],
77-
borderColor:colors.red[9],
76+
backgroundColor:colors.orange[12],
77+
borderColor:colors.orange[9],
7878

7979
"&:not(.MuiLoadingButton-loading)":{
8080
color:theme.palette.text.primary,
8181
},
8282

8383
"&:hover:not(:disabled)":{
84-
backgroundColor:colors.red[9],
85-
borderColor:colors.red[9],
84+
backgroundColor:colors.orange[9],
85+
borderColor:colors.orange[9],
8686
},
8787

8888
"&.Mui-disabled":{
89-
backgroundColor:colors.red[15],
90-
borderColor:colors.red[15],
89+
backgroundColor:colors.orange[14],
90+
borderColor:colors.orange[15],
9191

9292
"&:not(.MuiLoadingButton-loading)":{
93-
color:colors.red[9],
93+
color:colors.orange[12],
9494
},
9595
},
9696
},

‎site/src/components/MoreMenu/MoreMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export const MoreMenuItem = (
113113
{...menuItemProps}
114114
css={(theme)=>({
115115
fontSize:14,
116-
color:danger ?theme.palette.error.light :undefined,
116+
color:danger ?theme.palette.warning.light :undefined,
117117
"& .MuiSvgIcon-root":{
118118
width:16,
119119
height:16,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import{typeComponentProps}from"react";
2+
import{Meta,StoryObj}from"@storybook/react";
3+
import{WorkspaceDeleteDialog}from"./WorkspaceDeleteDialog";
4+
import{MockWorkspace}from"testHelpers/entities";
5+
6+
constmeta:Meta<typeofWorkspaceDeleteDialog>={
7+
title:"pages/WorkspacePage/WorkspaceDeleteDialog",
8+
component:WorkspaceDeleteDialog,
9+
};
10+
11+
exportdefaultmeta;
12+
typeStory=StoryObj<typeofWorkspaceDeleteDialog>;
13+
14+
constargs:ComponentProps<typeofWorkspaceDeleteDialog>={
15+
workspace:MockWorkspace,
16+
canUpdateTemplate:false,
17+
isOpen:true,
18+
onCancel:()=>{},
19+
onConfirm:()=>{},
20+
workspaceBuildDateStr:"2 days ago",
21+
};
22+
23+
exportconstNotTemplateAdmin:Story={
24+
args,
25+
};
26+
27+
exportconstTemplateAdmin:Story={
28+
args:{
29+
...args,
30+
canUpdateTemplate:true,
31+
},
32+
};
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import{Workspace,CreateWorkspaceBuildRequest}from"api/typesGenerated";
2+
import{useId,useState,FormEvent}from"react";
3+
import{ConfirmDialog}from"components/Dialogs/ConfirmDialog/ConfirmDialog";
4+
import{typeInterpolation,typeTheme}from"@emotion/react";
5+
import{colors}from"theme/colors";
6+
importTextFieldfrom"@mui/material/TextField";
7+
import{docs}from"utils/docs";
8+
importLinkfrom"@mui/material/Link";
9+
importCheckboxfrom"@mui/material/Checkbox";
10+
11+
conststyles={
12+
workspaceInfo:(theme)=>({
13+
display:"flex",
14+
justifyContent:"space-between",
15+
backgroundColor:colors.gray[14],
16+
border:`1px solid${theme.palette.divider}`,
17+
borderRadius:8,
18+
padding:12,
19+
marginBottom:20,
20+
lineHeight:"1.3em",
21+
22+
"& .name":{
23+
fontSize:18,
24+
fontWeight:800,
25+
color:theme.palette.text.primary,
26+
},
27+
28+
"& .label":{
29+
fontSize:11,
30+
color:theme.palette.text.secondary,
31+
},
32+
33+
"& .info":{
34+
fontSize:14,
35+
fontWeight:500,
36+
color:theme.palette.text.primary,
37+
},
38+
}),
39+
orphanContainer:()=>({
40+
marginTop:24,
41+
display:"flex",
42+
backgroundColor:colors.orange[15],
43+
justifyContent:"space-between",
44+
border:`1px solid${colors.orange[11]}`,
45+
borderRadius:8,
46+
padding:12,
47+
lineHeight:"18px",
48+
49+
"& .option":{
50+
color:colors.orange[11],
51+
"&.Mui-checked":{
52+
color:colors.orange[11],
53+
},
54+
},
55+
56+
"& .info":{
57+
fontSize:"14px",
58+
color:colors.orange[10],
59+
fontWeight:500,
60+
},
61+
}),
62+
}satisfiesRecord<string,Interpolation<Theme>>;
63+
64+
interfaceWorkspaceDeleteDialogProps{
65+
workspace:Workspace;
66+
canUpdateTemplate:boolean;
67+
isOpen:boolean;
68+
onCancel:()=>void;
69+
onConfirm:(arg:CreateWorkspaceBuildRequest["orphan"])=>void;
70+
workspaceBuildDateStr:string;
71+
}
72+
exportconstWorkspaceDeleteDialog=(props:WorkspaceDeleteDialogProps)=>{
73+
const{
74+
workspace,
75+
canUpdateTemplate,
76+
isOpen,
77+
onCancel,
78+
onConfirm,
79+
workspaceBuildDateStr,
80+
}=props;
81+
consthookId=useId();
82+
const[userConfirmationText,setUserConfirmationText]=useState("");
83+
const[orphanWorkspace,setOrphanWorkspace]=
84+
useState<CreateWorkspaceBuildRequest["orphan"]>(false);
85+
const[isFocused,setIsFocused]=useState(false);
86+
87+
constdeletionConfirmed=workspace.name===userConfirmationText;
88+
constonSubmit=(event:FormEvent)=>{
89+
event.preventDefault();
90+
if(deletionConfirmed){
91+
onConfirm(orphanWorkspace);
92+
}
93+
};
94+
95+
consthasError=!deletionConfirmed&&userConfirmationText.length>0;
96+
constdisplayErrorMessage=hasError&&!isFocused;
97+
constinputColor=hasError ?"error" :"primary";
98+
99+
return(
100+
<ConfirmDialog
101+
type="delete"
102+
hideCancel={false}
103+
open={isOpen}
104+
title="Delete Workspace"
105+
onConfirm={()=>onConfirm(orphanWorkspace)}
106+
onClose={onCancel}
107+
disabled={!deletionConfirmed}
108+
description={
109+
<>
110+
<divcss={styles.workspaceInfo}>
111+
<div>
112+
<pclassName="name">{workspace.name}</p>
113+
<pclassName="label">workspace</p>
114+
</div>
115+
<div>
116+
<pclassName="info">{workspaceBuildDateStr}</p>
117+
<pclassName="label">created</p>
118+
</div>
119+
</div>
120+
121+
<p>Deleting this workspace is irreversible!</p>
122+
<p>
123+
Type &ldquo;<strong>{workspace.name}</strong>&ldquo; below to
124+
confirm:
125+
</p>
126+
127+
<formonSubmit={onSubmit}>
128+
<TextField
129+
fullWidth
130+
autoFocus
131+
css={{marginTop:32}}
132+
name="confirmation"
133+
autoComplete="off"
134+
id={`${hookId}-confirm`}
135+
placeholder={workspace.name}
136+
value={userConfirmationText}
137+
onChange={(event)=>setUserConfirmationText(event.target.value)}
138+
onFocus={()=>setIsFocused(true)}
139+
onBlur={()=>setIsFocused(false)}
140+
label="Workspace name"
141+
color={inputColor}
142+
error={displayErrorMessage}
143+
helperText={
144+
displayErrorMessage&&
145+
`${userConfirmationText} does not match the name of this workspace`
146+
}
147+
InputProps={{color:inputColor}}
148+
inputProps={{
149+
"data-testid":"delete-dialog-name-confirmation",
150+
}}
151+
/>
152+
{canUpdateTemplate&&(
153+
<divcss={styles.orphanContainer}>
154+
<divcss={{flexDirection:"column"}}>
155+
<Checkbox
156+
id="orphan_resources"
157+
size="small"
158+
color="warning"
159+
onChange={()=>{
160+
setOrphanWorkspace(!orphanWorkspace);
161+
}}
162+
className="option"
163+
name="orphan_resources"
164+
checked={orphanWorkspace}
165+
data-testid="orphan-checkbox"
166+
/>
167+
</div>
168+
<divcss={{flexDirection:"column"}}>
169+
<pclassName="info">Orphan resources</p>
170+
<spancss={{fontSize:"11px"}}>
171+
Skip resource cleanup. Resources such as volumes and virtual
172+
machines will not be destroyed.&nbsp;
173+
<Link
174+
href={docs("/workspaces#workspace-resources")}
175+
target="_blank"
176+
rel="noreferrer"
177+
>
178+
Learn more...
179+
</Link>
180+
</span>
181+
</div>
182+
</div>
183+
)}
184+
</form>
185+
</>
186+
}
187+
/>
188+
);
189+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export*from"./WorkspaceDeleteDialog";

‎site/src/pages/WorkspacePage/WorkspacePage.test.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
MockTemplateVersion3,
1616
MockUser,
1717
MockDeploymentConfig,
18+
MockWorkspaceBuildDelete,
1819
}from"testHelpers/entities";
1920
import*asapifrom"api/api";
2021
import{renderWithAuth}from"testHelpers/renderHelpers";
@@ -90,7 +91,7 @@ describe("WorkspacePage", () => {
9091

9192
// Get dialog and confirm
9293
constdialog=awaitscreen.findByTestId("dialog");
93-
constlabelText="Name of the workspace to delete";
94+
constlabelText="Workspace name";
9495
consttextField=within(dialog).getByLabelText(labelText);
9596
awaituser.type(textField,MockWorkspace.name);
9697
constconfirmButton=within(dialog).getByRole("button",{
@@ -101,6 +102,62 @@ describe("WorkspacePage", () => {
101102
expect(deleteWorkspaceMock).toBeCalled();
102103
});
103104

105+
it("orphans the workspace on delete if option is selected",async()=>{
106+
constuser=userEvent.setup({delay:0});
107+
108+
// set permissions
109+
server.use(
110+
rest.post("/api/v2/authcheck",async(req,res,ctx)=>{
111+
returnres(
112+
ctx.status(200),
113+
ctx.json({
114+
updateTemplates:true,
115+
updateWorkspace:true,
116+
updateTemplate:true,
117+
}),
118+
);
119+
}),
120+
);
121+
122+
constdeleteWorkspaceMock=jest
123+
.spyOn(api,"deleteWorkspace")
124+
.mockResolvedValueOnce(MockWorkspaceBuildDelete);
125+
awaitrenderWorkspacePage();
126+
127+
// open the workspace action popover so we have access to all available ctas
128+
consttrigger=screen.getByTestId("workspace-options-button");
129+
awaituser.click(trigger);
130+
131+
// Click on delete
132+
constbutton=awaitscreen.findByTestId("delete-button");
133+
awaituser.click(button);
134+
135+
// Get dialog and enter confirmation text
136+
constdialog=awaitscreen.findByTestId("dialog");
137+
constlabelText="Workspace name";
138+
consttextField=within(dialog).getByLabelText(labelText);
139+
awaituser.type(textField,MockWorkspace.name);
140+
141+
// check orphan option
142+
constorphanCheckbox=within(
143+
screen.getByTestId("orphan-checkbox"),
144+
).getByRole("checkbox");
145+
146+
awaituser.click(orphanCheckbox);
147+
148+
// confirm
149+
constconfirmButton=within(dialog).getByRole("button",{
150+
name:"Delete",
151+
hidden:false,
152+
});
153+
awaituser.click(confirmButton);
154+
// arguments are workspace.name, log level (undefined), and orphan
155+
expect(deleteWorkspaceMock).toBeCalledWith(MockWorkspace.id,{
156+
log_level:undefined,
157+
orphan:true,
158+
});
159+
});
160+
104161
it("requests a start job when the user presses Start",async()=>{
105162
server.use(
106163
rest.get(

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp