3 * Copyright (C) 2009-2020 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 import * as React from 'react';
21 import { Alert } from 'sonar-ui-common/components/ui/Alert';
22 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
23 import { translate } from 'sonar-ui-common/helpers/l10n';
24 import { getDuplications } from '../../../api/components';
25 import { getIssueFlowSnippets } from '../../../api/issues';
26 import throwGlobalError from '../../../app/utils/throwGlobalError';
27 import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
29 filterDuplicationBlocksByLine,
30 getDuplicationBlocksForIndex,
31 isDuplicationBlockInRemovedComponent
32 } from '../../../components/SourceViewer/helpers/duplications';
34 duplicationsByLine as getDuplicationsByLine,
35 issuesByComponentAndLine
36 } from '../../../components/SourceViewer/helpers/indexing';
37 import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
38 import { WorkspaceContext } from '../../../components/workspace/context';
39 import { getBranchLikeQuery } from '../../../helpers/branch-like';
40 import { BranchLike } from '../../../types/branch-like';
41 import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer';
42 import { groupLocationsByComponent } from './utils';
45 branchLike: BranchLike | undefined;
46 highlightedLocationMessage?: { index: number; text: string | undefined };
49 locations: T.FlowLocation[];
50 onIssueChange: (issue: T.Issue) => void;
51 onLoaded?: () => void;
52 onLocationSelect: (index: number) => void;
53 scroll?: (element: HTMLElement) => void;
54 selectedFlowIndex: number | undefined;
58 components: T.Dict<T.SnippetsByComponent>;
59 duplicatedFiles?: T.Dict<T.DuplicatedFile>;
60 duplications?: T.Duplication[];
61 duplicationsByLine: { [line: number]: number[] };
62 issuePopup?: { issue: string; name: string };
63 linePopup?: T.LinePopup & { component: string };
65 notAccessible: boolean;
68 export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
72 duplicationsByLine: {},
79 this.fetchIssueFlowSnippets(this.props.issue.key);
82 componentWillReceiveProps(newProps: Props) {
83 if (newProps.issue.key !== this.props.issue.key) {
84 this.fetchIssueFlowSnippets(newProps.issue.key);
88 componentWillUnmount() {
92 fetchDuplications = (component: string, line: T.SourceLine) => {
95 ...getBranchLikeQuery(this.props.branchLike)
99 this.setState(state => ({
100 duplicatedFiles: r.files,
101 duplications: r.duplications,
102 duplicationsByLine: getDuplicationsByLine(r.duplications),
104 r.duplications.length === 1
105 ? { component, index: 0, line: line.line, name: 'duplications' }
114 fetchIssueFlowSnippets(issueKey: string) {
115 this.setState({ loading: true });
116 getIssueFlowSnippets(issueKey).then(
121 issuePopup: undefined,
122 linePopup: undefined,
125 if (this.props.onLoaded) {
126 this.props.onLoaded();
130 (response: Response) => {
131 if (response.status !== 403) {
132 throwGlobalError(response);
135 this.setState({ loading: false, notAccessible: response.status === 403 });
141 handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
142 this.setState((state: State) => {
144 state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
145 if (open !== false && !samePopup) {
146 return { issuePopup: { issue, name: popupName } };
147 } else if (open !== true && samePopup) {
148 return { issuePopup: undefined };
154 handleLinePopupToggle = ({
160 }: T.LinePopup & { component: string }) => {
161 this.setState((state: State) => {
163 state.linePopup !== undefined &&
164 state.linePopup.line === line &&
165 state.linePopup.name === name &&
166 state.linePopup.component === component &&
167 state.linePopup.index === index;
168 if (open !== false && !samePopup) {
169 return { linePopup: { component, index, line, name } };
170 } else if (open !== true && samePopup) {
171 return { linePopup: undefined };
177 handleCloseLinePopup = () => {
178 this.setState({ linePopup: undefined });
181 renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => {
182 const { duplicatedFiles, duplications } = this.state;
184 if (!component || !duplicatedFiles) {
188 const blocks = getDuplicationBlocksForIndex(duplications, index);
191 <WorkspaceContext.Consumer>
192 {({ openComponent }) => (
194 blocks={filterDuplicationBlocksByLine(blocks, line)}
195 branchLike={this.props.branchLike}
196 duplicatedFiles={duplicatedFiles}
197 inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
198 onClose={this.handleCloseLinePopup}
199 openComponent={openComponent}
200 sourceViewerFile={component}
203 </WorkspaceContext.Consumer>
208 const { loading, notAccessible } = this.state;
220 <Alert className="spacer-top" variant="warning">
221 {translate('code_viewer.no_source_code_displayed_due_to_security')}
226 const { issue, locations } = this.props;
227 const { components, duplications, duplicationsByLine, linePopup } = this.state;
228 const issuesByComponent = issuesByComponentAndLine(this.props.issues);
229 const locationsByComponent = groupLocationsByComponent(issue, locations, components);
233 {locationsByComponent.map((snippetGroup, i) => {
234 let componentProps = {};
235 if (linePopup && snippetGroup.component.key === linePopup.component) {
239 linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name }
243 <SourceViewerContext.Provider
244 // eslint-disable-next-line react/no-array-index-key
245 key={`${issue.key}-${this.props.selectedFlowIndex || 0}-${i}`}
246 value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
247 <ComponentSourceSnippetGroupViewer
248 branchLike={this.props.branchLike}
249 highlightedLocationMessage={this.props.highlightedLocationMessage}
251 issuePopup={this.state.issuePopup}
252 issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
253 lastSnippetGroup={i === locationsByComponent.length - 1}
254 loadDuplications={this.fetchDuplications}
255 locations={snippetGroup.locations || []}
256 onIssueChange={this.props.onIssueChange}
257 onIssuePopupToggle={this.handleIssuePopupToggle}
258 onLinePopupToggle={this.handleLinePopupToggle}
259 onLocationSelect={this.props.onLocationSelect}
260 renderDuplicationPopup={this.renderDuplicationPopup}
261 scroll={this.props.scroll}
262 snippetGroup={snippetGroup}
265 </SourceViewerContext.Provider>