newAuthToken.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import {useCallback, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import FieldGroup from 'sentry/components/forms/fieldGroup';
  9. import TextField from 'sentry/components/forms/fields/textField';
  10. import Form from 'sentry/components/forms/form';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import Panel from 'sentry/components/panels/panel';
  13. import PanelBody from 'sentry/components/panels/panelBody';
  14. import PanelHeader from 'sentry/components/panels/panelHeader';
  15. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  16. import {t, tct} from 'sentry/locale';
  17. import type {Organization} from 'sentry/types/organization';
  18. import type {OrgAuthToken} from 'sentry/types/user';
  19. import getDynamicText from 'sentry/utils/getDynamicText';
  20. import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
  21. import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
  22. import type RequestError from 'sentry/utils/requestError/requestError';
  23. import useApi from 'sentry/utils/useApi';
  24. import {useNavigate} from 'sentry/utils/useNavigate';
  25. import withOrganization from 'sentry/utils/withOrganization';
  26. import NewTokenHandler from 'sentry/views/settings/components/newTokenHandler';
  27. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  28. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  29. import {makeFetchOrgAuthTokensForOrgQueryKey} from 'sentry/views/settings/organizationAuthTokens';
  30. type CreateTokenQueryVariables = {
  31. name: string;
  32. };
  33. type OrgAuthTokenWithToken = OrgAuthToken & {token: string};
  34. type CreateOrgAuthTokensResponse = OrgAuthTokenWithToken;
  35. function AuthTokenCreateForm({
  36. organization,
  37. onCreatedToken,
  38. }: {
  39. onCreatedToken: (token: OrgAuthTokenWithToken) => void;
  40. organization: Organization;
  41. }) {
  42. const initialData = {
  43. name: '',
  44. };
  45. const navigate = useNavigate();
  46. const api = useApi();
  47. const queryClient = useQueryClient();
  48. const handleGoBack = useCallback(
  49. () => navigate(`/settings/${organization.slug}/auth-tokens/`),
  50. [navigate, organization.slug]
  51. );
  52. const {mutate: submitToken} = useMutation<
  53. CreateOrgAuthTokensResponse,
  54. RequestError,
  55. CreateTokenQueryVariables
  56. >({
  57. mutationFn: ({name}) => {
  58. addLoadingMessage();
  59. return api.requestPromise(`/organizations/${organization.slug}/org-auth-tokens/`, {
  60. method: 'POST',
  61. data: {
  62. name,
  63. },
  64. });
  65. },
  66. onSuccess: (token: OrgAuthTokenWithToken) => {
  67. addSuccessMessage(t('Created auth token.'));
  68. queryClient.invalidateQueries({
  69. queryKey: makeFetchOrgAuthTokensForOrgQueryKey({orgSlug: organization.slug}),
  70. });
  71. onCreatedToken(token);
  72. },
  73. onError: error => {
  74. const detail = error.responseJSON?.detail;
  75. const code = detail && typeof detail === 'object' ? detail.code : undefined;
  76. const message =
  77. code === 'missing_system_url_prefix'
  78. ? t(
  79. 'You have to configure `system.url-prefix` in your Sentry instance in order to generate tokens.'
  80. )
  81. : t('Failed to create a new auth token.');
  82. handleXhrErrorResponse(message, error);
  83. addErrorMessage(message);
  84. },
  85. });
  86. return (
  87. <Form
  88. apiMethod="POST"
  89. initialData={initialData}
  90. apiEndpoint={`/organizations/${organization.slug}/org-auth-tokens/`}
  91. onSubmit={({name}) => {
  92. submitToken({
  93. name,
  94. });
  95. }}
  96. onCancel={handleGoBack}
  97. submitLabel={t('Create Auth Token')}
  98. requireChanges
  99. >
  100. <TextField
  101. name="name"
  102. label={t('Name')}
  103. required
  104. help={t('A name to help you identify this token.')}
  105. />
  106. <FieldGroup
  107. label={t('Scopes')}
  108. help={t('Organization auth tokens currently have a limited set of scopes.')}
  109. >
  110. <div>
  111. <div>org:ci</div>
  112. <ScopeHelpText>{t('Source Map Upload, Release Creation')}</ScopeHelpText>
  113. </div>
  114. </FieldGroup>
  115. </Form>
  116. );
  117. }
  118. export function OrganizationAuthTokensNewAuthToken({
  119. organization,
  120. }: {
  121. organization: Organization;
  122. }) {
  123. const navigate = useNavigate();
  124. const [newToken, setNewToken] = useState<OrgAuthTokenWithToken | null>(null);
  125. const handleGoBack = useCallback(
  126. () => navigate(`/settings/${organization.slug}/auth-tokens/`),
  127. [navigate, organization.slug]
  128. );
  129. return (
  130. <div>
  131. <SentryDocumentTitle title={t('Create New Auth Token')} />
  132. <SettingsPageHeader title={t('Create New Auth Token')} />
  133. <TextBlock>
  134. {t(
  135. 'Organization Auth Tokens can be used in many places to interact with Sentry programatically. For example, they can be used for sentry-cli, bundler plugins or similar uses cases.'
  136. )}
  137. </TextBlock>
  138. <TextBlock>
  139. {tct(
  140. 'For more information on how to use the web API, see our [link:documentation].',
  141. {
  142. link: <ExternalLink href="https://docs.sentry.io/api/" />,
  143. }
  144. )}
  145. </TextBlock>
  146. <Panel>
  147. <PanelHeader>{t('Create New Auth Token')}</PanelHeader>
  148. <PanelBody>
  149. {newToken ? (
  150. <NewTokenHandler
  151. token={getDynamicText({value: newToken.token, fixed: 'ORG_AUTH_TOKEN'})}
  152. handleGoBack={handleGoBack}
  153. />
  154. ) : (
  155. <AuthTokenCreateForm
  156. organization={organization}
  157. onCreatedToken={setNewToken}
  158. />
  159. )}
  160. </PanelBody>
  161. </Panel>
  162. </div>
  163. );
  164. }
  165. export default withOrganization(OrganizationAuthTokensNewAuthToken);
  166. const ScopeHelpText = styled('div')`
  167. color: ${p => p.theme.gray300};
  168. `;