diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2022-11-02 12:10:36 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-11-04 20:03:11 +0000 |
commit | 91f3b6fdb5b206ee06a922d0bed7fbdb9a9158d5 (patch) | |
tree | 3467457d470850d7dde86b1c71198da1d87a0a5b | |
parent | 910b43f5a002352292f2159f93928dc56e7b1b99 (diff) | |
download | sonarqube-91f3b6fdb5b206ee06a922d0bed7fbdb9a9158d5.tar.gz sonarqube-91f3b6fdb5b206ee06a922d0bed7fbdb9a9158d5.zip |
SONAR-17386 Improve UI of closed issues
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} |