aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2019-04-05 10:58:53 +0200
committerSonarTech <sonartech@sonarsource.com>2019-04-23 20:21:09 +0200
commit4f885e88db04134cea9d500e89f28e6a09b65db2 (patch)
tree35909428587fcc68ccbcb14453f9c1da996769ce /server/sonar-web/src
parent867949e745986a6113051381267edf25ed462968 (diff)
downloadsonarqube-4f885e88db04134cea9d500e89f28e6a09b65db2.tar.gz
sonarqube-4f885e88db04134cea9d500e89f28e6a09b65db2.zip
SONAR-11886 Highlight Hotspots in issues page
* Update see rule button style in issues * Update selected and hover styling of concise issues * Update issues selected and hover styling * Issues type facet don't filter out hotspots by default anymore * Update issue box styling for hotspots * Automatically open severity & standard facet based on the issue type * Add security hotspots newsbox on issues page * Update clear icon and close buttons * Allow to dismiss hotspots newsbox on issues page * Display help tooltip on hotspots entry of the types facet
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap28
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/notifications.css18
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/badges.css7
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/menu.css2
-rw-r--r--server/sonar-web/src/main/js/app/theme.js4
-rw-r--r--server/sonar-web/src/main/js/app/types.d.ts5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssuesPageSelector-test.tsx56
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/__snapshots__/IssuesPageSelector-test.tsx.snap17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts28
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx80
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/TypeFacet-test.tsx101
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/TypeFacet-test.tsx.snap210
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css29
-rw-r--r--server/sonar-web/src/main/js/apps/issues/utils.ts13
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/styles.css2
-rw-r--r--server/sonar-web/src/main/js/components/common/LocationIndex.css2
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetItem.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/ClearIcon.tsx21
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.css38
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueView.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap87
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx17
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap14
-rw-r--r--server/sonar-web/src/main/js/components/ui/NewsBox.css31
-rw-r--r--server/sonar-web/src/main/js/components/ui/NewsBox.tsx50
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/NewsBox-test.tsx42
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap43
-rw-r--r--server/sonar-web/src/main/js/components/ui/buttons.css11
38 files changed, 933 insertions, 146 deletions
diff --git a/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx b/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
index 957a0f78ce9..7617d416ebc 100644
--- a/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
+++ b/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
@@ -20,7 +20,6 @@
import * as React from 'react';
import ClearIcon from '../../../components/icons-components/ClearIcon';
import NotificationIcon from '../../../components/icons-components/NotificationIcon';
-import { sonarcloudBlack500 } from '../../theme';
import { PrismicFeatureNews } from '../../../api/news';
import { differenceInSeconds, parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
@@ -77,7 +76,7 @@ export default class NavLatestNotification extends React.PureComponent<Props> {
</li>
<li className="navbar-latest-notification-dismiss">
<a className="navbar-icon" href="#" onClick={this.handleDismiss}>
- <ClearIcon fill={sonarcloudBlack500} size={10} />
+ <ClearIcon size={12} thin={true} />
</a>
</li>
</>
diff --git a/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx b/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
index b577dd086b0..443987abc97 100644
--- a/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
+++ b/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
@@ -19,10 +19,12 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
+import * as theme from '../../../app/theme';
import ClearIcon from '../../../components/icons-components/ClearIcon';
import DateFormatter from '../../../components/intl/DateFormatter';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Modal from '../../../components/controls/Modal';
+import { ButtonIcon } from '../../../components/ui/buttons';
import { PrismicFeatureNews } from '../../../api/news';
import { differenceInSeconds } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
@@ -44,9 +46,9 @@ export default function NotificationsSidebar(props: Props) {
<div className="notifications-sidebar">
<div className="notifications-sidebar-top">
<h3>{translate('embed_docs.whats_new')}</h3>
- <a className="close" href="#" onClick={props.onClose}>
- <ClearIcon />
- </a>
+ <ButtonIcon className="button-tiny" color={theme.gray80} onClick={props.onClose}>
+ <ClearIcon fill={theme.baseFontColor} size={12} thin={true} />
+ </ButtonIcon>
</div>
<div className="notifications-sidebar-content">
{loading ? (
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
index 83cb55464a0..4e32113a75c 100644
--- a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
@@ -30,8 +30,8 @@ exports[`should render correctly if there are new features, and the user has not
onClick={[Function]}
>
<ClearIcon
- fill="#8a8c8f"
- size={10}
+ size={12}
+ thin={true}
/>
</a>
</li>
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
index 9859b9bbb1d..321569b2d0b 100644
--- a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
@@ -135,13 +135,17 @@ exports[`#NotificationSidebar should render correctly if there are new features
<h3>
embed_docs.whats_new
</h3>
- <a
- className="close"
- href="#"
+ <ButtonIcon
+ className="button-tiny"
+ color="#cdcdcd"
onClick={[MockFunction]}
>
- <ClearIcon />
- </a>
+ <ClearIcon
+ fill="#444"
+ size={12}
+ thin={true}
+ />
+ </ButtonIcon>
</div>
<div
className="notifications-sidebar-content"
@@ -172,13 +176,17 @@ exports[`#NotificationSidebar should render correctly if there are new features
<h3>
embed_docs.whats_new
</h3>
- <a
- className="close"
- href="#"
+ <ButtonIcon
+ className="button-tiny"
+ color="#cdcdcd"
onClick={[MockFunction]}
>
- <ClearIcon />
- </a>
+ <ClearIcon
+ fill="#444"
+ size={12}
+ thin={true}
+ />
+ </ButtonIcon>
</div>
<div
className="notifications-sidebar-content"
diff --git a/server/sonar-web/src/main/js/app/components/notifications/notifications.css b/server/sonar-web/src/main/js/app/components/notifications/notifications.css
index e1760937ab2..2f4b3a42ce3 100644
--- a/server/sonar-web/src/main/js/app/components/notifications/notifications.css
+++ b/server/sonar-web/src/main/js/app/components/notifications/notifications.css
@@ -65,13 +65,14 @@
height: 28px;
background-color: #000;
border-radius: 0 3px 3px 0;
- padding: 9px var(--gridSize) !important;
+ padding: var(--gridSize) 7px !important;
margin-left: 1px;
margin-right: var(--gridSize);
+ color: var(--sonarcloudBlack500) !important;
}
-.navbar-latest-notification-dismiss .navbar-icon:hover path {
- fill: var(--sonarcloudBlack300) !important;
+.navbar-latest-notification-dismiss .navbar-icon:hover {
+ color: var(--sonarcloudBlack300) !important;
}
.notifications-sidebar {
@@ -87,6 +88,9 @@
.notifications-sidebar-top {
position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
padding: calc(2 * var(--gridSize));
border-bottom: 1px solid var(--sonarcloudBlack250);
background-color: var(--sonarcloudBlack100);
@@ -97,14 +101,6 @@
font-size: var(--bigFontSize);
}
-.notifications-sidebar-top .close {
- position: absolute;
- top: calc(2 * var(--gridSize));
- right: calc(2 * var(--gridSize));
- border: 0;
- color: var(--sonarcloudBlack500);
-}
-
.notifications-sidebar-content {
flex: 1 1;
overflow-y: auto;
diff --git a/server/sonar-web/src/main/js/app/styles/components/badges.css b/server/sonar-web/src/main/js/app/styles/components/badges.css
index cdd025107ec..924db837ed5 100644
--- a/server/sonar-web/src/main/js/app/styles/components/badges.css
+++ b/server/sonar-web/src/main/js/app/styles/components/badges.css
@@ -77,7 +77,7 @@ a.badge {
.badge-tiny-height {
height: var(--tinyControlHeight) !important;
- line-height: calc(var(--tinyControlHeight) - 1px) !important;
+ line-height: var(--tinyControlHeight) !important;
}
.badge-new {
@@ -86,6 +86,7 @@ a.badge {
text-transform: uppercase;
background-color: var(--lightBlue);
color: var(--darkBlue);
+ font-weight: 600;
}
.badge-muted {
@@ -115,9 +116,9 @@ a.badge {
}
.badge-danger-light {
- border: 1px solid #ebccd1 !important;
+ border: 1px solid var(--alertBorderError) !important;
border-radius: 3px;
- background-color: #f2dede;
+ background-color: var(--alertBackgroundError);
color: #a94442;
}
diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css
index 376f50ad1ba..137bee29b31 100644
--- a/server/sonar-web/src/main/js/app/styles/components/menu.css
+++ b/server/sonar-web/src/main/js/app/styles/components/menu.css
@@ -40,7 +40,7 @@
.menu > li > span {
display: block;
padding: 4px 16px;
- line-height: 16px;
+ line-height: 14px;
clear: both;
font-weight: normal;
overflow: hidden;
diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js
index 22869b60a77..62053ce4d4d 100644
--- a/server/sonar-web/src/main/js/app/theme.js
+++ b/server/sonar-web/src/main/js/app/theme.js
@@ -64,6 +64,10 @@ module.exports = {
snippetFontColor: '#f0f0f0',
+ //issues
+ issueBgColor: '#ffeaea',
+ hotspotBgColor: '#eeeff4',
+
// alerts
warningIconColor: '#e2bf41',
diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts
index b8e02584b8a..2f0b08d424c 100644
--- a/server/sonar-web/src/main/js/app/types.d.ts
+++ b/server/sonar-web/src/main/js/app/types.d.ts
@@ -225,7 +225,10 @@ declare namespace T {
value: string;
}
- type CurrentUserSettingNames = 'notifications.optOut' | 'notifications.readDate';
+ type CurrentUserSettingNames =
+ | 'notifications.optOut'
+ | 'notifications.readDate'
+ | 'newsbox.dismiss.hotspots';
export interface CustomMeasure {
createdAt?: string;
diff --git a/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx b/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx
index 6fd588d2f3b..7846107689a 100644
--- a/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx
@@ -18,28 +18,20 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { connect } from 'react-redux';
import AppContainer from './components/AppContainer';
-import { RawQuery } from '../../helpers/query';
-import { getCurrentUser, Store } from '../../store/rootReducer';
import { isSonarCloud } from '../../helpers/system';
import { isLoggedIn } from '../../helpers/users';
+import { withCurrentUser } from '../../components/hoc/withCurrentUser';
+import { Location } from '../../components/hoc/withRouter';
-interface StateProps {
+export interface Props {
currentUser: T.CurrentUser;
+ location: Location;
}
-interface Props extends StateProps {
- location: { pathname: string; query: RawQuery };
-}
-
-function IssuesPage({ currentUser, location }: Props) {
+export function IssuesPage({ currentUser, location }: Props) {
const myIssues = (isLoggedIn(currentUser) && isSonarCloud()) || undefined;
return <AppContainer location={location} myIssues={myIssues} />;
}
-const stateToProps = (state: Store) => ({
- currentUser: getCurrentUser(state)
-});
-
-export default connect(stateToProps)(IssuesPage);
+export default withCurrentUser(IssuesPage);
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesPageSelector-test.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesPageSelector-test.tsx
new file mode 100644
index 00000000000..65abdcd1186
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesPageSelector-test.tsx
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { IssuesPage, Props } from '../IssuesPageSelector';
+import { mockLocation, mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
+import { isSonarCloud } from '../../../helpers/system';
+
+jest.mock('../../../helpers/system', () => ({ isSonarCloud: jest.fn().mockReturnValue(false) }));
+
+it('should render normal issues page', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(
+ shallowRender({ currentUser: mockLoggedInUser() })
+ .find('Connect(IssuesAppContainer)')
+ .prop('myIssues')
+ ).toBeFalsy();
+ (isSonarCloud as jest.Mock).mockReturnValueOnce(true);
+ expect(
+ shallowRender()
+ .find('Connect(IssuesAppContainer)')
+ .prop('myIssues')
+ ).toBeFalsy();
+});
+
+it('should render my issues page', () => {
+ (isSonarCloud as jest.Mock).mockReturnValueOnce(true);
+ expect(
+ shallowRender({ currentUser: mockLoggedInUser() })
+ .find('Connect(IssuesAppContainer)')
+ .prop('myIssues')
+ ).toBeTruthy();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <IssuesPage currentUser={mockCurrentUser()} location={mockLocation()} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/__snapshots__/IssuesPageSelector-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/__tests__/__snapshots__/IssuesPageSelector-test.tsx.snap
new file mode 100644
index 00000000000..cc2de94d3fb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/__snapshots__/IssuesPageSelector-test.tsx.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render normal issues page 1`] = `
+<Connect(IssuesAppContainer)
+ location={
+ Object {
+ "action": "PUSH",
+ "hash": "",
+ "key": "key",
+ "pathname": "/path",
+ "query": Object {},
+ "search": "",
+ "state": Object {},
+ }
+ }
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
index 5b3528a60b2..dc06b443cc2 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { scrollToElement } from '../../../helpers/scrolling';
-import { scrollToIssue } from '../utils';
+import { shouldOpenSeverityFacet, shouldOpenStandardFacet, scrollToIssue } from '../utils';
jest.mock('../../../helpers/scrolling', () => ({
scrollToElement: jest.fn()
@@ -55,3 +55,29 @@ describe('scrollToIssue', () => {
);
});
});
+
+describe('shouldOpenStandardFacet', () => {
+ it('should open standard facet', () => {
+ expect(shouldOpenStandardFacet(['VULNERABILITY'])).toBe(true);
+ expect(shouldOpenStandardFacet(['SECURITY_HOTSPOT'])).toBe(true);
+ expect(shouldOpenStandardFacet(['VULNERABILITY', 'SECURITY_HOTSPOT'])).toBe(true);
+ });
+
+ it('should NOT open standard facet', () => {
+ expect(shouldOpenStandardFacet([])).toBe(false);
+ expect(shouldOpenStandardFacet(['BUGS'])).toBe(false);
+ expect(shouldOpenStandardFacet(['BUGS', 'SECURITY_HOTSPOT'])).toBe(false);
+ });
+});
+
+describe('shouldDisableSeverityFacet', () => {
+ it('should open severity facet', () => {
+ expect(shouldOpenSeverityFacet([])).toBe(true);
+ expect(shouldOpenSeverityFacet(['SECURITY'])).toBe(true);
+ expect(shouldOpenSeverityFacet(['BUGS', 'SECURITY_HOTSPOT'])).toBe(true);
+ });
+
+ it('should NOT open severity facet', () => {
+ expect(shouldOpenSeverityFacet(['SECURITY_HOTSPOT'])).toBe(false);
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
index 8fe9d110644..159bff63ae6 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
@@ -46,12 +46,14 @@ import {
RawFacet,
ReferencedComponent,
ReferencedLanguage,
+ ReferencedRule,
ReferencedUser,
saveMyIssues,
serializeQuery,
- STANDARDS,
- ReferencedRule,
- scrollToIssue
+ scrollToIssue,
+ shouldOpenSeverityFacet,
+ shouldOpenStandardFacet,
+ STANDARDS
} from '../utils';
import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
import { Alert } from '../../../components/ui/Alert';
@@ -144,6 +146,7 @@ export class App extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
+ const query = parseQuery(props.location.query);
this.state = {
bulkChangeModal: false,
checked: [],
@@ -154,8 +157,12 @@ export class App extends React.PureComponent<Props, State> {
loadingMore: false,
locationsNavigator: false,
myIssues: props.myIssues || areMyIssuesSelected(props.location.query),
- openFacets: { severities: true, types: true },
- query: parseQuery(props.location.query),
+ openFacets: {
+ severities: shouldOpenSeverityFacet(query.types),
+ standards: shouldOpenStandardFacet(query.types),
+ types: true
+ },
+ query,
referencedComponentsById: {},
referencedComponentsByKey: {},
referencedLanguages: {},
@@ -644,7 +651,6 @@ export class App extends React.PureComponent<Props, State> {
};
handleFilterChange = (changes: Partial<Query>) => {
- this.setState({ loading: true });
this.props.router.push({
pathname: this.props.location.pathname,
query: {
@@ -654,6 +660,14 @@ export class App extends React.PureComponent<Props, State> {
myIssues: this.state.myIssues ? 'true' : undefined
}
});
+ this.setState(({ openFacets }) => ({
+ loading: true,
+ openFacets: {
+ ...openFacets,
+ severities: openFacets.severities || shouldOpenSeverityFacet(changes.types),
+ standards: openFacets.standards || shouldOpenStandardFacet(changes.types)
+ }
+ }));
};
handleMyIssuesChange = (myIssues: boolean) => {
diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
index 29a05dae7dd..4fc0a026b2a 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
@@ -90,4 +90,4 @@ const mapDispatchToProps = { fetchIssues: fetchIssues as any } as DispatchProps;
export default connect(
mapStateToProps,
mapDispatchToProps
-)(lazyLoad(() => import('./App')));
+)(lazyLoad(() => import('./App'), 'IssuesAppContainer'));
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
index d978c70f208..da45c568f93 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
@@ -131,6 +131,38 @@ it('should fetch issues for component', async () => {
expect(wrapper.state('issues')).toHaveLength(6);
});
+it('should display the right facets open', () => {
+ expect(
+ shallowRender({
+ location: mockLocation({ query: { types: 'SECURITY_HOTSPOT' } })
+ }).state('openFacets')
+ ).toEqual({ severities: false, standards: true, types: true });
+ expect(
+ shallowRender({
+ location: mockLocation({ query: { types: 'VULNERABILITY,SECURITY_HOTSPOT' } })
+ }).state('openFacets')
+ ).toEqual({ severities: true, standards: true, types: true });
+ expect(
+ shallowRender({
+ location: mockLocation({ query: { types: 'BUGS,SECURITY_HOTSPOT' } })
+ }).state('openFacets')
+ ).toEqual({ severities: true, standards: false, types: true });
+});
+
+it('should correctly handle filter changes', () => {
+ const push = jest.fn();
+ const instance = shallowRender({ router: mockRouter({ push }) }).instance();
+ instance.setState({ openFacets: { types: true } });
+ instance.handleFilterChange({ types: ['VULNERABILITY'] });
+ expect(instance.state.openFacets).toEqual({ types: true, severities: true, standards: true });
+ expect(push).toBeCalled();
+ instance.handleFilterChange({ types: ['BUGS'] });
+ expect(instance.state.openFacets).toEqual({ types: true, severities: true, standards: true });
+ instance.setState({ openFacets: { types: true } });
+ instance.handleFilterChange({ types: ['SECURITY_HOTSPOT'] });
+ expect(instance.state.openFacets).toEqual({ types: true, severities: false, standards: true });
+});
+
it('should fetch issues until defined', async () => {
const mockDone = (_lastIssue: T.Issue, paging: T.Paging) =>
paging.total <= paging.pageIndex * paging.pageSize;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
index f315d310387..388dccd9d57 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
@@ -18,28 +18,36 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { connect } from 'react-redux';
+import { Link } from 'react-router';
import { orderBy, without } from 'lodash';
-import { formatFacetStat, Query } from '../utils';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
import FacetBox from '../../../components/facet/FacetBox';
import FacetHeader from '../../../components/facet/FacetHeader';
import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
-import { translate } from '../../../helpers/l10n';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
+import NewsBox from '../../../components/ui/NewsBox';
+import { formatFacetStat, Query } from '../utils';
+import { getCurrentUser, getCurrentUserSetting, Store } from '../../../store/rootReducer';
+import { setCurrentUserSetting } from '../../../store/users';
import { ISSUE_TYPES } from '../../../helpers/constants';
+import { translate } from '../../../helpers/l10n';
interface Props {
fetching: boolean;
+ newsBoxDismissHotspots?: boolean;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
+ setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
stats: T.Dict<number> | undefined;
types: string[];
}
-export default class TypeFacet extends React.PureComponent<Props> {
+export class TypeFacet extends React.PureComponent<Props> {
property = 'types';
static defaultProps = {
@@ -68,19 +76,17 @@ export default class TypeFacet extends React.PureComponent<Props> {
this.props.onChange({ [this.property]: [] });
};
+ handleDismiss = () => {
+ this.props.setCurrentUserSetting({ key: 'newsbox.dismiss.hotspots', value: 'true' });
+ };
+
getStat(type: string) {
const { stats } = this.props;
return stats ? stats[type] : undefined;
}
isFacetItemActive(type: string) {
- const { types } = this.props;
- return (
- // type is selected explicitly
- types.includes(type) ||
- // bugs, vulnerabilities and code smells are selected implicitly by default
- (types.length === 0 && ['BUG', 'VULNERABILITY', 'CODE_SMELL'].includes(type))
- );
+ return this.props.types.includes(type);
}
renderItem = (type: string) => {
@@ -93,22 +99,39 @@ export default class TypeFacet extends React.PureComponent<Props> {
disabled={stat === 0 && !active}
key={type}
name={
- <span>
- <IssueTypeIcon query={type} /> {translate('issue.type', type)}
+ <span className="display-flex-center">
+ <IssueTypeIcon className="little-spacer-right" query={type} />{' '}
+ {translate('issue.type', type)}
+ {type === 'SECURITY_HOTSPOT' && this.props.newsBoxDismissHotspots && (
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay={
+ <>
+ <p>{translate('issues.hotspots.helper')}</p>
+ <hr className="spacer-top spacer-bottom" />
+ <Link target="_blank" to="/documentation/user-guide/security-hotspots/">
+ {translate('learn_more')}
+ </Link>
+ </>
+ }
+ />
+ )}
</span>
}
onClick={this.handleItemClick}
stat={formatFacetStat(stat)}
- tooltip={translate('issue.type', type)}
value={type}
/>
);
};
render() {
- const { types, stats = {} } = this.props;
+ const { newsBoxDismissHotspots, types, stats = {} } = this.props;
const values = types.map(type => translate('issue.type', type));
+ const showHotspotNewsBox =
+ types.includes('SECURITY_HOTSPOT') || (types.length === 0 && stats['SECURITY_HOTSPOT'] > 0);
+
return (
<FacetBox property={this.property}>
<FacetHeader
@@ -124,6 +147,18 @@ export default class TypeFacet extends React.PureComponent<Props> {
{this.props.open && (
<>
<FacetItemsList>{ISSUE_TYPES.map(this.renderItem)}</FacetItemsList>
+ {!newsBoxDismissHotspots && showHotspotNewsBox && (
+ <NewsBox
+ onClose={this.handleDismiss}
+ title={translate('issue.type.SECURITY_HOTSPOT.plural')}>
+ <p>{translate('issues.hotspots.helper')}</p>
+ <p className="text-right spacer-top">
+ <Link target="_blank" to="/documentation/user-guide/security-hotspots/">
+ {translate('learn_more')}
+ </Link>
+ </p>
+ </NewsBox>
+ )}
<MultipleSelectionHint options={Object.keys(stats).length} values={types.length} />
</>
)}
@@ -131,3 +166,18 @@ export default class TypeFacet extends React.PureComponent<Props> {
);
}
}
+
+const mapStateToProps = (state: Store) => ({
+ newsBoxDismissHotspots:
+ !getCurrentUser(state).isLoggedIn ||
+ getCurrentUserSetting(state, 'newsbox.dismiss.hotspots') === 'true'
+});
+
+const mapDispatchToProps = {
+ setCurrentUserSetting
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TypeFacet);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/TypeFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/TypeFacet-test.tsx
new file mode 100644
index 00000000000..25e240a7227
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/TypeFacet-test.tsx
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { TypeFacet } from '../TypeFacet';
+import { click } from '../../../../helpers/testUtils';
+
+it('should render open by default', () => {
+ expect(shallowRender({ types: ['VULNERABILITY', 'CODE_SMELL'] })).toMatchSnapshot();
+});
+
+it('should toggle type facet', () => {
+ const onToggle = jest.fn();
+ const wrapper = shallowRender({ onToggle });
+ click(wrapper.children('FacetHeader'));
+ expect(onToggle).toBeCalledWith('types');
+});
+
+it('should clear types facet', () => {
+ const onChange = jest.fn();
+ const wrapper = shallowRender({ onChange, types: ['BUGS'] });
+ wrapper.children('FacetHeader').prop<Function>('onClear')();
+ expect(onChange).toBeCalledWith({ types: [] });
+});
+
+it('should select a type', () => {
+ const onChange = jest.fn();
+ const wrapper = shallowRender({ onChange });
+ clickAndCheck('CODE_SMELL');
+ clickAndCheck('VULNERABILITY', true, ['CODE_SMELL', 'VULNERABILITY']);
+ clickAndCheck('SECURITY_HOTSPOT');
+
+ function clickAndCheck(type: string, multiple = false, expected = [type]) {
+ wrapper
+ .find(`FacetItemsList`)
+ .find(`FacetItem[value="${type}"]`)
+ .prop<Function>('onClick')(type, multiple);
+ expect(onChange).lastCalledWith({ types: expected });
+ wrapper.setProps({ types: expected });
+ }
+});
+
+it('should display the hotspot newsbox', () => {
+ expect(shallowRender({ types: ['SECURITY_HOTSPOT'] }).find('NewsBox')).toMatchSnapshot();
+ expect(
+ shallowRender({ types: [] })
+ .find('NewsBox')
+ .exists()
+ ).toBe(true);
+});
+
+it('should display the hotspot tooltip helper only', () => {
+ let wrapper = shallowRender({ types: ['SECURITY_HOTSPOT'], newsBoxDismissHotspots: true });
+ expect(wrapper.find('NewsBox').exists()).toBe(false);
+ expect(
+ wrapper
+ .find(`FacetItemsList`)
+ .find(`FacetItem[value="SECURITY_HOTSPOT"]`)
+ .prop('name')
+ ).toMatchSnapshot();
+
+ wrapper = shallowRender({ types: ['BUGS'], newsBoxDismissHotspots: true });
+ expect(wrapper.find('NewsBox').exists()).toBe(false);
+ expect(
+ wrapper
+ .find(`FacetItemsList`)
+ .find(`FacetItem[value="SECURITY_HOTSPOT"]`)
+ .prop('name')
+ ).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<TypeFacet['props']> = {}) {
+ return shallow(
+ <TypeFacet
+ fetching={false}
+ onChange={jest.fn()}
+ onToggle={jest.fn()}
+ setCurrentUserSetting={jest.fn()}
+ stats={{ BUG: 0, VULNERABILITY: 2, CODE_SMELL: 5, SECURITY_HOTSPOT: 1 }}
+ types={[]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
index 1eafde6ef86..5d74e0af9d2 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
@@ -2,7 +2,7 @@
exports[`should render facets for developer 1`] = `
Array [
- "TypeFacet",
+ "Connect(TypeFacet)",
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
@@ -20,7 +20,7 @@ Array [
exports[`should render facets for directory 1`] = `
Array [
- "TypeFacet",
+ "Connect(TypeFacet)",
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
@@ -37,7 +37,7 @@ Array [
exports[`should render facets for global page 1`] = `
Array [
- "TypeFacet",
+ "Connect(TypeFacet)",
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
@@ -54,7 +54,7 @@ Array [
exports[`should render facets for module 1`] = `
Array [
- "TypeFacet",
+ "Connect(TypeFacet)",
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
@@ -72,7 +72,7 @@ Array [
exports[`should render facets for project 1`] = `
Array [
- "TypeFacet",
+ "Connect(TypeFacet)",
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
@@ -90,7 +90,7 @@ Array [
exports[`should render facets when my issues are selected 1`] = `
Array [
- "TypeFacet",
+ "Connect(TypeFacet)",
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/TypeFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/TypeFacet-test.tsx.snap
new file mode 100644
index 00000000000..4f6c78ff04c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/TypeFacet-test.tsx.snap
@@ -0,0 +1,210 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the hotspot newsbox 1`] = `
+<NewsBox
+ onClose={[Function]}
+ title="issue.type.SECURITY_HOTSPOT.plural"
+>
+ <p>
+ issues.hotspots.helper
+ </p>
+ <p
+ className="text-right spacer-top"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/user-guide/security-hotspots/"
+ >
+ learn_more
+ </Link>
+ </p>
+</NewsBox>
+`;
+
+exports[`should display the hotspot tooltip helper only 1`] = `
+<span
+ className="display-flex-center"
+>
+ <IssueTypeIcon
+ className="little-spacer-right"
+ query="SECURITY_HOTSPOT"
+ />
+
+ issue.type.SECURITY_HOTSPOT
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay={
+ <React.Fragment>
+ <p>
+ issues.hotspots.helper
+ </p>
+ <hr
+ className="spacer-top spacer-bottom"
+ />
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/user-guide/security-hotspots/"
+ >
+ learn_more
+ </Link>
+ </React.Fragment>
+ }
+ />
+</span>
+`;
+
+exports[`should display the hotspot tooltip helper only 2`] = `
+<span
+ className="display-flex-center"
+>
+ <IssueTypeIcon
+ className="little-spacer-right"
+ query="SECURITY_HOTSPOT"
+ />
+
+ issue.type.SECURITY_HOTSPOT
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay={
+ <React.Fragment>
+ <p>
+ issues.hotspots.helper
+ </p>
+ <hr
+ className="spacer-top spacer-bottom"
+ />
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/user-guide/security-hotspots/"
+ >
+ learn_more
+ </Link>
+ </React.Fragment>
+ }
+ />
+</span>
+`;
+
+exports[`should render open by default 1`] = `
+<FacetBox
+ property="types"
+>
+ <FacetHeader
+ clearLabel="reset_verb"
+ name="issues.facet.types"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+ values={
+ Array [
+ "issue.type.VULNERABILITY",
+ "issue.type.CODE_SMELL",
+ ]
+ }
+ />
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <FacetItemsList>
+ <FacetItem
+ active={false}
+ disabled={true}
+ halfWidth={false}
+ key="BUG"
+ loading={false}
+ name={
+ <span
+ className="display-flex-center"
+ >
+ <IssueTypeIcon
+ className="little-spacer-right"
+ query="BUG"
+ />
+
+ issue.type.BUG
+ </span>
+ }
+ onClick={[Function]}
+ stat={0}
+ value="BUG"
+ />
+ <FacetItem
+ active={true}
+ disabled={false}
+ halfWidth={false}
+ key="VULNERABILITY"
+ loading={false}
+ name={
+ <span
+ className="display-flex-center"
+ >
+ <IssueTypeIcon
+ className="little-spacer-right"
+ query="VULNERABILITY"
+ />
+
+ issue.type.VULNERABILITY
+ </span>
+ }
+ onClick={[Function]}
+ stat="2"
+ value="VULNERABILITY"
+ />
+ <FacetItem
+ active={true}
+ disabled={false}
+ halfWidth={false}
+ key="CODE_SMELL"
+ loading={false}
+ name={
+ <span
+ className="display-flex-center"
+ >
+ <IssueTypeIcon
+ className="little-spacer-right"
+ query="CODE_SMELL"
+ />
+
+ issue.type.CODE_SMELL
+ </span>
+ }
+ onClick={[Function]}
+ stat="5"
+ value="CODE_SMELL"
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ key="SECURITY_HOTSPOT"
+ loading={false}
+ name={
+ <span
+ className="display-flex-center"
+ >
+ <IssueTypeIcon
+ className="little-spacer-right"
+ query="SECURITY_HOTSPOT"
+ />
+
+ issue.type.SECURITY_HOTSPOT
+ </span>
+ }
+ onClick={[Function]}
+ stat="1"
+ value="SECURITY_HOTSPOT"
+ />
+ </FacetItemsList>
+ <MultipleSelectionHint
+ options={4}
+ values={2}
+ />
+</FacetBox>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
index 9964171042b..a6687a9e5b0 100644
--- a/server/sonar-web/src/main/js/apps/issues/styles.css
+++ b/server/sonar-web/src/main/js/apps/issues/styles.css
@@ -50,25 +50,25 @@
}
.concise-issue-component {
- margin-top: 16px;
- margin-bottom: 4px;
- padding-left: 8px;
- padding-right: 8px;
+ margin-top: calc(var(--gridSize) * 2);
+ margin-bottom: calc(var(--gridSize) / 2);
+ padding-left: var(--gridSize);
+ padding-right: var(--gridSize);
}
.concise-issue-box {
position: relative;
z-index: var(--belowNormalZIndex);
- margin-bottom: 4px;
- padding: 8px;
- border: 1px solid var(--barBorderColor);
+ margin-bottom: calc(var(--gridSize) / 2);
+ padding: calc(var(--gridSize) - 1px);
+ border: 2px solid var(--barBorderColor);
background-color: #fff;
cursor: pointer;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.concise-issue-box:hover {
- background-color: #ffeaea;
+ border: 2px dashed var(--blue);
}
.concise-issue-box:focus {
@@ -77,8 +77,7 @@
.concise-issue-box.selected {
z-index: var(--normalZIndex);
- border-color: #dd4040;
- background-color: #ffeaea;
+ border: 2px solid var(--blue);
cursor: default;
}
@@ -97,7 +96,7 @@
}
.concise-issue-box-attributes {
- margin-top: 8px;
+ margin-top: var(--gridSize);
line-height: 16px;
font-size: var(--smallFontSize);
}
@@ -166,7 +165,7 @@
border: 1px solid #d18582;
border-radius: 100%;
box-sizing: border-box;
- background-color: #ffeaea;
+ background-color: var(--issueBgColor);
}
.concise-issue-location-file-circle-multiple {
@@ -223,12 +222,12 @@
}
.issues .issue {
- border: 1px solid transparent;
+ border: 2px solid transparent;
cursor: pointer;
}
.issues .issue:hover {
- border: 1px dashed #dd4040;
+ border: 2px dashed var(--blue);
transition: all 0.3s ease;
}
@@ -268,7 +267,7 @@
.issues-predefined-periods .search-navigator-facet {
width: auto;
- margin-right: 4px;
+ margin-right: calc(var(--gridSize) / 2);
}
.bulk-change-radio-button {
diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts
index 528be4778af..0b9b5e3bb0c 100644
--- a/server/sonar-web/src/main/js/apps/issues/utils.ts
+++ b/server/sonar-web/src/main/js/apps/issues/utils.ts
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { difference } from 'lodash';
import { searchMembers } from '../../api/organizations';
import { searchUsers } from '../../api/users';
import { formatMeasure } from '../../helpers/measures';
@@ -275,3 +276,15 @@ export function scrollToIssue(issue: string, smooth = true) {
scrollToElement(element, { topOffset: 250, bottomOffset: 100, smooth });
}
}
+
+export function shouldOpenStandardFacet(types?: string[]) {
+ return Boolean(
+ types &&
+ types.length > 0 &&
+ difference(types, ['VULNERABILITY', 'SECURITY_HOTSPOT']).length === 0
+ );
+}
+
+export function shouldOpenSeverityFacet(types?: string[]) {
+ return !types || !(types.length === 1 && types[0] === 'SECURITY_HOTSPOT');
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
index b0483f272d0..30d184d481f 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
@@ -34,7 +34,7 @@ export default function PluginChangeLogButton({ release, update }: Props) {
<Dropdown
className="display-inline-block little-spacer-left"
overlay={<PluginChangeLog release={release} update={update} />}>
- <ButtonLink className="js-changelog issue-rule">
+ <ButtonLink className="js-changelog">
<EllipsisIcon />
</ButtonLink>
</Dropdown>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
index 6999c412acd..5b3843f3438 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css
+++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
@@ -531,7 +531,7 @@
line-height: 18px;
height: 18px;
box-sizing: border-box;
- background-color: #ffeaea;
+ background-color: var(--issueBgColor);
transition: background-color 0.3s ease;
}
diff --git a/server/sonar-web/src/main/js/components/common/LocationIndex.css b/server/sonar-web/src/main/js/components/common/LocationIndex.css
index 3c8fb3a47ce..e9ddf9c0e47 100644
--- a/server/sonar-web/src/main/js/components/common/LocationIndex.css
+++ b/server/sonar-web/src/main/js/components/common/LocationIndex.css
@@ -38,7 +38,7 @@
}
.location-index.muted {
- background-color: #ccc;
+ background-color: var(--gray80);
}
.location-index.is-leading {
diff --git a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
index 1445de30906..e9e25e70613 100644
--- a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
+++ b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
@@ -29,7 +29,7 @@ export interface Props {
onClick: (x: string, multiple?: boolean) => void;
stat?: React.ReactNode;
/** Textual version of `name` */
- tooltip: string;
+ tooltip?: string;
value: string;
}
diff --git a/server/sonar-web/src/main/js/components/icons-components/ClearIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/ClearIcon.tsx
index 551fae63679..0e2f39e113d 100644
--- a/server/sonar-web/src/main/js/components/icons-components/ClearIcon.tsx
+++ b/server/sonar-web/src/main/js/components/icons-components/ClearIcon.tsx
@@ -20,13 +20,24 @@
import * as React from 'react';
import Icon, { IconProps } from './Icon';
-export default function ClearIcon({ className, fill = 'currentColor', size }: IconProps) {
+interface Props extends IconProps {
+ thin?: boolean;
+}
+
+export default function ClearIcon({ className, fill = 'currentColor', size, thin }: Props) {
return (
<Icon className={className} size={size}>
- <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 }}
- />
+ {thin ? (
+ <path
+ d="M14 3.209l-1.209-1.209-4.791 4.791-4.791-4.791-1.209 1.209 4.791 4.791-4.791 4.791 1.209 1.209 4.791-4.791 4.791 4.791 1.209-1.209-4.791-4.791z"
+ style={{ fill }}
+ />
+ ) : (
+ <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 }}
+ />
+ )}
</Icon>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.css b/server/sonar-web/src/main/js/components/issue/Issue.css
index 35af21af2c9..71c39dd6915 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.css
+++ b/server/sonar-web/src/main/js/components/issue/Issue.css
@@ -19,16 +19,19 @@
*/
.issue {
position: relative;
- padding-top: 8px;
- padding-bottom: 8px;
- background-color: #ffeaea;
- box-shadow: inset 0px 0px 0px 1px #ffeaea;
- transition: all 0.3s ease, border 0 ease;
+ padding-top: var(--gridSize);
+ padding-bottom: var(--gridSize);
+ background-color: var(--issueBgColor);
+ transition: all 0.3s ease, border 0s ease;
+}
+
+.issue.hotspot {
+ background-color: var(--hotspotBgColor);
}
.issue.selected {
box-shadow: none;
- border: 1px solid #dd4040 !important;
+ border: 2px solid var(--blue) !important;
}
.issue + .issue,
@@ -55,27 +58,10 @@
flex-grow: 1;
padding-left: var(--gridSize);
padding-right: var(--gridSize);
- line-height: 1.5;
+ line-height: 18px;
font-size: var(--baseFontSize);
font-weight: 600;
text-overflow: ellipsis;
- overflow: hidden;
-}
-
-.issue-message .button-link {
- height: 16px;
-}
-
-.issue-rule {
- vertical-align: top;
- margin-top: 2px;
- padding: 0 3px;
- background: rgba(75, 159, 213, 0.3);
- opacity: 0.5;
-}
-
-.issue-rule:hover {
- background: rgba(75, 159, 213, 0.3);
}
.issue-actions {
@@ -215,7 +201,7 @@
.issue-checkbox-container {
display: none;
position: absolute;
- width: 29px;
+ width: 28px;
top: 0;
bottom: 0;
left: 0;
@@ -227,7 +213,7 @@
}
.issue:not(.selected) .location-index {
- background-color: #ccc;
+ background-color: var(--gray80);
}
.issue .menu:not(.issues-similar-issues-menu):not(.issue-changelog) {
diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.tsx b/server/sonar-web/src/main/js/components/issue/IssueView.tsx
index f5cb65b0241..21a0eaee419 100644
--- a/server/sonar-web/src/main/js/components/issue/IssueView.tsx
+++ b/server/sonar-web/src/main/js/components/issue/IssueView.tsx
@@ -70,6 +70,7 @@ export default class IssueView extends React.PureComponent<Props> {
const hasCheckbox = this.props.onCheck != null;
const issueClass = classNames('issue', {
+ hotspot: issue.type === 'SECURITY_HOTSPOT',
'issue-with-checkbox': hasCheckbox,
selected: this.props.selected
});
diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx
index 59795351516..e7470f7dbaf 100644
--- a/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx
+++ b/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx
@@ -22,9 +22,14 @@ import { shallow } from 'enzyme';
import IssueView from '../IssueView';
import { mockIssue } from '../../../helpers/testMocks';
-it('should render correctly', () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
+it('should render issues correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should render hotspots correctly', () => {
+ expect(
+ shallowRender({ issue: mockIssue(false, { type: 'SECURITY_HOTSPOT' }) })
+ ).toMatchSnapshot();
});
function shallowRender(props: Partial<IssueView['props']> = {}) {
diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap
index b76291398cd..159ead79b11 100644
--- a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap
@@ -1,6 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly 1`] = `
+exports[`should render hotspots correctly 1`] = `
+<div
+ className="issue hotspot selected"
+ data-issue="AVsae-CQS-9G3txfbFN2"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+>
+ <IssueTitleBar
+ issue={
+ Object {
+ "actions": Array [],
+ "component": "main.js",
+ "componentLongName": "main.js",
+ "componentQualifier": "FIL",
+ "componentUuid": "foo1234",
+ "creationDate": "2017-03-01T09:36:01+0100",
+ "flows": Array [],
+ "fromHotspot": false,
+ "key": "AVsae-CQS-9G3txfbFN2",
+ "line": 25,
+ "message": "Reduce the number of conditional operators (4) used in the expression",
+ "organization": "myorg",
+ "project": "myproject",
+ "projectKey": "foo",
+ "projectName": "Foo",
+ "projectOrganization": "org",
+ "rule": "javascript:S1067",
+ "ruleName": "foo",
+ "secondaryLocations": Array [],
+ "severity": "MAJOR",
+ "status": "OPEN",
+ "textRange": Object {
+ "endLine": 26,
+ "endOffset": 15,
+ "startLine": 25,
+ "startOffset": 0,
+ },
+ "transitions": Array [],
+ "type": "SECURITY_HOTSPOT",
+ }
+ }
+ togglePopup={[MockFunction]}
+ />
+ <IssueActionsBar
+ issue={
+ Object {
+ "actions": Array [],
+ "component": "main.js",
+ "componentLongName": "main.js",
+ "componentQualifier": "FIL",
+ "componentUuid": "foo1234",
+ "creationDate": "2017-03-01T09:36:01+0100",
+ "flows": Array [],
+ "fromHotspot": false,
+ "key": "AVsae-CQS-9G3txfbFN2",
+ "line": 25,
+ "message": "Reduce the number of conditional operators (4) used in the expression",
+ "organization": "myorg",
+ "project": "myproject",
+ "projectKey": "foo",
+ "projectName": "Foo",
+ "projectOrganization": "org",
+ "rule": "javascript:S1067",
+ "ruleName": "foo",
+ "secondaryLocations": Array [],
+ "severity": "MAJOR",
+ "status": "OPEN",
+ "textRange": Object {
+ "endLine": 26,
+ "endOffset": 15,
+ "startLine": 25,
+ "startOffset": 0,
+ },
+ "transitions": Array [],
+ "type": "SECURITY_HOTSPOT",
+ }
+ }
+ onAssign={[MockFunction]}
+ onChange={[MockFunction]}
+ togglePopup={[MockFunction]}
+ />
+</div>
+`;
+
+exports[`should render issues correctly 1`] = `
<div
className="issue selected"
data-issue="AVsae-CQS-9G3txfbFN2"
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx
index 51586026298..b5e405385d2 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx
@@ -18,9 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import EllipsisIcon from '../../icons-components/EllipsisIcon';
import Tooltip from '../../controls/Tooltip';
-import { ButtonLink } from '../../ui/buttons';
+import { Button } from '../../ui/buttons';
import { WorkspaceContextShape } from '../../workspace/context';
import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -44,24 +43,24 @@ export default class IssueMessage extends React.PureComponent<Props> {
render() {
return (
<div className="issue-message">
- {this.props.message}
- <ButtonLink
+ <span className="little-spacer-right">{this.props.message}</span>
+ <Button
aria-label={translate('issue.rule_details')}
- className="issue-rule little-spacer-left"
+ className="button button-grey button-tiny spacer-right vertical-top"
onClick={this.handleClick}>
- <EllipsisIcon />
- </ButtonLink>
+ {translate('issue.see_rule')}
+ </Button>
{this.props.engine && (
<Tooltip
overlay={translateWithParameters('issue.from_external_rule_engine', this.props.engine)}>
- <div className="outline-badge badge-tiny-height spacer-left vertical-text-top">
+ <div className="outline-badge badge-tiny-height spacer-right vertical-top">
{this.props.engine}
</div>
</Tooltip>
)}
{this.props.manualVulnerability && (
<Tooltip overlay={translate('issue.manual_vulnerability.description')}>
- <div className="outline-badge badge-tiny-height spacer-left vertical-text-top">
+ <div className="outline-badge badge-tiny-height spacer-right vertical-top">
{translate('issue.manual_vulnerability')}
</div>
</Tooltip>
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap
index 2c54ba0f2af..e358fb0ed68 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap
@@ -4,13 +4,17 @@ exports[`should render with the message and a link to open the rule 1`] = `
<div
className="issue-message"
>
- Reduce the number of conditional operators (4) used in the expression
- <ButtonLink
+ <span
+ className="little-spacer-right"
+ >
+ Reduce the number of conditional operators (4) used in the expression
+ </span>
+ <Button
aria-label="issue.rule_details"
- className="issue-rule little-spacer-left"
+ className="button button-grey button-tiny spacer-right vertical-top"
onClick={[Function]}
>
- <EllipsisIcon />
- </ButtonLink>
+ issue.see_rule
+ </Button>
</div>
`;
diff --git a/server/sonar-web/src/main/js/components/ui/NewsBox.css b/server/sonar-web/src/main/js/components/ui/NewsBox.css
new file mode 100644
index 00000000000..5db753fd211
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/NewsBox.css
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+.news-box {
+ border: 1px solid var(--alertBorderInfo);
+ border-radius: 2px;
+ background-color: var(--veryLightBlue);
+ padding: var(--gridSize);
+}
+
+.news-box-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
diff --git a/server/sonar-web/src/main/js/components/ui/NewsBox.tsx b/server/sonar-web/src/main/js/components/ui/NewsBox.tsx
new file mode 100644
index 00000000000..8fe54f49495
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/NewsBox.tsx
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { ButtonIcon } from './buttons';
+import ClearIcon from '../icons-components/ClearIcon';
+import * as theme from '../../app/theme';
+import { translate } from '../../helpers/l10n';
+import './NewsBox.css';
+
+export interface Props {
+ children: React.ReactNode;
+ className?: string;
+ onClose: () => void;
+ title: string;
+}
+
+export default function NewsBox({ children, className, onClose, title }: Props) {
+ return (
+ <div className={classNames('news-box', className)} role="alert">
+ <div className="news-box-header">
+ <div className="display-flex-center">
+ <span className="badge badge-new spacer-right">{translate('new')}</span>
+ <strong>{title}</strong>
+ </div>
+ <ButtonIcon className="button-tiny" color={theme.gray80} onClick={onClose}>
+ <ClearIcon fill={theme.baseFontColor} size={12} thin={true} />
+ </ButtonIcon>
+ </div>
+ <div className="big-spacer-top note">{children}</div>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/NewsBox-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/NewsBox-test.tsx
new file mode 100644
index 00000000000..e6758a4ec54
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/NewsBox-test.tsx
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import NewsBox, { Props } from '../NewsBox';
+import { click } from '../../../helpers/testUtils';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should call onClose', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender({ onClose });
+ click(wrapper.find('ButtonIcon'));
+ expect(onClose).toBeCalled();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <NewsBox onClose={jest.fn()} title="title" {...props}>
+ <div>description</div>
+ </NewsBox>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap
new file mode 100644
index 00000000000..068bf719d63
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="news-box"
+ role="alert"
+>
+ <div
+ className="news-box-header"
+ >
+ <div
+ className="display-flex-center"
+ >
+ <span
+ className="badge badge-new spacer-right"
+ >
+ new
+ </span>
+ <strong>
+ title
+ </strong>
+ </div>
+ <ButtonIcon
+ className="button-tiny"
+ color="#cdcdcd"
+ onClick={[MockFunction]}
+ >
+ <ClearIcon
+ fill="#444"
+ size={12}
+ thin={true}
+ />
+ </ButtonIcon>
+ </div>
+ <div
+ className="big-spacer-top note"
+ >
+ <div>
+ description
+ </div>
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/ui/buttons.css b/server/sonar-web/src/main/js/components/ui/buttons.css
index 93b755a2230..171cd166d51 100644
--- a/server/sonar-web/src/main/js/components/ui/buttons.css
+++ b/server/sonar-web/src/main/js/components/ui/buttons.css
@@ -109,7 +109,7 @@
.button-grey:hover,
.button-grey.active {
background: var(--gray71);
- color: #ffffff;
+ color: #fff;
}
.button-grey:focus {
@@ -145,7 +145,8 @@
color: var(--blue);
}
-.button-link:active {
+.button-link:active,
+.button-link:focus {
box-shadow: none;
outline: thin dotted #ccc;
}
@@ -168,6 +169,12 @@
font-size: 11px;
}
+.button-tiny {
+ height: var(--tinyControlHeight);
+ line-height: var(--tinyControlHeight);
+ padding: 0 calc(var(--gridSize) / 2);
+}
+
.button-large {
height: var(--largeControlHeight);
padding: 0 16px;