newAuthToken.tsx 5.8 KB

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