newProviderForm.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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 {hasEveryAccess} from 'sentry/components/acl/access';
  9. import {
  10. PROVIDER_TO_SETUP_WEBHOOK_URL,
  11. WebhookProviderEnum,
  12. } from 'sentry/components/events/featureFlags/utils';
  13. import FieldGroup from 'sentry/components/forms/fieldGroup';
  14. import SelectField from 'sentry/components/forms/fields/selectField';
  15. import TextField from 'sentry/components/forms/fields/textField';
  16. import Form from 'sentry/components/forms/form';
  17. import ExternalLink from 'sentry/components/links/externalLink';
  18. import TextCopyInput from 'sentry/components/textCopyInput';
  19. import {t, tct} from 'sentry/locale';
  20. import {space} from 'sentry/styles/space';
  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 {useNavigate} from 'sentry/utils/useNavigate';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. import {makeFetchSecretQueryKey} from 'sentry/views/settings/featureFlags/changeTracking';
  29. export type CreateSecretQueryVariables = {
  30. provider: string;
  31. secret: string;
  32. };
  33. export type CreateSecretResponse = string;
  34. export default function NewProviderForm({
  35. onCreatedSecret,
  36. onSetProvider,
  37. }: {
  38. onCreatedSecret: (secret: string) => void;
  39. onSetProvider: (provider: string) => void;
  40. }) {
  41. const initialData = {
  42. provider: '',
  43. secret: '',
  44. };
  45. const organization = useOrganization();
  46. const api = useApi();
  47. const queryClient = useQueryClient();
  48. const navigate = useNavigate();
  49. const [selectedProvider, setSelectedProvider] = useState('<provider_name>');
  50. const handleGoBack = useCallback(() => {
  51. navigate(
  52. normalizeUrl(`/settings/${organization.slug}/feature-flags/change-tracking/`)
  53. );
  54. }, [organization.slug, navigate]);
  55. const {mutate: submitSecret, isPending} = useMutation<
  56. CreateSecretResponse,
  57. RequestError,
  58. CreateSecretQueryVariables
  59. >({
  60. mutationFn: ({provider, secret}) => {
  61. addLoadingMessage();
  62. return api.requestPromise(
  63. `/organizations/${organization.slug}/flags/signing-secrets/`,
  64. {
  65. method: 'POST',
  66. data: {
  67. provider: provider.toLowerCase(),
  68. secret,
  69. },
  70. }
  71. );
  72. },
  73. onSuccess: (_response, {secret, provider}) => {
  74. addSuccessMessage(t('Added provider and secret.'));
  75. onCreatedSecret(secret);
  76. onSetProvider(provider);
  77. queryClient.invalidateQueries({
  78. queryKey: makeFetchSecretQueryKey({orgSlug: organization.slug}),
  79. });
  80. },
  81. onError: error => {
  82. const message = t('Failed to add provider or secret.');
  83. handleXhrErrorResponse(message, error);
  84. addErrorMessage(message);
  85. },
  86. });
  87. const canRead = hasEveryAccess(['org:read'], {organization});
  88. const canWrite = hasEveryAccess(['org:write'], {organization});
  89. const canAdmin = hasEveryAccess(['org:admin'], {organization});
  90. const hasAccess = canRead || canWrite || canAdmin;
  91. return (
  92. <Form
  93. apiMethod="POST"
  94. initialData={initialData}
  95. apiEndpoint={`/organizations/${organization.slug}/flags/signing-secret/`}
  96. onSubmit={({provider, secret}) => {
  97. submitSecret({
  98. provider,
  99. secret,
  100. });
  101. }}
  102. onCancel={handleGoBack}
  103. submitLabel={t('Add Provider')}
  104. requireChanges
  105. submitDisabled={!hasAccess || isPending}
  106. >
  107. <SelectField
  108. required
  109. label={t('Provider')}
  110. onChange={setSelectedProvider}
  111. value={selectedProvider}
  112. placeholder={t('Select a provider')}
  113. name="provider"
  114. options={Object.values(WebhookProviderEnum).map(provider => ({
  115. value: provider,
  116. label: provider,
  117. }))}
  118. help={t(
  119. 'If you have already linked this provider, pasting a new secret will override the existing secret.'
  120. )}
  121. />
  122. <StyledFieldGroup
  123. label={t('Webhook URL')}
  124. help={
  125. Object.keys(PROVIDER_TO_SETUP_WEBHOOK_URL).includes(selectedProvider)
  126. ? tct(
  127. "Create a webhook integration with your [link:feature flag service]. When you do so, you'll need to enter this URL.",
  128. {
  129. link: (
  130. <ExternalLink
  131. href={
  132. PROVIDER_TO_SETUP_WEBHOOK_URL[
  133. selectedProvider as WebhookProviderEnum
  134. ]
  135. }
  136. />
  137. ),
  138. }
  139. )
  140. : t(
  141. "Create a webhook integration with your feature flag service. When you do so, you'll need to enter this URL."
  142. )
  143. }
  144. inline
  145. flexibleControlStateSize
  146. >
  147. <TextCopyInput
  148. aria-label={t('Webhook URL')}
  149. >{`https://sentry.io/api/0/organizations/${organization.slug}/flags/hooks/provider/${selectedProvider.toLowerCase()}/`}</TextCopyInput>
  150. </StyledFieldGroup>
  151. <TextField
  152. name="secret"
  153. label={t('Secret')}
  154. maxLength={32}
  155. minLength={32}
  156. required
  157. help={t(
  158. 'Paste the signing secret given by your provider when creating the webhook.'
  159. )}
  160. />
  161. </Form>
  162. );
  163. }
  164. const StyledFieldGroup = styled(FieldGroup)`
  165. padding: ${space(2)};
  166. `;