@@ -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} |
@@ -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, |
@@ -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} | |||
> |
@@ -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) |
@@ -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 }); | |||
@@ -11,6 +11,7 @@ exports[`should render correctly 1`] = ` | |||
"name": "master", | |||
} | |||
} | |||
className="" | |||
expandable={true} | |||
loading={false} | |||
onExpand={[Function]} |
@@ -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; |
@@ -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} |