]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21480 migrate authentication tabs to MIUI
authorViktor Vorona <viktor.vorona@sonarsource.com>
Fri, 26 Jan 2024 10:11:32 +0000 (11:11 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 30 Jan 2024 15:02:03 +0000 (15:02 +0000)
17 files changed:
server/sonar-web/design-system/src/components/FlagMessage.tsx
server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx
server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.tsx
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationDetails.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/ProvisioningSection.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/TabHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
server/sonar-web/src/main/js/apps/settings/styles.css
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 767af9b1631fef3fa4509407d2b9093e912e490f..47fb9b9fa582d85f9f59d1ccf756f233c55d789c 100644 (file)
@@ -71,10 +71,12 @@ export function FlagMessage(props: Props & React.HTMLAttributes<HTMLDivElement>)
       className={classNames('alert', className)}
       {...domProps}
     >
-      <div className="flag-inner">
-        <div className="flag-icon">{variantInfo.icon}</div>
-        <div className="flag-content">{props.children}</div>
-      </div>
+      {props.children && (
+        <div className="flag-inner">
+          <div className="flag-icon">{variantInfo.icon}</div>
+          <div className="flag-content">{props.children}</div>
+        </div>
+      )}
     </StyledFlag>
   );
 }
@@ -117,6 +119,10 @@ export const StyledFlag = styled.div<{
   border: ${({ borderColor }) => themeBorder('default', borderColor)};
   background-color: ${themeColor('flagMessageBackground')};
 
+  :empty {
+    display: none;
+  }
+
   & > .flag-inner {
     ${tw`sw-flex sw-items-stretch`}
     ${tw`sw-box-border`}
index 238236156cea2d88e4710644f5f21d2b31a9ef8e..9f6259e4a323e863de6e0924ce203aa532c28fda 100644 (file)
  */
 import styled from '@emotion/styled';
 import { formatDistance } from 'date-fns';
-import { CheckIcon, FlagMessage, FlagWarningIcon, Link, themeColor } from 'design-system';
+import { CheckIcon, FlagMessage, FlagWarningIcon, Link, Spinner, themeColor } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { Alert } from '../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { AlmSyncStatus } from '../../types/provisioning';
 import { TaskStatuses } from '../../types/tasks';
@@ -82,53 +81,59 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
       </div>
     ) : (
       <FlagMessage variant="error">
-        <FormattedMessage
-          id="settings.authentication.github.synchronization_failed_short"
-          defaultMessage={translate('settings.authentication.github.synchronization_failed_short')}
-          values={{
-            details: (
-              <Link className="sw-ml-2" to="/admin/settings?category=authentication&tab=github">
-                {translate('settings.authentication.github.synchronization_details_link')}
-              </Link>
-            ),
-          }}
-        />
+        <div>
+          <FormattedMessage
+            id="settings.authentication.github.synchronization_failed_short"
+            defaultMessage={translate(
+              'settings.authentication.github.synchronization_failed_short',
+            )}
+            values={{
+              details: (
+                <Link className="sw-ml-2" to="/admin/settings?category=authentication&tab=github">
+                  {translate('settings.authentication.github.synchronization_details_link')}
+                </Link>
+              ),
+            }}
+          />
+        </div>
       </FlagMessage>
     );
   }
 
   return (
     <>
-      <Alert
+      <FlagMessage
         variant={status === TaskStatuses.Success ? 'success' : 'error'}
         role="alert"
         aria-live="assertive"
       >
-        {status === TaskStatuses.Success ? (
-          <>
-            {translateWithParameters(
-              'settings.authentication.github.synchronization_successful',
-              formattedDate,
-            )}
-            <br />
-            {summary ?? ''}
-          </>
-        ) : (
-          <React.Fragment key={`synch-alert-${finishedAt}`}>
-            <div>
+        <div>
+          {status === TaskStatuses.Success ? (
+            <>
               {translateWithParameters(
-                'settings.authentication.github.synchronization_failed',
+                'settings.authentication.github.synchronization_successful',
                 formattedDate,
               )}
-            </div>
-            <br />
-            {errorMessage ?? ''}
-          </React.Fragment>
-        )}
-      </Alert>
-      <Alert variant="warning" role="alert" aria-live="assertive">
+              <br />
+              {summary ?? ''}
+            </>
+          ) : (
+            <React.Fragment key={`synch-alert-${finishedAt}`}>
+              <div>
+                {translateWithParameters(
+                  'settings.authentication.github.synchronization_failed',
+                  formattedDate,
+                )}
+              </div>
+              <br />
+              {errorMessage ?? ''}
+            </React.Fragment>
+          )}
+        </div>
+      </FlagMessage>
+      <FlagMessage variant="warning" role="alert" aria-live="assertive">
         {warningMessage}
-      </Alert>
+      </FlagMessage>
     </>
   );
 }
@@ -137,28 +142,21 @@ export default function AlmSynchronisationWarning({
   short,
   data,
 }: Readonly<SynchronisationWarningProps>) {
+  const loadingLabel =
+    data.nextSync &&
+    translate(
+      data.nextSync.status === TaskStatuses.Pending
+        ? 'settings.authentication.github.synchronization_pending'
+        : 'settings.authentication.github.synchronization_in_progress',
+    );
   return (
     <>
-      <Alert
-        variant="loading"
-        className="spacer-bottom"
-        aria-atomic
-        role="alert"
-        aria-live="assertive"
-        aria-label={
-          data.nextSync === undefined
-            ? translate('settings.authentication.github.synchronization_finish')
-            : ''
-        }
-      >
-        {!short &&
-          data?.nextSync &&
-          translate(
-            data.nextSync.status === TaskStatuses.Pending
-              ? 'settings.authentication.github.synchronization_pending'
-              : 'settings.authentication.github.synchronization_in_progress',
-          )}
-      </Alert>
+      {!short && (
+        <div className={data.nextSync ? 'sw-flex sw-gap-2 sw-mb-4' : ''}>
+          <Spinner loading={!!data.nextSync} ariaLabel={loadingLabel} />
+          <div>{data.nextSync && loadingLabel}</div>
+        </div>
+      )}
 
       <LastSyncAlert short={short} info={data.lastSync} />
     </>
index 71c8c4e3bd40019b86e21d630003c78af635380d..92d95141b5b4e072bb86945162c33a30d3c33fcc 100644 (file)
@@ -34,6 +34,7 @@ interface Props {
   definitions: ExtendedSettingDefinition[];
   subCategory?: string;
   displaySubCategoryTitle?: boolean;
+  noPadding?: boolean;
 }
 
 interface State {
@@ -87,7 +88,7 @@ export default class CategoryDefinitionsList extends React.PureComponent<Props,
   }
 
   render() {
-    const { category, component, subCategory, displaySubCategoryTitle } = this.props;
+    const { category, component, subCategory, displaySubCategoryTitle, noPadding } = this.props;
     const { settings } = this.state;
 
     return (
@@ -97,6 +98,7 @@ export default class CategoryDefinitionsList extends React.PureComponent<Props,
         settings={settings}
         subCategory={subCategory}
         displaySubCategoryTitle={displaySubCategoryTitle}
+        noPadding={noPadding}
       />
     );
   }
index 14abcc7a811ec3b06704f99fe0792fcf5aa9b8c1..c5e2614aecf8cb697ad6f74fc0ec17d245abb8d7 100644 (file)
@@ -35,6 +35,7 @@ export interface SubCategoryDefinitionsListProps {
   settings: Array<SettingDefinitionAndValue>;
   subCategory?: string;
   displaySubCategoryTitle?: boolean;
+  noPadding?: boolean;
 }
 
 class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryDefinitionsListProps> {
@@ -65,7 +66,13 @@ class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryDefiniti
   };
 
   render() {
-    const { displaySubCategoryTitle = true, settings, subCategory, component } = this.props;
+    const {
+      displaySubCategoryTitle = true,
+      settings,
+      subCategory,
+      component,
+      noPadding,
+    } = this.props;
     const bySubCategory = groupBy(settings, (setting) => setting.definition.subCategory);
     const subCategories = Object.keys(bySubCategory).map((key) => ({
       key,
@@ -81,7 +88,7 @@ class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryDefiniti
     return (
       <ul>
         {filteredSubCategories.map((subCategory, index) => (
-          <li className="sw-p-6" key={subCategory.key}>
+          <li className={noPadding ? '' : 'sw-p-6'} key={subCategory.key}>
             {displaySubCategoryTitle && (
               <SubTitle
                 as="h3"
index ace7a1cff7568d88fc0fba62d0f18ff53794d4b6..7e72e9c37211ed15c4f0ff4b154b7134e9bf2735 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import classNames from 'classnames';
+import { FlagMessage, Link, SubTitle, ToggleButton } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { useSearchParams } from 'react-router-dom';
 import withAvailableFeatures, {
   WithAvailableFeaturesProps,
 } from '../../../../app/components/available-features/withAvailableFeatures';
-import DocLink from '../../../../components/common/DocLink';
-import Link from '../../../../components/common/Link';
-import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
-import BoxedTabs, { getTabId, getTabPanelId } from '../../../../components/controls/BoxedTabs';
-import { Alert } from '../../../../components/ui/Alert';
+import DocumentationLink from '../../../../components/common/DocumentationLink';
+import { getTabId, getTabPanelId } from '../../../../components/controls/BoxedTabs';
 import { translate } from '../../../../helpers/l10n';
 import { getBaseUrl } from '../../../../helpers/system';
 import { searchParamsToQuery } from '../../../../helpers/urls';
@@ -45,9 +43,6 @@ interface Props {
   definitions: ExtendedSettingDefinition[];
 }
 
-// We substract the footer height with padding (80) and the main layout padding (20)
-const HEIGHT_ADJUSTMENT = 100;
-
 export type AuthenticationTabs =
   | typeof SAML
   | AlmKeys.GitHub
@@ -81,11 +76,11 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
 
   const tabs = [
     {
-      key: SAML,
+      value: SAML,
       label: 'SAML',
     },
     {
-      key: AlmKeys.GitHub,
+      value: AlmKeys.GitHub,
       label: (
         <>
           {renderDevOpsIcon(AlmKeys.GitHub)}
@@ -94,7 +89,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
       ),
     },
     {
-      key: AlmKeys.BitbucketServer,
+      value: AlmKeys.BitbucketServer,
       label: (
         <>
           {renderDevOpsIcon(AlmKeys.BitbucketServer)}
@@ -103,7 +98,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
       ),
     },
     {
-      key: AlmKeys.GitLab,
+      value: AlmKeys.GitLab,
       label: (
         <>
           {renderDevOpsIcon(AlmKeys.GitLab)}
@@ -123,104 +118,92 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
 
   return (
     <>
-      <header className="page-header">
-        <h3 className="page-title h2">{translate('settings.authentication.title')}</h3>
-      </header>
+      <SubTitle as="h3">{translate('settings.authentication.title')}</SubTitle>
 
       {props.hasFeature(Feature.LoginMessage) && (
-        <Alert variant="info">
-          <FormattedMessage
-            id="settings.authentication.custom_message_information"
-            defaultMessage={translate('settings.authentication.custom_message_information')}
-            values={{
-              link: (
-                <Link to="/admin/settings?category=general#sonar.login.message">
-                  {translate('settings.authentication.custom_message_information.link')}
-                </Link>
-              ),
-            }}
-          />
-        </Alert>
+        <FlagMessage variant="info">
+          <div>
+            <FormattedMessage
+              id="settings.authentication.custom_message_information"
+              defaultMessage={translate('settings.authentication.custom_message_information')}
+              values={{
+                link: (
+                  <Link to="/admin/settings?category=general#sonar.login.message">
+                    {translate('settings.authentication.custom_message_information.link')}
+                  </Link>
+                ),
+              }}
+            />
+          </div>
+        </FlagMessage>
       )}
 
-      <div className="big-spacer-top huge-spacer-bottom">
+      <div className="sw-my-6">
         <p>{translate('settings.authentication.description')}</p>
       </div>
 
-      <BoxedTabs
-        onSelect={(tab: AuthenticationTabs) => {
+      <ToggleButton
+        role="tablist"
+        onChange={(tab: AuthenticationTabs) => {
           setSearchParams({ ...searchParamsToQuery(query), tab });
         }}
-        selected={currentTab}
-        tabs={tabs}
+        value={currentTab}
+        options={tabs}
       />
-      {/* Adding a key to force re-rendering of the tab container, so that it resets the scroll position */}
-      <ScreenPositionHelper>
-        {({ top }) => (
-          <>
-            {tabs.map((tab) => (
-              <div
-                style={{
-                  maxHeight:
-                    tab.key === AlmKeys.BitbucketServer
-                      ? `calc(100vh - ${top + HEIGHT_ADJUSTMENT}px)`
-                      : '',
-                }}
-                className={classNames('bordered overflow-y-auto tabbed-definitions', {
-                  hidden: currentTab !== tab.key,
-                })}
-                key={tab.key}
-                role="tabpanel"
-                aria-labelledby={getTabId(tab.key)}
-                id={getTabPanelId(tab.key)}
-              >
-                {currentTab === tab.key && (
-                  <div className="big-padded-top big-padded-left big-padded-right">
-                    {tab.key === SAML && <SamlAuthenticationTab definitions={samlDefinitions} />}
-
-                    {tab.key === AlmKeys.GitHub && (
-                      <GithubAuthenticationTab
-                        currentTab={currentTab}
-                        definitions={githubDefinitions}
+      {tabs.map((tab) => (
+        <div
+          className={classNames('sw-overflow-y-auto', {
+            hidden: currentTab !== tab.value,
+          })}
+          key={tab.value}
+          role="tabpanel"
+          aria-labelledby={getTabId(tab.value)}
+          id={getTabPanelId(tab.value)}
+        >
+          {currentTab === tab.value && (
+            <div className="sw-mt-6">
+              {tab.value === SAML && <SamlAuthenticationTab definitions={samlDefinitions} />}
+
+              {tab.value === AlmKeys.GitHub && (
+                <GithubAuthenticationTab currentTab={currentTab} definitions={githubDefinitions} />
+              )}
+
+              {tab.value === AlmKeys.GitLab && <GitLabAuthenticationTab />}
+
+              {tab.value === AlmKeys.BitbucketServer && (
+                <>
+                  <FlagMessage variant="info">
+                    <div>
+                      <FormattedMessage
+                        id="settings.authentication.help"
+                        defaultMessage={translate('settings.authentication.help')}
+                        values={{
+                          link: (
+                            <DocumentationLink
+                              to={`/instance-administration/authentication/${
+                                DOCUMENTATION_LINK_SUFFIXES[tab.value]
+                              }/`}
+                            >
+                              {translate('settings.authentication.help.link')}
+                            </DocumentationLink>
+                          ),
+                        }}
                       />
-                    )}
-
-                    {tab.key === AlmKeys.GitLab && <GitLabAuthenticationTab />}
-
-                    {tab.key === AlmKeys.BitbucketServer && (
-                      <>
-                        <Alert variant="info">
-                          <FormattedMessage
-                            id="settings.authentication.help"
-                            defaultMessage={translate('settings.authentication.help')}
-                            values={{
-                              link: (
-                                <DocLink
-                                  to={`/instance-administration/authentication/${
-                                    DOCUMENTATION_LINK_SUFFIXES[tab.key]
-                                  }/`}
-                                >
-                                  {translate('settings.authentication.help.link')}
-                                </DocLink>
-                              ),
-                            }}
-                          />
-                        </Alert>
-                        <CategoryDefinitionsList
-                          category={AUTHENTICATION_CATEGORY}
-                          definitions={definitions}
-                          subCategory={tab.key}
-                          displaySubCategoryTitle={false}
-                        />
-                      </>
-                    )}
-                  </div>
-                )}
-              </div>
-            ))}
-          </>
-        )}
-      </ScreenPositionHelper>
+                    </div>
+                  </FlagMessage>
+                  <CategoryDefinitionsList
+                    category={AUTHENTICATION_CATEGORY}
+                    definitions={definitions}
+                    subCategory={tab.value}
+                    displaySubCategoryTitle={false}
+                    noPadding
+                  />
+                </>
+              )}
+            </div>
+          )}
+        </div>
+      ))}
     </>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationDetails.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationDetails.tsx
new file mode 100644 (file)
index 0000000..7d83087
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 {
+  ButtonPrimary,
+  ButtonSecondary,
+  DangerButtonSecondary,
+  SubHeading,
+  Tooltip,
+} from 'design-system';
+import React, { ReactElement } from 'react';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  title: string;
+  url: string | string[] | undefined;
+  canDisable: boolean;
+  onEdit: () => void;
+  onDelete: () => void;
+  onToggle: () => void;
+  extraActions?: ReactElement;
+  isDeleting: boolean;
+  enabled: boolean;
+}
+
+export default function ConfigurationDetails(props: Readonly<Props>) {
+  const { title, url, canDisable, onEdit, onDelete, onToggle, extraActions, isDeleting, enabled } =
+    props;
+
+  return (
+    <div className="sw-flex sw-mb-6 sw-justify-between">
+      <div className="sw-min-w-0">
+        <SubHeading as="h5" title={title} className="sw-truncate">
+          {title}
+        </SubHeading>
+        <p>{url}</p>
+        <Tooltip
+          overlay={!canDisable ? translate('settings.authentication.form.disable.tooltip') : null}
+        >
+          {enabled ? (
+            <ButtonSecondary className="sw-mt-4" onClick={onToggle} disabled={!canDisable}>
+              {translate('settings.authentication.form.disable')}
+            </ButtonSecondary>
+          ) : (
+            <ButtonPrimary className="sw-mt-4" onClick={onToggle} disabled={!canDisable}>
+              {translate('settings.authentication.form.enable')}
+            </ButtonPrimary>
+          )}
+        </Tooltip>
+      </div>
+      <div className="sw-flex sw-gap-2 sw-flex-nowrap sw-shrink-0">
+        {extraActions}
+        <ButtonSecondary onClick={onEdit}>
+          {translate('settings.authentication.form.edit')}
+        </ButtonSecondary>
+        <Tooltip
+          overlay={
+            enabled || isDeleting ? translate('settings.authentication.form.delete.tooltip') : null
+          }
+        >
+          <DangerButtonSecondary disabled={enabled || isDeleting} onClick={onDelete}>
+            {translate('settings.authentication.form.delete')}
+          </DangerButtonSecondary>
+        </Tooltip>
+      </div>
+    </div>
+  );
+}
index 30c5db1c14d6e152d4ddff952063aec06ba546f8..a56cca16fce06b83990f1c13d29f0745552bc948 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 { Spinner } from 'design-system';
 import { omitBy } from 'lodash';
 import React, { FormEvent, useContext } from 'react';
 import { FormattedMessage } from 'react-intl';
 import GitLabSynchronisationWarning from '../../../../app/components/GitLabSynchronisationWarning';
 import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
-import DocLink from '../../../../components/common/DocLink';
+import DocumentationLink from '../../../../components/common/DocumentationLink';
 import ConfirmModal from '../../../../components/controls/ConfirmModal';
-import RadioCard from '../../../../components/controls/RadioCard';
-import Tooltip from '../../../../components/controls/Tooltip';
-import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import DeleteIcon from '../../../../components/icons/DeleteIcon';
-import EditIcon from '../../../../components/icons/EditIcon';
-import { Alert } from '../../../../components/ui/Alert';
-import Spinner from '../../../../components/ui/Spinner';
-import { translate } from '../../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
 import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
 import {
   useDeleteGitLabConfigurationMutation,
@@ -46,8 +40,11 @@ import { DefinitionV2, SettingType } from '../../../../types/settings';
 import { Provider } from '../../../../types/types';
 import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
 import AuthenticationFormField from './AuthenticationFormField';
+import ConfigurationDetails from './ConfigurationDetails';
 import GitLabConfigurationForm from './GitLabConfigurationForm';
 import GitLabConfigurationValidity from './GitLabConfigurationValidity';
+import ProvisioningSection from './ProvisioningSection';
+import TabHeader from './TabHeader';
 
 interface ChangesForm {
   provisioningType?: GitLabConfigurationUpdateBody['provisioningType'];
@@ -187,254 +184,154 @@ export default function GitLabAuthenticationTab() {
 
   return (
     <Spinner loading={isLoadingList}>
-      <div className="authentication-configuration">
-        <div className="spacer-bottom display-flex-space-between display-flex-center">
-          <h4>{translate('settings.authentication.gitlab.configuration')}</h4>
-          {!configuration && (
-            <div>
-              <Button onClick={() => setOpenForm(true)}>
-                {translate('settings.authentication.form.create')}
-              </Button>
-            </div>
-          )}
-        </div>
-        {!isLoadingList && configuration?.enabled && (
-          <GitLabConfigurationValidity
-            configuration={configuration}
-            loading={isFetching}
-            onRecheck={refetch}
-          />
-        )}
+      <div>
+        <TabHeader
+          title={translate('settings.authentication.gitlab.configuration')}
+          showCreate={!configuration}
+          onCreate={() => setOpenForm(true)}
+          configurationValidity={
+            <>
+              {!isLoadingList && configuration?.enabled && (
+                <GitLabConfigurationValidity
+                  configuration={configuration}
+                  loading={isFetching}
+                  onRecheck={refetch}
+                />
+              )}
+            </>
+          }
+        />
         {!configuration && (
-          <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
-            {translate('settings.authentication.gitlab.form.not_configured')}
-          </div>
+          <div>{translate('settings.authentication.gitlab.form.not_configured')}</div>
         )}
         {configuration && (
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <div>
-              <p>{configuration.url}</p>
-              <Tooltip
-                overlay={
-                  configuration.provisioningType === ProvisioningType.auto
-                    ? translate('settings.authentication.form.disable.tooltip')
-                    : null
-                }
-              >
-                <Button
-                  className="spacer-top"
-                  onClick={toggleEnable}
-                  disabled={isUpdating || configuration.provisioningType === ProvisioningType.auto}
-                >
-                  {configuration.enabled
-                    ? translate('settings.authentication.form.disable')
-                    : translate('settings.authentication.form.enable')}
-                </Button>
-              </Tooltip>
-            </div>
-            <div>
-              <Button className="spacer-right" onClick={() => setOpenForm(true)}>
-                <EditIcon />
-                {translate('settings.authentication.form.edit')}
-              </Button>
-              <Tooltip
-                overlay={
-                  configuration.enabled
-                    ? translate('settings.authentication.form.delete.tooltip')
-                    : null
-                }
-              >
-                <Button
-                  className="button-red"
-                  disabled={configuration.enabled || isDeleting}
-                  onClick={deleteConfiguration}
-                >
-                  <DeleteIcon />
-                  {translate('settings.authentication.form.delete')}
-                </Button>
-              </Tooltip>
-            </div>
-          </div>
-        )}
-        {configuration && (
-          <div className="spacer-bottom big-padded bordered">
-            <form onSubmit={handleSubmit}>
-              <fieldset className="display-flex-column big-spacer-bottom">
-                <label className="h5">
-                  {translate('settings.authentication.form.provisioning')}
-                </label>
-
-                {configuration.enabled ? (
-                  <div className="display-flex-column spacer-top">
-                    <RadioCard
-                      className="sw-min-h-0"
-                      label={translate('settings.authentication.gitlab.provisioning_at_login')}
-                      title={translate('settings.authentication.gitlab.provisioning_at_login')}
-                      selected={provisioningType === ProvisioningType.jit}
-                      onClick={setJIT}
-                    >
-                      <p className="spacer-bottom">
-                        <FormattedMessage id="settings.authentication.gitlab.provisioning_at_login.description" />
-                      </p>
-                      <p className="spacer-bottom">
-                        <DocLink
-                          to={`/instance-administration/authentication/${
-                            DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitLab]
-                          }/#choosing-the-provisioning-method`}
-                        >
-                          {translate(
-                            `settings.authentication.gitlab.description.${ProvisioningType.jit}.learn_more`,
-                          )}
-                        </DocLink>
-                      </p>
-                      {provisioningType === ProvisioningType.jit &&
-                        allowUsersToSignUpDefinition !== undefined && (
-                          <AuthenticationFormField
-                            settingValue={allowUsersToSignUp}
-                            definition={allowUsersToSignUpDefinition}
-                            mandatory
-                            onFieldChange={(_, value) =>
-                              setChangesWithCheck({
-                                ...changes,
-                                allowUsersToSignUp: value as boolean,
-                              })
-                            }
-                            isNotSet={configuration.provisioningType !== ProvisioningType.auto}
-                          />
-                        )}
-                    </RadioCard>
-                    <RadioCard
-                      className="spacer-top sw-min-h-0"
-                      label={translate(
-                        'settings.authentication.gitlab.form.provisioning_with_gitlab',
-                      )}
-                      title={translate(
-                        'settings.authentication.gitlab.form.provisioning_with_gitlab',
-                      )}
-                      selected={provisioningType === ProvisioningType.auto}
-                      onClick={setAuto}
-                      disabled={!hasGitlabProvisioningFeature || hasDifferentProvider}
-                    >
-                      {hasGitlabProvisioningFeature ? (
-                        <>
-                          {hasDifferentProvider && (
-                            <p className="spacer-bottom text-bold ">
-                              {translate('settings.authentication.form.other_provisioning_enabled')}
-                            </p>
-                          )}
-                          <p className="spacer-bottom">
-                            {translate(
-                              'settings.authentication.gitlab.form.provisioning_with_gitlab.description',
-                            )}
-                          </p>
-                          <p className="spacer-bottom">
-                            <DocLink
-                              to={`/instance-administration/authentication/${
-                                DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitLab]
-                              }/#choosing-the-provisioning-method`}
-                            >
-                              {translate(
-                                `settings.authentication.gitlab.description.${ProvisioningType.auto}.learn_more`,
-                              )}
-                            </DocLink>
-                          </p>
-
-                          {configuration.provisioningType === ProvisioningType.auto && (
-                            <GitLabSynchronisationWarning />
-                          )}
-
-                          {provisioningType === ProvisioningType.auto && (
-                            <>
-                              <div className="sw-flex sw-flex-1 spacer-bottom">
-                                <Button
-                                  className="spacer-top width-30"
-                                  onClick={synchronizeNow}
-                                  disabled={!canSyncNow}
-                                >
-                                  {translate('settings.authentication.github.synchronize_now')}
-                                </Button>
-                              </div>
-                              <hr />
-                              <AuthenticationFormField
-                                settingValue={provisioningToken}
-                                key={tokenKey}
-                                definition={provisioningTokenDefinition}
-                                mandatory
-                                onFieldChange={(_, value) =>
-                                  setChangesWithCheck({
-                                    ...changes,
-                                    provisioningToken: value as string,
-                                  })
-                                }
-                                isNotSet={!configuration.isProvisioningTokenSet}
-                              />
-                            </>
-                          )}
-                        </>
-                      ) : (
-                        <p>
-                          <FormattedMessage
-                            id="settings.authentication.gitlab.form.provisioning.disabled"
-                            defaultMessage={translate(
-                              'settings.authentication.gitlab.form.provisioning.disabled',
-                            )}
-                            values={{
-                              documentation: (
-                                <DocLink to="/instance-administration/authentication/gitlab">
-                                  {translate('documentation')}
-                                </DocLink>
-                              ),
-                            }}
-                          />
-                        </p>
-                      )}
-                    </RadioCard>
-                  </div>
-                ) : (
-                  <Alert className="big-spacer-top" variant="info">
-                    {translate('settings.authentication.gitlab.enable_first')}
-                  </Alert>
-                )}
-              </fieldset>
-              {configuration.enabled && (
-                <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
-                  <SubmitButton disabled={!canSave()}>{translate('save')}</SubmitButton>
-                  <ResetButtonLink
-                    onClick={() => {
-                      setChanges(undefined);
-                      setTokenKey(tokenKey + 1);
-                    }}
-                    disabled={false}
-                  >
-                    {translate('cancel')}
-                  </ResetButtonLink>
-                  <Alert variant="warning" className="sw-mb-0">
-                    {canSave() &&
-                      translate('settings.authentication.gitlab.configuration.unsaved_changes')}
-                  </Alert>
-                  <Spinner loading={isUpdating} />
-                </div>
+          <>
+            <ConfigurationDetails
+              title={translateWithParameters(
+                'settings.authentication.gitlab.applicationId.name',
+                configuration.applicationId,
               )}
-              {showConfirmProvisioningModal && provisioningType && (
-                <ConfirmModal
-                  onConfirm={updateProvisioning}
-                  header={translate('settings.authentication.gitlab.confirm', provisioningType)}
-                  onClose={() => setShowConfirmProvisioningModal(false)}
-                  confirmButtonText={translate(
-                    'settings.authentication.gitlab.provisioning_change.confirm_changes',
-                  )}
-                >
-                  {translate(
-                    'settings.authentication.gitlab.confirm',
-                    provisioningType,
-                    'description',
+              url={configuration.url}
+              canDisable={!isUpdating && configuration.provisioningType !== ProvisioningType.auto}
+              enabled={configuration.enabled}
+              isDeleting={isDeleting}
+              onEdit={() => setOpenForm(true)}
+              onDelete={deleteConfiguration}
+              onToggle={toggleEnable}
+            />
+            <ProvisioningSection
+              provisioningType={provisioningType ?? ProvisioningType.jit}
+              onChangeProvisioningType={(val: ProvisioningType) =>
+                val === ProvisioningType.auto ? setAuto() : setJIT()
+              }
+              disabledConfigText={translate('settings.authentication.gitlab.enable_first')}
+              enabled={configuration.enabled}
+              hasUnsavedChanges={changes !== undefined}
+              canSave={canSave()}
+              onSave={handleSubmit}
+              onCancel={() => {
+                setChanges(undefined);
+                setTokenKey(tokenKey + 1);
+              }}
+              jitTitle={translate('settings.authentication.gitlab.provisioning_at_login')}
+              jitDescription={
+                <FormattedMessage
+                  id="settings.authentication.gitlab.provisioning_at_login.description"
+                  values={{
+                    documentation: (
+                      <DocumentationLink
+                        to={`/instance-administration/authentication/${
+                          DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitLab]
+                        }/#choosing-the-provisioning-method`}
+                      >
+                        {translate(`learn_more`)}
+                      </DocumentationLink>
+                    ),
+                  }}
+                />
+              }
+              jitSettings={
+                <AuthenticationFormField
+                  settingValue={allowUsersToSignUp}
+                  definition={allowUsersToSignUpDefinition}
+                  mandatory
+                  onFieldChange={(_, value) =>
+                    setChangesWithCheck({
+                      ...changes,
+                      allowUsersToSignUp: value as boolean,
+                    })
+                  }
+                  isNotSet={configuration.provisioningType !== ProvisioningType.auto}
+                />
+              }
+              autoTitle={translate('settings.authentication.gitlab.form.provisioning_with_gitlab')}
+              hasDifferentProvider={hasDifferentProvider}
+              hasFeatureEnabled={hasGitlabProvisioningFeature}
+              autoFeatureDisabledText={
+                <FormattedMessage
+                  id="settings.authentication.gitlab.form.provisioning.disabled"
+                  defaultMessage={translate(
+                    'settings.authentication.gitlab.form.provisioning.disabled',
                   )}
-                </ConfirmModal>
-              )}
-            </form>
-          </div>
+                  values={{
+                    documentation: (
+                      <DocumentationLink to="/instance-administration/authentication/gitlab">
+                        {translate('documentation')}
+                      </DocumentationLink>
+                    ),
+                  }}
+                />
+              }
+              autoDescription={
+                <FormattedMessage
+                  id="settings.authentication.gitlab.form.provisioning_with_gitlab.description"
+                  values={{
+                    documentation: (
+                      <DocumentationLink
+                        to={`/instance-administration/authentication/${
+                          DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitLab]
+                        }/#choosing-the-provisioning-method`}
+                      >
+                        {translate(`learn_more`)}
+                      </DocumentationLink>
+                    ),
+                  }}
+                />
+              }
+              onSyncNow={synchronizeNow}
+              canSync={canSyncNow}
+              synchronizationDetails={<GitLabSynchronisationWarning />}
+              autoSettings={
+                <AuthenticationFormField
+                  settingValue={provisioningToken}
+                  key={tokenKey}
+                  definition={provisioningTokenDefinition}
+                  mandatory
+                  onFieldChange={(_, value) =>
+                    setChangesWithCheck({
+                      ...changes,
+                      provisioningToken: value as string,
+                    })
+                  }
+                  isNotSet={!configuration.isProvisioningTokenSet}
+                />
+              }
+            />
+          </>
         )}
       </div>
+      {showConfirmProvisioningModal && provisioningType && (
+        <ConfirmModal
+          onConfirm={updateProvisioning}
+          header={translate('settings.authentication.gitlab.confirm', provisioningType)}
+          onClose={() => setShowConfirmProvisioningModal(false)}
+          confirmButtonText={translate(
+            'settings.authentication.gitlab.provisioning_change.confirm_changes',
+          )}
+        >
+          {translate('settings.authentication.gitlab.confirm', provisioningType, 'description')}
+        </ConfirmModal>
+      )}
       {openForm && (
         <GitLabConfigurationForm data={configuration ?? null} onClose={() => setOpenForm(false)} />
       )}
index d66da0f2a4cb3bba7991aa284be389689e2bbe95..287ef88a7e6dd9a4d0cf70547b4691d70a24ea22 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 { ButtonSecondary, FlagMessage, Highlight, Note, Spinner } from 'design-system';
 import React, { FormEvent, useState } from 'react';
 import { FormattedMessage } from 'react-intl';
 import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning';
-import DocLink from '../../../../components/common/DocLink';
+import DocumentationLink from '../../../../components/common/DocumentationLink';
 import ConfirmModal from '../../../../components/controls/ConfirmModal';
-import RadioCard from '../../../../components/controls/RadioCard';
-import Tooltip from '../../../../components/controls/Tooltip';
-import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import DeleteIcon from '../../../../components/icons/DeleteIcon';
-import EditIcon from '../../../../components/icons/EditIcon';
-import { Alert } from '../../../../components/ui/Alert';
-import Spinner from '../../../../components/ui/Spinner';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
 import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
 import {
@@ -36,15 +30,18 @@ import {
   useSyncWithGitHubNow,
 } from '../../../../queries/identity-provider/github';
 import { AlmKeys } from '../../../../types/alm-settings';
+import { ProvisioningType } from '../../../../types/provisioning';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { Provider } from '../../../../types/types';
 import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
 import AuthenticationFormField from './AuthenticationFormField';
-import AuthenticationFormFieldWrapper from './AuthenticationFormFieldWrapper';
 import AutoProvisioningConsent from './AutoProvisionningConsent';
+import ConfigurationDetails from './ConfigurationDetails';
 import ConfigurationForm from './ConfigurationForm';
 import GitHubConfigurationValidity from './GitHubConfigurationValidity';
 import GitHubMappingModal from './GitHubMappingModal';
+import ProvisioningSection from './ProvisioningSection';
+import TabHeader from './TabHeader';
 import useGithubConfiguration, {
   GITHUB_ADDITIONAL_FIELDS,
   GITHUB_JIT_FIELDS,
@@ -112,325 +109,220 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
     }
   };
 
+  const handleProvisioningTypeChange = (type: ProvisioningType) => {
+    setProvisioningType(type === ProvisioningType.auto);
+  };
+
   return (
     <Spinner loading={isLoading}>
-      <div className="authentication-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>
-        {enabled && !hasLegacyConfiguration && (
-          <GitHubConfigurationValidity
-            selectedOrganizations={
-              (values['sonar.auth.github.organizations']?.value as string[]) ?? []
-            }
-            isAutoProvisioning={!!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
-          />
-        )}
+      <div>
+        <TabHeader
+          title={translate('settings.authentication.github.configuration')}
+          showCreate={!hasConfiguration}
+          onCreate={handleCreateConfiguration}
+          configurationValidity={
+            <>
+              {enabled && !hasLegacyConfiguration && (
+                <GitHubConfigurationValidity
+                  selectedOrganizations={
+                    (values['sonar.auth.github.organizations']?.value as string[]) ?? []
+                  }
+                  isAutoProvisioning={!!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
+                />
+              )}
+            </>
+          }
+        />
         {!hasConfiguration && !hasLegacyConfiguration && (
-          <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
-            {translate('settings.authentication.github.form.not_configured')}
-          </div>
+          <div>{translate('settings.authentication.github.form.not_configured')}</div>
         )}
         {!hasConfiguration && hasLegacyConfiguration && (
-          <div className="big-padded">
-            <Alert variant="warning">
+          <FlagMessage variant="warning">
+            <div>
               <FormattedMessage
                 id="settings.authentication.github.form.legacy_configured"
                 defaultMessage={translate('settings.authentication.github.form.legacy_configured')}
                 values={{
                   documentation: (
-                    <DocLink to="/instance-administration/authentication/github">
+                    <DocumentationLink to="/instance-administration/authentication/github">
                       {translate('settings.authentication.github.form.legacy_configured.link')}
-                    </DocLink>
+                    </DocumentationLink>
                   ),
                 }}
               />
-            </Alert>
-          </div>
+            </div>
+          </FlagMessage>
         )}
         {hasConfiguration && (
           <>
-            <div className="spacer-bottom big-padded bordered display-flex-space-between">
-              <div>
-                <h5>{translateWithParameters('settings.authentication.github.appid_x', appId)}</h5>
-                <p>{url}</p>
-                <Tooltip
-                  overlay={
-                    githubProvisioningStatus
-                      ? translate('settings.authentication.form.disable.tooltip')
-                      : null
-                  }
-                >
-                  <Button
-                    className="spacer-top"
-                    onClick={toggleEnable}
-                    disabled={githubProvisioningStatus}
-                  >
-                    {enabled
-                      ? translate('settings.authentication.form.disable')
-                      : translate('settings.authentication.form.enable')}
-                  </Button>
-                </Tooltip>
-              </div>
-              <div>
-                <Button className="spacer-right" onClick={handleCreateConfiguration}>
-                  <EditIcon />
-                  {translate('settings.authentication.form.edit')}
-                </Button>
-                <Tooltip
-                  overlay={
-                    enabled || isDeleting
-                      ? translate('settings.authentication.form.delete.tooltip')
-                      : null
-                  }
-                >
-                  <Button
-                    className="button-red"
-                    disabled={enabled || isDeleting}
-                    onClick={deleteConfiguration}
-                  >
-                    <DeleteIcon />
-                    {translate('settings.authentication.form.delete')}
-                  </Button>
-                </Tooltip>
-              </div>
-            </div>
-            <div className="spacer-bottom big-padded bordered display-flex-space-between">
-              <form onSubmit={handleSubmit}>
-                <fieldset className="display-flex-column big-spacer-bottom">
-                  <label className="h5">
-                    {translate('settings.authentication.form.provisioning')}
-                  </label>
+            <ConfigurationDetails
+              title={translateWithParameters('settings.authentication.github.appid_x', appId)}
+              url={url}
+              canDisable={!githubProvisioningStatus}
+              enabled={enabled}
+              isDeleting={isDeleting}
+              onEdit={handleCreateConfiguration}
+              onDelete={deleteConfiguration}
+              onToggle={toggleEnable}
+            />
 
-                  {enabled ? (
-                    <div className="display-flex-column spacer-top">
-                      <RadioCard
-                        className="sw-min-h-0"
-                        label={translate('settings.authentication.form.provisioning_at_login')}
-                        title={translate('settings.authentication.form.provisioning_at_login')}
-                        selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
-                        onClick={() => setProvisioningType(false)}
+            <ProvisioningSection
+              provisioningType={
+                newGithubProvisioningStatus ?? githubProvisioningStatus
+                  ? ProvisioningType.auto
+                  : ProvisioningType.jit
+              }
+              onChangeProvisioningType={handleProvisioningTypeChange}
+              disabledConfigText={translate('settings.authentication.github.enable_first')}
+              enabled={enabled}
+              hasUnsavedChanges={!!hasGithubProvisioningConfigChange}
+              onSave={handleSubmit}
+              onCancel={() => {
+                setProvisioningType(undefined);
+                resetJitSetting();
+              }}
+              jitTitle={translate('settings.authentication.form.provisioning_at_login')}
+              jitDescription={
+                <FormattedMessage
+                  id="settings.authentication.github.form.provisioning_at_login.description"
+                  values={{
+                    documentation: (
+                      <DocumentationLink
+                        to={`/instance-administration/authentication/${
+                          DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
+                        }/`}
                       >
-                        <p className="spacer-bottom">
-                          <FormattedMessage id="settings.authentication.github.form.provisioning_at_login.description" />
-                        </p>
-                        <p className="spacer-bottom">
-                          <FormattedMessage
-                            id="settings.authentication.github.form.description.doc"
-                            values={{
-                              documentation: (
-                                <DocLink
-                                  to={`/instance-administration/authentication/${
-                                    DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
-                                  }/`}
-                                >
-                                  {translate('documentation')}
-                                </DocLink>
-                              ),
-                            }}
-                          />
-                        </p>
-
-                        {!(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
-                          <>
-                            <hr />
-                            {Object.values(values).map((val) => {
-                              if (!GITHUB_JIT_FIELDS.includes(val.key)) {
-                                return null;
-                              }
-                              return (
-                                <div key={val.key} className="sw-mb-8">
-                                  <AuthenticationFormField
-                                    settingValue={
-                                      values[val.key]?.newValue ?? values[val.key]?.value
-                                    }
-                                    definition={val.definition}
-                                    mandatory={val.mandatory}
-                                    onFieldChange={setNewValue}
-                                    isNotSet={val.isNotSet}
-                                  />
-                                </div>
-                              );
-                            })}
-                          </>
-                        )}
-                      </RadioCard>
-                      <RadioCard
-                        className="spacer-top sw-min-h-0"
-                        label={translate(
-                          'settings.authentication.github.form.provisioning_with_github',
-                        )}
-                        title={translate(
-                          'settings.authentication.github.form.provisioning_with_github',
+                        {translate('learn_more')}
+                      </DocumentationLink>
+                    ),
+                  }}
+                />
+              }
+              jitSettings={
+                <>
+                  {Object.values(values).map((val) => {
+                    if (!GITHUB_JIT_FIELDS.includes(val.key)) {
+                      return null;
+                    }
+                    return (
+                      <AuthenticationFormField
+                        key={val.key}
+                        settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
+                        definition={val.definition}
+                        mandatory={val.mandatory}
+                        onFieldChange={setNewValue}
+                        isNotSet={val.isNotSet}
+                      />
+                    );
+                  })}
+                </>
+              }
+              autoTitle={translate('settings.authentication.github.form.provisioning_with_github')}
+              hasDifferentProvider={hasDifferentProvider}
+              hasFeatureEnabled={hasGithubProvisioning}
+              autoFeatureDisabledText={
+                <FormattedMessage
+                  id="settings.authentication.github.form.provisioning.disabled"
+                  defaultMessage={translate(
+                    'settings.authentication.github.form.provisioning.disabled',
+                  )}
+                  values={{
+                    documentation: (
+                      <DocumentationLink to="/instance-administration/authentication/github">
+                        {translate('documentation')}
+                      </DocumentationLink>
+                    ),
+                  }}
+                />
+              }
+              autoDescription={
+                <FormattedMessage
+                  id="settings.authentication.github.form.provisioning_with_github.description"
+                  values={{
+                    documentation: (
+                      <DocumentationLink
+                        to={`/instance-administration/authentication/${
+                          DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
+                        }/`}
+                      >
+                        {translate('learn_more')}
+                      </DocumentationLink>
+                    ),
+                  }}
+                />
+              }
+              synchronizationDetails={<GitHubSynchronisationWarning />}
+              onSyncNow={synchronizeNow}
+              canSync={canSyncNow}
+              autoSettings={
+                <>
+                  {Object.values(values).map((val) => {
+                    if (!GITHUB_PROVISIONING_FIELDS.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}
+                        />
+                      </div>
+                    );
+                  })}
+                  <div className="sw-mt-6">
+                    <div className="sw-flex">
+                      <Highlight className="sw-mb-4 sw-mr-4 sw-flex sw-items-center sw-gap-2">
+                        {translate(
+                          'settings.authentication.github.configuration.roles_mapping.title',
                         )}
-                        selected={newGithubProvisioningStatus ?? githubProvisioningStatus}
-                        onClick={() => setProvisioningType(true)}
-                        disabled={!hasGithubProvisioning || hasDifferentProvider}
+                      </Highlight>
+                      <ButtonSecondary
+                        className="sw--mt-2"
+                        onClick={() => setShowMappingModal(true)}
                       >
-                        {hasGithubProvisioning ? (
-                          <>
-                            {hasDifferentProvider && (
-                              <p className="spacer-bottom text-bold ">
-                                {translate(
-                                  'settings.authentication.form.other_provisioning_enabled',
-                                )}
-                              </p>
-                            )}
-                            <p className="spacer-bottom">
-                              {translate(
-                                'settings.authentication.github.form.provisioning_with_github.description',
-                              )}
-                            </p>
-                            <p className="spacer-bottom">
-                              <FormattedMessage
-                                id="settings.authentication.github.form.description.doc"
-                                values={{
-                                  documentation: (
-                                    <DocLink
-                                      to={`/instance-administration/authentication/${
-                                        DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
-                                      }/`}
-                                    >
-                                      {translate('documentation')}
-                                    </DocLink>
-                                  ),
-                                }}
-                              />
-                            </p>
-
-                            {githubProvisioningStatus && <GitHubSynchronisationWarning />}
-                            {(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
-                              <>
-                                <div className="sw-flex sw-flex-1 spacer-bottom">
-                                  <Button
-                                    className="spacer-top width-30"
-                                    onClick={synchronizeNow}
-                                    disabled={!canSyncNow}
-                                  >
-                                    {translate('settings.authentication.github.synchronize_now')}
-                                  </Button>
-                                </div>
-                                <hr />
-                                {Object.values(values).map((val) => {
-                                  if (!GITHUB_PROVISIONING_FIELDS.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}
-                                      />
-                                    </div>
-                                  );
-                                })}
-                                <AuthenticationFormFieldWrapper
-                                  title={translate(
-                                    'settings.authentication.github.configuration.roles_mapping.title',
-                                  )}
-                                  description={translate(
-                                    'settings.authentication.github.configuration.roles_mapping.description',
-                                  )}
-                                >
-                                  <Button
-                                    className="spacer-top"
-                                    onClick={() => setShowMappingModal(true)}
-                                  >
-                                    {translate(
-                                      'settings.authentication.github.configuration.roles_mapping.button_label',
-                                    )}
-                                  </Button>
-                                </AuthenticationFormFieldWrapper>
-                              </>
-                            )}
-                          </>
-                        ) : (
-                          <p>
-                            <FormattedMessage
-                              id="settings.authentication.github.form.provisioning.disabled"
-                              defaultMessage={translate(
-                                'settings.authentication.github.form.provisioning.disabled',
-                              )}
-                              values={{
-                                documentation: (
-                                  // Documentation page not ready yet.
-                                  <DocLink to="/instance-administration/authentication/github">
-                                    {translate('documentation')}
-                                  </DocLink>
-                                ),
-                              }}
-                            />
-                          </p>
+                        {translate(
+                          'settings.authentication.github.configuration.roles_mapping.button_label',
                         )}
-                      </RadioCard>
+                      </ButtonSecondary>
                     </div>
-                  ) : (
-                    <Alert className="big-spacer-top" variant="info">
-                      {translate('settings.authentication.github.enable_first')}
-                    </Alert>
-                  )}
-                </fieldset>
-                {enabled && (
-                  <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
-                    <SubmitButton disabled={!hasGithubProvisioningConfigChange}>
-                      {translate('save')}
-                    </SubmitButton>
-                    <ResetButtonLink
-                      onClick={() => {
-                        setProvisioningType(undefined);
-                        resetJitSetting();
-                      }}
-                      disabled={!hasGithubProvisioningConfigChange}
-                    >
-                      {translate('cancel')}
-                    </ResetButtonLink>
-                    <Alert variant="warning" className="sw-mb-0">
-                      {hasGithubProvisioningConfigChange &&
-                        translate('settings.authentication.github.configuration.unsaved_changes')}
-                    </Alert>
+                    <Note className="sw-mt-2">
+                      {translate(
+                        'settings.authentication.github.configuration.roles_mapping.description',
+                      )}
+                    </Note>
                   </div>
+                </>
+              }
+            />
+            {showConfirmProvisioningModal && (
+              <ConfirmModal
+                onConfirm={() => changeProvisioning()}
+                header={translate(
+                  'settings.authentication.github.confirm',
+                  newGithubProvisioningStatus ? 'auto' : 'jit',
                 )}
-                {showConfirmProvisioningModal && (
-                  <ConfirmModal
-                    onConfirm={() => changeProvisioning()}
-                    header={translate(
-                      'settings.authentication.github.confirm',
-                      newGithubProvisioningStatus ? 'auto' : 'jit',
-                    )}
-                    onClose={() => setShowConfirmProvisioningModal(false)}
-                    confirmButtonText={translate(
-                      'settings.authentication.github.provisioning_change.confirm_changes',
-                    )}
-                  >
-                    {translate(
-                      'settings.authentication.github.confirm',
-                      newGithubProvisioningStatus ? 'auto' : 'jit',
-                      'description',
-                    )}
-                  </ConfirmModal>
+                onClose={() => setShowConfirmProvisioningModal(false)}
+                confirmButtonText={translate(
+                  'settings.authentication.github.provisioning_change.confirm_changes',
                 )}
-              </form>
-              {showMappingModal && (
-                <GitHubMappingModal
-                  mapping={rolesMapping}
-                  setMapping={setRolesMapping}
-                  onClose={() => setShowMappingModal(false)}
-                />
-              )}
-            </div>
+              >
+                {translate(
+                  'settings.authentication.github.confirm',
+                  newGithubProvisioningStatus ? 'auto' : 'jit',
+                  'description',
+                )}
+              </ConfirmModal>
+            )}
+            {showMappingModal && (
+              <GitHubMappingModal
+                mapping={rolesMapping}
+                setMapping={setRolesMapping}
+                onClose={() => setShowMappingModal(false)}
+              />
+            )}
           </>
         )}
 
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/ProvisioningSection.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/ProvisioningSection.tsx
new file mode 100644 (file)
index 0000000..014ebeb
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 {
+  BasicSeparator,
+  ButtonPrimary,
+  ButtonSecondary,
+  FlagMessage,
+  RadioButton,
+  SubHeading,
+} from 'design-system';
+import React, { FormEvent, ReactElement } from 'react';
+import { translate } from '../../../../helpers/l10n';
+import { ProvisioningType } from '../../../../types/provisioning';
+
+interface Props {
+  provisioningType: ProvisioningType;
+  onChangeProvisioningType: (val: ProvisioningType) => void;
+  disabledConfigText: string;
+  jitTitle: string;
+  jitDescription: string | ReactElement;
+  jitSettings?: ReactElement;
+  autoTitle: string;
+  autoDescription: ReactElement;
+  synchronizationDetails?: ReactElement;
+  autoSettings?: ReactElement;
+  hasFeatureEnabled: boolean;
+  hasDifferentProvider: boolean;
+  autoFeatureDisabledText: string | ReactElement;
+  onSave: (e: FormEvent) => void;
+  onSyncNow?: () => void;
+  onCancel: () => void;
+  hasUnsavedChanges: boolean;
+  canSave?: boolean;
+  canSync?: boolean;
+  enabled: boolean;
+}
+
+export default function ProvisioningSection(props: Readonly<Props>) {
+  const {
+    provisioningType,
+    jitTitle,
+    jitDescription,
+    jitSettings,
+    autoTitle,
+    autoDescription,
+    autoSettings,
+    hasFeatureEnabled,
+    hasDifferentProvider,
+    autoFeatureDisabledText,
+    synchronizationDetails,
+    onChangeProvisioningType,
+    onSave,
+    onSyncNow,
+    onCancel,
+    hasUnsavedChanges,
+    enabled,
+    disabledConfigText,
+    canSave = true,
+    canSync,
+  } = props;
+  return (
+    <div className="sw-mb-2">
+      <form onSubmit={onSave}>
+        <SubHeading as="h5">{translate('settings.authentication.form.provisioning')}</SubHeading>
+        {enabled ? (
+          <>
+            <ul>
+              <li>
+                <RadioButton
+                  id="jit"
+                  checked={provisioningType === ProvisioningType.jit}
+                  onCheck={onChangeProvisioningType}
+                  className="sw-items-start"
+                  value={ProvisioningType.jit}
+                >
+                  <div>
+                    <div className="sw-body-sm-highlight">{jitTitle}</div>
+
+                    <div className="sw-mt-1">{jitDescription}</div>
+                  </div>
+                </RadioButton>
+                {provisioningType === ProvisioningType.jit && jitSettings && (
+                  <div className="sw-ml-16 sw-mt-6 sw-max-w-[435px]">{jitSettings}</div>
+                )}
+                <BasicSeparator className="sw-my-4" />
+              </li>
+              <li>
+                <RadioButton
+                  id="github-auto"
+                  className="sw-items-start"
+                  checked={provisioningType === ProvisioningType.auto}
+                  onCheck={onChangeProvisioningType}
+                  value={ProvisioningType.auto}
+                  disabled={!hasFeatureEnabled || hasDifferentProvider}
+                >
+                  <div>
+                    <div className="sw-body-sm-highlight">{autoTitle}</div>
+                    <div className="sw-mt-1">
+                      {hasFeatureEnabled ? (
+                        <>
+                          {hasDifferentProvider && (
+                            <p className="sw-mb-2 sw-body-sm-highlight">
+                              {translate('settings.authentication.form.other_provisioning_enabled')}
+                            </p>
+                          )}
+                          {autoDescription}
+                        </>
+                      ) : (
+                        autoFeatureDisabledText
+                      )}
+                    </div>
+                  </div>
+                </RadioButton>
+                {provisioningType === ProvisioningType.auto && (
+                  <div className="sw-ml-6 sw-mt-6">
+                    {synchronizationDetails}
+                    {onSyncNow && (
+                      <div className="sw-mb-4 sw-mt-6">
+                        <ButtonPrimary onClick={onSyncNow} disabled={!canSync}>
+                          {translate('settings.authentication.github.synchronize_now')}
+                        </ButtonPrimary>
+                      </div>
+                    )}
+                    <div className="sw-ml-10 sw-mt-8 sw-max-w-[435px]">{autoSettings}</div>
+                  </div>
+                )}
+                <BasicSeparator className="sw-my-4" />
+              </li>
+            </ul>
+            <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
+              <ButtonPrimary type="submit" disabled={!hasUnsavedChanges || !canSave}>
+                {translate('save')}
+              </ButtonPrimary>
+              <ButtonSecondary onClick={onCancel} disabled={!hasUnsavedChanges}>
+                {translate('cancel')}
+              </ButtonSecondary>
+              <FlagMessage variant="warning" className="sw-mb-0">
+                {hasUnsavedChanges &&
+                  translate('settings.authentication.github.configuration.unsaved_changes')}
+              </FlagMessage>
+            </div>
+          </>
+        ) : (
+          <FlagMessage className="sw-mt-4" variant="info">
+            {disabledConfigText}
+          </FlagMessage>
+        )}
+      </form>
+    </div>
+  );
+}
index 66a7bef88c8a62a166f7da7e04f2a268857061eb..b253dc807878e960464aa230a156c2912080d6f6 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 React from 'react';
+import { ButtonSecondary, Spinner } from 'design-system';
+import React, { FormEvent } from 'react';
 import { FormattedMessage } from 'react-intl';
-import DocLink from '../../../../components/common/DocLink';
-import Link from '../../../../components/common/Link';
+import DocumentationLink from '../../../../components/common/DocumentationLink';
 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 Spinner from '../../../../components/ui/Spinner';
 import { translate } from '../../../../helpers/l10n';
 import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
 import { useToggleScimMutation } from '../../../../queries/identity-provider/scim';
 import { useSaveValueMutation } from '../../../../queries/settings';
+import { ProvisioningType } from '../../../../types/provisioning';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { Provider } from '../../../../types/types';
+import ConfigurationDetails from './ConfigurationDetails';
 import ConfigurationForm from './ConfigurationForm';
+import ProvisioningSection from './ProvisioningSection';
+import TabHeader from './TabHeader';
 import useSamlConfiguration, {
   SAML_ENABLED_FIELD,
   SAML_SCIM_DEPRECATED,
@@ -108,196 +105,121 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
 
   return (
     <Spinner loading={isLoading}>
-      <div className="authentication-configuration">
-        <div className="spacer-bottom display-flex-space-between display-flex-center">
-          <h4>{translate('settings.authentication.saml.configuration')}</h4>
+      <div>
+        <TabHeader
+          title={translate('settings.authentication.saml.configuration')}
+          showCreate={!hasConfiguration}
+          onCreate={handleCreateConfiguration}
+        />
 
-          {!hasConfiguration && (
-            <div>
-              <Button onClick={handleCreateConfiguration}>
-                {translate('settings.authentication.form.create')}
-              </Button>
-            </div>
-          )}
-        </div>
         {!hasConfiguration && (
-          <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
-            {translate('settings.authentication.saml.form.not_configured')}
-          </div>
+          <div>{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="authentication-enabled spacer-left">
-                      <CheckIcon className="spacer-right" />
-                      {translate('settings.authentication.form.enabled')}
-                    </span>
-                  ) : (
-                    translate('settings.authentication.form.not_enabled')
-                  )}
-                </p>
-                <Button className="spacer-top" disabled={scimStatus} onClick={handleToggleEnable}>
-                  {samlEnabled
-                    ? translate('settings.authentication.form.disable')
-                    : translate('settings.authentication.form.enable')}
-                </Button>
-              </div>
-              <div>
-                <Link className="button spacer-right" target="_blank" to={CONFIG_TEST_PATH}>
+            <ConfigurationDetails
+              title={name?.toString() ?? ''}
+              url={url}
+              canDisable={!scimStatus}
+              enabled={samlEnabled}
+              isDeleting={isDeleting}
+              onEdit={handleCreateConfiguration}
+              onDelete={deleteConfiguration}
+              onToggle={handleToggleEnable}
+              extraActions={
+                <ButtonSecondary target="_blank" to={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 || isDeleting}
-                  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 (hasScimTypeChange) {
-                    setShowConfirmProvisioningModal(true);
-                  } else {
-                    handleSaveGroup();
-                  }
-                }}
-              >
-                <fieldset className="display-flex-column big-spacer-bottom">
-                  <label className="h5">
-                    {translate('settings.authentication.form.provisioning')}
-                  </label>
-                  {samlEnabled ? (
-                    <div className="display-flex-column spacer-top">
-                      <RadioCard
-                        className="sw-min-h-0"
-                        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>
-                      </RadioCard>
-                      <RadioCard
-                        className="spacer-top sw-min-h-0"
-                        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 || hasDifferentProvider}
-                      >
-                        {!hasScim ? (
-                          <p>
-                            <FormattedMessage
-                              id="settings.authentication.saml.form.provisioning.disabled"
-                              values={{
-                                documentation: (
-                                  <DocLink to="/instance-administration/authentication/saml/scim/overview">
-                                    {translate('documentation')}
-                                  </DocLink>
-                                ),
-                              }}
-                            />
-                          </p>
-                        ) : (
-                          <>
-                            {hasDifferentProvider && (
-                              <p className="spacer-bottom text-bold">
-                                {translate(
-                                  'settings.authentication.form.other_provisioning_enabled',
-                                )}
-                              </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>
-                    </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')}
-                  >
+                </ButtonSecondary>
+              }
+            />
+            <ProvisioningSection
+              provisioningType={
+                newScimStatus ?? scimStatus ? ProvisioningType.auto : ProvisioningType.jit
+              }
+              onChangeProvisioningType={(val: ProvisioningType) =>
+                setNewScimStatus(val === ProvisioningType.auto)
+              }
+              disabledConfigText={translate('settings.authentication.saml.enable_first')}
+              enabled={samlEnabled}
+              hasUnsavedChanges={hasScimConfigChange}
+              onSave={(e: FormEvent) => {
+                e.preventDefault();
+                if (hasScimTypeChange) {
+                  setShowConfirmProvisioningModal(true);
+                } else {
+                  handleSaveGroup();
+                }
+              }}
+              onCancel={() => {
+                setNewScimStatus(undefined);
+                setNewGroupSetting();
+              }}
+              jitTitle={translate('settings.authentication.saml.form.provisioning_at_login')}
+              jitDescription={translate(
+                'settings.authentication.saml.form.provisioning_at_login.sub',
+              )}
+              autoTitle={translate('settings.authentication.saml.form.provisioning_with_scim')}
+              hasDifferentProvider={hasDifferentProvider}
+              hasFeatureEnabled={hasScim}
+              autoFeatureDisabledText={
+                <FormattedMessage
+                  id="settings.authentication.saml.form.provisioning.disabled"
+                  values={{
+                    documentation: (
+                      <DocumentationLink to="/instance-administration/authentication/saml/scim/overview">
+                        {translate('documentation')}
+                      </DocumentationLink>
+                    ),
+                  }}
+                />
+              }
+              autoDescription={
+                <>
+                  <p className="sw-mb-2">
+                    {translate('settings.authentication.saml.form.provisioning_with_scim.sub')}
+                  </p>
+                  <p className="sw-mb-2">
                     {translate(
-                      'settings.authentication.saml.confirm',
-                      newScimStatus ? 'scim' : 'jit',
-                      'description',
+                      'settings.authentication.saml.form.provisioning_with_scim.description',
                     )}
-                  </ConfirmModal>
+                  </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: (
+                          <DocumentationLink to="/instance-administration/authentication/saml/scim/overview">
+                            {translate('documentation')}
+                          </DocumentationLink>
+                        ),
+                      }}
+                    />
+                  </p>
+                </>
+              }
+            />
+            {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',
                 )}
-              </form>
-            </div>
+              </ConfirmModal>
+            )}
           </>
         )}
         {showEditModal && (
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/TabHeader.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/TabHeader.tsx
new file mode 100644 (file)
index 0000000..5b9daef
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { BasicSeparator, ButtonPrimary, SubHeading } from 'design-system';
+import React, { ReactElement } from 'react';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  title: string;
+  showCreate: boolean;
+  onCreate: () => void;
+  configurationValidity?: ReactElement;
+}
+
+export default function TabHeader({
+  title,
+  showCreate,
+  onCreate,
+  configurationValidity,
+}: Readonly<Props>) {
+  return (
+    <>
+      <div className="sw-mb-4">
+        <SubHeading as="h4">{title}</SubHeading>
+        {showCreate && (
+          <ButtonPrimary className="sw-mt-2" onClick={onCreate}>
+            {translate('settings.authentication.form.create')}
+          </ButtonPrimary>
+        )}
+      </div>
+      {configurationValidity}
+      <BasicSeparator className="sw-my-6" />
+    </>
+  );
+}
index 121c39908a74b204cbfb56e629488a14e65d60b5..759a8d3c8647765f5c5d9e3cf8ab8cc385441775 100644 (file)
@@ -138,13 +138,19 @@ const ui = {
     }),
   enableFirstMessage: ghContainer.byText('settings.authentication.github.enable_first'),
   jitProvisioningButton: ghContainer.byRole('radio', {
-    name: 'settings.authentication.form.provisioning_at_login',
+    name: /settings.authentication.form.provisioning_at_login/,
   }),
   githubProvisioningButton: ghContainer.byRole('radio', {
-    name: 'settings.authentication.github.form.provisioning_with_github',
+    name: /settings.authentication.github.form.provisioning_with_github/,
   }),
-  githubProvisioningPending: ghContainer.byText(/synchronization_pending/),
-  githubProvisioningInProgress: ghContainer.byText(/synchronization_in_progress/),
+  githubProvisioningPending: ghContainer
+    .byRole('list')
+    .byRole('status')
+    .byText(/synchronization_pending/),
+  githubProvisioningInProgress: ghContainer
+    .byRole('list')
+    .byRole('status')
+    .byText(/synchronization_in_progress/),
   githubProvisioningSuccess: ghContainer.byText(/synchronization_successful/),
   githubProvisioningAlert: ghContainer.byText(/synchronization_failed/),
   configurationValidityLoading: ghContainer.byRole('status', {
@@ -369,7 +375,7 @@ describe('Github tab', () => {
       renderAuthentication([Feature.GithubProvisioning]);
       await ui.enableProvisioning(user);
       expect(ui.githubProvisioningAlert.get()).toBeInTheDocument();
-      expect(ui.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
+      expect(ghContainer.get()).toHaveTextContent("T'es mauvais Jacques");
       expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
     });
 
@@ -386,7 +392,7 @@ describe('Github tab', () => {
       renderAuthentication([Feature.GithubProvisioning]);
       await ui.enableProvisioning(user);
       expect(ui.githubProvisioningAlert.get()).toBeInTheDocument();
-      expect(ui.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
+      expect(ghContainer.get()).toHaveTextContent("T'es mauvais Jacques");
       expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
       expect(ui.githubProvisioningInProgress.get()).toBeInTheDocument();
     });
index 432e781aa0e601584db8fe5be2b5208b1d7fb607..d9cfe1f9e8f6cf704e31e6a3e157e625e379e917 100644 (file)
@@ -90,10 +90,10 @@ const ui = {
   }),
   saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
   jitProvisioningRadioButton: glContainer.byRole('radio', {
-    name: 'settings.authentication.gitlab.provisioning_at_login',
+    name: /settings.authentication.gitlab.provisioning_at_login/,
   }),
   autoProvisioningRadioButton: glContainer.byRole('radio', {
-    name: 'settings.authentication.gitlab.form.provisioning_with_gitlab',
+    name: /settings.authentication.gitlab.form.provisioning_with_gitlab/,
   }),
   jitAllowUsersToSignUpToggle: byRole('switch', { name: 'property.allowUsersToSignUp.name' }),
   autoProvisioningToken: byRole('textbox', {
@@ -122,11 +122,19 @@ const ui = {
   }),
   syncSummary: glContainer.byText(/Test summary/),
   syncWarning: glContainer.byText(/Warning/),
-  gitlabProvisioningPending: glContainer.byText(/synchronization_pending/),
-  gitlabProvisioningInProgress: glContainer.byText(/synchronization_in_progress/),
+  gitlabProvisioningPending: glContainer
+    .byRole('list')
+    .byRole('status')
+    .byText(/synchronization_pending/),
+  gitlabProvisioningInProgress: glContainer
+    .byRole('list')
+    .byRole('status')
+    .byText(/synchronization_in_progress/),
   gitlabProvisioningSuccess: glContainer.byText(/synchronization_successful/),
   gitlabProvisioningAlert: glContainer.byText(/synchronization_failed/),
-  gitlabConfigurationStatus: glContainer.byRole('status'),
+  gitlabConfigurationStatus: glContainer.byRole('status', {
+    name: /settings.authentication.gitlab.configuration/,
+  }),
   testConfiguration: glContainer.byRole('button', {
     name: 'settings.authentication.configuration.test',
   }),
@@ -424,7 +432,7 @@ describe('Gitlab Provisioning', () => {
     });
     renderAuthentication([Feature.GitlabProvisioning]);
     expect(await ui.gitlabProvisioningAlert.find()).toBeInTheDocument();
-    expect(ui.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
+    expect(glContainer.get()).toHaveTextContent("T'es mauvais Jacques");
     expect(ui.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
   });
 
@@ -442,7 +450,7 @@ describe('Gitlab Provisioning', () => {
     });
     renderAuthentication([Feature.GitlabProvisioning]);
     expect(await ui.gitlabProvisioningAlert.find()).toBeInTheDocument();
-    expect(ui.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
+    expect(glContainer.get()).toHaveTextContent("T'es mauvais Jacques");
     expect(ui.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
     expect(ui.gitlabProvisioningInProgress.get()).toBeInTheDocument();
   });
@@ -464,20 +472,20 @@ describe('Gitlab Provisioning', () => {
     const user = userEvent.setup();
     renderAuthentication([Feature.GitlabProvisioning]);
 
-    expect((await ui.gitlabConfigurationStatus.findAll())[1]).toHaveTextContent(
+    expect(await ui.gitlabConfigurationStatus.find()).toHaveTextContent(
       'settings.authentication.gitlab.configuration.valid.AUTO_PROVISIONING',
     );
     await user.click(ui.jitProvisioningRadioButton.get());
     await user.click(ui.saveProvisioning.get());
     await user.click(ui.confirmProvisioningChange.get());
-    expect(ui.gitlabConfigurationStatus.getAll()[1]).toHaveTextContent(
+    expect(ui.gitlabConfigurationStatus.get()).toHaveTextContent(
       'settings.authentication.gitlab.configuration.valid.JIT',
     );
     handler.setGitlabConfigurations([
       mockGitlabConfiguration({ ...handler.gitlabConfigurations[0], errorMessage: 'ERROR' }),
     ]);
     await user.click(ui.testConfiguration.get());
-    expect(ui.gitlabConfigurationStatus.getAll()[1]).toHaveTextContent('ERROR');
+    expect(glContainer.get()).toHaveTextContent('ERROR');
     await user.click(ui.disableConfigButton.get());
     expect(ui.gitlabConfigurationStatus.query()).not.toBeInTheDocument();
   });
index 48a34862af8795b581c0bb4ac9a059b3d0fb887b..d2b8376f2669e0d811b2be77a99563f876f03785 100644 (file)
@@ -96,10 +96,10 @@ const ui = {
   }),
   enableFirstMessage: byText('settings.authentication.saml.enable_first'),
   jitProvisioningButton: byRole('radio', {
-    name: 'settings.authentication.saml.form.provisioning_at_login',
+    name: /settings.authentication.saml.form.provisioning_at_login/,
   }),
   scimProvisioningButton: byRole('radio', {
-    name: 'settings.authentication.saml.form.provisioning_with_scim',
+    name: /settings.authentication.saml.form.provisioning_with_scim/,
   }),
   fillForm: async (user: UserEvent) => {
     await user.clear(ui.providerName.get());
index abcec5b1ded60d3f9ff6a69dc6b70a0f52a41631..bf2345a6c0e83c5f835de06daf04dbe76dbdf247 100644 (file)
@@ -66,13 +66,13 @@ it('should render tabs and allow navigation', async () => {
 
   expect(screen.getAllByRole('tab')).toHaveLength(4);
 
-  expect(screen.getByRole('tab', { name: 'SAML' })).toHaveAttribute('aria-selected', 'true');
+  expect(screen.getByRole('tab', { name: 'SAML' })).toHaveAttribute('aria-current', 'true');
 
   await user.click(screen.getByRole('tab', { name: 'github GitHub' }));
 
-  expect(screen.getByRole('tab', { name: 'SAML' })).toHaveAttribute('aria-selected', 'false');
+  expect(screen.getByRole('tab', { name: 'SAML' })).toHaveAttribute('aria-current', 'false');
   expect(screen.getByRole('tab', { name: 'github GitHub' })).toHaveAttribute(
-    'aria-selected',
+    'aria-current',
     'true',
   );
 });
index 0ad2ea180139485efe422043283ad982d5c8709f..2eaca9065a931078852825c59d74dfb0a1246e87 100644 (file)
   padding: 16px;
   overflow-wrap: break-word;
 }
-
-.authentication-enabled {
-  color: var(--success500);
-}
-
-.authentication-no-config {
-  background-color: var(--neutral50);
-  color: var(--blacka60);
-}
-
-.authentication-configuration .radio-card {
-  width: 100%;
-  min-height: 250px;
-  background-color: var(--neutral50);
-  border: 1px solid var(--neutral200);
-}
-
-.authentication-configuration .radio-card.selected {
-  background-color: var(--info50);
-  border: 1px solid var(--info500);
-}
-
-.authentication-configuration .radio-card:hover:not(.selected) {
-  border: 1px solid var(--info500);
-}
-
-.authentication-configuration fieldset > div {
-  justify-content: space-between;
-}
-
-.authentication-configuration .radio-card-body {
-  justify-content: flex-start;
-}
-
-.authentication-configuration .settings-definition-left {
-  width: 50%;
-}
-
-.authentication-configuration .settings-definition-right {
-  align-items: end;
-  width: 20%;
-}
index 3fdcc71687d548bee8b6a0e639c6bf1c6eb306d9..7b41997b9cb8f83725119d96856cd5dc5bda948a 100644 (file)
@@ -1508,8 +1508,6 @@ settings.authentication.form.edit=Edit
 settings.authentication.form.delete=Delete
 settings.authentication.form.delete.tooltip=You can only delete a configuration if it is disabled.
 settings.authentication.form.loading=Loading configuration
-settings.authentication.form.enabled=Enabled
-settings.authentication.form.not_enabled=This configuration is disabled
 settings.authentication.form.enable=Enable configuration
 settings.authentication.form.disable=Disable configuration
 settings.authentication.form.disable.tooltip=You can not disable this configuration while using Automatic Provisioning. You must be in Just-in-Time Provisioning mode to disable this configuration.
@@ -1541,9 +1539,9 @@ settings.authentication.github.enable_first=Enable your GitHub configuration for
 settings.authentication.github.form.provisioning_with_github=Automatic user, group, and permission provisioning
 settings.authentication.github.form.provisioning_with_github_short.autoProvisioning=Automatic provisioning
 settings.authentication.github.form.provisioning_with_github_short.jit=Just-in-Time provisioning
-settings.authentication.github.form.provisioning_with_github.description=Users, groups and permissions are automatically provisioned from your GitHub organizations. Once activated, users and groups can only be created and modified from your GitHub organizations/teams. Existing local users will be kept and can only be deactivated.
+settings.authentication.github.form.provisioning_with_github.description=Users, groups and permissions are automatically provisioned from your GitHub organizations. Once activated, users and groups can only be created and modified from your GitHub organizations/teams. Existing local users will be kept and can only be deactivated. {documentation}
 settings.authentication.github.form.description.doc=For more details, see {documentation}.
-settings.authentication.github.form.provisioning_at_login.description=Users and groups are synchronized only when users log in to SonarQube.
+settings.authentication.github.form.provisioning_at_login.description=Users and groups are synchronized only when users log in to SonarQube. {documentation}
 settings.authentication.github.form.provisioning.disabled=Your current edition does not support provisioning with GitHub. See the {documentation} for more information.
 settings.authentication.github.synchronize_now=Synchronize now
 settings.authentication.github.synchronization_in_progress=Synchronization is in progress.
@@ -1596,9 +1594,10 @@ settings.authentication.gitlab.form.allowUsersToSignUp.name=Allow users to sign
 settings.authentication.gitlab.form.allowUsersToSignUp.description=Allow new users to authenticate. When set to disabled, only existing users will be able to authenticate to the server.
 settings.authentication.gitlab.form.provisioningToken.name=Provisioning token
 settings.authentication.gitlab.form.provisioningToken.description=Token used for user provisioning. You can either use a group or a personal access token, as long as it has visibility on the groups that need to be imported.
+settings.authentication.gitlab.applicationId.name=App ID: {0}
 settings.authentication.gitlab.enable_first=Enable your GitLab configuration for more provisioning options.
 settings.authentication.gitlab.provisioning_at_login=Just-in-Time user provisioning (default)
-settings.authentication.gitlab.provisioning_at_login.description=Users are synchronized only when users log in to SonarQube.
+settings.authentication.gitlab.provisioning_at_login.description=Users are synchronized only when users log in to SonarQube. {documentation}
 settings.authentication.gitlab.description.JIT.learn_more=Learn more about Just-in-Time provisioning with GitLab
 settings.authentication.gitlab.description.AUTO_PROVISIONING.learn_more=Learn more about automatic provisioning with GitLab
 settings.authentication.gitlab.confirm.AUTO_PROVISIONING=Switch to automatic provisioning
@@ -1607,7 +1606,7 @@ settings.authentication.gitlab.confirm.AUTO_PROVISIONING.description=Once you tr
 settings.authentication.gitlab.confirm.JIT.description=Switching to Just-in-Time provisioning removes the automatic synchronization of users and groups. Users are provisioned and updated only at user login. Are you sure?
 settings.authentication.gitlab.provisioning_change.confirm_changes=Confirm Changes
 settings.authentication.gitlab.form.provisioning_with_gitlab=Automatic user and group provisioning
-settings.authentication.gitlab.form.provisioning_with_gitlab.description=Users and groups are automatically provisioned from GitLab. Once activated, users and groups can only be created and modified from GitLab. Existing local users will be kept and can only be deactivated.
+settings.authentication.gitlab.form.provisioning_with_gitlab.description=Users and groups are automatically provisioned from GitLab. Once activated, users and groups can only be created and modified from GitLab. Existing local users will be kept and can only be deactivated. {documentation}
 settings.authentication.gitlab.form.provisioning.disabled=Your current edition does not support provisioning with GitLab. See the {documentation} for more information.
 settings.authentication.gitlab.configuration.unsaved_changes=You have unsaved changes.
 settings.authentication.gitlab.configuration.valid.JIT=Configuration is valid for Just-in-Time provisioning.