newAuthToken.tsx 6.7 KB

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