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.

CodeAppRenderer.tsx 8.6KB

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 {
  22. Card,
  23. FlagMessage,
  24. HelperHintIcon,
  25. KeyboardHint,
  26. LargeCenteredLayout,
  27. LightLabel,
  28. } from 'design-system';
  29. import { difference, intersection } from 'lodash';
  30. import * as React from 'react';
  31. import { Helmet } from 'react-helmet-async';
  32. import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
  33. import HelpTooltip from '../../../components/controls/HelpTooltip';
  34. import ListFooter from '../../../components/controls/ListFooter';
  35. import Suggestions from '../../../components/embed-docs-modal/Suggestions';
  36. import { Location } from '../../../components/hoc/withRouter';
  37. import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
  38. import { CCT_SOFTWARE_QUALITY_METRICS, OLD_TAXONOMY_METRICS } from '../../../helpers/constants';
  39. import { KeyboardKeys } from '../../../helpers/keycodes';
  40. import { translate } from '../../../helpers/l10n';
  41. import { areCCTMeasuresComputed } from '../../../helpers/measures';
  42. import { BranchLike } from '../../../types/branch-like';
  43. import { isApplication, isPortfolioLike } from '../../../types/component';
  44. import { Breadcrumb, Component, ComponentMeasure, Dict, Metric } from '../../../types/types';
  45. import { getCodeMetrics } from '../utils';
  46. import CodeBreadcrumbs from './CodeBreadcrumbs';
  47. import Components from './Components';
  48. import Search from './Search';
  49. import SourceViewerWrapper from './SourceViewerWrapper';
  50. interface Props {
  51. branchLike?: BranchLike;
  52. component: Component;
  53. location: Location;
  54. metrics: Dict<Metric>;
  55. baseComponent?: ComponentMeasure;
  56. breadcrumbs: Breadcrumb[];
  57. components?: ComponentMeasure[];
  58. highlighted?: ComponentMeasure;
  59. loading: boolean;
  60. searchResults?: ComponentMeasure[];
  61. sourceViewer?: ComponentMeasure;
  62. total: number;
  63. newCodeSelected: boolean;
  64. handleGoToParent: () => void;
  65. handleHighlight: (highlighted: ComponentMeasure) => void;
  66. handleLoadMore: () => void;
  67. handleSearchClear: () => void;
  68. handleSearchResults: (searchResults: ComponentMeasure[]) => void;
  69. handleSelect: (component: ComponentMeasure) => void;
  70. handleSelectNewCode: (newCodeSelected: boolean) => void;
  71. }
  72. export default function CodeAppRenderer(props: Readonly<Props>) {
  73. const {
  74. branchLike,
  75. component,
  76. location,
  77. baseComponent,
  78. breadcrumbs,
  79. components = [],
  80. highlighted,
  81. loading,
  82. metrics,
  83. newCodeSelected,
  84. total,
  85. searchResults,
  86. sourceViewer,
  87. } = props;
  88. const { canBrowseAllChildProjects, qualifier } = component;
  89. const showSearch = searchResults !== undefined;
  90. const hasComponents = components.length > 0 || searchResults !== undefined;
  91. const showBreadcrumbs = breadcrumbs.length > 1 && !showSearch;
  92. const showComponentList = sourceViewer === undefined && components.length > 0 && !showSearch;
  93. const metricKeys = intersection(
  94. getCodeMetrics(component.qualifier, branchLike, { newCode: newCodeSelected }),
  95. Object.keys(metrics),
  96. );
  97. const allComponentsHaveSoftwareQualityMeasures = components.every((component) =>
  98. areCCTMeasuresComputed(component.measures),
  99. );
  100. const filteredMetrics = difference(
  101. metricKeys,
  102. allComponentsHaveSoftwareQualityMeasures ? OLD_TAXONOMY_METRICS : CCT_SOFTWARE_QUALITY_METRICS,
  103. ).map((key) => metrics[key]);
  104. let defaultTitle = translate('code.page');
  105. if (isApplication(baseComponent?.qualifier)) {
  106. defaultTitle = translate('projects.page');
  107. } else if (isPortfolioLike(baseComponent?.qualifier)) {
  108. defaultTitle = translate('portfolio_breakdown.page');
  109. }
  110. const isPortfolio = isPortfolioLike(qualifier);
  111. return (
  112. <LargeCenteredLayout className="sw-py-8 sw-body-md" id="code-page">
  113. <Suggestions suggestions="code" />
  114. <Helmet defer={false} title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle} />
  115. <A11ySkipTarget anchor="code_main" />
  116. {!canBrowseAllChildProjects && isPortfolio && (
  117. <FlagMessage variant="warning" className="it__portfolio_warning sw-mb-4">
  118. {translate('code_viewer.not_all_measures_are_shown')}
  119. <HelpTooltip
  120. className="sw-ml-2"
  121. overlay={translate('code_viewer.not_all_measures_are_shown.help')}
  122. >
  123. <HelperHintIcon />
  124. </HelpTooltip>
  125. </FlagMessage>
  126. )}
  127. {!allComponentsHaveSoftwareQualityMeasures && (
  128. <AnalysisMissingInfoMessage qualifier={component.qualifier} className="sw-mb-4" />
  129. )}
  130. <div className="sw-flex sw-justify-between">
  131. <div>
  132. {hasComponents && (
  133. <Search
  134. branchLike={branchLike}
  135. className="sw-mb-4"
  136. component={component}
  137. newCodeSelected={newCodeSelected}
  138. onNewCodeToggle={props.handleSelectNewCode}
  139. onSearchClear={props.handleSearchClear}
  140. onSearchResults={props.handleSearchResults}
  141. />
  142. )}
  143. {!hasComponents && sourceViewer === undefined && (
  144. <div className="sw-flex sw-align-center sw-flex-col sw-fixed sw-top-1/2">
  145. <LightLabel>
  146. {translate(
  147. 'code_viewer.no_source_code_displayed_due_to_empty_analysis',
  148. component.qualifier,
  149. )}
  150. </LightLabel>
  151. </div>
  152. )}
  153. {showBreadcrumbs && (
  154. <CodeBreadcrumbs
  155. branchLike={branchLike}
  156. breadcrumbs={breadcrumbs}
  157. rootComponent={component}
  158. />
  159. )}
  160. </div>
  161. {(showComponentList || showSearch) && (
  162. <div className="sw-flex sw-items-end sw-body-sm">
  163. <KeyboardHint
  164. className="sw-mr-4 sw-ml-6"
  165. command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
  166. title={translate('component_measures.select_files')}
  167. />
  168. <KeyboardHint
  169. command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
  170. title={translate('component_measures.navigate')}
  171. />
  172. </div>
  173. )}
  174. </div>
  175. {(showComponentList || showSearch) && (
  176. <Card className="sw-mt-2 sw-overflow-auto">
  177. <Spinner isLoading={loading}>
  178. {showComponentList && (
  179. <Components
  180. baseComponent={baseComponent}
  181. branchLike={branchLike}
  182. components={components}
  183. cycle
  184. metrics={filteredMetrics}
  185. onEndOfList={props.handleLoadMore}
  186. onGoToParent={props.handleGoToParent}
  187. onHighlight={props.handleHighlight}
  188. onSelect={props.handleSelect}
  189. rootComponent={component}
  190. selected={highlighted}
  191. newCodeSelected={newCodeSelected}
  192. showAnalysisDate={isPortfolio}
  193. />
  194. )}
  195. {showSearch && (
  196. <Components
  197. branchLike={branchLike}
  198. components={searchResults}
  199. metrics={[]}
  200. onHighlight={props.handleHighlight}
  201. onSelect={props.handleSelect}
  202. rootComponent={component}
  203. selected={highlighted}
  204. />
  205. )}
  206. </Spinner>
  207. </Card>
  208. )}
  209. {showComponentList && (
  210. <ListFooter count={components.length} loadMore={props.handleLoadMore} total={total} />
  211. )}
  212. {sourceViewer !== undefined && !showSearch && (
  213. <div className="sw-mt-2">
  214. <SourceViewerWrapper
  215. branchLike={branchLike}
  216. component={sourceViewer.key}
  217. componentMeasures={sourceViewer.measures}
  218. isFile
  219. location={location}
  220. onGoToParent={props.handleGoToParent}
  221. />
  222. </div>
  223. )}
  224. </LargeCenteredLayout>
  225. );
  226. }