Bläddra i källkod

SONAR-20871 Add new status facet in issues list

tags/10.4.0.87286
stanislavh 7 månader sedan
förälder
incheckning
a1be2cd128
36 ändrade filer med 381 tillägg och 528 borttagningar
  1. 7
    14
      server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
  2. 2
    1
      server/sonar-web/src/main/js/api/mocks/data/issues.ts
  3. 2
    2
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  4. 2
    1
      server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
  5. 3
    2
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
  6. 4
    3
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx
  7. 2
    12
      server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
  8. 67
    8
      server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
  9. 3
    5
      server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
  10. 3
    5
      server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx
  11. 0
    102
      server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx
  12. 2
    1
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
  13. 0
    143
      server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
  14. 6
    19
      server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
  15. 109
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/SimpleStatusFacet.tsx
  16. 0
    139
      server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
  17. 3
    6
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
  18. 4
    4
      server/sonar-web/src/main/js/apps/issues/test-utils.tsx
  19. 72
    11
      server/sonar-web/src/main/js/apps/issues/utils.ts
  20. 2
    2
      server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx
  21. 6
    2
      server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx
  22. 2
    1
      server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx
  23. 6
    2
      server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx
  24. 2
    2
      server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx
  25. 2
    2
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
  26. 3
    2
      server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts
  27. 2
    1
      server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx
  28. 2
    2
      server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts
  29. 27
    22
      server/sonar-web/src/main/js/components/shared/utils.ts
  30. 3
    2
      server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
  31. 15
    1
      server/sonar-web/src/main/js/helpers/constants.ts
  32. 1
    3
      server/sonar-web/src/main/js/helpers/mocks/issues.ts
  33. 6
    1
      server/sonar-web/src/main/js/helpers/query.ts
  34. 2
    2
      server/sonar-web/src/main/js/helpers/testMocks.ts
  35. 2
    1
      server/sonar-web/src/main/js/helpers/urls.ts
  36. 7
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 7
- 14
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts Visa fil

@@ -20,13 +20,7 @@
import { cloneDeep, uniqueId } from 'lodash';
import { RuleDescriptionSections } from '../../apps/coding-rules/rule';

import {
ISSUE_TYPES,
RESOLUTIONS,
SEVERITIES,
SOURCE_SCOPES,
STATUSES,
} from '../../helpers/constants';
import { ISSUE_TYPES, SEVERITIES, SIMPLE_STATUSES, SOURCE_SCOPES } from '../../helpers/constants';
import { mockIssueAuthors, mockIssueChangelog } from '../../helpers/mocks/issues';
import { RequestData } from '../../helpers/request';
import { getStandards } from '../../helpers/security-standard';
@@ -336,9 +330,8 @@ export default class IssuesServiceMock {
property: name,
values: (
{
resolutions: RESOLUTIONS,
severities: SEVERITIES,
statuses: STATUSES,
simpleStatuses: SIMPLE_STATUSES,
types: ISSUE_TYPES,
scopes: SOURCE_SCOPES.map(({ scope }) => scope),
projects: ['org.project1', 'org.project2'],
@@ -389,6 +382,11 @@ export default class IssuesServiceMock {

// Filter list (only supports assignee, type and severity)
const filteredList = this.list
.filter(
(item) =>
!query.simpleStatuses ||
query.simpleStatuses.split(',').includes(item.issue.simpleStatus),
)
.filter((item) => {
if (!query.cleanCodeAttributeCategories) {
return true;
@@ -448,13 +446,8 @@ export default class IssuesServiceMock {
(item) => !query.severities || query.severities.split(',').includes(item.issue.severity),
)
.filter((item) => !query.scopes || query.scopes.split(',').includes(item.issue.scope))
.filter((item) => !query.statuses || query.statuses.split(',').includes(item.issue.status))
.filter((item) => !query.projects || query.projects.split(',').includes(item.issue.project))
.filter((item) => !query.rules || query.rules.split(',').includes(item.issue.rule))
.filter(
(item) =>
!query.resolutions || query.resolutions.split(',').includes(item.issue.resolution),
)
.filter(
(item) =>
!query.inNewCodePeriod || new Date(item.issue.creationDate) > new Date('2023-01-10'),

+ 2
- 1
server/sonar-web/src/main/js/api/mocks/data/issues.ts Visa fil

@@ -317,10 +317,10 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
impacts: [
{ softwareQuality: SoftwareQuality.Security, severity: SoftwareImpactSeverity.High },
],
ruleDescriptionContextKey: 'spring',
resolution: IssueResolution.Unresolved,
status: IssueStatus.Open,
simpleStatus: IssueSimpleStatus.Open,
ruleDescriptionContextKey: 'spring',
}),
snippets: keyBy(
[
@@ -347,6 +347,7 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
},
resolution: IssueResolution.Fixed,
status: IssueStatus.Confirmed,
simpleStatus: IssueSimpleStatus.Confirmed,
}),
snippets: keyBy(
[

+ 2
- 2
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx Visa fil

@@ -87,7 +87,7 @@ it('should render the component nav correctly for portfolio', async () => {
expect(await ui.portfolioTitle.find()).toHaveAttribute('href', '/portfolio?id=portfolioKey');
expect(ui.issuesPageLink.get()).toHaveAttribute(
'href',
'/project/issues?id=portfolioKey&resolved=false',
'/project/issues?id=portfolioKey&simpleStatuses=OPEN%2CCONFIRMED',
);
expect(ui.measuresPageLink.get()).toHaveAttribute('href', '/component_measures?id=portfolioKey');
expect(ui.activityPageLink.get()).toHaveAttribute('href', '/project/activity?id=portfolioKey');
@@ -119,7 +119,7 @@ it('should render the component nav correctly for projects', async () => {
expect(ui.overviewPageLink.get()).toHaveAttribute('href', '/dashboard?id=project-key');
expect(ui.issuesPageLink.get()).toHaveAttribute(
'href',
'/project/issues?id=project-key&resolved=false',
'/project/issues?id=project-key&simpleStatuses=OPEN%2CCONFIRMED',
);
expect(ui.hotspotsPageLink.get()).toHaveAttribute('href', '/security_hotspots?id=project-key');
expect(ui.measuresPageLink.get()).toHaveAttribute('href', '/component_measures?id=project-key');

+ 2
- 1
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx Visa fil

@@ -27,6 +27,7 @@ import {
} from 'design-system';
import * as React from 'react';
import Tooltip from '../../../../components/controls/Tooltip';
import { DEFAULT_ISSUES_QUERY } from '../../../../components/shared/utils';
import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like';
import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n';
import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls';
@@ -183,7 +184,7 @@ export function Menu(props: Props) {
return renderMenuLink({
label: translate('issues.page'),
pathname: '/project/issues',
additionalQueryParams: { resolved: 'false' },
additionalQueryParams: DEFAULT_ISSUES_QUERY,
});
};


+ 3
- 2
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx Visa fil

@@ -23,6 +23,7 @@ import * as React from 'react';
import { NavLink } from 'react-router-dom';
import { isMySet } from '../../../../apps/issues/utils';
import Link from '../../../../components/common/Link';
import { DEFAULT_ISSUES_QUERY } from '../../../../components/shared/utils';
import { translate } from '../../../../helpers/l10n';
import { getQualityGatesUrl } from '../../../../helpers/urls';
import { AppState } from '../../../../types/appstate';
@@ -71,8 +72,8 @@ class GlobalNavMenu extends React.PureComponent<Props> {
renderIssuesLink() {
const search = (
this.props.currentUser.isLoggedIn && isMySet()
? new URLSearchParams({ resolved: 'false', myIssues: 'true' })
: new URLSearchParams({ resolved: 'false' })
? new URLSearchParams({ myIssues: 'true', ...DEFAULT_ISSUES_QUERY })
: new URLSearchParams(DEFAULT_ISSUES_QUERY)
).toString();

return (

+ 4
- 3
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx Visa fil

@@ -27,6 +27,7 @@ import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../../app/components/available-features/withAvailableFeatures';
import Tooltip from '../../../components/controls/Tooltip';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
import { translate } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { getIssuesUrl } from '../../../helpers/urls';
@@ -81,7 +82,7 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> {
this.setState({ loading: true });
getFacet(
{
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
rules: key,
},
FacetName.Projects,
@@ -139,7 +140,7 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> {
if (total === undefined) {
return null;
}
const path = getIssuesUrl({ resolved: 'false', rules: key });
const path = getIssuesUrl({ ...DEFAULT_ISSUES_QUERY, rules: key });

const totalItem = (
<span className="little-spacer-left">
@@ -163,7 +164,7 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> {
ruleDetails: { key },
} = this.props;

const path = getIssuesUrl({ resolved: 'false', rules: key, projects: project.key });
const path = getIssuesUrl({ ...DEFAULT_ISSUES_QUERY, rules: key, projects: project.key });
return (
<TableRow key={project.key}>
<ContentCell>{project.name}</ContentCell>

+ 2
- 12
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx Visa fil

@@ -86,17 +86,11 @@ describe('issues app filtering', () => {
await user.click(ui.mainScopeFilter.get());
expect(ui.issueItem4.query()).not.toBeInTheDocument();

// Resolution
await user.click(ui.resolutionFacet.get());
await user.click(ui.fixedResolutionFilter.get());
expect(ui.issueItem2.query()).not.toBeInTheDocument();

// Check that filters were applied as expected
expect(ui.issueItem6.get()).toBeInTheDocument();

// Status
await user.click(ui.statusFacet.get());

await user.click(ui.simpleStatusFacet.get());
await user.click(ui.openStatusFilter.get());
expect(ui.issueItem6.query()).not.toBeInTheDocument(); // Issue 6 should vanish

@@ -106,9 +100,6 @@ describe('issues app filtering', () => {
await user.keyboard('{/Control}');
expect(ui.issueItem6.get()).toBeInTheDocument(); // Issue 6 should come back

// Clear resolution filter
await user.click(ui.clearResolutionFacet.get());

// Rule
await user.click(ui.ruleFacet.get());
await user.click(screen.getByRole('checkbox', { name: 'other' }));
@@ -154,7 +145,6 @@ describe('issues app filtering', () => {
await user.click(ui.clearIssueTypeFacet.get());
await user.click(ui.clearSeverityFacet.get());
await user.click(ui.clearScopeFacet.get());
await user.click(ui.clearStatusFacet.get());
await user.click(ui.clearRuleFacet.get());
await user.click(ui.clearTagFacet.get());
await user.click(ui.clearProjectFacet.get());
@@ -360,7 +350,7 @@ describe('issues app when reindexing', () => {
expect(ui.resolutionFacet.query()).not.toBeInTheDocument();
expect(ui.ruleFacet.query()).not.toBeInTheDocument();
expect(ui.scopeFacet.query()).not.toBeInTheDocument();
expect(ui.statusFacet.query()).not.toBeInTheDocument();
expect(ui.simpleStatusFacet.query()).not.toBeInTheDocument();
expect(ui.tagFacet.query()).not.toBeInTheDocument();

// Indexation message

+ 67
- 8
server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts Visa fil

@@ -22,6 +22,7 @@ import {
SoftwareImpactSeverity,
SoftwareQuality,
} from '../../../types/clean-code-taxonomy';
import { IssueSimpleStatus } from '../../../types/issues';
import { SecurityStandard } from '../../../types/security';
import {
parseQuery,
@@ -62,15 +63,13 @@ describe('serialize/deserialize', () => {
'owaspAsvs-4.0': ['2'],
owaspAsvsLevel: '2',
projects: ['a', 'b'],
resolutions: ['a', 'b'],
resolved: true,
rules: ['a', 'b'],
sort: 'rules',
scopes: ['a', 'b'],
severities: ['a', 'b'],
inNewCodePeriod: true,
sonarsourceSecurity: ['a', 'b'],
statuses: ['a', 'b'],
simpleStatuses: [IssueSimpleStatus.Accepted, IssueSimpleStatus.Confirmed],
tags: ['a', 'b'],
types: ['a', 'b'],
}),
@@ -97,14 +96,13 @@ describe('serialize/deserialize', () => {
'owaspAsvs-4.0': '2',
owaspAsvsLevel: '2',
projects: 'a,b',
resolutions: 'a,b',
rules: 'a,b',
s: 'rules',
scopes: 'a,b',
inNewCodePeriod: 'true',
severities: 'a,b',
sonarsourceSecurity: 'a,b',
statuses: 'a,b',
simpleStatuses: 'ACCEPTED,CONFIRMED',
tags: 'a,b',
types: 'a,b',
});
@@ -146,18 +144,79 @@ describe('serialize/deserialize', () => {
'pciDss-3.2': [],
'pciDss-4.0': [],
projects: [],
resolutions: [],
resolved: true,
rules: [],
scopes: [],
severities: ['CRITICAL', 'MAJOR'],
sonarsourceSecurity: [],
sort: '',
statuses: [],
simpleStatuses: [],
tags: [],
types: [],
});
});

it('should map deprecated status and resolution query to new simple statuses', () => {
expect(parseQuery({ statuses: 'OPEN' }).simpleStatuses).toEqual([IssueSimpleStatus.Open]);
expect(parseQuery({ statuses: 'REOPENED' }).simpleStatuses).toEqual([IssueSimpleStatus.Open]);
expect(parseQuery({ statuses: 'CONFIRMED' }).simpleStatuses).toEqual([
IssueSimpleStatus.Confirmed,
]);
expect(parseQuery({ statuses: 'RESOLVED' }).simpleStatuses).toEqual([
IssueSimpleStatus.Fixed,
IssueSimpleStatus.Accepted,
IssueSimpleStatus.FalsePositive,
]);
expect(parseQuery({ statuses: 'OPEN,REOPENED' }).simpleStatuses).toEqual([
IssueSimpleStatus.Open,
]);
expect(parseQuery({ statuses: 'OPEN,CONFIRMED' }).simpleStatuses).toEqual([
IssueSimpleStatus.Open,
IssueSimpleStatus.Confirmed,
]);

// Resolutions
expect(parseQuery({ resolutions: 'FALSE-POSITIVE' }).simpleStatuses).toEqual([
IssueSimpleStatus.FalsePositive,
]);
expect(parseQuery({ resolutions: 'WONTFIX' }).simpleStatuses).toEqual([
IssueSimpleStatus.Accepted,
]);
expect(parseQuery({ resolutions: 'REMOVED' }).simpleStatuses).toEqual([
IssueSimpleStatus.Fixed,
]);
expect(parseQuery({ resolutions: 'REMOVED,WONTFIX,FALSE-POSITIVE' }).simpleStatuses).toEqual([
IssueSimpleStatus.Fixed,
IssueSimpleStatus.Accepted,
IssueSimpleStatus.FalsePositive,
]);

// Both statuses and resolutions
expect(
parseQuery({ resolutions: 'FALSE-POSITIVE', statuses: 'RESOLVED' }).simpleStatuses,
).toEqual([IssueSimpleStatus.FalsePositive]);
expect(parseQuery({ resolutions: 'WONTFIX', statuses: 'RESOLVED' }).simpleStatuses).toEqual([
IssueSimpleStatus.Accepted,
]);

// With resolved=false
expect(
parseQuery({ resolutions: 'WONTFIX', statuses: 'RESOLVED', resolved: 'false' })
.simpleStatuses,
).toEqual([IssueSimpleStatus.Accepted, IssueSimpleStatus.Open, IssueSimpleStatus.Confirmed]);
expect(parseQuery({ statuses: 'OPEN', resolved: 'false' }).simpleStatuses).toEqual([
IssueSimpleStatus.Open,
]);

// With simple status
expect(
parseQuery({
resolutions: 'WONTFIX',
statuses: 'RESOLVED',
resolved: 'false',
simpleStatuses: 'FIXED',
}).simpleStatuses,
).toEqual([IssueSimpleStatus.Fixed]);
});
});

describe('shouldOpenStandardsFacet', () => {

+ 3
- 5
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx Visa fil

@@ -51,6 +51,7 @@ import withIndexationGuard from '../../../components/hoc/withIndexationGuard';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import IssueTabViewer from '../../../components/rules/IssueTabViewer';
import '../../../components/search-navigator.css';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
import Spinner from '../../../components/ui/Spinner';
import { fillBranchLike, getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication';
@@ -148,7 +149,6 @@ export interface State {
selectedLocationIndex?: number;
}

const DEFAULT_QUERY = { resolved: 'false' };
const MAX_INITAL_FETCH = 1000;
const VARIANTS_FACET = 'codeVariants';
const ISSUES_PAGE_SIZE = 100;
@@ -700,7 +700,7 @@ export class App extends React.PureComponent<Props, State> {
isFiltered = () => {
const serialized = serializeQuery(this.state.query);

return !areQueriesEqual(serialized, DEFAULT_QUERY);
return !areQueriesEqual(serialized, DEFAULT_ISSUES_QUERY);
};

getCheckedIssues = () => {
@@ -828,7 +828,7 @@ export class App extends React.PureComponent<Props, State> {
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...DEFAULT_QUERY,
...DEFAULT_ISSUES_QUERY,
...getBranchLikeQuery(this.props.branchLike),
id: this.props.component?.key,
myIssues: this.state.myIssues ? 'true' : undefined,
@@ -1156,7 +1156,6 @@ export class App extends React.PureComponent<Props, State> {
checked={this.state.checked}
component={component}
issues={issues}
onFilterChange={this.handleFilterChange}
onIssueChange={this.handleIssueChange}
onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined}
onIssueSelect={this.selectIssue}
@@ -1234,7 +1233,6 @@ export class App extends React.PureComponent<Props, State> {
{this.renderHeader({ openIssue, paging })}

<Spinner loading={loadingRule}>
{/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner */}
{openIssue && openRuleDetails ? (
<IssueTabViewer
activityTabContent={

+ 3
- 5
server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx Visa fil

@@ -19,18 +19,17 @@
*/
import { groupBy } from 'lodash';
import * as React from 'react';
import IssueItem from '../../../components/issue/Issue';
import { BranchLike } from '../../../types/branch-like';
import { Component, Issue } from '../../../types/types';
import { Query } from '../utils';
import ComponentBreadcrumbs from './ComponentBreadcrumbs';
import ListItem from './ListItem';

interface Props {
branchLike: BranchLike | undefined;
checked: string[];
component: Component | undefined;
issues: Issue[];
onFilterChange: (changes: Partial<Query>) => void;
onIssueChange: (issue: Issue) => void;
onIssueCheck: ((issueKey: string) => void) | undefined;
onIssueSelect: (issueKey: string) => void;
@@ -69,7 +68,7 @@ export default class IssuesList extends React.PureComponent<Props, State> {
</li>
<ul>
{issues.map((issue) => (
<ListItem
<IssueItem
branchLike={branchLike}
checked={checked.includes(issue.key)}
issue={issue}
@@ -77,7 +76,6 @@ export default class IssuesList extends React.PureComponent<Props, State> {
onChange={this.props.onIssueChange}
onCheck={this.props.onIssueCheck}
onSelect={this.props.onIssueSelect}
onFilterChange={this.props.onFilterChange}
onPopupToggle={this.props.onPopupToggle}
openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : undefined}
selected={selectedIssue != null && selectedIssue.key === issue.key}

+ 0
- 102
server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx Visa fil

@@ -1,102 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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 Issue from '../../../components/issue/Issue';
import { BranchLike } from '../../../types/branch-like';
import { Issue as TypeIssue } from '../../../types/types';
import { Query } from '../utils';

interface Props {
branchLike: BranchLike | undefined;
checked: boolean;
issue: TypeIssue;
onChange: (issue: TypeIssue) => void;
onCheck: ((issueKey: string) => void) | undefined;
onSelect: (issueKey: string) => void;
onFilterChange: (changes: Partial<Query>) => void;
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void;
openPopup: string | undefined;
selected: boolean;
}

export default class ListItem extends React.PureComponent<Props> {
handleFilter = (property: string, issue: TypeIssue) => {
const { onFilterChange } = this.props;

const issuesReset = { issues: [] };

if (property.startsWith('tag###')) {
const tag = property.substring('tag###'.length);
onFilterChange({ ...issuesReset, tags: [tag] });
} else {
switch (property) {
case 'type':
onFilterChange({ ...issuesReset, types: [issue.type] });
break;
case 'severity':
onFilterChange({ ...issuesReset, severities: [issue.severity] });
break;
case 'status':
onFilterChange({ ...issuesReset, statuses: [issue.status] });
break;
case 'resolution':
if (issue.resolution) {
onFilterChange({ ...issuesReset, resolved: true, resolutions: [issue.resolution] });
} else {
onFilterChange({ ...issuesReset, resolved: false, resolutions: [] });
}
break;
case 'assignee':
if (issue.assignee) {
onFilterChange({ ...issuesReset, assigned: true, assignees: [issue.assignee] });
} else {
onFilterChange({ ...issuesReset, assigned: false, assignees: [] });
}
break;
case 'rule':
onFilterChange({ ...issuesReset, rules: [issue.rule] });
break;
case 'project':
onFilterChange({ ...issuesReset, projects: [issue.projectKey] });
break;
case 'file':
onFilterChange({ ...issuesReset, files: [issue.componentUuid] });
}
}
};

render() {
const { branchLike, issue } = this.props;

return (
<Issue
branchLike={branchLike}
checked={this.props.checked}
issue={issue}
onChange={this.props.onChange}
onCheck={this.props.onCheck}
onSelect={this.props.onSelect}
onPopupToggle={this.props.onPopupToggle}
openPopup={this.props.openPopup}
selected={this.props.selected}
/>
);
}
}

+ 2
- 1
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx Visa fil

@@ -37,6 +37,7 @@ import { ComponentContext } from '../../../app/components/componentContext/Compo
import { useCurrentUser } from '../../../app/components/current-user/CurrentUserContext';
import Tooltip from '../../../components/controls/Tooltip';
import { ClipboardBase } from '../../../components/controls/clipboard';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
import { getBranchLikeQuery, isBranch, isPullRequest } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path';
@@ -181,7 +182,7 @@ export function IssueSourceViewerHeader(props: Readonly<Props>) {
to={getComponentIssuesUrl(project, {
...getBranchLikeQuery(branchLike),
files: path,
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
})}
>
{translate('source_viewer.view_all_issues')}

+ 0
- 143
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx Visa fil

@@ -1,143 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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 { FacetBox, FacetItem } from 'design-system';
import { orderBy, without } from 'lodash';
import * as React from 'react';
import { RESOLUTIONS } from '../../../helpers/constants';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
import { Query, formatFacetStat } from '../utils';
import { FacetItemsColumns } from './FacetItemsColumns';
import { MultipleSelectionHint } from './MultipleSelectionHint';

interface Props {
fetching: boolean;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
resolved: boolean;
resolutions: string[];
stats: Dict<number> | undefined;
}

export class ResolutionFacet extends React.PureComponent<Props> {
property = 'resolutions';

static defaultProps = {
open: true,
};

handleItemClick = (itemValue: string, multiple: boolean) => {
const { resolutions } = this.props;

if (itemValue === '') {
// unresolved
this.props.onChange({ resolved: !this.props.resolved, resolutions: [] });
} else if (multiple) {
const newValue = orderBy(
resolutions.includes(itemValue)
? without(resolutions, itemValue)
: [...resolutions, itemValue],
);

this.props.onChange({ resolved: true, [this.property]: newValue });
} else {
this.props.onChange({
resolved: true,
[this.property]:
resolutions.includes(itemValue) && resolutions.length < 2 ? [] : [itemValue],
});
}
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ resolved: false, resolutions: [] });
};

isFacetItemActive(resolution: string) {
return resolution === '' ? !this.props.resolved : this.props.resolutions.includes(resolution);
}

getFacetItemName(resolution: string) {
return resolution === '' ? translate('unresolved') : translate('issue.resolution', resolution);
}

getStat(resolution: string) {
const { stats } = this.props;

return stats ? stats[resolution] : undefined;
}

renderItem = (resolution: string) => {
const active = this.isFacetItemActive(resolution);
const stat = this.getStat(resolution);

return (
<FacetItem
active={active}
className="it__search-navigator-facet"
key={resolution}
name={this.getFacetItemName(resolution)}
onClick={this.handleItemClick}
stat={formatFacetStat(stat) ?? 0}
tooltip={this.getFacetItemName(resolution)}
value={resolution}
/>
);
};

render() {
const { fetching, open, resolutions } = this.props;

// below: -1 because "Unresolved" is mutually exclusive with the rest
const nbSelectableItems = RESOLUTIONS.filter(this.getStat.bind(this)).length - 1;

const nbSelectedItems = resolutions.length;
const headerId = `facet_${this.property}`;

return (
<FacetBox
className="it__search-navigator-facet-box it__search-navigator-facet-header"
clearIconLabel={translate('clear')}
count={nbSelectedItems}
countLabel={translateWithParameters('x_selected', nbSelectedItems)}
data-property={this.property}
id={headerId}
loading={fetching}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={open}
>
<FacetItemsColumns>{RESOLUTIONS.map(this.renderItem)}</FacetItemsColumns>

<MultipleSelectionHint
nbSelectableItems={nbSelectableItems}
nbSelectedItems={nbSelectedItems}
/>
</FacetBox>
);
}
}

+ 6
- 19
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx Visa fil

@@ -52,13 +52,12 @@ import { FileFacet } from './FileFacet';
import { LanguageFacet } from './LanguageFacet';
import { PeriodFilter } from './PeriodFilter';
import { ProjectFacet } from './ProjectFacet';
import { ResolutionFacet } from './ResolutionFacet';
import { RuleFacet } from './RuleFacet';
import { ScopeFacet } from './ScopeFacet';
import { SeverityFacet } from './SeverityFacet';
import { SimpleStatusFacet } from './SimpleStatusFacet';
import { SoftwareQualityFacet } from './SoftwareQualityFacet';
import { StandardFacet } from './StandardFacet';
import { StatusFacet } from './StatusFacet';
import { TagFacet } from './TagFacet';
import { TypeFacet } from './TypeFacet';
import { VariantFacet } from './VariantFacet';
@@ -246,25 +245,13 @@ export class SidebarClass extends React.PureComponent<Props> {

<BasicSeparator className="sw-my-4" />

<ResolutionFacet
fetching={this.props.loadingFacets.resolutions === true}
<SimpleStatusFacet
fetching={this.props.loadingFacets.simpleStatuses === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.resolutions}
resolutions={query.resolutions}
resolved={query.resolved}
stats={facets.resolutions}
/>

<BasicSeparator className="sw-my-4" />

<StatusFacet
fetching={this.props.loadingFacets.statuses === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.statuses}
stats={facets.statuses}
statuses={query.statuses}
open={!!openFacets.simpleStatuses}
simpleStatuses={query.simpleStatuses}
stats={facets.simpleStatuses}
/>

<BasicSeparator className="sw-my-4" />

+ 109
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/SimpleStatusFacet.tsx Visa fil

@@ -0,0 +1,109 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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 { FacetBox, FacetItem } from 'design-system';
import { FacetItemsList } from './FacetItemsList';

import { isEqual, sortBy, without } from 'lodash';
import * as React from 'react';
import { useIntl } from 'react-intl';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
import { SIMPLE_STATUSES } from '../../../helpers/constants';
import { IssueSimpleStatus } from '../../../types/issues';
import { formatFacetStat } from '../utils';
import { MultipleSelectionHint } from './MultipleSelectionHint';
import { CommonProps } from './SimpleListStyleFacet';

interface Props extends CommonProps {
simpleStatuses: Array<IssueSimpleStatus>;
}

const property = 'simpleStatuses';
const headerId = `facet_${property}`;

const defaultStatuses = DEFAULT_ISSUES_QUERY.simpleStatuses.split(',') as IssueSimpleStatus[];

export function SimpleStatusFacet(props: Readonly<Props>) {
const { simpleStatuses = [], stats = {}, fetching, open, help, needIssueSync } = props;
const intl = useIntl();

const nbSelectableItems = SIMPLE_STATUSES.filter(
(item) => !defaultStatuses.includes(item) && stats[item],
).length;
const hasDefaultSelection = isEqual(sortBy(simpleStatuses), sortBy(defaultStatuses));
const nbSelectedItems = hasDefaultSelection ? 0 : simpleStatuses.length;

return (
<FacetBox
className="it__search-navigator-facet-box it__search-navigator-facet-header"
clearIconLabel={intl.formatMessage({ id: 'clear' })}
count={nbSelectedItems}
countLabel={intl.formatMessage({ id: 'x_selected' }, { '0': nbSelectedItems })}
data-property={property}
id={headerId}
loading={fetching}
name={intl.formatMessage({ id: `issues.facet.${property}` })}
onClear={() =>
props.onChange({
[property]: defaultStatuses,
})
}
onClick={() => props.onToggle(property)}
open={open}
help={help}
>
<FacetItemsList labelledby={headerId}>
{SIMPLE_STATUSES.map((item) => {
const active = simpleStatuses.includes(item);
const stat = stats[item];

return (
<FacetItem
active={active}
className="it__search-navigator-facet"
key={item}
name={intl.formatMessage({ id: `issue.simple_status.${item}` })}
onClick={(itemValue: IssueSimpleStatus, multiple) => {
if (multiple) {
props.onChange({
[property]: active
? without(simpleStatuses, itemValue)
: [...simpleStatuses, itemValue],
});
} else {
props.onChange({
[property]: active && simpleStatuses.length === 1 ? [] : [itemValue],
});
}
}}
stat={(!needIssueSync && formatFacetStat(stat)) ?? 0}
value={item}
/>
);
})}
</FacetItemsList>

<MultipleSelectionHint
nbSelectableItems={nbSelectableItems}
nbSelectedItems={simpleStatuses.length}
/>
</FacetBox>
);
}

+ 0
- 139
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx Visa fil

@@ -1,139 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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 {
FacetBox,
FacetItem,
StatusConfirmedIcon,
StatusOpenIcon,
StatusReopenedIcon,
StatusResolvedIcon,
} from 'design-system';
import { orderBy, without } from 'lodash';
import * as React from 'react';
import { STATUSES } from '../../../helpers/constants';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
import { Query, formatFacetStat } from '../utils';
import { FacetItemsColumns } from './FacetItemsColumns';
import { MultipleSelectionHint } from './MultipleSelectionHint';

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

export class StatusFacet extends React.PureComponent<Props> {
property = 'statuses';

static defaultProps = { open: true };

handleItemClick = (itemValue: string, multiple: boolean) => {
const { statuses } = this.props;

if (multiple) {
const newValue = orderBy(
statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue],
);

this.props.onChange({ [this.property]: newValue });
} else {
this.props.onChange({
[this.property]: statuses.includes(itemValue) && statuses.length < 2 ? [] : [itemValue],
});
}
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

handleClear = () => {
this.props.onChange({ [this.property]: [] });
};

getStat(status: string) {
const { stats } = this.props;

return stats ? stats[status] : undefined;
}

renderItem = (status: string) => {
const active = this.props.statuses.includes(status);
const stat = this.getStat(status);

return (
<FacetItem
active={active}
className="it__search-navigator-facet"
icon={
{
CLOSED: <StatusResolvedIcon />,
CONFIRMED: <StatusConfirmedIcon />,
OPEN: <StatusOpenIcon />,
REOPENED: <StatusReopenedIcon />,
RESOLVED: <StatusResolvedIcon />,
}[status]
}
key={status}
name={translate('issue.status', status)}
onClick={this.handleItemClick}
stat={formatFacetStat(stat) ?? 0}
tooltip={translate('issue.status', status)}
value={status}
/>
);
};

render() {
const { fetching, open, statuses } = this.props;

const nbSelectableItems = STATUSES.filter(this.getStat.bind(this)).length;
const nbSelectedItems = statuses.length;
const headerId = `facet_${this.property}`;

return (
<FacetBox
className="it__search-navigator-facet-box it__search-navigator-facet-header"
clearIconLabel={translate('clear')}
count={nbSelectedItems}
countLabel={translateWithParameters('x_selected', nbSelectedItems)}
data-property={this.property}
id={headerId}
loading={fetching}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={open}
>
<FacetItemsColumns>{STATUSES.map(this.renderItem)}</FacetItemsColumns>

<MultipleSelectionHint
nbSelectableItems={nbSelectableItems}
nbSelectedItems={nbSelectedItems}
/>
</FacetBox>
);
}
}

+ 3
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx Visa fil

@@ -51,8 +51,7 @@ it('should render correct facets for Application', () => {
'issues.facet.impactSeverities',
'issues.facet.types',
'issues.facet.scopes',
'issues.facet.resolutions',
'issues.facet.statuses',
'issues.facet.simpleStatuses',
'issues.facet.standards',
'issues.facet.createdAt',
'issues.facet.languages',
@@ -74,8 +73,7 @@ it('should render correct facets for Portfolio', () => {
'issues.facet.impactSeverities',
'issues.facet.types',
'issues.facet.scopes',
'issues.facet.resolutions',
'issues.facet.statuses',
'issues.facet.simpleStatuses',
'issues.facet.standards',
'issues.facet.createdAt',
'issues.facet.languages',
@@ -97,8 +95,7 @@ it('should render correct facets for SubPortfolio', () => {
'issues.facet.impactSeverities',
'issues.facet.types',
'issues.facet.scopes',
'issues.facet.resolutions',
'issues.facet.statuses',
'issues.facet.simpleStatuses',
'issues.facet.standards',
'issues.facet.createdAt',
'issues.facet.languages',

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/test-utils.tsx Visa fil

@@ -74,7 +74,7 @@ export const ui = {
resolutionFacet: byRole('button', { name: 'issues.facet.resolutions' }),
ruleFacet: byRole('button', { name: 'issues.facet.rules' }),
scopeFacet: byRole('button', { name: 'issues.facet.scopes' }),
statusFacet: byRole('button', { name: 'issues.facet.statuses' }),
simpleStatusFacet: byRole('button', { name: 'issues.facet.simpleStatuses' }),
tagFacet: byRole('button', { name: 'issues.facet.tags' }),
typeFacet: byRole('button', { name: 'issues.facet.types' }),
cleanCodeAttributeCategoryFacet: byRole('button', {
@@ -97,7 +97,7 @@ export const ui = {
clearRuleFacet: byTestId('clear-issues.facet.rules'),
clearScopeFacet: byTestId('clear-issues.facet.scopes'),
clearSeverityFacet: byTestId('clear-issues.facet.impactSeverities'),
clearStatusFacet: byTestId('clear-issues.facet.statuses'),
clearSimpleStatusFacet: byTestId('clear-issues.facet.simpleStatuses'),
clearTagFacet: byTestId('clear-issues.facet.tags'),

responsibleCategoryFilter: byRole('checkbox', {
@@ -110,11 +110,11 @@ export const ui = {
name: `software_quality.${SoftwareQuality.Maintainability}`,
}),
codeSmellIssueTypeFilter: byRole('checkbox', { name: 'issue.type.CODE_SMELL' }),
confirmedStatusFilter: byRole('checkbox', { name: 'issue.status.CONFIRMED' }),
confirmedStatusFilter: byRole('checkbox', { name: 'issue.simple_status.CONFIRMED' }),
fixedResolutionFilter: byRole('checkbox', { name: 'issue.resolution.FIXED' }),
mainScopeFilter: byRole('checkbox', { name: 'issue.scope.MAIN' }),
mediumSeverityFilter: byRole('checkbox', { name: `severity.${SoftwareImpactSeverity.Medium}` }),
openStatusFilter: byRole('checkbox', { name: 'issue.status.OPEN' }),
openStatusFilter: byRole('checkbox', { name: 'issue.simple_status.OPEN' }),
vulnerabilityIssueTypeFilter: byRole('checkbox', { name: 'issue.type.VULNERABILITY' }),

clearAllFilters: byRole('button', { name: 'clear_all_filters' }),

+ 72
- 11
server/sonar-web/src/main/js/apps/issues/utils.ts Visa fil

@@ -17,8 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { isArray } from 'lodash';
import { intersection, isArray, uniq } from 'lodash';
import { getUsers } from '../../api/users';
import { DEFAULT_ISSUES_QUERY } from '../../components/shared/utils';
import { formatMeasure } from '../../helpers/measures';
import {
cleanQuery,
@@ -38,7 +39,13 @@ import {
SoftwareImpactSeverity,
SoftwareQuality,
} from '../../types/clean-code-taxonomy';
import { Facet, RawFacet } from '../../types/issues';
import {
Facet,
IssueResolution,
IssueSimpleStatus,
IssueStatus,
RawFacet,
} from '../../types/issues';
import { MetricType } from '../../types/metrics';
import { SecurityStandard } from '../../types/security';
import { Dict, Issue, Paging, RawQuery } from '../../types/types';
@@ -70,15 +77,13 @@ export interface Query {
[OWASP_ASVS_4_0]: string[];
owaspAsvsLevel: string;
projects: string[];
resolutions: string[];
resolved: boolean;
rules: string[];
scopes: string[];
severities: string[];
inNewCodePeriod: boolean;
sonarsourceSecurity: string[];
sort: string;
statuses: string[];
simpleStatuses: IssueSimpleStatus[];
tags: string[];
types: string[];
}
@@ -120,20 +125,78 @@ export function parseQuery(query: RawQuery): Query {
[OWASP_ASVS_4_0]: parseAsArray(query[OWASP_ASVS_4_0], parseAsString),
owaspAsvsLevel: parseAsString(query['owaspAsvsLevel']),
projects: parseAsArray(query.projects, parseAsString),
resolutions: parseAsArray(query.resolutions, parseAsString),
resolved: parseAsBoolean(query.resolved),
rules: parseAsArray(query.rules, parseAsString),
scopes: parseAsArray(query.scopes, parseAsString),
severities: parseAsArray(query.severities, parseAsString),
sonarsourceSecurity: parseAsArray(query.sonarsourceSecurity, parseAsString),
sort: parseAsSort(query.s),
statuses: parseAsArray(query.statuses, parseAsString),
simpleStatuses: parseSimpleStatuses(query),
tags: parseAsArray(query.tags, parseAsString),
types: parseAsArray(query.types, parseAsString),
codeVariants: parseAsArray(query.codeVariants, parseAsString),
};
}

function parseSimpleStatuses(query: RawQuery) {
let result: Array<IssueSimpleStatus> = [];

if (query.simpleStatuses) {
return parseAsArray<IssueSimpleStatus>(query.simpleStatuses, parseAsString);
}

const deprecatedStatusesMap = {
[IssueStatus.Open]: [IssueSimpleStatus.Open],
[IssueStatus.Confirmed]: [IssueSimpleStatus.Confirmed],
[IssueStatus.Reopened]: [IssueSimpleStatus.Open],
[IssueStatus.Resolved]: [
IssueSimpleStatus.Fixed,
IssueSimpleStatus.Accepted,
IssueSimpleStatus.FalsePositive,
],
[IssueStatus.Closed]: [IssueSimpleStatus.Fixed],
};
const deprecatedResolutionsMap = {
[IssueResolution.FalsePositive]: [IssueSimpleStatus.FalsePositive],
[IssueResolution.WontFix]: [IssueSimpleStatus.Accepted],
[IssueResolution.Fixed]: [IssueSimpleStatus.Fixed],
[IssueResolution.Removed]: [IssueSimpleStatus.Fixed],
[IssueResolution.Unresolved]: [IssueSimpleStatus.Open, IssueSimpleStatus.Confirmed],
};

const simpleStatusesFromStatuses = parseAsArray<IssueStatus>(query.statuses, parseAsString)
.map((status) => deprecatedStatusesMap[status])
.filter(Boolean)
.flat();
const simpleStatusesFromResolutions = parseAsArray<IssueResolution>(
query.resolutions,
parseAsString,
)
.map((status) => deprecatedResolutionsMap[status])
.filter(Boolean)
.flat();

const intesectedSimpleStatuses = intersection(
simpleStatusesFromStatuses,
simpleStatusesFromResolutions,
);
result = intesectedSimpleStatuses.length
? intesectedSimpleStatuses
: simpleStatusesFromResolutions.concat(simpleStatusesFromStatuses);

if (
query.resolved === 'false' &&
[IssueSimpleStatus.Open, IssueSimpleStatus.Confirmed].every(
(status) => !result.includes(status),
)
) {
result = result.concat(
parseAsArray<IssueSimpleStatus>(DEFAULT_ISSUES_QUERY.simpleStatuses, parseAsString),
);
}

return uniq(result);
}

export function getOpen(query: RawQuery): string | undefined {
return query.open;
}
@@ -167,8 +230,6 @@ export function serializeQuery(query: Query): RawQuery {
[OWASP_ASVS_4_0]: serializeStringArray(query[OWASP_ASVS_4_0]),
owaspAsvsLevel: serializeString(query['owaspAsvsLevel']),
projects: serializeStringArray(query.projects),
resolutions: serializeStringArray(query.resolutions),
resolved: query.resolved ? undefined : 'false',
rules: serializeStringArray(query.rules),
s: serializeString(query.sort),
scopes: serializeStringArray(query.scopes),
@@ -177,7 +238,7 @@ export function serializeQuery(query: Query): RawQuery {
impactSoftwareQualities: serializeStringArray(query.impactSoftwareQualities),
inNewCodePeriod: query.inNewCodePeriod ? 'true' : undefined,
sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity),
statuses: serializeStringArray(query.statuses),
simpleStatuses: serializeStringArray(query.simpleStatuses),
tags: serializeStringArray(query.tags),
types: serializeStringArray(query.types),
codeVariants: serializeStringArray(query.codeVariants),

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx Visa fil

@@ -21,6 +21,7 @@ import classNames from 'classnames';
import * as React from 'react';
import { useIntl } from 'react-intl';
import { getLeakValue } from '../../../components/measure/utils';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { findMeasure } from '../../../helpers/measures';
import {
@@ -62,7 +63,7 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
label={newViolations === '1' ? 'issue' : 'issues'}
url={getComponentIssuesUrl(component.key, {
...getBranchLikeQuery(branchLike),
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
})}
value={newViolations}
failedConditions={failedConditions}
@@ -104,7 +105,6 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
}
url={getComponentSecurityHotspotsUrl(component.key, {
...getBranchLikeQuery(branchLike),
resolved: 'false',
})}
value={newSecurityHotspots}
failedConditions={failedConditions}

+ 6
- 2
server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx Visa fil

@@ -21,7 +21,11 @@
import { ChevronRightIcon, DangerButtonSecondary } from 'design-system';
import React from 'react';
import { useIntl } from 'react-intl';
import { isIssueMeasure, propsToIssueParams } from '../../../components/shared/utils';
import {
DEFAULT_ISSUES_QUERY,
isIssueMeasure,
propsToIssueParams,
} from '../../../components/shared/utils';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { getLocalizedMetricName } from '../../../helpers/l10n';
import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures';
@@ -181,7 +185,7 @@ function getQGConditionUrl(
});
}
return getComponentIssuesUrl(componentKey, {
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
types: ratingIssueType,
...getBranchLikeQuery(branchLike),
...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}),

+ 2
- 1
server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx Visa fil

@@ -23,6 +23,7 @@ import * as React from 'react';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import Tooltip from '../../../components/controls/Tooltip';
import { getLeakValue } from '../../../components/measure/utils';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
@@ -58,7 +59,7 @@ export function IssueLabel(props: IssueLabelProps) {
const params = {
...getBranchLikeQuery(branchLike),
inNewCodePeriod: useDiffMetric ? 'true' : 'false',
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
types: type,
};


+ 6
- 2
server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx Visa fil

@@ -22,7 +22,11 @@ import * as React from 'react';
import { Path } from 'react-router-dom';
import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
import MeasureIndicator from '../../../components/measure/MeasureIndicator';
import { isIssueMeasure, propsToIssueParams } from '../../../components/shared/utils';
import {
DEFAULT_ISSUES_QUERY,
isIssueMeasure,
propsToIssueParams,
} from '../../../components/shared/utils';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures';
@@ -47,7 +51,7 @@ interface Props {
export default class QualityGateCondition extends React.PureComponent<Props> {
getIssuesUrl = (inNewCodePeriod: boolean, customQuery: Dict<string>) => {
const query: Dict<string | undefined> = {
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
...getBranchLikeQuery(this.props.branchLike),
...customQuery,
};

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx Visa fil

@@ -39,7 +39,7 @@ it('renders failed QG', () => {
expect(maintainabilityRatingLink).toBeInTheDocument();
expect(maintainabilityRatingLink).toHaveAttribute(
'href',
'/project/issues?resolved=false&types=CODE_SMELL&pullRequest=1001&sinceLeakPeriod=true&id=my-project',
'/project/issues?simpleStatuses=OPEN%2CCONFIRMED&types=CODE_SMELL&pullRequest=1001&sinceLeakPeriod=true&id=my-project',
);

// Security Hotspots rating condition
@@ -59,7 +59,7 @@ it('renders failed QG', () => {
expect(codeSmellsLink).toBeInTheDocument();
expect(codeSmellsLink).toHaveAttribute(
'href',
'/project/issues?resolved=false&types=CODE_SMELL&pullRequest=1001&id=my-project',
'/project/issues?simpleStatuses=OPEN%2CCONFIRMED&types=CODE_SMELL&pullRequest=1001&id=my-project',
);

// Conditions to cover

+ 2
- 2
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx Visa fil

@@ -37,7 +37,6 @@ import {
themeColor,
} from 'design-system';
import * as React from 'react';

import { getBranchLikeQuery } from '../../helpers/branch-like';
import { ISSUE_TYPES } from '../../helpers/constants';
import { ISSUETYPE_METRIC_KEYS_MAP } from '../../helpers/issues';
@@ -52,6 +51,7 @@ import {
getComponentIssuesUrl,
getComponentSecurityHotspotsUrl,
} from '../../helpers/urls';
import { DEFAULT_ISSUES_QUERY } from '../shared/utils';

import { ComponentQualifier } from '../../types/component';
import { IssueType } from '../../types/issues';
@@ -89,7 +89,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
const params = {
...getBranchLikeQuery(branchLike),
files: sourceViewerFile.path,
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
types: type,
};


+ 3
- 2
server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts Visa fil

@@ -23,6 +23,7 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { parseIssueFromResponse } from '../../../helpers/issues';
import { BranchLike } from '../../../types/branch-like';
import { Issue, RawQuery } from '../../../types/types';
import { DEFAULT_ISSUES_QUERY } from '../../shared/utils';

// maximum possible value
const PAGE_SIZE = 500;
@@ -32,16 +33,16 @@ const PAGE_MAX = 20;
function buildListQuery(component: string, branchLike: BranchLike | undefined) {
return {
component,
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
...getBranchLikeQuery(branchLike),
};
}

function buildSearchQuery(component: string, branchLike: BranchLike | undefined) {
return {
...DEFAULT_ISSUES_QUERY,
additionalFields: '_all',
componentKeys: component,
resolved: 'false',
s: 'FILE_LINE',
...getBranchLikeQuery(branchLike),
};

+ 2
- 1
server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx Visa fil

@@ -27,6 +27,7 @@ import { getComponentIssuesUrl, getIssuesUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { Issue } from '../../../types/types';
import { useLocation } from '../../hoc/withRouter';
import { DEFAULT_ISSUES_QUERY } from '../../shared/utils';

export interface IssueMessageProps {
issue: Issue;
@@ -48,7 +49,7 @@ export default function IssueMessage(props: IssueMessageProps) {
...getBranchLikeQuery(branchLike),
files: issue.componentLongName,
open: issue.key,
resolved: 'false',
...DEFAULT_ISSUES_QUERY,
why: '1',
});


+ 2
- 2
server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts Visa fil

@@ -23,13 +23,13 @@ import { propsToIssueParams } from '../utils';

describe('propsToIssueParams', () => {
it('should render correct default parameters', () => {
expect(propsToIssueParams('other')).toEqual({ resolved: 'false' });
expect(propsToIssueParams('other')).toEqual({ simpleStatuses: 'OPEN,CONFIRMED' });
});

it(`should render correct params`, () => {
expect(propsToIssueParams(MetricKey.false_positive_issues, true)).toEqual({
resolutions: 'FALSE-POSITIVE',
inNewCodePeriod: true,
simpleStatuses: 'FALSE_POSITIVE',
});
});
});

+ 27
- 22
server/sonar-web/src/main/js/components/shared/utils.ts Visa fil

@@ -17,7 +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 { IssueSimpleStatus } from '../../types/issues';
import { MetricKey } from '../../types/metrics';
import { Dict } from '../../types/types';

@@ -46,27 +46,31 @@ const ISSUE_MEASURES = [
MetricKey.new_vulnerabilities,
];

export const DEFAULT_ISSUES_QUERY = {
simpleStatuses: `${IssueSimpleStatus.Open},${IssueSimpleStatus.Confirmed}`,
};

const issueParamsPerMetric: Dict<Dict<string>> = {
[MetricKey.blocker_violations]: { resolved: 'false', severities: 'BLOCKER' },
[MetricKey.new_blocker_violations]: { resolved: 'false', severities: 'BLOCKER' },
[MetricKey.critical_violations]: { resolved: 'false', severities: 'CRITICAL' },
[MetricKey.new_critical_violations]: { resolved: 'false', severities: 'CRITICAL' },
[MetricKey.major_violations]: { resolved: 'false', severities: 'MAJOR' },
[MetricKey.new_major_violations]: { resolved: 'false', severities: 'MAJOR' },
[MetricKey.minor_violations]: { resolved: 'false', severities: 'MINOR' },
[MetricKey.new_minor_violations]: { resolved: 'false', severities: 'MINOR' },
[MetricKey.info_violations]: { resolved: 'false', severities: 'INFO' },
[MetricKey.new_info_violations]: { resolved: 'false', severities: 'INFO' },
[MetricKey.open_issues]: { resolved: 'false', statuses: 'OPEN' },
[MetricKey.reopened_issues]: { resolved: 'false', statuses: 'REOPENED' },
[MetricKey.confirmed_issues]: { resolved: 'false', statuses: 'CONFIRMED' },
[MetricKey.false_positive_issues]: { resolutions: 'FALSE-POSITIVE' },
[MetricKey.code_smells]: { resolved: 'false', types: 'CODE_SMELL' },
[MetricKey.new_code_smells]: { resolved: 'false', types: 'CODE_SMELL' },
[MetricKey.bugs]: { resolved: 'false', types: 'BUG' },
[MetricKey.new_bugs]: { resolved: 'false', types: 'BUG' },
[MetricKey.vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' },
[MetricKey.new_vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' },
[MetricKey.blocker_violations]: { severities: 'BLOCKER' },
[MetricKey.new_blocker_violations]: { severities: 'BLOCKER' },
[MetricKey.critical_violations]: { severities: 'CRITICAL' },
[MetricKey.new_critical_violations]: { severities: 'CRITICAL' },
[MetricKey.major_violations]: { severities: 'MAJOR' },
[MetricKey.new_major_violations]: { severities: 'MAJOR' },
[MetricKey.minor_violations]: { severities: 'MINOR' },
[MetricKey.new_minor_violations]: { severities: 'MINOR' },
[MetricKey.info_violations]: { severities: 'INFO' },
[MetricKey.new_info_violations]: { severities: 'INFO' },
[MetricKey.open_issues]: { simpleStatuses: IssueSimpleStatus.Open },
[MetricKey.reopened_issues]: { simpleStatuses: IssueSimpleStatus.Open },
[MetricKey.confirmed_issues]: { simpleStatuses: IssueSimpleStatus.Confirmed },
[MetricKey.false_positive_issues]: { simpleStatuses: IssueSimpleStatus.FalsePositive },
[MetricKey.code_smells]: { types: 'CODE_SMELL' },
[MetricKey.new_code_smells]: { types: 'CODE_SMELL' },
[MetricKey.bugs]: { types: 'BUG' },
[MetricKey.new_bugs]: { types: 'BUG' },
[MetricKey.vulnerabilities]: { types: 'VULNERABILITY' },
[MetricKey.new_vulnerabilities]: { types: 'VULNERABILITY' },
};

export function isIssueMeasure(metric: string) {
@@ -75,7 +79,8 @@ export function isIssueMeasure(metric: string) {

export function propsToIssueParams(metric: string, inNewCodePeriod = false) {
const params: Dict<string | boolean> = {
...(issueParamsPerMetric[metric] || { resolved: 'false' }),
...DEFAULT_ISSUES_QUERY,
...issueParamsPerMetric[metric],
};

if (inNewCodePeriod) {

+ 3
- 2
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts Visa fil

@@ -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 { DEFAULT_ISSUES_QUERY } from '../../components/shared/utils';
import { AlmKeys } from '../../types/alm-settings';
import { ComponentQualifier } from '../../types/component';
import { IssueType } from '../../types/issues';
@@ -102,10 +103,10 @@ describe('#getComponentIssuesUrl', () => {
});

it('should work with parameters', () => {
expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { resolved: 'false' })).toEqual(
expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, DEFAULT_ISSUES_QUERY)).toEqual(
expect.objectContaining({
pathname: '/project/issues',
search: queryToSearch({ resolved: 'false', id: SIMPLE_COMPONENT_KEY }),
search: queryToSearch({ ...DEFAULT_ISSUES_QUERY, id: SIMPLE_COMPONENT_KEY }),
}),
);
});

+ 15
- 1
server/sonar-web/src/main/js/helpers/constants.ts Visa fil

@@ -25,7 +25,13 @@ import {
SoftwareQuality,
} from '../types/clean-code-taxonomy';
import { ComponentQualifier } from '../types/component';
import { IssueResolution, IssueScope, IssueSeverity, IssueType } from '../types/issues';
import {
IssueResolution,
IssueScope,
IssueSeverity,
IssueSimpleStatus,
IssueType,
} from '../types/issues';
import { RuleType } from '../types/types';

export const SEVERITIES = Object.values(IssueSeverity);
@@ -38,6 +44,14 @@ export const SOFTWARE_QUALITIES = Object.values(SoftwareQuality);

export const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED'];

export const SIMPLE_STATUSES = [
IssueSimpleStatus.Open,
IssueSimpleStatus.Accepted,
IssueSimpleStatus.FalsePositive,
IssueSimpleStatus.Confirmed,
IssueSimpleStatus.Fixed,
];

export const ISSUE_TYPES: IssueType[] = [
IssueType.Bug,
IssueType.Vulnerability,

+ 1
- 3
server/sonar-web/src/main/js/helpers/mocks/issues.ts Visa fil

@@ -75,8 +75,6 @@ export function mockQuery(overrides: Partial<Query> = {}): Query {
'owaspAsvs-4.0': [],
owaspAsvsLevel: '',
projects: [],
resolutions: [],
resolved: false,
rules: [],
scopes: [],
severities: [],
@@ -84,8 +82,8 @@ export function mockQuery(overrides: Partial<Query> = {}): Query {
impactSoftwareQualities: [],
inNewCodePeriod: false,
sonarsourceSecurity: [],
simpleStatuses: [],
sort: '',
statuses: [],
tags: [],
types: [],
...overrides,

+ 6
- 1
server/sonar-web/src/main/js/helpers/query.ts Visa fil

@@ -29,7 +29,12 @@ export function queriesEqual(a: RawQuery, b: RawQuery): boolean {
return false;
}

return keysA.every((key) => isEqual(a[key], b[key]));
return keysA.every((key) =>
isEqual(
Array.isArray(a[key]) ? a[key].sort() : a[key],
Array.isArray(b[key]) ? b[key].sort() : b[key],
),
);
}

export function cleanQuery(query: RawQuery): RawQuery {

+ 2
- 2
server/sonar-web/src/main/js/helpers/testMocks.ts Visa fil

@@ -310,10 +310,10 @@ export function mockRawIssue(withLocations = false, overrides: Partial<RawIssue>
project: 'myproject',
rule: 'javascript:S1067',
severity: IssueSeverity.Major,
status: IssueStatus.Open,
simpleStatus: IssueSimpleStatus.Open,
textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 },
type: IssueType.CodeSmell,
status: IssueStatus.Open,
simpleStatus: IssueSimpleStatus.Open,
transitions: [],
scope: IssueScope.Main,
cleanCodeAttributeCategory: CleanCodeAttributeCategory.Responsible,

+ 2
- 1
server/sonar-web/src/main/js/helpers/urls.ts Visa fil

@@ -20,6 +20,7 @@
import { isArray, mapValues, omitBy, pick } from 'lodash';
import { Path, To } from 'react-router-dom';
import { getProfilePath } from '../apps/quality-profiles/utils';
import { DEFAULT_ISSUES_QUERY } from '../components/shared/utils';
import { BranchLike, BranchParameters } from '../types/branch-like';
import { ComponentQualifier, isApplication, isPortfolioLike } from '../types/component';
import { MeasurePageView } from '../types/measures';
@@ -423,7 +424,7 @@ export function getHomePageUrl(homepage: HomePage) {
return '/projects';
case 'ISSUES':
case 'MY_ISSUES':
return { pathname: '/issues', query: { resolved: 'false' } };
return { pathname: '/issues', query: DEFAULT_ISSUES_QUERY };
}

// should never happen, but just in case...

+ 7
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties Visa fil

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

issue.simple_status.OPEN=Open
issue.simple_status.ACCEPTED=Accepted
issue.simple_status.CONFIRMED=Confirmed
issue.simple_status.FIXED=Fixed
issue.simple_status.FALSE_POSITIVE=False Positive

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

@@ -1173,7 +1179,7 @@ issues.facet.types=Type
issues.facet.severities=Severity
issues.facet.scopes=Scope
issues.facet.projects=Project
issues.facet.statuses=Status
issues.facet.simpleStatuses=Status
issues.facet.hotspotStatuses=Hotspot Status
issues.facet.assignees=Assignee
issues.facet.files=File
@@ -1181,7 +1187,6 @@ issues.facet.modules=Module
issues.facet.directories=Directory
issues.facet.tags=Tag
issues.facet.rules=Rule
issues.facet.resolutions=Resolution
issues.facet.languages=Language
issues.facet.cleanCodeAttributeCategories=Clean Code Attribute
issues.facet.impactSoftwareQualities=Software Quality

Laddar…
Avbryt
Spara