authTokenDetails.tsx 6.5 KB

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