@@ -28,6 +28,7 @@ import { ContextMenuEntry } from "../components/ContextMenu";
28
28
import PillLabel from "../components/PillLabel" ;
29
29
import dayjs from "dayjs" ;
30
30
import { ItemFieldContextMenu } from "../components/ItemsList" ;
31
+ import { SpinnerLoader , SpinnerOverlayLoader } from "../components/Loader" ;
31
32
32
33
export default function PersonalAccessTokens ( ) {
33
34
const { enablePersonalAccessTokens } = useContext ( FeatureFlagContext ) ;
@@ -182,6 +183,7 @@ export function PersonalAccessTokenCreateView() {
182
183
const params = useParams < { tokenId ?: string } > ( ) ;
183
184
const history = useHistory < TokenInfo > ( ) ;
184
185
186
+ const [ loading , setLoading ] = useState ( false ) ;
185
187
const [ errorMsg , setErrorMsg ] = useState ( "" ) ;
186
188
const [ editTokenID , setEditTokenID ] = useState < string > ( ) ;
187
189
const [ editToken , setEditToken ] = useState < PersonalAccessToken > ( ) ;
@@ -209,7 +211,7 @@ export function PersonalAccessTokenCreateView() {
209
211
}
210
212
// update UI to `edit style` immediately
211
213
setEditTokenID ( tokenId ) ;
212
-
214
+ setLoading ( true ) ;
213
215
const resp = await personalAccessTokensService . getPersonalAccessToken ( { id : tokenId } ) ;
214
216
const token = resp . token ! ;
215
217
setEditToken ( token ) ;
@@ -220,6 +222,7 @@ export function PersonalAccessTokenCreateView() {
220
222
} catch ( e ) {
221
223
setErrorMsg ( e . message ) ;
222
224
}
225
+ setLoading ( false ) ;
223
226
} ) ( ) ;
224
227
} , [ ] ) ;
225
228
@@ -331,82 +334,88 @@ export function PersonalAccessTokenCreateView() {
331
334
/>
332
335
) }
333
336
</ >
334
- < div className = "max-w-md mb-6" >
335
- < div className = "flex flex-col mb-4" >
336
- < h3 > { editTokenID ? "Edit" : "New" } Personal Access Token</ h3 >
337
- { editTokenID ? (
338
- < h2 className = "text-gray-500 dark:text-gray-400" >
339
- Update token name, expiration date, permissions, or regenerate token.
340
- </ h2 >
341
- ) : (
342
- < h2 className = "text-gray-500 dark:text-gray-400" > Create a new access token.</ h2 >
343
- ) }
344
- </ div >
345
- < div className = "flex flex-col gap-4" >
346
- < div >
347
- < h4 > Token Name</ h4 >
348
- < input
349
- className = "w-full"
350
- value = { token . name }
351
- onChange = { ( e ) => update ( { name : e . target . value } ) }
352
- onKeyDown = { ( e ) => {
353
- if ( e . key === "Enter" ) {
354
- e . preventDefault ( ) ;
355
- handleConfirm ( ) ;
356
- }
357
- } }
358
- type = "text"
359
- placeholder = "Token Name"
360
- />
361
- < p className = "text-gray-500 dark:text-gray-400 mt-2" >
362
- The application name using the token or the purpose of the token.
363
- </ p >
337
+ < SpinnerOverlayLoader content = "loading access token" loading = { loading } >
338
+ < div className = "mb-6" >
339
+ < div className = "flex flex-col mb-4" >
340
+ < h3 > { editTokenID ? "Edit" : "New" } Personal Access Token</ h3 >
341
+ { editTokenID ? (
342
+ < h2 className = "text-gray-500 dark:text-gray-400" >
343
+ Update token name, expiration date, permissions, or regenerate token.
344
+ </ h2 >
345
+ ) : (
346
+ < h2 className = "text-gray-500 dark:text-gray-400" >
347
+ Create a new access token.
348
+ </ h2 >
349
+ ) }
364
350
</ div >
365
- { ! editTokenID && (
366
- < DateSelector
367
- title = "Expiration Date"
368
- description = { `The token will expire on ${ dayjs ( token . expirationDate ) . format (
369
- "MMM D, YYYY" ,
370
- ) } `}
371
- options = { TokenExpirationDays }
372
- value = { TokenExpirationDays . find ( ( i ) => i . value === token . expirationDays ) ?. value }
373
- onChange = { ( value ) => {
374
- update ( { expirationDays : value } ) ;
375
- } }
376
- />
377
- ) }
378
- < div >
379
- < h4 > Permission</ h4 >
380
- < div className = "space-y-2" >
381
- { AllPermissions . map ( ( item ) => (
382
- < CheckBox
383
- className = ""
384
- title = { item . name }
385
- desc = { item . description }
386
- checked = { item . scopes . every ( ( s ) => token . scopes . has ( s ) ) }
387
- onChange = { ( e ) => {
388
- if ( e . target . checked ) {
389
- update ( { } , item . scopes ) ;
390
- } else {
391
- update ( { } , undefined , item . scopes ) ;
392
- }
393
- } }
394
- />
395
- ) ) }
351
+ < div className = "flex flex-col gap-4" >
352
+ < div >
353
+ < h4 > Token Name</ h4 >
354
+ < input
355
+ className = "max-w-md"
356
+ value = { token . name }
357
+ onChange = { ( e ) => update ( { name : e . target . value } ) }
358
+ onKeyDown = { ( e ) => {
359
+ if ( e . key === "Enter" ) {
360
+ e . preventDefault ( ) ;
361
+ handleConfirm ( ) ;
362
+ }
363
+ } }
364
+ type = "text"
365
+ placeholder = "Token Name"
366
+ />
367
+ < p className = "text-gray-500 dark:text-gray-400 mt-2" >
368
+ The application name using the token or the purpose of the token.
369
+ </ p >
370
+ </ div >
371
+ { ! editTokenID && (
372
+ < DateSelector
373
+ title = "Expiration Date"
374
+ description = { `The token will expire on ${ dayjs ( token . expirationDate ) . format (
375
+ "MMM D, YYYY" ,
376
+ ) } `}
377
+ options = { TokenExpirationDays }
378
+ value = { TokenExpirationDays . find ( ( i ) => i . value === token . expirationDays ) ?. value }
379
+ onChange = { ( value ) => {
380
+ update ( { expirationDays : value } ) ;
381
+ } }
382
+ />
383
+ ) }
384
+ < div >
385
+ < h4 > Permission</ h4 >
386
+ < div className = "space-y-2" >
387
+ { AllPermissions . map ( ( item ) => (
388
+ < CheckBox
389
+ className = ""
390
+ title = { item . name }
391
+ desc = { item . description }
392
+ checked = { item . scopes . every ( ( s ) => token . scopes . has ( s ) ) }
393
+ onChange = { ( e ) => {
394
+ if ( e . target . checked ) {
395
+ update ( { } , item . scopes ) ;
396
+ } else {
397
+ update ( { } , undefined , item . scopes ) ;
398
+ }
399
+ } }
400
+ />
401
+ ) ) }
402
+ </ div >
396
403
</ div >
397
404
</ div >
398
405
</ div >
399
- </ div >
400
- < div className = "flex gap-2" >
401
- { editTokenID && (
402
- < Link to = { settingsPathPersonalAccessTokens } >
403
- < button className = "secondary" onClick = { handleConfirm } >
404
- Cancel
405
- </ button >
406
- </ Link >
407
- ) }
408
- < button onClick = { handleConfirm } > { editTokenID ? "Update" : "Create" } Personal Access Token</ button >
409
- </ div >
406
+ < div className = "flex gap-2" >
407
+ { editTokenID && (
408
+ < Link to = { settingsPathPersonalAccessTokens } >
409
+ < button className = "secondary" onClick = { handleConfirm } >
410
+ Cancel
411
+ </ button >
412
+ </ Link >
413
+ ) }
414
+ < button onClick = { handleConfirm } >
415
+ { editTokenID ? "Update" : "Create" } Personal Access Token
416
+ </ button >
417
+ </ div >
418
+ </ SpinnerOverlayLoader >
410
419
</ PageWithSettingsSubMenu >
411
420
</ div >
412
421
) ;
@@ -473,18 +482,21 @@ interface TokenInfo {
473
482
function ListAccessTokensView ( ) {
474
483
const location = useLocation ( ) ;
475
484
485
+ const [ loading , setLoading ] = useState < boolean > ( false ) ;
476
486
const [ tokens , setTokens ] = useState < PersonalAccessToken [ ] > ( [ ] ) ;
477
487
const [ tokenInfo , setTokenInfo ] = useState < TokenInfo > ( ) ;
478
488
const [ modalData , setModalData ] = useState < { token : PersonalAccessToken ; action : TokenAction } > ( ) ;
479
489
const [ errorMsg , setErrorMsg ] = useState ( "" ) ;
480
490
481
491
async function loadTokens ( ) {
482
492
try {
493
+ setLoading ( true ) ;
483
494
const response = await personalAccessTokensService . listPersonalAccessTokens ( { } ) ;
484
495
setTokens ( response . tokens ) ;
485
496
} catch ( e ) {
486
497
setErrorMsg ( e . message ) ;
487
498
}
499
+ setLoading ( false ) ;
488
500
}
489
501
490
502
useEffect ( ( ) => {
@@ -594,52 +606,62 @@ function ListAccessTokensView() {
594
606
</ >
595
607
) }
596
608
</ >
597
- { tokens . length === 0 ? (
598
- < div className = "bg-gray-100 dark:bg-gray-800 rounded-xl w-full py-28 flex flex-col items-center" >
599
- < h3 className = "text-center pb-3 text-gray-500 dark:text-gray-400" > No Access Tokens</ h3 >
600
- < p className = "text-center pb-6 text-gray-500 text-base w-96" >
601
- Generate an access token for applications that need access to the Gitpod API.{ " " }
602
- </ p >
603
- < Link to = { settingsPathPersonalAccessTokenCreate } >
604
- < button > New Access Token</ button >
605
- </ Link >
606
- </ div >
609
+ { loading ? (
610
+ < SpinnerLoader content = "loading access token list" />
607
611
) : (
608
612
< >
609
- < div className = "px-3 py-3 flex justify-between space-x-2 text-sm text-gray-400 mb-2 bg-gray-100 dark:bg-gray-800 rounded-xl" >
610
- < h2 className = "w-4/12" > Token Name</ h2 >
611
- < h2 className = "w-4/12" > Permissions</ h2 >
612
- < h2 className = "w-3/12" > Expires</ h2 >
613
- < div className = "w-1/12" > </ div >
614
- </ div >
615
- { tokens . map ( ( t : PersonalAccessToken ) => (
616
- < TokenEntry
617
- key = { t . id }
618
- token = { t }
619
- menuEntries = { [
620
- {
621
- title : "Edit" ,
622
- link : `${ settingsPathPersonalAccessTokenEdit } /${ t . id } ` ,
623
- } ,
624
- {
625
- title : "Regenerate" ,
626
- href : "" ,
627
- customFontStyle :
628
- "text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300" ,
629
- onClick : ( ) => setModalData ( { token : t , action : TokenAction . Regerenrate } ) ,
630
- } ,
631
- {
632
- title : "Delete" ,
633
- href : "" ,
634
- customFontStyle :
635
- "text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300" ,
636
- onClick : ( ) => setModalData ( { token : t , action : TokenAction . Delete } ) ,
637
- } ,
638
- ] }
639
- />
640
- ) ) }
613
+ { tokens . length === 0 ? (
614
+ < div className = "bg-gray-100 dark:bg-gray-800 rounded-xl w-full py-28 flex flex-col items-center" >
615
+ < h3 className = "text-center pb-3 text-gray-500 dark:text-gray-400" >
616
+ No Access Tokens (PAT)
617
+ </ h3 >
618
+ < p className = "text-center pb-6 text-gray-500 text-base w-96" >
619
+ Generate a access token (PAT) for applications that need access to the Gitpod
620
+ API.{ " " }
621
+ </ p >
622
+ < Link to = { settingsPathPersonalAccessTokenCreate } >
623
+ < button > New Access Token</ button >
624
+ </ Link >
625
+ </ div >
626
+ ) : (
627
+ < >
628
+ < div className = "px-3 py-3 flex justify-between space-x-2 text-sm text-gray-400 mb-2 bg-gray-100 dark:bg-gray-800 rounded-xl" >
629
+ < h2 className = "w-4/12" > Token Name</ h2 >
630
+ < h2 className = "w-4/12" > Permissions</ h2 >
631
+ < h2 className = "w-3/12" > Expires</ h2 >
632
+ < div className = "w-1/12" > </ div >
633
+ </ div >
634
+ { tokens . map ( ( t : PersonalAccessToken ) => (
635
+ < TokenEntry
636
+ key = { t . id }
637
+ token = { t }
638
+ menuEntries = { [
639
+ {
640
+ title : "Edit" ,
641
+ link : `${ settingsPathPersonalAccessTokenEdit } /${ t . id } ` ,
642
+ } ,
643
+ {
644
+ title : "Regenerate" ,
645
+ href : "" ,
646
+ customFontStyle :
647
+ "text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300" ,
648
+ onClick : ( ) => setModalData ( { token : t , action : TokenAction . Regerenrate } ) ,
649
+ } ,
650
+ {
651
+ title : "Delete" ,
652
+ href : "" ,
653
+ customFontStyle :
654
+ "text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300" ,
655
+ onClick : ( ) => setModalData ( { token : t , action : TokenAction . Delete } ) ,
656
+ } ,
657
+ ] }
658
+ />
659
+ ) ) }
660
+ </ >
661
+ ) }
641
662
</ >
642
663
) }
664
+
643
665
{ modalData ?. action === TokenAction . Delete && (
644
666
< ShowTokenModal
645
667
token = { modalData . token }
0 commit comments