aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2022-11-02 12:10:36 +0100
committersonartech <sonartech@sonarsource.com>2022-11-04 20:03:11 +0000
commit91f3b6fdb5b206ee06a922d0bed7fbdb9a9158d5 (patch)
tree3467457d470850d7dde86b1c71198da1d87a0a5b
parent910b43f5a002352292f2159f93928dc56e7b1b99 (diff)
downloadsonarqube-91f3b6fdb5b206ee06a922d0bed7fbdb9a9158d5.tar.gz
sonarqube-91f3b6fdb5b206ee06a922d0bed7fbdb9a9158d5.zip
SONAR-17386 Improve UI of closed issues
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx57
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/types/issues.ts8
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties2
8 files changed, 106 insertions, 15 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
index 1209fa02ba1..d0ec15e8d2a 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
@@ -18,13 +18,17 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
import { getSources } from '../../../api/components';
import IssueMessageBox from '../../../components/issue/IssueMessageBox';
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
+import { Alert } from '../../../components/ui/Alert';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
-import { isFile } from '../../../types/component';
+import { ComponentQualifier, isFile } from '../../../types/component';
+import { IssueStatus } from '../../../types/issues';
import {
Dict,
Duplication,
@@ -265,28 +269,55 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
const isFlow = issue.secondaryLocations.length === 0;
const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
+ const issueIsClosed = issue.status === IssueStatus.Closed;
+ const issueIsFileLevel = issue.componentQualifier === ComponentQualifier.File;
+ const closedIssueMessageKey = issueIsFileLevel
+ ? 'issue.closed.file_level'
+ : 'issue.closed.project_level';
+
return (
<>
+ {issueIsClosed && (
+ <Alert variant="success">
+ <FormattedMessage
+ id={closedIssueMessageKey}
+ defaultMessage={translate(closedIssueMessageKey)}
+ values={{
+ status: (
+ <strong>
+ {translate('issue.status', issue.status)} (
+ {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'})
+ </strong>
+ ),
+ }}
+ />
+ </Alert>
+ )}
+
<IssueSourceViewerHeader
branchLike={branchLike}
+ className={issueIsClosed && !issueIsFileLevel ? 'null-spacer-bottom' : ''}
expandable={!fullyShown && isFile(snippetGroup.component.q)}
loading={loading}
onExpand={this.expandComponent}
sourceViewerFile={snippetGroup.component}
/>
- {issue.component === snippetGroup.component.key && issue.textRange === undefined && (
- <IssueSourceViewerScrollContext.Consumer>
- {(ctx) => (
- <IssueMessageBox
- selected={true}
- issue={issue}
- onClick={this.props.onIssueSelect}
- ref={ctx?.registerPrimaryLocationRef}
- />
- )}
- </IssueSourceViewerScrollContext.Consumer>
- )}
+ {issue.component === snippetGroup.component.key &&
+ issue.textRange === undefined &&
+ !issueIsClosed && (
+ <IssueSourceViewerScrollContext.Consumer>
+ {(ctx) => (
+ <IssueMessageBox
+ selected={true}
+ issue={issue}
+ onClick={this.props.onIssueSelect}
+ ref={ctx?.registerPrimaryLocationRef}
+ />
+ )}
+ </IssueSourceViewerScrollContext.Consumer>
+ )}
+
{snippetLines.map((snippet, index) => (
<SnippetViewer
key={snippets[index].index}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
index 395342ee5ef..dc3d78c0026 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
@@ -41,6 +41,7 @@ import { translate } from '../../../helpers/l10n';
import { HttpStatus } from '../../../helpers/request';
import { BranchLike } from '../../../types/branch-like';
import { isFile } from '../../../types/component';
+import { IssueStatus } from '../../../types/issues';
import {
Dict,
DuplicatedFile,
@@ -122,7 +123,8 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop
this.setState({ loading: true });
try {
- const components = await getIssueFlowSnippets(issue.key);
+ const components =
+ issue.status === IssueStatus.Closed ? {} : await getIssueFlowSnippets(issue.key);
if (components[issue.component] === undefined) {
const issueComponent = await getComponentForSourceViewer({
component: issue.component,
@@ -139,6 +141,7 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop
components[issue.component].sources = sources;
}
}
+
if (this.mounted) {
this.setState({
components,
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
index b8457c447b2..f0277a53e5f 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
@@ -36,6 +36,7 @@ import './IssueSourceViewerHeader.css';
export interface Props {
branchLike: BranchLike | undefined;
+ className?: string;
expandable?: boolean;
displayProjectName?: boolean;
linkToProject?: boolean;
@@ -47,6 +48,7 @@ export interface Props {
export default function IssueSourceViewerHeader(props: Props) {
const {
branchLike,
+ className,
expandable,
displayProjectName = true,
linkToProject = true,
@@ -66,7 +68,10 @@ export default function IssueSourceViewerHeader(props: Props) {
return (
<div
- className="issue-source-viewer-header display-flex-row display-flex-space-between"
+ className={classNames(
+ 'issue-source-viewer-header display-flex-row display-flex-space-between',
+ className
+ )}
role="separator"
aria-label={sourceViewerFile.path}
>
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
index 9a4cf4441bb..089f8748835 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
@@ -20,6 +20,7 @@
import { shallow } from 'enzyme';
import { range, times } from 'lodash';
import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
import { getSources } from '../../../../api/components';
import IssueMessageBox from '../../../../components/issue/IssueMessageBox';
import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
@@ -30,6 +31,8 @@ import {
} from '../../../../helpers/mocks/sources';
import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { IssueStatus } from '../../../../types/issues';
import { SnippetGroup } from '../../../../types/types';
import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer';
import SnippetViewer from '../SnippetViewer';
@@ -147,6 +150,36 @@ it('should render file-level issue correctly', () => {
expect(wrapper.find('ContextConsumer').dive().find(IssueMessageBox).exists()).toBe(true);
});
+it.each([
+ ['file-level', ComponentQualifier.File, 'issue.closed.file_level'],
+ ['project-level', ComponentQualifier.Project, 'issue.closed.project_level'],
+])(
+ 'should render a closed %s issue correctly',
+ async (_level, componentQualifier, expectedLabel) => {
+ // issue with secondary locations and no primary location
+ const issue = mockIssue(true, {
+ component: 'project:main.js',
+ componentQualifier,
+ flows: [],
+ textRange: undefined,
+ status: IssueStatus.Closed,
+ });
+
+ const wrapper = shallowRender({
+ issue,
+ snippetGroup: {
+ locations: [],
+ ...mockSnippetsByComponent('main.js', 'project', range(1, 10)),
+ },
+ });
+
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.find<FormattedMessage>(FormattedMessage).prop('id')).toEqual(expectedLabel);
+ expect(wrapper.find('ContextConsumer').exists()).toBe(false);
+ }
+);
+
it('should expand block', async () => {
(getSources as jest.Mock).mockResolvedValueOnce(
Object.values(mockSnippetsByComponent('a', 'project', range(6, 59)).sources)
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
index 711cf415e54..c5735a79fb3 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
@@ -28,6 +28,7 @@ import {
} from '../../../../helpers/mocks/sources';
import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { IssueStatus } from '../../../../types/issues';
import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer';
import CrossComponentSourceViewer from '../CrossComponentSourceViewer';
@@ -74,6 +75,13 @@ it('Should fetch data', async () => {
expect(getIssueFlowSnippets).toHaveBeenCalledWith('foo');
});
+it('Should handle a closed issue', async () => {
+ const wrapper = shallowRender({ issue: mockIssue(true, { status: IssueStatus.Closed }) });
+ wrapper.instance().fetchIssueFlowSnippets();
+ await waitAndUpdate(wrapper);
+ expect(getIssueFlowSnippets).not.toHaveBeenCalled();
+});
+
it('Should handle no access rights', async () => {
(getIssueFlowSnippets as jest.Mock).mockRejectedValueOnce({ status: 403 });
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap
index 98d86611f24..6e83c714137 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap
@@ -11,6 +11,7 @@ exports[`should render correctly 1`] = `
"name": "master",
}
}
+ className=""
expandable={true}
loading={false}
onExpand={[Function]}
diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts
index d94ab952f2d..0e8c55bdb00 100644
--- a/server/sonar-web/src/main/js/types/issues.ts
+++ b/server/sonar-web/src/main/js/types/issues.ts
@@ -32,6 +32,14 @@ export enum IssueScope {
Test = 'TEST',
}
+export enum IssueStatus {
+ Open = 'OPEN',
+ Confirmed = 'CONFIRMED',
+ Reopened = 'REOPENED',
+ Resolved = 'RESOLVED',
+ Closed = 'CLOSED',
+}
+
interface Comment {
createdAt: string;
htmlText: string;
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 2acd0079329..ae40c64092d 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -876,6 +876,8 @@ issue.transition.resetastoreview.description=The Security Hotspot should be anal
issue.tabs.code=Where is the issue?
issue.x_data_flows={0} data flow(s)
issue.execution_flow=Full execution flow
+issue.closed.file_level=This issue is {status}. It was detected in the file below and is no longer being detected.
+issue.closed.project_level=This issue is {status}. It was detected in the project below and is no longer being detected.
issues.action_select=Select issue
issues.action_select.label=Select issue {0}