Browse Source

SONAR-17386 Improve UI of closed issues

tags/9.8.0.63668
Jeremy Davis 1 year ago
parent
commit
91f3b6fdb5

+ 44
- 13
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx View File

@@ -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}

+ 4
- 1
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx View File

@@ -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,

+ 6
- 1
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx View File

@@ -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}
>

+ 33
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx View File

@@ -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)

+ 8
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx View File

@@ -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 });


+ 1
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap View File

@@ -11,6 +11,7 @@ exports[`should render correctly 1`] = `
"name": "master",
}
}
className=""
expandable={true}
loading={false}
onExpand={[Function]}

+ 8
- 0
server/sonar-web/src/main/js/types/issues.ts View File

@@ -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;

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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}

Loading…
Cancel
Save