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.

App.tsx 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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 { connect } from 'react-redux';
  22. import { Link } from 'react-router';
  23. import MeasuresButtonLink from './MeasuresButtonLink';
  24. import MetricBox from './MetricBox';
  25. import Report from './Report';
  26. import WorstProjects from './WorstProjects';
  27. import { SubComponent } from '../types';
  28. import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../utils';
  29. import Measure from '../../../components/measure/Measure';
  30. import { getChildren } from '../../../api/components';
  31. import { getMeasures } from '../../../api/measures';
  32. import { translate } from '../../../helpers/l10n';
  33. import { getComponentDrilldownUrl } from '../../../helpers/urls';
  34. import { fetchMetrics } from '../../../store/rootActions';
  35. import { getMetrics, Store } from '../../../store/rootReducer';
  36. import '../styles.css';
  37. interface OwnProps {
  38. component: T.Component;
  39. }
  40. interface StateToProps {
  41. metrics: T.Dict<T.Metric>;
  42. }
  43. interface DispatchToProps {
  44. fetchMetrics: () => void;
  45. }
  46. type Props = StateToProps & DispatchToProps & OwnProps;
  47. interface State {
  48. loading: boolean;
  49. measures?: T.Dict<string | undefined>;
  50. subComponents?: SubComponent[];
  51. totalSubComponents?: number;
  52. }
  53. export class App extends React.PureComponent<Props, State> {
  54. mounted = false;
  55. state: State = { loading: true };
  56. componentDidMount() {
  57. this.mounted = true;
  58. this.props.fetchMetrics();
  59. this.fetchData();
  60. }
  61. componentDidUpdate(prevProps: Props) {
  62. if (prevProps.component !== this.props.component) {
  63. this.fetchData();
  64. }
  65. }
  66. componentWillUnmount() {
  67. this.mounted = false;
  68. }
  69. fetchData() {
  70. this.setState({ loading: true });
  71. Promise.all([
  72. getMeasures({ component: this.props.component.key, metricKeys: PORTFOLIO_METRICS.join() }),
  73. getChildren(this.props.component.key, SUB_COMPONENTS_METRICS, { ps: 20, s: 'qualifier' })
  74. ]).then(
  75. ([measures, subComponents]) => {
  76. if (this.mounted) {
  77. this.setState({
  78. loading: false,
  79. measures: convertMeasures(measures),
  80. subComponents: subComponents.components.map((component: any) => ({
  81. ...component,
  82. measures: convertMeasures(component.measures)
  83. })),
  84. totalSubComponents: subComponents.paging.total
  85. });
  86. }
  87. },
  88. () => {
  89. if (this.mounted) {
  90. this.setState({ loading: false });
  91. }
  92. }
  93. );
  94. }
  95. isEmpty = () => this.state.measures === undefined || this.state.measures['ncloc'] === undefined;
  96. isNotComputed = () =>
  97. this.state.measures && this.state.measures['reliability_rating'] === undefined;
  98. renderSpinner() {
  99. return (
  100. <div className="page page-limited">
  101. <div className="text-center">
  102. <i className="spinner spinner-margin" />
  103. </div>
  104. </div>
  105. );
  106. }
  107. renderEmpty() {
  108. return (
  109. <div className="empty-search">
  110. <h3>
  111. {!this.state.measures || !this.state.measures['projects']
  112. ? translate('portfolio.empty')
  113. : translate('portfolio.no_lines_of_code')}
  114. </h3>
  115. </div>
  116. );
  117. }
  118. renderWhenNotComputed() {
  119. return (
  120. <div className="empty-search">
  121. <h3>{translate('portfolio.not_computed')}</h3>
  122. </div>
  123. );
  124. }
  125. render() {
  126. const { component } = this.props;
  127. const { loading, measures, subComponents, totalSubComponents } = this.state;
  128. if (loading) {
  129. return this.renderSpinner();
  130. }
  131. if (this.isEmpty()) {
  132. return this.renderEmpty();
  133. }
  134. if (this.isNotComputed()) {
  135. return this.renderWhenNotComputed();
  136. }
  137. return (
  138. <div className="page page-limited portfolio-overview">
  139. <div className="page-actions">
  140. <Report component={component} />
  141. </div>
  142. <h1>{translate('portfolio.health_factors')}</h1>
  143. <div className="portfolio-boxes">
  144. <MetricBox component={component.key} measures={measures!} metricKey="releasability" />
  145. <MetricBox component={component.key} measures={measures!} metricKey="reliability" />
  146. <MetricBox component={component.key} measures={measures!} metricKey="vulnerabilities" />
  147. <MetricBox component={component.key} measures={measures!} metricKey="security_hotspots" />
  148. <MetricBox component={component.key} measures={measures!} metricKey="maintainability" />
  149. </div>
  150. <h1>{translate('portfolio.breakdown')}</h1>
  151. <div className="portfolio-breakdown">
  152. <div className="portfolio-breakdown-box">
  153. <h2>{translate('portfolio.number_of_projects')}</h2>
  154. <div className="portfolio-breakdown-metric">
  155. <Measure
  156. metricKey="projects"
  157. metricType="SHORT_INT"
  158. value={(measures && measures.projects) || '0'}
  159. />
  160. </div>
  161. <div className="portfolio-breakdown-box-link">
  162. <div>
  163. <MeasuresButtonLink component={component.key} metric="projects" />
  164. </div>
  165. </div>
  166. </div>
  167. <div className="portfolio-breakdown-box">
  168. <h2>{translate('portfolio.number_of_lines')}</h2>
  169. <div className="portfolio-breakdown-metric">
  170. <Measure
  171. metricKey="ncloc"
  172. metricType="SHORT_INT"
  173. value={(measures && measures.ncloc) || '0'}
  174. />
  175. </div>
  176. <div className="portfolio-breakdown-box-link">
  177. <div>
  178. <Link
  179. to={getComponentDrilldownUrl({ componentKey: component.key, metric: 'ncloc' })}>
  180. <span>{translate('portfolio.language_breakdown_link')}</span>
  181. </Link>
  182. </div>
  183. </div>
  184. </div>
  185. </div>
  186. {subComponents !== undefined && totalSubComponents !== undefined && (
  187. <WorstProjects
  188. component={component.key}
  189. subComponents={subComponents}
  190. total={totalSubComponents}
  191. />
  192. )}
  193. </div>
  194. );
  195. }
  196. }
  197. const mapDispatchToProps: DispatchToProps = { fetchMetrics };
  198. const mapStateToProps = (state: Store): StateToProps => ({
  199. metrics: getMetrics(state)
  200. });
  201. export default connect(
  202. mapStateToProps,
  203. mapDispatchToProps
  204. )(App);