6
6
7
7
import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb" ;
8
8
import { useContext , useEffect , useState } from "react" ;
9
- import { Redirect , useHistory , useLocation } from "react-router" ;
9
+ import { Redirect , useHistory , useLocation , useParams } from "react-router" ;
10
10
import { Link } from "react-router-dom" ;
11
11
import CheckBox from "../components/CheckBox" ;
12
+ import Modal from "../components/Modal" ;
12
13
import { FeatureFlagContext } from "../contexts/FeatureFlagContext" ;
13
14
import { personalAccessTokensService } from "../service/public-api" ;
14
15
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu" ;
@@ -42,17 +43,98 @@ interface EditPATData {
42
43
expirationDate : Date ;
43
44
}
44
45
46
+ interface TokenModalProps {
47
+ token : PersonalAccessToken ;
48
+ title : string ;
49
+ description : string ;
50
+ descriptionImportant : string ;
51
+ actionDescription : string ;
52
+ onSave ?: ( ) => void ;
53
+ onClose ?: ( ) => void ;
54
+ }
55
+
56
+ enum Method {
57
+ Create = "CREATED" ,
58
+ Regerenrate = "REGENERATED" ,
59
+ }
60
+
61
+ export function ShowTokenModal ( props : TokenModalProps ) {
62
+ const onEnter = ( ) => {
63
+ if ( props . onSave ) {
64
+ props . onSave ( ) ;
65
+ }
66
+ return true ;
67
+ } ;
68
+
69
+ return (
70
+ < Modal
71
+ title = { props . title }
72
+ buttons = { [
73
+ < button
74
+ className = "secondary"
75
+ onClick = { ( ) => {
76
+ props . onClose && props . onClose ( ) ;
77
+ } }
78
+ >
79
+ Cancel
80
+ </ button > ,
81
+ < button onClick = { props . onSave } > { props . actionDescription } </ button > ,
82
+ ] }
83
+ visible = { true }
84
+ onClose = { ( ) => {
85
+ props . onClose && props . onClose ( ) ;
86
+ } }
87
+ onEnter = { onEnter }
88
+ >
89
+ < div className = "text-gray-500 dark:text-gray-400 text-md" >
90
+ < span > { props . description } </ span > < span className = "font-semibold" > { props . descriptionImportant } </ span >
91
+ </ div >
92
+ < div className = "p-4 mt-2 rounded-xl bg-gray-50 dark:bg-gray-800" >
93
+ < div className = "font-semibold text-gray-700 dark:text-gray-200" > { props . token . name } </ div >
94
+ < div className = "font-medium text-gray-400 dark:text-gray-300" >
95
+ Expires on{ " " }
96
+ { Intl . DateTimeFormat ( "en-US" , { dateStyle : "long" } ) . format ( props . token . expirationTime ?. toDate ( ) ) }
97
+ </ div >
98
+ </ div >
99
+ </ Modal >
100
+ ) ;
101
+ }
102
+
45
103
export function PersonalAccessTokenCreateView ( ) {
46
104
const { enablePersonalAccessTokens } = useContext ( FeatureFlagContext ) ;
47
105
106
+ const params = useParams ( ) ;
48
107
const history = useHistory ( ) ;
108
+
109
+ const [ editTokenID , setEditTokenID ] = useState < null | string > ( null ) ;
49
110
const [ errorMsg , setErrorMsg ] = useState ( "" ) ;
50
111
const [ value , setValue ] = useState < EditPATData > ( {
51
112
name : "" ,
52
113
expirationDays : 30 ,
53
114
expirationDate : new Date ( Date . now ( ) + 30 * 24 * 60 * 60 * 1000 ) ,
54
115
} ) ;
55
116
117
+ const [ showModal , setShowModal ] = useState < boolean > ( false ) ;
118
+ const [ modalData , setModalData ] = useState < PersonalAccessToken > ( ) ;
119
+
120
+ useEffect ( ( ) => {
121
+ ( async ( ) => {
122
+ try {
123
+ const { tokenId } = params as { tokenId : string } ;
124
+ if ( ! tokenId ) {
125
+ return ;
126
+ }
127
+ setEditTokenID ( tokenId ) ;
128
+ const resp = await personalAccessTokensService . getPersonalAccessToken ( { id : tokenId } ) ;
129
+ const token = resp . token ;
130
+ value . name = token ! . name ;
131
+ setModalData ( token ! ) ;
132
+ } catch ( e ) {
133
+ setErrorMsg ( e . message ) ;
134
+ }
135
+ } ) ( ) ;
136
+ } , [ ] ) ;
137
+
56
138
const update = ( change : Partial < EditPATData > ) => {
57
139
if ( change . expirationDays ) {
58
140
change . expirationDate = new Date ( Date . now ( ) + change . expirationDays * 24 * 60 * 60 * 1000 ) ;
@@ -61,7 +143,32 @@ export function PersonalAccessTokenCreateView() {
61
143
setValue ( { ...value , ...change } ) ;
62
144
} ;
63
145
146
+ const regenerate = async ( ) => {
147
+ if ( ! editTokenID ) {
148
+ return ;
149
+ }
150
+ try {
151
+ const resp = await personalAccessTokensService . regeneratePersonalAccessToken ( {
152
+ id : editTokenID ,
153
+ expirationTime : Timestamp . fromDate ( value . expirationDate ) ,
154
+ } ) ;
155
+ history . push ( {
156
+ pathname : settingsPathPersonalAccessTokens ,
157
+ state : {
158
+ method : Method . Regerenrate ,
159
+ data : resp . token ,
160
+ } as TokenInfo ,
161
+ } ) ;
162
+ } catch ( e ) {
163
+ setErrorMsg ( e . message ) ;
164
+ }
165
+ } ;
166
+
64
167
const createToken = async ( ) => {
168
+ if ( editTokenID ) {
169
+ setErrorMsg ( "Edit token is not implemented yet" ) ;
170
+ return ;
171
+ }
65
172
if ( value . name . length < 3 ) {
66
173
setErrorMsg ( "Token Name should have at least three characters." ) ;
67
174
return ;
@@ -77,9 +184,9 @@ export function PersonalAccessTokenCreateView() {
77
184
history . push ( {
78
185
pathname : settingsPathPersonalAccessTokens ,
79
186
state : {
80
- method : "CREATED" ,
187
+ method : Method . Create ,
81
188
data : resp . token ,
82
- } ,
189
+ } as TokenInfo ,
83
190
} ) ;
84
191
} catch ( e ) {
85
192
setErrorMsg ( e . message ) ;
@@ -93,7 +200,7 @@ export function PersonalAccessTokenCreateView() {
93
200
return (
94
201
< div >
95
202
< PageWithSettingsSubMenu title = "Access Tokens" subtitle = "Manage your personal access tokens." >
96
- < div className = "mb-4" >
203
+ < div className = "mb-4 flex gap-2 " >
97
204
< Link to = { settingsPathPersonalAccessTokens } >
98
205
< button className = "secondary" >
99
206
< div className = "flex place-content-center" >
@@ -102,6 +209,14 @@ export function PersonalAccessTokenCreateView() {
102
209
</ div >
103
210
</ button >
104
211
</ Link >
212
+ { editTokenID && (
213
+ < button
214
+ className = "danger bg-red-50 dark:bg-red-600 text-red-600 dark:text-red-50"
215
+ onClick = { ( ) => setShowModal ( true ) }
216
+ >
217
+ Regenerate
218
+ </ button >
219
+ ) }
105
220
</ div >
106
221
< >
107
222
{ errorMsg . length > 0 && (
@@ -110,10 +225,39 @@ export function PersonalAccessTokenCreateView() {
110
225
</ Alert >
111
226
) }
112
227
</ >
228
+ < >
229
+ { showModal && (
230
+ < ShowTokenModal
231
+ token = { modalData ! }
232
+ title = "Regenerate Token"
233
+ description = "Are you sure you want to regenerate this personal access token?"
234
+ descriptionImportant = "Any applications using this token will no longer be able to access the Gitpod API."
235
+ actionDescription = "Regenerate Token"
236
+ onSave = { ( ) => {
237
+ regenerate ( ) ;
238
+ } }
239
+ onClose = { ( ) => {
240
+ setShowModal ( false ) ;
241
+ } }
242
+ />
243
+ ) }
244
+ </ >
113
245
< div className = "max-w-md mb-6" >
114
246
< div className = "flex flex-col mb-4" >
115
- < h3 > New Personal Access Token</ h3 >
116
- < h2 className = "text-gray-500" > Create a new personal access token.</ h2 >
247
+ < h3 > { editTokenID ? "Edit" : "New" } Personal Access Token</ h3 >
248
+ { editTokenID ? (
249
+ < >
250
+ < h2 className = "text-gray-500 dark:text-gray-400 dark:text-gray-400" >
251
+ Update token name, expiration date, permissions, or regenerate token.
252
+ </ h2 >
253
+ </ >
254
+ ) : (
255
+ < >
256
+ < h2 className = "text-gray-500 dark:text-gray-400" >
257
+ Create a new personal access token.
258
+ </ h2 >
259
+ </ >
260
+ ) }
117
261
</ div >
118
262
< div className = "flex flex-col gap-4" >
119
263
< div >
@@ -127,7 +271,7 @@ export function PersonalAccessTokenCreateView() {
127
271
type = "text"
128
272
placeholder = "Token Name"
129
273
/>
130
- < p className = "text-gray-500 mt-2" >
274
+ < p className = "text-gray-500 dark:text-gray-400 mt-2" >
131
275
The application name using the token or the purpose of the token.
132
276
</ p >
133
277
</ div >
@@ -144,7 +288,7 @@ export function PersonalAccessTokenCreateView() {
144
288
< option value = "90" > 90 Days</ option >
145
289
< option value = "180" > 180 Days</ option >
146
290
</ select >
147
- < p className = "text-gray-500 mt-2" >
291
+ < p className = "text-gray-500 dark:text-gray-400 mt-2" >
148
292
The token will expire on{ " " }
149
293
{ Intl . DateTimeFormat ( "en-US" , { dateStyle : "long" } ) . format ( value . expirationDate ) } .
150
294
</ p >
@@ -161,14 +305,23 @@ export function PersonalAccessTokenCreateView() {
161
305
</ div >
162
306
</ div >
163
307
</ div >
164
- < button onClick = { createToken } > Create Personal Access Token</ button >
308
+ < div className = "flex gap-2" >
309
+ { editTokenID && (
310
+ < Link to = { settingsPathPersonalAccessTokens } >
311
+ < button className = "secondary" onClick = { createToken } >
312
+ Cancel
313
+ </ button >
314
+ </ Link >
315
+ ) }
316
+ < button onClick = { createToken } > { editTokenID ? "Edit" : "Create" } Personal Access Token</ button >
317
+ </ div >
165
318
</ PageWithSettingsSubMenu >
166
319
</ div >
167
320
) ;
168
321
}
169
322
170
323
interface TokenInfo {
171
- method : string ;
324
+ method : Method ;
172
325
data : PersonalAccessToken ;
173
326
}
174
327
@@ -178,11 +331,13 @@ function ListAccessTokensView() {
178
331
const [ tokens , setTokens ] = useState < PersonalAccessToken [ ] > ( [ ] ) ;
179
332
const [ tokenInfo , setTokenInfo ] = useState < TokenInfo > ( ) ;
180
333
334
+ async function loadTokens ( ) {
335
+ const response = await personalAccessTokensService . listPersonalAccessTokens ( { } ) ;
336
+ setTokens ( response . tokens ) ;
337
+ }
338
+
181
339
useEffect ( ( ) => {
182
- ( async ( ) => {
183
- const response = await personalAccessTokensService . listPersonalAccessTokens ( { } ) ;
184
- setTokens ( response . tokens ) ;
185
- } ) ( ) ;
340
+ loadTokens ( ) ;
186
341
} , [ ] ) ;
187
342
188
343
useEffect ( ( ) => {
@@ -196,6 +351,13 @@ function ListAccessTokensView() {
196
351
copyToClipboard ( tokenInfo ! . data . value ) ;
197
352
} ;
198
353
354
+ const handleDeleteToken = ( tokenId : string ) => {
355
+ if ( tokenId === tokenInfo ?. data . id ) {
356
+ setTokenInfo ( undefined ) ;
357
+ }
358
+ loadTokens ( ) ;
359
+ } ;
360
+
199
361
return (
200
362
< >
201
363
< div className = "flex items-center sm:justify-between mb-4" >
@@ -212,15 +374,22 @@ function ListAccessTokensView() {
212
374
< >
213
375
{ tokenInfo && (
214
376
< >
215
- < div className = "p-4 mb-4 divide-y rounded-xl bg-gray-100 dark:bg-gray-700 " >
377
+ < div className = "p-4 mb-4 divide-y rounded-xl bg-gray-50 dark:bg-gray-800 " >
216
378
< div className = "pb-2" >
217
- < div className = "font-semibold text-gray-700 dark:text-gray-200" >
218
- { tokenInfo . data . name } { " " }
219
- < span className = "px-2 py-1 rounded-full text-sm text-green-600 bg-green-100" >
379
+ < div className = "flex gap-2 content-center font-semibold text-gray-700 dark:text-gray-200" >
380
+ < span className = "ml-1" > { tokenInfo . data . name } </ span >
381
+ < span
382
+ className = {
383
+ "font-medium px-1 py-1 rounded-full text-xs" +
384
+ ( tokenInfo . method === Method . Create
385
+ ? " text-green-600 bg-green-100"
386
+ : " text-blue-600 bg-blue-100" )
387
+ }
388
+ >
220
389
{ tokenInfo . method . toUpperCase ( ) }
221
390
</ span >
222
391
</ div >
223
- < div className = "font-semibold text-gray-400 dark:text-gray-300" >
392
+ < div className = "font-medium text-gray-400 dark:text-gray-300" >
224
393
< span >
225
394
Expires on{ " " }
226
395
{ Intl . DateTimeFormat ( "en-US" , { dateStyle : "long" } ) . format (
@@ -237,13 +406,15 @@ function ListAccessTokensView() {
237
406
</ div >
238
407
</ div >
239
408
< div className = "pt-2" >
240
- < div className = "text-gray-600 font-semibold" > Your New Personal Access Token</ div >
409
+ < div className = "font-semibold text-gray-600 dark:text-gray-200" >
410
+ Your New Personal Access Token
411
+ </ div >
241
412
< InputWithCopy
242
413
className = "my-2 max-w-md"
243
414
value = { tokenInfo . data . value }
244
415
tip = "Copy Token"
245
416
/>
246
- < div className = "mb-2 text-gray-500 font-medium text-sm " >
417
+ < div className = "mb-2 font-medium text-sm text- gray-500 dark: text-gray-300 " >
247
418
Make sure to copy your personal access token — you won't be able to access it again.
248
419
</ div >
249
420
< button className = "secondary" onClick = { handleCopyToken } >
@@ -268,14 +439,14 @@ function ListAccessTokensView() {
268
439
</ div >
269
440
) : (
270
441
< >
271
- < div className = "px-6 py-3 flex justify-between space-x-2 text-sm text-gray-400 mb-2 bg-gray-100 rounded-xl" >
442
+ < div className = "px-6 py-3 flex justify-between space-x-2 text-sm text-gray-400 mb-2 bg-gray-100 dark:bg-gray-800 rounded-xl" >
272
443
< h2 className = "w-3/12" > Token Name</ h2 >
273
444
< h2 className = "w-3/12" > Permissions</ h2 >
274
445
< h2 className = "w-3/12" > Expires</ h2 >
275
446
< div className = "w-3/12" > </ div >
276
447
</ div >
277
448
{ tokens . map ( ( t : PersonalAccessToken ) => {
278
- return < TokenEntry token = { t } /> ;
449
+ return < TokenEntry token = { t } onDelete = { handleDeleteToken } /> ;
279
450
} ) }
280
451
</ >
281
452
) }
0 commit comments