diff options
author | Viktor Vorona <viktor.vorona@sonarsource.com> | 2024-01-26 11:11:32 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-01-30 15:02:03 +0000 |
commit | 4edb157cedb8327b0417bd7a9e472ff145f79445 (patch) | |
tree | 77545dfdec7b8b43886489ee6056bcf961ad4331 /server | |
parent | d95da9d9c71190ee47bb3028bea7d1208f36c87d (diff) | |
download | sonarqube-4edb157cedb8327b0417bd7a9e472ff145f79445.tar.gz sonarqube-4edb157cedb8327b0417bd7a9e472ff145f79445.zip |
SONAR-21480 migrate authentication tabs to MIUI
Diffstat (limited to 'server')
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%; -} |