]> source.dussan.org Git - sonarqube.git/blob
aa0c2434930fe025ed0dfc4f9e61d16f507782c1
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2020 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
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';
28 import {
29   filterDuplicationBlocksByLine,
30   getDuplicationBlocksForIndex,
31   isDuplicationBlockInRemovedComponent
32 } from '../../../components/SourceViewer/helpers/duplications';
33 import {
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';
43
44 interface Props {
45   branchLike: BranchLike | undefined;
46   highlightedLocationMessage?: { index: number; text: string | undefined };
47   issue: T.Issue;
48   issues: T.Issue[];
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;
55 }
56
57 interface State {
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 };
64   loading: boolean;
65   notAccessible: boolean;
66 }
67
68 export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
69   mounted = false;
70   state: State = {
71     components: {},
72     duplicationsByLine: {},
73     loading: true,
74     notAccessible: false
75   };
76
77   componentDidMount() {
78     this.mounted = true;
79     this.fetchIssueFlowSnippets(this.props.issue.key);
80   }
81
82   componentWillReceiveProps(newProps: Props) {
83     if (newProps.issue.key !== this.props.issue.key) {
84       this.fetchIssueFlowSnippets(newProps.issue.key);
85     }
86   }
87
88   componentWillUnmount() {
89     this.mounted = false;
90   }
91
92   fetchDuplications = (component: string, line: T.SourceLine) => {
93     getDuplications({
94       key: component,
95       ...getBranchLikeQuery(this.props.branchLike)
96     }).then(
97       r => {
98         if (this.mounted) {
99           this.setState(state => ({
100             duplicatedFiles: r.files,
101             duplications: r.duplications,
102             duplicationsByLine: getDuplicationsByLine(r.duplications),
103             linePopup:
104               r.duplications.length === 1
105                 ? { component, index: 0, line: line.line, name: 'duplications' }
106                 : state.linePopup
107           }));
108         }
109       },
110       () => {}
111     );
112   };
113
114   fetchIssueFlowSnippets(issueKey: string) {
115     this.setState({ loading: true });
116     getIssueFlowSnippets(issueKey).then(
117       components => {
118         if (this.mounted) {
119           this.setState({
120             components,
121             issuePopup: undefined,
122             linePopup: undefined,
123             loading: false
124           });
125           if (this.props.onLoaded) {
126             this.props.onLoaded();
127           }
128         }
129       },
130       (response: Response) => {
131         if (response.status !== 403) {
132           throwGlobalError(response);
133         }
134         if (this.mounted) {
135           this.setState({ loading: false, notAccessible: response.status === 403 });
136         }
137       }
138     );
139   }
140
141   handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
142     this.setState((state: State) => {
143       const samePopup =
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 };
149       }
150       return null;
151     });
152   };
153
154   handleLinePopupToggle = ({
155     component,
156     index,
157     line,
158     name,
159     open
160   }: T.LinePopup & { component: string }) => {
161     this.setState((state: State) => {
162       const samePopup =
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 };
172       }
173       return null;
174     });
175   };
176
177   handleCloseLinePopup = () => {
178     this.setState({ linePopup: undefined });
179   };
180
181   renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => {
182     const { duplicatedFiles, duplications } = this.state;
183
184     if (!component || !duplicatedFiles) {
185       return null;
186     }
187
188     const blocks = getDuplicationBlocksForIndex(duplications, index);
189
190     return (
191       <WorkspaceContext.Consumer>
192         {({ openComponent }) => (
193           <DuplicationPopup
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}
201           />
202         )}
203       </WorkspaceContext.Consumer>
204     );
205   };
206
207   render() {
208     const { loading, notAccessible } = this.state;
209
210     if (loading) {
211       return (
212         <div>
213           <DeferredSpinner />
214         </div>
215       );
216     }
217
218     if (notAccessible) {
219       return (
220         <Alert className="spacer-top" variant="warning">
221           {translate('code_viewer.no_source_code_displayed_due_to_security')}
222         </Alert>
223       );
224     }
225
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);
230
231     return (
232       <div>
233         {locationsByComponent.map((snippetGroup, i) => {
234           let componentProps = {};
235           if (linePopup && snippetGroup.component.key === linePopup.component) {
236             componentProps = {
237               duplications,
238               duplicationsByLine,
239               linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name }
240             };
241           }
242           return (
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}
250                 issue={issue}
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}
263                 {...componentProps}
264               />
265             </SourceViewerContext.Provider>
266           );
267         })}
268       </div>
269     );
270   }
271 }