From 4f885e88db04134cea9d500e89f28e6a09b65db2 Mon Sep 17 00:00:00 2001 From: Grégoire Aubert Date: Fri, 5 Apr 2019 10:58:53 +0200 Subject: 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 --- .../notifications/NavLatestNotification.tsx | 3 +- .../notifications/NotificationsSidebar.tsx | 8 +- .../NavLatestNotification-test.tsx.snap | 4 +- .../NotificationsSidebar-test.tsx.snap | 28 ++- .../app/components/notifications/notifications.css | 18 +- .../src/main/js/app/styles/components/badges.css | 7 +- .../src/main/js/app/styles/components/menu.css | 2 +- server/sonar-web/src/main/js/app/theme.js | 4 + server/sonar-web/src/main/js/app/types.d.ts | 5 +- .../src/main/js/apps/issues/IssuesPageSelector.tsx | 20 +- .../issues/__tests__/IssuesPageSelector-test.tsx | 56 ++++++ .../__snapshots__/IssuesPageSelector-test.tsx.snap | 17 ++ .../main/js/apps/issues/__tests__/utils-test.ts | 28 ++- .../src/main/js/apps/issues/components/App.tsx | 26 ++- .../js/apps/issues/components/AppContainer.tsx | 2 +- .../apps/issues/components/__tests__/App-test.tsx | 32 ++++ .../src/main/js/apps/issues/sidebar/TypeFacet.tsx | 80 ++++++-- .../issues/sidebar/__tests__/TypeFacet-test.tsx | 101 ++++++++++ .../__tests__/__snapshots__/Sidebar-test.tsx.snap | 12 +- .../__snapshots__/TypeFacet-test.tsx.snap | 210 +++++++++++++++++++++ .../sonar-web/src/main/js/apps/issues/styles.css | 29 ++- server/sonar-web/src/main/js/apps/issues/utils.ts | 13 ++ .../components/PluginChangeLogButton.tsx | 2 +- .../src/main/js/components/SourceViewer/styles.css | 2 +- .../main/js/components/common/LocationIndex.css | 2 +- .../src/main/js/components/facet/FacetItem.tsx | 2 +- .../js/components/icons-components/ClearIcon.tsx | 21 ++- .../src/main/js/components/issue/Issue.css | 38 ++-- .../src/main/js/components/issue/IssueView.tsx | 1 + .../components/issue/__tests__/IssueView-test.tsx | 11 +- .../__snapshots__/IssueView-test.tsx.snap | 87 ++++++++- .../components/issue/components/IssueMessage.tsx | 17 +- .../__snapshots__/IssueMessage-test.tsx.snap | 14 +- .../src/main/js/components/ui/NewsBox.css | 31 +++ .../src/main/js/components/ui/NewsBox.tsx | 50 +++++ .../js/components/ui/__tests__/NewsBox-test.tsx | 42 +++++ .../__tests__/__snapshots__/NewsBox-test.tsx.snap | 43 +++++ .../src/main/js/components/ui/buttons.css | 11 +- 38 files changed, 933 insertions(+), 146 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/issues/__tests__/IssuesPageSelector-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/issues/__tests__/__snapshots__/IssuesPageSelector-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/TypeFacet-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/TypeFacet-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/ui/NewsBox.css create mode 100644 server/sonar-web/src/main/js/components/ui/NewsBox.tsx create mode 100644 server/sonar-web/src/main/js/components/ui/__tests__/NewsBox-test.tsx create mode 100644 server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap (limited to 'server/sonar-web/src') 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 {
  • - +
  • 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) {

    {translate('embed_docs.whats_new')}

    - - - + + +
    {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]} > 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

    embed_docs.whats_new

    - - - + +
    embed_docs.whats_new - - - + +
    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 ; } -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 = {}) { + return shallow( + + ); +} 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`] = ` + +`; 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 { 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 { 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 { }; handleFilterChange = (changes: Partial) => { - this.setState({ loading: true }); this.props.router.push({ pathname: this.props.location.pathname, query: { @@ -654,6 +660,14 @@ export class App extends React.PureComponent { 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) => void; onToggle: (property: string) => void; open: boolean; + setCurrentUserSetting: (setting: T.CurrentUserSetting) => void; stats: T.Dict | undefined; types: string[]; } -export default class TypeFacet extends React.PureComponent { +export class TypeFacet extends React.PureComponent { property = 'types'; static defaultProps = { @@ -68,19 +76,17 @@ export default class TypeFacet extends React.PureComponent { 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 { disabled={stat === 0 && !active} key={type} name={ - - {translate('issue.type', type)} + + {' '} + {translate('issue.type', type)} + {type === 'SECURITY_HOTSPOT' && this.props.newsBoxDismissHotspots && ( + +

    {translate('issues.hotspots.helper')}

    +
    + + {translate('learn_more')} + + + } + /> + )}
    } 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 ( { {this.props.open && ( <> {ISSUE_TYPES.map(this.renderItem)} + {!newsBoxDismissHotspots && showHotspotNewsBox && ( + +

    {translate('issues.hotspots.helper')}

    +

    + + {translate('learn_more')} + +

    +
    + )} )} @@ -131,3 +166,18 @@ export default class TypeFacet extends React.PureComponent { ); } } + +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('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('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 = {}) { + return shallow( + + ); +} 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`] = ` + +

    + issues.hotspots.helper +

    +

    + + learn_more + +

    +
    +`; + +exports[`should display the hotspot tooltip helper only 1`] = ` + + + + issue.type.SECURITY_HOTSPOT + +

    + issues.hotspots.helper +

    +
    + + learn_more + + + } + /> +
    +`; + +exports[`should display the hotspot tooltip helper only 2`] = ` + + + + issue.type.SECURITY_HOTSPOT + +

    + issues.hotspots.helper +

    +
    + + learn_more + + + } + /> +
    +`; + +exports[`should render open by default 1`] = ` + + + + + + + + issue.type.BUG +
    + } + onClick={[Function]} + stat={0} + value="BUG" + /> + + + + issue.type.VULNERABILITY + + } + onClick={[Function]} + stat="2" + value="VULNERABILITY" + /> + + + + issue.type.CODE_SMELL + + } + onClick={[Function]} + stat="5" + value="CODE_SMELL" + /> + + + + issue.type.SECURITY_HOTSPOT + + } + onClick={[Function]} + stat="1" + value="SECURITY_HOTSPOT" + /> + + + +`; 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) { }> - + 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 ( - + {thin ? ( + + ) : ( + + )} ); } 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 { 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 = {}) { 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`] = ` +
    + + +
    +`; + +exports[`should render issues correctly 1`] = `
    { render() { return (
    - {this.props.message} - {this.props.message} + {this.props.engine && ( -
    +
    {this.props.engine}
    )} {this.props.manualVulnerability && ( -
    +
    {translate('issue.manual_vulnerability')}
    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`] = `
    - Reduce the number of conditional operators (4) used in the expression - + Reduce the number of conditional operators (4) used in the expression + +
    `; 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 ( +
    +
    +
    + {translate('new')} + {title} +
    + + + +
    +
    {children}
    +
    + ); +} 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 = {}) { + return shallow( + +
    description
    +
    + ); +} 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`] = ` +
    +
    +
    + + new + + + title + +
    + + + +
    +
    +
    + description +
    +
    +
    +`; 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; -- cgit v1.2.3