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 } from "react-router" ;
9
+ import { Redirect , useHistory , useLocation } from "react-router" ;
10
+ import { Link } from "react-router-dom" ;
11
+ import CheckBox from "../components/CheckBox" ;
10
12
import { FeatureFlagContext } from "../contexts/FeatureFlagContext" ;
11
13
import { personalAccessTokensService } from "../service/public-api" ;
12
14
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu" ;
15
+ import { settingsPathPersonalAccessTokenCreate , settingsPathPersonalAccessTokens } from "./settings.routes" ;
16
+ import arrowDown from "../images/sort-arrow.svg" ;
17
+ import { Timestamp } from "@bufbuild/protobuf" ;
18
+ import Alert from "../components/Alert" ;
19
+ import { InputWithCopy } from "../components/InputWithCopy" ;
20
+ import { copyToClipboard } from "../utils" ;
13
21
14
22
function PersonalAccessTokens ( ) {
15
23
const { enablePersonalAccessTokens } = useContext ( FeatureFlagContext ) ;
@@ -27,31 +35,222 @@ function PersonalAccessTokens() {
27
35
) ;
28
36
}
29
37
38
+ interface EditPATData {
39
+ name : string ;
40
+ expirationDays : number ;
41
+ expirationDate : Date ;
42
+ }
43
+
44
+ export function PersonalAccessTokenCreateView ( ) {
45
+ const { enablePersonalAccessTokens } = useContext ( FeatureFlagContext ) ;
46
+
47
+ const history = useHistory ( ) ;
48
+ const [ errorMsg , setErrorMsg ] = useState ( "" ) ;
49
+ const [ value , setValue ] = useState < EditPATData > ( {
50
+ name : "" ,
51
+ expirationDays : 30 ,
52
+ expirationDate : new Date ( Date . now ( ) + 30 * 24 * 60 * 60 * 1000 ) ,
53
+ } ) ;
54
+
55
+ const update = ( change : Partial < EditPATData > ) => {
56
+ if ( change . expirationDays ) {
57
+ change . expirationDate = new Date ( Date . now ( ) + change . expirationDays * 24 * 60 * 60 * 1000 ) ;
58
+ }
59
+ setErrorMsg ( "" ) ;
60
+ setValue ( { ...value , ...change } ) ;
61
+ } ;
62
+
63
+ const createToken = async ( ) => {
64
+ if ( value . name . length < 3 ) {
65
+ setErrorMsg ( "Token Name should have at least three characters." ) ;
66
+ return ;
67
+ }
68
+ try {
69
+ const resp = await personalAccessTokensService . createPersonalAccessToken ( {
70
+ token : {
71
+ name : value . name ,
72
+ expirationTime : Timestamp . fromDate ( value . expirationDate ) ,
73
+ scopes : [ "function:*" , "resource:default" ] ,
74
+ } ,
75
+ } ) ;
76
+ history . push ( {
77
+ pathname : settingsPathPersonalAccessTokens ,
78
+ state : {
79
+ method : "CREATED" ,
80
+ data : resp . token ,
81
+ } ,
82
+ } ) ;
83
+ } catch ( e ) {
84
+ setErrorMsg ( e . message ) ;
85
+ }
86
+ } ;
87
+
88
+ if ( ! enablePersonalAccessTokens ) {
89
+ return < Redirect to = "/" /> ;
90
+ }
91
+
92
+ return (
93
+ < div >
94
+ < PageWithSettingsSubMenu title = "Access Tokens" subtitle = "Manage your personal access tokens." >
95
+ < div className = "mb-4" >
96
+ < Link to = { settingsPathPersonalAccessTokens } >
97
+ < button className = "secondary" >
98
+ < div className = "flex place-content-center" >
99
+ < img src = { arrowDown } className = "w-4 mr-2 transform rotate-90 mb-0" alt = "Back arrow" />
100
+ < span > Back to list</ span >
101
+ </ div >
102
+ </ button >
103
+ </ Link >
104
+ </ div >
105
+ < >
106
+ { errorMsg . length > 0 && (
107
+ < Alert type = "error" className = "mb-2" >
108
+ { errorMsg }
109
+ </ Alert >
110
+ ) }
111
+ </ >
112
+ < div className = "max-w-md mb-6" >
113
+ < div className = "flex flex-col mb-4" >
114
+ < h3 > New Personal Access Token</ h3 >
115
+ < h2 className = "text-gray-500" > Create a new personal access token.</ h2 >
116
+ </ div >
117
+ < div className = "flex flex-col gap-4" >
118
+ < div >
119
+ < h4 > Token Name</ h4 >
120
+ < input
121
+ className = "w-full"
122
+ value = { value . name }
123
+ onChange = { ( e ) => {
124
+ update ( { name : e . target . value } ) ;
125
+ } }
126
+ type = "text"
127
+ placeholder = "Token Name"
128
+ />
129
+ < p className = "text-gray-500 mt-2" >
130
+ The application name using the token or the purpose of the token.
131
+ </ p >
132
+ </ div >
133
+ < div >
134
+ < h4 > Expiration Date</ h4 >
135
+ < select
136
+ name = "expiration"
137
+ value = { value . expirationDays }
138
+ onChange = { ( e ) => {
139
+ update ( { expirationDays : Number ( e . target . value ) } ) ;
140
+ } }
141
+ >
142
+ < option value = "30" > 30 Days</ option >
143
+ < option value = "90" > 90 Days</ option >
144
+ < option value = "180" > 180 Days</ option >
145
+ </ select >
146
+ < p className = "text-gray-500 mt-2" >
147
+ The token will expire on{ " " }
148
+ { Intl . DateTimeFormat ( "en-US" , { dateStyle : "long" } ) . format ( value . expirationDate ) } .
149
+ </ p >
150
+ </ div >
151
+ < div >
152
+ < h4 > Permission</ h4 >
153
+ < CheckBox
154
+ className = ""
155
+ title = "Access the user's API"
156
+ desc = "Grant complete read and write access to the API."
157
+ checked = { true }
158
+ disabled = { true }
159
+ />
160
+ </ div >
161
+ </ div >
162
+ </ div >
163
+ < button onClick = { createToken } > Create Personal Access Token</ button >
164
+ </ PageWithSettingsSubMenu >
165
+ </ div >
166
+ ) ;
167
+ }
168
+
169
+ interface TokenInfo {
170
+ method : string ;
171
+ data : PersonalAccessToken ;
172
+ }
173
+
30
174
function ListAccessTokensView ( ) {
175
+ const location = useLocation ( ) ;
176
+
31
177
const [ tokens , setTokens ] = useState < PersonalAccessToken [ ] > ( [ ] ) ;
178
+ const [ tokenInfo , setTokenInfo ] = useState < TokenInfo > ( ) ;
32
179
33
180
useEffect ( ( ) => {
34
181
( async ( ) => {
35
182
const response = await personalAccessTokensService . listPersonalAccessTokens ( { } ) ;
36
183
setTokens ( response . tokens ) ;
37
184
} ) ( ) ;
38
- } , [ ] ) ;
185
+ } ) ;
186
+
187
+ useEffect ( ( ) => {
188
+ if ( location . state ) {
189
+ setTokenInfo ( location . state as any as TokenInfo ) ;
190
+ window . history . replaceState ( { } , "" ) ;
191
+ }
192
+ } , [ location . state ] ) ;
193
+
194
+ const handleCopyToken = ( ) => {
195
+ copyToClipboard ( tokenInfo ! . data . value ) ;
196
+ } ;
39
197
40
198
return (
41
199
< >
42
- < div className = "flex items-start sm:justify-between mb-2" >
200
+ < div className = "flex items-center sm:justify-between mb-2" >
43
201
< div >
44
202
< h3 > Personal Access Tokens</ h3 >
45
203
< h2 className = "text-gray-500" > Create or regenerate active personal access tokens.</ h2 >
46
204
</ div >
205
+ < Link to = { settingsPathPersonalAccessTokenCreate } >
206
+ < button > New Personal Access Token</ button >
207
+ </ Link >
47
208
</ div >
48
- < div className = "bg-gray-100 dark:bg-gray-800 rounded-xl w-full py-28 flex flex-col items-center" >
49
- < h3 className = "text-center pb-3 text-gray-500 dark:text-gray-400" > No Personal Access Tokens (PAT)</ h3 >
50
- < p className = "text-center pb-6 text-gray-500 text-base w-96" >
51
- Generate a personal access token (PAT) for applications that need access to the Gitpod API.{ " " }
52
- </ p >
53
- < button > New Personal Access Token</ button >
54
- </ div >
209
+ < >
210
+ { tokenInfo && (
211
+ < >
212
+ < div className = "p-4 mb-4 divide-y rounded-xl bg-gray-100 dark:bg-gray-700" >
213
+ < div className = "pb-2" >
214
+ < div className = "font-semibold text-gray-700 dark:text-gray-200" >
215
+ { tokenInfo . data . name } { " " }
216
+ < span className = "px-2 py-1 rounded-full text-sm text-green-600 bg-green-100" >
217
+ { tokenInfo . method . toUpperCase ( ) }
218
+ </ span >
219
+ </ div >
220
+ < div className = "font-semibold text-gray-400 dark:text-gray-300" >
221
+ < span >
222
+ Expires on{ " " }
223
+ { Intl . DateTimeFormat ( "en-US" , { dateStyle : "long" } ) . format (
224
+ tokenInfo . data . expirationTime ?. toDate ( ) ,
225
+ ) }
226
+ </ span >
227
+ < span > • </ span >
228
+ < span >
229
+ Created on{ " " }
230
+ { Intl . DateTimeFormat ( "en-US" , { dateStyle : "long" } ) . format (
231
+ tokenInfo . data . createdAt ?. toDate ( ) ,
232
+ ) }
233
+ </ span >
234
+ </ div >
235
+ </ div >
236
+ < div className = "pt-2" >
237
+ < div className = "text-gray-600 font-semibold" > Your New Personal Access Token</ div >
238
+ < InputWithCopy
239
+ className = "my-2 max-w-md"
240
+ value = { tokenInfo . data . value }
241
+ tip = "Copy Token"
242
+ />
243
+ < div className = "mb-2 text-gray-500 font-medium text-sm" >
244
+ Make sure to copy your personal access token — you won't be able to access it again.
245
+ </ div >
246
+ < button className = "secondary" onClick = { handleCopyToken } >
247
+ Copy Token To Clipboard
248
+ </ button >
249
+ </ div >
250
+ </ div >
251
+ </ >
252
+ ) }
253
+ </ >
55
254
{ tokens . length > 0 && (
56
255
< ul >
57
256
{ tokens . map ( ( t : PersonalAccessToken ) => {
0 commit comments