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.

CrossComponentSourceViewer.tsx 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 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 { Spinner } from '@sonarsource/echoes-react';
  21. import { FlagMessage } from 'design-system';
  22. import { findLastIndex, keyBy } from 'lodash';
  23. import * as React from 'react';
  24. import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';
  25. import { getIssueFlowSnippets } from '../../../api/issues';
  26. import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
  27. import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
  28. import {
  29. filterDuplicationBlocksByLine,
  30. getDuplicationBlocksForIndex,
  31. isDuplicationBlockInRemovedComponent,
  32. } from '../../../components/SourceViewer/helpers/duplications';
  33. import {
  34. duplicationsByLine as getDuplicationsByLine,
  35. issuesByComponentAndLine,
  36. } from '../../../components/SourceViewer/helpers/indexing';
  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 { IssueDeprecatedStatus } from '../../../types/issues';
  45. import {
  46. Dict,
  47. DuplicatedFile,
  48. Duplication,
  49. FlowLocation,
  50. Issue,
  51. SnippetsByComponent,
  52. SourceViewerFile,
  53. } from '../../../types/types';
  54. import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer';
  55. import { groupLocationsByComponent } from './utils';
  56. interface Props {
  57. branchLike: BranchLike | undefined;
  58. highlightedLocationMessage?: { index: number; text: string | undefined };
  59. issue: Issue;
  60. issues: Issue[];
  61. locations: FlowLocation[];
  62. onIssueSelect: (issueKey: string) => void;
  63. onLocationSelect: (index: number) => void;
  64. selectedFlowIndex: number | undefined;
  65. }
  66. interface State {
  67. components: Dict<SnippetsByComponent>;
  68. duplicatedFiles?: Dict<DuplicatedFile>;
  69. duplications?: Duplication[];
  70. duplicationsByLine: { [line: number]: number[] };
  71. loading: boolean;
  72. notAccessible: boolean;
  73. }
  74. export default class CrossComponentSourceViewer extends React.PureComponent<Props, State> {
  75. mounted = false;
  76. state: State = {
  77. components: {},
  78. duplicationsByLine: {},
  79. loading: true,
  80. notAccessible: false,
  81. };
  82. componentDidMount() {
  83. this.mounted = true;
  84. this.fetchIssueFlowSnippets();
  85. }
  86. componentDidUpdate(prevProps: Props) {
  87. if (prevProps.issue.key !== this.props.issue.key) {
  88. this.fetchIssueFlowSnippets();
  89. }
  90. }
  91. componentWillUnmount() {
  92. this.mounted = false;
  93. }
  94. fetchDuplications = (component: string) => {
  95. getDuplications({
  96. key: component,
  97. ...getBranchLikeQuery(this.props.branchLike),
  98. }).then(
  99. (r) => {
  100. if (this.mounted) {
  101. this.setState({
  102. duplicatedFiles: r.files,
  103. duplications: r.duplications,
  104. duplicationsByLine: getDuplicationsByLine(r.duplications),
  105. });
  106. }
  107. },
  108. () => {
  109. /* No error hanlding here */
  110. },
  111. );
  112. };
  113. async fetchIssueFlowSnippets() {
  114. const { issue, branchLike } = this.props;
  115. this.setState({ loading: true });
  116. try {
  117. const components =
  118. issue.status === IssueDeprecatedStatus.Closed ? {} : await getIssueFlowSnippets(issue.key);
  119. if (components[issue.component] === undefined) {
  120. const issueComponent = await getComponentForSourceViewer({
  121. // If the issue's component doesn't exist anymore (typically a deleted file), use the project
  122. component: issue.componentEnabled ? issue.component : issue.project,
  123. ...getBranchLikeQuery(branchLike),
  124. });
  125. components[issue.component] = { component: issueComponent, sources: [] };
  126. if (isFile(issueComponent.q)) {
  127. const sources = await getSources({
  128. key: issueComponent.key,
  129. ...getBranchLikeQuery(branchLike),
  130. from: 1,
  131. to: 10,
  132. }).then((lines) => keyBy(lines, 'line'));
  133. components[issue.component].sources = sources;
  134. }
  135. }
  136. if (this.mounted) {
  137. this.setState({
  138. components,
  139. loading: false,
  140. });
  141. }
  142. } catch (response) {
  143. const rsp = response as Response;
  144. if (rsp.status !== HttpStatus.Forbidden) {
  145. throwGlobalError(response);
  146. }
  147. if (this.mounted) {
  148. this.setState({ loading: false, notAccessible: rsp.status === HttpStatus.Forbidden });
  149. }
  150. }
  151. }
  152. renderDuplicationPopup = (component: SourceViewerFile, index: number, line: number) => {
  153. const { duplicatedFiles, duplications } = this.state;
  154. if (!component || !duplicatedFiles) {
  155. return null;
  156. }
  157. const blocks = getDuplicationBlocksForIndex(duplications, index);
  158. return (
  159. <WorkspaceContext.Consumer>
  160. {({ openComponent }) => (
  161. <DuplicationPopup
  162. blocks={filterDuplicationBlocksByLine(blocks, line)}
  163. branchLike={this.props.branchLike}
  164. inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
  165. duplicatedFiles={duplicatedFiles}
  166. openComponent={openComponent}
  167. sourceViewerFile={component}
  168. duplicationHeader={translate('component_viewer.transition.duplication')}
  169. />
  170. )}
  171. </WorkspaceContext.Consumer>
  172. );
  173. };
  174. render() {
  175. const { loading, notAccessible } = this.state;
  176. if (loading) {
  177. return (
  178. <div>
  179. <Spinner ariaLabel={translate('code_viewer.loading')} />
  180. </div>
  181. );
  182. }
  183. if (notAccessible) {
  184. return (
  185. <FlagMessage className="sw-mt-2" variant="warning">
  186. {translate('code_viewer.no_source_code_displayed_due_to_security')}
  187. </FlagMessage>
  188. );
  189. }
  190. const { issue, locations } = this.props;
  191. const { components, duplications, duplicationsByLine } = this.state;
  192. const issuesByComponent = issuesByComponentAndLine(this.props.issues);
  193. const locationsByComponent = groupLocationsByComponent(issue, locations, components);
  194. const lastOccurenceOfPrimaryComponent = findLastIndex(locationsByComponent, ({ component }) =>
  195. component ? component.key === issue.component : true,
  196. );
  197. if (components[issue.component] === undefined) {
  198. return null;
  199. }
  200. return (
  201. <>
  202. {locationsByComponent.map((snippetGroup, i) => {
  203. return (
  204. <SourceViewerContext.Provider
  205. key={`${issue.key}-${this.props.selectedFlowIndex}-${snippetGroup.component.key}`}
  206. value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}
  207. >
  208. <ComponentSourceSnippetGroupViewer
  209. branchLike={this.props.branchLike}
  210. duplications={duplications}
  211. duplicationsByLine={duplicationsByLine}
  212. highlightedLocationMessage={this.props.highlightedLocationMessage}
  213. issue={issue}
  214. issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
  215. isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent}
  216. loadDuplications={this.fetchDuplications}
  217. locations={snippetGroup.locations || []}
  218. onIssueSelect={this.props.onIssueSelect}
  219. onLocationSelect={this.props.onLocationSelect}
  220. renderDuplicationPopup={this.renderDuplicationPopup}
  221. snippetGroup={snippetGroup}
  222. />
  223. </SourceViewerContext.Provider>
  224. );
  225. })}
  226. </>
  227. );
  228. }
  229. }