]> source.dussan.org Git - sonarqube.git/commitdiff
Fetch metrics with redux on project dashboard pages
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 18 Jan 2018 08:10:50 +0000 (09:10 +0100)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 25 Jan 2018 14:16:50 +0000 (15:16 +0100)
20 files changed:
server/sonar-web/src/main/js/app/components/App.tsx
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx
server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx
server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx
server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx
server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/utils.js
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js

index 2b83e2a64689db2b9259082993aeeb3963a7fb41..0b7375c9c93c32b5be6d2d6af056dd92c8e34647 100644 (file)
@@ -38,6 +38,7 @@ interface State {
   canAdmin: boolean;
   loading: boolean;
   onSonarCloud: boolean;
+  organizationsEnabled: boolean;
 }
 
 class App extends React.PureComponent<Props, State> {
@@ -46,19 +47,27 @@ class App extends React.PureComponent<Props, State> {
   static childContextTypes = {
     branchesEnabled: PropTypes.bool.isRequired,
     canAdmin: PropTypes.bool.isRequired,
-    onSonarCloud: PropTypes.bool
+    onSonarCloud: PropTypes.bool,
+    organizationsEnabled: PropTypes.bool
   };
 
   constructor(props: Props) {
     super(props);
-    this.state = { branchesEnabled: false, canAdmin: false, loading: true, onSonarCloud: false };
+    this.state = {
+      branchesEnabled: false,
+      canAdmin: false,
+      loading: true,
+      onSonarCloud: false,
+      organizationsEnabled: false
+    };
   }
 
   getChildContext() {
     return {
       branchesEnabled: this.state.branchesEnabled,
       canAdmin: this.state.canAdmin,
-      onSonarCloud: this.state.onSonarCloud
+      onSonarCloud: this.state.onSonarCloud,
+      organizationsEnabled: this.state.organizationsEnabled
     };
   }
 
@@ -93,7 +102,8 @@ class App extends React.PureComponent<Props, State> {
           canAdmin: appState.canAdmin,
           onSonarCloud: Boolean(
             appState.settings && appState.settings['sonar.sonarcloud.enabled'] === 'true'
-          )
+          ),
+          organizationsEnabled: appState.organizationsEnabled
         });
       }
       return appState;
index de3102a3957f10e9dad045ac5e824161b18e03ea..75d48a09dffc53afc698a6da1d426a5fee6e0d1a 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import * as PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import ComponentContainerNotFound from './ComponentContainerNotFound';
 import ComponentNav from './nav/component/ComponentNav';
@@ -28,7 +29,6 @@ import { Task, getTasksForComponent } from '../../api/ce';
 import { getComponentData } from '../../api/components';
 import { getComponentNavigation } from '../../api/nav';
 import { fetchOrganizations } from '../../store/rootActions';
-import { areThereCustomOrganizations } from '../../store/rootReducer';
 import { STATUSES } from '../../apps/background-tasks/constants';
 
 interface Props {
@@ -37,7 +37,6 @@ interface Props {
   location: {
     query: { branch?: string; id: string };
   };
-  organizationsEnabled?: boolean;
 }
 
 interface State {
@@ -52,6 +51,10 @@ interface State {
 export class ComponentContainer extends React.PureComponent<Props, State> {
   mounted: boolean;
 
+  static contextTypes = {
+    organizationsEnabled: PropTypes.bool
+  };
+
   constructor(props: Props) {
     super(props);
     this.state = { branches: [], loading: true };
@@ -98,7 +101,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       ([nav, data]) => {
         const component = this.addQualifier({ ...nav, ...data });
 
-        if (this.props.organizationsEnabled) {
+        if (this.context.organizationsEnabled) {
           this.props.fetchOrganizations([component.organization]);
         }
 
@@ -197,10 +200,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
   }
 }
 
-const mapStateToProps = (state: any) => ({
-  organizationsEnabled: areThereCustomOrganizations(state)
-});
-
 const mapDispatchToProps = { fetchOrganizations };
 
-export default connect<any, any, any>(mapStateToProps, mapDispatchToProps)(ComponentContainer);
+export default connect<any, any, any>(null, mapDispatchToProps)(ComponentContainer);
index bb6c69b6098ebcef6024666c2a0137f4b523c7c8..b66a2aff8da6d9502545c50f4e4617501f438d43 100644 (file)
@@ -132,12 +132,10 @@ it('loads organization', async () => {
 
   const fetchOrganizations = jest.fn();
   mount(
-    <ComponentContainer
-      fetchOrganizations={fetchOrganizations}
-      location={{ query: { id: 'foo' } }}
-      organizationsEnabled={true}>
+    <ComponentContainer fetchOrganizations={fetchOrganizations} location={{ query: { id: 'foo' } }}>
       <Inner />
-    </ComponentContainer>
+    </ComponentContainer>,
+    { context: { organizationsEnabled: true } }
   );
 
   await new Promise(setImmediate);
@@ -150,12 +148,10 @@ it('fetches status', async () => {
   );
 
   mount(
-    <ComponentContainer
-      fetchOrganizations={jest.fn()}
-      location={{ query: { id: 'foo' } }}
-      organizationsEnabled={true}>
+    <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}>
       <Inner />
-    </ComponentContainer>
+    </ComponentContainer>,
+    { context: { organizationsEnabled: true } }
   );
 
   await new Promise(setImmediate);
index e0b1ca1d754f6bcbb1398e7d9c115a0f729e641c..fc9a9db4554f1aab9f6c48802f811ef77dad6318 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
 import Select from '../../../components/controls/Select';
 import { fetchWebApi } from '../../../api/web-api';
-import { Metric } from '../../../app/types';
 import { BadgeColors, BadgeType, BadgeOptions } from './utils';
 import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import { fetchMetrics } from '../../../store/rootActions';
-import { getMetrics } from '../../../store/rootReducer';
-
-interface StateToProps {
-  metrics: { [key: string]: Metric };
-}
-
-interface DispatchToProps {
-  fetchMetrics: () => void;
-}
+import { Metric } from '../../../app/types';
 
-interface OwnProps {
+interface Props {
   className?: string;
+  metrics: { [key: string]: Metric };
   options: BadgeOptions;
   type: BadgeType;
   updateOptions: (options: Partial<BadgeOptions>) => void;
 }
 
-type Props = StateToProps & DispatchToProps & OwnProps;
-
 interface State {
   badgeMetrics: string[];
 }
 
-export class BadgeParams extends React.PureComponent<Props> {
+export default class BadgeParams extends React.PureComponent<Props> {
   mounted: boolean;
   state: State = { badgeMetrics: [] };
 
   componentDidMount() {
     this.mounted = true;
-    this.props.fetchMetrics();
     this.fetchBadgeMetrics();
   }
 
@@ -84,16 +71,14 @@ export class BadgeParams extends React.PureComponent<Props> {
       value: color
     }));
 
-  getMetricOptions = () => {
-    const { metrics } = this.props;
-    return this.state.badgeMetrics.map(key => {
-      const metric = metrics[key];
+  getMetricOptions = () =>
+    this.state.badgeMetrics.map(key => {
+      const metric = this.props.metrics[key];
       return {
         value: key,
-        label: metric && getLocalizedMetricName(metric)
+        label: metric ? getLocalizedMetricName(metric) : key
       };
     });
-  };
 
   handleColorChange = ({ value }: { value: BadgeColors }) =>
     this.props.updateOptions({ color: value });
@@ -143,14 +128,3 @@ export class BadgeParams extends React.PureComponent<Props> {
     }
   }
 }
-
-const mapDispatchToProps: DispatchToProps = { fetchMetrics };
-
-const mapStateToProps = (state: any): StateToProps => ({
-  metrics: getMetrics(state)
-});
-
-export default connect<StateToProps, DispatchToProps, OwnProps>(
-  mapStateToProps,
-  mapDispatchToProps
-)(BadgeParams);
index 7e4e0daf24944785a2e02cdf43ee26942319ec0f..ae664043b7ccf95a029706d01902e5e684e51228 100644 (file)
@@ -22,12 +22,14 @@ import Modal from '../../../components/controls/Modal';
 import BadgeButton from './BadgeButton';
 import BadgeSnippet from './BadgeSnippet';
 import BadgeParams from './BadgeParams';
-import { getBadgeUrl, BadgeType, BadgeOptions } from './utils';
+import { BadgeType, BadgeOptions, getBadgeUrl } from './utils';
+import { Metric } from '../../../app/types';
 import { translate } from '../../../helpers/l10n';
 import './styles.css';
 
 interface Props {
   branch?: string;
+  metrics: { [key: string]: Metric };
   project: string;
 }
 
@@ -90,6 +92,7 @@ export default class BadgesModal extends React.PureComponent<Props, State> {
               </p>
               <BadgeParams
                 className="big-spacer-bottom"
+                metrics={this.props.metrics}
                 options={badgeOptions}
                 type={selectedType}
                 updateOptions={this.handleUpdateOptions}
index 5108e572be4192bd4481f0a465ff3febdf4df037..5e8936739b384ab304e881760c2e39a21a390545 100644 (file)
@@ -19,7 +19,7 @@
  */
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import { BadgeParams } from '../BadgeParams';
+import BadgeParams from '../BadgeParams';
 import { BadgeType } from '../utils';
 import { Metric } from '../../../../app/types';
 
@@ -62,7 +62,6 @@ it('should display measure badge params', () => {
 function getWrapper(props = {}) {
   return shallow(
     <BadgeParams
-      fetchMetrics={jest.fn()}
       metrics={METRICS}
       options={{ color: 'white', metric: 'alert_status' }}
       type={BadgeType.marketing}
index 3eee7bf70740f5efb28d73875fff6e719f872492..fc403fc3f173caf3b0c4f8eb11e23986193c456a 100644 (file)
@@ -27,7 +27,7 @@ jest.mock('../../../../helpers/urls', () => ({
 }));
 
 it('should display the modal after click', () => {
-  const wrapper = shallow(<BadgesModal branch="branch-6.6" project="foo" />);
+  const wrapper = shallow(<BadgesModal branch="branch-6.6" metrics={{}} project="foo" />);
   expect(wrapper).toMatchSnapshot();
   click(wrapper.find('button'));
   expect(wrapper.find('Modal')).toMatchSnapshot();
index 00e92664a30989b93f1d6fb61c68b44036c56c8a..f43e19a571555d57eab8913fb6551be2735dda2e 100644 (file)
@@ -61,8 +61,9 @@ exports[`should display the modal after click 2`] = `
     >
       overview.badges.measure.description
     </p>
-    <Connect(BadgeParams)
+    <BadgeParams
       className="big-spacer-bottom"
+      metrics={Object {}}
       options={
         Object {
           "color": "white",
index 07d22017edf65464790dc1f16dc22a40e4f4926d..44285fe03da1722a836ef781ec1e4335f4106727 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import { uniq } from 'lodash';
+import { connect } from 'react-redux';
 import QualityGate from '../qualityGate/QualityGate';
 import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate';
 import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities';
@@ -36,15 +37,27 @@ import { getCustomGraph, getGraph } from '../../../helpers/storage';
 import { METRICS, HISTORY_METRICS_LIST } from '../utils';
 import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils';
 import { getBranchName } from '../../../helpers/branches';
-import { Branch, Component } from '../../../app/types';
+import { fetchMetrics } from '../../../store/rootActions';
+import { getMetrics } from '../../../store/rootReducer';
+import { Branch, Component, Metric } from '../../../app/types';
 import '../styles.css';
 
-interface Props {
+interface OwnProps {
   branch?: Branch;
   component: Component;
   onComponentChange: (changes: {}) => void;
 }
 
+interface StateToProps {
+  metrics: { [key: string]: Metric };
+}
+
+interface DispatchToProps {
+  fetchMetrics: () => void;
+}
+
+type Props = StateToProps & DispatchToProps & OwnProps;
+
 interface State {
   history?: History;
   historyStartDate?: Date;
@@ -53,12 +66,13 @@ interface State {
   periods?: Period[];
 }
 
-export default class OverviewApp extends React.PureComponent<Props, State> {
+export class OverviewApp extends React.PureComponent<Props, State> {
   mounted: boolean;
   state: State = { loading: true, measures: [] };
 
   componentDidMount() {
     this.mounted = true;
+    this.props.fetchMetrics();
     this.loadMeasures().then(this.loadHistory, () => {});
   }
 
@@ -183,6 +197,7 @@ export default class OverviewApp extends React.PureComponent<Props, State> {
               component={component}
               history={history}
               measures={measures}
+              metrics={this.props.metrics}
               onComponentChange={this.props.onComponentChange}
             />
           </div>
@@ -191,3 +206,14 @@ export default class OverviewApp extends React.PureComponent<Props, State> {
     );
   }
 }
+
+const mapDispatchToProps: DispatchToProps = { fetchMetrics };
+
+const mapStateToProps = (state: any): StateToProps => ({
+  metrics: getMetrics(state)
+});
+
+export default connect<StateToProps, DispatchToProps, OwnProps>(
+  mapStateToProps,
+  mapDispatchToProps
+)(OverviewApp);
index 30a3d98e7b1547784dff2fae354e60a1bfd48f7c..ac3f80bb2a835f95d39654f2f8c3dedc9a5ea753 100644 (file)
@@ -20,7 +20,6 @@
 import * as React from 'react';
 import { Link } from 'react-router';
 import Analysis from './Analysis';
-import { getAllMetrics } from '../../../api/metrics';
 import { getProjectActivity, Analysis as IAnalysis } from '../../../api/projectActivity';
 import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
 import { translate } from '../../../helpers/l10n';
@@ -31,20 +30,20 @@ interface Props {
   branch?: string;
   component: Component;
   history?: History;
+  metrics: { [key: string]: Metric };
   qualifier: string;
 }
 
 interface State {
   analyses: IAnalysis[];
   loading: boolean;
-  metrics: Metric[];
 }
 
 const PAGE_SIZE = 3;
 
 export default class AnalysesList extends React.PureComponent<Props, State> {
   mounted: boolean;
-  state: State = { analyses: [], loading: true, metrics: [] };
+  state: State = { analyses: [], loading: true };
 
   componentDidMount() {
     this.mounted = true;
@@ -75,17 +74,15 @@ export default class AnalysesList extends React.PureComponent<Props, State> {
 
   fetchData = () => {
     this.setState({ loading: true });
-    Promise.all([
-      getProjectActivity({
-        branch: this.props.branch,
-        project: this.getTopLevelComponent(),
-        ps: PAGE_SIZE
-      }),
-      getAllMetrics()
-    ]).then(
-      ([{ analyses }, metrics]) => {
+
+    getProjectActivity({
+      branch: this.props.branch,
+      project: this.getTopLevelComponent(),
+      ps: PAGE_SIZE
+    }).then(
+      ({ analyses }) => {
         if (this.mounted) {
-          this.setState({ analyses, metrics, loading: false });
+          this.setState({ analyses, loading: false });
         }
       },
       () => {
@@ -125,7 +122,7 @@ export default class AnalysesList extends React.PureComponent<Props, State> {
           branch={this.props.branch}
           history={this.props.history}
           project={this.props.component.key}
-          metrics={this.state.metrics}
+          metrics={this.props.metrics}
         />
 
         {this.renderList(analyses)}
index bfffecd1f7cd8329c0cfefedc079bc75b3112a27..b84da6cb1f8676261db7872e805d68beabf2b7dc 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
+import * as PropTypes from 'prop-types';
 import MetaKey from './MetaKey';
 import MetaOrganizationKey from './MetaOrganizationKey';
 import MetaLinks from './MetaLinks';
@@ -28,90 +28,86 @@ import AnalysesList from '../events/AnalysesList';
 import MetaSize from './MetaSize';
 import MetaTags from './MetaTags';
 import BadgesModal from '../badges/BadgesModal';
-import { areThereCustomOrganizations, getGlobalSettingValue } from '../../../store/rootReducer';
-import { Visibility, Component } from '../../../app/types';
+import { Visibility, Component, Metric } from '../../../app/types';
 import { History } from '../../../api/time-machine';
 import { MeasureEnhanced } from '../../../helpers/measures';
 
-interface OwnProps {
+interface Props {
   branch?: string;
   component: Component;
   history?: History;
   measures: MeasureEnhanced[];
+  metrics: { [key: string]: Metric };
   onComponentChange: (changes: {}) => void;
 }
 
-interface StateToProps {
-  areThereCustomOrganizations: boolean;
-  onSonarCloud: boolean;
-}
-
-export function Meta(props: OwnProps & StateToProps) {
-  const { branch, component, areThereCustomOrganizations } = props;
-  const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;
-
-  const isProject = qualifier === 'TRK';
-  const isPrivate = visibility === Visibility.Private;
+export default class Meta extends React.PureComponent<Props> {
+  static contextTypes = {
+    onSonarCloud: PropTypes.bool,
+    organizationsEnabled: PropTypes.bool
+  };
 
-  const hasDescription = !!description;
-  const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
-  const hasQualityGate = !!qualityGate;
+  render() {
+    const { onSonarCloud, organizationsEnabled } = this.context;
+    const { branch, component, metrics } = this.props;
+    const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;
 
-  const shouldShowQualityProfiles = isProject && hasQualityProfiles;
-  const shouldShowQualityGate = isProject && hasQualityGate;
-  const hasOrganization = component.organization != null && areThereCustomOrganizations;
+    const isProject = qualifier === 'TRK';
+    const isPrivate = visibility === Visibility.Private;
 
-  return (
-    <div className="overview-meta">
-      {hasDescription && (
-        <div className="overview-meta-card overview-meta-description">{description}</div>
-      )}
+    const hasDescription = !!description;
+    const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
+    const hasQualityGate = !!qualityGate;
 
-      <MetaSize branch={branch} component={component} measures={props.measures} />
+    const shouldShowQualityProfiles = isProject && hasQualityProfiles;
+    const shouldShowQualityGate = isProject && hasQualityGate;
+    const hasOrganization = component.organization != null && organizationsEnabled;
 
-      {isProject && <MetaTags component={component} onComponentChange={props.onComponentChange} />}
+    return (
+      <div className="overview-meta">
+        {hasDescription && (
+          <div className="overview-meta-card overview-meta-description">{description}</div>
+        )}
 
-      <AnalysesList
-        branch={branch}
-        component={component}
-        qualifier={component.qualifier}
-        history={props.history}
-      />
+        <MetaSize branch={branch} component={component} measures={this.props.measures} />
 
-      {shouldShowQualityGate && (
-        <MetaQualityGate
-          gate={qualityGate}
-          organization={hasOrganization && component.organization}
-        />
-      )}
+        {isProject && (
+          <MetaTags component={component} onComponentChange={this.props.onComponentChange} />
+        )}
 
-      {shouldShowQualityProfiles && (
-        <MetaQualityProfiles
+        <AnalysesList
+          branch={branch}
           component={component}
-          customOrganizations={areThereCustomOrganizations}
-          profiles={qualityProfiles}
+          history={this.props.history}
+          metrics={metrics}
+          qualifier={component.qualifier}
         />
-      )}
-
-      {isProject && <MetaLinks component={component} />}
 
-      <MetaKey component={component} />
-
-      {hasOrganization && <MetaOrganizationKey component={component} />}
-
-      {props.onSonarCloud &&
-        isProject &&
-        !isPrivate && <BadgesModal branch={branch} project={component.key} />}
-    </div>
-  );
+        {shouldShowQualityGate && (
+          <MetaQualityGate
+            gate={qualityGate}
+            organization={hasOrganization && component.organization}
+          />
+        )}
+
+        {shouldShowQualityProfiles && (
+          <MetaQualityProfiles
+            component={component}
+            customOrganizations={organizationsEnabled}
+            profiles={qualityProfiles}
+          />
+        )}
+
+        {isProject && <MetaLinks component={component} />}
+
+        <MetaKey component={component} />
+
+        {hasOrganization && <MetaOrganizationKey component={component} />}
+
+        {onSonarCloud &&
+          isProject &&
+          !isPrivate && <BadgesModal branch={branch} metrics={metrics} project={component.key} />}
+      </div>
+    );
+  }
 }
-
-const mapStateToProps = (state: any): StateToProps => {
-  const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');
-  return {
-    areThereCustomOrganizations: areThereCustomOrganizations(state),
-    onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true')
-  };
-};
-
-export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(Meta);
index e433633238e2548666bbfb088e530316bfa75eeb..b90026b85241a16f0e45742d9aac5cb3d95ef287 100644 (file)
@@ -20,8 +20,7 @@
 import * as React from 'react';
 import { getDisplayedHistoryMetrics, DEFAULT_GRAPH } from '../../projectActivity/utils';
 import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
-import { getAllMetrics } from '../../../api/metrics';
-import { getAllTimeMachineData } from '../../../api/time-machine';
+import { getAllTimeMachineData, History } from '../../../api/time-machine';
 import { Metric } from '../../../app/types';
 import { parseDate } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
@@ -29,18 +28,14 @@ import { getCustomGraph, getGraph } from '../../../helpers/storage';
 
 const AnyPreviewGraph = PreviewGraph as any;
 
-interface History {
-  [metric: string]: Array<{ date: Date; value: string }>;
-}
-
 interface Props {
   component: string;
+  metrics: { [key: string]: Metric };
 }
 
 interface State {
   history?: History;
   loading: boolean;
-  metrics?: Metric[];
 }
 
 export default class Activity extends React.PureComponent<Props> {
@@ -71,8 +66,8 @@ export default class Activity extends React.PureComponent<Props> {
     }
 
     this.setState({ loading: true });
-    return Promise.all([getAllTimeMachineData(component, graphMetrics), getAllMetrics()]).then(
-      ([timeMachine, metrics]) => {
+    return getAllTimeMachineData(component, graphMetrics).then(
+      timeMachine => {
         if (this.mounted) {
           const history: History = {};
           timeMachine.measures.forEach(measure => {
@@ -82,7 +77,7 @@ export default class Activity extends React.PureComponent<Props> {
             }));
             history[measure.metric] = measureHistory;
           });
-          this.setState({ history, loading: false, metrics });
+          this.setState({ history, loading: false });
         }
       },
       () => {
@@ -103,11 +98,10 @@ export default class Activity extends React.PureComponent<Props> {
         {this.state.loading ? (
           <i className="spinner" />
         ) : (
-          this.state.metrics !== undefined &&
           this.state.history !== undefined && (
             <AnyPreviewGraph
               history={this.state.history}
-              metrics={this.state.metrics}
+              metrics={this.props.metrics}
               project={this.props.component}
               renderWhenEmpty={this.renderWhenEmpty}
             />
index f5450166b5d55387d84c9293103f8377b3243b52..84ba6bbbf0a917a92e6592e4b058933d4572039e 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { connect } from 'react-redux';
 import Summary from './Summary';
 import Report from './Report';
 import WorstProjects from './WorstProjects';
@@ -31,12 +32,25 @@ import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../u
 import { getMeasures } from '../../../api/measures';
 import { getChildren } from '../../../api/components';
 import { translate } from '../../../helpers/l10n';
+import { fetchMetrics } from '../../../store/rootActions';
+import { getMetrics } from '../../../store/rootReducer';
+import { Metric } from '../../../app/types';
 import '../styles.css';
 
-interface Props {
+interface OwnProps {
   component: { key: string; name: string };
 }
 
+interface StateToProps {
+  metrics: { [key: string]: Metric };
+}
+
+interface DispatchToProps {
+  fetchMetrics: () => void;
+}
+
+type Props = StateToProps & DispatchToProps & OwnProps;
+
 interface State {
   loading: boolean;
   measures?: { [key: string]: string | undefined };
@@ -44,12 +58,13 @@ interface State {
   totalSubComponents?: number;
 }
 
-export default class App extends React.PureComponent<Props, State> {
+export class App extends React.PureComponent<Props, State> {
   mounted: boolean;
   state: State = { loading: true };
 
   componentDidMount() {
     this.mounted = true;
+    this.props.fetchMetrics();
     this.fetchData();
   }
 
@@ -171,7 +186,7 @@ export default class App extends React.PureComponent<Props, State> {
           <aside className="page-sidebar-fixed">
             {!this.isEmpty() &&
               !this.isNotComputed() && <Summary component={component} measures={measures!} />}
-            <Activity component={component.key} />
+            <Activity component={component.key} metrics={this.props.metrics} />
             <Report component={component} />
           </aside>
         </div>
@@ -179,3 +194,13 @@ export default class App extends React.PureComponent<Props, State> {
     );
   }
 }
+
+const mapDispatchToProps: DispatchToProps = { fetchMetrics };
+
+const mapStateToProps = (state: any): StateToProps => ({
+  metrics: getMetrics(state)
+});
+
+export default connect<StateToProps, DispatchToProps, Props>(mapStateToProps, mapDispatchToProps)(
+  App
+);
index 8a9462dd6972b75c5db4807134b3c525704f2a3d..c8c2e81f295b497c788f61b6ecf7c3e0841aae52 100644 (file)
@@ -23,10 +23,6 @@ jest.mock('../../../../helpers/storage', () => ({
   getGraph: () => 'custom'
 }));
 
-jest.mock('../../../../api/metrics', () => ({
-  getAllMetrics: jest.fn(() => Promise.resolve([]))
-}));
-
 jest.mock('../../../../api/time-machine', () => ({
   getAllTimeMachineData: jest.fn(() =>
     Promise.resolve({
@@ -47,17 +43,15 @@ import * as React from 'react';
 import { mount, shallow } from 'enzyme';
 import Activity from '../Activity';
 
-const getAllMetrics = require('../../../../api/metrics').getAllMetrics as jest.Mock<any>;
 const getAllTimeMachineData = require('../../../../api/time-machine')
   .getAllTimeMachineData as jest.Mock<any>;
 
 beforeEach(() => {
-  getAllMetrics.mockClear();
   getAllTimeMachineData.mockClear();
 });
 
 it('renders', () => {
-  const wrapper = shallow(<Activity component="foo" />);
+  const wrapper = shallow(<Activity component="foo" metrics={{}} />);
   wrapper.setState({
     history: {
       coverage: [
@@ -72,7 +66,6 @@ it('renders', () => {
 });
 
 it('fetches history', () => {
-  mount(<Activity component="foo" />);
-  expect(getAllMetrics).toBeCalled();
+  mount(<Activity component="foo" metrics={{}} />);
   expect(getAllTimeMachineData).toBeCalledWith('foo', ['coverage']);
 });
index dc474149e9f99a45acd75433afa0a48c3aa0bd17..6cfe8d6890be29617f450c68d3b325ff478b726c 100644 (file)
@@ -43,7 +43,7 @@ jest.mock('../Report', () => ({
 
 import * as React from 'react';
 import { shallow, mount } from 'enzyme';
-import App from '../App';
+import { App } from '../App';
 
 const getMeasures = require('../../../../api/measures').getMeasures as jest.Mock<any>;
 const getChildren = require('../../../../api/components').getChildren as jest.Mock<any>;
@@ -51,7 +51,7 @@ const getChildren = require('../../../../api/components').getChildren as jest.Mo
 const component = { key: 'foo', name: 'Foo' };
 
 it('renders', () => {
-  const wrapper = shallow(<App component={component} />);
+  const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
   wrapper.setState({
     loading: false,
     measures: { ncloc: '173', reliability_rating: '1' },
@@ -62,13 +62,13 @@ it('renders', () => {
 });
 
 it('renders when portfolio is empty', () => {
-  const wrapper = shallow(<App component={component} />);
+  const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
   wrapper.setState({ loading: false, measures: { reliability_rating: '1' } });
   expect(wrapper).toMatchSnapshot();
 });
 
 it('renders when portfolio is not computed', () => {
-  const wrapper = shallow(<App component={component} />);
+  const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
   wrapper.setState({ loading: false, measures: { ncloc: '173' } });
   expect(wrapper).toMatchSnapshot();
 });
@@ -76,7 +76,7 @@ it('renders when portfolio is not computed', () => {
 it('fetches measures and children components', () => {
   getMeasures.mockClear();
   getChildren.mockClear();
-  mount(<App component={component} />);
+  mount(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
   expect(getMeasures).toBeCalledWith('foo', [
     'projects',
     'ncloc',
index 2a0038daecbf4e8207ea09a09fb383b37049720a..d48d3089a5d63e4d00cfdf4feddb5a24dc4077a6 100644 (file)
@@ -22,13 +22,7 @@ exports[`renders 1`] = `
         ],
       }
     }
-    metrics={
-      Array [
-        Object {
-          "key": "coverage",
-        },
-      ]
-    }
+    metrics={Object {}}
     project="foo"
     renderWhenEmpty={[Function]}
   />
index 2e26c3cc408c12e8fba384de13b228af00cf2070..76ccc5b4cb8fe7868d8847d38292c6aea7d40ae3 100644 (file)
@@ -77,6 +77,7 @@ exports[`renders 1`] = `
       />
       <Activity
         component="foo"
+        metrics={Object {}}
       />
       <Report
         component={
@@ -117,6 +118,7 @@ exports[`renders when portfolio is empty 1`] = `
     >
       <Activity
         component="foo"
+        metrics={Object {}}
       />
       <Report
         component={
@@ -154,6 +156,7 @@ exports[`renders when portfolio is not computed 1`] = `
     >
       <Activity
         component="foo"
+        metrics={Object {}}
       />
       <Report
         component={
index 0718ef5d03d1a404f16a79139746bbc93c2d3795..5a8ae9aa5a1326ceb2022728ba9bae96b9801777 100644 (file)
@@ -98,10 +98,17 @@ export const generateCoveredLinesMetric = (
   };
 };
 
+function findMetric(key /*: string */, metrics /*:  Array<Metric> | { [string]: Metric } */) {
+  if (Array.isArray(metrics)) {
+    return metrics.find(metric => metric.key === key);
+  }
+  return metrics[key];
+}
+
 export function generateSeries(
   measuresHistory /*: Array<MeasureHistory> */,
   graph /*: string */,
-  metrics /*: Array<Metric> */,
+  metrics /*:  Array<Metric> | { [string]: Metric } */,
   displayedMetrics /*: Array<string> */
 ) /*: Array<Serie> */ {
   if (displayedMetrics.length <= 0) {
@@ -114,7 +121,7 @@ export function generateSeries(
         if (measure.metric === 'uncovered_lines' && !isCustomGraph(graph)) {
           return generateCoveredLinesMetric(measure, measuresHistory);
         }
-        const metric = metrics.find(metric => metric.key === measure.metric);
+        const metric = findMetric(measure.metric, metrics);
         return {
           data: measure.history.map(analysis => ({
             x: analysis.date,
index 744eed0493d36956fe9cbd9e1c5a81d7790baf7a..61de9d4f695c97ec33e2f2b4fd0be37116328379 100644 (file)
@@ -24,7 +24,7 @@ import { Metric } from '../../app/types';
 interface Props {
   branch?: string;
   history?: History;
-  metrics: Metric[];
+  metrics: { [key: string]: Metric };
   project: string;
   renderWhenEmpty?: () => void;
 }
index 9483414c1a8a8a8cebc03c9eeabf9a565f203ec6..8a8ac8443c52ff3843116a8e8e696de6509fcb4c 100644 (file)
@@ -41,7 +41,7 @@ import { formatMeasure, getShortType } from '../../helpers/measures';
 type Props = {
   branch?: string,
   history: ?History,
-  metrics: Array<Metric>,
+  metrics: { [string]: Metric },
   project: string,
   renderWhenEmpty?: () => void
 };
@@ -121,7 +121,7 @@ export default class PreviewGraph extends React.PureComponent {
     history /*: ?History */,
     graph /*: string */,
     customMetrics /*: Array<string> */,
-    metrics /*: Array<Metric> */
+    metrics /*: { [string]: Metric } */
   ) => {
     const myHistory = history;
     if (!myHistory) {