diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2022-04-20 12:10:30 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-04-22 20:03:02 +0000 |
commit | b69c2cc6a20c6c50bcc93be8d74012c31ffcefe3 (patch) | |
tree | 3fdd4cc5273b845cd6364a15765152cc64135f68 /server/sonar-web/src | |
parent | b4e61d3047b13ab82102d360271c9d67ad9fdcaf (diff) | |
download | sonarqube-b69c2cc6a20c6c50bcc93be8d74012c31ffcefe3.tar.gz sonarqube-b69c2cc6a20c6c50bcc93be8d74012c31ffcefe3.zip |
[NO JIRA] Extract global messages and remove redux
Diffstat (limited to 'server/sonar-web/src')
55 files changed, 325 insertions, 777 deletions
diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 3ee41a0606e..b02e8c98ebc 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -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/components/controls/GlobalMessages.tsx b/server/sonar-web/src/main/js/app/components/GlobalMessage.tsx index 1598d35e80b..9a0a64e1a2d 100644 --- a/server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalMessage.tsx @@ -20,69 +20,32 @@ import { keyframes } from '@emotion/react'; import styled from '@emotion/styled'; import * as React from 'react'; -import { colors, sizes, zIndexes } from '../../app/theme'; +import { ClearButton } from '../../components/controls/buttons'; import { cutLongWords } from '../../helpers/path'; -import { ClearButton } from './buttons'; +import { Message } from '../../types/globalMessages'; +import { colors, sizes } from '../theme'; -interface IMessage { - id: string; - level: 'ERROR' | 'SUCCESS'; - message: string; -} - -export interface GlobalMessagesProps { +export interface GlobalMessageProps { closeGlobalMessage: (id: string) => void; - messages: IMessage[]; + message: Message; } -export default function GlobalMessages({ closeGlobalMessage, messages }: GlobalMessagesProps) { - if (messages.length === 0) { - return null; - } - +export default function GlobalMessage(props: GlobalMessageProps) { + const { message } = props; 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}`} + <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} - role={message.level === 'SUCCESS' ? 'status' : 'alert'}> - {cutLongWords(message.message)} - <CloseButton - className="button-small" - color="#fff" - level={message.level} - onClick={this.handleClose} - /> - </Message> - ); - } + onClick={() => props.closeGlobalMessage(message.id)} + /> + </MessageBox> + ); } const appearAnim = keyframes` @@ -94,7 +57,7 @@ const appearAnim = keyframes` } `; -const Message = styled.div<Pick<IMessage, 'level'>>` +const MessageBox = styled.div<Pick<Message, 'level'>>` position: relative; padding: 0 30px 0 10px; line-height: ${sizes.controlHeight}; @@ -112,13 +75,13 @@ const Message = styled.div<Pick<IMessage, 'level'>>` } `; -const CloseButton = styled(ClearButton)<Pick<IMessage, 'level'>>` +const CloseButton = styled(ClearButton)<Pick<Message, 'level'>>` position: absolute; top: calc(${sizes.gridSize} / 4); right: calc(${sizes.gridSize} / 4); - &:hover svg, - &:focus svg { + &.button-icon:hover svg, + &.button-icon:focus svg { color: ${({ level }) => (level === 'SUCCESS' ? colors.green : colors.red)}; } `; diff --git a/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.tsx index ac544be2cfa..742d449ac58 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.tsx @@ -17,15 +17,85 @@ * 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; +`; diff --git a/server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx b/server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx index 6b560a90ca2..394fa4a10b3 100644 --- a/server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx +++ b/server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/app/components/ResetPassword.tsx b/server/sonar-web/src/main/js/app/components/ResetPassword.tsx index 183cd2accb8..1bb9f1db4c3 100644 --- a/server/sonar-web/src/main/js/app/components/ResetPassword.tsx +++ b/server/sonar-web/src/main/js/app/components/ResetPassword.tsx @@ -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')} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index f1c4226abca..e9e3361093a 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -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/store/rootReducer.ts b/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts index 2a68fa75667..cdab690199a 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.ts +++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts @@ -17,17 +17,17 @@ * 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'; +import { screen } from '@testing-library/react'; +import { renderComponentApp } from '../../../helpers/testReactTestingUtils'; +import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../utils/globalMessagesService'; +import GlobalMessagesContainer from '../GlobalMessagesContainer'; -export type Store = { - globalMessages: fromGlobalMessages.State; -}; +it('should display messages', () => { + renderComponentApp('sonarqube', GlobalMessagesContainer); -export default combineReducers<Store>({ - globalMessages -}); + addGlobalErrorMessage('This is an error'); + addGlobalSuccessMessage('This was a triumph!'); -export function getGlobalMessages(state: Store) { - return fromGlobalMessages.getGlobalMessages(state.globalMessages); -} + expect(screen.getByRole('alert')).toHaveTextContent('This is an error'); + expect(screen.getByRole('status')).toHaveTextContent('This was a triumph!'); +}); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap index 2f15d0754f4..16b94edc8ff 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap @@ -33,7 +33,6 @@ exports[`should render correctly 1`] = ` } } /> - <Connect(GlobalMessages) /> <withCurrentUserContext(withIndexationContext(IndexationNotification)) /> <withCurrentUserContext(withAppStateContext(UpdateNotification)) dismissable={true} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PluginRiskConsent-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PluginRiskConsent-test.tsx.snap index 5e5cb14f15e..ec59ee3ae60 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PluginRiskConsent-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PluginRiskConsent-test.tsx.snap @@ -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" > diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap index cf445a7dcb5..e6107f27865 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap @@ -7,7 +7,6 @@ exports[`should render correctly 1`] = ` <div className="page-simple" > - <Connect(GlobalMessages) /> <h1 className="text-center huge" > diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx index 1220526fb6a..563c1c1efcf 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx @@ -20,19 +20,17 @@ 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)))); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx index 72befde92a8..efc6ae1a1ae 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx index 08ff7438d87..3001705b905 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx index f3b7a57bfc4..7b29077a35f 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap index 4ab2a2f58c2..4821cb22c56 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap @@ -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], diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap index 41c11c85e36..690ff078cd9 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap @@ -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", diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap index bb710428d4c..c99ec489367 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap @@ -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", diff --git a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts index 7650130479e..154fb38d9ff 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts +++ b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts @@ -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'; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx index 49b64ed497c..d85531ebc4d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx @@ -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/store/__tests__/globalMessages-test.ts b/server/sonar-web/src/main/js/app/utils/__tests__/globalMessagesService-test.ts index a5ceed74a08..0d402470d43 100644 --- a/server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts +++ b/server/sonar-web/src/main/js/app/utils/__tests__/globalMessagesService-test.ts @@ -17,28 +17,33 @@ * 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'; +import { MessageLevel } from '../../../types/globalMessages'; +import { + addGlobalErrorMessage, + addGlobalSuccessMessage, + registerListener +} from '../globalMessagesService'; -describe('globalMessagesReducer', () => { - it('should handle ADD_GLOBAL_MESSAGE', () => { - const actionAttributes = { id: 'id', message: 'There was an error', level: MessageLevel.Error }; +it('should work as expected', () => { + const listener1 = jest.fn(); + registerListener(listener1); - expect( - globalMessagesReducer([], { - type: 'ADD_GLOBAL_MESSAGE', - ...actionAttributes - }) - ).toEqual([actionAttributes]); - }); + addGlobalErrorMessage('test'); - 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(listener1).toBeCalledWith( + expect.objectContaining({ text: 'test', level: MessageLevel.Error }) + ); - expect(globalMessagesReducer(state, { type: 'CLOSE_GLOBAL_MESSAGE', id: 'm2' })).toEqual([ - state[0] - ]); - }); + 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/addGlobalSuccessMessage.ts b/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts deleted file mode 100644 index e631beefd68..00000000000 --- a/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts +++ /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 index 251dd073983..00000000000 --- a/server/sonar-web/src/main/js/app/utils/getStore.ts +++ /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/store/utils/configureStore.ts b/server/sonar-web/src/main/js/app/utils/globalMessagesService.ts index 65459f8eea3..fb3c82b6234 100644 --- a/server/sonar-web/src/main/js/store/utils/configureStore.ts +++ b/server/sonar-web/src/main/js/app/utils/globalMessagesService.ts @@ -17,22 +17,38 @@ * 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; +import { uniqueId } from 'lodash'; +import { Message, MessageLevel } from '../../types/globalMessages'; -const middlewares = [thunk as ThunkMiddleware<State, any>]; -const composed = []; +const listeners: Array<(message: Message) => void> = []; -if (process.env.NODE_ENV === 'development') { - const { __REDUX_DEVTOOLS_EXTENSION__ } = window as any; - composed.push(__REDUX_DEVTOOLS_EXTENSION__ ? __REDUX_DEVTOOLS_EXTENSION__() : (f: Function) => f); +export function registerListener(callback: (message: Message) => void) { + listeners.push(callback); } -const finalCreateStore = compose(applyMiddleware(...middlewares), ...composed)(createStore); +export function unregisterListener(callback: (message: Message) => void) { + const index = listeners.indexOf(callback); -export default function configureStore(rootReducer: RootReducer, initialState?: State) { - return finalCreateStore(rootReducer, initialState); + 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); } diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index fc17431e3fa..622c56db585 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -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 ); diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx index 448bb44a8fb..8536936cac5 100644 --- a/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx index fe8e3e54412..054078b14cb 100644 --- a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx @@ -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) })); diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap index 836b65c7375..c962cf50736 100644 --- a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap @@ -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" > diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx index a133e859fc1..f3461980844 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx b/server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx index 01a1b86645f..aa519367723 100644 --- a/server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx +++ b/server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx index f5b13837fbb..e86d860f149 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx index 79a458c4baf..0d22641ab3b 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx @@ -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()); diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx index 4a997f43086..930558eef26 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx index 7e0bb24ba5f..df5c7cf9553 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx @@ -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()); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx index 3132ff9cffb..3767d0df7b9 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx index af216e4f858..8f6fa5f60a8 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx @@ -18,8 +18,10 @@ * 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'; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx index 51f34af34bc..e0e44703080 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx index b67579254b7..b95aad482a0 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx @@ -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(); diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx b/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx index 1934450b65d..1de29b310b3 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx +++ b/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx b/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx index 294c57e47e8..e4a2a2a46e5 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx +++ b/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx @@ -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(); }); }; diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx b/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx index 4e4cae3b245..4064b879c8c 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx +++ b/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx @@ -19,13 +19,12 @@ */ 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; diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx index 5b0d0012913..01e81b89e13 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx +++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx @@ -20,17 +20,16 @@ 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; diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap index d9ee013c6b7..22b570e17b1 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap @@ -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> `; diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap index f510e29fcf7..49bf97a3c59 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap @@ -4,7 +4,6 @@ exports[`should not redirect if logout fails 1`] = ` <div className="page page-limited" > - <Connect(GlobalMessages) /> <div className="text-center" > diff --git a/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx b/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx index 86f9416a7fe..75f32f7c300 100644 --- a/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx b/server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx index dcb3359e167..5cb685c46e9 100644 --- a/server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx +++ b/server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx @@ -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/__tests__/ComponentReportActions-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx index 4e53e1a3982..c0c419e1ea6 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx @@ -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 index f7022a78ceb..00000000000 --- a/server/sonar-web/src/main/js/components/controls/__tests__/GlobalMessages-test.tsx +++ /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 index af1e41ecb7a..00000000000 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap +++ /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> -`; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/error-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/error-test.ts index ca6f81a60e9..cf126933261 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/error-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/error-test.ts @@ -17,19 +17,27 @@ * 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', () => { diff --git a/server/sonar-web/src/main/js/helpers/error.ts b/server/sonar-web/src/main/js/helpers/error.ts index 78f2fa12d9a..70ffdcc1795 100644 --- a/server/sonar-web/src/main/js/helpers/error.ts +++ b/server/sonar-web/src/main/js/helpers/error.ts @@ -17,13 +17,10 @@ * 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)); } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index ff43df271a4..437ed0de99e 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -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, diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx index 989783705fb..e5119855674 100644 --- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx +++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx @@ -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/globalMessages.ts b/server/sonar-web/src/main/js/store/globalMessages.ts deleted file mode 100644 index 58776e7d253..00000000000 --- a/server/sonar-web/src/main/js/store/globalMessages.ts +++ /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/types/extension.ts b/server/sonar-web/src/main/js/types/extension.ts index 73324b6ce66..2540bc44cfe 100644 --- a/server/sonar-web/src/main/js/types/extension.ts +++ b/server/sonar-web/src/main/js/types/extension.ts @@ -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/app/utils/addGlobalErrorMessage.ts b/server/sonar-web/src/main/js/types/globalMessages.ts index 39ba68b70c0..dec0e9a3c8e 100644 --- a/server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts +++ b/server/sonar-web/src/main/js/types/globalMessages.ts @@ -17,10 +17,13 @@ * 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 enum MessageLevel { + Error = 'ERROR', + Success = 'SUCCESS' +} -export default function addGlobalErrorMessage(message: string): void { - const store = getStore(); - store.dispatch(globalMessages.addGlobalErrorMessage(message)); +export interface Message { + id: string; + level: MessageLevel; + text: string; } |