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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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
  129. qualifier={component.qualifier}
  130. hide={isPortfolio}
  131. className="sw-mb-4"
  132. />
  133. )}
  134. <div className="sw-flex sw-justify-between">
  135. <div>
  136. {hasComponents && (
  137. <Search
  138. branchLike={branchLike}
  139. className="sw-mb-4"
  140. component={component}
  141. newCodeSelected={newCodeSelected}
  142. onNewCodeToggle={props.handleSelectNewCode}
  143. onSearchClear={props.handleSearchClear}
  144. onSearchResults={props.handleSearchResults}
  145. />
  146. )}
  147. {!hasComponents && sourceViewer === undefined && (
  148. <div className="sw-flex sw-align-center sw-flex-col sw-fixed sw-top-1/2">
  149. <LightLabel>
  150. {translate(
  151. 'code_viewer.no_source_code_displayed_due_to_empty_analysis',
  152. component.qualifier,
  153. )}
  154. </LightLabel>
  155. </div>
  156. )}
  157. {showBreadcrumbs && (
  158. <CodeBreadcrumbs
  159. branchLike={branchLike}
  160. breadcrumbs={breadcrumbs}
  161. rootComponent={component}
  162. />
  163. )}
  164. </div>
  165. {(showComponentList || showSearch) && (
  166. <div className="sw-flex sw-items-end sw-body-sm">
  167. <KeyboardHint
  168. className="sw-mr-4 sw-ml-6"
  169. command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
  170. title={translate('component_measures.select_files')}
  171. />
  172. <KeyboardHint
  173. command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
  174. title={translate('component_measures.navigate')}
  175. />
  176. </div>
  177. )}
  178. </div>
  179. {(showComponentList || showSearch) && (
  180. <Card className="sw-mt-2 sw-overflow-auto">
  181. <Spinner isLoading={loading}>
  182. {showComponentList && (
  183. <Components
  184. baseComponent={baseComponent}
  185. branchLike={branchLike}
  186. components={components}
  187. cycle
  188. metrics={filteredMetrics}
  189. onEndOfList={props.handleLoadMore}
  190. onGoToParent={props.handleGoToParent}
  191. onHighlight={props.handleHighlight}
  192. onSelect={props.handleSelect}
  193. rootComponent={component}
  194. selected={highlighted}
  195. newCodeSelected={newCodeSelected}
  196. showAnalysisDate={isPortfolio}
  197. />
  198. )}
  199. {showSearch && (
  200. <Components
  201. branchLike={branchLike}
  202. components={searchResults}
  203. metrics={[]}
  204. onHighlight={props.handleHighlight}
  205. onSelect={props.handleSelect}
  206. rootComponent={component}
  207. selected={highlighted}
  208. />
  209. )}
  210. </Spinner>
  211. </Card>
  212. )}
  213. {showComponentList && (
  214. <ListFooter count={components.length} loadMore={props.handleLoadMore} total={total} />
  215. )}
  216. {sourceViewer !== undefined && !showSearch && (
  217. <div className="sw-mt-2">
  218. <SourceViewerWrapper
  219. branchLike={branchLike}
  220. component={sourceViewer.key}
  221. componentMeasures={sourceViewer.measures}
  222. isFile
  223. location={location}
  224. onGoToParent={props.handleGoToParent}
  225. />
  226. </div>
  227. )}
  228. </LargeCenteredLayout>
  229. );
  230. }