newAuthToken.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import {useCallback, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {
  5. addErrorMessage,
  6. addLoadingMessage,
  7. addSuccessMessage,
  8. } from 'sentry/actionCreators/indicator';
  9. import Alert from 'sentry/components/alert';
  10. import {Button} from 'sentry/components/button';
  11. import FieldGroup from 'sentry/components/forms/fieldGroup';
  12. import TextField from 'sentry/components/forms/fields/textField';
  13. import Form from 'sentry/components/forms/form';
  14. import ExternalLink from 'sentry/components/links/externalLink';
  15. import Panel from 'sentry/components/panels/panel';
  16. import PanelBody from 'sentry/components/panels/panelBody';
  17. import PanelHeader from 'sentry/components/panels/panelHeader';
  18. import PanelItem from 'sentry/components/panels/panelItem';
  19. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  20. import TextCopyInput from 'sentry/components/textCopyInput';
  21. import {t, tct} from 'sentry/locale';
  22. import {space} from 'sentry/styles/space';
  23. import {Organization, OrgAuthToken} from 'sentry/types';
  24. import getDynamicText from 'sentry/utils/getDynamicText';
  25. import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
  26. import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
  27. import RequestError from 'sentry/utils/requestError/requestError';
  28. import useApi from 'sentry/utils/useApi';
  29. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  30. import withOrganization from 'sentry/utils/withOrganization';
  31. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  32. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  33. import {makeFetchOrgAuthTokensForOrgQueryKey} from 'sentry/views/settings/organizationAuthTokens';
  34. type CreateTokenQueryVariables = {
  35. name: string;
  36. };
  37. type OrgAuthTokenWithToken = OrgAuthToken & {token: string};
  38. type CreateOrgAuthTokensResponse = OrgAuthTokenWithToken;
  39. function AuthTokenCreateForm({
  40. organization,
  41. onCreatedToken,
  42. }: {
  43. onCreatedToken: (token: OrgAuthTokenWithToken) => void;
  44. organization: Organization;
  45. }) {
  46. const initialData = {
  47. name: '',
  48. };
  49. const api = useApi();
  50. const queryClient = useQueryClient();
  51. const handleGoBack = useCallback(() => {
  52. browserHistory.push(normalizeUrl(`/settings/${organization.slug}/auth-tokens/`));
  53. }, [organization.slug]);
  54. const {mutate: submitToken} = useMutation<
  55. CreateOrgAuthTokensResponse,
  56. RequestError,
  57. CreateTokenQueryVariables
  58. >({
  59. mutationFn: ({name}) => {
  60. addLoadingMessage();
  61. return api.requestPromise(`/organizations/${organization.slug}/org-auth-tokens/`, {
  62. method: 'POST',
  63. data: {
  64. name,
  65. },
  66. });
  67. },
  68. onSuccess: (token: OrgAuthTokenWithToken) => {
  69. addSuccessMessage(t('Created auth token.'));
  70. queryClient.invalidateQueries({
  71. queryKey: makeFetchOrgAuthTokensForOrgQueryKey({orgSlug: organization.slug}),
  72. });
  73. onCreatedToken(token);
  74. },
  75. onError: error => {
  76. const detail = error.responseJSON?.detail;
  77. const code = detail && typeof detail === 'object' ? detail.code : undefined;
  78. const message =
  79. code === 'missing_system_url_prefix'
  80. ? t(
  81. 'You have to configure `system.url-prefix` in your Sentry instance in order to generate tokens.'
  82. )
  83. : t('Failed to create a new auth token.');
  84. handleXhrErrorResponse(message, error);
  85. addErrorMessage(message);
  86. },
  87. });
  88. return (
  89. <Form
  90. apiMethod="POST"
  91. initialData={initialData}
  92. apiEndpoint={`/organizations/${organization.slug}/org-auth-tokens/`}
  93. onSubmit={({name}) => {
  94. submitToken({
  95. name,
  96. });
  97. }}
  98. onCancel={handleGoBack}
  99. submitLabel={t('Create Auth Token')}
  100. requireChanges
  101. >
  102. <TextField
  103. name="name"
  104. label={t('Name')}
  105. required
  106. help={t('A name to help you identify this token.')}
  107. />
  108. <FieldGroup
  109. label={t('Scopes')}
  110. help={t('Organization auth tokens currently have a limited set of scopes.')}
  111. >
  112. <div>
  113. <div>org:ci</div>
  114. <ScopeHelpText>{t('Source Map Upload, Release Creation')}</ScopeHelpText>
  115. </div>
  116. </FieldGroup>
  117. </Form>
  118. );
  119. }
  120. function ShowNewToken({
  121. token,
  122. organization,
  123. }: {
  124. organization: Organization;
  125. token: OrgAuthTokenWithToken;
  126. }) {
  127. const handleGoBack = useCallback(() => {
  128. browserHistory.push(normalizeUrl(`/settings/${organization.slug}/auth-tokens/`));
  129. }, [organization.slug]);
  130. return (
  131. <div>
  132. <Alert type="warning" showIcon system>
  133. {t("Please copy this token to a safe place — it won't be shown again!")}
  134. </Alert>
  135. <PanelItem>
  136. <InputWrapper>
  137. <FieldGroupNoPadding
  138. label={t('Token')}
  139. help={t('You can only view this token when it was created.')}
  140. inline
  141. flexibleControlStateSize
  142. >
  143. <TextCopyInput aria-label={t('Generated token')}>
  144. {getDynamicText({value: token.token, fixed: 'ORG_AUTH_TOKEN'})}
  145. </TextCopyInput>
  146. </FieldGroupNoPadding>
  147. </InputWrapper>
  148. </PanelItem>
  149. <PanelItem>
  150. <ButtonWrapper>
  151. <Button onClick={handleGoBack} priority="primary">
  152. {t('Done')}
  153. </Button>
  154. </ButtonWrapper>
  155. </PanelItem>
  156. </div>
  157. );
  158. }
  159. export function OrganizationAuthTokensNewAuthToken({
  160. organization,
  161. }: {
  162. organization: Organization;
  163. }) {
  164. const [newToken, setNewToken] = useState<OrgAuthTokenWithToken | null>(null);
  165. return (
  166. <div>
  167. <SentryDocumentTitle title={t('Create New Auth Token')} />
  168. <SettingsPageHeader title={t('Create New Auth Token')} />
  169. <TextBlock>
  170. {t(
  171. '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.'
  172. )}
  173. </TextBlock>
  174. <TextBlock>
  175. {tct(
  176. 'For more information on how to use the web API, see our [link:documentation].',
  177. {
  178. link: <ExternalLink href="https://docs.sentry.io/api/" />,
  179. }
  180. )}
  181. </TextBlock>
  182. <Panel>
  183. <PanelHeader>{t('Create New Auth Token')}</PanelHeader>
  184. <PanelBody>
  185. {newToken ? (
  186. <ShowNewToken token={newToken} organization={organization} />
  187. ) : (
  188. <AuthTokenCreateForm
  189. organization={organization}
  190. onCreatedToken={setNewToken}
  191. />
  192. )}
  193. </PanelBody>
  194. </Panel>
  195. </div>
  196. );
  197. }
  198. export default withOrganization(OrganizationAuthTokensNewAuthToken);
  199. const InputWrapper = styled('div')`
  200. flex: 1;
  201. `;
  202. const ButtonWrapper = styled('div')`
  203. margin-left: auto;
  204. display: flex;
  205. flex-direction: column;
  206. align-items: flex-end;
  207. font-size: ${p => p.theme.fontSizeSmall};
  208. gap: ${space(1)};
  209. `;
  210. const FieldGroupNoPadding = styled(FieldGroup)`
  211. padding: 0;
  212. `;
  213. const ScopeHelpText = styled('div')`
  214. color: ${p => p.theme.gray300};
  215. `;