* 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 facettags/7.8
@@ -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) |
@@ -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"); | |||
} | |||
} |
@@ -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> | |||
</> |
@@ -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 ? ( |
@@ -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> |
@@ -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" |
@@ -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; |
@@ -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; | |||
} | |||
@@ -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; |
@@ -64,6 +64,10 @@ module.exports = { | |||
snippetFontColor: '#f0f0f0', | |||
//issues | |||
issueBgColor: '#ffeaea', | |||
hotspotBgColor: '#eeeff4', | |||
// alerts | |||
warningIconColor: '#e2bf41', | |||
@@ -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; |
@@ -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); |
@@ -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} /> | |||
); | |||
} |
@@ -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 {}, | |||
} | |||
} | |||
/> | |||
`; |
@@ -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); | |||
}); | |||
}); |
@@ -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) => { |
@@ -90,4 +90,4 @@ const mapDispatchToProps = { fetchIssues: fetchIssues as any } as DispatchProps; | |||
export default connect( | |||
mapStateToProps, | |||
mapDispatchToProps | |||
)(lazyLoad(() => import('./App'))); | |||
)(lazyLoad(() => import('./App'), 'IssuesAppContainer')); |
@@ -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; |
@@ -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); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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", |
@@ -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> | |||
`; |
@@ -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 { |
@@ -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'); | |||
} |
@@ -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> |
@@ -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; | |||
} | |||
@@ -38,7 +38,7 @@ | |||
} | |||
.location-index.muted { | |||
background-color: #ccc; | |||
background-color: var(--gray80); | |||
} | |||
.location-index.is-leading { |
@@ -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; | |||
} | |||
@@ -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> | |||
); | |||
} |
@@ -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) { |
@@ -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 | |||
}); |
@@ -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']> = {}) { |
@@ -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" |
@@ -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> |
@@ -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> | |||
`; |
@@ -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; | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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; |
@@ -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 |