aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2023-03-08 16:59:09 +0100
committersonartech <sonartech@sonarsource.com>2023-03-22 20:04:07 +0000
commitd9803bb1f6e484c7e1c42384b0848952efcb54d5 (patch)
treeea9bdf92ec8e3b195128eb0b7b16c7e5f736c461
parent9d02b0639e617458d771c4a794db33e70cbb35a1 (diff)
downloadsonarqube-d9803bb1f6e484c7e1c42384b0848952efcb54d5.tar.gz
sonarqube-d9803bb1f6e484c7e1c42384b0848952efcb54d5.zip
SONAR-18654 Disable user creation when user auto provisioning is enable
-rw-r--r--server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts75
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/users/Header.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersApp.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx56
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/components/common/Link.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/Link-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/controls/ValidationInput.tsx10
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/components/icons/Icon.tsx9
-rw-r--r--server/sonar-web/src/main/js/components/icons/QualifierIcon.tsx34
-rw-r--r--server/sonar-web/src/main/js/components/icons/SeverityIcon.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/ui/Alert.tsx18
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/types/types.ts2
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties2
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})"?