aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorViktor Vorona <viktor.vorona@sonarsource.com>2024-01-26 11:11:32 +0100
committersonartech <sonartech@sonarsource.com>2024-01-30 15:02:03 +0000
commit4edb157cedb8327b0417bd7a9e472ff145f79445 (patch)
tree77545dfdec7b8b43886489ee6056bcf961ad4331 /server
parentd95da9d9c71190ee47bb3028bea7d1208f36c87d (diff)
downloadsonarqube-4edb157cedb8327b0417bd7a9e472ff145f79445.tar.gz
sonarqube-4edb157cedb8327b0417bd7a9e472ff145f79445.zip
SONAR-21480 migrate authentication tabs to MIUI
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/design-system/src/components/FlagMessage.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx108
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx181
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationDetails.tsx84
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx397
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx496
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/ProvisioningSection.tsx168
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx302
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/TabHeader.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/settings/styles.css42
16 files changed, 948 insertions, 966 deletions
diff --git a/server/sonar-web/design-system/src/components/FlagMessage.tsx b/server/sonar-web/design-system/src/components/FlagMessage.tsx
index 767af9b1631..47fb9b9fa58 100644
--- a/server/sonar-web/design-system/src/components/FlagMessage.tsx
+++ b/server/sonar-web/design-system/src/components/FlagMessage.tsx
@@ -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`}
diff --git a/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx
index 238236156ce..9f6259e4a32 100644
--- a/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx
+++ b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx
@@ -19,10 +19,9 @@
*/
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} />
</>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.tsx b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.tsx
index 71c8c4e3bd4..92d95141b5b 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.tsx
@@ -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}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
index 14abcc7a811..c5e2614aecf 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
@@ -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"
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
index ace7a1cff75..7e72e9c3721 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
@@ -18,17 +18,15 @@
* 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
index 00000000000..7d8308754e0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationDetails.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx
index 30c5db1c14d..a56cca16fce 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx
@@ -17,21 +17,15 @@
* 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)} />
)}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
index d66da0f2a4c..287ef88a7e6 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
@@ -17,18 +17,12 @@
* 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
index 00000000000..014ebebcd4f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/ProvisioningSection.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
index 66a7bef88c8..b253dc80787 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
@@ -17,25 +17,22 @@
* 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
index 00000000000..5b9daefc18b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/TabHeader.tsx
@@ -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" />
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx
index 121c39908a7..759a8d3c864 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx
@@ -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();
});
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx
index 432e781aa0e..d9cfe1f9e8f 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx
@@ -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();
});
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx
index 48a34862af8..d2b8376f266 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx
@@ -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());
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
index abcec5b1ded..bf2345a6c0e 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
@@ -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',
);
});
diff --git a/server/sonar-web/src/main/js/apps/settings/styles.css b/server/sonar-web/src/main/js/apps/settings/styles.css
index 0ad2ea18013..2eaca9065a9 100644
--- a/server/sonar-web/src/main/js/apps/settings/styles.css
+++ b/server/sonar-web/src/main/js/apps/settings/styles.css
@@ -239,45 +239,3 @@
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%;
-}