newAuthToken.tsx 5.7 KB

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