You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ConfigurationForm.tsx 6.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { Spinner } from '@sonarsource/echoes-react';
  21. import { ButtonPrimary, FlagMessage, Modal } from 'design-system';
  22. import { keyBy } from 'lodash';
  23. import * as React from 'react';
  24. import { FormattedMessage } from 'react-intl';
  25. import DocumentationLink from '../../../../components/common/DocumentationLink';
  26. import { translate } from '../../../../helpers/l10n';
  27. import { useSaveValuesMutation } from '../../../../queries/settings';
  28. import { AlmKeys } from '../../../../types/alm-settings';
  29. import { ProvisioningType } from '../../../../types/provisioning';
  30. import { Dict } from '../../../../types/types';
  31. import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
  32. import AuthenticationFormField from './AuthenticationFormField';
  33. import GitHubConfirmModal from './GitHubConfirmModal';
  34. import { SettingValue } from './hook/useConfiguration';
  35. import { isAllowToSignUpEnabled, isOrganizationListEmpty } from './hook/useGithubConfiguration';
  36. interface Props {
  37. canBeSave: boolean;
  38. create: boolean;
  39. excludedField: string[];
  40. hasLegacyConfiguration?: boolean;
  41. loading: boolean;
  42. onClose: () => void;
  43. provisioningStatus?: ProvisioningType;
  44. setNewValue: (key: string, value: string | boolean) => void;
  45. tab: AuthenticationTabs;
  46. values: Dict<SettingValue>;
  47. }
  48. interface ErrorValue {
  49. key: string;
  50. message: string;
  51. }
  52. export default function ConfigurationForm(props: Readonly<Props>) {
  53. const {
  54. canBeSave,
  55. create,
  56. excludedField,
  57. hasLegacyConfiguration,
  58. loading,
  59. provisioningStatus,
  60. setNewValue,
  61. tab,
  62. values,
  63. } = props;
  64. const [errors, setErrors] = React.useState<Dict<ErrorValue>>({});
  65. const [showConfirmModal, setShowConfirmModal] = React.useState(false);
  66. const { mutateAsync: changeConfig } = useSaveValuesMutation();
  67. const header = translate('settings.authentication.form', create ? 'create' : 'edit', tab);
  68. const handleSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
  69. event.preventDefault();
  70. if (canBeSave) {
  71. if (
  72. tab === AlmKeys.GitHub &&
  73. isOrganizationListEmpty(values) &&
  74. (provisioningStatus === ProvisioningType.auto || isAllowToSignUpEnabled(values))
  75. ) {
  76. setShowConfirmModal(true);
  77. } else {
  78. await onSave();
  79. }
  80. } else {
  81. const errors = Object.values(values)
  82. .filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory)
  83. .map((v) => ({ key: v.key, message: translate('field_required') }));
  84. setErrors(keyBy(errors, 'key'));
  85. }
  86. };
  87. const onSave = async () => {
  88. const data = await changeConfig(Object.values(values));
  89. const errors = data
  90. .filter(({ success }) => !success)
  91. .map(({ key }) => ({ key, message: translate('default_save_field_error_message') }));
  92. setErrors(keyBy(errors, 'key'));
  93. if (errors.length === 0) {
  94. props.onClose();
  95. }
  96. };
  97. const helpMessage = hasLegacyConfiguration ? `legacy_help.${tab}` : 'help';
  98. const FORM_ID = 'configuration-form';
  99. const formBody = (
  100. <form id={FORM_ID} onSubmit={handleSubmit}>
  101. <Spinner ariaLabel={translate('settings.authentication.form.loading')} isLoading={loading}>
  102. <FlagMessage
  103. className="sw-w-full sw-mb-8"
  104. variant={hasLegacyConfiguration ? 'warning' : 'info'}
  105. >
  106. <span>
  107. <FormattedMessage
  108. defaultMessage={translate(`settings.authentication.${helpMessage}`)}
  109. id={`settings.authentication.${helpMessage}`}
  110. values={{
  111. link: (
  112. <DocumentationLink
  113. to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES[tab]}/`}
  114. >
  115. {translate('settings.authentication.help.link')}
  116. </DocumentationLink>
  117. ),
  118. }}
  119. />
  120. </span>
  121. </FlagMessage>
  122. {Object.values(values).map((val) => {
  123. if (excludedField.includes(val.key)) {
  124. return null;
  125. }
  126. const isSet = hasLegacyConfiguration ? false : !val.isNotSet;
  127. return (
  128. <div key={val.key} className="sw-mb-8">
  129. <AuthenticationFormField
  130. definition={val.definition}
  131. error={errors[val.key]?.message}
  132. isNotSet={!isSet}
  133. mandatory={val.mandatory}
  134. onFieldChange={setNewValue}
  135. settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
  136. />
  137. </div>
  138. );
  139. })}
  140. </Spinner>
  141. </form>
  142. );
  143. return (
  144. <>
  145. <Modal
  146. body={formBody}
  147. headerTitle={header}
  148. isScrollable
  149. onClose={props.onClose}
  150. primaryButton={
  151. <ButtonPrimary form={FORM_ID} type="submit" autoFocus disabled={!canBeSave}>
  152. {translate('settings.almintegration.form.save')}
  153. <Spinner className="sw-ml-2" isLoading={loading} />
  154. </ButtonPrimary>
  155. }
  156. />
  157. {showConfirmModal && (
  158. <GitHubConfirmModal
  159. onClose={() => setShowConfirmModal(false)}
  160. onConfirm={onSave}
  161. provisioningStatus={provisioningStatus ?? ProvisioningType.jit}
  162. values={values}
  163. />
  164. )}
  165. </>
  166. );
  167. }