authTokenDetails.tsx 6.5 KB

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