newProviderForm.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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 {browserHistory} from 'sentry/utils/browserHistory';
  19. import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
  20. import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
  21. import type RequestError from 'sentry/utils/requestError/requestError';
  22. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  23. import useApi from 'sentry/utils/useApi';
  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 [selectedProvider, setSelectedProvider] = useState('<provider_name>');
  46. const handleGoBack = useCallback(() => {
  47. browserHistory.push(normalizeUrl(`/settings/${organization.slug}/feature-flags/`));
  48. }, [organization.slug]);
  49. const {mutate: submitSecret, isPending} = useMutation<
  50. CreateSecretResponse,
  51. RequestError,
  52. CreateSecretQueryVariables
  53. >({
  54. mutationFn: ({provider, secret}) => {
  55. addLoadingMessage();
  56. return api.requestPromise(
  57. `/organizations/${organization.slug}/flags/signing-secrets/`,
  58. {
  59. method: 'POST',
  60. data: {
  61. provider: provider.toLowerCase(),
  62. secret,
  63. },
  64. }
  65. );
  66. },
  67. onSuccess: (_response, {secret, provider}) => {
  68. addSuccessMessage(t('Added provider and secret.'));
  69. onCreatedSecret(secret);
  70. onSetProvider(provider);
  71. queryClient.invalidateQueries({
  72. queryKey: makeFetchSecretQueryKey({orgSlug: organization.slug}),
  73. });
  74. },
  75. onError: error => {
  76. const message = t('Failed to add provider or secret.');
  77. handleXhrErrorResponse(message, error);
  78. addErrorMessage(message);
  79. },
  80. });
  81. const canRead = hasEveryAccess(['org:read'], {organization});
  82. const canWrite = hasEveryAccess(['org:write'], {organization});
  83. const canAdmin = hasEveryAccess(['org:admin'], {organization});
  84. const hasAccess = canRead || canWrite || canAdmin;
  85. return (
  86. <Form
  87. apiMethod="POST"
  88. initialData={initialData}
  89. apiEndpoint={`/organizations/${organization.slug}/flags/signing-secret/`}
  90. onSubmit={({provider, secret}) => {
  91. submitSecret({
  92. provider,
  93. secret,
  94. });
  95. }}
  96. onCancel={handleGoBack}
  97. submitLabel={t('Add Provider')}
  98. requireChanges
  99. submitDisabled={!hasAccess || isPending}
  100. >
  101. <SelectField
  102. required
  103. label={t('Provider')}
  104. onChange={setSelectedProvider}
  105. value={selectedProvider}
  106. placeholder={t('Select a provider')}
  107. name="provider"
  108. options={[{value: 'LaunchDarkly', label: 'LaunchDarkly'}]}
  109. help={t(
  110. 'If you have already linked this provider, pasting a new secret will override the existing secret.'
  111. )}
  112. />
  113. <StyledFieldGroup
  114. label={t('Webhook URL')}
  115. help={tct(
  116. "Create a webhook integration with your [link:feature flag service]. When you do so, you'll need to enter this URL.",
  117. {
  118. link: <ExternalLink href={PROVIDER_OPTION_TO_URLS[selectedProvider]} />,
  119. }
  120. )}
  121. inline
  122. flexibleControlStateSize
  123. >
  124. <TextCopyInput
  125. aria-label={t('Webhook URL')}
  126. >{`https://sentry.io/api/0/organizations/sentry/flags/hooks/provider/${selectedProvider.toLowerCase()}/`}</TextCopyInput>
  127. </StyledFieldGroup>
  128. <TextField
  129. name="secret"
  130. label={t('Secret')}
  131. maxLength={32}
  132. minLength={32}
  133. required
  134. help={t(
  135. 'Paste the signing secret given by your provider when creating the webhook.'
  136. )}
  137. />
  138. </Form>
  139. );
  140. }
  141. const StyledFieldGroup = styled(FieldGroup)`
  142. padding: ${space(2)};
  143. `;