]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18654 Disable user creation when user auto provisioning is enable
authorMathieu Suen <mathieu.suen@sonarsource.com>
Wed, 8 Mar 2023 15:59:09 +0000 (16:59 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 22 Mar 2023 20:04:07 +0000 (20:04 +0000)
20 files changed:
server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx
server/sonar-web/src/main/js/apps/users/Header.tsx
server/sonar-web/src/main/js/apps/users/UsersApp.tsx
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap
server/sonar-web/src/main/js/components/common/Link.tsx
server/sonar-web/src/main/js/components/common/__tests__/Link-test.tsx
server/sonar-web/src/main/js/components/controls/ValidationInput.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
server/sonar-web/src/main/js/components/icons/Icon.tsx
server/sonar-web/src/main/js/components/icons/QualifierIcon.tsx
server/sonar-web/src/main/js/components/icons/SeverityIcon.tsx
server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx
server/sonar-web/src/main/js/components/ui/Alert.tsx
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

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 (file)
index 0000000..f575559
--- /dev/null
@@ -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));
+  }
+}
index 77ed5afb53cd08af0f261c7b3e25d109ae97cb71..1ac0b03a7f6d604c0019d57c0191c07fea12a240 100644 (file)
@@ -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();
index 7999699162fd61057331e365a3eba613bc466034..f1043548c4400559d34528836655029e12f99954 100644 (file)
@@ -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');
index 98d85ea341e48424a15fa9e6bb65c38f59cd159b..c762837b87cdff5515c14f07f87f9600ec0354e9 100644 (file)
@@ -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),
index 4613b1304205f14d55681c21312fffdca0b7b205..a063aed87cd40d5b3f8de510287fd33138aebcaa 100644 (file)
  * 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>
+  );
 }
index b0d7c36df4fa5d26112a1fa559dedcb8007a0d3b..768b2c8f7e6386547caf2722496f753c72c1c928 100644 (file)
  */
 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 (file)
index 0000000..3917a11
--- /dev/null
@@ -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 />);
+}
index e09d8ad808849a957a7edd83bde1cae205dade6e..1e3f25b280fad2782ac0ac3ca79011eb1655df9d 100644 (file)
@@ -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>
 `;
index 3ef1206d0d960f9dd5d16cdc3bac45c199c00318..e208546b3fbfae3e3ee26e6051137d6ee24cafa3 100644 (file)
@@ -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"
           />
index e37c4692140ac41efe1ddf765f77cf9d0e1c5c78..b4078004879f66e8d0ceef7b786b8cf3bbd2bfa5 100644 (file)
@@ -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> = {}) {
index a853ef3fbeffb9d58b97b7d1fc99bb918e24e687..7ca095822de7be62f85625e7c2fbad5cf9d027d4 100644 (file)
@@ -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" />}
index 2233f79b19fabc2f4105560c7c84ca8232b1c2cc..32dd5f7393623c858466160941a57752c2d4448a 100644 (file)
@@ -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
index 92fc47ab7fbf2a6c75de07ff1ccb71acbbfccac1..e55755c6fb4bb1b18b98beaf97aac3c1d3c40729 100644 (file)
@@ -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>
   );
index aadcec8dfa6d24c397f412e6737198f432e6793f..8c19cef80e5652ee2acad7696cec14b98baca136 100644 (file)
@@ -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 }}
index 6bff6be69ab7e7c9d50412dcc9449ee374adfd34..ef6cb2ce9f27033c948c378a5812376c02e1cd66 100644 (file)
@@ -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;
 }
 
index 5f873b242b01a61c60030923054ea7dd339d1bff..a4ab9133b34c47baf77e08dc575f175c13062fd8 100644 (file)
@@ -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>
       )}
index b73db54ab2c46edb1e7e8aa03110d2d7194fc9c7..98ac6328b6c93a219b73f6a7feaa5c1f8c5ed58f 100644 (file)
@@ -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}
     >
index 9fb48a19931ecafb29cb7ceca2463d777e6d590a..0d404bb9aa13efc50fdb17a608391e38f75d1ad8 100644 (file)
@@ -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",
 }
index 7c98fcd7bbdef3981eb3ce133a6cb0cf7981fd7e..9fbefdbeb0165fe23dcc82313a95ac7629955363 100644 (file)
@@ -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;
   };
 }
 
index a92f62d0183d314861dbbf9592bdc9faddf6ba97..f0c58f35917cfbaa94944b6fb42e47ee02d7cda6 100644 (file)
@@ -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})"?