]> source.dussan.org Git - sonarqube.git/blob
49376aafc483f4f9198d7a29d132e2698e25c2f1
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2019 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 DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
22 import ComponentSourceSnippetViewer from './ComponentSourceSnippetViewer';
23 import { groupLocationsByComponent } from './utils';
24 import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
25 import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
26 import { WorkspaceContext } from '../../../components/workspace/context';
27 import { getIssueFlowSnippets } from '../../../api/issues';
28 import {
29   filterDuplicationBlocksByLine,
30   isDuplicationBlockInRemovedComponent,
31   getDuplicationBlocksForIndex
32 } from '../../../components/SourceViewer/helpers/duplications';
33 import {
34   duplicationsByLine,
35   issuesByComponentAndLine
36 } from '../../../components/SourceViewer/helpers/indexing';
37 import { getDuplications } from '../../../api/components';
38 import { getBranchLikeQuery } from '../../../helpers/branches';
39
40 interface Props {
41   branchLike: T.Branch | T.PullRequest | undefined;
42   highlightedLocationMessage?: { index: number; text: string | undefined };
43   issue: T.Issue;
44   issues: T.Issue[];
45   locations: T.FlowLocation[];
46   onIssueChange: (issue: T.Issue) => void;
47   onLoaded?: () => void;
48   onLocationSelect: (index: number) => void;
49   scroll?: (element: HTMLElement) => void;
50   selectedFlowIndex: number | undefined;
51 }
52
53 interface State {
54   components: T.Dict<T.SnippetsByComponent>;
55   duplicatedFiles?: T.Dict<T.DuplicatedFile>;
56   duplications?: T.Duplication[];
57   duplicationsByLine: { [line: number]: number[] };
58   issuePopup?: { issue: string; name: string };
59   linePopup?: T.LinePopup & { component: string };
60   loading: boolean;
61 }
62
63 export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
64   mounted = false;
65   state: State = {
66     components: {},
67     duplicationsByLine: {},
68     loading: true
69   };
70
71   componentDidMount() {
72     this.mounted = true;
73     this.fetchIssueFlowSnippets(this.props.issue.key);
74   }
75
76   componentWillReceiveProps(newProps: Props) {
77     if (newProps.issue.key !== this.props.issue.key) {
78       this.fetchIssueFlowSnippets(newProps.issue.key);
79     }
80   }
81
82   componentWillUnmount() {
83     this.mounted = false;
84   }
85
86   fetchDuplications = (component: string, line: T.SourceLine) => {
87     getDuplications({
88       key: component,
89       ...getBranchLikeQuery(this.props.branchLike)
90     }).then(
91       r => {
92         if (this.mounted) {
93           this.setState(state => ({
94             duplicatedFiles: r.files,
95             duplications: r.duplications,
96             duplicationsByLine: duplicationsByLine(r.duplications),
97             linePopup:
98               r.duplications.length === 1
99                 ? { component, index: 0, line: line.line, name: 'duplications' }
100                 : state.linePopup
101           }));
102         }
103       },
104       () => {}
105     );
106   };
107
108   fetchIssueFlowSnippets(issueKey: string) {
109     this.setState({ loading: true });
110     getIssueFlowSnippets(issueKey).then(
111       components => {
112         if (this.mounted) {
113           this.setState({
114             components,
115             issuePopup: undefined,
116             linePopup: undefined,
117             loading: false
118           });
119           if (this.props.onLoaded) {
120             this.props.onLoaded();
121           }
122         }
123       },
124       () => {
125         if (this.mounted) {
126           this.setState({ loading: false });
127         }
128       }
129     );
130   }
131
132   handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
133     this.setState((state: State) => {
134       const samePopup =
135         state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
136       if (open !== false && !samePopup) {
137         return { issuePopup: { issue, name: popupName } };
138       } else if (open !== true && samePopup) {
139         return { issuePopup: undefined };
140       }
141       return null;
142     });
143   };
144
145   handleLinePopupToggle = ({
146     component,
147     index,
148     line,
149     name,
150     open
151   }: T.LinePopup & { component: string }) => {
152     this.setState((state: State) => {
153       const samePopup =
154         state.linePopup !== undefined &&
155         state.linePopup.line === line &&
156         state.linePopup.name === name &&
157         state.linePopup.component === component &&
158         state.linePopup.index === index;
159       if (open !== false && !samePopup) {
160         return { linePopup: { component, index, line, name } };
161       } else if (open !== true && samePopup) {
162         return { linePopup: undefined };
163       }
164       return null;
165     });
166   };
167
168   handleCloseLinePopup = () => {
169     this.setState({ linePopup: undefined });
170   };
171
172   renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => {
173     const { duplicatedFiles, duplications } = this.state;
174
175     if (!component || !duplicatedFiles) {
176       return null;
177     }
178
179     const blocks = getDuplicationBlocksForIndex(duplications, index);
180
181     return (
182       <WorkspaceContext.Consumer>
183         {({ openComponent }) => (
184           <DuplicationPopup
185             blocks={filterDuplicationBlocksByLine(blocks, line)}
186             branchLike={this.props.branchLike}
187             duplicatedFiles={duplicatedFiles}
188             inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
189             onClose={this.handleCloseLinePopup}
190             openComponent={openComponent}
191             sourceViewerFile={component}
192           />
193         )}
194       </WorkspaceContext.Consumer>
195     );
196   };
197
198   render() {
199     const { loading } = this.state;
200
201     if (loading) {
202       return (
203         <div>
204           <DeferredSpinner />
205         </div>
206       );
207     }
208
209     const { components, duplications, duplicationsByLine, linePopup } = this.state;
210     const issuesByComponent = issuesByComponentAndLine(this.props.issues);
211     const locationsByComponent = groupLocationsByComponent(this.props.locations, components);
212
213     return (
214       <div>
215         {locationsByComponent.map((snippetGroup, i) => {
216           let componentProps = {};
217           if (linePopup && snippetGroup.component.key === linePopup.component) {
218             componentProps = {
219               duplications,
220               duplicationsByLine,
221               linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name }
222             };
223           }
224           return (
225             <SourceViewerContext.Provider
226               key={`${this.props.issue.key}-${this.props.selectedFlowIndex}-${i}`}
227               value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
228               <ComponentSourceSnippetViewer
229                 branchLike={this.props.branchLike}
230                 highlightedLocationMessage={this.props.highlightedLocationMessage}
231                 issue={this.props.issue}
232                 issuePopup={this.state.issuePopup}
233                 issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
234                 last={i === locationsByComponent.length - 1}
235                 loadDuplications={this.fetchDuplications}
236                 locations={snippetGroup.locations || []}
237                 onIssueChange={this.props.onIssueChange}
238                 onIssuePopupToggle={this.handleIssuePopupToggle}
239                 onLinePopupToggle={this.handleLinePopupToggle}
240                 onLocationSelect={this.props.onLocationSelect}
241                 renderDuplicationPopup={this.renderDuplicationPopup}
242                 scroll={this.props.scroll}
243                 snippetGroup={snippetGroup}
244                 {...componentProps}
245               />
246             </SourceViewerContext.Provider>
247           );
248         })}
249       </div>
250     );
251   }
252 }