newProviderForm.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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. ]}
  113. help={t(
  114. 'If you have already linked this provider, pasting a new secret will override the existing secret.'
  115. )}
  116. />
  117. <StyledFieldGroup
  118. label={t('Webhook URL')}
  119. help={tct(
  120. "Create a webhook integration with your [link:feature flag service]. When you do so, you'll need to enter this URL.",
  121. {
  122. link: <ExternalLink href={PROVIDER_OPTION_TO_URLS[selectedProvider]} />,
  123. }
  124. )}
  125. inline
  126. flexibleControlStateSize
  127. >
  128. <TextCopyInput
  129. aria-label={t('Webhook URL')}
  130. >{`https://sentry.io/api/0/organizations/${organization.slug}/flags/hooks/provider/${selectedProvider.toLowerCase()}/`}</TextCopyInput>
  131. </StyledFieldGroup>
  132. <TextField
  133. name="secret"
  134. label={t('Secret')}
  135. maxLength={32}
  136. minLength={32}
  137. required
  138. help={t(
  139. 'Paste the signing secret given by your provider when creating the webhook.'
  140. )}
  141. />
  142. </Form>
  143. );
  144. }
  145. const StyledFieldGroup = styled(FieldGroup)`
  146. padding: ${space(2)};
  147. `;