7
7
import { CommitContext , Workspace , WorkspaceInfo , WorkspaceInstance , WorkspaceInstanceConditions , WorkspaceInstancePhase } from '@gitpod/gitpod-protocol' ;
8
8
import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url' ;
9
9
import moment from 'moment' ;
10
- import React , { useState } from 'react' ;
10
+ import React , { useRef , useState } from 'react' ;
11
11
import ConfirmationModal from '../components/ConfirmationModal' ;
12
+ import Modal from '../components/Modal' ;
12
13
import { ContextMenuEntry } from '../components/ContextMenu' ;
13
14
import { Item , ItemField , ItemFieldContextMenu , ItemFieldIcon } from '../components/ItemsList' ;
14
15
import PendingChangesDropdown from '../components/PendingChangesDropdown' ;
15
16
import Tooltip from '../components/Tooltip' ;
16
17
import { WorkspaceModel } from './workspace-model' ;
18
+ import { getGitpodService } from '../service/service' ;
17
19
18
20
function getLabel ( state : WorkspaceInstancePhase , conditions ?: WorkspaceInstanceConditions ) {
19
21
if ( conditions ?. failed ) {
@@ -30,10 +32,15 @@ interface Props {
30
32
}
31
33
32
34
export function WorkspaceEntry ( { desc, model, isAdmin, stopWorkspace } : Props ) {
33
- const [ isModalVisible , setModalVisible ] = useState ( false ) ;
35
+ const [ isDeleteModalVisible , setDeleteModalVisible ] = useState ( false ) ;
36
+ const [ isRenameModalVisible , setRenameModalVisible ] = useState ( false ) ;
37
+ const renameInputRef = useRef < HTMLInputElement > ( null ) ;
38
+ const [ errorMessage , setErrorMessage ] = useState ( '' ) ;
34
39
const state : WorkspaceInstancePhase = desc . latestInstance ?. status ?. phase || 'stopped' ;
35
40
const currentBranch = desc . latestInstance ?. status . repo ?. branch || Workspace . getBranchName ( desc . workspace ) || '<unknown>' ;
36
41
const ws = desc . workspace ;
42
+ const [ workspaceDescription , setWsDescription ] = useState ( ws . description ) ;
43
+
37
44
const startUrl = new GitpodHostUrl ( window . location . href ) . with ( {
38
45
pathname : '/start/' ,
39
46
hash : '#' + ws . id
@@ -45,7 +52,16 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
45
52
{
46
53
title : 'Open' ,
47
54
href : startUrl . toString ( )
48
- } ] ;
55
+ } ,
56
+ {
57
+ title : 'Rename' ,
58
+ href : "" ,
59
+ onClick : ( ) => {
60
+ setRenameModalVisible ( true ) ;
61
+ }
62
+ } ,
63
+
64
+ ] ;
49
65
if ( state === 'running' ) {
50
66
menuEntries . push ( {
51
67
title : 'Stop' ,
@@ -78,13 +94,41 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
78
94
title : 'Delete' ,
79
95
customFontStyle : 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300' ,
80
96
onClick : ( ) => {
81
- setModalVisible ( true ) ;
97
+ setDeleteModalVisible ( true ) ;
82
98
}
83
99
}
84
100
) ;
85
101
}
86
102
const project = getProject ( ws ) ;
87
103
104
+ const updateWorkspaceDescription = async ( ) => {
105
+ // Need this check because ref is called twice
106
+ // https://reactjs.org/docs/refs-and-the-dom.html#caveats-with-callback-refs
107
+ if ( ! renameInputRef . current ) {
108
+ return ;
109
+ }
110
+
111
+ try {
112
+ if ( renameInputRef . current ! . value . length === 0 ) {
113
+ setErrorMessage ( 'Description must not be empty.' ) ;
114
+ return false ;
115
+ }
116
+
117
+ if ( renameInputRef . current ! . value . length > 250 ) {
118
+ setErrorMessage ( 'Description is too long for readability.' ) ;
119
+ return false ;
120
+ }
121
+
122
+ setWsDescription ( renameInputRef . current ! . value ) ;
123
+ await getGitpodService ( ) . server . setWorkspaceDescription ( ws . id , renameInputRef . current ! . value ) ;
124
+ setErrorMessage ( '' ) ;
125
+ setRenameModalVisible ( false ) ;
126
+ } catch ( error ) {
127
+ console . error ( error ) ;
128
+ window . alert ( "Something went wrong. Please try renaming again." ) ;
129
+ }
130
+ }
131
+
88
132
return < Item className = "whitespace-nowrap py-6 px-6" >
89
133
< ItemFieldIcon >
90
134
< WorkspaceStatusIndicator instance = { desc ?. latestInstance } />
@@ -96,7 +140,7 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
96
140
</ Tooltip >
97
141
</ ItemField >
98
142
< ItemField className = "w-4/12 flex flex-col" >
99
- < div className = "text-gray-500 dark:text-gray-400 overflow-ellipsis truncate" > { ws . description } </ div >
143
+ < div className = "text-gray-500 dark:text-gray-400 overflow-ellipsis truncate" > { workspaceDescription } </ div >
100
144
< a href = { ws . contextURL } >
101
145
< div className = "text-sm text-gray-400 dark:text-gray-500 overflow-ellipsis truncate hover:text-blue-600 dark:hover:text-blue-400" > { ws . contextURL } </ div >
102
146
</ a >
@@ -111,18 +155,37 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
111
155
</ Tooltip >
112
156
</ ItemField >
113
157
< ItemFieldContextMenu menuEntries = { menuEntries } />
114
- { isModalVisible && < ConfirmationModal
158
+ { isDeleteModalVisible && < ConfirmationModal
115
159
title = "Delete Workspace"
116
160
areYouSureText = "Are you sure you want to delete this workspace?"
117
161
children = { {
118
162
name : ws . id ,
119
163
description : ws . description ,
120
164
} }
121
165
buttonText = "Delete Workspace"
122
- visible = { isModalVisible }
123
- onClose = { ( ) => setModalVisible ( false ) }
166
+ visible = { isDeleteModalVisible }
167
+ onClose = { ( ) => setDeleteModalVisible ( false ) }
124
168
onConfirm = { ( ) => model . deleteWorkspace ( ws . id ) }
125
169
/> }
170
+ < Modal visible = { isRenameModalVisible } onClose = { ( ) => setRenameModalVisible ( false ) } onEnter = { ( ) => { updateWorkspaceDescription ( ) ; return isRenameModalVisible } } >
171
+ < h3 className = "mb-4" > Rename Workspace Description</ h3 >
172
+ < div className = "border-t border-b border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4 space-y-2" >
173
+ { errorMessage . length > 0 ?
174
+ < div className = "bg-gitpod-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2" >
175
+ { errorMessage }
176
+ </ div >
177
+ : null }
178
+ < input className = "w-full truncate" type = "text" defaultValue = { workspaceDescription } ref = { renameInputRef } />
179
+ < div className = "mt-1" >
180
+ < p className = "text-gray-500" > Change the description to make it easier to go back to a workspace.</ p >
181
+ < p className = "text-gray-500" > Workspace URLs and endpoints will remain the same.</ p >
182
+ </ div >
183
+ </ div >
184
+ < div className = "flex justify-end mt-6" >
185
+ < button className = "secondary" onClick = { ( ) => setRenameModalVisible ( false ) } > Cancel</ button >
186
+ < button className = "ml-2" type = "submit" onClick = { updateWorkspaceDescription } > Rename</ button >
187
+ </ div >
188
+ </ Modal >
126
189
</ Item > ;
127
190
}
128
191
@@ -134,7 +197,7 @@ export function getProject(ws: Workspace) {
134
197
}
135
198
}
136
199
137
- export function WorkspaceStatusIndicator ( { instance} : { instance ?: WorkspaceInstance } ) {
200
+ export function WorkspaceStatusIndicator ( { instance } : { instance ?: WorkspaceInstance } ) {
138
201
const state : WorkspaceInstancePhase = instance ?. status ?. phase || 'stopped' ;
139
202
const conditions = instance ?. status ?. conditions ;
140
203
let stateClassName = 'rounded-full w-3 h-3 text-sm align-middle' ;
0 commit comments