]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19084 Improve github authentication setting
authorMathieu Suen <mathieu.suen@sonarsource.com>
Mon, 24 Apr 2023 15:18:45 +0000 (17:18 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 11 May 2023 20:03:13 +0000 (20:03 +0000)
21 files changed:
server/sonar-web/src/main/js/app/components/extensions/CreateApplicationForm.tsx
server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthentication.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlConfigurationForm.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlFormField.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlSecuredField.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlToggleField.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useLoadSamlSettings.ts
server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 674dc5bd20ed8ca1c0f022bc3d24373579a9fe9f..450c50989d7e7d0c8173687a5ae6edc2a42f8f5f 100644 (file)
@@ -19,9 +19,9 @@
  */
 import * as React from 'react';
 import { createApplication } from '../../../api/application';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Radio from '../../../components/controls/Radio';
 import SimpleModal from '../../../components/controls/SimpleModal';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
@@ -104,7 +104,7 @@ export default class CreateApplicationForm extends React.PureComponent<Props, St
         size="small"
       >
         {({ onCloseClick, onFormSubmit, submitting }) => (
-          <form className="views-form" onSubmit={onFormSubmit}>
+          <form onSubmit={onFormSubmit}>
             <div className="modal-head">
               <h2>{header}</h2>
             </div>
index eca852deb5190e686b73b4169e2b48dee66bf23e..a193f2cd305dd5645c453a3a04ab313cd838b523 100644 (file)
@@ -18,7 +18,6 @@ exports[`should render correctly: form 1`] = `
   size="small"
 >
   <form
-    className="views-form"
     onSubmit={[Function]}
   >
     <div
index 238b25851920b0feb5271d83bf0d776d172ef12a..5e95c87d2c389259819345a9bcf219a9a2b40ce5 100644 (file)
@@ -18,8 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
 import Modal from '../../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
 import { Alert } from '../../../../components/ui/Alert';
 import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
 import { translate } from '../../../../helpers/l10n';
@@ -112,7 +112,7 @@ export default class AlmBindingDefinitionFormRenderer extends React.PureComponen
         shouldCloseOnOverlayClick={false}
         size="medium"
       >
-        <form className="views-form" onSubmit={handleSubmit}>
+        <form onSubmit={handleSubmit}>
           <div className="modal-head">
             <h2>{header}</h2>
           </div>
index 361c2495e633f6e3fcb2e223903cd88540e97863..a10aa8293f26d030f20aad94c73bce4b7c6aa4af 100644 (file)
@@ -37,7 +37,8 @@ import { Feature } from '../../../../types/features';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { AUTHENTICATION_CATEGORY } from '../../constants';
 import CategoryDefinitionsList from '../CategoryDefinitionsList';
-import SamlAuthentication, { SAML } from './SamlAuthentication';
+import GithubAithentication from './GithubAutheticationTab';
+import SamlAuthenticationTab, { SAML } from './SamlAuthenticationTab';
 
 interface Props {
   definitions: ExtendedSettingDefinition[];
@@ -52,7 +53,7 @@ export type AuthenticationTabs =
   | AlmKeys.GitLab
   | AlmKeys.BitbucketServer;
 
-const DOCUMENTATION_LINK_SUFFIXES = {
+export const DOCUMENTATION_LINK_SUFFIXES = {
   [SAML]: 'saml/overview',
   [AlmKeys.GitHub]: 'github',
   [AlmKeys.GitLab]: 'gitlab',
@@ -109,7 +110,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
         </>
       ),
     },
-  ];
+  ] as const;
 
   return (
     <>
@@ -151,7 +152,10 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
             {tabs.map((tab) => (
               <div
                 style={{
-                  maxHeight: tab.key !== SAML ? `calc(100vh - ${top + HEIGHT_ADJUSTMENT}px)` : '',
+                  maxHeight:
+                    tab.key !== SAML && tab.key !== AlmKeys.GitHub
+                      ? `calc(100vh - ${top + HEIGHT_ADJUSTMENT}px)`
+                      : '',
                 }}
                 className={classNames('bordered overflow-y-auto tabbed-definitions', {
                   hidden: currentTab !== tab.key,
@@ -162,9 +166,19 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
                 id={getTabPanelId(tab.key)}
               >
                 <div className="big-padded-top big-padded-left big-padded-right">
-                  {tab.key === SAML && <SamlAuthentication definitions={definitions} />}
+                  {tab.key === SAML && (
+                    <SamlAuthenticationTab
+                      definitions={definitions.filter((def) => def.subCategory === SAML)}
+                    />
+                  )}
+
+                  {tab.key === AlmKeys.GitHub && (
+                    <GithubAithentication
+                      definitions={definitions.filter((def) => def.subCategory === AlmKeys.GitHub)}
+                    />
+                  )}
 
-                  {tab.key !== SAML && (
+                  {tab.key !== SAML && tab.key !== AlmKeys.GitHub && (
                     <>
                       <Alert variant="info">
                         <FormattedMessage
@@ -174,7 +188,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
                             link: (
                               <DocLink
                                 to={`/instance-administration/authentication/${
-                                  DOCUMENTATION_LINK_SUFFIXES[tab.key as AuthenticationTabs]
+                                  DOCUMENTATION_LINK_SUFFIXES[tab.key]
                                 }/`}
                               >
                                 {translate('settings.authentication.help.link')}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx
new file mode 100644 (file)
index 0000000..8570658
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import ValidationInput, {
+  ValidationInputErrorPlacement,
+} from '../../../../components/controls/ValidationInput';
+import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
+import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
+import { isSecuredDefinition } from '../../utils';
+import AuthenticationSecuredField from './AuthenticationSecuredField';
+import AuthenticationToggleField from './AuthenticationToggleField';
+
+interface SamlToggleFieldProps {
+  settingValue?: string | boolean;
+  definition: ExtendedSettingDefinition;
+  mandatory?: boolean;
+  onFieldChange: (key: string, value: string | boolean) => void;
+  isNotSet: boolean;
+  error?: string;
+}
+
+export default function AuthenticationFormField(props: SamlToggleFieldProps) {
+  const { mandatory = false, definition, settingValue, isNotSet, error } = props;
+
+  return (
+    <div className="settings-definition">
+      <div className="settings-definition-left">
+        <label className="h3" htmlFor={definition.key}>
+          {definition.name}
+        </label>
+        {mandatory && <MandatoryFieldMarker />}
+        {definition.description && (
+          <div className="markdown small spacer-top">{definition.description}</div>
+        )}
+      </div>
+      <div className="settings-definition-right big-padded-top display-flex-column">
+        {isSecuredDefinition(definition) && (
+          <AuthenticationSecuredField
+            definition={definition}
+            settingValue={String(settingValue ?? '')}
+            onFieldChange={props.onFieldChange}
+            isNotSet={isNotSet}
+          />
+        )}
+        {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && (
+          <AuthenticationToggleField
+            definition={definition}
+            settingValue={settingValue}
+            onChange={(value) => props.onFieldChange(definition.key, value)}
+          />
+        )}
+        {!isSecuredDefinition(definition) && definition.type === undefined && (
+          <ValidationInput
+            error={error}
+            errorPlacement={ValidationInputErrorPlacement.Bottom}
+            isValid={false}
+            isInvalid={Boolean(error)}
+          >
+            <input
+              className="width-100"
+              id={definition.key}
+              maxLength={4000}
+              name={definition.key}
+              onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
+              type="text"
+              value={String(settingValue ?? '')}
+            />
+          </ValidationInput>
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx
new file mode 100644 (file)
index 0000000..ed1345b
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React, { useEffect } from 'react';
+import { ButtonLink } from '../../../../components/controls/buttons';
+import { translate } from '../../../../helpers/l10n';
+import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
+import { isSecuredDefinition } from '../../utils';
+
+interface SamlToggleFieldProps {
+  onFieldChange: (key: string, value: string) => void;
+  settingValue?: string;
+  definition: ExtendedSettingDefinition;
+  optional?: boolean;
+  isNotSet: boolean;
+}
+
+export default function AuthenticationSecuredField(props: SamlToggleFieldProps) {
+  const { settingValue, definition, optional = true, isNotSet } = props;
+  const [showSecretField, setShowSecretField] = React.useState(
+    !isNotSet && isSecuredDefinition(definition)
+  );
+
+  useEffect(() => {
+    setShowSecretField(!isNotSet && isSecuredDefinition(definition));
+  }, [isNotSet, definition]);
+
+  return (
+    <>
+      {!showSecretField &&
+        (definition.type === SettingType.TEXT ? (
+          <textarea
+            className="width-100"
+            id={definition.key}
+            maxLength={4000}
+            onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
+            required={!optional}
+            rows={5}
+            value={settingValue ?? ''}
+          />
+        ) : (
+          <input
+            className="width-100"
+            id={definition.key}
+            maxLength={4000}
+            name={definition.key}
+            onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
+            type="text"
+            value={String(settingValue ?? '')}
+          />
+        ))}
+      {showSecretField && (
+        <div>
+          <p>{translate('settings.almintegration.form.secret.field')}</p>
+          <ButtonLink
+            onClick={() => {
+              setShowSecretField(false);
+            }}
+          >
+            {translate('settings.almintegration.form.secret.update_field')}
+          </ButtonLink>
+        </div>
+      )}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx
new file mode 100644 (file)
index 0000000..40b71d6
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import Toggle from '../../../../components/controls/Toggle';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+
+interface SamlToggleFieldProps {
+  onChange: (value: boolean) => void;
+  settingValue?: string | boolean;
+  definition: ExtendedSettingDefinition;
+}
+
+export default function AuthenticationToggleField(props: SamlToggleFieldProps) {
+  const { settingValue, definition } = props;
+
+  return <Toggle name={definition.key} onChange={props.onChange} value={settingValue ?? ''} />;
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx
new file mode 100644 (file)
index 0000000..5920471
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { isEmpty, keyBy } from 'lodash';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { setSettingValue } from '../../../../api/settings';
+import DocLink from '../../../../components/common/DocLink';
+import Modal from '../../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
+import { Alert } from '../../../../components/ui/Alert';
+import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
+import { translate } from '../../../../helpers/l10n';
+import { Dict } from '../../../../types/types';
+import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
+import AuthenticationFormField from './AuthenticationFormField';
+import { SettingValue } from './hook/useConfiguration';
+
+interface Props {
+  create: boolean;
+  loading: boolean;
+  values: Dict<SettingValue>;
+  setNewValue: (key: string, value: string | boolean) => void;
+  canBeSave: boolean;
+  onClose: () => void;
+  onReload: () => Promise<void>;
+  tab: AuthenticationTabs;
+  excludedField: string[];
+}
+
+interface ErrorValue {
+  key: string;
+  message: string;
+}
+
+export default function ConfigurationForm(props: Props) {
+  const { create, loading, values, setNewValue, canBeSave, tab, excludedField } = props;
+  const [errors, setErrors] = React.useState<Dict<ErrorValue>>({});
+
+  const headerLabel = translate('settings.authentication.form', create ? 'create' : 'edit', tab);
+
+  const handleSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
+
+    if (canBeSave) {
+      const r = await Promise.all(
+        Object.values(values)
+          .filter((v) => v.newValue !== undefined)
+          .map(async ({ key, newValue, definition }) => {
+            try {
+              await setSettingValue(definition, newValue);
+              return { key, success: true };
+            } catch (error) {
+              return { key, success: false };
+            }
+          })
+      );
+      const errors = r
+        .filter(({ success }) => !success)
+        .map(({ key }) => ({ key, message: translate('default_save_field_error_message') }));
+      setErrors(keyBy(errors, 'key'));
+      if (isEmpty(errors)) {
+        await props.onReload();
+        props.onClose();
+      }
+    } else {
+      const errors = Object.values(values)
+        .filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory)
+        .map((v) => ({ key: v.key, message: translate('field_required') }));
+      setErrors(keyBy(errors, 'key'));
+    }
+  };
+
+  return (
+    <Modal contentLabel={headerLabel} shouldCloseOnOverlayClick={false} size="medium">
+      <form onSubmit={handleSubmit}>
+        <div className="modal-head">
+          <h2>{headerLabel}</h2>
+        </div>
+        <div className="modal-body modal-container">
+          <DeferredSpinner
+            loading={loading}
+            ariaLabel={translate('settings.authentication.form.loading')}
+          >
+            <Alert variant="info">
+              <FormattedMessage
+                id="settings.authentication.help"
+                defaultMessage={translate('settings.authentication.help')}
+                values={{
+                  link: (
+                    <DocLink
+                      to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES[tab]}/`}
+                    >
+                      {translate('settings.authentication.help.link')}
+                    </DocLink>
+                  ),
+                }}
+              />
+            </Alert>
+            {Object.values(values).map((val) => {
+              if (excludedField.includes(val.key)) {
+                return null;
+              }
+              return (
+                <div key={val.key}>
+                  <AuthenticationFormField
+                    settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
+                    definition={val.definition}
+                    mandatory={val.mandatory}
+                    onFieldChange={setNewValue}
+                    isNotSet={val.isNotSet}
+                    error={errors[val.key]?.message}
+                  />
+                </div>
+              );
+            })}
+          </DeferredSpinner>
+        </div>
+
+        <div className="modal-foot">
+          <SubmitButton disabled={!canBeSave}>
+            {translate('settings.almintegration.form.save')}
+            <DeferredSpinner className="spacer-left" loading={loading} />
+          </SubmitButton>
+          <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
+        </div>
+      </form>
+    </Modal>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAutheticationTab.tsx
new file mode 100644 (file)
index 0000000..837ef38
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { setSettingValue } from '../../../../api/settings';
+import { Button } from '../../../../components/controls/buttons';
+import CheckIcon from '../../../../components/icons/CheckIcon';
+import DeleteIcon from '../../../../components/icons/DeleteIcon';
+import EditIcon from '../../../../components/icons/EditIcon';
+import { translate } from '../../../../helpers/l10n';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+import ConfigurationForm from './ConfigurationForm';
+import useGithubConfiguration, { GITHUB_ENABLED_FIELD } from './hook/useGithubConfiguration';
+
+interface SamlAuthenticationProps {
+  definitions: ExtendedSettingDefinition[];
+}
+
+const GITHUB_EXCLUDED_FIELD = [
+  'sonar.auth.github.enabled',
+  'sonar.auth.github.groupsSync',
+  'sonar.auth.github.allowUsersToSignUp',
+];
+
+export default function GithubAithentication(props: SamlAuthenticationProps) {
+  const [showEditModal, setShowEditModal] = React.useState(false);
+  const {
+    hasConfiguration,
+    loading,
+    values,
+    setNewValue,
+    canBeSave,
+    reload,
+    url,
+    appId,
+    enabled,
+    deleteConfiguration,
+  } = useGithubConfiguration(props.definitions);
+
+  const handleCreateConfiguration = () => {
+    setShowEditModal(true);
+  };
+
+  const handleCancelConfiguration = () => {
+    setShowEditModal(false);
+  };
+
+  const handleToggleEnable = async () => {
+    const value = values[GITHUB_ENABLED_FIELD];
+    await setSettingValue(value.definition, !enabled);
+    await reload();
+  };
+
+  return (
+    <div className="saml-configuration">
+      <div className="spacer-bottom display-flex-space-between display-flex-center">
+        <h4>{translate('settings.authentication.github.configuration')}</h4>
+
+        {!hasConfiguration && (
+          <div>
+            <Button onClick={handleCreateConfiguration}>
+              {translate('settings.authentication.form.create')}
+            </Button>
+          </div>
+        )}
+      </div>
+      {!hasConfiguration ? (
+        <div className="big-padded text-center huge-spacer-bottom saml-no-config">
+          {translate('settings.authentication.github.form.not_configured')}
+        </div>
+      ) : (
+        <>
+          <div className="spacer-bottom big-padded bordered display-flex-space-between">
+            <div>
+              <h5>{appId}</h5>
+              <p>{url}</p>
+              <p className="big-spacer-top big-spacer-bottom">
+                {enabled ? (
+                  <span className="saml-enabled spacer-left">
+                    <CheckIcon className="spacer-right" />
+                    {translate('settings.authentication.saml.form.enabled')}
+                  </span>
+                ) : (
+                  translate('settings.authentication.saml.form.not_enabled')
+                )}
+              </p>
+              <Button className="spacer-top" onClick={handleToggleEnable}>
+                {enabled
+                  ? translate('settings.authentication.saml.form.disable')
+                  : translate('settings.authentication.saml.form.enable')}
+              </Button>
+            </div>
+            <div>
+              <Button className="spacer-right" onClick={handleCreateConfiguration}>
+                <EditIcon />
+                {translate('settings.authentication.form.edit')}
+              </Button>
+              <Button className="button-red" disabled={enabled} onClick={deleteConfiguration}>
+                <DeleteIcon />
+                {translate('settings.authentication.form.delete')}
+              </Button>
+            </div>
+          </div>
+          <div className="spacer-bottom big-padded bordered display-flex-space-between">
+            Provisioning TODO
+          </div>
+        </>
+      )}
+
+      {showEditModal && (
+        <ConfigurationForm
+          tab={AlmKeys.GitHub}
+          excludedField={GITHUB_EXCLUDED_FIELD}
+          loading={loading}
+          values={values}
+          setNewValue={setNewValue}
+          canBeSave={canBeSave}
+          onClose={handleCancelConfiguration}
+          create={!hasConfiguration}
+          onReload={reload}
+        />
+      )}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthentication.tsx
deleted file mode 100644 (file)
index e6e123b..0000000
+++ /dev/null
@@ -1,333 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { isEmpty } from 'lodash';
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import {
-  activateScim,
-  deactivateScim,
-  resetSettingValue,
-  setSettingValue,
-} from '../../../../api/settings';
-import DocLink from '../../../../components/common/DocLink';
-import Link from '../../../../components/common/Link';
-import ConfirmModal from '../../../../components/controls/ConfirmModal';
-import RadioCard from '../../../../components/controls/RadioCard';
-import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import CheckIcon from '../../../../components/icons/CheckIcon';
-import DeleteIcon from '../../../../components/icons/DeleteIcon';
-import EditIcon from '../../../../components/icons/EditIcon';
-import { Alert } from '../../../../components/ui/Alert';
-import { translate } from '../../../../helpers/l10n';
-import { getBaseUrl } from '../../../../helpers/system';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
-import { getPropertyName } from '../../utils';
-import DefinitionDescription from '../DefinitionDescription';
-import SamlConfigurationForm from './SamlConfigurationForm';
-import useSamlConfiguration, { SAML_ENABLED_FIELD } from './hook/useLoadSamlSettings';
-
-interface SamlAuthenticationProps {
-  definitions: ExtendedSettingDefinition[];
-}
-
-export const SAML = 'saml';
-
-const CONFIG_TEST_PATH = '/saml/validation_init';
-
-export default function SamlAuthentication(props: SamlAuthenticationProps) {
-  const { definitions } = props;
-  const [showEditModal, setShowEditModal] = React.useState(false);
-  const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = React.useState(false);
-  const {
-    hasScim,
-    scimStatus,
-    loading,
-    samlEnabled,
-    name,
-    groupValue,
-    url,
-    hasConfiguration,
-    values,
-    setNewValue,
-    canBeSave,
-    hasScimConfigChange,
-    newScimStatus,
-    setNewScimStatus,
-    setNewGroupSetting,
-    onReload,
-  } = useSamlConfiguration(definitions);
-
-  const handleDeleteConfiguration = async () => {
-    await resetSettingValue({ keys: Object.keys(values).join(',') });
-    await onReload();
-  };
-
-  const handleCreateConfiguration = () => {
-    setShowEditModal(true);
-  };
-
-  const handleCancelConfiguration = () => {
-    setShowEditModal(false);
-  };
-
-  const handleToggleEnable = async () => {
-    const value = values[SAML_ENABLED_FIELD];
-    await setSettingValue(value.definition, !samlEnabled);
-    await onReload();
-  };
-
-  const handleSaveGroup = async () => {
-    if (groupValue.newValue !== undefined) {
-      if (isEmpty(groupValue.newValue)) {
-        await resetSettingValue({ keys: groupValue.definition.key });
-      } else {
-        await setSettingValue(groupValue.definition, groupValue.newValue);
-      }
-      await onReload();
-    }
-  };
-
-  const handleConfirmChangeProvisioning = async () => {
-    if (newScimStatus) {
-      await activateScim();
-    } else {
-      await deactivateScim();
-      await handleSaveGroup();
-    }
-    await onReload();
-  };
-
-  return (
-    <div className="saml-configuration">
-      <div className="spacer-bottom display-flex-space-between display-flex-center">
-        <h4>{translate('settings.authentication.saml.configuration')}</h4>
-
-        {!hasConfiguration && (
-          <div>
-            <Button onClick={handleCreateConfiguration}>
-              {translate('settings.authentication.form.create')}
-            </Button>
-          </div>
-        )}
-      </div>
-      {!hasConfiguration && (
-        <div className="big-padded text-center huge-spacer-bottom saml-no-config">
-          {translate('settings.authentication.saml.form.not_configured')}
-        </div>
-      )}
-
-      {hasConfiguration && (
-        <>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <div>
-              <h5>{name}</h5>
-              <p>{url}</p>
-              <p className="big-spacer-top big-spacer-bottom">
-                {samlEnabled ? (
-                  <span className="saml-enabled spacer-left">
-                    <CheckIcon className="spacer-right" />
-                    {translate('settings.authentication.saml.form.enabled')}
-                  </span>
-                ) : (
-                  translate('settings.authentication.saml.form.not_enabled')
-                )}
-              </p>
-              <Button className="spacer-top" disabled={scimStatus} onClick={handleToggleEnable}>
-                {samlEnabled
-                  ? translate('settings.authentication.saml.form.disable')
-                  : translate('settings.authentication.saml.form.enable')}
-              </Button>
-            </div>
-            <div>
-              <Link
-                className="button spacer-right"
-                target="_blank"
-                to={`${getBaseUrl()}${CONFIG_TEST_PATH}`}
-              >
-                {translate('settings.authentication.saml.form.test')}
-              </Link>
-              <Button className="spacer-right" onClick={handleCreateConfiguration}>
-                <EditIcon />
-                {translate('settings.authentication.form.edit')}
-              </Button>
-              <Button
-                className="button-red"
-                disabled={samlEnabled}
-                onClick={handleDeleteConfiguration}
-              >
-                <DeleteIcon />
-                {translate('settings.authentication.form.delete')}
-              </Button>
-            </div>
-          </div>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <form
-              onSubmit={(e) => {
-                e.preventDefault();
-                if (newScimStatus !== scimStatus) {
-                  setShowConfirmProvisioningModal(true);
-                } else {
-                  handleSaveGroup();
-                }
-              }}
-            >
-              <fieldset className="display-flex-column big-spacer-bottom">
-                <label className="h5">
-                  {translate('settings.authentication.saml.form.provisioning')}
-                </label>
-                {samlEnabled ? (
-                  <div className="display-flex-row spacer-top">
-                    <RadioCard
-                      label={translate('settings.authentication.saml.form.provisioning_with_scim')}
-                      title={translate('settings.authentication.saml.form.provisioning_with_scim')}
-                      selected={newScimStatus ?? scimStatus}
-                      onClick={() => setNewScimStatus(true)}
-                      disabled={!hasScim}
-                    >
-                      {!hasScim ? (
-                        <p>
-                          <FormattedMessage
-                            id="settings.authentication.saml.form.provisioning.disabled"
-                            defaultMessage={translate(
-                              'settings.authentication.saml.form.provisioning.disabled'
-                            )}
-                            values={{
-                              documentation: (
-                                <DocLink to="/instance-administration/authentication/saml/scim/overview">
-                                  {translate('documentation')}
-                                </DocLink>
-                              ),
-                            }}
-                          />
-                        </p>
-                      ) : (
-                        <>
-                          <p className="spacer-bottom">
-                            {translate(
-                              'settings.authentication.saml.form.provisioning_with_scim.sub'
-                            )}
-                          </p>
-                          <p className="spacer-bottom">
-                            {translate(
-                              'settings.authentication.saml.form.provisioning_with_scim.description'
-                            )}
-                          </p>
-                          <p>
-                            <FormattedMessage
-                              id="settings.authentication.saml.form.provisioning_with_scim.description.doc"
-                              defaultMessage={translate(
-                                'settings.authentication.saml.form.provisioning_with_scim.description.doc'
-                              )}
-                              values={{
-                                documentation: (
-                                  <DocLink to="/instance-administration/authentication/saml/scim/overview">
-                                    {translate('documentation')}
-                                  </DocLink>
-                                ),
-                              }}
-                            />
-                          </p>
-                        </>
-                      )}
-                    </RadioCard>
-                    <RadioCard
-                      label={translate('settings.authentication.saml.form.provisioning_at_login')}
-                      title={translate('settings.authentication.saml.form.provisioning_at_login')}
-                      selected={!(newScimStatus ?? scimStatus)}
-                      onClick={() => setNewScimStatus(false)}
-                    >
-                      <p>
-                        {translate('settings.authentication.saml.form.provisioning_at_login.sub')}
-                      </p>
-                      {groupValue && (
-                        <div className="settings-definition">
-                          <DefinitionDescription definition={groupValue.definition} />
-                          <div className="settings-definition-right">
-                            <input
-                              id={groupValue.definition.key}
-                              maxLength={4000}
-                              name={groupValue.definition.key}
-                              onChange={(e) => setNewGroupSetting(e.currentTarget.value)}
-                              type="text"
-                              value={String(groupValue.newValue ?? groupValue.value ?? '')}
-                              aria-label={getPropertyName(groupValue.definition)}
-                            />
-                          </div>
-                        </div>
-                      )}
-                    </RadioCard>
-                  </div>
-                ) : (
-                  <Alert className="big-spacer-top" variant="info">
-                    {translate('settings.authentication.saml.enable_first')}
-                  </Alert>
-                )}
-              </fieldset>
-              {samlEnabled && (
-                <>
-                  <SubmitButton disabled={!hasScimConfigChange}>{translate('save')}</SubmitButton>
-                  <ResetButtonLink
-                    className="spacer-left"
-                    onClick={() => {
-                      setNewScimStatus(undefined);
-                      setNewGroupSetting();
-                    }}
-                    disabled={!hasScimConfigChange}
-                  >
-                    {translate('cancel')}
-                  </ResetButtonLink>
-                </>
-              )}
-              {showConfirmProvisioningModal && (
-                <ConfirmModal
-                  onConfirm={() => handleConfirmChangeProvisioning()}
-                  header={translate(
-                    'settings.authentication.saml.confirm',
-                    newScimStatus ? 'scim' : 'jit'
-                  )}
-                  onClose={() => setShowConfirmProvisioningModal(false)}
-                  isDestructive={!newScimStatus}
-                  confirmButtonText={translate('yes')}
-                >
-                  {translate(
-                    'settings.authentication.saml.confirm',
-                    newScimStatus ? 'scim' : 'jit',
-                    'description'
-                  )}
-                </ConfirmModal>
-              )}
-            </form>
-          </div>
-        </>
-      )}
-      {showEditModal && (
-        <SamlConfigurationForm
-          loading={loading}
-          values={values}
-          setNewValue={setNewValue}
-          canBeSave={canBeSave}
-          onClose={handleCancelConfiguration}
-          create={!hasConfiguration}
-          onReload={onReload}
-        />
-      )}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
new file mode 100644 (file)
index 0000000..43ab887
--- /dev/null
@@ -0,0 +1,332 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { isEmpty } from 'lodash';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import {
+  activateScim,
+  deactivateScim,
+  resetSettingValue,
+  setSettingValue,
+} from '../../../../api/settings';
+import DocLink from '../../../../components/common/DocLink';
+import Link from '../../../../components/common/Link';
+import ConfirmModal from '../../../../components/controls/ConfirmModal';
+import RadioCard from '../../../../components/controls/RadioCard';
+import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
+import CheckIcon from '../../../../components/icons/CheckIcon';
+import DeleteIcon from '../../../../components/icons/DeleteIcon';
+import EditIcon from '../../../../components/icons/EditIcon';
+import { Alert } from '../../../../components/ui/Alert';
+import { translate } from '../../../../helpers/l10n';
+import { getBaseUrl } from '../../../../helpers/system';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { getPropertyName } from '../../utils';
+import DefinitionDescription from '../DefinitionDescription';
+import ConfigurationForm from './ConfigurationForm';
+import useSamlConfiguration, {
+  SAML_ENABLED_FIELD,
+  SAML_GROUP_NAME,
+  SAML_SCIM_DEPRECATED,
+} from './hook/useLoadSamlSettings';
+
+interface SamlAuthenticationProps {
+  definitions: ExtendedSettingDefinition[];
+}
+
+export const SAML = 'saml';
+
+const CONFIG_TEST_PATH = '/saml/validation_init';
+const SAML_EXCLUDED_FIELD = [SAML_ENABLED_FIELD, SAML_GROUP_NAME, SAML_SCIM_DEPRECATED];
+
+export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
+  const { definitions } = props;
+  const [showEditModal, setShowEditModal] = React.useState(false);
+  const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = React.useState(false);
+  const {
+    hasScim,
+    scimStatus,
+    loading,
+    samlEnabled,
+    name,
+    groupValue,
+    url,
+    hasConfiguration,
+    values,
+    setNewValue,
+    canBeSave,
+    hasScimConfigChange,
+    newScimStatus,
+    setNewScimStatus,
+    setNewGroupSetting,
+    reload,
+    deleteConfiguration,
+  } = useSamlConfiguration(definitions);
+
+  const handleCreateConfiguration = () => {
+    setShowEditModal(true);
+  };
+
+  const handleCancelConfiguration = () => {
+    setShowEditModal(false);
+  };
+
+  const handleToggleEnable = async () => {
+    const value = values[SAML_ENABLED_FIELD];
+    await setSettingValue(value.definition, !samlEnabled);
+    await reload();
+  };
+
+  const handleSaveGroup = async () => {
+    if (groupValue.newValue !== undefined) {
+      if (isEmpty(groupValue.newValue)) {
+        await resetSettingValue({ keys: groupValue.definition.key });
+      } else {
+        await setSettingValue(groupValue.definition, groupValue.newValue);
+      }
+      await reload();
+    }
+  };
+
+  const handleConfirmChangeProvisioning = async () => {
+    if (newScimStatus) {
+      await activateScim();
+    } else {
+      await deactivateScim();
+      await handleSaveGroup();
+    }
+    await reload();
+  };
+
+  return (
+    <div className="saml-configuration">
+      <div className="spacer-bottom display-flex-space-between display-flex-center">
+        <h4>{translate('settings.authentication.saml.configuration')}</h4>
+
+        {!hasConfiguration && (
+          <div>
+            <Button onClick={handleCreateConfiguration}>
+              {translate('settings.authentication.form.create')}
+            </Button>
+          </div>
+        )}
+      </div>
+      {!hasConfiguration && (
+        <div className="big-padded text-center huge-spacer-bottom saml-no-config">
+          {translate('settings.authentication.saml.form.not_configured')}
+        </div>
+      )}
+
+      {hasConfiguration && (
+        <>
+          <div className="spacer-bottom big-padded bordered display-flex-space-between">
+            <div>
+              <h5>{name}</h5>
+              <p>{url}</p>
+              <p className="big-spacer-top big-spacer-bottom">
+                {samlEnabled ? (
+                  <span className="saml-enabled spacer-left">
+                    <CheckIcon className="spacer-right" />
+                    {translate('settings.authentication.saml.form.enabled')}
+                  </span>
+                ) : (
+                  translate('settings.authentication.saml.form.not_enabled')
+                )}
+              </p>
+              <Button className="spacer-top" disabled={scimStatus} onClick={handleToggleEnable}>
+                {samlEnabled
+                  ? translate('settings.authentication.saml.form.disable')
+                  : translate('settings.authentication.saml.form.enable')}
+              </Button>
+            </div>
+            <div>
+              <Link
+                className="button spacer-right"
+                target="_blank"
+                to={`${getBaseUrl()}${CONFIG_TEST_PATH}`}
+              >
+                {translate('settings.authentication.saml.form.test')}
+              </Link>
+              <Button className="spacer-right" onClick={handleCreateConfiguration}>
+                <EditIcon />
+                {translate('settings.authentication.form.edit')}
+              </Button>
+              <Button className="button-red" disabled={samlEnabled} onClick={deleteConfiguration}>
+                <DeleteIcon />
+                {translate('settings.authentication.form.delete')}
+              </Button>
+            </div>
+          </div>
+          <div className="spacer-bottom big-padded bordered display-flex-space-between">
+            <form
+              onSubmit={(e) => {
+                e.preventDefault();
+                if (newScimStatus !== scimStatus) {
+                  setShowConfirmProvisioningModal(true);
+                } else {
+                  handleSaveGroup();
+                }
+              }}
+            >
+              <fieldset className="display-flex-column big-spacer-bottom">
+                <label className="h5">
+                  {translate('settings.authentication.saml.form.provisioning')}
+                </label>
+                {samlEnabled ? (
+                  <div className="display-flex-row spacer-top">
+                    <RadioCard
+                      label={translate('settings.authentication.saml.form.provisioning_with_scim')}
+                      title={translate('settings.authentication.saml.form.provisioning_with_scim')}
+                      selected={newScimStatus ?? scimStatus}
+                      onClick={() => setNewScimStatus(true)}
+                      disabled={!hasScim}
+                    >
+                      {!hasScim ? (
+                        <p>
+                          <FormattedMessage
+                            id="settings.authentication.saml.form.provisioning.disabled"
+                            defaultMessage={translate(
+                              'settings.authentication.saml.form.provisioning.disabled'
+                            )}
+                            values={{
+                              documentation: (
+                                <DocLink to="/instance-administration/authentication/saml/scim/overview">
+                                  {translate('documentation')}
+                                </DocLink>
+                              ),
+                            }}
+                          />
+                        </p>
+                      ) : (
+                        <>
+                          <p className="spacer-bottom">
+                            {translate(
+                              'settings.authentication.saml.form.provisioning_with_scim.sub'
+                            )}
+                          </p>
+                          <p className="spacer-bottom">
+                            {translate(
+                              'settings.authentication.saml.form.provisioning_with_scim.description'
+                            )}
+                          </p>
+                          <p>
+                            <FormattedMessage
+                              id="settings.authentication.saml.form.provisioning_with_scim.description.doc"
+                              defaultMessage={translate(
+                                'settings.authentication.saml.form.provisioning_with_scim.description.doc'
+                              )}
+                              values={{
+                                documentation: (
+                                  <DocLink to="/instance-administration/authentication/saml/scim/overview">
+                                    {translate('documentation')}
+                                  </DocLink>
+                                ),
+                              }}
+                            />
+                          </p>
+                        </>
+                      )}
+                    </RadioCard>
+                    <RadioCard
+                      label={translate('settings.authentication.saml.form.provisioning_at_login')}
+                      title={translate('settings.authentication.saml.form.provisioning_at_login')}
+                      selected={!(newScimStatus ?? scimStatus)}
+                      onClick={() => setNewScimStatus(false)}
+                    >
+                      <p>
+                        {translate('settings.authentication.saml.form.provisioning_at_login.sub')}
+                      </p>
+                      {groupValue && (
+                        <div className="settings-definition">
+                          <DefinitionDescription definition={groupValue.definition} />
+                          <div className="settings-definition-right">
+                            <input
+                              id={groupValue.definition.key}
+                              maxLength={4000}
+                              name={groupValue.definition.key}
+                              onChange={(e) => setNewGroupSetting(e.currentTarget.value)}
+                              type="text"
+                              value={String(groupValue.newValue ?? groupValue.value ?? '')}
+                              aria-label={getPropertyName(groupValue.definition)}
+                            />
+                          </div>
+                        </div>
+                      )}
+                    </RadioCard>
+                  </div>
+                ) : (
+                  <Alert className="big-spacer-top" variant="info">
+                    {translate('settings.authentication.saml.enable_first')}
+                  </Alert>
+                )}
+              </fieldset>
+              {samlEnabled && (
+                <>
+                  <SubmitButton disabled={!hasScimConfigChange}>{translate('save')}</SubmitButton>
+                  <ResetButtonLink
+                    className="spacer-left"
+                    onClick={() => {
+                      setNewScimStatus(undefined);
+                      setNewGroupSetting();
+                    }}
+                    disabled={!hasScimConfigChange}
+                  >
+                    {translate('cancel')}
+                  </ResetButtonLink>
+                </>
+              )}
+              {showConfirmProvisioningModal && (
+                <ConfirmModal
+                  onConfirm={() => handleConfirmChangeProvisioning()}
+                  header={translate(
+                    'settings.authentication.saml.confirm',
+                    newScimStatus ? 'scim' : 'jit'
+                  )}
+                  onClose={() => setShowConfirmProvisioningModal(false)}
+                  isDestructive={!newScimStatus}
+                  confirmButtonText={translate('yes')}
+                >
+                  {translate(
+                    'settings.authentication.saml.confirm',
+                    newScimStatus ? 'scim' : 'jit',
+                    'description'
+                  )}
+                </ConfirmModal>
+              )}
+            </form>
+          </div>
+        </>
+      )}
+      {showEditModal && (
+        <ConfigurationForm
+          tab={SAML}
+          excludedField={SAML_EXCLUDED_FIELD}
+          loading={loading}
+          values={values}
+          setNewValue={setNewValue}
+          canBeSave={canBeSave}
+          onClose={handleCancelConfiguration}
+          create={!hasConfiguration}
+          onReload={reload}
+        />
+      )}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlConfigurationForm.tsx
deleted file mode 100644 (file)
index 028429f..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { isEmpty, keyBy } from 'lodash';
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { setSettingValue } from '../../../../api/settings';
-import DocLink from '../../../../components/common/DocLink';
-import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import Modal from '../../../../components/controls/Modal';
-import { Alert } from '../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
-import { translate } from '../../../../helpers/l10n';
-import { Dict } from '../../../../types/types';
-import {
-  SamlSettingValue,
-  SAML_ENABLED_FIELD,
-  SAML_GROUP_NAME,
-  SAML_SCIM_DEPRECATED,
-} from './hook/useLoadSamlSettings';
-import SamlFormField from './SamlFormField';
-
-interface Props {
-  create: boolean;
-  loading: boolean;
-  values: Dict<SamlSettingValue>;
-  setNewValue: (key: string, value: string | boolean) => void;
-  canBeSave: boolean;
-  onClose: () => void;
-  onReload: () => Promise<void>;
-}
-
-interface ErrorValue {
-  key: string;
-  message: string;
-}
-
-export const SAML = 'saml';
-
-const SAML_EXCLUDED_FIELD = [SAML_ENABLED_FIELD, SAML_GROUP_NAME, SAML_SCIM_DEPRECATED];
-
-export default function SamlConfigurationForm(props: Props) {
-  const { create, loading, values, setNewValue, canBeSave } = props;
-  const [errors, setErrors] = React.useState<Dict<ErrorValue>>({});
-
-  const headerLabel = translate('settings.authentication.saml.form', create ? 'create' : 'edit');
-
-  const handleSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
-
-    if (canBeSave) {
-      const r = await Promise.all(
-        Object.values(values)
-          .filter((v) => v.newValue !== undefined)
-          .map(async ({ key, newValue, definition }) => {
-            try {
-              await setSettingValue(definition, newValue);
-              return { key, success: true };
-            } catch (error) {
-              return { key, success: false };
-            }
-          })
-      );
-      const errors = r
-        .filter(({ success }) => !success)
-        .map(({ key }) => ({ key, message: translate('default_save_field_error_message') }));
-      setErrors(keyBy(errors, 'key'));
-      if (isEmpty(errors)) {
-        await props.onReload();
-        props.onClose();
-      }
-    } else {
-      const errors = Object.values(values)
-        .filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory)
-        .map((v) => ({ key: v.key, message: translate('field_required') }));
-      setErrors(keyBy(errors, 'key'));
-    }
-  };
-
-  return (
-    <Modal contentLabel={headerLabel} shouldCloseOnOverlayClick={false} size="medium">
-      <form className="views-form create-saml-form" onSubmit={handleSubmit}>
-        <div className="modal-head">
-          <h2>{headerLabel}</h2>
-        </div>
-        <div className="modal-body modal-container">
-          <DeferredSpinner
-            loading={loading}
-            ariaLabel={translate('settings.authentication.saml.form.loading')}
-          >
-            <Alert variant="info">
-              <FormattedMessage
-                id="settings.authentication.help"
-                defaultMessage={translate('settings.authentication.help')}
-                values={{
-                  link: (
-                    <DocLink to="/instance-administration/authentication/saml/overview/">
-                      {translate('settings.authentication.help.link')}
-                    </DocLink>
-                  ),
-                }}
-              />
-            </Alert>
-            {Object.values(values).map((val) => {
-              if (SAML_EXCLUDED_FIELD.includes(val.key)) {
-                return null;
-              }
-              return (
-                <div key={val.key}>
-                  <SamlFormField
-                    settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
-                    definition={val.definition}
-                    mandatory={val.mandatory}
-                    onFieldChange={setNewValue}
-                    isNotSet={val.isNotSet}
-                    error={errors[val.key]?.message}
-                  />
-                </div>
-              );
-            })}
-          </DeferredSpinner>
-        </div>
-
-        <div className="modal-foot">
-          <SubmitButton disabled={!canBeSave}>
-            {translate('settings.almintegration.form.save')}
-            <DeferredSpinner className="spacer-left" loading={loading} />
-          </SubmitButton>
-          <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
-        </div>
-      </form>
-    </Modal>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlFormField.tsx
deleted file mode 100644 (file)
index d86afef..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import ValidationInput, {
-  ValidationInputErrorPlacement,
-} from '../../../../components/controls/ValidationInput';
-import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
-import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
-import SamlSecuredField from './SamlSecuredField';
-import SamlToggleField from './SamlToggleField';
-
-interface SamlToggleFieldProps {
-  settingValue?: string | boolean;
-  definition: ExtendedSettingDefinition;
-  mandatory?: boolean;
-  onFieldChange: (key: string, value: string | boolean) => void;
-  isNotSet: boolean;
-  error?: string;
-}
-
-export default function SamlFormField(props: SamlToggleFieldProps) {
-  const { mandatory = false, definition, settingValue, isNotSet, error } = props;
-
-  return (
-    <div className="settings-definition">
-      <div className="settings-definition-left">
-        <label className="h3" htmlFor={definition.key}>
-          {definition.name}
-        </label>
-        {mandatory && <MandatoryFieldMarker />}
-        {definition.description && (
-          <div className="markdown small spacer-top">{definition.description}</div>
-        )}
-      </div>
-      <div className="settings-definition-right big-padded-top display-flex-column">
-        {definition.type === SettingType.PASSWORD && (
-          <SamlSecuredField
-            definition={definition}
-            settingValue={String(settingValue ?? '')}
-            onFieldChange={props.onFieldChange}
-            isNotSet={isNotSet}
-          />
-        )}
-        {definition.type === SettingType.BOOLEAN && (
-          <SamlToggleField
-            definition={definition}
-            settingValue={settingValue}
-            toggleDisabled={false}
-            onChange={(value) => props.onFieldChange(definition.key, value)}
-          />
-        )}
-        {definition.type === undefined && (
-          <ValidationInput
-            error={error}
-            errorPlacement={ValidationInputErrorPlacement.Bottom}
-            isValid={false}
-            isInvalid={Boolean(error)}
-          >
-            <input
-              className="width-100"
-              id={definition.key}
-              maxLength={4000}
-              name={definition.key}
-              onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
-              type="text"
-              value={String(settingValue ?? '')}
-            />
-          </ValidationInput>
-        )}
-      </div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlSecuredField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlSecuredField.tsx
deleted file mode 100644 (file)
index a7177a2..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React, { useEffect } from 'react';
-import { ButtonLink } from '../../../../components/controls/buttons';
-import { translate } from '../../../../helpers/l10n';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
-import { isSecuredDefinition } from '../../utils';
-
-interface SamlToggleFieldProps {
-  onFieldChange: (key: string, value: string) => void;
-  settingValue?: string;
-  definition: ExtendedSettingDefinition;
-  optional?: boolean;
-  isNotSet: boolean;
-}
-
-export default function SamlSecuredField(props: SamlToggleFieldProps) {
-  const { settingValue, definition, optional = true, isNotSet } = props;
-  const [showSecretField, setShowSecretField] = React.useState(
-    !isNotSet && isSecuredDefinition(definition)
-  );
-
-  useEffect(() => {
-    setShowSecretField(!isNotSet && isSecuredDefinition(definition));
-  }, [isNotSet, definition]);
-
-  return (
-    <>
-      {!showSecretField && (
-        <textarea
-          className="width-100"
-          id={definition.key}
-          maxLength={4000}
-          onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
-          required={!optional}
-          rows={5}
-          value={settingValue ?? ''}
-        />
-      )}
-      {showSecretField && (
-        <div>
-          <p>{translate('settings.almintegration.form.secret.field')}</p>
-          <ButtonLink
-            onClick={() => {
-              setShowSecretField(false);
-            }}
-          >
-            {translate('settings.almintegration.form.secret.update_field')}
-          </ButtonLink>
-        </div>
-      )}
-    </>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlToggleField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlToggleField.tsx
deleted file mode 100644 (file)
index a7c787c..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import Toggle from '../../../../components/controls/Toggle';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
-
-interface SamlToggleFieldProps {
-  toggleDisabled: boolean;
-  onChange: (value: boolean) => void;
-  settingValue?: string | boolean;
-  definition: ExtendedSettingDefinition;
-}
-
-export default function SamlToggleField(props: SamlToggleFieldProps) {
-  const { toggleDisabled, settingValue, definition } = props;
-
-  return (
-    <Toggle
-      name={definition.key}
-      onChange={props.onChange}
-      value={settingValue ?? ''}
-      disabled={toggleDisabled}
-    />
-  );
-}
index b8136d361e4da7d740824a7e96a7d97d00855bc2..3b7b730add5aa38f9f99f0cd344decfae53324fa 100644 (file)
@@ -84,7 +84,7 @@ const ui = {
     createConfiguration: async (user: UserEvent) => {
       const { saml } = ui;
       await act(async () => {
-        await user.click(await saml.createConfigButton.find());
+        await user.click((await saml.createConfigButton.findAll())[0]);
       });
       await saml.fillForm(user);
       await act(async () => {
@@ -135,7 +135,7 @@ describe('SAML tab', () => {
     const user = userEvent.setup();
     renderAuthentication();
 
-    await user.click(await saml.createConfigButton.find());
+    await user.click((await saml.createConfigButton.findAll())[0]);
 
     expect(saml.saveConfigButton.get()).toBeDisabled();
     await saml.fillForm(user);
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts
new file mode 100644 (file)
index 0000000..6559686
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { every, isEmpty, keyBy } from 'lodash';
+import React from 'react';
+import { getValues, resetSettingValue } from '../../../../../api/settings';
+import { ExtendedSettingDefinition } from '../../../../../types/settings';
+import { Dict } from '../../../../../types/types';
+
+export interface SettingValue {
+  key: string;
+  mandatory: boolean;
+  isNotSet: boolean;
+  value?: string;
+  newValue?: string | boolean;
+  definition: ExtendedSettingDefinition;
+}
+
+export default function useConfiguration(
+  definitions: ExtendedSettingDefinition[],
+  optionalFields: string[]
+) {
+  const [loading, setLoading] = React.useState(true);
+  const [values, setValues] = React.useState<Dict<SettingValue>>({});
+
+  const reload = React.useCallback(async () => {
+    const keys = definitions.map((definition) => definition.key);
+
+    setLoading(true);
+
+    try {
+      const values = await getValues({
+        keys,
+      });
+
+      setValues(
+        keyBy(
+          definitions.map((definition) => ({
+            key: definition.key,
+            value: values.find((v) => v.key === definition.key)?.value,
+            mandatory: !optionalFields.includes(definition.key),
+            isNotSet: values.find((v) => v.key === definition.key) === undefined,
+            definition,
+          })),
+          'key'
+        )
+      );
+    } finally {
+      setLoading(false);
+    }
+  }, [...definitions]);
+
+  React.useEffect(() => {
+    (async () => {
+      await reload();
+    })();
+  }, [...definitions]);
+
+  const setNewValue = (key: string, newValue?: string | boolean) => {
+    const newValues = {
+      ...values,
+      [key]: {
+        key,
+        newValue,
+        mandatory: values[key]?.mandatory,
+        isNotSet: values[key]?.isNotSet,
+        value: values[key]?.value,
+        definition: values[key]?.definition,
+      },
+    };
+    setValues(newValues);
+  };
+
+  const canBeSave = every(
+    Object.values(values).filter((v) => v.mandatory),
+    (v) =>
+      (v.newValue !== undefined && !isEmpty(v.newValue)) ||
+      (!v.isNotSet && v.newValue === undefined)
+  );
+
+  const hasConfiguration = every(
+    Object.values(values).filter((v) => v.mandatory),
+    (v) => !v.isNotSet
+  );
+
+  const deleteConfiguration = React.useCallback(async () => {
+    await resetSettingValue({ keys: Object.keys(values).join(',') });
+    await reload();
+  }, [reload, values]);
+
+  const isValueChange = React.useCallback(
+    (setting: string) => {
+      const value = values[setting];
+      return value && value.newValue !== undefined && (value.value ?? '') !== value.newValue;
+    },
+    [values]
+  );
+
+  return {
+    values,
+    reload,
+    setNewValue,
+    canBeSave,
+    loading,
+    hasConfiguration,
+    isValueChange,
+    deleteConfiguration,
+  };
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
new file mode 100644 (file)
index 0000000..05ec87e
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { ExtendedSettingDefinition } from '../../../../../types/settings';
+import useConfiguration from './useConfiguration';
+
+export const GITHUB_ENABLED_FIELD = 'sonar.auth.github.enabled';
+export const GITHUB_APP_ID_FIELD = 'sonar.auth.github.appId';
+export const GITHUB_API_URL_FIELD = 'sonar.auth.github.apiUrl';
+
+const OPTIONAL_FIELDS = [
+  GITHUB_ENABLED_FIELD,
+  'sonar.auth.github.organizations',
+  'sonar.auth.github.allowUsersToSignUp',
+  'sonar.auth.github.groupsSync',
+  'sonar.auth.github.organizations',
+];
+
+export interface SamlSettingValue {
+  key: string;
+  mandatory: boolean;
+  isNotSet: boolean;
+  value?: string;
+  newValue?: string | boolean;
+  definition: ExtendedSettingDefinition;
+}
+
+export default function useGithubConfiguration(definitions: ExtendedSettingDefinition[]) {
+  const config = useConfiguration(definitions, OPTIONAL_FIELDS);
+
+  const { values } = config;
+
+  const enabled = values[GITHUB_ENABLED_FIELD]?.value === 'true';
+  const appId = values[GITHUB_APP_ID_FIELD]?.value;
+  const url = values[GITHUB_API_URL_FIELD]?.value;
+
+  return { ...config, url, enabled, appId };
+}
index af29e0ea0c5d002aef0681f397226d4d0de749a5..7c06147aacf7a94fb8796c48a4e49e7c8ca8aded 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { every, isEmpty, keyBy } from 'lodash';
 import React from 'react';
-import { fetchIsScimEnabled, getValues } from '../../../../../api/settings';
+import { fetchIsScimEnabled } from '../../../../../api/settings';
 import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
 import { Feature } from '../../../../../types/features';
 import { ExtendedSettingDefinition } from '../../../../../types/settings';
-import { Dict } from '../../../../../types/types';
-
-const SAML = 'saml';
+import useConfiguration from './useConfiguration';
 
 export const SAML_ENABLED_FIELD = 'sonar.auth.saml.enabled';
 export const SAML_GROUP_NAME = 'sonar.auth.saml.group.name';
@@ -42,87 +39,28 @@ const OPTIONAL_FIELDS = [
   SAML_SCIM_DEPRECATED,
 ];
 
-export interface SamlSettingValue {
-  key: string;
-  mandatory: boolean;
-  isNotSet: boolean;
-  value?: string;
-  newValue?: string | boolean;
-  definition: ExtendedSettingDefinition;
-}
-
 export default function useSamlConfiguration(definitions: ExtendedSettingDefinition[]) {
-  const [loading, setLoading] = React.useState(true);
   const [scimStatus, setScimStatus] = React.useState<boolean>(false);
-  const [values, setValues] = React.useState<Dict<SamlSettingValue>>({});
   const [newScimStatus, setNewScimStatus] = React.useState<boolean>();
   const hasScim = React.useContext(AvailableFeaturesContext).includes(Feature.Scim);
+  const {
+    loading,
+    reload: reloadConfig,
+    values,
+    setNewValue,
+    canBeSave,
+    hasConfiguration,
+    deleteConfiguration,
+    isValueChange,
+  } = useConfiguration(definitions, OPTIONAL_FIELDS);
 
-  const onReload = React.useCallback(async () => {
-    const samlDefinition = definitions.filter((def) => def.subCategory === SAML);
-    const keys = samlDefinition.map((definition) => definition.key);
-
-    setLoading(true);
-
-    try {
-      const values = await getValues({
-        keys,
-      });
-
-      setValues(
-        keyBy(
-          samlDefinition.map((definition) => ({
-            key: definition.key,
-            value: values.find((v) => v.key === definition.key)?.value,
-            mandatory: !OPTIONAL_FIELDS.includes(definition.key),
-            isNotSet: values.find((v) => v.key === definition.key) === undefined,
-            definition,
-          })),
-          'key'
-        )
-      );
-
+  React.useEffect(() => {
+    (async () => {
       if (hasScim) {
         setScimStatus(await fetchIsScimEnabled());
       }
-    } finally {
-      setLoading(false);
-    }
-  }, [...definitions]);
-
-  React.useEffect(() => {
-    // eslint-disable-next-line @typescript-eslint/no-floating-promises
-    (async () => {
-      await onReload();
     })();
-  }, [...definitions]);
-
-  const setNewValue = (key: string, newValue?: string | boolean) => {
-    const newValues = {
-      ...values,
-      [key]: {
-        key,
-        newValue,
-        mandatory: values[key]?.mandatory,
-        isNotSet: values[key]?.isNotSet,
-        value: values[key]?.value,
-        definition: values[key]?.definition,
-      },
-    };
-    setValues(newValues);
-  };
-
-  const canBeSave = every(
-    Object.values(values).filter((v) => v.mandatory),
-    (v) =>
-      (v.newValue !== undefined && !isEmpty(v.newValue)) ||
-      (!v.isNotSet && v.newValue === undefined)
-  );
-
-  const hasConfiguration = every(
-    Object.values(values).filter((v) => v.mandatory),
-    (v) => !v.isNotSet
-  );
+  }, [hasScim]);
 
   const name = values[SAML_PROVIDER_NAME]?.value;
   const url = values[SAML_LOGIN_URL]?.value;
@@ -134,10 +72,12 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit
   };
 
   const hasScimConfigChange =
-    newScimStatus !== undefined &&
-    groupValue &&
-    (newScimStatus !== scimStatus ||
-      (groupValue.newValue !== undefined && (groupValue.value ?? '') !== groupValue.newValue));
+    isValueChange(SAML_GROUP_NAME) || (newScimStatus !== undefined && newScimStatus !== scimStatus);
+
+  const reload = React.useCallback(async () => {
+    await reloadConfig();
+    setScimStatus(await fetchIsScimEnabled());
+  }, [reloadConfig]);
 
   return {
     hasScim,
@@ -151,10 +91,11 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit
     canBeSave,
     values,
     setNewValue,
-    onReload,
+    reload,
     hasScimConfigChange,
     newScimStatus,
     setNewScimStatus,
     setNewGroupSetting,
+    deleteConfiguration,
   };
 }
index 1df7333356d792646a3333110d73cbe35c551a09..1794ecd47c77ec2bccf00db0b3c95138041ce758 100644 (file)
@@ -25,7 +25,7 @@ export interface BoxedTabsProps<K extends string | number> {
   className?: string;
   onSelect: (key: K) => void;
   selected?: K;
-  tabs: Array<{ key: K; label: React.ReactNode }>;
+  tabs: ReadonlyArray<{ key: K; label: React.ReactNode }>;
 }
 
 const TabContainer = styled.div`
index 4702ad16e86caa99460fe7c098d3ee1756fc123d..0416859153b17b075425db4935d8f75263d73f44 100644 (file)
@@ -1323,6 +1323,15 @@ settings.authentication.help.link=documentation
 settings.authentication.form.create=Create configuration
 settings.authentication.form.edit=Edit
 settings.authentication.form.delete=Delete
+settings.authentication.form.loading=Loading configuration
+
+settings.authentication.form.create.saml=New SAML configuration
+settings.authentication.form.edit.saml=Edit SAML configuration
+settings.authentication.form.create.github=New Github configuration
+settings.authentication.form.edit.github=Edit Github configuration
+
+settings.authentication.github.configuration=Github Configuration
+settings.authentication.github.form.not_configured=Github App is not configured
 settings.authentication.saml.configuration=SAML Configuration
 settings.authentication.saml.confirm.scim=Switch to automatic provisioning
 settings.authentication.saml.confirm.jit=Switch to Just-in-Time provisioning