diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2019-05-06 17:12:05 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-05-22 20:21:13 +0200 |
commit | e861cb3f45bc28434dc59d857c38e414dba2a0bc (patch) | |
tree | d856732255bfb0fcc92ae28bc0eb7ef0a5d339af /server/sonar-web | |
parent | 7a547418b7fc4beae7a541f81a0c225b0af60631 (diff) | |
download | sonarqube-e861cb3f45bc28434dc59d857c38e414dba2a0bc.tar.gz sonarqube-e861cb3f45bc28434dc59d857c38e414dba2a0bc.zip |
SONAR-12026 Add new hotspot status facet in issues page
Diffstat (limited to 'server/sonar-web')
9 files changed, 304 insertions, 9 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx index 5e069b27e14..958245f1ad6 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx @@ -38,7 +38,7 @@ interface Props { stats: T.Dict<number> | undefined; } -const RESOLUTIONS = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED']; +const RESOLUTIONS = ['', 'FALSE-POSITIVE', 'FIXED', 'REMOVED', 'WONTFIX']; export default class ResolutionFacet extends React.PureComponent<Props> { property = 'resolutions'; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx index 03d6864947a..ea83e7a20c2 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx @@ -38,14 +38,14 @@ interface Props { statuses: string[]; } -const STATUSES = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED']; +const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED']; +const HOTSPOT_STATUSES = ['TO_REVIEW', 'REVIEWED', 'IN_REVIEW']; +const COMMON_STATUSES = ['CLOSED']; export default class StatusFacet extends React.PureComponent<Props> { property = 'statuses'; - static defaultProps = { - open: true - }; + static defaultProps = { open: true }; handleItemClick = (itemValue: string, multiple: boolean) => { const { statuses } = this.props; @@ -110,7 +110,15 @@ export default class StatusFacet extends React.PureComponent<Props> { <DeferredSpinner loading={this.props.fetching} /> {this.props.open && ( <> - <FacetItemsList>{STATUSES.map(this.renderItem)}</FacetItemsList> + <FacetItemsList title={translate('issues')}> + {STATUSES.map(this.renderItem)} + </FacetItemsList> + <FacetItemsList title={translate('issue.type.SECURITY_HOTSPOT.plural')}> + {HOTSPOT_STATUSES.map(this.renderItem)} + </FacetItemsList> + <FacetItemsList title={translate('issues.issues_and_hotspots')}> + {COMMON_STATUSES.map(this.renderItem)} + </FacetItemsList> <MultipleSelectionHint options={Object.keys(stats).length} values={statuses.length} /> </> )} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StatusFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StatusFacet-test.tsx new file mode 100644 index 00000000000..3b7b1482d35 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StatusFacet-test.tsx @@ -0,0 +1,81 @@ +/* + * 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 StatusFacet from '../StatusFacet'; +import { click } from '../../../../helpers/testUtils'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should toggle status facet', () => { + const onToggle = jest.fn(); + const wrapper = shallowRender({ onToggle }); + click(wrapper.children('FacetHeader')); + expect(onToggle).toBeCalledWith('statuses'); +}); + +it('should clear status facet', () => { + const onChange = jest.fn(); + const wrapper = shallowRender({ onChange, statuses: ['TO_REVIEW'] }); + wrapper.children('FacetHeader').prop<Function>('onClear')(); + expect(onChange).toBeCalledWith({ statuses: [] }); +}); + +it('should select a status', () => { + const onChange = jest.fn(); + const wrapper = shallowRender({ onChange }); + clickAndCheck('TO_REVIEW'); + clickAndCheck('OPEN', true, ['OPEN', 'TO_REVIEW']); + clickAndCheck('CONFIRMED'); + + function clickAndCheck(status: string, multiple = false, expected = [status]) { + wrapper + .find(`FacetItemsList`) + .find(`FacetItem[value="${status}"]`) + .prop<Function>('onClick')(status, multiple); + expect(onChange).lastCalledWith({ statuses: expected }); + wrapper.setProps({ statuses: expected }); + } +}); + +function shallowRender(props: Partial<StatusFacet['props']> = {}) { + return shallow( + <StatusFacet + fetching={false} + onChange={jest.fn()} + onToggle={jest.fn()} + open={true} + stats={{ + OPEN: 104, + CONFIRMED: 8, + REOPENED: 0, + RESOLVED: 0, + CLOSED: 8, + TO_REVIEW: 150, + IN_REVIEW: 7, + REVIEWED: 1105 + }} + statuses={[]} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StatusFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StatusFacet-test.tsx.snap new file mode 100644 index 00000000000..9d01da1c88e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StatusFacet-test.tsx.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<FacetBox + property="statuses" +> + <FacetHeader + name="issues.facet.statuses" + onClear={[Function]} + onClick={[Function]} + open={true} + values={Array []} + /> + <DeferredSpinner + loading={false} + timeout={100} + /> + <FacetItemsList + title="issues" + > + <FacetItem + active={false} + disabled={false} + halfWidth={true} + key="OPEN" + loading={false} + name={ + <StatusHelper + status="OPEN" + /> + } + onClick={[Function]} + stat="104" + tooltip="issue.status.OPEN" + value="OPEN" + /> + <FacetItem + active={false} + disabled={false} + halfWidth={true} + key="CONFIRMED" + loading={false} + name={ + <StatusHelper + status="CONFIRMED" + /> + } + onClick={[Function]} + stat="8" + tooltip="issue.status.CONFIRMED" + value="CONFIRMED" + /> + <FacetItem + active={false} + disabled={true} + halfWidth={true} + key="REOPENED" + loading={false} + name={ + <StatusHelper + status="REOPENED" + /> + } + onClick={[Function]} + stat={0} + tooltip="issue.status.REOPENED" + value="REOPENED" + /> + <FacetItem + active={false} + disabled={true} + halfWidth={true} + key="RESOLVED" + loading={false} + name={ + <StatusHelper + status="RESOLVED" + /> + } + onClick={[Function]} + stat={0} + tooltip="issue.status.RESOLVED" + value="RESOLVED" + /> + </FacetItemsList> + <FacetItemsList + title="issue.type.SECURITY_HOTSPOT.plural" + > + <FacetItem + active={false} + disabled={false} + halfWidth={true} + key="TO_REVIEW" + loading={false} + name={ + <StatusHelper + status="TO_REVIEW" + /> + } + onClick={[Function]} + stat="150" + tooltip="issue.status.TO_REVIEW" + value="TO_REVIEW" + /> + <FacetItem + active={false} + disabled={false} + halfWidth={true} + key="REVIEWED" + loading={false} + name={ + <StatusHelper + status="REVIEWED" + /> + } + onClick={[Function]} + stat="1.1short_number_suffix.k" + tooltip="issue.status.REVIEWED" + value="REVIEWED" + /> + <FacetItem + active={false} + disabled={false} + halfWidth={true} + key="IN_REVIEW" + loading={false} + name={ + <StatusHelper + status="IN_REVIEW" + /> + } + onClick={[Function]} + stat="7" + tooltip="issue.status.IN_REVIEW" + value="IN_REVIEW" + /> + </FacetItemsList> + <FacetItemsList + title="issues.issues_and_hotspots" + > + <FacetItem + active={false} + disabled={false} + halfWidth={true} + key="CLOSED" + loading={false} + name={ + <StatusHelper + status="CLOSED" + /> + } + onClick={[Function]} + stat="8" + tooltip="issue.status.CLOSED" + value="CLOSED" + /> + </FacetItemsList> + <MultipleSelectionHint + options={8} + values={0} + /> +</FacetBox> +`; diff --git a/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx b/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx index f72a84915e8..b58c19d585b 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx +++ b/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx @@ -21,8 +21,14 @@ import * as React from 'react'; interface Props { children?: React.ReactNode; + title?: string; } -export default function FacetItemsList(props: Props) { - return <div className="search-navigator-facet-list">{props.children}</div>; +export default function FacetItemsList({ children, title }: Props) { + return ( + <div className="search-navigator-facet-list"> + {title && <div className="search-navigator-facet-list-title">{title}</div>} + {children} + </div> + ); } diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx index 82ebe6d53fd..c8f21ca697b 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx +++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx @@ -30,3 +30,13 @@ it('should render', () => { ) ).toMatchSnapshot(); }); + +it('should render with title', () => { + expect( + shallow( + <FacetItemsList title="title test"> + <div /> + </FacetItemsList> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap index 9962cfc364e..fe288787b43 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap @@ -7,3 +7,16 @@ exports[`should render 1`] = ` <div /> </div> `; + +exports[`should render with title 1`] = ` +<div + className="search-navigator-facet-list" +> + <div + className="search-navigator-facet-list-title" + > + title test + </div> + <div /> +</div> +`; diff --git a/server/sonar-web/src/main/js/components/icons-components/StatusIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/StatusIcon.tsx index a4d0e3bfcb9..74fd63f4bd1 100644 --- a/server/sonar-web/src/main/js/components/icons-components/StatusIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/StatusIcon.tsx @@ -31,7 +31,10 @@ const statusIcons: T.Dict<(props: IconProps) => React.ReactElement<any>> = { confirmed: ConfirmedStatusIcon, reopened: ReopenedStatusIcon, resolved: ResolvedStatusIcon, - closed: ClosedStatusIcon + closed: ClosedStatusIcon, + to_review: OpenStatusIcon, + in_review: ConfirmedStatusIcon, + reviewed: ResolvedStatusIcon }; export default function StatusIcon(props: Props) { diff --git a/server/sonar-web/src/main/js/components/search-navigator.css b/server/sonar-web/src/main/js/components/search-navigator.css index a723606ffb5..75c8d0f5110 100644 --- a/server/sonar-web/src/main/js/components/search-navigator.css +++ b/server/sonar-web/src/main/js/components/search-navigator.css @@ -358,6 +358,17 @@ a.search-navigator-facet:focus, font-size: 0; } +.search-navigator-facet-list-title { + margin: 0 var(--gridSize) calc(var(--gridSize) / 2); + font-size: var(--smallFontSize); + font-weight: bold; +} + +.search-navigator-facet-list + .search-navigator-facet-list > .search-navigator-facet-list-title { + border-top: 1px solid var(--barBorderColor); + padding-top: var(--gridSize); +} + .search-navigator-facet-empty { margin: 0 0 0 0; padding: 0 10px 10px; |