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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2018 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 Summary from './Summary';
  23. import Report from './Report';
  24. import WorstProjects from './WorstProjects';
  25. import ReleasabilityBox from './ReleasabilityBox';
  26. import ReliabilityBox from './ReliabilityBox';
  27. import SecurityBox from './SecurityBox';
  28. import MaintainabilityBox from './MaintainabilityBox';
  29. import Activity from './Activity';
  30. import { SubComponent } from '../types';
  31. import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../utils';
  32. import { getMeasures } from '../../../api/measures';
  33. import { getChildren } from '../../../api/components';
  34. import { translate } from '../../../helpers/l10n';
  35. import { fetchMetrics } from '../../../store/rootActions';
  36. import { getMetrics } from '../../../store/rootReducer';
  37. import { Metric, Component } from '../../../app/types';
  38. import '../styles.css';
  39. interface OwnProps {
  40. component: Component;
  41. }
  42. interface StateToProps {
  43. metrics: { [key: string]: Metric };
  44. }
  45. interface DispatchToProps {
  46. fetchMetrics: () => void;
  47. }
  48. type Props = StateToProps & DispatchToProps & OwnProps;
  49. interface State {
  50. loading: boolean;
  51. measures?: { [key: string]: string | undefined };
  52. subComponents?: SubComponent[];
  53. totalSubComponents?: number;
  54. }
  55. export class App extends React.PureComponent<Props, State> {
  56. mounted = false;
  57. state: State = { loading: true };
  58. componentDidMount() {
  59. this.mounted = true;
  60. this.props.fetchMetrics();
  61. this.fetchData();
  62. }
  63. componentDidUpdate(prevProps: Props) {
  64. if (prevProps.component !== this.props.component) {
  65. this.fetchData();
  66. }
  67. }
  68. componentWillUnmount() {
  69. this.mounted = false;
  70. }
  71. fetchData() {
  72. this.setState({ loading: true });
  73. Promise.all([
  74. getMeasures({ componentKey: this.props.component.key, metricKeys: PORTFOLIO_METRICS.join() }),
  75. getChildren(this.props.component.key, SUB_COMPONENTS_METRICS, { ps: 20, s: 'qualifier' })
  76. ]).then(
  77. ([measures, subComponents]) => {
  78. if (this.mounted) {
  79. this.setState({
  80. loading: false,
  81. measures: convertMeasures(measures),
  82. subComponents: subComponents.components.map((component: any) => ({
  83. ...component,
  84. measures: convertMeasures(component.measures)
  85. })),
  86. totalSubComponents: subComponents.paging.total
  87. });
  88. }
  89. },
  90. () => {
  91. if (this.mounted) {
  92. this.setState({ loading: false });
  93. }
  94. }
  95. );
  96. }
  97. isEmpty = () => this.state.measures === undefined || this.state.measures['ncloc'] === undefined;
  98. isNotComputed = () =>
  99. this.state.measures && this.state.measures['reliability_rating'] === undefined;
  100. renderSpinner() {
  101. return (
  102. <div className="page page-limited">
  103. <div className="text-center">
  104. <i className="spinner spinner-margin" />
  105. </div>
  106. </div>
  107. );
  108. }
  109. renderEmpty() {
  110. return (
  111. <div className="empty-search">
  112. <h3>{translate('portfolio.empty')}</h3>
  113. <p>{translate('portfolio.empty.hint')}</p>
  114. </div>
  115. );
  116. }
  117. renderWhenNotComputed() {
  118. return (
  119. <div className="empty-search">
  120. <h3>{translate('portfolio.not_computed')}</h3>
  121. </div>
  122. );
  123. }
  124. renderMain() {
  125. const { component } = this.props;
  126. const { measures, subComponents, totalSubComponents } = this.state;
  127. if (this.isEmpty()) {
  128. return this.renderEmpty();
  129. }
  130. if (this.isNotComputed()) {
  131. return this.renderWhenNotComputed();
  132. }
  133. return (
  134. <div>
  135. <div className="portfolio-boxes">
  136. <ReleasabilityBox component={component.key} measures={measures!} />
  137. <ReliabilityBox component={component.key} measures={measures!} />
  138. <SecurityBox component={component.key} measures={measures!} />
  139. <MaintainabilityBox component={component.key} measures={measures!} />
  140. </div>
  141. {subComponents !== undefined &&
  142. totalSubComponents !== undefined && (
  143. <WorstProjects
  144. component={component.key}
  145. subComponents={subComponents}
  146. total={totalSubComponents}
  147. />
  148. )}
  149. </div>
  150. );
  151. }
  152. render() {
  153. const { component } = this.props;
  154. const { loading, measures } = this.state;
  155. if (loading) {
  156. return this.renderSpinner();
  157. }
  158. return (
  159. <div className="page page-limited">
  160. <div className="page-with-sidebar">
  161. <div className="page-main">{this.renderMain()}</div>
  162. <aside className="page-sidebar-fixed">
  163. <div className="portfolio-meta-card">
  164. <h4 className="portfolio-meta-header">
  165. {translate('overview.about_this_portfolio')}
  166. </h4>
  167. {!this.isEmpty() &&
  168. !this.isNotComputed() && <Summary component={component} measures={measures!} />}
  169. </div>
  170. <div className="portfolio-meta-card">
  171. <Activity component={component.key} metrics={this.props.metrics} />
  172. </div>
  173. <div className="portfolio-meta-card">
  174. <Report component={component} />
  175. </div>
  176. </aside>
  177. </div>
  178. </div>
  179. );
  180. }
  181. }
  182. const mapDispatchToProps: DispatchToProps = { fetchMetrics };
  183. const mapStateToProps = (state: any): StateToProps => ({
  184. metrics: getMetrics(state)
  185. });
  186. export default connect<StateToProps, DispatchToProps, Props>(mapStateToProps, mapDispatchToProps)(
  187. App
  188. );