1
- import { useCallback , useEffect , useState } from 'react' ;
1
+ import { useCallback } from 'react' ;
2
2
import { browserHistory } from 'react-router' ;
3
3
4
4
import { addErrorMessage , addSuccessMessage } from 'sentry/actionCreators/indicator' ;
5
- import Alert from 'sentry/components/alert' ;
6
5
import { Form , TextField } from 'sentry/components/forms' ;
7
6
import FieldGroup from 'sentry/components/forms/fieldGroup' ;
8
7
import ExternalLink from 'sentry/components/links/externalLink' ;
@@ -11,109 +10,151 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
11
10
import { Panel , PanelBody , PanelHeader } from 'sentry/components/panels' ;
12
11
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle' ;
13
12
import { t , tct } from 'sentry/locale' ;
14
- import { Organization , Project } from 'sentry/types' ;
15
- import { setDateToTime } from 'sentry/utils/dates' ;
16
- import getDynamicText from 'sentry/utils/getDynamicText' ;
13
+ import { Organization , OrgAuthToken } from 'sentry/types' ;
17
14
import { handleXhrErrorResponse } from 'sentry/utils/handleXhrErrorResponse' ;
15
+ import {
16
+ setApiQueryData ,
17
+ useApiQuery ,
18
+ useMutation ,
19
+ useQueryClient ,
20
+ } from 'sentry/utils/queryClient' ;
21
+ import RequestError from 'sentry/utils/requestError/requestError' ;
22
+ import useApi from 'sentry/utils/useApi' ;
18
23
import { normalizeUrl } from 'sentry/utils/withDomainRequired' ;
19
24
import withOrganization from 'sentry/utils/withOrganization' ;
20
25
import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader' ;
21
26
import TextBlock from 'sentry/views/settings/components/text/textBlock' ;
22
- import { tokenPreview , TokenWip } from 'sentry/views/settings/organizationAuthTokens' ;
23
-
24
- function generateMockToken ( {
25
- id,
26
- name,
27
- scopes,
28
- dateCreated = new Date ( ) ,
29
- dateLastUsed,
30
- projectLastUsed,
31
- } : {
32
- id : string ;
33
- name : string ;
34
- scopes : string [ ] ;
35
- dateCreated ?: Date ;
36
- dateLastUsed ?: Date ;
37
- projectLastUsed ?: Project ;
38
- } ) : TokenWip {
39
- return {
40
- id,
41
- name,
42
- tokenLastCharacters : crypto . randomUUID ( ) . slice ( 0 , 4 ) ,
43
- scopes,
44
- dateCreated,
45
- dateLastUsed,
46
- projectLastUsed,
47
- } ;
48
- }
27
+ import {
28
+ makeFetchOrgAuthTokensForOrgQueryKey ,
29
+ tokenPreview ,
30
+ } from 'sentry/views/settings/organizationAuthTokens' ;
49
31
50
32
type Props = {
51
33
organization : Organization ;
52
34
params : { tokenId : string } ;
53
35
} ;
54
36
37
+ type FetchOrgAuthTokenParameters = {
38
+ orgSlug : string ;
39
+ tokenId : string ;
40
+ } ;
41
+ type FetchOrgAuthTokenResponse = OrgAuthToken ;
42
+ type UpdateTokenQueryVariables = {
43
+ name : string ;
44
+ } ;
45
+
46
+ export const makeFetchOrgAuthTokenKey = ( {
47
+ orgSlug,
48
+ tokenId,
49
+ } : FetchOrgAuthTokenParameters ) =>
50
+ [ `/organizations/${ orgSlug } /org-auth-tokens/${ tokenId } /` ] as const ;
51
+
55
52
function AuthTokenDetailsForm ( {
56
53
token,
57
54
organization,
58
55
} : {
59
56
organization : Organization ;
60
- token : TokenWip ;
57
+ token : OrgAuthToken ;
61
58
} ) {
62
59
const initialData = {
63
60
name : token . name ,
64
61
tokenPreview : tokenPreview ( token . tokenLastCharacters || '****' ) ,
65
62
} ;
66
63
64
+ const api = useApi ( ) ;
65
+ const queryClient = useQueryClient ( ) ;
66
+
67
+ const handleGoBack = useCallback ( ( ) => {
68
+ browserHistory . push ( normalizeUrl ( `/settings/${ organization . slug } /auth-tokens/` ) ) ;
69
+ } , [ organization . slug ] ) ;
70
+
71
+ const { mutate : submitToken } = useMutation < { } , RequestError , UpdateTokenQueryVariables > ( {
72
+ mutationFn : ( { name} ) =>
73
+ api . requestPromise (
74
+ `/organizations/${ organization . slug } /org-auth-tokens/${ token . id } /` ,
75
+ {
76
+ method : 'PUT' ,
77
+ data : {
78
+ name,
79
+ } ,
80
+ }
81
+ ) ,
82
+
83
+ onSuccess : ( _data , { name} ) => {
84
+ addSuccessMessage ( t ( 'Updated auth token.' ) ) ;
85
+
86
+ // Update get by id query
87
+ setApiQueryData (
88
+ queryClient ,
89
+ makeFetchOrgAuthTokenKey ( { orgSlug : organization . slug , tokenId : token . id } ) ,
90
+ ( oldData : OrgAuthToken | undefined ) => {
91
+ if ( ! oldData ) {
92
+ return oldData ;
93
+ }
94
+
95
+ oldData . name = name ;
96
+
97
+ return oldData ;
98
+ }
99
+ ) ;
100
+
101
+ // Update get list query
102
+ setApiQueryData (
103
+ queryClient ,
104
+ makeFetchOrgAuthTokensForOrgQueryKey ( { orgSlug : organization . slug } ) ,
105
+ ( oldData : OrgAuthToken [ ] | undefined ) => {
106
+ if ( ! Array . isArray ( oldData ) ) {
107
+ return oldData ;
108
+ }
109
+
110
+ const existingToken = oldData . find ( oldToken => oldToken . id === token . id ) ;
111
+
112
+ if ( existingToken ) {
113
+ existingToken . name = name ;
114
+ }
115
+
116
+ return oldData ;
117
+ }
118
+ ) ;
119
+
120
+ handleGoBack ( ) ;
121
+ } ,
122
+ onError : error => {
123
+ const message = t ( 'Failed to update the auth token.' ) ;
124
+ handleXhrErrorResponse ( message , error ) ;
125
+ addErrorMessage ( message ) ;
126
+ } ,
127
+ } ) ;
128
+
67
129
return (
68
130
< Form
69
131
apiMethod = "PUT"
70
132
initialData = { initialData }
71
- apiEndpoint = { `/organizations/${ organization . slug } /auth-tokens/${ token . id } /` }
72
- onSubmit = { ( ) => {
73
- // TODO FN: Actually submit data
74
-
75
- try {
76
- const message = t ( 'Successfully updated the auth token.' ) ;
77
- addSuccessMessage ( message ) ;
78
- } catch ( error ) {
79
- const message = t ( 'Failed to update the auth token.' ) ;
80
- handleXhrErrorResponse ( message , error ) ;
81
- addErrorMessage ( message ) ;
82
- }
133
+ apiEndpoint = { `/organizations/${ organization . slug } /org-auth-tokens/${ token . id } /` }
134
+ onSubmit = { ( { name} ) => {
135
+ submitToken ( {
136
+ name,
137
+ } ) ;
83
138
} }
84
- onCancel = { ( ) =>
85
- browserHistory . push ( normalizeUrl ( `/settings/${ organization . slug } /auth-tokens/` ) )
86
- }
139
+ onCancel = { handleGoBack }
87
140
>
88
141
< TextField
89
142
name = "name"
90
143
label = { t ( 'Name' ) }
91
- value = { token . dateLastUsed }
92
144
required
93
145
help = { t ( 'A name to help you identify this token.' ) }
94
146
/>
95
147
96
148
< TextField
97
149
name = "tokenPreview"
98
150
label = { t ( 'Token' ) }
99
- value = { tokenPreview (
100
- token . tokenLastCharacters
101
- ? getDynamicText ( {
102
- value : token . tokenLastCharacters ,
103
- fixed : 'ABCD' ,
104
- } )
105
- : '****'
106
- ) }
107
151
disabled
108
152
help = { t ( 'You can only view the token once after creation.' ) }
109
153
/>
110
154
111
155
< FieldGroup
112
156
label = { t ( 'Scopes' ) }
113
- inline = { false }
114
- help = { t (
115
- 'You cannot change the scopes of an existing token. If you need different scopes, please create a new token.'
116
- ) }
157
+ help = { t ( 'You cannot change the scopes of an existing token.' ) }
117
158
>
118
159
< div > { token . scopes . slice ( ) . sort ( ) . join ( ', ' ) } </ div >
119
160
</ FieldGroup >
@@ -122,44 +163,25 @@ function AuthTokenDetailsForm({
122
163
}
123
164
124
165
export function OrganizationAuthTokensDetails ( { params, organization} : Props ) {
125
- const [ token , setToken ] = useState < TokenWip | null > ( null ) ;
126
- const [ hasLoadingError , setHasLoadingError ] = useState ( false ) ;
127
-
128
166
const { tokenId} = params ;
129
167
130
- const fetchToken = useCallback ( async ( ) => {
131
- try {
132
- // TODO FN: Actually do something here
133
- await new Promise ( resolve => setTimeout ( resolve , 500 ) ) ;
134
- setToken (
135
- generateMockToken ( {
136
- id : tokenId ,
137
- name : 'custom token' ,
138
- scopes : [ 'org:ci' ] ,
139
- dateLastUsed : setDateToTime ( new Date ( ) , '00:05:00' ) ,
140
- projectLastUsed : { slug : 'my-project' , name : 'My Project' } as Project ,
141
- dateCreated : setDateToTime ( new Date ( ) , '00:01:00' ) ,
142
- } )
143
- ) ;
144
- setHasLoadingError ( false ) ;
145
- } catch ( error ) {
146
- const message = t ( 'Failed to load auth token.' ) ;
147
- handleXhrErrorResponse ( message , error ) ;
148
- setHasLoadingError ( error ) ;
168
+ const {
169
+ isLoading,
170
+ isError,
171
+ data : token ,
172
+ refetch : refetchToken ,
173
+ } = useApiQuery < FetchOrgAuthTokenResponse > (
174
+ makeFetchOrgAuthTokenKey ( { orgSlug : organization . slug , tokenId} ) ,
175
+ {
176
+ staleTime : Infinity ,
149
177
}
150
- } , [ tokenId ] ) ;
151
-
152
- useEffect ( ( ) => {
153
- fetchToken ( ) ;
154
- } , [ fetchToken ] ) ;
178
+ ) ;
155
179
156
180
return (
157
181
< div >
158
182
< SentryDocumentTitle title = { t ( 'Edit Auth Token' ) } />
159
183
< SettingsPageHeader title = { t ( 'Edit Auth Token' ) } />
160
184
161
- < Alert > Note: This page is WIP and currently only shows mocked data.</ Alert >
162
-
163
185
< TextBlock >
164
186
{ t (
165
187
"Authentication tokens allow you to perform actions against the Sentry API on behalf of your organization. They're the easiest way to get started using the API."
@@ -177,16 +199,16 @@ export function OrganizationAuthTokensDetails({params, organization}: Props) {
177
199
< PanelHeader > { t ( 'Auth Token Details' ) } </ PanelHeader >
178
200
179
201
< PanelBody >
180
- { hasLoadingError && (
202
+ { isError && (
181
203
< LoadingError
182
204
message = { t ( 'Failed to load auth token.' ) }
183
- onRetry = { fetchToken }
205
+ onRetry = { refetchToken }
184
206
/>
185
207
) }
186
208
187
- { ! hasLoadingError && ! token && < LoadingIndicator /> }
209
+ { isLoading && < LoadingIndicator /> }
188
210
189
- { ! hasLoadingError && token && (
211
+ { ! isLoading && ! isError && token && (
190
212
< AuthTokenDetailsForm token = { token } organization = { organization } />
191
213
) }
192
214
</ PanelBody >
0 commit comments