From 3914a4b0a3936faf140f7343e368fd28aca90004 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Fri, 10 Jul 2020 17:04:41 +0200 Subject: [PATCH] SONAR-13597 Add scope distribution to issues page --- .gitignore | 1 + .../components/ComponentBreadcrumbs.tsx | 4 + .../__tests__/ComponentBreadcrumbs-test.tsx | 2 + .../ComponentBreadcrumbs-test.tsx.snap | 8 + .../js/apps/issues/sidebar/ScopeFacet.tsx | 96 ++++++++ .../main/js/apps/issues/sidebar/Sidebar.tsx | 9 + .../sidebar/__tests__/ScopeFacet-test.tsx | 94 ++++++++ .../__snapshots__/ScopeFacet-test.tsx.snap | 210 ++++++++++++++++++ .../__snapshots__/Sidebar-test.tsx.snap | 7 + .../src/main/js/apps/issues/styles.css | 2 +- .../src/main/js/apps/issues/utils.ts | 3 + .../src/main/js/helpers/constants.ts | 7 +- server/sonar-web/src/main/js/types/issues.ts | 5 + .../resources/org/sonar/l10n/core.properties | 4 + 14 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/ScopeFacet-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ScopeFacet-test.tsx.snap diff --git a/.gitignore b/.gitignore index efb7abc3de6..37062d90dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ scripts/patches/*license*.txt !scripts/patches/debug_ce.sh !scripts/patches/debug_web.sh !scripts/patches/postgres.sh +gherkin-features/ diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx index b0e134740ce..0b0cfdc2cd0 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx @@ -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 (
+ + {displayOrganization && } {displayProject && ( diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx index d2fb5d9295f..0b2dd2ca53f 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx @@ -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', diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap index 82ae1824db0..33e198ab34e 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap @@ -4,6 +4,10 @@ exports[`renders 1`] = `
+ + ) => void; + onToggle: (property: string) => void; + open: boolean; + scopes: string[]; + stats: T.Dict | undefined; +} + +export default function ScopeFacet(props: ScopeFacetProps) { + const { fetching, open, scopes = [], stats = {} } = props; + const values = scopes.map(scope => translate('issue.scope', scope)); + + return ( + + props.onChange({ scopes: [] })} + onClick={() => props.onToggle('scopes')} + open={open} + values={values} + /> + + {open && ( + <> + + {SOURCE_SCOPES.map(({ scope, qualifier }) => { + const active = scopes.includes(scope); + const stat = stats[scope]; + + return ( + + {' '} + {translate('issue.scope', scope)} + + } + 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} + /> + ); + })} + + + + )} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx index 8890583195b..878cac3cae8 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx @@ -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 { severities={query.severities} stats={facets.severities} /> + { + 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, + scope: IssueScope, + multiple = false +) { + return wrapper + .find(FacetItem) + .filterWhere(f => f.key() === scope) + .props() + .onClick(scope, multiple); +} + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ScopeFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ScopeFacet-test.tsx.snap new file mode 100644 index 00000000000..ca871a44fa6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ScopeFacet-test.tsx.snap @@ -0,0 +1,210 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: active facet 1`] = ` + + + + + + + issue.scope.MAIN + + } + onClick={[Function]} + value="MAIN" + /> + + + + issue.scope.TEST + + } + onClick={[Function]} + value="TEST" + /> + + + +`; + +exports[`should render correctly: default 1`] = ` + + + +`; + +exports[`should render correctly: disabled facet 1`] = ` + + + + + + + issue.scope.MAIN + + } + onClick={[Function]} + stat={0} + value="MAIN" + /> + + + + issue.scope.TEST + + } + onClick={[Function]} + value="TEST" + /> + + + +`; + +exports[`should render correctly: open 1`] = ` + + + + + + + issue.scope.MAIN + + } + onClick={[Function]} + value="MAIN" + /> + + + + issue.scope.TEST + + } + onClick={[Function]} + value="TEST" + /> + + + +`; 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 09df17ecf9b..5168fd8c58e 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 @@ -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", 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 1c166a3a065..69417bf91e4 100644 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ b/server/sonar-web/src/main/js/apps/issues/styles.css @@ -291,7 +291,7 @@ } .issues-workspace-list-component { - padding: 10px 10px 6px; + padding: 10px 0 6px; } .issues-workspace-list-item + .issues-workspace-list-item { 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 f486bf64fb5..5d25e6d22a0 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -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), diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index 2e101a490d0..5bf81e56973 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -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']; diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts index a9d2ae5cb8b..ee0613d6a57 100644 --- a/server/sonar-web/src/main/js/types/issues.ts +++ b/server/sonar-web/src/main/js/types/issues.ts @@ -24,3 +24,8 @@ export enum IssueType { Bug = 'BUG', SecurityHotspot = 'SECURITY_HOTSPOT' } + +export enum IssueScope { + Main = 'MAIN', + Test = 'TEST' +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 71c985dbd21..219683c2bc4 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 -- 2.39.5