3 * Copyright (C) 2009-2022 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 { findLastIndex, keyBy } from 'lodash';
21 import * as React from 'react';
22 import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';
23 import { getIssueFlowSnippets } from '../../../api/issues';
24 import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
26 filterDuplicationBlocksByLine,
27 getDuplicationBlocksForIndex,
28 isDuplicationBlockInRemovedComponent
29 } from '../../../components/SourceViewer/helpers/duplications';
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 { isFile } from '../../../types/component';
51 } from '../../../types/types';
52 import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer';
53 import { getPrimaryLocation, groupLocationsByComponent } from './utils';
56 branchLike: BranchLike | undefined;
57 highlightedLocationMessage?: { index: number; text: string | undefined };
60 locations: FlowLocation[];
61 onIssueChange: (issue: Issue) => void;
62 onLoaded?: () => void;
63 onLocationSelect: (index: number) => void;
64 scroll?: (element: HTMLElement) => void;
65 selectedFlowIndex: number | undefined;
69 components: Dict<SnippetsByComponent>;
70 duplicatedFiles?: Dict<DuplicatedFile>;
71 duplications?: Duplication[];
72 duplicationsByLine: { [line: number]: number[] };
73 issuePopup?: { issue: string; name: string };
75 notAccessible: boolean;
78 export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
82 duplicationsByLine: {},
89 this.fetchIssueFlowSnippets();
92 componentDidUpdate(prevProps: Props) {
93 if (prevProps.issue.key !== this.props.issue.key) {
94 this.fetchIssueFlowSnippets();
98 componentWillUnmount() {
102 fetchDuplications = (component: string) => {
105 ...getBranchLikeQuery(this.props.branchLike)
110 duplicatedFiles: r.files,
111 duplications: r.duplications,
112 duplicationsByLine: getDuplicationsByLine(r.duplications)
120 async fetchIssueFlowSnippets() {
121 const { issue, branchLike } = this.props;
122 this.setState({ loading: true });
125 const components = await getIssueFlowSnippets(issue.key);
126 if (components[issue.component] === undefined) {
127 const issueComponent = await getComponentForSourceViewer({
128 component: issue.component,
129 ...getBranchLikeQuery(branchLike)
131 components[issue.component] = { component: issueComponent, sources: [] };
132 if (isFile(issueComponent.q)) {
133 const sources = await getSources({
134 key: issueComponent.key,
135 ...getBranchLikeQuery(branchLike),
138 }).then(lines => keyBy(lines, 'line'));
139 components[issue.component].sources = sources;
145 issuePopup: undefined,
148 if (this.props.onLoaded) {
149 this.props.onLoaded();
153 const rsp = response as Response;
154 if (rsp.status !== 403) {
155 throwGlobalError(response);
158 this.setState({ loading: false, notAccessible: rsp.status === 403 });
163 handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
164 this.setState((state: State) => {
166 state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
167 if (open !== false && !samePopup) {
168 return { issuePopup: { issue, name: popupName } };
169 } else if (open !== true && samePopup) {
170 return { issuePopup: undefined };
176 renderDuplicationPopup = (component: SourceViewerFile, index: number, line: number) => {
177 const { duplicatedFiles, duplications } = this.state;
179 if (!component || !duplicatedFiles) {
183 const blocks = getDuplicationBlocksForIndex(duplications, index);
186 <WorkspaceContext.Consumer>
187 {({ openComponent }) => (
189 blocks={filterDuplicationBlocksByLine(blocks, line)}
190 branchLike={this.props.branchLike}
191 duplicatedFiles={duplicatedFiles}
192 inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
193 openComponent={openComponent}
194 sourceViewerFile={component}
197 </WorkspaceContext.Consumer>
202 const { loading, notAccessible } = this.state;
214 <Alert className="spacer-top" variant="warning">
215 {translate('code_viewer.no_source_code_displayed_due_to_security')}
220 const { issue, locations } = this.props;
221 const { components, duplications, duplicationsByLine } = this.state;
222 const issuesByComponent = issuesByComponentAndLine(this.props.issues);
223 const locationsByComponent = groupLocationsByComponent(issue, locations, components);
225 const lastOccurenceOfPrimaryComponent = findLastIndex(
226 locationsByComponent,
227 ({ component }) => component.key === issue.component
230 if (components[issue.component] === undefined) {
236 {locationsByComponent.map((snippetGroup, i) => {
238 <SourceViewerContext.Provider
239 // eslint-disable-next-line react/no-array-index-key
240 key={`${issue.key}-${this.props.selectedFlowIndex || 0}-${i}`}
241 value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
242 <ComponentSourceSnippetGroupViewer
243 branchLike={this.props.branchLike}
244 duplications={duplications}
245 duplicationsByLine={duplicationsByLine}
246 highlightedLocationMessage={this.props.highlightedLocationMessage}
248 issuePopup={this.state.issuePopup}
249 issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
250 isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent}
251 lastSnippetGroup={i === locationsByComponent.length - 1}
252 loadDuplications={this.fetchDuplications}
253 locations={snippetGroup.locations || []}
254 onIssueChange={this.props.onIssueChange}
255 onIssuePopupToggle={this.handleIssuePopupToggle}
256 onLocationSelect={this.props.onLocationSelect}
257 renderDuplicationPopup={this.renderDuplicationPopup}
258 scroll={this.props.scroll}
259 snippetGroup={snippetGroup}
261 </SourceViewerContext.Provider>
265 {locationsByComponent.length === 0 && (
266 <ComponentSourceSnippetGroupViewer
267 branchLike={this.props.branchLike}
268 duplications={duplications}
269 duplicationsByLine={duplicationsByLine}
270 highlightedLocationMessage={this.props.highlightedLocationMessage}
272 issuePopup={this.state.issuePopup}
273 issuesByLine={issuesByComponent[issue.component] || {}}
274 isLastOccurenceOfPrimaryComponent={true}
275 lastSnippetGroup={true}
276 loadDuplications={this.fetchDuplications}
278 onIssueChange={this.props.onIssueChange}
279 onIssuePopupToggle={this.handleIssuePopupToggle}
280 onLocationSelect={this.props.onLocationSelect}
281 renderDuplicationPopup={this.renderDuplicationPopup}
282 scroll={this.props.scroll}
284 locations: [getPrimaryLocation(issue)],
285 ...components[issue.component]