From d9803bb1f6e484c7e1c42384b0848952efcb54d5 Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Wed, 8 Mar 2023 16:59:09 +0100 Subject: SONAR-18654 Disable user creation when user auto provisioning is enable --- .../src/main/js/api/mocks/UsersServiceMock.ts | 75 ++++++++++++++++++++++ .../project/__tests__/ManualProjectCreate-test.tsx | 2 +- .../main/js/apps/issues/__tests__/IssuesApp-it.tsx | 8 +-- .../components/__tests__/Definition-it.tsx | 2 +- server/sonar-web/src/main/js/apps/users/Header.tsx | 72 ++++++++++++--------- .../sonar-web/src/main/js/apps/users/UsersApp.tsx | 18 +++++- .../main/js/apps/users/__tests__/UsersApp-it.tsx | 56 ++++++++++++++++ .../__tests__/__snapshots__/Header-test.tsx.snap | 12 ++-- .../src/main/js/components/common/Link.tsx | 2 +- .../js/components/common/__tests__/Link-test.tsx | 6 +- .../js/components/controls/ValidationInput.tsx | 10 +-- .../__snapshots__/ValidationInput-test.tsx.snap | 4 +- .../src/main/js/components/icons/Icon.tsx | 9 +-- .../src/main/js/components/icons/QualifierIcon.tsx | 34 +++++----- .../src/main/js/components/icons/SeverityIcon.tsx | 4 +- .../issue/components/IssueMessageTags.tsx | 2 +- .../sonar-web/src/main/js/components/ui/Alert.tsx | 18 ++++-- .../ui/__tests__/__snapshots__/Alert-test.tsx.snap | 12 +++- server/sonar-web/src/main/js/types/types.ts | 2 + .../main/resources/org/sonar/l10n/core.properties | 2 + 20 files changed, 261 insertions(+), 89 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts create mode 100644 server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx diff --git a/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts new file mode 100644 index 00000000000..f575559c69a --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { cloneDeep } from 'lodash'; +import { mockClusterSysInfo, mockIdentityProvider } from '../../helpers/testMocks'; +import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types'; +import { User } from '../../types/users'; +import { getSystemInfo } from '../system'; +import { getIdentityProviders, searchUsers } from '../users'; + +export default class UsersServiceMock { + isManaged = true; + + constructor() { + jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo); + jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders); + jest.mocked(searchUsers).mockImplementation(this.handleSearchUsers); + } + + setIsManaged(managed: boolean) { + this.isManaged = managed; + } + + handleSearchUsers = (): Promise<{ paging: Paging; users: User[] }> => { + return this.reply({ + paging: { + pageIndex: 1, + pageSize: 100, + total: 0, + }, + users: [], + }); + }; + + handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => { + return this.reply({ identityProviders: [mockIdentityProvider()] }); + }; + + handleGetSystemInfo = (): Promise => { + return this.reply( + mockClusterSysInfo( + this.isManaged + ? { + System: { + 'High Availability': true, + 'Server ID': 'asd564-asd54a-5dsfg45', + 'External Users and Groups Provisioning': 'Okta', + }, + } + : {} + ) + ); + }; + + reply(response: T): Promise { + return Promise.resolve(cloneDeep(response)); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx index 77ed5afb53c..1ac0b03a7f6 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx @@ -97,7 +97,7 @@ it('should validate form input', async () => { expect( screen.getByRole('textbox', { name: 'onboarding.create_project.display_name field_required' }) ).toHaveValue(''); - expect(screen.getByLabelText('valid_input')).toBeInTheDocument(); + expect(screen.getByText('valid_input')).toBeInTheDocument(); expect( screen.getByText('onboarding.create_project.display_name.error.empty') ).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index 7999699162f..f1043548c44 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -82,13 +82,13 @@ describe('issues app', () => { canBrowseAllChildProjects: false, qualifier: ComponentQualifier.Portfolio, }); - expect(screen.getByRole('alert', { name: 'alert.tooltip.warning' })).toBeInTheDocument(); + expect(screen.getByText('issues.not_all_issue_show')).toBeInTheDocument(); await act(async () => { await user.keyboard('{ArrowRight}'); }); - expect(screen.getByRole('alert', { name: 'alert.tooltip.warning' })).toBeInTheDocument(); + expect(screen.getByText('issues.not_all_issue_show')).toBeInTheDocument(); }); it('should support OWASP Top 10 version 2021', async () => { @@ -637,8 +637,8 @@ describe('issues item', () => { // open severity popup on key press 'i' await user.keyboard('i'); - expect(screen.getByText('severity.MINOR')).toBeInTheDocument(); - expect(screen.getByText('severity.INFO')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'severity.MINOR' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'severity.INFO' })).toBeInTheDocument(); // open status popup on key press 'f' await user.keyboard('f'); diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx index 98d85ea341e..c762837b87c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx @@ -56,7 +56,7 @@ const ui = { fieldsInput: (name: string) => byRole('textbox', { name: `property.${name}.name` }), savedMsg: byText('settings.state.saved'), validationMsg: byText(/settings.state.validation_failed/), - jsonFormatStatus: byRole('status', { name: 'alert.tooltip.info' }), + jsonFormatStatus: byText('settings.json.format_error'), jsonFormatButton: byRole('button', { name: 'settings.json.format' }), toggleButton: byRole('switch'), selectOption: (value: string) => byText(value), diff --git a/server/sonar-web/src/main/js/apps/users/Header.tsx b/server/sonar-web/src/main/js/apps/users/Header.tsx index 4613b130420..a063aed87cd 100644 --- a/server/sonar-web/src/main/js/apps/users/Header.tsx +++ b/server/sonar-web/src/main/js/apps/users/Header.tsx @@ -18,7 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import DocLink from '../../components/common/DocLink'; import { Button } from '../../components/controls/buttons'; +import { Alert } from '../../components/ui/Alert'; import DeferredSpinner from '../../components/ui/DeferredSpinner'; import { translate } from '../../helpers/l10n'; import UserForm from './components/UserForm'; @@ -26,40 +29,49 @@ import UserForm from './components/UserForm'; interface Props { loading: boolean; onUpdateUsers: () => void; + manageProvider?: string; } -interface State { - openUserForm: boolean; -} - -export default class Header extends React.PureComponent { - state: State = { openUserForm: false }; - - handleOpenUserForm = () => { - this.setState({ openUserForm: true }); - }; - - handleCloseUserForm = () => { - this.setState({ openUserForm: false }); - }; +export default function Header(props: Props) { + const [openUserForm, setOpenUserForm] = React.useState(false); - render() { - return ( -
-

{translate('users.page')}

- + const { manageProvider, loading } = props; + return ( +
+

{translate('users.page')}

+ -
- -
+
+ +
+ {manageProvider === undefined ? (

{translate('users.page.description')}

- {this.state.openUserForm && ( - - )} -
- ); - } + ) : ( + + + {translate('documentation')} + + ), + }} + /> + + )} + {openUserForm && ( + setOpenUserForm(false)} onUpdateUsers={props.onUpdateUsers} /> + )} + + ); } diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx index b0d7c36df4f..768b2c8f7e6 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -19,13 +19,14 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; +import { getSystemInfo } from '../../api/system'; import { getIdentityProviders, searchUsers } from '../../api/users'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; import ListFooter from '../../components/controls/ListFooter'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { translate } from '../../helpers/l10n'; -import { IdentityProvider, Paging } from '../../types/types'; +import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types'; import { CurrentUser, User } from '../../types/users'; import Header from './Header'; import Search from './Search'; @@ -40,6 +41,7 @@ interface Props { interface State { identityProviders: IdentityProvider[]; + manageProvider?: string; loading: boolean; paging?: Paging; users: User[]; @@ -52,6 +54,7 @@ export class UsersApp extends React.PureComponent { componentDidMount() { this.mounted = true; this.fetchIdentityProviders(); + this.fetchManageInstance(); this.fetchUsers(); } @@ -71,6 +74,15 @@ export class UsersApp extends React.PureComponent { } }; + async fetchManageInstance() { + const info = (await getSystemInfo()) as SysInfoCluster; + if (this.mounted) { + this.setState({ + manageProvider: info.System['External Users and Groups Provisioning'], + }); + } + } + fetchIdentityProviders = () => getIdentityProviders().then(({ identityProviders }) => { if (this.mounted) { @@ -116,12 +128,12 @@ export class UsersApp extends React.PureComponent { render() { const query = parseQuery(this.props.location.query); - const { loading, paging, users } = this.state; + const { loading, paging, users, manageProvider } = this.state; return (
-
+
{ + handler.setIsManaged(false); + renderUsersApp(); + + expect(await ui.description.find()).toBeInTheDocument(); + expect(ui.createUserButton.get()).toBeEnabled(); +}); + +it('should render list of user in manage mode', async () => { + handler.setIsManaged(true); + renderUsersApp(); + + expect(await ui.infoManageMode.find()).toBeInTheDocument(); + expect(ui.createUserButton.get()).toBeDisabled(); +}); + +function renderUsersApp() { + renderApp('admin/users', ); +} diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap index e09d8ad8088..1e3f25b280f 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap @@ -1,15 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -
-

users.page -

+ @@ -17,6 +16,7 @@ exports[`should render correctly 1`] = ` className="page-actions" >
+ `; diff --git a/server/sonar-web/src/main/js/components/common/Link.tsx b/server/sonar-web/src/main/js/components/common/Link.tsx index 3ef1206d0d9..e208546b3fb 100644 --- a/server/sonar-web/src/main/js/components/common/Link.tsx +++ b/server/sonar-web/src/main/js/components/common/Link.tsx @@ -45,7 +45,7 @@ function Link({ children, size, ...props }: LinkProps, ref: React.ForwardedRef {anchorProps.target === '_blank' && ( diff --git a/server/sonar-web/src/main/js/components/common/__tests__/Link-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/Link-test.tsx index e37c4692140..b4078004879 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/Link-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/Link-test.tsx @@ -35,8 +35,10 @@ it('should correctly render a link that opens in a new window, but is not consid it('should correctly render an external link', () => { renderLink({ target: '_blank', to: 'http://example.com' }); - expect(screen.getByRole('link')).toHaveAttribute('rel', 'noopener noreferrer'); - expect(screen.getByLabelText('opens_in_new_window')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'opens_in_new_window click me' })).toHaveAttribute( + 'rel', + 'noopener noreferrer' + ); }); function renderLink(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx b/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx index a853ef3fbef..7ca095822de 100644 --- a/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx @@ -69,10 +69,7 @@ export default function ValidationInput(props: ValidationInputProps) { <> {children} {showValidIcon && isValid && ( - + )} {isInvalid && } {hasError && {error}} @@ -83,10 +80,7 @@ export default function ValidationInput(props: ValidationInputProps) { <> {children} {showValidIcon && isValid && ( - + )}
{isInvalid && } diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap index 2233f79b19f..32dd5f73936 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap @@ -19,8 +19,8 @@ exports[`should render correctly: default 1`] = ` >
+ {label && !hidden && {label}} {children} ); diff --git a/server/sonar-web/src/main/js/components/icons/QualifierIcon.tsx b/server/sonar-web/src/main/js/components/icons/QualifierIcon.tsx index aadcec8dfa6..8c19cef80e5 100644 --- a/server/sonar-web/src/main/js/components/icons/QualifierIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons/QualifierIcon.tsx @@ -53,13 +53,13 @@ export default function QualifierIcon({ const FoundIcon = qualifierIcons[qualifier.toLowerCase()]; const ariaLabel = qualifier ? translate(`qualifier.${qualifier}`) : undefined; return FoundIcon ? ( - + ) : null; } -function ApplicationIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function ApplicationIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - + + + + + + + + React.ReactElement> = { info: InfoSeverityIcon, }; -export default function SeverityIcon({ severity, ariaLabel, ...iconProps }: Props) { +export default function SeverityIcon({ severity, ...iconProps }: Omit) { if (!severity) { return null; } const DesiredIcon = severityIcons[severity.toLowerCase()]; return DesiredIcon ? ( - + ) : null; } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx index 5f873b242b0..a4ab9133b34 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx @@ -64,7 +64,7 @@ export default function IssueMessageTags(props: IssueMessageTagsProps) { )} diff --git a/server/sonar-web/src/main/js/components/ui/Alert.tsx b/server/sonar-web/src/main/js/components/ui/Alert.tsx index b73db54ab2c..98ac6328b6c 100644 --- a/server/sonar-web/src/main/js/components/ui/Alert.tsx +++ b/server/sonar-web/src/main/js/components/ui/Alert.tsx @@ -109,28 +109,37 @@ const StyledAlert = styled.div<{ isInline: boolean; variantInfo: AlertVariantInf function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { const variantList: Dict = { error: { - icon: , + icon: ( + + ), color: colors.alertTextError, borderColor: colors.alertBorderError, backGroundColor: colors.alertBackgroundError, role: 'alert', }, warning: { - icon: , + icon: ( + + ), color: colors.alertTextWarning, borderColor: colors.alertBorderWarning, backGroundColor: colors.alertBackgroundWarning, role: 'alert', }, success: { - icon: , + icon: ( + + ), color: colors.alertTextSuccess, borderColor: colors.alertBorderSuccess, backGroundColor: colors.alertBackgroundSuccess, role: 'status', }, info: { - icon: , + icon: , color: colors.alertTextInfo, borderColor: colors.alertBorderInfo, backGroundColor: colors.alertBackgroundInfo, @@ -159,7 +168,6 @@ export function Alert(props: AlertProps & React.HTMLAttributes) className={classNames('alert', className)} isInline={isInline} role={variantInfo.role} - aria-label={translate('alert.tooltip', variant)} variantInfo={variantInfo} {...domProps} > diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap index 9fb48a19931..0d404bb9aa1 100644 --- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap @@ -74,7 +74,6 @@ exports[`should render banner alert with correct css 1`] = ` }