Browse Source

SONAR-13597 Add scope distribution to issues page

tags/8.5.0.37579
Wouter Admiraal 3 years ago
parent
commit
3914a4b0a3

+ 1
- 0
.gitignore View File

@@ -68,3 +68,4 @@ scripts/patches/*license*.txt
!scripts/patches/debug_ce.sh
!scripts/patches/debug_web.sh
!scripts/patches/postgres.sh
gherkin-features/

+ 4
- 0
server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
import { collapsePath, limitComponentName } from 'sonar-ui-common/helpers/path';
import Organization from '../../../components/shared/Organization';
import { getSelectedLocation } from '../utils';
@@ -28,6 +29,7 @@ interface Props {
T.Issue,
| 'component'
| 'componentLongName'
| 'componentQualifier'
| 'flows'
| 'organization'
| 'project'
@@ -59,6 +61,8 @@ export default function ComponentBreadcrumbs({

return (
<div className="component-name text-ellipsis">
<QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} />

{displayOrganization && <Organization link={false} organizationKey={issue.organization} />}

{displayProject && (

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

@@ -19,11 +19,13 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { ComponentQualifier } from '../../../../types/component';
import ComponentBreadcrumbs from '../ComponentBreadcrumbs';

const baseIssue = {
component: 'comp',
componentLongName: 'comp-name',
componentQualifier: ComponentQualifier.File,
flows: [],
organization: 'org',
project: 'proj',

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

@@ -4,6 +4,10 @@ exports[`renders 1`] = `
<div
className="component-name text-ellipsis"
>
<QualifierIcon
className="spacer-right"
qualifier="FIL"
/>
<Connect(Organization)
link={false}
organizationKey="org"
@@ -28,6 +32,10 @@ exports[`renders with sub-project 1`] = `
<div
className="component-name text-ellipsis"
>
<QualifierIcon
className="spacer-right"
qualifier="FIL"
/>
<Connect(Organization)
link={false}
organizationKey="org"

+ 96
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx View File

@@ -0,0 +1,96 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { without } from 'lodash';
import * as React from 'react';
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
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 MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
import { SOURCE_SCOPES } from '../../../helpers/constants';
import { formatFacetStat, Query } from '../utils';

export interface ScopeFacetProps {
fetching: boolean;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
scopes: string[];
stats: T.Dict<number> | undefined;
}

export default function ScopeFacet(props: ScopeFacetProps) {
const { fetching, open, scopes = [], stats = {} } = props;
const values = scopes.map(scope => translate('issue.scope', scope));

return (
<FacetBox property="scopes">
<FacetHeader
fetching={fetching}
name={translate('issues.facet.scopes')}
onClear={() => props.onChange({ scopes: [] })}
onClick={() => props.onToggle('scopes')}
open={open}
values={values}
/>

{open && (
<>
<FacetItemsList>
{SOURCE_SCOPES.map(({ scope, qualifier }) => {
const active = scopes.includes(scope);
const stat = stats[scope];

return (
<FacetItem
active={active}
disabled={stat === 0 && !active}
key={scope}
name={
<span className="display-flex-center">
<QualifierIcon className="little-spacer-right" qualifier={qualifier} />{' '}
{translate('issue.scope', scope)}
</span>
}
onClick={(itemValue: string, multiple: boolean) => {
if (multiple) {
props.onChange({
scopes: active ? without(scopes, itemValue) : [...scopes, itemValue]
});
} else {
props.onChange({
scopes: active && scopes.length === 1 ? [] : [itemValue]
});
}
}}
stat={formatFacetStat(stat)}
value={scope}
/>
);
})}
</FacetItemsList>
<MultipleSelectionHint options={Object.keys(stats).length} values={scopes.length} />
</>
)}
</FacetBox>
);
}

+ 9
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx View File

@@ -32,6 +32,7 @@ import LanguageFacet from './LanguageFacet';
import ProjectFacet from './ProjectFacet';
import ResolutionFacet from './ResolutionFacet';
import RuleFacet from './RuleFacet';
import ScopeFacet from './ScopeFacet';
import SeverityFacet from './SeverityFacet';
import StandardFacet from './StandardFacet';
import StatusFacet from './StatusFacet';
@@ -126,6 +127,14 @@ export class Sidebar extends React.PureComponent<Props> {
severities={query.severities}
stats={facets.severities}
/>
<ScopeFacet
fetching={this.props.loadingFacets.scopes === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.scopes}
stats={facets.scopes}
scopes={query.scopes}
/>
<ResolutionFacet
fetching={this.props.loadingFacets.resolutions === true}
onChange={this.props.onFilterChange}

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

@@ -0,0 +1,94 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow, ShallowWrapper } from 'enzyme';
import * as React from 'react';
import FacetHeader from '../../../../components/facet/FacetHeader';
import FacetItem from '../../../../components/facet/FacetItem';
import { IssueScope } from '../../../../types/issues';
import ScopeFacet, { ScopeFacetProps } from '../ScopeFacet';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ open: true })).toMatchSnapshot('open');
expect(shallowRender({ open: true, scopes: [IssueScope.Main] })).toMatchSnapshot('active facet');
expect(shallowRender({ open: true, stats: { [IssueScope.Main]: 0 } })).toMatchSnapshot(
'disabled facet'
);
});

it('should correctly handle facet header clicks', () => {
const onChange = jest.fn();
const onToggle = jest.fn();
const wrapper = shallowRender({ onChange, onToggle });

wrapper.find(FacetHeader).props().onClear!();
expect(onChange).toBeCalledWith({ scopes: [] });

wrapper.find(FacetHeader).props().onClick!();
expect(onToggle).toBeCalledWith('scopes');
});

it('should correctly handle facet item clicks', () => {
const wrapper = shallowRender({ open: true, scopes: [IssueScope.Main] });
const onChange = jest.fn(({ scopes }) => wrapper.setProps({ scopes }));
wrapper.setProps({ onChange });

clickFacetItem(wrapper, IssueScope.Test);
expect(onChange).toHaveBeenLastCalledWith({ scopes: [IssueScope.Test] });

clickFacetItem(wrapper, IssueScope.Test);
expect(onChange).toHaveBeenLastCalledWith({ scopes: [] });

clickFacetItem(wrapper, IssueScope.Test, true);
clickFacetItem(wrapper, IssueScope.Main, true);
expect(onChange).toHaveBeenLastCalledWith({
scopes: expect.arrayContaining([IssueScope.Main, IssueScope.Test])
});

clickFacetItem(wrapper, IssueScope.Test, true);
expect(onChange).toHaveBeenLastCalledWith({ scopes: [IssueScope.Main] });
});

function clickFacetItem(
wrapper: ShallowWrapper<ScopeFacetProps>,
scope: IssueScope,
multiple = false
) {
return wrapper
.find(FacetItem)
.filterWhere(f => f.key() === scope)
.props()
.onClick(scope, multiple);
}

function shallowRender(props: Partial<ScopeFacetProps> = {}) {
return shallow<ScopeFacetProps>(
<ScopeFacet
fetching={true}
onChange={jest.fn()}
onToggle={jest.fn()}
open={false}
scopes={[]}
stats={{}}
{...props}
/>
);
}

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

@@ -0,0 +1,210 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: active facet 1`] = `
<FacetBox
property="scopes"
>
<FacetHeader
fetching={true}
name="issues.facet.scopes"
onClear={[Function]}
onClick={[Function]}
open={true}
values={
Array [
"issue.scope.MAIN",
]
}
/>
<FacetItemsList>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="MAIN"
loading={false}
name={
<span
className="display-flex-center"
>
<QualifierIcon
className="little-spacer-right"
qualifier="FIL"
/>
issue.scope.MAIN
</span>
}
onClick={[Function]}
value="MAIN"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="TEST"
loading={false}
name={
<span
className="display-flex-center"
>
<QualifierIcon
className="little-spacer-right"
qualifier="UTS"
/>
issue.scope.TEST
</span>
}
onClick={[Function]}
value="TEST"
/>
</FacetItemsList>
<MultipleSelectionHint
options={0}
values={1}
/>
</FacetBox>
`;

exports[`should render correctly: default 1`] = `
<FacetBox
property="scopes"
>
<FacetHeader
fetching={true}
name="issues.facet.scopes"
onClear={[Function]}
onClick={[Function]}
open={false}
values={Array []}
/>
</FacetBox>
`;

exports[`should render correctly: disabled facet 1`] = `
<FacetBox
property="scopes"
>
<FacetHeader
fetching={true}
name="issues.facet.scopes"
onClear={[Function]}
onClick={[Function]}
open={true}
values={Array []}
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={true}
halfWidth={false}
key="MAIN"
loading={false}
name={
<span
className="display-flex-center"
>
<QualifierIcon
className="little-spacer-right"
qualifier="FIL"
/>
issue.scope.MAIN
</span>
}
onClick={[Function]}
stat={0}
value="MAIN"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="TEST"
loading={false}
name={
<span
className="display-flex-center"
>
<QualifierIcon
className="little-spacer-right"
qualifier="UTS"
/>
issue.scope.TEST
</span>
}
onClick={[Function]}
value="TEST"
/>
</FacetItemsList>
<MultipleSelectionHint
options={1}
values={0}
/>
</FacetBox>
`;

exports[`should render correctly: open 1`] = `
<FacetBox
property="scopes"
>
<FacetHeader
fetching={true}
name="issues.facet.scopes"
onClear={[Function]}
onClick={[Function]}
open={true}
values={Array []}
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="MAIN"
loading={false}
name={
<span
className="display-flex-center"
>
<QualifierIcon
className="little-spacer-right"
qualifier="FIL"
/>
issue.scope.MAIN
</span>
}
onClick={[Function]}
value="MAIN"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="TEST"
loading={false}
name={
<span
className="display-flex-center"
>
<QualifierIcon
className="little-spacer-right"
qualifier="UTS"
/>
issue.scope.TEST
</span>
}
onClick={[Function]}
value="TEST"
/>
</FacetItemsList>
<MultipleSelectionHint
options={0}
values={0}
/>
</FacetBox>
`;

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

@@ -4,6 +4,7 @@ exports[`should not render developer nominative facets when asked not to 1`] = `
Array [
"TypeFacet",
"SeverityFacet",
"ScopeFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
@@ -19,6 +20,7 @@ exports[`should render facets for developer 1`] = `
Array [
"TypeFacet",
"SeverityFacet",
"ScopeFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
@@ -37,6 +39,7 @@ exports[`should render facets for directory 1`] = `
Array [
"TypeFacet",
"SeverityFacet",
"ScopeFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
@@ -54,6 +57,7 @@ exports[`should render facets for global page 1`] = `
Array [
"TypeFacet",
"SeverityFacet",
"ScopeFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
@@ -71,6 +75,7 @@ exports[`should render facets for module 1`] = `
Array [
"TypeFacet",
"SeverityFacet",
"ScopeFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
@@ -89,6 +94,7 @@ exports[`should render facets for project 1`] = `
Array [
"TypeFacet",
"SeverityFacet",
"ScopeFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
@@ -107,6 +113,7 @@ exports[`should render facets when my issues are selected 1`] = `
Array [
"TypeFacet",
"SeverityFacet",
"ScopeFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",

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

@@ -291,7 +291,7 @@
}

.issues-workspace-list-component {
padding: 10px 10px 6px;
padding: 10px 0 6px;
}

.issues-workspace-list-item + .issues-workspace-list-item {

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

@@ -54,6 +54,7 @@ export interface Query {
resolved: boolean;
rules: string[];
sansTop25: string[];
scopes: string[];
severities: string[];
sinceLeakPeriod: boolean;
sonarsourceSecurity: string[];
@@ -96,6 +97,7 @@ export function parseQuery(query: T.RawQuery): Query {
resolved: parseAsBoolean(query.resolved),
rules: parseAsArray(query.rules, parseAsString),
sansTop25: parseAsArray(query.sansTop25, parseAsString),
scopes: parseAsArray(query.scopes, parseAsString),
severities: parseAsArray(query.severities, parseAsString),
sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false),
sonarsourceSecurity: parseAsArray(query.sonarsourceSecurity, parseAsString),
@@ -134,6 +136,7 @@ export function serializeQuery(query: Query): T.RawQuery {
rules: serializeStringArray(query.rules),
s: serializeString(query.sort),
sansTop25: serializeStringArray(query.sansTop25),
scopes: serializeStringArray(query.scopes),
severities: serializeStringArray(query.severities),
sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined,
sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity),

+ 6
- 1
server/sonar-web/src/main/js/helpers/constants.ts View File

@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { colors } from '../app/theme';
import { IssueType } from '../types/issues';
import { ComponentQualifier } from '../types/component';
import { IssueScope, IssueType } from '../types/issues';

export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED'];
@@ -28,6 +29,10 @@ export const ISSUE_TYPES: T.IssueType[] = [
IssueType.CodeSmell,
IssueType.SecurityHotspot
];
export const SOURCE_SCOPES = [
{ scope: IssueScope.Main, qualifier: ComponentQualifier.File },
{ scope: IssueScope.Test, qualifier: ComponentQualifier.TestFile }
];
export const RULE_TYPES: T.RuleType[] = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT'];
export const RULE_STATUSES = ['READY', 'BETA', 'DEPRECATED'];


+ 5
- 0
server/sonar-web/src/main/js/types/issues.ts View File

@@ -24,3 +24,8 @@ export enum IssueType {
Bug = 'BUG',
SecurityHotspot = 'SECURITY_HOTSPOT'
}

export enum IssueScope {
Main = 'MAIN',
Test = 'TEST'
}

+ 4
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -785,6 +785,9 @@ issue.status.TO_REVIEW=To Review
issue.status.IN_REVIEW=In Review
issue.status.REVIEWED=Reviewed

issue.scope.MAIN=Main code
issue.scope.TEST=Test code

issue.resolution.FALSE-POSITIVE=False Positive
issue.resolution.FALSE-POSITIVE.description=Issues that manual review determined were False Positives. Effort from these issues is ignored.
issue.resolution.FIXED=Fixed
@@ -849,6 +852,7 @@ issue.changelog.field.file=File
#------------------------------------------------------------------------------
issues.facet.types=Type
issues.facet.severities=Severity
issues.facet.scopes=Scope
issues.facet.projects=Project
issues.facet.statuses=Status
issues.facet.hotspotStatuses=Hotspot Status

Loading…
Cancel
Save