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