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