authTokenDetails.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import {useCallback} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {Form, TextField} from 'sentry/components/forms';
  9. import FieldGroup from 'sentry/components/forms/fieldGroup';
  10. import ExternalLink from 'sentry/components/links/externalLink';
  11. import LoadingError from 'sentry/components/loadingError';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import {Panel, PanelBody, PanelHeader} from 'sentry/components/panels';
  14. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  15. import {t, tct} from 'sentry/locale';
  16. import {Organization, OrgAuthToken} from 'sentry/types';
  17. import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
  18. import {
  19. setApiQueryData,
  20. useApiQuery,
  21. useMutation,
  22. useQueryClient,
  23. } from 'sentry/utils/queryClient';
  24. import RequestError from 'sentry/utils/requestError/requestError';
  25. import useApi from 'sentry/utils/useApi';
  26. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  27. import withOrganization from 'sentry/utils/withOrganization';
  28. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  29. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  30. import {
  31. makeFetchOrgAuthTokensForOrgQueryKey,
  32. tokenPreview,
  33. } from 'sentry/views/settings/organizationAuthTokens';
  34. type Props = {
  35. organization: Organization;
  36. params: {tokenId: string};
  37. };
  38. type FetchOrgAuthTokenParameters = {
  39. orgSlug: string;
  40. tokenId: string;
  41. };
  42. type FetchOrgAuthTokenResponse = OrgAuthToken;
  43. type UpdateTokenQueryVariables = {
  44. name: string;
  45. };
  46. export const makeFetchOrgAuthTokenKey = ({
  47. orgSlug,
  48. tokenId,
  49. }: FetchOrgAuthTokenParameters) =>
  50. [`/organizations/${orgSlug}/org-auth-tokens/${tokenId}/`] as const;
  51. function AuthTokenDetailsForm({
  52. token,
  53. organization,
  54. }: {
  55. organization: Organization;
  56. token: OrgAuthToken;
  57. }) {
  58. const initialData = {
  59. name: token.name,
  60. tokenPreview: tokenPreview(token.tokenLastCharacters || '****'),
  61. };
  62. const api = useApi();
  63. const queryClient = useQueryClient();
  64. const handleGoBack = useCallback(() => {
  65. browserHistory.push(normalizeUrl(`/settings/${organization.slug}/auth-tokens/`));
  66. }, [organization.slug]);
  67. const {mutate: submitToken} = useMutation<{}, RequestError, UpdateTokenQueryVariables>({
  68. mutationFn: ({name}) =>
  69. api.requestPromise(
  70. `/organizations/${organization.slug}/org-auth-tokens/${token.id}/`,
  71. {
  72. method: 'PUT',
  73. data: {
  74. name,
  75. },
  76. }
  77. ),
  78. onSuccess: (_data, {name}) => {
  79. addSuccessMessage(t('Updated auth token.'));
  80. // Update get by id query
  81. setApiQueryData(
  82. queryClient,
  83. makeFetchOrgAuthTokenKey({orgSlug: organization.slug, tokenId: token.id}),
  84. (oldData: OrgAuthToken | undefined) => {
  85. if (!oldData) {
  86. return oldData;
  87. }
  88. oldData.name = name;
  89. return oldData;
  90. }
  91. );
  92. // Update get list query
  93. setApiQueryData(
  94. queryClient,
  95. makeFetchOrgAuthTokensForOrgQueryKey({orgSlug: organization.slug}),
  96. (oldData: OrgAuthToken[] | undefined) => {
  97. if (!Array.isArray(oldData)) {
  98. return oldData;
  99. }
  100. const existingToken = oldData.find(oldToken => oldToken.id === token.id);
  101. if (existingToken) {
  102. existingToken.name = name;
  103. }
  104. return oldData;
  105. }
  106. );
  107. handleGoBack();
  108. },
  109. onError: error => {
  110. const message = t('Failed to update the auth token.');
  111. handleXhrErrorResponse(message, error);
  112. addErrorMessage(message);
  113. },
  114. });
  115. return (
  116. <Form
  117. apiMethod="PUT"
  118. initialData={initialData}
  119. apiEndpoint={`/organizations/${organization.slug}/org-auth-tokens/${token.id}/`}
  120. onSubmit={({name}) => {
  121. addLoadingMessage();
  122. return submitToken({
  123. name,
  124. });
  125. }}
  126. onCancel={handleGoBack}
  127. >
  128. <TextField
  129. name="name"
  130. label={t('Name')}
  131. required
  132. help={t('A name to help you identify this token.')}
  133. />
  134. <TextField
  135. name="tokenPreview"
  136. label={t('Token')}
  137. disabled
  138. help={t('You can only view the token once after creation.')}
  139. />
  140. <FieldGroup
  141. label={t('Scopes')}
  142. help={t('You cannot change the scopes of an existing token.')}
  143. >
  144. <div>{token.scopes.slice().sort().join(', ')}</div>
  145. </FieldGroup>
  146. </Form>
  147. );
  148. }
  149. export function OrganizationAuthTokensDetails({params, organization}: Props) {
  150. const {tokenId} = params;
  151. const {
  152. isLoading,
  153. isError,
  154. data: token,
  155. refetch: refetchToken,
  156. } = useApiQuery<FetchOrgAuthTokenResponse>(
  157. makeFetchOrgAuthTokenKey({orgSlug: organization.slug, tokenId}),
  158. {
  159. staleTime: Infinity,
  160. }
  161. );
  162. return (
  163. <div>
  164. <SentryDocumentTitle title={t('Edit Auth Token')} />
  165. <SettingsPageHeader title={t('Edit Auth Token')} />
  166. <TextBlock>
  167. {t(
  168. "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."
  169. )}
  170. </TextBlock>
  171. <TextBlock>
  172. {tct(
  173. 'For more information on how to use the web API, see our [link:documentation].',
  174. {
  175. link: <ExternalLink href="https://docs.sentry.io/api/" />,
  176. }
  177. )}
  178. </TextBlock>
  179. <Panel>
  180. <PanelHeader>{t('Auth Token Details')}</PanelHeader>
  181. <PanelBody>
  182. {isError && (
  183. <LoadingError
  184. message={t('Failed to load auth token.')}
  185. onRetry={refetchToken}
  186. />
  187. )}
  188. {isLoading && <LoadingIndicator />}
  189. {!isLoading && !isError && token && (
  190. <AuthTokenDetailsForm token={token} organization={organization} />
  191. )}
  192. </PanelBody>
  193. </Panel>
  194. </div>
  195. );
  196. }
  197. export default withOrganization(OrganizationAuthTokensDetails);