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

Commit0b59ed3

Browse files
authored
feat: ui autostop extension (#1987)
Resolves:#1460Summary:An 'Extend' CTA on workspace schedule banner is added so that a user canextend their workspace lease from the UI.Details:* feat: putWorkspaceExtension handler* refactor: TypesGen dflt import in workspace.ts* feat: defaultWorkspaceExtension utilImpact:This completes the UI<-->CLI parity epic in an MVP way. Of course, afuture improvement to make is extending by times other than the default90 minutes.
1 parent1a07d02 commit0b59ed3

File tree

10 files changed

+174
-14
lines changed

10 files changed

+174
-14
lines changed

‎site/src/api/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,10 @@ export const getWorkspaceBuildLogs = async (buildname: string): Promise<TypesGen
270270
constresponse=awaitaxios.get<TypesGen.ProvisionerJobLog[]>(`/api/v2/workspacebuilds/${buildname}/logs`)
271271
returnresponse.data
272272
}
273+
274+
exportconstputWorkspaceExtension=async(
275+
workspaceId:string,
276+
extendWorkspaceRequest:TypesGen.PutExtendWorkspaceRequest,
277+
):Promise<void>=>{
278+
awaitaxios.put(`/api/v2/workspaces/${workspaceId}/extend`,extendWorkspaceRequest)
279+
}

‎site/src/components/Workspace/Workspace.stories.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ const Template: Story<WorkspaceProps> = (args) => <Workspace {...args} />
1313

1414
exportconstStarted=Template.bind({})
1515
Started.args={
16+
bannerProps:{
17+
isLoading:false,
18+
onExtend:action("extend"),
19+
},
1620
workspace:Mocks.MockWorkspace,
1721
handleStart:action("start"),
1822
handleStop:action("stop"),

‎site/src/components/Workspace/Workspace.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
1313
import{WorkspaceStats}from"../WorkspaceStats/WorkspaceStats"
1414

1515
exportinterfaceWorkspaceProps{
16+
bannerProps:{
17+
isLoading?:boolean
18+
onExtend:()=>void
19+
}
1620
handleStart:()=>void
1721
handleStop:()=>void
1822
handleDelete:()=>void
@@ -28,6 +32,7 @@ export interface WorkspaceProps {
2832
* Workspace is the top-level component for viewing an individual workspace
2933
*/
3034
exportconstWorkspace:FC<WorkspaceProps>=({
35+
bannerProps,
3136
handleStart,
3237
handleStop,
3338
handleDelete,
@@ -54,6 +59,7 @@ export const Workspace: FC<WorkspaceProps> = ({
5459
{workspace.owner_name}
5560
</Typography>
5661
</div>
62+
5763
<WorkspaceActions
5864
workspace={workspace}
5965
handleStart={handleStart}
@@ -70,9 +76,16 @@ export const Workspace: FC<WorkspaceProps> = ({
7076

7177
<Stackdirection="row"spacing={3}>
7278
<Stackdirection="column"className={styles.firstColumnSpacer}spacing={3}>
73-
<WorkspaceScheduleBannerworkspace={workspace}/>
79+
<WorkspaceScheduleBanner
80+
isLoading={bannerProps.isLoading}
81+
onExtend={bannerProps.onExtend}
82+
workspace={workspace}
83+
/>
84+
7485
<WorkspaceStatsworkspace={workspace}/>
86+
7587
<Resourcesresources={resources}getResourcesError={getResourcesError}workspace={workspace}/>
88+
7689
<WorkspaceSectiontitle="Timeline"contentsProps={{className:styles.timelineContents}}>
7790
<BuildsTablebuilds={builds}className={styles.timelineTable}/>
7891
</WorkspaceSection>

‎site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.stories.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import{action}from"@storybook/addon-actions"
12
import{Story}from"@storybook/react"
23
importdayjsfrom"dayjs"
34
importutcfrom"dayjs/plugin/utc"
@@ -15,8 +16,11 @@ const Template: Story<WorkspaceScheduleBannerProps> = (args) => <WorkspaceSchedu
1516

1617
exportconstExample=Template.bind({})
1718
Example.args={
19+
isLoading:false,
20+
onExtend:action("extend"),
1821
workspace:{
1922
...Mocks.MockWorkspace,
23+
2024
latest_build:{
2125
...Mocks.MockWorkspaceBuild,
2226
deadline:dayjs().utc().format(),
@@ -26,6 +30,13 @@ Example.args = {
2630
},
2731
transition:"start",
2832
},
29-
ttl:2*60*60*1000*1_000_000,// 2 hours
33+
34+
ttl_ms:2*60*60*1000,// 2 hours
3035
},
3136
}
37+
38+
exportconstLoading=Template.bind({})
39+
Loading.args={
40+
...Example.args,
41+
isLoading:true,
42+
}

‎site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
importButtonfrom"@material-ui/core/Button"
12
importAlertfrom"@material-ui/lab/Alert"
23
importAlertTitlefrom"@material-ui/lab/AlertTitle"
34
importdayjsfrom"dayjs"
@@ -11,10 +12,13 @@ dayjs.extend(utc)
1112
dayjs.extend(isSameOrBefore)
1213

1314
exportconstLanguage={
15+
bannerAction:"Extend",
1416
bannerTitle:"Your workspace is scheduled to automatically shut down soon.",
1517
}
1618

1719
exportinterfaceWorkspaceScheduleBannerProps{
20+
isLoading?:boolean
21+
onExtend:()=>void
1822
workspace:TypesGen.Workspace
1923
}
2024

@@ -31,12 +35,19 @@ export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
3135
}
3236
}
3337

34-
exportconstWorkspaceScheduleBanner:FC<WorkspaceScheduleBannerProps>=({ workspace})=>{
38+
exportconstWorkspaceScheduleBanner:FC<WorkspaceScheduleBannerProps>=({isLoading, onExtend,workspace})=>{
3539
if(!shouldDisplay(workspace)){
3640
returnnull
3741
}else{
3842
return(
39-
<Alertseverity="warning">
43+
<Alert
44+
action={
45+
<Buttoncolor="inherit"disabled={isLoading}onClick={onExtend}size="small">
46+
{Language.bannerAction}
47+
</Button>
48+
}
49+
severity="warning"
50+
>
4051
<AlertTitle>{Language.bannerTitle}</AlertTitle>
4152
</Alert>
4253
)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Stack } from "../../components/Stack/Stack"
99
import{Workspace}from"../../components/Workspace/Workspace"
1010
import{firstOrItem}from"../../util/array"
1111
import{workspaceMachine}from"../../xServices/workspace/workspaceXService"
12+
import{workspaceScheduleBannerMachine}from"../../xServices/workspaceSchedule/workspaceScheduleBannerXService"
1213

1314
exportconstWorkspacePage:React.FC=()=>{
1415
const{workspace:workspaceQueryParam}=useParams()
@@ -18,6 +19,8 @@ export const WorkspacePage: React.FC = () => {
1819
const[workspaceState,workspaceSend]=useMachine(workspaceMachine)
1920
const{ workspace, resources, getWorkspaceError, getResourcesError, builds}=workspaceState.context
2021

22+
const[bannerState,bannerSend]=useMachine(workspaceScheduleBannerMachine)
23+
2124
/**
2225
* Get workspace, template, and organization on mount and whenever workspaceId changes.
2326
* workspaceSend should not change.
@@ -36,6 +39,12 @@ export const WorkspacePage: React.FC = () => {
3639
<Stackspacing={4}>
3740
<>
3841
<Workspace
42+
bannerProps={{
43+
isLoading:bannerState.hasTag("loading"),
44+
onExtend:()=>{
45+
bannerSend({type:"EXTEND_DEADLINE_DEFAULT",workspaceId:workspace.id})
46+
},
47+
}}
3948
workspace={workspace}
4049
handleStart={()=>workspaceSend("START")}
4150
handleStop={()=>workspaceSend("STOP")}

‎site/src/testHelpers/handlers.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ export const handlers = [
109109
rest.put("/api/v2/workspaces/:workspaceId/ttl",async(req,res,ctx)=>{
110110
returnres(ctx.status(200))
111111
}),
112+
rest.put("/api/v2/workspaces/:workspaceId/extend",async(req,res,ctx)=>{
113+
returnres(ctx.status(200))
114+
}),
115+
116+
// workspace builds
112117
rest.post("/api/v2/workspaces/:workspaceId/builds",async(req,res,ctx)=>{
113118
const{ transition}=req.bodyasCreateWorkspaceBuildRequest
114119
consttransitionToBuild={
@@ -122,8 +127,6 @@ export const handlers = [
122127
rest.get("/api/v2/workspaces/:workspaceId/builds",async(req,res,ctx)=>{
123128
returnres(ctx.status(200),ctx.json(M.MockBuilds))
124129
}),
125-
126-
// workspace builds
127130
rest.get("/api/v2/workspacebuilds/:workspaceBuildId",(req,res,ctx)=>{
128131
returnres(ctx.status(200),ctx.json(M.MockWorkspaceBuild))
129132
}),

‎site/src/util/workspace.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
importdayjsfrom"dayjs"
12
import*asTypesGenfrom"../api/typesGenerated"
23
import*asMocksfrom"../testHelpers/entities"
3-
import{isWorkspaceOn}from"./workspace"
4+
import{defaultWorkspaceExtension,isWorkspaceOn}from"./workspace"
45

56
describe("util > workspace",()=>{
67
describe("isWorkspaceOn",()=>{
@@ -40,4 +41,26 @@ describe("util > workspace", () => {
4041
expect(isWorkspaceOn(workspace)).toBe(isOn)
4142
})
4243
})
44+
45+
describe("defaultWorkspaceExtension",()=>{
46+
it.each<[string,TypesGen.PutExtendWorkspaceRequest]>([
47+
[
48+
"2022-06-02T14:56:34Z",
49+
{
50+
deadline:"2022-06-02T16:26:34Z",
51+
},
52+
],
53+
54+
// This case is the same as above, but in a different timezone to prove
55+
// that UTC conversion for deadline works as expected
56+
[
57+
"2022-06-02T10:56:20-04:00",
58+
{
59+
deadline:"2022-06-02T16:26:20Z",
60+
},
61+
],
62+
])(`defaultWorkspaceExtension(%p) returns %p`,(startTime,request)=>{
63+
expect(defaultWorkspaceExtension(dayjs(startTime))).toEqual(request)
64+
})
65+
})
4366
})

‎site/src/util/workspace.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import{Theme}from"@material-ui/core/styles"
22
importdayjsfrom"dayjs"
3+
importutcfrom"dayjs/plugin/utc"
34
import{WorkspaceBuildTransition}from"../api/types"
4-
import{Workspace,WorkspaceAgent,WorkspaceBuild}from"../api/typesGenerated"
5+
import*asTypesGenfrom"../api/typesGenerated"
6+
7+
dayjs.extend(utc)
58

69
exporttypeWorkspaceStatus=
710
|"queued"
@@ -29,7 +32,7 @@ const succeededToStatus: Record<WorkspaceBuildTransition, WorkspaceStatus> = {
2932
}
3033

3134
// Converts a workspaces status to a human-readable form.
32-
exportconstgetWorkspaceStatus=(workspaceBuild?:WorkspaceBuild):WorkspaceStatus=>{
35+
exportconstgetWorkspaceStatus=(workspaceBuild?:TypesGen.WorkspaceBuild):WorkspaceStatus=>{
3336
consttransition=workspaceBuild?.transitionasWorkspaceBuildTransition
3437
constjobStatus=workspaceBuild?.job.status
3538
switch(jobStatus){
@@ -67,7 +70,7 @@ export const DisplayStatusLanguage = {
6770
// Localize workspace status and provide corresponding color from theme
6871
exportconstgetDisplayStatus=(
6972
theme:Theme,
70-
build:WorkspaceBuild,
73+
build:TypesGen.WorkspaceBuild,
7174
):{
7275
color:string
7376
status:string
@@ -133,7 +136,7 @@ export const getDisplayStatus = (
133136
thrownewError("unknown status "+status)
134137
}
135138

136-
exportconstgetWorkspaceBuildDurationInSeconds=(build:WorkspaceBuild):number|undefined=>{
139+
exportconstgetWorkspaceBuildDurationInSeconds=(build:TypesGen.WorkspaceBuild):number|undefined=>{
137140
constisCompleted=build.job.started_at&&build.job.completed_at
138141

139142
if(!isCompleted){
@@ -145,7 +148,10 @@ export const getWorkspaceBuildDurationInSeconds = (build: WorkspaceBuild): numbe
145148
returncompletedAt.diff(startedAt,"seconds")
146149
}
147150

148-
exportconstdisplayWorkspaceBuildDuration=(build:WorkspaceBuild,inProgressLabel="In progress"):string=>{
151+
exportconstdisplayWorkspaceBuildDuration=(
152+
build:TypesGen.WorkspaceBuild,
153+
inProgressLabel="In progress",
154+
):string=>{
149155
constduration=getWorkspaceBuildDurationInSeconds(build)
150156
returnduration ?`${duration} seconds` :inProgressLabel
151157
}
@@ -158,7 +164,7 @@ export const DisplayAgentStatusLanguage = {
158164

159165
exportconstgetDisplayAgentStatus=(
160166
theme:Theme,
161-
agent:WorkspaceAgent,
167+
agent:TypesGen.WorkspaceAgent,
162168
):{
163169
color:string
164170
status:string
@@ -187,8 +193,17 @@ export const getDisplayAgentStatus = (
187193
}
188194
}
189195

190-
exportconstisWorkspaceOn=(workspace:Workspace):boolean=>{
196+
exportconstisWorkspaceOn=(workspace:TypesGen.Workspace):boolean=>{
191197
consttransition=workspace.latest_build.transition
192198
conststatus=workspace.latest_build.job.status
193199
returntransition==="start"&&status==="succeeded"
194200
}
201+
202+
exportconstdefaultWorkspaceExtension=(__startDate?:dayjs.Dayjs):TypesGen.PutExtendWorkspaceRequest=>{
203+
constnow=__startDate ?dayjs(__startDate) :dayjs()
204+
constNinetyMinutesFromNow=now.add(90,"minutes").utc()
205+
206+
return{
207+
deadline:NinetyMinutesFromNow.format(),
208+
}
209+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
*@fileoverview workspaceScheduleBanner is an xstate machine backing a form,
3+
* presented as an Alert/banner, for reactively extending a workspace schedule.
4+
*/
5+
import{createMachine}from"xstate"
6+
import*asAPIfrom"../../api/api"
7+
import{displayError,displaySuccess}from"../../components/GlobalSnackbar/utils"
8+
import{defaultWorkspaceExtension}from"../../util/workspace"
9+
10+
exportconstLanguage={
11+
errorExtension:"Failed to extend workspace deadline.",
12+
successExtension:"Successfully extended workspace deadline.",
13+
}
14+
15+
exporttypeWorkspaceScheduleBannerEvent={type:"EXTEND_DEADLINE_DEFAULT";workspaceId:string}
16+
17+
exportconstworkspaceScheduleBannerMachine=createMachine(
18+
{
19+
tsTypes:{}asimport("./workspaceScheduleBannerXService.typegen").Typegen0,
20+
schema:{
21+
events:{}asWorkspaceScheduleBannerEvent,
22+
},
23+
id:"workspaceScheduleBannerState",
24+
initial:"idle",
25+
states:{
26+
idle:{
27+
on:{
28+
EXTEND_DEADLINE_DEFAULT:"extendingDeadline",
29+
},
30+
},
31+
extendingDeadline:{
32+
invoke:{
33+
src:"extendDeadlineDefault",
34+
id:"extendDeadlineDefault",
35+
onDone:{
36+
target:"idle",
37+
actions:"displaySuccessMessage",
38+
},
39+
onError:{
40+
target:"idle",
41+
actions:"displayFailureMessage",
42+
},
43+
},
44+
tags:"loading",
45+
},
46+
},
47+
},
48+
{
49+
actions:{
50+
displayFailureMessage:()=>{
51+
displayError(Language.errorExtension)
52+
},
53+
displaySuccessMessage:()=>{
54+
displaySuccess(Language.successExtension)
55+
},
56+
},
57+
58+
services:{
59+
extendDeadlineDefault:async(_,event)=>{
60+
awaitAPI.putWorkspaceExtension(event.workspaceId,defaultWorkspaceExtension())
61+
},
62+
},
63+
},
64+
)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp