newAuthToken.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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 {Form, TextField} from 'sentry/components/forms';
  12. import FieldGroup from 'sentry/components/forms/fieldGroup';
  13. import ExternalLink from 'sentry/components/links/externalLink';
  14. import {Panel, PanelBody, PanelHeader, PanelItem} from 'sentry/components/panels';
  15. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  16. import TextCopyInput from 'sentry/components/textCopyInput';
  17. import {t, tct} from 'sentry/locale';
  18. import {space} from 'sentry/styles/space';
  19. import {Organization, OrgAuthToken} from 'sentry/types';
  20. import getDynamicText from 'sentry/utils/getDynamicText';
  21. import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
  22. import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
  23. import RequestError from 'sentry/utils/requestError/requestError';
  24. import useApi from 'sentry/utils/useApi';
  25. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  26. import withOrganization from 'sentry/utils/withOrganization';
  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 message = t('Failed to create a new auth token.');
  73. handleXhrErrorResponse(message, error);
  74. addErrorMessage(message);
  75. },
  76. });
  77. return (
  78. <Form
  79. apiMethod="POST"
  80. initialData={initialData}
  81. apiEndpoint={`/organizations/${organization.slug}/org-auth-tokens/`}
  82. onSubmit={({name}) => {
  83. submitToken({
  84. name,
  85. });
  86. }}
  87. onCancel={handleGoBack}
  88. submitLabel={t('Create Auth Token')}
  89. requireChanges
  90. >
  91. <TextField
  92. name="name"
  93. label={t('Name')}
  94. required
  95. help={t('A name to help you identify this token.')}
  96. />
  97. <FieldGroup
  98. label={t('Scopes')}
  99. help={t('Organization auth tokens currently have a limited set of scopes.')}
  100. >
  101. <div>
  102. <div>org:ci</div>
  103. <ScopeHelpText>{t('Source Map Upload, Release Creation')}</ScopeHelpText>
  104. </div>
  105. </FieldGroup>
  106. </Form>
  107. );
  108. }
  109. function ShowNewToken({
  110. token,
  111. organization,
  112. }: {
  113. organization: Organization;
  114. token: OrgAuthTokenWithToken;
  115. }) {
  116. const handleGoBack = useCallback(() => {
  117. browserHistory.push(normalizeUrl(`/settings/${organization.slug}/auth-tokens/`));
  118. }, [organization.slug]);
  119. return (
  120. <div>
  121. <Alert type="warning" showIcon system>
  122. {t("Please copy this token to a safe place — it won't be shown again!")}
  123. </Alert>
  124. <PanelItem>
  125. <InputWrapper>
  126. <FieldGroupNoPadding
  127. label={t('Token')}
  128. help={t('You can only view this token when it was created.')}
  129. inline
  130. flexibleControlStateSize
  131. >
  132. <TextCopyInput aria-label={t('Generated token')}>
  133. {getDynamicText({value: token.token, fixed: 'ORG_AUTH_TOKEN'})}
  134. </TextCopyInput>
  135. </FieldGroupNoPadding>
  136. </InputWrapper>
  137. </PanelItem>
  138. <PanelItem>
  139. <ButtonWrapper>
  140. <Button onClick={handleGoBack} priority="primary">
  141. {t('Done')}
  142. </Button>
  143. </ButtonWrapper>
  144. </PanelItem>
  145. </div>
  146. );
  147. }
  148. export function OrganizationAuthTokensNewAuthToken({
  149. organization,
  150. }: {
  151. organization: Organization;
  152. }) {
  153. const [newToken, setNewToken] = useState<OrgAuthTokenWithToken | null>(null);
  154. return (
  155. <div>
  156. <SentryDocumentTitle title={t('Create New Auth Token')} />
  157. <SettingsPageHeader title={t('Create New Auth Token')} />
  158. <TextBlock>
  159. {t(
  160. "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."
  161. )}
  162. </TextBlock>
  163. <TextBlock>
  164. {tct(
  165. 'For more information on how to use the web API, see our [link:documentation].',
  166. {
  167. link: <ExternalLink href="https://docs.sentry.io/api/" />,
  168. }
  169. )}
  170. </TextBlock>
  171. <Panel>
  172. <PanelHeader>{t('Create New Auth Token')}</PanelHeader>
  173. <PanelBody>
  174. {newToken ? (
  175. <ShowNewToken token={newToken} organization={organization} />
  176. ) : (
  177. <AuthTokenCreateForm
  178. organization={organization}
  179. onCreatedToken={setNewToken}
  180. />
  181. )}
  182. </PanelBody>
  183. </Panel>
  184. </div>
  185. );
  186. }
  187. export default withOrganization(OrganizationAuthTokensNewAuthToken);
  188. const InputWrapper = styled('div')`
  189. flex: 1;
  190. `;
  191. const ButtonWrapper = styled('div')`
  192. margin-left: auto;
  193. display: flex;
  194. flex-direction: column;
  195. align-items: flex-end;
  196. font-size: ${p => p.theme.fontSizeSmall};
  197. gap: ${space(1)};
  198. `;
  199. const FieldGroupNoPadding = styled(FieldGroup)`
  200. padding: 0;
  201. `;
  202. const ScopeHelpText = styled('div')`
  203. color: ${p => p.theme.gray300};
  204. `;