Browse Source

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
tags/7.8
Grégoire Aubert 5 years ago
parent
commit
4f885e88db
41 changed files with 946 additions and 150 deletions
  1. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java
  2. 9
    2
      server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java
  3. 1
    2
      server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
  4. 5
    3
      server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
  5. 2
    2
      server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
  6. 18
    10
      server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
  7. 7
    11
      server/sonar-web/src/main/js/app/components/notifications/notifications.css
  8. 4
    3
      server/sonar-web/src/main/js/app/styles/components/badges.css
  9. 1
    1
      server/sonar-web/src/main/js/app/styles/components/menu.css
  10. 4
    0
      server/sonar-web/src/main/js/app/theme.js
  11. 4
    1
      server/sonar-web/src/main/js/app/types.d.ts
  12. 6
    14
      server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx
  13. 56
    0
      server/sonar-web/src/main/js/apps/issues/__tests__/IssuesPageSelector-test.tsx
  14. 17
    0
      server/sonar-web/src/main/js/apps/issues/__tests__/__snapshots__/IssuesPageSelector-test.tsx.snap
  15. 27
    1
      server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
  16. 20
    6
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  17. 1
    1
      server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
  18. 32
    0
      server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
  19. 65
    15
      server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
  20. 101
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/TypeFacet-test.tsx
  21. 6
    6
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
  22. 210
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/TypeFacet-test.tsx.snap
  23. 14
    15
      server/sonar-web/src/main/js/apps/issues/styles.css
  24. 13
    0
      server/sonar-web/src/main/js/apps/issues/utils.ts
  25. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx
  26. 1
    1
      server/sonar-web/src/main/js/components/SourceViewer/styles.css
  27. 1
    1
      server/sonar-web/src/main/js/components/common/LocationIndex.css
  28. 1
    1
      server/sonar-web/src/main/js/components/facet/FacetItem.tsx
  29. 16
    5
      server/sonar-web/src/main/js/components/icons-components/ClearIcon.tsx
  30. 12
    26
      server/sonar-web/src/main/js/components/issue/Issue.css
  31. 1
    0
      server/sonar-web/src/main/js/components/issue/IssueView.tsx
  32. 8
    3
      server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx
  33. 86
    1
      server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap
  34. 8
    9
      server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx
  35. 9
    5
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap
  36. 31
    0
      server/sonar-web/src/main/js/components/ui/NewsBox.css
  37. 50
    0
      server/sonar-web/src/main/js/components/ui/NewsBox.tsx
  38. 42
    0
      server/sonar-web/src/main/js/components/ui/__tests__/NewsBox-test.tsx
  39. 43
    0
      server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap
  40. 9
    2
      server/sonar-web/src/main/js/components/ui/buttons.css
  41. 3
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java View File

@@ -57,7 +57,7 @@ public class SetSettingAction implements UsersWsAction {
.setRequired(true)
.setMaximumLength(100)
.setDescription("Setting key")
.setPossibleValues("notifications.optOut", UserUpdater.NOTIFICATIONS_READ_DATE);
.setPossibleValues("notifications.optOut", UserUpdater.NOTIFICATIONS_READ_DATE, "newsbox.dismiss.hotspots");

action.createParam(PARAM_VALUE)
.setRequired(true)

+ 9
- 2
server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java View File

@@ -91,11 +91,17 @@ public class SetSettingActionTest {
.setParam("value", "true")
.execute();

ws.newRequest()
.setParam("key", "newsbox.dismiss.hotspots")
.setParam("value", "true")
.execute();

assertThat(db.getDbClient().userPropertiesDao().selectByUser(db.getSession(), user))
.extracting(UserPropertyDto::getKey, UserPropertyDto::getValue)
.containsExactlyInAnyOrder(
tuple("notifications.readDate", "1234"),
tuple("notifications.optOut", "true"));
tuple("notifications.optOut", "true"),
tuple("newsbox.dismiss.hotspots", "true"));
}

@Test
@@ -125,7 +131,8 @@ public class SetSettingActionTest {

assertThat(definition.param("key").possibleValues()).containsExactlyInAnyOrder(
"notifications.optOut",
"notifications.readDate");
"notifications.readDate",
"newsbox.dismiss.hotspots");
}

}

+ 1
- 2
server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx View File

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

+ 5
- 3
server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx View File

@@ -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 ? (

+ 2
- 2
server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap View File

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

+ 18
- 10
server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap View File

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

+ 7
- 11
server/sonar-web/src/main/js/app/components/notifications/notifications.css View File

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

+ 4
- 3
server/sonar-web/src/main/js/app/styles/components/badges.css View File

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


+ 1
- 1
server/sonar-web/src/main/js/app/styles/components/menu.css View File

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

+ 4
- 0
server/sonar-web/src/main/js/app/theme.js View File

@@ -64,6 +64,10 @@ module.exports = {

snippetFontColor: '#f0f0f0',

//issues
issueBgColor: '#ffeaea',
hotspotBgColor: '#eeeff4',

// alerts
warningIconColor: '#e2bf41',


+ 4
- 1
server/sonar-web/src/main/js/app/types.d.ts View File

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

+ 6
- 14
server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx View File

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

+ 56
- 0
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesPageSelector-test.tsx View File

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

+ 17
- 0
server/sonar-web/src/main/js/apps/issues/__tests__/__snapshots__/IssuesPageSelector-test.tsx.snap View File

@@ -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 {},
}
}
/>
`;

+ 27
- 1
server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts View File

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

+ 20
- 6
server/sonar-web/src/main/js/apps/issues/components/App.tsx View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx View File

@@ -90,4 +90,4 @@ const mapDispatchToProps = { fetchIssues: fetchIssues as any } as DispatchProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(lazyLoad(() => import('./App')));
)(lazyLoad(() => import('./App'), 'IssuesAppContainer'));

+ 32
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx View File

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

+ 65
- 15
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx View File

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

+ 101
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/TypeFacet-test.tsx View File

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

+ 6
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap View File

@@ -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",

+ 210
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/TypeFacet-test.tsx.snap View File

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

+ 14
- 15
server/sonar-web/src/main/js/apps/issues/styles.css View File

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

+ 13
- 0
server/sonar-web/src/main/js/apps/issues/utils.ts View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLogButton.tsx View File

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

+ 1
- 1
server/sonar-web/src/main/js/components/SourceViewer/styles.css View File

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


+ 1
- 1
server/sonar-web/src/main/js/components/common/LocationIndex.css View File

@@ -38,7 +38,7 @@
}

.location-index.muted {
background-color: #ccc;
background-color: var(--gray80);
}

.location-index.is-leading {

+ 1
- 1
server/sonar-web/src/main/js/components/facet/FacetItem.tsx View File

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


+ 16
- 5
server/sonar-web/src/main/js/components/icons-components/ClearIcon.tsx View File

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

+ 12
- 26
server/sonar-web/src/main/js/components/issue/Issue.css View File

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

+ 1
- 0
server/sonar-web/src/main/js/components/issue/IssueView.tsx View File

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

+ 8
- 3
server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx View File

@@ -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']> = {}) {

+ 86
- 1
server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap View File

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

+ 8
- 9
server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx View File

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

+ 9
- 5
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap View File

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

+ 31
- 0
server/sonar-web/src/main/js/components/ui/NewsBox.css View File

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

+ 50
- 0
server/sonar-web/src/main/js/components/ui/NewsBox.tsx View File

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

+ 42
- 0
server/sonar-web/src/main/js/components/ui/__tests__/NewsBox-test.tsx View File

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

+ 43
- 0
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap View File

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

+ 9
- 2
server/sonar-web/src/main/js/components/ui/buttons.css View File

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

+ 3
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -602,6 +602,7 @@ issue.manual_vulnerability=Manual
issue.manual_vulnerability.description=This Vulnerability was created from a Security Hotspot and has its own issue workflow.
issue.rule_details=Rule Details
issue.send_notifications=Send Notifications
issue.see_rule=See Rule
issue.transition=Transition
issue.transition.confirm=Confirm
issue.transition.confirm.description=This issue has been reviewed and something should be done eventually to handle it.
@@ -685,6 +686,7 @@ issues.my_issues=My Issues
issues.no_my_issues=There are no issues assigned to you.
issues.no_issues=No Issues. Hooray!
issues.x_more_locations=+ {0} more location(s)
issues.hotspots.helper=Security Hotspots aren't necessarily issues, but they need to be reviewed to make sure they aren't vulnerabilities.


#------------------------------------------------------------------------------
@@ -2109,7 +2111,7 @@ projects_role.admin.desc=Access project settings and perform administration task
projects_role.issueadmin=Administer Issues
projects_role.issueadmin.desc=Change the type and severity of issues, resolve issues as being "won't fix" or "false-positive" (users also need "Browse" permission).
projects_role.securityhotspotadmin=Administer Security Hotspots
projects_role.securityhotspotadmin.desc=Detect a Vulnerability from a "Security Hotspot". Reject, clear, accept, reopen a "Security Hotspot" (users also need "Browse" permissions).
projects_role.securityhotspotadmin.desc=Detect a Vulnerability from a Security Hotspot. Reject, clear, accept, reopen a Security Hotspot (users also need "Browse" permissions).
projects_role.user=Browse
projects_role.user.desc=Access a project, browse its measures and issues, confirm or resolve issues as "fixed", change the assignee, comment on issues and change tags.
projects_role.codeviewer=See Source Code

Loading…
Cancel
Save