diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2023-03-08 16:59:09 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-03-22 20:04:07 +0000 |
commit | d9803bb1f6e484c7e1c42384b0848952efcb54d5 (patch) | |
tree | ea9bdf92ec8e3b195128eb0b7b16c7e5f736c461 | |
parent | 9d02b0639e617458d771c4a794db33e70cbb35a1 (diff) | |
download | sonarqube-d9803bb1f6e484c7e1c42384b0848952efcb54d5.tar.gz sonarqube-d9803bb1f6e484c7e1c42384b0848952efcb54d5.zip |
SONAR-18654 Disable user creation when user auto provisioning is enable
20 files changed, 261 insertions, 89 deletions
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<SysInfoCluster> => { + return this.reply( + mockClusterSysInfo( + this.isManaged + ? { + System: { + 'High Availability': true, + 'Server ID': 'asd564-asd54a-5dsfg45', + 'External Users and Groups Provisioning': 'Okta', + }, + } + : {} + ) + ); + }; + + reply<T>(response: T): Promise<T> { + 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<Props, State> { - 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 ( - <header className="page-header" id="users-header"> - <h1 className="page-title">{translate('users.page')}</h1> - <DeferredSpinner loading={this.props.loading} /> + const { manageProvider, loading } = props; + return ( + <div className="page-header null-spacer-bottom"> + <h2 className="page-title">{translate('users.page')}</h2> + <DeferredSpinner loading={loading} /> - <div className="page-actions"> - <Button id="users-create" onClick={this.handleOpenUserForm}> - {translate('users.create_user')} - </Button> - </div> + <div className="page-actions"> + <Button + id="users-create" + disabled={manageProvider !== undefined} + onClick={() => setOpenUserForm(true)} + > + {translate('users.create_user')} + </Button> + </div> + {manageProvider === undefined ? ( <p className="page-description">{translate('users.page.description')}</p> - {this.state.openUserForm && ( - <UserForm onClose={this.handleCloseUserForm} onUpdateUsers={this.props.onUpdateUsers} /> - )} - </header> - ); - } + ) : ( + <Alert className="page-description max-width-100" variant="info"> + <FormattedMessage + defaultMessage={translate('users.page.managed_description')} + id="users.page.managed_description" + values={{ + provider: manageProvider, + link: ( + <DocLink to="/instance-administration/authentication/overview/"> + {translate('documentation')} + </DocLink> + ), + }} + /> + </Alert> + )} + {openUserForm && ( + <UserForm onClose={() => setOpenUserForm(false)} onUpdateUsers={props.onUpdateUsers} /> + )} + </div> + ); } 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<Props, State> { componentDidMount() { this.mounted = true; this.fetchIdentityProviders(); + this.fetchManageInstance(); this.fetchUsers(); } @@ -71,6 +74,15 @@ export class UsersApp extends React.PureComponent<Props, State> { } }; + 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<Props, State> { render() { const query = parseQuery(this.props.location.query); - const { loading, paging, users } = this.state; + const { loading, paging, users, manageProvider } = this.state; return ( <main className="page page-limited" id="users-page"> <Suggestions suggestions="users" /> <Helmet defer={false} title={translate('users.page')} /> - <Header loading={loading} onUpdateUsers={this.fetchUsers} /> + <Header loading={loading} onUpdateUsers={this.fetchUsers} manageProvider={manageProvider} /> <Search query={query} updateQuery={this.updateQuery} /> <UsersList currentUser={this.props.currentUser} diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx new file mode 100644 index 00000000000..3917a11296e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx @@ -0,0 +1,56 @@ +/* + * 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 * as React from 'react'; +import { byRole, byText } from 'testing-library-selector'; +import UsersServiceMock from '../../../api/mocks/UsersServiceMock'; +import { renderApp } from '../../../helpers/testReactTestingUtils'; +import UsersApp from '../UsersApp'; + +jest.mock('../../../api/users'); +jest.mock('../../../api/system'); + +const handler = new UsersServiceMock(); + +const ui = { + createUserButton: byRole('button', { name: 'users.create_user' }), + infoManageMode: byText(/users\.page\.managed_description/), + description: byText('users.page.description'), +}; + +it('should render list of user in non manage mode', async () => { + 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', <UsersApp />); +} 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`] = ` -<header - className="page-header" - id="users-header" +<div + className="page-header null-spacer-bottom" > - <h1 + <h2 className="page-title" > users.page - </h1> + </h2> <DeferredSpinner loading={true} /> @@ -17,6 +16,7 @@ exports[`should render correctly 1`] = ` className="page-actions" > <Button + disabled={false} id="users-create" onClick={[Function]} > @@ -28,5 +28,5 @@ exports[`should render correctly 1`] = ` > users.page.description </p> -</header> +</div> `; 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<H > {anchorProps.target === '_blank' && ( <DetachIcon - ariaLabel={translate('opens_in_new_window')} + label={translate('opens_in_new_window')} size={size || DEFAULT_ICON_SIZE} className="little-spacer-right" /> 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<LinkProps> = {}) { 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 && ( - <AlertSuccessIcon - ariaLabel={translate('valid_input')} - className="spacer-left text-middle" - /> + <AlertSuccessIcon label={translate('valid_input')} className="spacer-left text-middle" /> )} {isInvalid && <AlertErrorIcon className="spacer-left text-middle" />} {hasError && <span className="little-spacer-left text-danger text-middle">{error}</span>} @@ -83,10 +80,7 @@ export default function ValidationInput(props: ValidationInputProps) { <> {children} {showValidIcon && isValid && ( - <AlertSuccessIcon - ariaLabel={translate('valid_input')} - className="spacer-left text-middle" - /> + <AlertSuccessIcon label={translate('valid_input')} className="spacer-left text-middle" /> )} <div className="spacer-top" style={{ display: 'flex' }}> {isInvalid && <AlertErrorIcon className="text-middle" />} 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`] = ` > <div /> <AlertSuccessIcon - ariaLabel="valid_input" className="spacer-left text-middle" + label="valid_input" /> </div> <div @@ -82,8 +82,8 @@ exports[`should render correctly: no label 1`] = ` > <div /> <AlertSuccessIcon - ariaLabel="valid_input" className="spacer-left text-middle" + label="valid_input" /> </div> <div diff --git a/server/sonar-web/src/main/js/components/icons/Icon.tsx b/server/sonar-web/src/main/js/components/icons/Icon.tsx index 92fc47ab7fb..e55755c6fb4 100644 --- a/server/sonar-web/src/main/js/components/icons/Icon.tsx +++ b/server/sonar-web/src/main/js/components/icons/Icon.tsx @@ -23,7 +23,7 @@ export interface IconProps extends React.AriaAttributes { className?: string; fill?: string; size?: number; - ariaLabel?: string; + label?: string; } interface Props extends React.AriaAttributes { @@ -31,7 +31,7 @@ interface Props extends React.AriaAttributes { className?: string; size?: number; style?: React.CSSProperties; - ariaLabel?: string; + label?: string; // try to avoid using these: width?: number; @@ -47,13 +47,13 @@ export default function Icon({ height = size, width = size, viewBox = '0 0 16 16', - ariaLabel, + label, + 'aria-hidden': hidden, ...iconProps }: Props) { return ( <svg className={className} - aria-label={ariaLabel} height={height} style={{ fillRule: 'evenodd', @@ -69,6 +69,7 @@ export default function Icon({ xmlSpace="preserve" {...iconProps} > + {label && !hidden && <title>{label}</title>} {children} </svg> ); 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 ? ( - <FoundIcon className={className} fill={fill} ariaLabel={ariaLabel} {...props} /> + <FoundIcon className={className} fill={fill} label={ariaLabel} {...props} /> ) : null; } -function ApplicationIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function ApplicationIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - <Icon {...iconProps} ariaLabel={ariaLabel}> + <Icon {...iconProps} label={ariaLabel}> <path d="M3.014 10.986a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zm9.984 0a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zm-5.004-.021c1.103 0 2 .896 2 2s-.897 2-2 2a2 2 0 0 1 0-4zm-4.98 1.021a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm-5.004-.021a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM2.984 6a2 2 0 1 1-.001 4.001A2 2 0 0 1 2.984 6zm9.984 0a2 2 0 1 1-.001 4.001A2 2 0 0 1 12.968 6zm-5.004-.021c1.103 0 2 .897 2 2a2 2 0 1 1-2-2zM2.984 7a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm-5.004-.021a1.001 1.001 0 0 1 0 2 1 1 0 0 1 0-2zM3 1.025a2 2 0 1 1-.001 4.001A2 2 0 0 1 3 1.025zm9.984 0a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zM7.98 1.004c1.103 0 2 .896 2 2s-.897 2-2 2a2 2 0 0 1 0-4zM3 2.025a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM7.98 2.004a1.001 1.001 0 0 1 0 2 1 1 0 0 1 0-2z" style={{ fill: fill || colors.primary }} @@ -68,9 +68,9 @@ function ApplicationIcon({ fill, ariaLabel, ...iconProps }: IconProps) { ); } -function DeveloperIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function DeveloperIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - <Icon {...iconProps} ariaLabel={ariaLabel}> + <Icon {...iconProps} label={ariaLabel}> <path d="M7.974 8.02a3.5 3.5 0 0 1-2.482-1.017 3.428 3.428 0 0 1-1.028-2.455c0-.927.365-1.8 1.028-2.455a3.505 3.505 0 0 1 2.482-1.017 3.5 3.5 0 0 1 2.482 1.017 3.434 3.434 0 0 1 1.027 2.455c0 .928-.365 1.8-1.027 2.455A3.504 3.504 0 0 1 7.974 8.02zm0-5.778c-1.286 0-2.332 1.034-2.332 2.306s1.046 2.307 2.332 2.307c1.285 0 2.332-1.035 2.332-2.307S9.258 2.242 7.974 2.242zm3.534 6.418c.127.016.243.045.348.086.17.066.302.146.406.246.132.124.253.282.36.47.126.218.226.442.3.668.08.253.15.535.206.838.056.313.095.604.113.867.02.28.03.57.03.862 0 .532-.174.758-.306.882-.142.132-.397.31-.973.31H3.948c-.233 0-.437-.03-.606-.09-.14-.05-.26-.123-.366-.222-.13-.123-.306-.35-.306-.88 0-.294.01-.584.03-.863.018-.263.056-.554.112-.867a6.5 6.5 0 0 1 .207-.838c.073-.226.173-.45.298-.667.108-.19.23-.347.36-.47.106-.1.238-.18.407-.247.105-.04.22-.07.348-.086.202.13.432.277.683.435.342.217.756.4 1.265.564.523.166 1.06.25 1.59.25a5.25 5.25 0 0 0 1.592-.25c.51-.164.923-.348 1.266-.565.25-.158.48-.304.682-.435l-.002.002zm-.244-1.18c-.055 0-.184.066-.387.196-.202.13-.43.276-.685.437-.255.16-.586.307-.994.437-.408.13-.818.196-1.23.196-.41 0-.82-.065-1.228-.196a4.303 4.303 0 0 1-.993-.437c-.255-.16-.484-.306-.686-.437-.202-.13-.33-.196-.386-.196-.374 0-.716.06-1.026.183-.31.12-.572.283-.787.487a3.28 3.28 0 0 0-.57.737 4.662 4.662 0 0 0-.395.888c-.098.303-.18.633-.244.988a9.652 9.652 0 0 0-.128.992c-.02.306-.032.62-.032.942 0 .73.224 1.304.672 1.726.448.42 1.043.632 1.785.632h8.044c.743 0 1.34-.21 1.787-.633.447-.42.67-.996.67-1.725 0-.32-.01-.635-.03-.942a9.159 9.159 0 0 0-.374-1.98c-.098-.304-.23-.6-.395-.888a3.23 3.23 0 0 0-.57-.737 2.404 2.404 0 0 0-.788-.487 2.779 2.779 0 0 0-1.026-.183h-.004z" style={{ fill: fill || colors.primary }} @@ -79,9 +79,9 @@ function DeveloperIcon({ fill, ariaLabel, ...iconProps }: IconProps) { ); } -function DirectoryIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function DirectoryIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - <Icon {...iconProps} ariaLabel={ariaLabel}> + <Icon {...iconProps} label={ariaLabel}> <path d="M14 12.286V5.703a.673.673 0 0 0-.195-.5.644.644 0 0 0-.49-.203H6.704a.686.686 0 0 1-.5-.214.707.707 0 0 1-.203-.51v-.57c0-.2-.07-.363-.207-.502A.679.679 0 0 0 5.29 3H2.707a.672.672 0 0 0-.5.204.683.683 0 0 0-.206.5v8.582c0 .2.07.367.206.506.137.14.304.208.5.208h10.61a.66.66 0 0 0 .49-.208.685.685 0 0 0 .194-.506H14zm1-6.598v6.65c0 .458-.152.83-.475 1.16-.324.326-.7.502-1.15.502H2.647c-.452 0-.84-.175-1.162-.503a1.572 1.572 0 0 1-.486-1.158V3.654a1.6 1.6 0 0 1 .486-1.17A1.578 1.578 0 0 1 2.648 2h2.7c.45 0 .84.157 1.164.485.324.328.488.714.488 1.17V4h6.373c.452 0 .83.174 1.152.5.323.33.475.73.475 1.187v.001z" style={{ fill: fill || colors.orange }} @@ -90,9 +90,9 @@ function DirectoryIcon({ fill, ariaLabel, ...iconProps }: IconProps) { ); } -function FileIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function FileIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - <Icon {...iconProps} ariaLabel={ariaLabel}> + <Icon {...iconProps} label={ariaLabel}> <path d="M14 15H2V1l7.997.02c1 .034 1.759.758 2.428 1.42.667.663 1.561 1.605 1.574 2.555H14V15zM9 2H3v12h10V6H9V2zm3 10H4v-1h8v1zm0-2H4V9h8v1zm-1.988-5h3.008c-.012-.674-.714-1.443-1.204-1.937-.488-.495-1.039-1.058-1.816-1.055v2.96l.012.032z" style={{ fill: fill || colors.primary }} @@ -101,9 +101,9 @@ function FileIcon({ fill, ariaLabel, ...iconProps }: IconProps) { ); } -function PortfolioIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function PortfolioIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - <Icon {...iconProps} ariaLabel={ariaLabel}> + <Icon {...iconProps} label={ariaLabel}> <path d="M14.97 14.97H1.016V1.015H14.97V14.97zm-1-12.955H2.015V13.97H13.97V2.015zm-.973 10.982H9V9h3.997v3.997zM7 12.996H3.004V9H7v3.996zM11.997 10H10v1.997h1.997V10zM6 10H4.004v1.996H6V10zm1-3H3.006V3.006H7V7zm5.985 0H9V3.015h3.985V7zM6 4.006H4.006V6H6V4.006zm5.985.009H10V6h1.985V4.015z" style={{ fill: fill || colors.primary }} @@ -112,9 +112,9 @@ function PortfolioIcon({ fill, ariaLabel, ...iconProps }: IconProps) { ); } -function ProjectIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function ProjectIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - <Icon {...iconProps} ariaLabel={ariaLabel}> + <Icon {...iconProps} label={ariaLabel}> <path d="M14.985 13.988L1 14.005 1.02 5h13.966v8.988h-.001zM1.998 5.995l.006 7.02L14.022 13 14 6.004l-12.002-.01v.001zM3 4.5V4h9.996l.004.5h1l-.005-1.497-11.98.003L2 4.5h1zm1-2v-.504h8.002L12 2.5h1l-.004-1.495H3.003L3 2.5h1z" style={{ fill: fill || colors.primary }} @@ -123,9 +123,9 @@ function ProjectIcon({ fill, ariaLabel, ...iconProps }: IconProps) { ); } -function SubPortfolioIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function SubPortfolioIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - <Icon {...iconProps} ariaLabel={ariaLabel}> + <Icon {...iconProps} label={ariaLabel}> <path d="M14 7h2v9H7v-2H0V0h14v7zM8 8v7h7V8H8zm3 6H9v-2h2v2zm3 0h-2v-2h2v2zm-1-7V1H1v12h6V7h6zm-7 5H2V8h4v4zm5-1H9V9h2v2zm3 0h-2V9h2v2zM5 9H3v2h2V9zm1-3H2V2h4v4zm6 0H8V2h4v4zM5 3H3v2h2V3zm6 0H9v2h2V3z" style={{ fill: fill || colors.primary }} @@ -134,9 +134,9 @@ function SubPortfolioIcon({ fill, ariaLabel, ...iconProps }: IconProps) { ); } -function UnitTestIcon({ fill, ariaLabel, ...iconProps }: IconProps) { +function UnitTestIcon({ fill, label: ariaLabel, ...iconProps }: IconProps) { return ( - <Icon {...iconProps} ariaLabel={ariaLabel}> + <Icon {...iconProps} label={ariaLabel}> <path d="M14 15H2V1l7.997.02c1.013-.03 1.57.893 2.239 1.555.667.663 1.75 1.47 1.763 2.42H14V15zM9 2H3v12h10V6H9V2zM7 8l-3 2.5L7 13V8zm1 5l3-2.5L8 8v5zm2.012-8h3.008c-.012-.674-.78-1.258-1.27-1.752-.488-.495-.973-1.243-1.75-1.24v2.96l.012.032z" style={{ fill: fill || colors.primary }} diff --git a/server/sonar-web/src/main/js/components/icons/SeverityIcon.tsx b/server/sonar-web/src/main/js/components/icons/SeverityIcon.tsx index 6bff6be69ab..ef6cb2ce9f2 100644 --- a/server/sonar-web/src/main/js/components/icons/SeverityIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons/SeverityIcon.tsx @@ -35,14 +35,14 @@ const severityIcons: Dict<(props: IconProps) => React.ReactElement> = { info: InfoSeverityIcon, }; -export default function SeverityIcon({ severity, ariaLabel, ...iconProps }: Props) { +export default function SeverityIcon({ severity, ...iconProps }: Omit<Props, 'label'>) { if (!severity) { return null; } const DesiredIcon = severityIcons[severity.toLowerCase()]; return DesiredIcon ? ( - <DesiredIcon {...iconProps} ariaLabel={ariaLabel ?? translate('severity', severity)} /> + <DesiredIcon {...iconProps} label={translate('severity', severity)} /> ) : 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) { <SonarLintIcon className="it__issues-sonarlint-quick-fix spacer-right" size={15} - ariaLabel="sonar-lint-icon" + label="sonar-lint-icon" /> </Tooltip> )} 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<AlertVariantInformation> = { error: { - icon: <AlertErrorIcon fill={colors.alertIconError} />, + icon: ( + <AlertErrorIcon label={translate('alert.tooltip.error')} fill={colors.alertIconError} /> + ), color: colors.alertTextError, borderColor: colors.alertBorderError, backGroundColor: colors.alertBackgroundError, role: 'alert', }, warning: { - icon: <AlertWarnIcon fill={colors.alertIconWarning} />, + icon: ( + <AlertWarnIcon label={translate('alert.tooltip.warning')} fill={colors.alertIconWarning} /> + ), color: colors.alertTextWarning, borderColor: colors.alertBorderWarning, backGroundColor: colors.alertBackgroundWarning, role: 'alert', }, success: { - icon: <AlertSuccessIcon fill={colors.alertIconSuccess} />, + icon: ( + <AlertSuccessIcon + label={translate('alert.tooltip.success')} + fill={colors.alertIconSuccess} + /> + ), color: colors.alertTextSuccess, borderColor: colors.alertBorderSuccess, backGroundColor: colors.alertBackgroundSuccess, role: 'status', }, info: { - icon: <InfoIcon fill={colors.alertIconInfo} />, + icon: <InfoIcon label={translate('alert.tooltip.info')} fill={colors.alertIconInfo} />, color: colors.alertTextInfo, borderColor: colors.alertBorderInfo, backGroundColor: colors.alertBackgroundInfo, @@ -159,7 +168,6 @@ export function Alert(props: AlertProps & React.HTMLAttributes<HTMLDivElement>) 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`] = ` } <div - aria-label="alert.tooltip.error" class="alert alert-test emotion-0" id="error-message" role="alert" @@ -94,6 +93,9 @@ exports[`should render banner alert with correct css 1`] = ` width="16" xlink="http://www.w3.org/1999/xlink" > + <title> + alert.tooltip.error + </title> <path d="M11.402 10.018q0-0.232-0.17-0.402l-1.616-1.616 1.616-1.616q0.17-0.17 0.17-0.402 0-0.241-0.17-0.411l-0.804-0.804q-0.17-0.17-0.411-0.17-0.232 0-0.402 0.17l-1.616 1.616-1.616-1.616q-0.17-0.17-0.402-0.17-0.241 0-0.411 0.17l-0.804 0.804q-0.17 0.17-0.17 0.411 0 0.232 0.17 0.402l1.616 1.616-1.616 1.616q-0.17 0.17-0.17 0.402 0 0.241 0.17 0.411l0.804 0.804q0.17 0.17 0.411 0.17 0.232 0 0.402-0.17l1.616-1.616 1.616 1.616q0.17 0.17 0.402 0.17 0.241 0 0.411-0.17l0.804-0.804q0.17-0.17 0.17-0.411zM14.857 8q0 1.866-0.92 3.442t-2.496 2.496-3.442 0.92-3.442-0.92-2.496-2.496-0.92-3.442 0.92-3.442 2.496-2.496 3.442-0.92 3.442 0.92 2.496 2.496 0.92 3.442z" style="fill:#a4030f" @@ -111,7 +113,6 @@ exports[`should render banner alert with correct css 1`] = ` exports[`should render properly 1`] = ` <Styled(div) - aria-label="alert.tooltip.error" className="alert alert-test" id="error-message" isInline={false} @@ -123,6 +124,7 @@ exports[`should render properly 1`] = ` "color": "#862422", "icon": <AlertErrorIcon fill="#a4030f" + label="alert.tooltip.error" />, "role": "alert", } @@ -140,6 +142,7 @@ exports[`should render properly 1`] = ` "color": "#862422", "icon": <AlertErrorIcon fill="#a4030f" + label="alert.tooltip.error" />, "role": "alert", } @@ -147,6 +150,7 @@ exports[`should render properly 1`] = ` > <AlertErrorIcon fill="#a4030f" + label="alert.tooltip.error" /> </Styled(div)> <Styled(div) @@ -165,6 +169,7 @@ exports[`verification of all variants of alert 1`] = ` "color": "#862422", "icon": <AlertErrorIcon fill="#a4030f" + label="alert.tooltip.error" />, "role": "alert", } @@ -177,6 +182,7 @@ exports[`verification of all variants of alert 2`] = ` "color": "#6f4f17", "icon": <AlertWarnIcon fill="#db781a" + label="alert.tooltip.warning" />, "role": "alert", } @@ -189,6 +195,7 @@ exports[`verification of all variants of alert 3`] = ` "color": "#215821", "icon": <AlertSuccessIcon fill="#6d9867" + label="alert.tooltip.success" />, "role": "status", } @@ -201,6 +208,7 @@ exports[`verification of all variants of alert 4`] = ` "color": "#0e516f", "icon": <InfoIcon fill="#0271b9" + label="alert.tooltip.info" />, "role": "status", } diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 7c98fcd7bbd..9fbefdbeb01 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -229,6 +229,7 @@ export interface IdentityProvider { iconPath: string; key: string; name: string; + manage?: boolean; } export interface Issue { @@ -696,6 +697,7 @@ export interface SysInfoCluster extends SysInfoBase { 'High Availability': true; 'Server ID': string; Version: string; + 'External Users and Groups Provisioning'?: string; }; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index a92f62d0183..f0c58f35917 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4325,6 +4325,8 @@ encryption.how_to_use.content4=For each property that you want to encrypt, gener #------------------------------------------------------------------------------ users.page=Users users.page.description=Create and administer individual users. +users.page.managed_description=Your instance is managed by {provider}. No modification is allowed except for tokens. You can still delete local users. All other operations should be done on your identity provider. See {link} for help managing users. +users.info=User users.deactivate=Deactivate users.deactivate_user=Deactivate User users.deactivate_user.confirmation=Are you sure you want to deactivate "{0} ({1})"? |