You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CrossComponentSourceViewerWrapper.tsx 10.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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, 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';
  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 { HttpStatus } from '../../../helpers/request';
  42. import { BranchLike } from '../../../types/branch-like';
  43. import { isFile } from '../../../types/component';
  44. import {
  45. Dict,
  46. DuplicatedFile,
  47. Duplication,
  48. FlowLocation,
  49. Issue,
  50. SnippetsByComponent,
  51. SourceViewerFile
  52. } from '../../../types/types';
  53. import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer';
  54. import { getPrimaryLocation, groupLocationsByComponent } from './utils';
  55. interface Props {
  56. branchLike: BranchLike | undefined;
  57. highlightedLocationMessage?: { index: number; text: string | undefined };
  58. issue: Issue;
  59. issues: Issue[];
  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;
  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. export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
  77. mounted = false;
  78. state: State = {
  79. components: {},
  80. duplicationsByLine: {},
  81. loading: true,
  82. notAccessible: false
  83. };
  84. componentDidMount() {
  85. this.mounted = true;
  86. this.fetchIssueFlowSnippets();
  87. }
  88. componentDidUpdate(prevProps: Props) {
  89. if (prevProps.issue.key !== this.props.issue.key) {
  90. this.fetchIssueFlowSnippets();
  91. }
  92. }
  93. componentWillUnmount() {
  94. this.mounted = false;
  95. }
  96. fetchDuplications = (component: string) => {
  97. getDuplications({
  98. key: component,
  99. ...getBranchLikeQuery(this.props.branchLike)
  100. }).then(
  101. r => {
  102. if (this.mounted) {
  103. this.setState({
  104. duplicatedFiles: r.files,
  105. duplications: r.duplications,
  106. duplicationsByLine: getDuplicationsByLine(r.duplications)
  107. });
  108. }
  109. },
  110. () => {}
  111. );
  112. };
  113. async fetchIssueFlowSnippets() {
  114. const { issue, branchLike } = this.props;
  115. this.setState({ loading: true });
  116. try {
  117. const components = await getIssueFlowSnippets(issue.key);
  118. if (components[issue.component] === undefined) {
  119. const issueComponent = await getComponentForSourceViewer({
  120. component: issue.component,
  121. ...getBranchLikeQuery(branchLike)
  122. });
  123. components[issue.component] = { component: issueComponent, sources: [] };
  124. if (isFile(issueComponent.q)) {
  125. const sources = await getSources({
  126. key: issueComponent.key,
  127. ...getBranchLikeQuery(branchLike),
  128. from: 1,
  129. to: 10
  130. }).then(lines => keyBy(lines, 'line'));
  131. components[issue.component].sources = sources;
  132. }
  133. }
  134. if (this.mounted) {
  135. this.setState({
  136. components,
  137. issuePopup: undefined,
  138. loading: false
  139. });
  140. if (this.props.onLoaded) {
  141. this.props.onLoaded();
  142. }
  143. }
  144. } catch (response) {
  145. const rsp = response as Response;
  146. if (rsp.status !== HttpStatus.Forbidden) {
  147. throwGlobalError(response);
  148. }
  149. if (this.mounted) {
  150. this.setState({ loading: false, notAccessible: rsp.status === HttpStatus.Forbidden });
  151. }
  152. }
  153. }
  154. handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
  155. this.setState((state: State) => {
  156. const samePopup =
  157. state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
  158. if (open !== false && !samePopup) {
  159. return { issuePopup: { issue, name: popupName } };
  160. } else if (open !== true && samePopup) {
  161. return { issuePopup: undefined };
  162. }
  163. return null;
  164. });
  165. };
  166. renderDuplicationPopup = (component: SourceViewerFile, index: number, line: number) => {
  167. const { duplicatedFiles, duplications } = this.state;
  168. if (!component || !duplicatedFiles) {
  169. return null;
  170. }
  171. const blocks = getDuplicationBlocksForIndex(duplications, index);
  172. return (
  173. <WorkspaceContext.Consumer>
  174. {({ openComponent }) => (
  175. <DuplicationPopup
  176. blocks={filterDuplicationBlocksByLine(blocks, line)}
  177. branchLike={this.props.branchLike}
  178. duplicatedFiles={duplicatedFiles}
  179. inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
  180. openComponent={openComponent}
  181. sourceViewerFile={component}
  182. />
  183. )}
  184. </WorkspaceContext.Consumer>
  185. );
  186. };
  187. render() {
  188. const { loading, notAccessible } = this.state;
  189. if (loading) {
  190. return (
  191. <div>
  192. <DeferredSpinner />
  193. </div>
  194. );
  195. }
  196. if (notAccessible) {
  197. return (
  198. <Alert className="spacer-top" variant="warning">
  199. {translate('code_viewer.no_source_code_displayed_due_to_security')}
  200. </Alert>
  201. );
  202. }
  203. const { issue, locations } = this.props;
  204. const { components, duplications, duplicationsByLine } = this.state;
  205. const issuesByComponent = issuesByComponentAndLine(this.props.issues);
  206. const locationsByComponent = groupLocationsByComponent(issue, locations, components);
  207. const lastOccurenceOfPrimaryComponent = findLastIndex(
  208. locationsByComponent,
  209. ({ component }) => component.key === issue.component
  210. );
  211. if (components[issue.component] === undefined) {
  212. return null;
  213. }
  214. return (
  215. <div>
  216. {locationsByComponent.map((snippetGroup, i) => {
  217. return (
  218. <SourceViewerContext.Provider
  219. // eslint-disable-next-line react/no-array-index-key
  220. key={`${issue.key}-${this.props.selectedFlowIndex || 0}-${i}`}
  221. value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
  222. <ComponentSourceSnippetGroupViewer
  223. branchLike={this.props.branchLike}
  224. duplications={duplications}
  225. duplicationsByLine={duplicationsByLine}
  226. highlightedLocationMessage={this.props.highlightedLocationMessage}
  227. issue={issue}
  228. issuePopup={this.state.issuePopup}
  229. issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
  230. isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent}
  231. lastSnippetGroup={i === locationsByComponent.length - 1}
  232. loadDuplications={this.fetchDuplications}
  233. locations={snippetGroup.locations || []}
  234. onIssueChange={this.props.onIssueChange}
  235. onIssuePopupToggle={this.handleIssuePopupToggle}
  236. onLocationSelect={this.props.onLocationSelect}
  237. renderDuplicationPopup={this.renderDuplicationPopup}
  238. scroll={this.props.scroll}
  239. snippetGroup={snippetGroup}
  240. />
  241. </SourceViewerContext.Provider>
  242. );
  243. })}
  244. {locationsByComponent.length === 0 && (
  245. <ComponentSourceSnippetGroupViewer
  246. branchLike={this.props.branchLike}
  247. duplications={duplications}
  248. duplicationsByLine={duplicationsByLine}
  249. highlightedLocationMessage={this.props.highlightedLocationMessage}
  250. issue={issue}
  251. issuePopup={this.state.issuePopup}
  252. issuesByLine={issuesByComponent[issue.component] || {}}
  253. isLastOccurenceOfPrimaryComponent={true}
  254. lastSnippetGroup={true}
  255. loadDuplications={this.fetchDuplications}
  256. locations={[]}
  257. onIssueChange={this.props.onIssueChange}
  258. onIssuePopupToggle={this.handleIssuePopupToggle}
  259. onLocationSelect={this.props.onLocationSelect}
  260. renderDuplicationPopup={this.renderDuplicationPopup}
  261. scroll={this.props.scroll}
  262. snippetGroup={{
  263. locations: [getPrimaryLocation(issue)],
  264. ...components[issue.component]
  265. }}
  266. />
  267. )}
  268. </div>
  269. );
  270. }
  271. }