]> source.dussan.org Git - sonarqube.git/commitdiff
[NO JIRA] Extract global messages and remove redux
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 20 Apr 2022 10:10:30 +0000 (12:10 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 22 Apr 2022 20:03:02 +0000 (20:03 +0000)
63 files changed:
server/sonar-web/package.json
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/GlobalMessage.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.tsx
server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx
server/sonar-web/src/main/js/app/components/ResetPassword.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PluginRiskConsent-test.tsx.snap
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx
server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx
server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap
server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap
server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap
server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx
server/sonar-web/src/main/js/app/utils/__tests__/globalMessagesService-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts [deleted file]
server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts [deleted file]
server/sonar-web/src/main/js/app/utils/getStore.ts [deleted file]
server/sonar-web/src/main/js/app/utils/globalMessagesService.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx
server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap
server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx
server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx
server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx [deleted file]
server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/GlobalMessages-test.tsx [deleted file]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/helpers/__tests__/error-test.ts
server/sonar-web/src/main/js/helpers/error.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts [deleted file]
server/sonar-web/src/main/js/store/globalMessages.ts [deleted file]
server/sonar-web/src/main/js/store/rootReducer.ts [deleted file]
server/sonar-web/src/main/js/store/utils/configureStore.ts [deleted file]
server/sonar-web/src/main/js/types/extension.ts
server/sonar-web/src/main/js/types/globalMessages.ts [new file with mode: 0644]
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 1047305a8ca8b8b87662951639e2d845d7a6f772..034b4e99247ad27230ac99158bc37112f3342d22 100644 (file)
     "react-helmet-async": "1.2.3",
     "react-intl": "3.12.1",
     "react-modal": "3.14.4",
-    "react-redux": "5.1.1",
     "react-router": "3.2.6",
     "react-select": "4.3.1",
     "react-virtualized": "9.22.3",
-    "redux": "4.1.2",
-    "redux-thunk": "2.4.1",
     "regenerator-runtime": "0.13.9",
     "rehype-raw": "4.0.2",
     "rehype-react": "5.0.0",
@@ -74,7 +71,6 @@
     "@types/react-dom": "16.8.4",
     "@types/react-helmet": "5.0.15",
     "@types/react-modal": "3.13.1",
-    "@types/react-redux": "6.0.6",
     "@types/react-router": "3.0.20",
     "@types/react-select": "4.0.16",
     "@types/react-virtualized": "9.21.20",
index 3ee41a0606eb8a99799f7d6a7d536e9e2a2deef6..b02e8c98ebcf3e0f094ecd4d9c3425ac348ee24e 100644 (file)
@@ -24,7 +24,6 @@ import A11ySkipLinks from './a11y/A11ySkipLinks';
 import BranchStatusContextProvider from './branch-status/BranchStatusContextProvider';
 import SuggestionsProvider from './embed-docs-modal/SuggestionsProvider';
 import GlobalFooter from './GlobalFooter';
-import GlobalMessagesContainer from './GlobalMessagesContainer';
 import IndexationContextProvider from './indexation/IndexationContextProvider';
 import IndexationNotification from './indexation/IndexationNotification';
 import LanguagesContextProvider from './languages/LanguagesContextProvider';
@@ -57,7 +56,6 @@ export default function GlobalContainer(props: Props) {
                       <LanguagesContextProvider>
                         <MetricsContextProvider>
                           <GlobalNav location={props.location} />
-                          <GlobalMessagesContainer />
                           <IndexationNotification />
                           <UpdateNotification dismissable={true} />
                           {props.children}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalMessage.tsx b/server/sonar-web/src/main/js/app/components/GlobalMessage.tsx
new file mode 100644 (file)
index 0000000..9a0a64e
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { keyframes } from '@emotion/react';
+import styled from '@emotion/styled';
+import * as React from 'react';
+import { ClearButton } from '../../components/controls/buttons';
+import { cutLongWords } from '../../helpers/path';
+import { Message } from '../../types/globalMessages';
+import { colors, sizes } from '../theme';
+
+export interface GlobalMessageProps {
+  closeGlobalMessage: (id: string) => void;
+  message: Message;
+}
+
+export default function GlobalMessage(props: GlobalMessageProps) {
+  const { message } = props;
+  return (
+    <MessageBox
+      data-test={`global-message__${message.level}`}
+      level={message.level}
+      role={message.level === 'SUCCESS' ? 'status' : 'alert'}>
+      {cutLongWords(message.text)}
+      <CloseButton
+        className="button-small"
+        color="#fff"
+        level={message.level}
+        onClick={() => props.closeGlobalMessage(message.id)}
+      />
+    </MessageBox>
+  );
+}
+
+const appearAnim = keyframes`
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+`;
+
+const MessageBox = styled.div<Pick<Message, 'level'>>`
+  position: relative;
+  padding: 0 30px 0 10px;
+  line-height: ${sizes.controlHeight};
+  border-radius: 0 0 3px 3px;
+  box-sizing: border-box;
+  color: #ffffff;
+  background-color: ${({ level }) => (level === 'SUCCESS' ? colors.green : colors.red)};
+  text-align: center;
+  opacity: 0;
+  animation: ${appearAnim} 0.2s ease forwards;
+
+  & + & {
+    margin-top: calc(${sizes.gridSize} / 2);
+    border-radius: 3px;
+  }
+`;
+
+const CloseButton = styled(ClearButton)<Pick<Message, 'level'>>`
+  position: absolute;
+  top: calc(${sizes.gridSize} / 4);
+  right: calc(${sizes.gridSize} / 4);
+
+  &.button-icon:hover svg,
+  &.button-icon:focus svg {
+    color: ${({ level }) => (level === 'SUCCESS' ? colors.green : colors.red)};
+  }
+`;
index ac544be2cfa88ead93ac689e90bd482631698f32..742d449ac58f0951d7f2b1ab23d7d5ada716e7ab 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { connect } from 'react-redux';
-import GlobalMessages from '../../components/controls/GlobalMessages';
-import { closeGlobalMessage } from '../../store/globalMessages';
-import { getGlobalMessages, Store } from '../../store/rootReducer';
+import styled from '@emotion/styled';
+import React from 'react';
+import { Message } from '../../types/globalMessages';
+import { zIndexes } from '../theme';
+import { registerListener, unregisterListener } from '../utils/globalMessagesService';
+import GlobalMessage from './GlobalMessage';
 
-const mapStateToProps = (state: Store) => ({
-  messages: getGlobalMessages(state)
-});
+const MESSAGE_DISPLAY_TIME = 5000;
+const MAX_MESSAGES = 3;
 
-const mapDispatchToProps = { closeGlobalMessage };
+interface State {
+  messages: Message[];
+}
 
-export default connect(mapStateToProps, mapDispatchToProps)(GlobalMessages);
+export default class GlobalMessagesContainer extends React.Component<{}, State> {
+  mounted = false;
+
+  constructor(props: {}) {
+    super(props);
+
+    this.state = {
+      messages: []
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    registerListener(this.handleAddMessage);
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    unregisterListener(this.handleAddMessage);
+  }
+
+  handleAddMessage = (message: Message) => {
+    if (this.mounted) {
+      this.setState(({ messages }) => ({ messages: [...messages, message].slice(-MAX_MESSAGES) }));
+
+      setTimeout(() => {
+        this.closeMessage(message.id);
+      }, MESSAGE_DISPLAY_TIME);
+    }
+  };
+
+  closeMessage = (messageId: string) => {
+    if (this.mounted) {
+      this.setState(({ messages }) => {
+        return { messages: messages.filter(m => m.id !== messageId) };
+      });
+    }
+  };
+
+  render() {
+    const { messages } = this.state;
+
+    if (messages.length === 0) {
+      return null;
+    }
+
+    return (
+      <MessagesContainer>
+        {messages.map(message => (
+          <GlobalMessage
+            closeGlobalMessage={this.closeMessage}
+            key={message.id}
+            message={message}
+          />
+        ))}
+      </MessagesContainer>
+    );
+  }
+}
+
+const MessagesContainer = styled.div`
+  position: fixed;
+  z-index: ${zIndexes.processContainerZIndex};
+  top: 0;
+  left: 50%;
+  width: 350px;
+  margin-left: -175px;
+`;
index 6b560a90ca21012334b8fee2c005ea0ec9d9275c..394fa4a10b3ea30066d27d9b3c431801fd0153ac 100644 (file)
@@ -28,7 +28,6 @@ import { Permissions } from '../../types/permissions';
 import { RiskConsent } from '../../types/plugins';
 import { SettingsKey } from '../../types/settings';
 import { LoggedInUser } from '../../types/users';
-import GlobalMessagesContainer from './GlobalMessagesContainer';
 import './PluginRiskConsent.css';
 
 export interface PluginRiskConsentProps {
@@ -59,8 +58,6 @@ export function PluginRiskConsent(props: PluginRiskConsentProps) {
 
   return (
     <div className="plugin-risk-consent-page">
-      <GlobalMessagesContainer />
-
       <div className="plugin-risk-consent-content boxed-group">
         <div className="boxed-group-inner text-center">
           <h1 className="big-spacer-bottom">{translate('plugin_risk_consent.title')}</h1>
index 183cd2accb8274398467ac8c4f7771ab99aa39f4..1bb9f1db4c3df13f4ba45c4edef44a909e6d0702 100644 (file)
@@ -23,7 +23,6 @@ import { whenLoggedIn } from '../../components/hoc/whenLoggedIn';
 import { translate } from '../../helpers/l10n';
 import { getBaseUrl } from '../../helpers/system';
 import { LoggedInUser } from '../../types/users';
-import GlobalMessagesContainer from './GlobalMessagesContainer';
 
 export interface ResetPasswordProps {
   currentUser: LoggedInUser;
@@ -33,8 +32,6 @@ export function ResetPassword({ currentUser }: ResetPasswordProps) {
   return (
     <div className="page-wrapper-simple">
       <div className="page-simple">
-        <GlobalMessagesContainer />
-
         <h1 className="text-center huge">{translate('my_account.reset_password')}</h1>
         <p className="text-center huge-spacer-top huge-spacer-bottom">
           {translate('my_account.reset_password.explain')}
index f1c4226abca8239a4a1d987585edec8e9e02d737..e9e3361093aa887f4bdc14db19b72544a97884c0 100644 (file)
@@ -77,9 +77,6 @@ jest.mock('../../../api/alm-settings', () => ({
   validateProjectAlmBinding: jest.fn().mockResolvedValue(undefined)
 }));
 
-// mock this, because some of its children are using redux store
-jest.mock('../nav/component/ComponentNav', () => () => null);
-
 jest.mock('../../utils/handleRequiredAuthorization', () => ({
   __esModule: true,
   default: jest.fn()
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts b/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts
new file mode 100644 (file)
index 0000000..cdab690
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
+import { renderComponentApp } from '../../../helpers/testReactTestingUtils';
+import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../utils/globalMessagesService';
+import GlobalMessagesContainer from '../GlobalMessagesContainer';
+
+it('should display messages', () => {
+  renderComponentApp('sonarqube', GlobalMessagesContainer);
+
+  addGlobalErrorMessage('This is an error');
+  addGlobalSuccessMessage('This was a triumph!');
+
+  expect(screen.getByRole('alert')).toHaveTextContent('This is an error');
+  expect(screen.getByRole('status')).toHaveTextContent('This was a triumph!');
+});
index 2f15d0754f40019b95599e222cfc87fc1411f930..16b94edc8ff435a21332454f13467628891f1cb6 100644 (file)
@@ -33,7 +33,6 @@ exports[`should render correctly 1`] = `
                           }
                         }
                       />
-                      <Connect(GlobalMessages) />
                       <withCurrentUserContext(withIndexationContext(IndexationNotification)) />
                       <withCurrentUserContext(withAppStateContext(UpdateNotification))
                         dismissable={true}
index 5e5cb14f15ecbb3c8b334351ea5d4d18bc7f65ef..ec59ee3ae601f55d72a1a975bd937a47d62683e7 100644 (file)
@@ -4,7 +4,6 @@ exports[`should render correctly: default 1`] = `
 <div
   className="plugin-risk-consent-page"
 >
-  <Connect(GlobalMessages) />
   <div
     className="plugin-risk-consent-content boxed-group"
   >
index cf445a7dcb5df630afc5c2a520921098c96a3f5f..e6107f27865b11de71cbb883f4dccbfb30fca02c 100644 (file)
@@ -7,7 +7,6 @@ exports[`should render correctly 1`] = `
   <div
     className="page-simple"
   >
-    <Connect(GlobalMessages) />
     <h1
       className="text-center huge"
     >
index 1220526fb6a5b07e0cfc96a46c9887e6023264dd..563c1c1efcf042ed288cc995f3d54539d216353e 100644 (file)
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { injectIntl, WrappedComponentProps } from 'react-intl';
-import { connect } from 'react-redux';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
 import { getExtensionStart } from '../../../helpers/extensions';
 import { translate } from '../../../helpers/l10n';
 import { getCurrentL10nBundle } from '../../../helpers/l10nBundle';
 import { getBaseUrl } from '../../../helpers/system';
-import { addGlobalErrorMessage } from '../../../store/globalMessages';
 import { AppState } from '../../../types/appstate';
 import { ExtensionStartMethod } from '../../../types/extension';
 import { Dict, Extension as TypeExtension } from '../../../types/types';
 import { CurrentUser } from '../../../types/users';
 import * as theme from '../../theme';
-import getStore from '../../utils/getStore';
+import { addGlobalErrorMessage } from '../../utils/globalMessagesService';
 import withAppStateContext from '../app-state/withAppStateContext';
 import withCurrentUserContext from '../current-user/withCurrentUserContext';
 
@@ -41,7 +39,6 @@ interface Props extends WrappedComponentProps {
   currentUser: CurrentUser;
   extension: TypeExtension;
   location: Location;
-  onFail: (message: string) => void;
   options?: Dict<any>;
   router: Router;
 }
@@ -73,10 +70,8 @@ export class Extension extends React.PureComponent<Props, State> {
   }
 
   handleStart = (start: ExtensionStartMethod) => {
-    const store = getStore();
     const result = start({
       appState: this.props.appState,
-      store,
       el: this.container,
       currentUser: this.props.currentUser,
       intl: this.props.intl,
@@ -98,7 +93,7 @@ export class Extension extends React.PureComponent<Props, State> {
   };
 
   handleFailure = () => {
-    this.props.onFail(translate('page_extension_failed'));
+    addGlobalErrorMessage(translate('page_extension_failed'));
   };
 
   startExtension() {
@@ -128,10 +123,4 @@ export class Extension extends React.PureComponent<Props, State> {
   }
 }
 
-export default injectIntl(
-  withRouter(
-    withAppStateContext(
-      withCurrentUserContext(connect(null, { onFail: addGlobalErrorMessage })(Extension))
-    )
-  )
-);
+export default injectIntl(withRouter(withAppStateContext(withCurrentUserContext(Extension))));
index 72befde92a816baa38bdcd29ea9415154c7151a7..efc6ae1a1ae9a7238392d6130c8cc742b9f35a3b 100644 (file)
@@ -18,8 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
-import { addGlobalErrorMessage } from '../../../store/globalMessages';
 import { Component } from '../../../types/types';
 import NotFound from '../NotFound';
 import Extension from './Extension';
@@ -29,7 +27,7 @@ export interface ProjectAdminPageExtensionProps {
   params: { extensionKey: string; pluginKey: string };
 }
 
-export function ProjectAdminPageExtension(props: ProjectAdminPageExtensionProps) {
+export default function ProjectAdminPageExtension(props: ProjectAdminPageExtensionProps) {
   const {
     component,
     params: { extensionKey, pluginKey }
@@ -45,7 +43,3 @@ export function ProjectAdminPageExtension(props: ProjectAdminPageExtensionProps)
     <NotFound withContainer={false} />
   );
 }
-
-const mapDispatchToProps = { onFail: addGlobalErrorMessage };
-
-export default connect(null, mapDispatchToProps)(ProjectAdminPageExtension);
index 08ff7438d8763a6cb9c39665cca7d86fc5a628cf..3001705b90514b9e02cb55f008e4fda1fdd3391e 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { mount } from 'enzyme';
 import * as React from 'react';
+import { IntlShape } from 'react-intl';
 import { getExtensionStart } from '../../../../helpers/extensions';
 import {
   mockAppState,
@@ -93,9 +94,8 @@ function shallowRender(props: Partial<Extension['props']> = {}) {
       appState={mockAppState()}
       currentUser={mockCurrentUser()}
       extension={{ key: 'foo', name: 'Foo' }}
-      intl={{} as any}
+      intl={{} as IntlShape}
       location={mockLocation()}
-      onFail={jest.fn()}
       router={mockRouter()}
       {...props}
     />
index f3b7a57bfc4c7d46048f3d08e329c45850c85acf..7b29077a35f833d790a7740dd7e00b00b761d13f 100644 (file)
@@ -20,8 +20,7 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockComponent } from '../../../../helpers/mocks/component';
-import {
-  ProjectAdminPageExtension,
+import ProjectAdminPageExtension, {
   ProjectAdminPageExtensionProps
 } from '../ProjectAdminPageExtension';
 
index 4ab2a2f58c20f40b1a3e26842b658b5d387ed17c..4821cb22c5668a307e580ed01609045a1c3b9f70 100644 (file)
@@ -36,7 +36,6 @@ exports[`should render React extensions correctly 1`] = `
       "state": Object {},
     }
   }
-  onFail={[MockFunction]}
   router={
     Object {
       "createHref": [MockFunction],
@@ -96,7 +95,6 @@ exports[`should render React extensions correctly 2`] = `
       "state": Object {},
     }
   }
-  onFail={[MockFunction]}
   router={
     Object {
       "createHref": [MockFunction],
index 41c11c85e36ba672c18a5f10e283ff96ddaa7ace..690ff078cd90689588fa3f485596363c2d5015c7 100644 (file)
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render correctly: extension exists 1`] = `
-<injectIntl(withRouter(withAppStateContext(withCurrentUserContext(Connect(Extension)))))
+<injectIntl(withRouter(withAppStateContext(withCurrentUserContext(Extension))))
   extension={
     Object {
       "key": "foo/bar",
index bb710428d4c5dcb79114d986142389901ef9df14..c99ec489367732164e16bc987929b0dda4d5122a 100644 (file)
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render correctly 1`] = `
-<injectIntl(withRouter(withAppStateContext(withCurrentUserContext(Connect(Extension)))))
+<injectIntl(withRouter(withAppStateContext(withCurrentUserContext(Extension))))
   extension={
     Object {
       "key": "plugin-key/extension-key",
index 7650130479ec260b76d993f67f282b01ab6a8467..154fb38d9ff69c71c9f92ebf2dea28b777170704 100644 (file)
@@ -117,7 +117,7 @@ import {
   getMeasureHistoryUrl,
   getRulesUrl
 } from '../../../helpers/urls';
-import addGlobalSuccessMessage from '../../utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../utils/globalMessagesService';
 import A11ySkipTarget from '../a11y/A11ySkipTarget';
 import Suggestions from '../embed-docs-modal/Suggestions';
 
index 49b64ed497cb75ab60e6789a87e01926800f1578..d85531ebc4da36228a528051f1ad1e011ac56f48 100644 (file)
@@ -22,10 +22,6 @@ import * as React from 'react';
 import { waitAndUpdate } from '../../../../../helpers/testUtils';
 import { GlobalNav, GlobalNavProps } from '../GlobalNav';
 
-// Solve redux warning issue "No reducer provided for key":
-// https://stackoverflow.com/questions/43375079/redux-warning-only-appearing-in-tests
-jest.mock('../../../../../store/rootReducer');
-
 const location = { pathname: '' };
 
 it('should render correctly', async () => {
diff --git a/server/sonar-web/src/main/js/app/utils/__tests__/globalMessagesService-test.ts b/server/sonar-web/src/main/js/app/utils/__tests__/globalMessagesService-test.ts
new file mode 100644 (file)
index 0000000..0d40247
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { MessageLevel } from '../../../types/globalMessages';
+import {
+  addGlobalErrorMessage,
+  addGlobalSuccessMessage,
+  registerListener
+} from '../globalMessagesService';
+
+it('should work as expected', () => {
+  const listener1 = jest.fn();
+  registerListener(listener1);
+
+  addGlobalErrorMessage('test');
+
+  expect(listener1).toBeCalledWith(
+    expect.objectContaining({ text: 'test', level: MessageLevel.Error })
+  );
+
+  listener1.mockClear();
+  const listener2 = jest.fn();
+  registerListener(listener2);
+
+  addGlobalSuccessMessage('test');
+
+  expect(listener1).toBeCalledWith(
+    expect.objectContaining({ text: 'test', level: MessageLevel.Success })
+  );
+  expect(listener2).toBeCalledWith(
+    expect.objectContaining({ text: 'test', level: MessageLevel.Success })
+  );
+});
diff --git a/server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts b/server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts
deleted file mode 100644 (file)
index 39ba68b..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 globalMessages from '../../store/globalMessages';
-import getStore from './getStore';
-
-export default function addGlobalErrorMessage(message: string): void {
-  const store = getStore();
-  store.dispatch(globalMessages.addGlobalErrorMessage(message));
-}
diff --git a/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts b/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts
deleted file mode 100644 (file)
index e631bee..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 globalMessages from '../../store/globalMessages';
-import getStore from './getStore';
-
-export default function addGlobalSuccessMessage(message: string): void {
-  const store = getStore();
-  store.dispatch(globalMessages.addGlobalSuccessMessage(message));
-}
diff --git a/server/sonar-web/src/main/js/app/utils/getStore.ts b/server/sonar-web/src/main/js/app/utils/getStore.ts
deleted file mode 100644 (file)
index 251dd07..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { Store } from 'redux';
-import rootReducer, { Store as State } from '../../store/rootReducer';
-import configureStore from '../../store/utils/configureStore';
-
-let store: Store<State, any>;
-
-const createStore = () => {
-  store = configureStore(rootReducer);
-  return store;
-};
-
-export default () => (store ? store : createStore());
diff --git a/server/sonar-web/src/main/js/app/utils/globalMessagesService.ts b/server/sonar-web/src/main/js/app/utils/globalMessagesService.ts
new file mode 100644 (file)
index 0000000..fb3c82b
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { uniqueId } from 'lodash';
+import { Message, MessageLevel } from '../../types/globalMessages';
+
+const listeners: Array<(message: Message) => void> = [];
+
+export function registerListener(callback: (message: Message) => void) {
+  listeners.push(callback);
+}
+
+export function unregisterListener(callback: (message: Message) => void) {
+  const index = listeners.indexOf(callback);
+
+  if (index > -1) {
+    listeners.splice(index, 1);
+  }
+}
+
+function addMessage(text: string, level: MessageLevel) {
+  listeners.forEach(listener =>
+    listener({
+      id: uniqueId('global-message-'),
+      level,
+      text
+    })
+  );
+}
+
+export function addGlobalErrorMessage(text: string) {
+  addMessage(text, MessageLevel.Error);
+}
+
+export function addGlobalSuccessMessage(text: string) {
+  addMessage(text, MessageLevel.Success);
+}
index fc17431e3fa6ca97f1f36c9736885cace2371e27..622c56db5853d6fee78bfc0f24faa88e6b9e639b 100644 (file)
@@ -24,7 +24,6 @@ import * as React from 'react';
 import { render } from 'react-dom';
 import { HelmetProvider } from 'react-helmet-async';
 import { IntlProvider } from 'react-intl';
-import { Provider } from 'react-redux';
 import { IndexRoute, Redirect, Route, RouteConfig, RouteProps, Router } from 'react-router';
 import accountRoutes from '../../apps/account/routes';
 import auditLogsRoutes from '../../apps/audit-logs/routes';
@@ -66,11 +65,11 @@ import App from '../components/App';
 import AppStateContextProvider from '../components/app-state/AppStateContextProvider';
 import CurrentUserContextProvider from '../components/current-user/CurrentUserContextProvider';
 import GlobalContainer from '../components/GlobalContainer';
+import GlobalMessagesContainer from '../components/GlobalMessagesContainer';
 import { PageContext } from '../components/indexation/PageUnavailableDueToIndexation';
 import MigrationContainer from '../components/MigrationContainer';
 import NonAdminPagesContainer from '../components/NonAdminPagesContainer';
 import exportModulesAsGlobals from './exportModulesAsGlobals';
-import getStore from './getStore';
 
 function handleUpdate(this: { state: { location: Location } }) {
   const { action } = this.state.location;
@@ -283,107 +282,102 @@ export default function startReactApp(lang: string, appState: AppState, currentU
   const el = document.getElementById('content');
 
   const history = getHistory();
-  const store = getStore();
 
   render(
     <HelmetProvider>
-      <Provider store={store}>
-        <AppStateContextProvider appState={appState}>
-          <CurrentUserContextProvider currentUser={currentUser}>
-            <IntlProvider defaultLocale={lang} locale={lang}>
-              <Router history={history} onUpdate={handleUpdate}>
-                {renderRedirects()}
+      <AppStateContextProvider appState={appState}>
+        <CurrentUserContextProvider currentUser={currentUser}>
+          <IntlProvider defaultLocale={lang} locale={lang}>
+            <GlobalMessagesContainer />
+            <Router history={history} onUpdate={handleUpdate}>
+              {renderRedirects()}
 
-                <Route
-                  path="formatting/help"
-                  component={lazyLoadComponent(() => import('../components/FormattingHelp'))}
-                />
-
-                <Route component={lazyLoadComponent(() => import('../components/SimpleContainer'))}>
-                  <Route path="maintenance">{maintenanceRoutes}</Route>
-                  <Route path="setup">{setupRoutes}</Route>
-                </Route>
-
-                <Route component={MigrationContainer}>
-                  <Route
-                    component={lazyLoadComponent(() =>
-                      import('../components/SimpleSessionsContainer')
-                    )}>
-                    <RouteWithChildRoutes path="/sessions" childRoutes={sessionsRoutes} />
-                  </Route>
+              <Route
+                path="formatting/help"
+                component={lazyLoadComponent(() => import('../components/FormattingHelp'))}
+              />
 
-                  <Route path="/" component={App}>
-                    <IndexRoute
-                      component={lazyLoadComponent(() => import('../components/Landing'))}
-                    />
+              <Route component={lazyLoadComponent(() => import('../components/SimpleContainer'))}>
+                <Route path="maintenance">{maintenanceRoutes}</Route>
+                <Route path="setup">{setupRoutes}</Route>
+              </Route>
 
-                    <Route component={GlobalContainer}>
-                      <RouteWithChildRoutes path="account" childRoutes={accountRoutes} />
-                      <RouteWithChildRoutes path="coding_rules" childRoutes={codingRulesRoutes} />
-                      <RouteWithChildRoutes
-                        path="documentation"
-                        childRoutes={documentationRoutes}
-                      />
-                      <Route
-                        path="extension/:pluginKey/:extensionKey"
-                        component={lazyLoadComponent(() =>
-                          import('../components/extensions/GlobalPageExtension')
-                        )}
-                      />
-                      <Route
-                        path="issues"
-                        component={withIndexationGuard(Issues, PageContext.Issues)}
-                      />
-                      <RouteWithChildRoutes path="projects" childRoutes={projectsRoutes} />
-                      <RouteWithChildRoutes path="quality_gates" childRoutes={qualityGatesRoutes} />
-                      <Route
-                        path="portfolios"
-                        component={lazyLoadComponent(() =>
-                          import('../components/extensions/PortfoliosPage')
-                        )}
-                      />
-                      <RouteWithChildRoutes path="profiles" childRoutes={qualityProfilesRoutes} />
-                      <RouteWithChildRoutes path="web_api" childRoutes={webAPIRoutes} />
+              <Route component={MigrationContainer}>
+                <Route
+                  component={lazyLoadComponent(() =>
+                    import('../components/SimpleSessionsContainer')
+                  )}>
+                  <RouteWithChildRoutes path="/sessions" childRoutes={sessionsRoutes} />
+                </Route>
 
-                      {renderComponentRoutes()}
+                <Route path="/" component={App}>
+                  <IndexRoute
+                    component={lazyLoadComponent(() => import('../components/Landing'))}
+                  />
 
-                      {renderAdminRoutes()}
-                    </Route>
-                    <Route
-                      // We don't want this route to have any menu.
-                      // That is why we can not have it under the accountRoutes
-                      path="account/reset_password"
-                      component={lazyLoadComponent(() => import('../components/ResetPassword'))}
-                    />
+                  <Route component={GlobalContainer}>
+                    <RouteWithChildRoutes path="account" childRoutes={accountRoutes} />
+                    <RouteWithChildRoutes path="coding_rules" childRoutes={codingRulesRoutes} />
+                    <RouteWithChildRoutes path="documentation" childRoutes={documentationRoutes} />
                     <Route
-                      // We don't want this route to have any menu. This is why we define it here
-                      // rather than under the admin routes.
-                      path="admin/change_admin_password"
+                      path="extension/:pluginKey/:extensionKey"
                       component={lazyLoadComponent(() =>
-                        import('../../apps/change-admin-password/ChangeAdminPasswordApp')
+                        import('../components/extensions/GlobalPageExtension')
                       )}
                     />
                     <Route
-                      // We don't want this route to have any menu. This is why we define it here
-                      // rather than under the admin routes.
-                      path="admin/plugin_risk_consent"
-                      component={lazyLoadComponent(() => import('../components/PluginRiskConsent'))}
+                      path="issues"
+                      component={withIndexationGuard(Issues, PageContext.Issues)}
                     />
+                    <RouteWithChildRoutes path="projects" childRoutes={projectsRoutes} />
+                    <RouteWithChildRoutes path="quality_gates" childRoutes={qualityGatesRoutes} />
                     <Route
-                      path="not_found"
-                      component={lazyLoadComponent(() => import('../components/NotFound'))}
-                    />
-                    <Route
-                      path="*"
-                      component={lazyLoadComponent(() => import('../components/NotFound'))}
+                      path="portfolios"
+                      component={lazyLoadComponent(() =>
+                        import('../components/extensions/PortfoliosPage')
+                      )}
                     />
+                    <RouteWithChildRoutes path="profiles" childRoutes={qualityProfilesRoutes} />
+                    <RouteWithChildRoutes path="web_api" childRoutes={webAPIRoutes} />
+
+                    {renderComponentRoutes()}
+
+                    {renderAdminRoutes()}
                   </Route>
+                  <Route
+                    // We don't want this route to have any menu.
+                    // That is why we can not have it under the accountRoutes
+                    path="account/reset_password"
+                    component={lazyLoadComponent(() => import('../components/ResetPassword'))}
+                  />
+                  <Route
+                    // We don't want this route to have any menu. This is why we define it here
+                    // rather than under the admin routes.
+                    path="admin/change_admin_password"
+                    component={lazyLoadComponent(() =>
+                      import('../../apps/change-admin-password/ChangeAdminPasswordApp')
+                    )}
+                  />
+                  <Route
+                    // We don't want this route to have any menu. This is why we define it here
+                    // rather than under the admin routes.
+                    path="admin/plugin_risk_consent"
+                    component={lazyLoadComponent(() => import('../components/PluginRiskConsent'))}
+                  />
+                  <Route
+                    path="not_found"
+                    component={lazyLoadComponent(() => import('../components/NotFound'))}
+                  />
+                  <Route
+                    path="*"
+                    component={lazyLoadComponent(() => import('../components/NotFound'))}
+                  />
                 </Route>
-              </Router>
-            </IntlProvider>
-          </CurrentUserContextProvider>
-        </AppStateContextProvider>
-      </Provider>
+              </Route>
+            </Router>
+          </IntlProvider>
+        </CurrentUserContextProvider>
+      </AppStateContextProvider>
     </HelmetProvider>,
     el
   );
index 448bb44a8fb24e959ae28f658db2f992c6cf7523..8536936cac55e351b090a5eef82e54eee399ca4b 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import GlobalMessagesContainer from '../../app/components/GlobalMessagesContainer';
 import { SubmitButton } from '../../components/controls/buttons';
 import { Location } from '../../components/hoc/withRouter';
 import { Alert } from '../../components/ui/Alert';
@@ -70,8 +69,6 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
           </Alert>
         ) : (
           <>
-            <GlobalMessagesContainer />
-
             <h1 className="text-center bg-danger big padded">
               {translate('users.change_admin_password.instance_is_at_risk')}
             </h1>
index fe8e3e54412bea4479b5df5de75b2558af35833e..054078b14cb7afdce717252126661578adfc8013 100644 (file)
@@ -25,10 +25,6 @@ import { waitAndUpdate } from '../../../helpers/testUtils';
 import { ChangeAdminPasswordApp } from '../ChangeAdminPasswordApp';
 import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from '../constants';
 
-jest.mock('react-redux', () => ({
-  connect: jest.fn(() => (a: any) => a)
-}));
-
 jest.mock('../../../api/users', () => ({
   changePassword: jest.fn().mockResolvedValue(null)
 }));
index 836b65c7375731dacacb4532afe39c554bb9baea..c962cf507364d79c1a048628b0739d8f966a84ec 100644 (file)
@@ -9,7 +9,6 @@ exports[`should render correctly: cannot submit 1`] = `
   <div
     className="page-simple"
   >
-    <Connect(GlobalMessages) />
     <h1
       className="text-center bg-danger big padded"
     >
@@ -94,7 +93,6 @@ exports[`should render correctly: default 1`] = `
   <div
     className="page-simple"
   >
-    <Connect(GlobalMessages) />
     <h1
       className="text-center bg-danger big padded"
     >
@@ -179,7 +177,6 @@ exports[`should render correctly: submitting 1`] = `
   <div
     className="page-simple"
   >
-    <Connect(GlobalMessages) />
     <h1
       className="text-center bg-danger big padded"
     >
@@ -292,7 +289,6 @@ exports[`should render correctly: trying to use default admin password 1`] = `
   <div
     className="page-simple"
   >
-    <Connect(GlobalMessages) />
     <h1
       className="text-center bg-danger big padded"
     >
index a133e859fc12f57605c51beea787d3bb0e7018e1..f34619808440dec2c3867fda3af8ad64a9caf185 100644 (file)
@@ -22,8 +22,6 @@ import * as React from 'react';
 import { Query } from '../../utils';
 import AssigneeFacet from '../AssigneeFacet';
 
-jest.mock('../../../../store/rootReducer', () => ({}));
-
 it('should render', () => {
   expect(shallowRender({ assignees: ['foo'] })).toMatchSnapshot();
 });
index 01a1b86645f6042d74898123106b27fa04187d1a..aa519367723452e195df0131ce0134f5558d265d 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { deleteApplication } from '../../api/application';
 import { deletePortfolio, deleteProject } from '../../api/components';
-import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../app/utils/globalMessagesService';
 import { Button } from '../../components/controls/buttons';
 import ConfirmButton from '../../components/controls/ConfirmButton';
 import { Router, withRouter } from '../../components/hoc/withRouter';
index f5b13837fbb12c87e6e67f13a1bb9b2a1a81d473..e86d860f149b5dabd2528c481f977e44e84a4ad9 100644 (file)
@@ -26,7 +26,7 @@ import {
   getGateForProject,
   searchProjects
 } from '../../api/quality-gates';
-import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../app/utils/globalMessagesService';
 import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
 import { translate } from '../../helpers/l10n';
 import { Component, QualityGate } from '../../types/types';
index 79a458c4baf69d8c6c2de1dfcdbcc8fd06bec258..0d22641ab3b8d6551d73d4bf96c8ade98e50cb88 100644 (file)
@@ -62,7 +62,9 @@ jest.mock('../../../api/quality-gates', () => {
   };
 });
 
-jest.mock('../../../app/utils/addGlobalSuccessMessage', () => jest.fn());
+jest.mock('../../../app/utils/globalMessagesService', () => ({
+  addGlobalSuccessMessage: jest.fn()
+}));
 
 jest.mock('../../../app/utils/handleRequiredAuthorization', () => jest.fn());
 
index 4a997f43086f0d9c627a82e0e653ba9512ad955d..930558eef263be7c906b0a879d439eecb1715fb2 100644 (file)
@@ -26,7 +26,7 @@ import {
   Profile,
   searchQualityProfiles
 } from '../../api/quality-profiles';
-import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../app/utils/globalMessagesService';
 import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
 import { translateWithParameters } from '../../helpers/l10n';
 import { isDefined } from '../../helpers/types';
index 7e0bb24ba5f24cd73d2397104705225904ebd620..df5c7cf9553a3b44f09f261da905715df73bb51d 100644 (file)
@@ -71,7 +71,9 @@ jest.mock('../../../api/quality-profiles', () => {
   };
 });
 
-jest.mock('../../../app/utils/addGlobalSuccessMessage', () => jest.fn());
+jest.mock('../../../app/utils/globalMessagesService', () => ({
+  addGlobalSuccessMessage: jest.fn()
+}));
 
 jest.mock('../../../app/utils/handleRequiredAuthorization', () => jest.fn());
 
index 3132ff9cffba27f1fba050c5ae74273fb68b1326..3767d0df7b9c1238c8cac15c011ac50f0214a410 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { fetchQualityGate } from '../../../api/quality-gates';
-import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../../app/utils/globalMessagesService';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { translate } from '../../../helpers/l10n';
 import { Condition, QualityGate } from '../../../types/types';
index af216e4f858c0b0df94edd4a3e51419b32054552..8f6fa5f60a80d0a8db6e79e11af39f996d59eb97 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import addGlobalErrorMessage from '../../../app/utils/addGlobalErrorMessage';
-import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage';
+import {
+  addGlobalErrorMessage,
+  addGlobalSuccessMessage
+} from '../../../app/utils/globalMessagesService';
 import { Button } from '../../../components/controls/buttons';
 import { DropdownOverlay } from '../../../components/controls/Dropdown';
 import Toggler from '../../../components/controls/Toggler';
index 51f34af34bc19fdf50d31719394e6d504a367ea8..e0e4470308094b80ae7928cad1b569fc37bd5186 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { assignSecurityHotspot } from '../../../../api/security-hotspots';
 import withCurrentUserContext from '../../../../app/components/current-user/withCurrentUserContext';
-import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../../../app/utils/globalMessagesService';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
 import { Hotspot, HotspotResolution, HotspotStatus } from '../../../../types/security-hotspots';
 import { CurrentUser, isLoggedIn, UserActive } from '../../../../types/users';
index b67579254b7df06285fd47ed6fbeea2ffa01d698..b95aad482a004ab0bffbc07ef08319384c50f3f1 100644 (file)
@@ -20,7 +20,7 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { assignSecurityHotspot } from '../../../../../api/security-hotspots';
-import addGlobalSuccessMessage from '../../../../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../../../../app/utils/globalMessagesService';
 import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
 import { mockCurrentUser, mockUser } from '../../../../../helpers/testMocks';
 import { waitAndUpdate } from '../../../../../helpers/testUtils';
@@ -33,7 +33,9 @@ jest.mock('../../../../../api/security-hotspots', () => ({
   assignSecurityHotspot: jest.fn()
 }));
 
-jest.mock('../../../../../app/utils/addGlobalSuccessMessage', () => jest.fn());
+jest.mock('../../../../../app/utils/globalMessagesService', () => ({
+  addGlobalSuccessMessage: jest.fn()
+}));
 
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot();
index 1934450b65d91e324eed38e51b45a4679ffc56e9..1de29b310b3a6e313d052ffeefe3dda2092549a7 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer';
 import { Location, withRouter } from '../../../components/hoc/withRouter';
 import { Alert } from '../../../components/ui/Alert';
 import { translate } from '../../../helpers/l10n';
@@ -55,8 +54,6 @@ export function Login(props: LoginProps) {
       )}
 
       <LoginForm collapsed={identityProviders.length > 0} onSubmit={props.onSubmit} />
-
-      <GlobalMessagesContainer />
     </div>
   );
 }
index 294c57e47e8916b1eddd5d199142abcca65b8348..e4a2a2a46e57111410c3230d772a6ca1e213657f 100644 (file)
@@ -21,7 +21,8 @@ import { Location } from 'history';
 import * as React from 'react';
 import { logIn } from '../../../api/auth';
 import { getIdentityProviders } from '../../../api/users';
-import addGlobalErrorMessage from '../../../app/utils/addGlobalErrorMessage';
+import { addGlobalErrorMessage } from '../../../app/utils/globalMessagesService';
+import { translate } from '../../../helpers/l10n';
 import { getReturnUrl } from '../../../helpers/urls';
 import { IdentityProvider } from '../../../types/types';
 import Login from './Login';
@@ -66,7 +67,7 @@ export class LoginContainer extends React.PureComponent<Props, State> {
     return logIn(id, password)
       .then(this.handleSuccessfulLogin)
       .catch(() => {
-        addGlobalErrorMessage('Authentication failed');
+        addGlobalErrorMessage(translate('login.authentication_failed'));
         return Promise.reject();
       });
   };
index 4e4cae3b24540061736cabc2ef68a1991f582688..4064b879c8cb13ed2309e5ac1db0ab901e2e3409 100644 (file)
  */
 import * as React from 'react';
 import { logOut } from '../../../api/auth';
-import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer';
 import RecentHistory from '../../../app/components/RecentHistory';
-import addGlobalErrorMessage from '../../../app/utils/addGlobalErrorMessage';
+import { addGlobalErrorMessage } from '../../../app/utils/globalMessagesService';
 import { translate } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
 
-export class Logout extends React.PureComponent<{}> {
+export default class Logout extends React.PureComponent<{}> {
   componentDidMount() {
     logOut()
       .then(() => {
@@ -33,18 +32,15 @@ export class Logout extends React.PureComponent<{}> {
         window.location.replace(getBaseUrl() + '/');
       })
       .catch(() => {
-        addGlobalErrorMessage('Logout failed');
+        addGlobalErrorMessage(translate('login.logout_failed'));
       });
   }
 
   render() {
     return (
       <div className="page page-limited">
-        <GlobalMessagesContainer />
         <div className="text-center">{translate('logging_out')}</div>
       </div>
     );
   }
 }
-
-export default Logout;
index 5b0d001291375a2439f04d5f35a5df6fdbfb1cc4..01e81b89e138de90a67c88ee8201a88b5c89a89e 100644 (file)
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { logOut } from '../../../../api/auth';
-import addGlobalErrorMessage from '../../../../app/utils/addGlobalErrorMessage';
+import { addGlobalErrorMessage } from '../../../../app/utils/globalMessagesService';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { Logout } from '../Logout';
+import Logout from '../Logout';
 
 jest.mock('../../../../api/auth', () => ({
   logOut: jest.fn().mockResolvedValue(true)
 }));
 
-jest.mock('../../../../app/utils/addGlobalErrorMessage', () => ({
-  __esModule: true,
-  default: jest.fn()
+jest.mock('../../../../app/utils/globalMessagesService', () => ({
+  addGlobalErrorMessage: jest.fn()
 }));
 
 const originalLocation = window.location;
index d9ee013c6b7a3f31fbd38d04449663acbb095214..22b570e17b123b7c9949aa2feb1cb5adb7ef26c2 100644 (file)
@@ -34,7 +34,6 @@ exports[`should render correctly: with authorization error 1`] = `
     collapsed={true}
     onSubmit={[MockFunction]}
   />
-  <Connect(GlobalMessages) />
 </div>
 `;
 
@@ -65,7 +64,6 @@ exports[`should render correctly: with identity providers 1`] = `
     collapsed={true}
     onSubmit={[MockFunction]}
   />
-  <Connect(GlobalMessages) />
 </div>
 `;
 
@@ -83,6 +81,5 @@ exports[`should render correctly: without any identity providers 1`] = `
     collapsed={false}
     onSubmit={[MockFunction]}
   />
-  <Connect(GlobalMessages) />
 </div>
 `;
index f510e29fcf7397ea56ab3180d8298ee220c6e50e..49bf97a3c5919c56aa7ec3dd4034625c192494fc 100644 (file)
@@ -4,7 +4,6 @@ exports[`should not redirect if logout fails 1`] = `
 <div
   className="page page-limited"
 >
-  <Connect(GlobalMessages) />
   <div
     className="text-center"
   >
index 86f9416a7fe148b8eb76890a57648e764420918f..75f32f7c300959d7e3b56a299db8997956780a00 100644 (file)
@@ -19,7 +19,7 @@
  */
 import * as React from 'react';
 import { changePassword } from '../../../api/users';
-import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../../app/utils/globalMessagesService';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Modal from '../../../components/controls/Modal';
 import { Alert } from '../../../components/ui/Alert';
index dcb3359e16703408d710660c121d38a35556d48d..5cb685c46e98273e2b9386ed632eb4a9a4994740 100644 (file)
@@ -25,7 +25,7 @@ import {
 } from '../../api/component-report';
 import withAppStateContext from '../../app/components/app-state/withAppStateContext';
 import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
-import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../app/utils/globalMessagesService';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { AppState } from '../../types/appstate';
 import { Branch } from '../../types/branch-like';
diff --git a/server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx b/server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx
deleted file mode 100644 (file)
index 1598d35..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { keyframes } from '@emotion/react';
-import styled from '@emotion/styled';
-import * as React from 'react';
-import { colors, sizes, zIndexes } from '../../app/theme';
-import { cutLongWords } from '../../helpers/path';
-import { ClearButton } from './buttons';
-
-interface IMessage {
-  id: string;
-  level: 'ERROR' | 'SUCCESS';
-  message: string;
-}
-
-export interface GlobalMessagesProps {
-  closeGlobalMessage: (id: string) => void;
-  messages: IMessage[];
-}
-
-export default function GlobalMessages({ closeGlobalMessage, messages }: GlobalMessagesProps) {
-  if (messages.length === 0) {
-    return null;
-  }
-
-  return (
-    <MessagesContainer>
-      {messages.map(message => (
-        <GlobalMessage closeGlobalMessage={closeGlobalMessage} key={message.id} message={message} />
-      ))}
-    </MessagesContainer>
-  );
-}
-
-const MessagesContainer = styled.div`
-  position: fixed;
-  z-index: ${zIndexes.processContainerZIndex};
-  top: 0;
-  left: 50%;
-  width: 350px;
-  margin-left: -175px;
-`;
-
-export class GlobalMessage extends React.PureComponent<{
-  closeGlobalMessage: (id: string) => void;
-  message: IMessage;
-}> {
-  handleClose = () => {
-    this.props.closeGlobalMessage(this.props.message.id);
-  };
-
-  render() {
-    const { message } = this.props;
-    return (
-      <Message
-        data-test={`global-message__${message.level}`}
-        level={message.level}
-        role={message.level === 'SUCCESS' ? 'status' : 'alert'}>
-        {cutLongWords(message.message)}
-        <CloseButton
-          className="button-small"
-          color="#fff"
-          level={message.level}
-          onClick={this.handleClose}
-        />
-      </Message>
-    );
-  }
-}
-
-const appearAnim = keyframes`
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
-`;
-
-const Message = styled.div<Pick<IMessage, 'level'>>`
-  position: relative;
-  padding: 0 30px 0 10px;
-  line-height: ${sizes.controlHeight};
-  border-radius: 0 0 3px 3px;
-  box-sizing: border-box;
-  color: #ffffff;
-  background-color: ${({ level }) => (level === 'SUCCESS' ? colors.green : colors.red)};
-  text-align: center;
-  opacity: 0;
-  animation: ${appearAnim} 0.2s ease forwards;
-
-  & + & {
-    margin-top: calc(${sizes.gridSize} / 2);
-    border-radius: 3px;
-  }
-`;
-
-const CloseButton = styled(ClearButton)<Pick<IMessage, 'level'>>`
-  position: absolute;
-  top: calc(${sizes.gridSize} / 4);
-  right: calc(${sizes.gridSize} / 4);
-
-  &:hover svg,
-  &:focus svg {
-    color: ${({ level }) => (level === 'SUCCESS' ? colors.green : colors.red)};
-  }
-`;
index 4e53e1a3982ff421dabb9640f4b50b0beed6f4d1..c0c419e1ea6aa1d9c7b0ffc6f5fcfb0ab8901eb7 100644 (file)
@@ -24,7 +24,7 @@ import {
   subscribeToEmailReport,
   unsubscribeFromEmailReport
 } from '../../../api/component-report';
-import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage';
+import { addGlobalSuccessMessage } from '../../../app/utils/globalMessagesService';
 import { mockBranch } from '../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../helpers/mocks/component';
 import { mockComponentReportStatus } from '../../../helpers/mocks/component-report';
@@ -49,7 +49,9 @@ jest.mock('../../../helpers/system', () => ({
   getBaseUrl: jest.fn().mockReturnValue('baseUrl')
 }));
 
-jest.mock('../../../app/utils/addGlobalSuccessMessage', () => jest.fn());
+jest.mock('../../../app/utils/globalMessagesService', () => ({
+  addGlobalSuccessMessage: jest.fn()
+}));
 
 beforeEach(jest.clearAllMocks);
 
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/GlobalMessages-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/GlobalMessages-test.tsx
deleted file mode 100644 (file)
index f7022a7..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { shallow } from 'enzyme';
-import { matchers } from '@emotion/jest';
-import * as React from 'react';
-import { colors } from '../../../app/theme';
-import GlobalMessages, { GlobalMessagesProps } from '../GlobalMessages';
-
-expect.extend(matchers);
-
-it('should not render when no message', () => {
-  expect(shallowRender({ messages: [] }).type()).toBeNull();
-});
-
-it('should render correctly with a message', () => {
-  const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
-  expect(
-    wrapper
-      .find('GlobalMessage')
-      .first()
-      .dive()
-  ).toMatchSnapshot();
-  expect(
-    wrapper
-      .find('GlobalMessage')
-      .last()
-      .dive()
-  ).toMatchSnapshot();
-});
-
-it('should render with correct css', () => {
-  const wrapper = shallowRender();
-  expect(wrapper.render()).toMatchSnapshot();
-  expect(
-    wrapper
-      .find('GlobalMessage')
-      .first()
-      .render()
-  ).toHaveStyleRule('background-color', colors.red);
-
-  expect(
-    wrapper
-      .find('GlobalMessage')
-      .last()
-      .render()
-  ).toHaveStyleRule('background-color', colors.green);
-});
-
-function shallowRender(props: Partial<GlobalMessagesProps> = {}) {
-  return shallow(
-    <GlobalMessages
-      closeGlobalMessage={jest.fn()}
-      messages={[
-        { id: '1', level: 'ERROR', message: 'Test' },
-        { id: '2', level: 'SUCCESS', message: 'Test 2' }
-      ]}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap
deleted file mode 100644 (file)
index af1e41e..0000000
+++ /dev/null
@@ -1,212 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly with a message 1`] = `
-<Styled(div)>
-  <GlobalMessage
-    closeGlobalMessage={[MockFunction]}
-    key="1"
-    message={
-      Object {
-        "id": "1",
-        "level": "ERROR",
-        "message": "Test",
-      }
-    }
-  />
-  <GlobalMessage
-    closeGlobalMessage={[MockFunction]}
-    key="2"
-    message={
-      Object {
-        "id": "2",
-        "level": "SUCCESS",
-        "message": "Test 2",
-      }
-    }
-  />
-</Styled(div)>
-`;
-
-exports[`should render correctly with a message 2`] = `
-<Styled(div)
-  data-test="global-message__ERROR"
-  level="ERROR"
-  role="alert"
->
-  Test
-  <Styled(ClearButton)
-    className="button-small"
-    color="#fff"
-    level="ERROR"
-    onClick={[Function]}
-  />
-</Styled(div)>
-`;
-
-exports[`should render correctly with a message 3`] = `
-<Styled(div)
-  data-test="global-message__SUCCESS"
-  level="SUCCESS"
-  role="status"
->
-  Test 2
-  <Styled(ClearButton)
-    className="button-small"
-    color="#fff"
-    level="SUCCESS"
-    onClick={[Function]}
-  />
-</Styled(div)>
-`;
-
-exports[`should render with correct css 1`] = `
-@keyframes animation-0 {
-  from {
-    opacity: 0;
-  }
-
-  to {
-    opacity: 1;
-  }
-}
-
-@keyframes animation-0 {
-  from {
-    opacity: 0;
-  }
-
-  to {
-    opacity: 1;
-  }
-}
-
-.emotion-4 {
-  position: fixed;
-  z-index: 7000;
-  top: 0;
-  left: 50%;
-  width: 350px;
-  margin-left: -175px;
-}
-
-.emotion-1 {
-  position: relative;
-  padding: 0 30px 0 10px;
-  line-height: 24px;
-  border-radius: 0 0 3px 3px;
-  box-sizing: border-box;
-  color: #ffffff;
-  background-color: #d4333f;
-  text-align: center;
-  opacity: 0;
-  -webkit-animation: animation-0 0.2s ease forwards;
-  animation: animation-0 0.2s ease forwards;
-}
-
-.emotion-1+.emotion-1 {
-  margin-top: calc(8px / 2);
-  border-radius: 3px;
-}
-
-.emotion-0 {
-  position: absolute;
-  top: calc(8px / 4);
-  right: calc(8px / 4);
-}
-
-.emotion-0:hover svg,
-.emotion-0:focus svg {
-  color: #d4333f;
-}
-
-.emotion-3 {
-  position: relative;
-  padding: 0 30px 0 10px;
-  line-height: 24px;
-  border-radius: 0 0 3px 3px;
-  box-sizing: border-box;
-  color: #ffffff;
-  background-color: #00aa00;
-  text-align: center;
-  opacity: 0;
-  -webkit-animation: animation-0 0.2s ease forwards;
-  animation: animation-0 0.2s ease forwards;
-}
-
-.emotion-3+.emotion-3 {
-  margin-top: calc(8px / 2);
-  border-radius: 3px;
-}
-
-.emotion-2 {
-  position: absolute;
-  top: calc(8px / 4);
-  right: calc(8px / 4);
-}
-
-.emotion-2:hover svg,
-.emotion-2:focus svg {
-  color: #00aa00;
-}
-
-<div
-  class="emotion-4"
->
-  <div
-    class="emotion-1"
-    data-test="global-message__ERROR"
-    role="alert"
-  >
-    Test
-    <button
-      class="button button-small emotion-0 button-icon"
-      level="ERROR"
-      style="color:#fff"
-      type="button"
-    >
-      <svg
-        height="16"
-        space="preserve"
-        style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421"
-        version="1.1"
-        viewBox="0 0 16 16"
-        width="16"
-        xlink="http://www.w3.org/1999/xlink"
-      >
-        <path
-          d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z"
-          style="fill:currentColor"
-        />
-      </svg>
-    </button>
-  </div>
-  <div
-    class="emotion-3"
-    data-test="global-message__SUCCESS"
-    role="status"
-  >
-    Test 2
-    <button
-      class="button button-small emotion-2 button-icon"
-      level="SUCCESS"
-      style="color:#fff"
-      type="button"
-    >
-      <svg
-        height="16"
-        space="preserve"
-        style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421"
-        version="1.1"
-        viewBox="0 0 16 16"
-        width="16"
-        xlink="http://www.w3.org/1999/xlink"
-      >
-        <path
-          d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z"
-          style="fill:currentColor"
-        />
-      </svg>
-    </button>
-  </div>
-</div>
-`;
index ca6f81a60e9ef6649bbfa42d5b02f633bc43c1bb..cf126933261d63a7f01a7aed88aff301ff4b3005 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import getStore from '../../app/utils/getStore';
+import { addGlobalErrorMessage } from '../../app/utils/globalMessagesService';
 import { throwGlobalError } from '../error';
 
+jest.mock('../../app/utils/globalMessagesService', () => ({
+  addGlobalErrorMessage: jest.fn()
+}));
+
 beforeAll(() => {
   jest.useFakeTimers();
 });
 
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
 afterAll(() => {
   jest.runOnlyPendingTimers();
   jest.useRealTimers();
 });
 
-it('should put the error message in the store', async () => {
+it('should display the error message', async () => {
   const response = new Response();
   response.json = jest.fn().mockResolvedValue({ errors: [{ msg: 'error 1' }] });
 
@@ -40,13 +48,10 @@ it('should put the error message in the store', async () => {
     })
     .catch(() => {});
 
-  expect(getStore().getState().globalMessages[0]).toMatchObject({
-    level: 'ERROR',
-    message: 'error 1'
-  });
+  expect(addGlobalErrorMessage).toBeCalledWith('error 1');
 });
 
-it('should put a default error messsage in the store', async () => {
+it('should display the default error messsage', async () => {
   const response = new Response();
   response.json = jest.fn().mockResolvedValue({});
 
@@ -57,10 +62,7 @@ it('should put a default error messsage in the store', async () => {
     })
     .catch(() => {});
 
-  expect(getStore().getState().globalMessages[0]).toMatchObject({
-    level: 'ERROR',
-    message: 'default_error_message'
-  });
+  expect(addGlobalErrorMessage).toBeCalledWith('default_error_message');
 });
 
 it('should handle weird response types', () => {
index 78f2fa12d9a3fba728cf176daab66608a9f2f322..70ffdcc1795514bf7fceccdc6278ab322746dbc8 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import getStore from '../app/utils/getStore';
-import { addGlobalErrorMessage } from '../store/globalMessages';
+import { addGlobalErrorMessage } from '../app/utils/globalMessagesService';
 import { parseError } from './request';
 
 export function throwGlobalError(param: Response | any): Promise<Response | any> {
-  const store = getStore();
-
   if (param.response instanceof Response) {
     /* eslint-disable-next-line no-console */
     console.warn('DEPRECATED: response should not be wrapped, pass it directly.');
@@ -32,12 +29,9 @@ export function throwGlobalError(param: Response | any): Promise<Response | any>
 
   if (param instanceof Response) {
     return parseError(param)
-      .then(
-        message => {
-          store.dispatch(addGlobalErrorMessage(message));
-        },
-        () => {}
-      )
+      .then(addGlobalErrorMessage, () => {
+        /* ignore parsing errors */
+      })
       .then(() => Promise.reject(param));
   }
 
index ff43df271a464899884b5eda94228910db3979b4..437ed0de99e20160af46815b46ff37e952e85130 100644 (file)
@@ -19,7 +19,6 @@
  */
 import { Location, LocationDescriptor } from 'history';
 import { InjectedRouter } from 'react-router';
-import { createStore, Store } from 'redux';
 import { DocumentationEntry } from '../apps/documentation/utils';
 import { Exporter, Profile } from '../apps/quality-profiles/types';
 import { AppState } from '../types/appstate';
@@ -712,10 +711,6 @@ export function mockStandaloneSysInfo(overrides: Partial<any> = {}): SysInfoStan
   };
 }
 
-export function mockStore(state: any = {}, reducer = (state: any) => state): Store {
-  return createStore(reducer, state);
-}
-
 export function mockUser(overrides: Partial<User> = {}): User {
   return {
     active: true,
index 989783705fbba7a4bf3e8070218df3a0ea368653..e5119855674a7044cea0d8efb784860b0baa6554 100644 (file)
@@ -22,7 +22,6 @@ import { History } from 'history';
 import * as React from 'react';
 import { HelmetProvider } from 'react-helmet-async';
 import { IntlProvider } from 'react-intl';
-import { Provider } from 'react-redux';
 import {
   createMemoryHistory,
   Route,
@@ -32,15 +31,12 @@ import {
   withRouter,
   WithRouterProps
 } from 'react-router';
-import { Store } from 'redux';
 import AdminContext from '../app/components/AdminContext';
 import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider';
 import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider';
 import { LanguagesContext } from '../app/components/languages/LanguagesContext';
 import { MetricsContext } from '../app/components/metrics/MetricsContext';
-import getStore from '../app/utils/getStore';
 import { RouteWithChildRoutes } from '../app/utils/startReactApp';
-import { Store as State } from '../store/rootReducer';
 import { AppState } from '../types/appstate';
 import { Dict, Extension, Languages, Metric, SysStatus } from '../types/types';
 import { CurrentUser } from '../types/users';
@@ -49,7 +45,6 @@ import { mockAppState, mockCurrentUser } from './testMocks';
 
 interface RenderContext {
   metrics?: Dict<Metric>;
-  store?: Store<State, any>;
   history?: History;
   appState?: AppState;
   languages?: Languages;
@@ -141,7 +136,6 @@ function renderRoutedApp(
     currentUser = mockCurrentUser(),
     navigateTo = indexPath,
     metrics = DEFAULT_METRICS,
-    store = getStore(),
     appState = mockAppState(),
     history = createMemoryHistory(),
     languages = {}
@@ -152,18 +146,16 @@ function renderRoutedApp(
     <HelmetProvider context={{}}>
       <IntlProvider defaultLocale="en" locale="en">
         <MetricsContext.Provider value={metrics}>
-          <Provider store={store}>
-            <LanguagesContext.Provider value={languages}>
-              <CurrentUserContextProvider currentUser={currentUser}>
-                <AppStateContextProvider appState={appState}>
-                  <Router history={history}>
-                    {children}
-                    <Route path="*" component={CatchAll} />
-                  </Router>
-                </AppStateContextProvider>
-              </CurrentUserContextProvider>
-            </LanguagesContext.Provider>
-          </Provider>
+          <LanguagesContext.Provider value={languages}>
+            <CurrentUserContextProvider currentUser={currentUser}>
+              <AppStateContextProvider appState={appState}>
+                <Router history={history}>
+                  {children}
+                  <Route path="*" component={CatchAll} />
+                </Router>
+              </AppStateContextProvider>
+            </CurrentUserContextProvider>
+          </LanguagesContext.Provider>
         </MetricsContext.Provider>
       </IntlProvider>
     </HelmetProvider>
diff --git a/server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts b/server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts
deleted file mode 100644 (file)
index a5ceed7..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 globalMessagesReducer, { MessageLevel } from '../globalMessages';
-
-describe('globalMessagesReducer', () => {
-  it('should handle ADD_GLOBAL_MESSAGE', () => {
-    const actionAttributes = { id: 'id', message: 'There was an error', level: MessageLevel.Error };
-
-    expect(
-      globalMessagesReducer([], {
-        type: 'ADD_GLOBAL_MESSAGE',
-        ...actionAttributes
-      })
-    ).toEqual([actionAttributes]);
-  });
-
-  it('should handle CLOSE_GLOBAL_MESSAGE', () => {
-    const state = [
-      { id: 'm1', message: 'message 1', level: MessageLevel.Success },
-      { id: 'm2', message: 'message 2', level: MessageLevel.Success }
-    ];
-
-    expect(globalMessagesReducer(state, { type: 'CLOSE_GLOBAL_MESSAGE', id: 'm2' })).toEqual([
-      state[0]
-    ]);
-  });
-});
diff --git a/server/sonar-web/src/main/js/store/globalMessages.ts b/server/sonar-web/src/main/js/store/globalMessages.ts
deleted file mode 100644 (file)
index 58776e7..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { uniqueId } from 'lodash';
-import { Dispatch } from 'redux';
-import { ActionType } from '../types/actions';
-
-export enum MessageLevel {
-  Error = 'ERROR',
-  Success = 'SUCCESS'
-}
-
-interface Message {
-  id: string;
-  message: string;
-  level: MessageLevel;
-}
-
-const MESSAGE_DISPLAY_TIME = 5000;
-
-/* Action creators */
-
-function addGlobalMessageActionCreator(id: string, message: string, level: MessageLevel) {
-  return { type: 'ADD_GLOBAL_MESSAGE', message, level, id };
-}
-
-export function closeGlobalMessage(id: string) {
-  return { type: 'CLOSE_GLOBAL_MESSAGE', id };
-}
-
-type Action =
-  | ActionType<typeof addGlobalMessageActionCreator, 'ADD_GLOBAL_MESSAGE'>
-  | ActionType<typeof closeGlobalMessage, 'CLOSE_GLOBAL_MESSAGE'>;
-
-function addGlobalMessage(message: string, level: MessageLevel) {
-  return (dispatch: Dispatch) => {
-    const id = uniqueId('global-message-');
-    dispatch(addGlobalMessageActionCreator(id, message, level));
-    setTimeout(() => dispatch(closeGlobalMessage(id)), MESSAGE_DISPLAY_TIME);
-  };
-}
-
-export function addGlobalErrorMessage(message: string) {
-  return addGlobalMessage(message, MessageLevel.Error);
-}
-
-export function addGlobalSuccessMessage(message: string) {
-  return addGlobalMessage(message, MessageLevel.Success);
-}
-
-export type State = Message[];
-
-export default function globalMessagesReducer(state: State = [], action: Action): State {
-  switch (action.type) {
-    case 'ADD_GLOBAL_MESSAGE':
-      return [{ id: action.id, message: action.message, level: action.level }];
-
-    case 'CLOSE_GLOBAL_MESSAGE':
-      return state.filter(message => message.id !== action.id);
-
-    default:
-      return state;
-  }
-}
-
-export function getGlobalMessages(state: State) {
-  return state;
-}
diff --git a/server/sonar-web/src/main/js/store/rootReducer.ts b/server/sonar-web/src/main/js/store/rootReducer.ts
deleted file mode 100644 (file)
index 2a68fa7..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { combineReducers } from 'redux';
-import globalMessages, * as fromGlobalMessages from './globalMessages';
-
-export type Store = {
-  globalMessages: fromGlobalMessages.State;
-};
-
-export default combineReducers<Store>({
-  globalMessages
-});
-
-export function getGlobalMessages(state: Store) {
-  return fromGlobalMessages.getGlobalMessages(state.globalMessages);
-}
diff --git a/server/sonar-web/src/main/js/store/utils/configureStore.ts b/server/sonar-web/src/main/js/store/utils/configureStore.ts
deleted file mode 100644 (file)
index 65459f8..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { applyMiddleware, compose, createStore } from 'redux';
-import thunk, { ThunkMiddleware } from 'redux-thunk';
-
-type RootReducer = typeof import('../rootReducer').default;
-type State = import('../rootReducer').Store;
-
-const middlewares = [thunk as ThunkMiddleware<State, any>];
-const composed = [];
-
-if (process.env.NODE_ENV === 'development') {
-  const { __REDUX_DEVTOOLS_EXTENSION__ } = window as any;
-  composed.push(__REDUX_DEVTOOLS_EXTENSION__ ? __REDUX_DEVTOOLS_EXTENSION__() : (f: Function) => f);
-}
-
-const finalCreateStore = compose(applyMiddleware(...middlewares), ...composed)(createStore);
-
-export default function configureStore(rootReducer: RootReducer, initialState?: State) {
-  return finalCreateStore(rootReducer, initialState);
-}
index 73324b6ce6605d52896411fb1970812fd99f5a05..2540bc44cfec3c94772932eaea8344d21662756a 100644 (file)
@@ -18,9 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { IntlShape } from 'react-intl';
-import { Store as ReduxStore } from 'redux';
 import { Location, Router } from '../components/hoc/withRouter';
-import { Store } from '../store/rootReducer';
 import { AppState } from './appstate';
 import { L10nBundle } from './l10nBundle';
 import { Dict } from './types';
@@ -41,7 +39,6 @@ export interface ExtensionStartMethod {
 
 export interface ExtensionStartMethodParameter {
   appState: AppState;
-  store: ReduxStore<Store, any>;
   el: HTMLElement | undefined | null;
   currentUser: CurrentUser;
   intl: IntlShape;
diff --git a/server/sonar-web/src/main/js/types/globalMessages.ts b/server/sonar-web/src/main/js/types/globalMessages.ts
new file mode 100644 (file)
index 0000000..dec0e9a
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.
+ */
+export enum MessageLevel {
+  Error = 'ERROR',
+  Success = 'SUCCESS'
+}
+
+export interface Message {
+  id: string;
+  level: MessageLevel;
+  text: string;
+}
index 666869997a26f51e986e21c186e1f24d77498c74..7b2021f9293c01382ac5045b1b185adf66aa11c0 100644 (file)
@@ -573,15 +573,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/runtime@npm:^7.1.2":
-  version: 7.5.0
-  resolution: "@babel/runtime@npm:7.5.0"
-  dependencies:
-    regenerator-runtime: ^0.13.2
-  checksum: ec97ab3e35e93d65e3cadb558bca9adbc7fcc0656ee4ed7b83e72c40cab19e3a091fb2c631278e3eb7a60b2c030ce6d6ff5dec4a8154c372d950aaa6b9016156
-  languageName: node
-  linkType: hard
-
 "@babel/runtime@npm:^7.10.2":
   version: 7.12.5
   resolution: "@babel/runtime@npm:7.12.5"
@@ -2023,16 +2014,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/react-redux@npm:6.0.6":
-  version: 6.0.6
-  resolution: "@types/react-redux@npm:6.0.6"
-  dependencies:
-    "@types/react": "*"
-    redux: ^4.0.0
-  checksum: 2b56dcb652e412c82a112cc605b4eb2e463f1634ee7e8a2930539db72bc7f08b913ec5ce438f1989c5c9c0a5c903376bcb2a197464764f60330139d71b3900c2
-  languageName: node
-  linkType: hard
-
 "@types/react-router@npm:3.0.20":
   version: 3.0.20
   resolution: "@types/react-router@npm:3.0.20"
@@ -2328,7 +2309,6 @@ __metadata:
     "@types/react-dom": 16.8.4
     "@types/react-helmet": 5.0.15
     "@types/react-modal": 3.13.1
-    "@types/react-redux": 6.0.6
     "@types/react-router": 3.0.20
     "@types/react-select": 4.0.16
     "@types/react-virtualized": 9.21.20
@@ -2388,13 +2368,10 @@ __metadata:
     react-helmet-async: 1.2.3
     react-intl: 3.12.1
     react-modal: 3.14.4
-    react-redux: 5.1.1
     react-router: 3.2.6
     react-select: 4.3.1
     react-select-event: 5.4.0
     react-virtualized: 9.22.3
-    redux: 4.1.2
-    redux-thunk: 2.4.1
     regenerator-runtime: 0.13.9
     rehype-raw: 4.0.2
     rehype-react: 5.0.0
@@ -5893,15 +5870,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"hoist-non-react-statics@npm:^3.1.0":
-  version: 3.3.0
-  resolution: "hoist-non-react-statics@npm:3.3.0"
-  dependencies:
-    react-is: ^16.7.0
-  checksum: 78f77efc6dd4bfa194a96e8c97248ce59f9bf0e63686ee76cb9ab0183d8bd317fcb6bd25f442c0ef9c19d6db144de0df05b79895fd64cae331dbd6e2e573a565
-  languageName: node
-  linkType: hard
-
 "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
   version: 3.3.2
   resolution: "hoist-non-react-statics@npm:3.3.2"
@@ -9151,7 +9119,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.1, react-is@npm:^16.8.6":
+"react-is@npm:^16.7.0, react-is@npm:^16.8.1, react-is@npm:^16.8.6":
   version: 16.8.6
   resolution: "react-is@npm:16.8.6"
   checksum: 9dfcf465def71ba96e7d77d7e9c49a6cce7e9017dada5a13001bfe5a1b60f4bfb00a839a7847245ffcd4d1d6518b4b52787e6f2a4275f3c6bbc1243bd1dbeb9d
@@ -9187,24 +9155,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-redux@npm:5.1.1":
-  version: 5.1.1
-  resolution: "react-redux@npm:5.1.1"
-  dependencies:
-    "@babel/runtime": ^7.1.2
-    hoist-non-react-statics: ^3.1.0
-    invariant: ^2.2.4
-    loose-envify: ^1.1.0
-    prop-types: ^15.6.1
-    react-is: ^16.6.0
-    react-lifecycles-compat: ^3.0.0
-  peerDependencies:
-    react: ^0.14.0 || ^15.0.0-0 || ^16.0.0-0
-    redux: ^2.0.0 || ^3.0.0 || ^4.0.0-0
-  checksum: 6c79892e8dd40d33af056fa6064184287f7237891c6a858708b4decc9e1a456bab84b4f0a9ae14cb7b0bf520bbed044adae952c0d688d9a1c33072d3a058ad3c
-  languageName: node
-  linkType: hard
-
 "react-router@npm:3.2.6":
   version: 3.2.6
   resolution: "react-router@npm:3.2.6"
@@ -9352,34 +9302,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"redux-thunk@npm:2.4.1":
-  version: 2.4.1
-  resolution: "redux-thunk@npm:2.4.1"
-  peerDependencies:
-    redux: ^4
-  checksum: af5abb425fb9dccda02e5f387d6f3003997f62d906542a3d35fc9420088f550dc1a018bdc246c7d23ee852b4d4ab8b5c64c5be426e45a328d791c4586a3c6b6e
-  languageName: node
-  linkType: hard
-
-"redux@npm:4.1.2":
-  version: 4.1.2
-  resolution: "redux@npm:4.1.2"
-  dependencies:
-    "@babel/runtime": ^7.9.2
-  checksum: 6a839cee5bd580c5298d968e9e2302150e961318253819bcd97f9d945a5a409559eacddf6026f4118bb68b681c593d90e8a2c5bbf278f014aff9bf0d2d8fa084
-  languageName: node
-  linkType: hard
-
-"redux@npm:^4.0.0":
-  version: 4.0.1
-  resolution: "redux@npm:4.0.1"
-  dependencies:
-    loose-envify: ^1.4.0
-    symbol-observable: ^1.2.0
-  checksum: f3a4e19b0413cc73ccdbe9f71977292dca9760606ab783aed516c90ca04e931fa1af573c6c55bc506580a2805d4ec0d50edde0b14d7d854f0a66da40f36184b2
-  languageName: node
-  linkType: hard
-
 "reflect.ownkeys@npm:^0.2.0":
   version: 0.2.0
   resolution: "reflect.ownkeys@npm:0.2.0"
@@ -10521,13 +10443,6 @@ resolve@^1.3.2:
   languageName: node
   linkType: hard
 
-"symbol-observable@npm:^1.2.0":
-  version: 1.2.0
-  resolution: "symbol-observable@npm:1.2.0"
-  checksum: 48ffbc22e3d75f9853b3ff2ae94a44d84f386415110aea5effc24d84c502e03a4a6b7a8f75ebaf7b585780bda34eb5d6da3121f826a6f93398429d30032971b6
-  languageName: node
-  linkType: hard
-
 "symbol-tree@npm:^3.2.4":
   version: 3.2.4
   resolution: "symbol-tree@npm:3.2.4"
index 806234475e3558a2cd6e0954d249208128946c8a..5bea3319a98f3cdf8ac07637a5275f8250e02d81 100644 (file)
@@ -1946,10 +1946,14 @@ login.login_with_x=Log in with {0}
 login.more_options=More options
 login.unauthorized_access_alert=You are not authorized to access this page. Please log in with more privileges and try again.
 login.with_x=With {0}
+login.authentication_failed=Authentication failed
+login.logout_failed=Logout failed
 
 unauthorized.message=You're not authorized to access this page. Please contact the administrator.
 unauthorized.reason=Reason:
 
+
+
 #------------------------------------------------------------------------------
 #
 # USERS & GROUPS PAGE