newProviderForm.tsx 5.2 KB

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