Browse Source

[NO JIRA] Extract global messages and remove redux

tags/9.5.0.56709
Jeremy Davis 2 years ago
parent
commit
b69c2cc6a2
58 changed files with 330 additions and 867 deletions
  1. 0
    4
      server/sonar-web/package.json
  2. 0
    2
      server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
  3. 23
    60
      server/sonar-web/src/main/js/app/components/GlobalMessage.tsx
  4. 79
    9
      server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.tsx
  5. 0
    3
      server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx
  6. 0
    3
      server/sonar-web/src/main/js/app/components/ResetPassword.tsx
  7. 0
    3
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  8. 11
    11
      server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts
  9. 0
    1
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap
  10. 0
    1
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PluginRiskConsent-test.tsx.snap
  11. 0
    1
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap
  12. 3
    14
      server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
  13. 1
    7
      server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
  14. 2
    2
      server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx
  15. 1
    2
      server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx
  16. 0
    2
      server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap
  17. 1
    1
      server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap
  18. 1
    1
      server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap
  19. 1
    1
      server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
  20. 0
    4
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx
  21. 25
    20
      server/sonar-web/src/main/js/app/utils/__tests__/globalMessagesService-test.ts
  22. 0
    26
      server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts
  23. 0
    31
      server/sonar-web/src/main/js/app/utils/getStore.ts
  24. 28
    12
      server/sonar-web/src/main/js/app/utils/globalMessagesService.ts
  25. 79
    85
      server/sonar-web/src/main/js/app/utils/startReactApp.tsx
  26. 0
    3
      server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx
  27. 0
    4
      server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx
  28. 0
    4
      server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap
  29. 0
    2
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
  30. 1
    1
      server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx
  31. 1
    1
      server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx
  32. 3
    1
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx
  33. 1
    1
      server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx
  34. 3
    1
      server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx
  35. 1
    1
      server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
  36. 4
    2
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx
  37. 1
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx
  38. 4
    2
      server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx
  39. 0
    3
      server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
  40. 3
    2
      server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
  41. 3
    7
      server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
  42. 4
    5
      server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx
  43. 0
    3
      server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap
  44. 0
    1
      server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap
  45. 1
    1
      server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx
  46. 1
    1
      server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx
  47. 4
    2
      server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx
  48. 0
    78
      server/sonar-web/src/main/js/components/controls/__tests__/GlobalMessages-test.tsx
  49. 0
    212
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap
  50. 13
    11
      server/sonar-web/src/main/js/helpers/__tests__/error-test.ts
  51. 4
    10
      server/sonar-web/src/main/js/helpers/error.ts
  52. 0
    5
      server/sonar-web/src/main/js/helpers/testMocks.ts
  53. 10
    18
      server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
  54. 0
    84
      server/sonar-web/src/main/js/store/globalMessages.ts
  55. 0
    3
      server/sonar-web/src/main/js/types/extension.ts
  56. 8
    5
      server/sonar-web/src/main/js/types/globalMessages.ts
  57. 1
    86
      server/sonar-web/yarn.lock
  58. 4
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 4
server/sonar-web/package.json View File

@@ -32,12 +32,9 @@
"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",

+ 0
- 2
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx View 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}

server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx → server/sonar-web/src/main/js/app/components/GlobalMessage.tsx View File

@@ -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)};
}
`;

+ 79
- 9
server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.tsx View File

@@ -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;
`;

+ 0
- 3
server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx View 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>

+ 0
- 3
server/sonar-web/src/main/js/app/components/ResetPassword.tsx View 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')}

+ 0
- 3
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx View 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()

server/sonar-web/src/main/js/store/rootReducer.ts → server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.ts View File

@@ -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!');
});

+ 0
- 1
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap View File

@@ -33,7 +33,6 @@ exports[`should render correctly 1`] = `
}
}
/>
<Connect(GlobalMessages) />
<withCurrentUserContext(withIndexationContext(IndexationNotification)) />
<withCurrentUserContext(withAppStateContext(UpdateNotification))
dismissable={true}

+ 0
- 1
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PluginRiskConsent-test.tsx.snap View 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"
>

+ 0
- 1
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap View File

@@ -7,7 +7,6 @@ exports[`should render correctly 1`] = `
<div
className="page-simple"
>
<Connect(GlobalMessages) />
<h1
className="text-center huge"
>

+ 3
- 14
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx View File

@@ -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))));

+ 1
- 7
server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx View 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);

+ 2
- 2
server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx View 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}
/>

+ 1
- 2
server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx View 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';


+ 0
- 2
server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap View 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],

+ 1
- 1
server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap View 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",

+ 1
- 1
server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectPageExtension-test.tsx.snap View 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",

+ 1
- 1
server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts View 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';


+ 0
- 4
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx View 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 () => {

server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts → server/sonar-web/src/main/js/app/utils/__tests__/globalMessagesService-test.ts View File

@@ -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 })
);
});

+ 0
- 26
server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts View File

@@ -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));
}

+ 0
- 31
server/sonar-web/src/main/js/app/utils/getStore.ts View File

@@ -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());

server/sonar-web/src/main/js/store/utils/configureStore.ts → server/sonar-web/src/main/js/app/utils/globalMessagesService.ts View File

@@ -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);
}

+ 79
- 85
server/sonar-web/src/main/js/app/utils/startReactApp.tsx View 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
);

+ 0
- 3
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx View 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>

+ 0
- 4
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx View 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)
}));

+ 0
- 4
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap View 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"
>

+ 0
- 2
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx View 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();
});

+ 1
- 1
server/sonar-web/src/main/js/apps/projectDeletion/Form.tsx View 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';

+ 1
- 1
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx View 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';

+ 3
- 1
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx View 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());


+ 1
- 1
server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx View 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';

+ 3
- 1
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx View 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());


+ 1
- 1
server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx View 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';

+ 4
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx View File

@@ -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';

+ 1
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx View 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';

+ 4
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx View 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();

+ 0
- 3
server/sonar-web/src/main/js/apps/sessions/components/Login.tsx View 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>
);
}

+ 3
- 2
server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx View 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();
});
};

+ 3
- 7
server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx View File

@@ -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;

+ 4
- 5
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx View File

@@ -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;

+ 0
- 3
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap View 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>
`;

+ 0
- 1
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap View File

@@ -4,7 +4,6 @@ exports[`should not redirect if logout fails 1`] = `
<div
className="page page-limited"
>
<Connect(GlobalMessages) />
<div
className="text-center"
>

+ 1
- 1
server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx View 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';

+ 1
- 1
server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx View 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';

+ 4
- 2
server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx View 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);


+ 0
- 78
server/sonar-web/src/main/js/components/controls/__tests__/GlobalMessages-test.tsx View File

@@ -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}
/>
);
}

+ 0
- 212
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap View File

@@ -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>
`;

+ 13
- 11
server/sonar-web/src/main/js/helpers/__tests__/error-test.ts View File

@@ -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', () => {

+ 4
- 10
server/sonar-web/src/main/js/helpers/error.ts View File

@@ -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));
}


+ 0
- 5
server/sonar-web/src/main/js/helpers/testMocks.ts View 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,

+ 10
- 18
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx View 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>

+ 0
- 84
server/sonar-web/src/main/js/store/globalMessages.ts View File

@@ -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;
}

+ 0
- 3
server/sonar-web/src/main/js/types/extension.ts View 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;

server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts → server/sonar-web/src/main/js/types/globalMessages.ts View File

@@ -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;
}

+ 1
- 86
server/sonar-web/yarn.lock View 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"

+ 4
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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

Loading…
Cancel
Save