From 91f3b6fdb5b206ee06a922d0bed7fbdb9a9158d5 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 2 Nov 2022 12:10:36 +0100 Subject: [PATCH] SONAR-17386 Improve UI of closed issues --- .../ComponentSourceSnippetGroupViewer.tsx | 57 ++++++++++++++----- .../CrossComponentSourceViewer.tsx | 5 +- .../IssueSourceViewerHeader.tsx | 7 ++- ...ComponentSourceSnippetGroupViewer-test.tsx | 33 +++++++++++ .../CrossComponentSourceViewer-test.tsx | 8 +++ ...nentSourceSnippetGroupViewer-test.tsx.snap | 1 + server/sonar-web/src/main/js/types/issues.ts | 8 +++ .../resources/org/sonar/l10n/core.properties | 2 + 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 && ( + + + {translate('issue.status', issue.status)} ( + {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'}) + + ), + }} + /> + + )} + - {issue.component === snippetGroup.component.key && issue.textRange === undefined && ( - - {(ctx) => ( - - )} - - )} + {issue.component === snippetGroup.component.key && + issue.textRange === undefined && + !issueIsClosed && ( + + {(ctx) => ( + + )} + + )} + {snippetLines.map((snippet, index) => ( 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).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} -- 2.39.5