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.

MeasureContent.tsx 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 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 * as React from 'react';
  21. import { InjectedRouter } from 'react-router';
  22. import { getComponentTree } from '../../../api/components';
  23. import { getMeasures } from '../../../api/measures';
  24. import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
  25. import SourceViewer from '../../../components/SourceViewer/SourceViewer';
  26. import PageActions from '../../../components/ui/PageActions';
  27. import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
  28. import { getComponentMeasureUniqueKey } from '../../../helpers/component';
  29. import { translate } from '../../../helpers/l10n';
  30. import { isDiffMetric } from '../../../helpers/measures';
  31. import { RequestData } from '../../../helpers/request';
  32. import { scrollToElement } from '../../../helpers/scrolling';
  33. import { getProjectUrl } from '../../../helpers/urls';
  34. import { BranchLike } from '../../../types/branch-like';
  35. import { isFile, isView } from '../../../types/component';
  36. import { MeasurePageView } from '../../../types/measures';
  37. import { MetricKey } from '../../../types/metrics';
  38. import { complementary } from '../config/complementary';
  39. import FilesView from '../drilldown/FilesView';
  40. import TreeMapView from '../drilldown/TreeMapView';
  41. import { enhanceComponent, Query } from '../utils';
  42. import Breadcrumbs from './Breadcrumbs';
  43. import MeasureContentHeader from './MeasureContentHeader';
  44. import MeasureHeader from './MeasureHeader';
  45. import MeasureViewSelect from './MeasureViewSelect';
  46. interface Props {
  47. branchLike?: BranchLike;
  48. leakPeriod?: T.Period;
  49. requestedMetric: Pick<T.Metric, 'key' | 'direction'>;
  50. metrics: T.Dict<T.Metric>;
  51. onIssueChange?: (issue: T.Issue) => void;
  52. rootComponent: T.ComponentMeasure;
  53. router: InjectedRouter;
  54. selected?: string;
  55. updateQuery: (query: Partial<Query>) => void;
  56. view: MeasurePageView;
  57. }
  58. interface State {
  59. baseComponent?: T.ComponentMeasure;
  60. components: T.ComponentMeasureEnhanced[];
  61. loadingMoreComponents: boolean;
  62. measure?: T.Measure;
  63. metric?: T.Metric;
  64. paging?: T.Paging;
  65. secondaryMeasure?: T.Measure;
  66. selectedComponent?: T.ComponentMeasureIntern;
  67. }
  68. export default class MeasureContent extends React.PureComponent<Props, State> {
  69. container?: HTMLElement | null;
  70. mounted = false;
  71. state: State = {
  72. components: [],
  73. loadingMoreComponents: false
  74. };
  75. componentDidMount() {
  76. this.mounted = true;
  77. this.fetchComponentTree();
  78. }
  79. componentDidUpdate(prevProps: Props) {
  80. const prevComponentKey = prevProps.selected || prevProps.rootComponent.key;
  81. const componentKey = this.props.selected || this.props.rootComponent.key;
  82. if (
  83. prevComponentKey !== componentKey ||
  84. !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
  85. prevProps.requestedMetric !== this.props.requestedMetric ||
  86. prevProps.view !== this.props.view
  87. ) {
  88. this.fetchComponentTree();
  89. }
  90. }
  91. componentWillUnmount() {
  92. this.mounted = false;
  93. }
  94. fetchComponentTree = () => {
  95. const { metricKeys, opts, strategy } = this.getComponentRequestParams(
  96. this.props.view,
  97. this.props.requestedMetric
  98. );
  99. const componentKey = this.props.selected || this.props.rootComponent.key;
  100. const baseComponentMetrics = [this.props.requestedMetric.key];
  101. if (this.props.requestedMetric.key === MetricKey.ncloc) {
  102. baseComponentMetrics.push('ncloc_language_distribution');
  103. }
  104. Promise.all([
  105. getComponentTree(strategy, componentKey, metricKeys, opts),
  106. getMeasures({
  107. component: componentKey,
  108. metricKeys: baseComponentMetrics.join(),
  109. ...getBranchLikeQuery(this.props.branchLike)
  110. })
  111. ]).then(([tree, measures]) => {
  112. if (this.mounted) {
  113. const metric = tree.metrics.find(m => m.key === this.props.requestedMetric.key);
  114. const components = tree.components.map(component =>
  115. enhanceComponent(component, metric, this.props.metrics)
  116. );
  117. const measure = measures.find(measure => measure.metric === this.props.requestedMetric.key);
  118. const secondaryMeasure = measures.find(
  119. measure => measure.metric !== this.props.requestedMetric.key
  120. );
  121. this.setState(({ selectedComponent }) => ({
  122. baseComponent: tree.baseComponent,
  123. components,
  124. measure,
  125. metric,
  126. paging: tree.paging,
  127. secondaryMeasure,
  128. selectedComponent:
  129. components.length > 0 &&
  130. components.find(
  131. c =>
  132. getComponentMeasureUniqueKey(c) === getComponentMeasureUniqueKey(selectedComponent)
  133. )
  134. ? selectedComponent
  135. : undefined
  136. }));
  137. }
  138. });
  139. };
  140. fetchMoreComponents = () => {
  141. const { metrics, view } = this.props;
  142. const { baseComponent, metric, paging } = this.state;
  143. if (!baseComponent || !paging || !metric) {
  144. return;
  145. }
  146. const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric, {
  147. p: paging.pageIndex + 1
  148. });
  149. this.setState({ loadingMoreComponents: true });
  150. getComponentTree(strategy, baseComponent.key, metricKeys, opts).then(
  151. r => {
  152. if (this.mounted && metric.key === this.props.requestedMetric.key) {
  153. this.setState(state => ({
  154. components: [
  155. ...state.components,
  156. ...r.components.map(component => enhanceComponent(component, metric, metrics))
  157. ],
  158. loadingMoreComponents: false,
  159. paging: r.paging
  160. }));
  161. }
  162. },
  163. () => {
  164. if (this.mounted) {
  165. this.setState({ loadingMoreComponents: false });
  166. }
  167. }
  168. );
  169. };
  170. getComponentRequestParams(
  171. view: MeasurePageView,
  172. metric: Pick<T.Metric, 'key' | 'direction'>,
  173. options: Object = {}
  174. ) {
  175. const strategy = view === 'list' ? 'leaves' : 'children';
  176. const metricKeys = [metric.key];
  177. const opts: RequestData = {
  178. ...getBranchLikeQuery(this.props.branchLike),
  179. additionalFields: 'metrics',
  180. ps: 500
  181. };
  182. const setMetricSort = () => {
  183. const isDiff = isDiffMetric(metric.key);
  184. opts.s = isDiff ? 'metricPeriod' : 'metric';
  185. opts.metricSortFilter = 'withMeasuresOnly';
  186. if (isDiff) {
  187. opts.metricPeriodSort = 1;
  188. }
  189. };
  190. const isDiff = isDiffMetric(metric.key);
  191. if (view === 'tree') {
  192. metricKeys.push(...(complementary[metric.key] || []));
  193. opts.asc = true;
  194. opts.s = 'qualifier,name';
  195. } else if (view === 'list') {
  196. metricKeys.push(...(complementary[metric.key] || []));
  197. opts.asc = metric.direction === 1;
  198. opts.metricSort = metric.key;
  199. setMetricSort();
  200. } else if (view === 'treemap') {
  201. const sizeMetric = isDiff ? 'new_lines' : 'ncloc';
  202. metricKeys.push(sizeMetric);
  203. opts.asc = false;
  204. opts.metricSort = sizeMetric;
  205. setMetricSort();
  206. }
  207. return { metricKeys, opts: { ...opts, ...options }, strategy };
  208. }
  209. updateSelected = (component: string) => {
  210. this.props.updateQuery({
  211. selected: component !== this.props.rootComponent.key ? component : undefined
  212. });
  213. };
  214. updateView = (view: MeasurePageView) => {
  215. this.props.updateQuery({ view });
  216. };
  217. onOpenComponent = (component: T.ComponentMeasureIntern) => {
  218. if (isView(this.props.rootComponent.qualifier)) {
  219. const comp = this.state.components.find(
  220. c =>
  221. c.refKey === component.key ||
  222. getComponentMeasureUniqueKey(c) === getComponentMeasureUniqueKey(component)
  223. );
  224. if (comp) {
  225. this.props.router.push(getProjectUrl(comp.refKey || comp.key, component.branch));
  226. }
  227. return;
  228. }
  229. this.setState(state => ({ selectedComponent: state.baseComponent }));
  230. this.updateSelected(component.key);
  231. if (this.container) {
  232. this.container.focus();
  233. }
  234. };
  235. onSelectComponent = (component: T.ComponentMeasureIntern) => {
  236. this.setState({ selectedComponent: component });
  237. };
  238. getSelectedIndex = () => {
  239. const componentKey = isFile(this.state.baseComponent?.qualifier)
  240. ? getComponentMeasureUniqueKey(this.state.baseComponent)
  241. : getComponentMeasureUniqueKey(this.state.selectedComponent);
  242. const index = this.state.components.findIndex(
  243. component => getComponentMeasureUniqueKey(component) === componentKey
  244. );
  245. return index !== -1 ? index : undefined;
  246. };
  247. handleScroll = (element: Element) => {
  248. const offset = window.innerHeight / 2;
  249. scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth: true });
  250. };
  251. renderMeasure() {
  252. const { view } = this.props;
  253. const { metric } = this.state;
  254. if (!metric) {
  255. return null;
  256. }
  257. if (view === 'tree' || view === 'list') {
  258. const selectedIdx = this.getSelectedIndex();
  259. return (
  260. <FilesView
  261. branchLike={this.props.branchLike}
  262. components={this.state.components}
  263. defaultShowBestMeasures={view === 'tree'}
  264. fetchMore={this.fetchMoreComponents}
  265. handleOpen={this.onOpenComponent}
  266. handleSelect={this.onSelectComponent}
  267. loadingMore={this.state.loadingMoreComponents}
  268. metric={metric}
  269. metrics={this.props.metrics}
  270. paging={this.state.paging}
  271. rootComponent={this.props.rootComponent}
  272. selectedIdx={selectedIdx}
  273. selectedComponent={
  274. selectedIdx !== undefined
  275. ? (this.state.selectedComponent as T.ComponentMeasureEnhanced)
  276. : undefined
  277. }
  278. view={view}
  279. />
  280. );
  281. }
  282. return (
  283. <TreeMapView
  284. branchLike={this.props.branchLike}
  285. components={this.state.components}
  286. handleSelect={this.onOpenComponent}
  287. metric={metric}
  288. />
  289. );
  290. }
  291. render() {
  292. const { branchLike, rootComponent, view } = this.props;
  293. const { baseComponent, measure, metric, paging, secondaryMeasure } = this.state;
  294. if (!baseComponent || !metric) {
  295. return null;
  296. }
  297. const measureValue =
  298. measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value);
  299. const isFileComponent = isFile(baseComponent.qualifier);
  300. const selectedIdx = this.getSelectedIndex();
  301. return (
  302. <div className="layout-page-main no-outline" ref={container => (this.container = container)}>
  303. <A11ySkipTarget anchor="measures_main" />
  304. <div className="layout-page-header-panel layout-page-main-header">
  305. <div className="layout-page-header-panel-inner layout-page-main-header-inner">
  306. <div className="layout-page-main-inner">
  307. <MeasureContentHeader
  308. left={
  309. <Breadcrumbs
  310. backToFirst={view === 'list'}
  311. branchLike={branchLike}
  312. className="text-ellipsis flex-1"
  313. component={baseComponent}
  314. handleSelect={this.onOpenComponent}
  315. rootComponent={rootComponent}
  316. />
  317. }
  318. right={
  319. <div className="display-flex-center">
  320. {!isFileComponent && metric && (
  321. <>
  322. <div>{translate('component_measures.view_as')}</div>
  323. <MeasureViewSelect
  324. className="measure-view-select spacer-left big-spacer-right"
  325. handleViewChange={this.updateView}
  326. metric={metric}
  327. view={view}
  328. />
  329. <PageActions
  330. componentQualifier={rootComponent.qualifier}
  331. current={
  332. selectedIdx !== undefined && view !== 'treemap'
  333. ? selectedIdx + 1
  334. : undefined
  335. }
  336. showShortcuts={['list', 'tree'].includes(view)}
  337. total={paging && paging.total}
  338. />
  339. </>
  340. )}
  341. </div>
  342. }
  343. />
  344. </div>
  345. </div>
  346. </div>
  347. <div className="layout-page-main-inner measure-details-content">
  348. <MeasureHeader
  349. branchLike={branchLike}
  350. component={baseComponent}
  351. leakPeriod={this.props.leakPeriod}
  352. measureValue={measureValue}
  353. metric={metric}
  354. secondaryMeasure={secondaryMeasure}
  355. />
  356. {isFileComponent ? (
  357. <div className="measure-details-viewer">
  358. <SourceViewer
  359. branchLike={branchLike}
  360. component={baseComponent.key}
  361. metricKey={this.state.metric?.key}
  362. onIssueChange={this.props.onIssueChange}
  363. scroll={this.handleScroll}
  364. />
  365. </div>
  366. ) : (
  367. this.renderMeasure()
  368. )}
  369. </div>
  370. </div>
  371. );
  372. }
  373. }