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