]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11165 Migrate rest of component measures page to TS
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 20 Aug 2018 15:28:52 +0000 (17:28 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Sep 2018 08:51:41 +0000 (10:51 +0200)
89 files changed:
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/measures.ts
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/styles/components/modals.css
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/component-measures/components/App.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/PageActions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts
server/sonar-web/src/main/js/apps/component-measures/config/domains.ts
server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/types.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/utils.ts
server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.tsx
server/sonar-web/src/main/js/apps/overview/main/enhance.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
server/sonar-web/src/main/js/components/charts/BubbleChart.tsx
server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap
server/sonar-web/src/main/js/components/measure/utils.ts
server/sonar-web/src/main/js/helpers/path.ts
server/sonar-web/src/main/js/helpers/periods.ts

index 39805e256497deb37aac7c91f92f0ffd262278f0..df793ee8a874b0d8fb1fbd79503c887fb738e3f8 100644 (file)
@@ -114,14 +114,14 @@ export function getComponentTree(
     metricKeys: metrics.join(','),
     strategy
   });
-  return getJSON(url, data);
+  return getJSON(url, data).catch(throwGlobalError);
 }
 
 export function getChildren(
   componentKey: string,
   metrics: string[] = [],
   additional: RequestData = {}
-): Promise<any> {
+) {
   return getComponentTree('children', componentKey, metrics, additional);
 }
 
@@ -129,14 +129,14 @@ export function getComponentLeaves(
   componentKey: string,
   metrics: string[] = [],
   additional: RequestData = {}
-): Promise<any> {
+) {
   return getComponentTree('leaves', componentKey, metrics, additional);
 }
 
 export function getComponent(
   data: { componentKey: string; metricKeys: string } & BranchParameters
 ): Promise<any> {
-  return getJSON('/api/measures/component', data).then(r => r.component);
+  return getJSON('/api/measures/component', data).then(r => r.component, throwGlobalError);
 }
 
 export interface TreeComponent extends LightComponent {
@@ -165,7 +165,7 @@ export function getTree(data: {
 }
 
 export function getComponentShow(data: { component: string } & BranchParameters): Promise<any> {
-  return getJSON('/api/components/show', data);
+  return getJSON('/api/components/show', data).catch(throwGlobalError);
 }
 
 export function getParents(component: string): Promise<any> {
index 1102c39870c20fa8016b29affb6b3628460169eb..378c5079242efa825cf5d7b5275fd74dd11482e0 100644 (file)
 import { getJSON, RequestData, postJSON, post } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
 import {
-  Metric,
   CustomMeasure,
-  Paging,
   BranchParameters,
   Measure,
-  MeasurePeriod
+  Metric,
+  Paging,
+  Period,
+  PeriodMeasure
 } from '../app/types';
-import { Period } from '../helpers/periods';
 
 export function getMeasures(
   data: { componentKey: string; metricKeys: string } & BranchParameters
@@ -55,7 +55,7 @@ export function getMeasuresAndMeta(
 interface MeasuresForProjects {
   component: string;
   metric: string;
-  periods?: MeasurePeriod[];
+  periods?: PeriodMeasure[];
   value?: string;
 }
 
index 31983bc39135cbb19733bf050dff86980fe57aaf..267ec368cb187a24ccb8fb8493bfd9b5eed778f6 100644 (file)
@@ -152,12 +152,6 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   }
 
   renderComponentMeasuresLink() {
-    const { branchLike } = this.props;
-
-    if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) {
-      return null;
-    }
-
     return (
       <li>
         <Link
index 0b2bf02dab9b3c731cf71b422fed5a2e2a165991..71afa0c4716a68a8e648c6e9b0c71c917debcd08 100644 (file)
@@ -1097,6 +1097,24 @@ exports[`should work for short-living branches 1`] = `
       issues.page
     </Link>
   </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/component_measures",
+          "query": Object {
+            "branch": "feature",
+            "id": "foo",
+          },
+        }
+      }
+    >
+      layout.measures
+    </Link>
+  </li>
   <li>
     <Link
       activeClassName="active"
index a89dbc9a62102876a34402e851c4e1b2ac3f8ee7..dd8be6880942039ad745195deef84781bc9efeaf 100644 (file)
 }
 
 .modal-foot {
-  line-height: var(--controlHeight);
   padding: 10px;
   border-top: 1px solid #ccc;
   background-color: var(--gray94);
index 237d4d6e981e9b8b1b6aa9d1c02de09dc673b6c5..954eecdd305c37104f46b75abe29fdf5fa62ccaa 100644 (file)
@@ -107,12 +107,14 @@ export interface ComponentQualityProfile {
 }
 
 interface ComponentMeasureIntern {
+  branch?: string;
   isFavorite?: boolean;
   isRecentlyBrowsed?: boolean;
   key: string;
   match?: string;
   name: string;
   organization?: string;
+  path?: string;
   project?: string;
   qualifier: string;
   refKey?: string;
@@ -376,18 +378,6 @@ export interface MainBranch extends Branch {
   status?: { qualityGateStatus: string };
 }
 
-export interface MeasurePeriod {
-  bestValue?: boolean;
-  index: number;
-  value: string;
-}
-
-interface MeasureIntern {
-  bestValue?: boolean;
-  periods?: MeasurePeriod[];
-  value?: string;
-}
-
 export interface Measure extends MeasureIntern {
   metric: string;
 }
@@ -397,6 +387,12 @@ export interface MeasureEnhanced extends MeasureIntern {
   leak?: string;
 }
 
+interface MeasureIntern {
+  bestValue?: boolean;
+  periods?: PeriodMeasure[];
+  value?: string;
+}
+
 export interface Metric {
   bestValue?: string;
   custom?: boolean;
@@ -478,6 +474,28 @@ export interface Paging {
   total: number;
 }
 
+export interface Period {
+  date: string;
+  index: number;
+  mode: PeriodMode;
+  modeParam?: string;
+  parameter?: string;
+}
+
+export interface PeriodMeasure {
+  bestValue?: boolean;
+  index: number;
+  value: string;
+}
+
+export enum PeriodMode {
+  Days = 'days',
+  Date = 'date',
+  Version = 'version',
+  PreviousAnalysis = 'previous_analysis',
+  PreviousVersion = 'previous_version'
+}
+
 export interface PermissionTemplate {
   defaultFor: string[];
   id: string;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js
deleted file mode 100644 (file)
index f866fd0..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import Helmet from 'react-helmet';
-import key from 'keymaster';
-import MeasureContentContainer from './MeasureContentContainer';
-import MeasureOverviewContainer from './MeasureOverviewContainer';
-import Sidebar from '../sidebar/Sidebar';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery } from '../utils';
-import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import {
-  getLocalizedMetricDomain,
-  translateWithParameters,
-  translate
-} from '../../../helpers/l10n';
-import { getDisplayMetrics } from '../../../helpers/measures';
-/*:: import type { Component, Query, Period } from '../types'; */
-/*:: import type { RawQuery } from '../../../helpers/query'; */
-/*:: import type { Metric } from '../../../app/flow-types'; */
-/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
-import '../../../components/search-navigator.css';
-import '../style.css';
-
-/*:: type Props = {|
-  branchLike?: { id?: string; name: string },
-  component: Component,
-  currentUser: { isLoggedIn: boolean },
-  location: { pathname: string, query: RawQuery },
-  fetchMeasures: (
-    component: string,
-    metricsKey: Array<string>,
-    branchLike?: { id?: string; name: string }
-  ) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }>,
-  fetchMetrics: () => void,
-  metrics: { [string]: Metric },
-  metricsKey: Array<string>,
-  router: {
-    push: ({ pathname: string, query?: RawQuery }) => void
-  }
-|}; */
-
-/*:: type State = {|
-  loading: boolean,
-  measures: Array<MeasureEnhanced>,
-  leakPeriod: ?Period
-|}; */
-
-export default class App extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      loading: true,
-      measures: [],
-      leakPeriod: null
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    // $FlowFixMe
-    document.body.classList.add('white-page');
-    // $FlowFixMe
-    document.documentElement.classList.add('white-page');
-    this.props.fetchMetrics();
-    this.fetchMeasures(this.props);
-    key.setScope('measures-files');
-    const footer = document.getElementById('footer');
-    if (footer) {
-      footer.classList.add('page-footer-with-sidebar');
-    }
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    if (
-      !isSameBranchLike(nextProps.branchLike, this.props.branchLike) ||
-      nextProps.component.key !== this.props.component.key ||
-      nextProps.metrics !== this.props.metrics
-    ) {
-      this.fetchMeasures(nextProps);
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-    // $FlowFixMe
-    document.body.classList.remove('white-page');
-    // $FlowFixMe
-    document.documentElement.classList.remove('white-page');
-    key.deleteScope('measures-files');
-    const footer = document.getElementById('footer');
-    if (footer) {
-      footer.classList.remove('page-footer-with-sidebar');
-    }
-  }
-
-  fetchMeasures = ({ branchLike, component, fetchMeasures, metrics } /*: Props */) => {
-    this.setState({ loading: true });
-    const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key);
-    fetchMeasures(component.key, filteredKeys, branchLike).then(
-      ({ measures, leakPeriod }) => {
-        if (this.mounted) {
-          this.setState({
-            loading: false,
-            leakPeriod,
-            measures: measures.filter(measure => measure.value != null || measure.leak != null)
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  updateQuery = (newQuery /*: Query */) => {
-    const query = serializeQuery({
-      ...parseQuery(this.props.location.query),
-      ...newQuery
-    });
-    this.props.router.push({
-      pathname: this.props.location.pathname,
-      query: {
-        ...query,
-        ...getBranchLikeQuery(this.props.branchLike),
-        id: this.props.component.key
-      }
-    });
-  };
-
-  getHelmetTitle = (
-    metric /*: Metric */,
-    query /*: {metric: string, selected: string, view: string }*/
-  ) => {
-    if (metric == null && hasBubbleChart(query.metric)) {
-      return isProjectOverview(query.metric)
-        ? translate('component_measures.overview.project_overview.facet')
-        : translateWithParameters(
-            'component_measures.domain_x_overview',
-            getLocalizedMetricDomain(query.metric)
-          );
-    }
-    return metric != null ? metric.name : translate('layout.measures');
-  };
-
-  render() {
-    const isLoading = this.state.loading || this.props.metricsKey.length <= 0;
-    if (isLoading) {
-      return <i className="spinner spinner-margin" />;
-    }
-    const { branchLike, component, fetchMeasures, metrics } = this.props;
-    const { leakPeriod } = this.state;
-    const query = parseQuery(this.props.location.query);
-    const metric = metrics[query.metric];
-    return (
-      <div className="layout-page" id="component-measures">
-        <Suggestions suggestions="component_measures" />
-        <Helmet title={this.getHelmetTitle(metric, query)} />
-
-        <ScreenPositionHelper className="layout-page-side-outer">
-          {({ top }) => (
-            <div className="layout-page-side" style={{ top }}>
-              <div className="layout-page-side-inner">
-                <div className="layout-page-filters">
-                  <Sidebar
-                    measures={this.state.measures}
-                    selectedMetric={query.metric}
-                    updateQuery={this.updateQuery}
-                  />
-                </div>
-              </div>
-            </div>
-          )}
-        </ScreenPositionHelper>
-
-        {metric != null && (
-          <MeasureContentContainer
-            branchLike={branchLike}
-            className="layout-page-main"
-            currentUser={this.props.currentUser}
-            fetchMeasures={fetchMeasures}
-            leakPeriod={leakPeriod}
-            metric={metric}
-            metrics={metrics}
-            rootComponent={component}
-            router={this.props.router}
-            selected={query.selected}
-            updateQuery={this.updateQuery}
-            view={query.view}
-          />
-        )}
-        {metric == null &&
-          hasBubbleChart(query.metric) && (
-            <MeasureOverviewContainer
-              branchLike={branchLike}
-              className="layout-page-main"
-              currentUser={this.props.currentUser}
-              domain={query.metric}
-              leakPeriod={leakPeriod}
-              metrics={metrics}
-              rootComponent={component}
-              router={this.props.router}
-              selected={query.selected}
-              updateQuery={this.updateQuery}
-            />
-          )}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx
new file mode 100644 (file)
index 0000000..ff78d3a
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import * as key from 'keymaster';
+import { InjectedRouter } from 'react-router';
+import Helmet from 'react-helmet';
+import MeasureContentContainer from './MeasureContentContainer';
+import MeasureOverviewContainer from './MeasureOverviewContainer';
+import Sidebar from '../sidebar/Sidebar';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery, Query } from '../utils';
+import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import {
+  getLocalizedMetricDomain,
+  translateWithParameters,
+  translate
+} from '../../../helpers/l10n';
+import { getDisplayMetrics } from '../../../helpers/measures';
+import { RawQuery } from '../../../helpers/query';
+import {
+  BranchLike,
+  ComponentMeasure,
+  MeasureEnhanced,
+  Metric,
+  CurrentUser,
+  Period
+} from '../../../app/types';
+import '../../../components/search-navigator.css';
+import '../style.css';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: ComponentMeasure;
+  currentUser: CurrentUser;
+  location: { pathname: string; query: RawQuery };
+  fetchMeasures: (
+    component: string,
+    metricsKey: string[],
+    branchLike?: BranchLike
+  ) => Promise<{ component: ComponentMeasure; measures: MeasureEnhanced[]; leakPeriod?: Period }>;
+  fetchMetrics: () => void;
+  metrics: { [metric: string]: Metric };
+  metricsKey: string[];
+  router: InjectedRouter;
+}
+
+interface State {
+  loading: boolean;
+  measures: MeasureEnhanced[];
+  leakPeriod?: Period;
+}
+
+export default class App extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { loading: true, measures: [] };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+
+    document.body.classList.add('white-page');
+    document.documentElement.classList.add('white-page');
+    const footer = document.getElementById('footer');
+    if (footer) {
+      footer.classList.add('page-footer-with-sidebar');
+    }
+
+    key.setScope('measures-files');
+    this.props.fetchMetrics();
+    this.fetchMeasures(this.props);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (
+      !isSameBranchLike(nextProps.branchLike, this.props.branchLike) ||
+      nextProps.component.key !== this.props.component.key ||
+      nextProps.metrics !== this.props.metrics
+    ) {
+      this.fetchMeasures(nextProps);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+
+    document.body.classList.remove('white-page');
+    document.documentElement.classList.remove('white-page');
+
+    const footer = document.getElementById('footer');
+    if (footer) {
+      footer.classList.remove('page-footer-with-sidebar');
+    }
+
+    key.deleteScope('measures-files');
+  }
+
+  fetchMeasures = ({ branchLike, component, fetchMeasures, metrics }: Props) => {
+    this.setState({ loading: true });
+    const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key);
+
+    fetchMeasures(component.key, filteredKeys, branchLike).then(
+      ({ measures, leakPeriod }) => {
+        if (this.mounted) {
+          this.setState({
+            loading: false,
+            leakPeriod,
+            measures: measures.filter(
+              measure => measure.value !== undefined || measure.leak !== undefined
+            )
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  updateQuery = (newQuery: Partial<Query>) => {
+    const query = serializeQuery({
+      ...parseQuery(this.props.location.query),
+      ...newQuery
+    });
+    this.props.router.push({
+      pathname: this.props.location.pathname,
+      query: {
+        ...query,
+        ...getBranchLikeQuery(this.props.branchLike),
+        id: this.props.component.key
+      }
+    });
+  };
+
+  getHelmetTitle = (metric?: Metric) => {
+    if (metric && hasBubbleChart(metric.key)) {
+      return isProjectOverview(metric.key)
+        ? translate('component_measures.overview.project_overview.facet')
+        : translateWithParameters(
+            'component_measures.domain_x_overview',
+            getLocalizedMetricDomain(metric.key)
+          );
+    }
+    return metric ? metric.name : translate('layout.measures');
+  };
+
+  render() {
+    const isLoading = this.state.loading || this.props.metricsKey.length <= 0;
+    if (isLoading) {
+      return <i className="spinner spinner-margin" />;
+    }
+    const { branchLike, component, fetchMeasures, metrics } = this.props;
+    const { leakPeriod } = this.state;
+    const query = parseQuery(this.props.location.query);
+    const metric = metrics[query.metric];
+    return (
+      <div className="layout-page" id="component-measures">
+        <Suggestions suggestions="component_measures" />
+        <Helmet title={this.getHelmetTitle(metric)} />
+
+        <ScreenPositionHelper className="layout-page-side-outer">
+          {({ top }) => (
+            <div className="layout-page-side" style={{ top }}>
+              <div className="layout-page-side-inner">
+                <div className="layout-page-filters">
+                  <Sidebar
+                    measures={this.state.measures}
+                    selectedMetric={query.metric}
+                    updateQuery={this.updateQuery}
+                  />
+                </div>
+              </div>
+            </div>
+          )}
+        </ScreenPositionHelper>
+
+        {metric && (
+          <MeasureContentContainer
+            branchLike={branchLike}
+            className="layout-page-main"
+            currentUser={this.props.currentUser}
+            fetchMeasures={fetchMeasures}
+            leakPeriod={leakPeriod}
+            metric={metric}
+            metrics={metrics}
+            rootComponent={component}
+            router={this.props.router}
+            selected={query.selected}
+            updateQuery={this.updateQuery}
+            view={query.view}
+          />
+        )}
+        {!metric &&
+          hasBubbleChart(query.metric) && (
+            <MeasureOverviewContainer
+              branchLike={branchLike}
+              className="layout-page-main"
+              currentUser={this.props.currentUser}
+              domain={query.metric}
+              leakPeriod={leakPeriod}
+              metrics={metrics}
+              rootComponent={component}
+              router={this.props.router}
+              selected={query.selected}
+              updateQuery={this.updateQuery}
+            />
+          )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js
deleted file mode 100644 (file)
index b02d379..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router';
-import App from './App';
-import throwGlobalError from '../../../app/utils/throwGlobalError';
-import { getCurrentUser, getMetrics, getMetricsKey } from '../../../store/rootReducer';
-import { fetchMetrics } from '../../../store/rootActions';
-import { getMeasuresAndMeta } from '../../../api/measures';
-import { getLeakPeriod } from '../../../helpers/periods';
-import { enhanceMeasure } from '../../../components/measure/utils';
-import { getBranchLikeQuery } from '../../../helpers/branches';
-/*:: import type { Component, Period } from '../types'; */
-/*:: import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; */
-
-const mapStateToProps = state => ({
-  currentUser: getCurrentUser(state),
-  metrics: getMetrics(state),
-  metricsKey: getMetricsKey(state)
-});
-
-function banQualityGate(component /*: Component */) /*: Array<Measure> */ {
-  const bannedMetrics = [];
-  if (!['VW', 'SVW'].includes(component.qualifier)) {
-    bannedMetrics.push('alert_status');
-  }
-  if (component.qualifier === 'APP') {
-    bannedMetrics.push('releasability_rating', 'releasability_effort');
-  }
-  return component.measures.filter(measure => !bannedMetrics.includes(measure.metric));
-}
-
-const fetchMeasures = (
-  component /*: string */,
-  metricsKey /*: Array<string> */,
-  branchLike /*: { id?: string; name: string } | void */
-) => (dispatch, getState) => {
-  if (metricsKey.length <= 0) {
-    return Promise.resolve({ component: {}, measures: [], leakPeriod: null });
-  }
-
-  return getMeasuresAndMeta(component, metricsKey, {
-    additionalFields: 'periods',
-    ...getBranchLikeQuery(branchLike)
-  }).then(r => {
-    const measures = banQualityGate(r.component).map(measure =>
-      enhanceMeasure(measure, getMetrics(getState()))
-    );
-
-    const newBugs = measures.find(measure => measure.metric.key === 'new_bugs');
-    const applicationPeriods = newBugs ? [{ index: 1 }] : [];
-    const periods = r.component.qualifier === 'APP' ? applicationPeriods : r.periods;
-    return { component: r.component, measures, leakPeriod: getLeakPeriod(periods) };
-  }, throwGlobalError);
-};
-
-const mapDispatchToProps = { fetchMeasures, fetchMetrics };
-
-export default connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(withRouter(App));
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.tsx
new file mode 100644 (file)
index 0000000..86c347c
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { withRouter, WithRouterProps } from 'react-router';
+import App from './App';
+import throwGlobalError from '../../../app/utils/throwGlobalError';
+import { getCurrentUser, getMetrics, getMetricsKey } from '../../../store/rootReducer';
+import { fetchMetrics } from '../../../store/rootActions';
+import { getMeasuresAndMeta } from '../../../api/measures';
+import { getLeakPeriod } from '../../../helpers/periods';
+import { enhanceMeasure } from '../../../components/measure/utils';
+import { getBranchLikeQuery } from '../../../helpers/branches';
+import {
+  BranchLike,
+  ComponentMeasure,
+  CurrentUser,
+  Measure,
+  MeasureEnhanced,
+  Metric,
+  Period
+} from '../../../app/types';
+
+interface StateToProps {
+  currentUser: CurrentUser;
+  metrics: { [metric: string]: Metric };
+  metricsKey: string[];
+}
+
+interface DispatchToProps {
+  fetchMeasures: (
+    component: string,
+    metricsKey: string[],
+    branchLike?: BranchLike
+  ) => Promise<{ component: ComponentMeasure; measures: MeasureEnhanced[]; leakPeriod?: Period }>;
+  fetchMetrics: () => void;
+}
+
+interface OwnProps {
+  branchLike?: BranchLike;
+  component: ComponentMeasure;
+}
+
+const mapStateToProps = (state: any): StateToProps => ({
+  currentUser: getCurrentUser(state),
+  metrics: getMetrics(state),
+  metricsKey: getMetricsKey(state)
+});
+
+function banQualityGate({ measures = [], qualifier }: ComponentMeasure): Measure[] {
+  const bannedMetrics: string[] = [];
+  if (!['VW', 'SVW'].includes(qualifier)) {
+    bannedMetrics.push('alert_status');
+  }
+  if (qualifier === 'APP') {
+    bannedMetrics.push('releasability_rating', 'releasability_effort');
+  }
+  return measures.filter(measure => !bannedMetrics.includes(measure.metric));
+}
+
+const fetchMeasures = (component: string, metricsKey: string[], branchLike?: BranchLike) => (
+  _dispatch: Dispatch<any>,
+  getState: () => any
+) => {
+  if (metricsKey.length <= 0) {
+    return Promise.resolve({ component: {}, measures: [], leakPeriod: null });
+  }
+
+  return getMeasuresAndMeta(component, metricsKey, {
+    additionalFields: 'periods',
+    ...getBranchLikeQuery(branchLike)
+  }).then(({ component, periods }) => {
+    const measures = banQualityGate(component).map(measure =>
+      enhanceMeasure(measure, getMetrics(getState()))
+    );
+
+    const newBugs = measures.find(measure => measure.metric.key === 'new_bugs');
+    const applicationPeriods = newBugs ? [{ index: 1 } as Period] : [];
+    const leakPeriod = getLeakPeriod(component.qualifier === 'APP' ? applicationPeriods : periods);
+    return { component, measures, leakPeriod };
+  }, throwGlobalError);
+};
+
+const mapDispatchToProps: DispatchToProps = { fetchMeasures: fetchMeasures as any, fetchMetrics };
+
+export default withRouter<OwnProps>(
+  connect<StateToProps, DispatchToProps, OwnProps & WithRouterProps>(
+    mapStateToProps,
+    mapDispatchToProps
+  )(App)
+);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js b/server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js
deleted file mode 100644 (file)
index 94ab8b0..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
-
-/*:: type Props = {
-  className?: string,
-  current: ?number,
-  total: number
-}; */
-
-export default function FilesCounter({ className, current, total } /*: Props */) {
-  return (
-    <span className={className}>
-      <strong>
-        {current != null && (
-          <span>
-            {formatMeasure(current, 'INT')}
-            {' / '}
-          </span>
-        )}
-        {formatMeasure(total, 'INT')}
-      </strong>{' '}
-      {translate('component_measures.files')}
-    </span>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.tsx
new file mode 100644 (file)
index 0000000..37e794a
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+
+interface Props {
+  className?: string;
+  current?: number;
+  total: number;
+}
+
+export default function FilesCounter({ className, current, total }: Props) {
+  return (
+    <span className={className}>
+      <strong>
+        {current !== undefined && (
+          <span>
+            {formatMeasure(current, 'INT')}
+            {' / '}
+          </span>
+        )}
+        {formatMeasure(total, 'INT')}
+      </strong>{' '}
+      {translate('component_measures.files')}
+    </span>
+  );
+}
index 8c61f4e87b71e40909998f2178032caa327b422a..f1d8747a2c8e12b1da5443c4dc73eec731c14617 100644 (file)
@@ -24,10 +24,10 @@ import DateFromNow from '../../../components/intl/DateFromNow';
 import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import Tooltip from '../../../components/controls/Tooltip';
-import { getPeriodLabel, getPeriodDate, Period, PeriodMode } from '../../../helpers/periods';
+import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { differenceInDays } from '../../../helpers/dates';
-import { ComponentMeasure } from '../../../app/types';
+import { ComponentMeasure, Period, PeriodMode } from '../../../app/types';
 
 interface Props {
   className?: string;
index 9472307bc7edaa8c7c7e5a91abeb8ab7371c5fb0..7eee54217b6bf98af191cf56b1506d3818b3526c 100644 (file)
@@ -44,10 +44,10 @@ import {
   isLoggedIn,
   Metric,
   Paging,
-  MeasureEnhanced
+  MeasureEnhanced,
+  Period
 } from '../../../app/types';
 import { RequestData } from '../../../helpers/request';
-import { Period } from '../../../helpers/periods';
 
 interface Props {
   branchLike?: BranchLike;
@@ -328,10 +328,8 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
             <MeasureHeader
               branchLike={branchLike}
               component={component}
-              components={this.state.components}
               leakPeriod={this.props.leakPeriod}
-              // fall back to `undefined` to be compatible with typescript files where we compare with `=== undefined`
-              measure={measure || undefined}
+              measure={measure}
               metric={metric}
               secondaryMeasure={this.props.secondaryMeasure}
             />
index 3c3a636538feb92ad2f71c81f2a7191fa8f8a726..c8a1301038dbcbc3a5a02cead672ce5ed6e4e4ae 100644 (file)
@@ -26,9 +26,9 @@ import {
   Metric,
   BranchLike,
   CurrentUser,
-  MeasureEnhanced
+  MeasureEnhanced,
+  Period
 } from '../../../app/types';
-import { Period } from '../../../helpers/periods';
 
 interface Props {
   branchLike?: BranchLike;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
deleted file mode 100644 (file)
index c39214f..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { Link } from 'react-router';
-import LeakPeriodLegend from './LeakPeriodLegend';
-import HistoryIcon from '../../../components/icons-components/HistoryIcon';
-import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
-import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
-import Measure from '../../../components/measure/Measure';
-import Tooltip from '../../../components/controls/Tooltip';
-import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import { getMeasureHistoryUrl } from '../../../helpers/urls';
-import { isDiffMetric } from '../../../helpers/measures';
-/*:: import type { Component, Period } from '../types'; */
-/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
-/*:: import type { Metric } from '../../../app/flow-types'; */
-
-/*:: type Props = {|
-  branchLike?: { id?: string; name: string },
-  component: Component,
-  components: Array<Component>,
-  leakPeriod?: Period,
-  measure?: MeasureEnhanced,
-  metric: Metric,
-  secondaryMeasure: ?MeasureEnhanced
-|}; */
-
-export default function MeasureHeader(props /*: Props*/) {
-  const { branchLike, component, leakPeriod, measure, metric, secondaryMeasure } = props;
-  const isDiff = isDiffMetric(metric.key);
-  const hasHistory = component.qualifier !== 'FIL' && component.qualifier !== 'UTS';
-  return (
-    <div className="measure-details-header big-spacer-bottom">
-      <div className="measure-details-primary">
-        <div className="measure-details-metric">
-          <IssueTypeIcon className="little-spacer-right text-text-bottom" query={metric.key} />
-          {getLocalizedMetricName(metric)}
-          <span className="measure-details-value spacer-left">
-            <strong>
-              {isDiff ? (
-                <Measure
-                  className="domain-measures-leak"
-                  metricKey={metric.key}
-                  metricType={metric.type}
-                  value={measure && measure.leak}
-                />
-              ) : (
-                <Measure
-                  metricKey={metric.key}
-                  metricType={metric.type}
-                  value={measure && measure.value}
-                />
-              )}
-            </strong>
-          </span>
-          {!isDiff &&
-            hasHistory && (
-              <Tooltip overlay={translate('component_measures.show_metric_history')}>
-                <Link
-                  className="js-show-history spacer-left button button-small"
-                  to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}>
-                  <HistoryIcon />
-                </Link>
-              </Tooltip>
-            )}
-        </div>
-        <div className="measure-details-primary-actions">
-          {leakPeriod != null && (
-            <LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
-          )}
-        </div>
-      </div>
-      {secondaryMeasure &&
-        secondaryMeasure.metric.key === 'ncloc_language_distribution' && (
-          <div className="measure-details-secondary">
-            <LanguageDistributionContainer
-              alignTicks={true}
-              distribution={secondaryMeasure.value}
-              width={260}
-            />
-          </div>
-        )}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
new file mode 100644 (file)
index 0000000..61840e1
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import LeakPeriodLegend from './LeakPeriodLegend';
+import HistoryIcon from '../../../components/icons-components/HistoryIcon';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
+import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
+import Measure from '../../../components/measure/Measure';
+import Tooltip from '../../../components/controls/Tooltip';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { getMeasureHistoryUrl } from '../../../helpers/urls';
+import { isDiffMetric } from '../../../helpers/measures';
+import { MeasureEnhanced, Metric, ComponentMeasure, BranchLike, Period } from '../../../app/types';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: ComponentMeasure;
+  leakPeriod?: Period;
+  measure?: MeasureEnhanced;
+  metric: Metric;
+  secondaryMeasure?: MeasureEnhanced;
+}
+
+export default function MeasureHeader(props: Props) {
+  const { branchLike, component, leakPeriod, measure, metric, secondaryMeasure } = props;
+  const isDiff = isDiffMetric(metric.key);
+  const hasHistory = component.qualifier !== 'FIL' && component.qualifier !== 'UTS';
+  return (
+    <div className="measure-details-header big-spacer-bottom">
+      <div className="measure-details-primary">
+        <div className="measure-details-metric">
+          <IssueTypeIcon className="little-spacer-right text-text-bottom" query={metric.key} />
+          {getLocalizedMetricName(metric)}
+          <span className="measure-details-value spacer-left">
+            <strong>
+              {isDiff ? (
+                <Measure
+                  className="domain-measures-leak"
+                  metricKey={metric.key}
+                  metricType={metric.type}
+                  value={measure && measure.leak}
+                />
+              ) : (
+                <Measure
+                  metricKey={metric.key}
+                  metricType={metric.type}
+                  value={measure && measure.value}
+                />
+              )}
+            </strong>
+          </span>
+          {!isDiff &&
+            hasHistory && (
+              <Tooltip overlay={translate('component_measures.show_metric_history')}>
+                <Link
+                  className="js-show-history spacer-left button button-small"
+                  to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}>
+                  <HistoryIcon />
+                </Link>
+              </Tooltip>
+            )}
+        </div>
+        <div className="measure-details-primary-actions">
+          {leakPeriod && (
+            <LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
+          )}
+        </div>
+      </div>
+      {secondaryMeasure &&
+        secondaryMeasure.metric.key === 'ncloc_language_distribution' &&
+        secondaryMeasure.value !== undefined && (
+          <div className="measure-details-secondary">
+            <LanguageDistributionContainer
+              alignTicks={true}
+              distribution={secondaryMeasure.value}
+              width={260}
+            />
+          </div>
+        )}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js
deleted file mode 100644 (file)
index 495626e..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import Breadcrumbs from './Breadcrumbs';
-import LeakPeriodLegend from './LeakPeriodLegend';
-import MeasureFavoriteContainer from './MeasureFavoriteContainer';
-import PageActions from './PageActions';
-import BubbleChart from '../drilldown/BubbleChart';
-import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import { getComponentLeaves } from '../../../api/components';
-import { enhanceComponent, getBubbleMetrics, isFileType } from '../utils';
-import { getBranchLikeQuery } from '../../../helpers/branches';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-/*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */
-/*:: import type { Metric } from '../../../app/flow-types'; */
-
-/*:: type Props = {|
-  branchLike?: { id?: string; name: string },
-  className?: string,
-  component: Component,
-  currentUser: { isLoggedIn: boolean },
-  domain: string,
-  leakPeriod: Period,
-  loading: boolean,
-  metrics: { [string]: Metric },
-  rootComponent: Component,
-  updateLoading: ({ [string]: boolean }) => void,
-  updateSelected: string => void
-|}; */
-
-/*:: type State = {
-  components: Array<ComponentEnhanced>,
-  paging?: Paging
-}; */
-
-const BUBBLES_LIMIT = 500;
-
-export default class MeasureOverview extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  state /*: State */ = {
-    components: [],
-    paging: null
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchComponents(this.props);
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    if (
-      nextProps.component !== this.props.component ||
-      nextProps.metrics !== this.props.metrics ||
-      nextProps.domain !== this.props.domain
-    ) {
-      this.fetchComponents(nextProps);
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchComponents = (props /*: Props */) => {
-    const { branchLike, component, domain, metrics } = props;
-    if (isFileType(component)) {
-      this.setState({ components: [], paging: null });
-      return;
-    }
-    const { x, y, size, colors } = getBubbleMetrics(domain, metrics);
-    const metricsKey = [x.key, y.key, size.key];
-    if (colors) {
-      metricsKey.push(colors.map(metric => metric.key));
-    }
-    const options = {
-      ...getBranchLikeQuery(branchLike),
-      s: 'metric',
-      metricSort: size.key,
-      asc: false,
-      ps: BUBBLES_LIMIT
-    };
-
-    this.props.updateLoading({ bubbles: true });
-    getComponentLeaves(component.key, metricsKey, options).then(
-      r => {
-        if (domain === this.props.domain) {
-          if (this.mounted) {
-            this.setState({
-              components: r.components.map(component => enhanceComponent(component, null, metrics)),
-              paging: r.paging
-            });
-          }
-          this.props.updateLoading({ bubbles: false });
-        }
-      },
-      () => this.props.updateLoading({ bubbles: false })
-    );
-  };
-
-  renderContent() {
-    const { branchLike, component } = this.props;
-    if (isFileType(component)) {
-      return (
-        <div className="measure-details-viewer">
-          <SourceViewer branchLike={branchLike} component={component.key} />
-        </div>
-      );
-    }
-
-    return (
-      <BubbleChart
-        component={this.props.component}
-        components={this.state.components}
-        domain={this.props.domain}
-        metrics={this.props.metrics}
-        updateSelected={this.props.updateSelected}
-      />
-    );
-  }
-
-  render() {
-    const { branchLike, component, currentUser, leakPeriod, rootComponent } = this.props;
-    const isLoggedIn = currentUser && currentUser.isLoggedIn;
-    const isFile = isFileType(component);
-    return (
-      <div className={this.props.className}>
-        <div className="layout-page-header-panel layout-page-main-header">
-          <div className="layout-page-header-panel-inner layout-page-main-header-inner">
-            <div className="layout-page-main-inner">
-              <Breadcrumbs
-                backToFirst={true}
-                branchLike={branchLike}
-                className="measure-breadcrumbs spacer-right text-ellipsis"
-                component={component}
-                handleSelect={this.props.updateSelected}
-                rootComponent={rootComponent}
-              />
-              {component.key !== rootComponent.key &&
-                isLoggedIn && (
-                  <MeasureFavoriteContainer
-                    className="measure-favorite spacer-right"
-                    component={component.key}
-                  />
-                )}
-              <PageActions
-                current={this.state.components.length}
-                isFile={isFile}
-                paging={this.state.paging}
-              />
-            </div>
-          </div>
-        </div>
-        <div className="layout-page-main-inner measure-details-content">
-          <div className="clearfix big-spacer-bottom">
-            {leakPeriod != null && (
-              <LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />
-            )}
-          </div>
-          <DeferredSpinner loading={this.props.loading} />
-          {!this.props.loading && this.renderContent()}
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
new file mode 100644 (file)
index 0000000..18c34ea
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Breadcrumbs from './Breadcrumbs';
+import LeakPeriodLegend from './LeakPeriodLegend';
+import MeasureFavoriteContainer from './MeasureFavoriteContainer';
+import PageActions from './PageActions';
+import BubbleChart from '../drilldown/BubbleChart';
+import SourceViewer from '../../../components/SourceViewer/SourceViewer';
+import { getComponentLeaves } from '../../../api/components';
+import { enhanceComponent, getBubbleMetrics, isFileType } from '../utils';
+import { getBranchLikeQuery } from '../../../helpers/branches';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import {
+  BranchLike,
+  ComponentMeasure,
+  ComponentMeasureEnhanced,
+  CurrentUser,
+  Metric,
+  Paging,
+  Period
+} from '../../../app/types';
+
+interface Props {
+  branchLike?: BranchLike;
+  className?: string;
+  component: ComponentMeasure;
+  currentUser: CurrentUser;
+  domain: string;
+  leakPeriod?: Period;
+  loading: boolean;
+  metrics: { [metric: string]: Metric };
+  rootComponent: ComponentMeasure;
+  updateLoading: (param: { [key: string]: boolean }) => void;
+  updateSelected: (component: string) => void;
+}
+
+interface State {
+  components: ComponentMeasureEnhanced[];
+  paging?: Paging;
+}
+
+const BUBBLES_LIMIT = 500;
+
+export default class MeasureOverview extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { components: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchComponents(this.props);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (
+      nextProps.component !== this.props.component ||
+      nextProps.metrics !== this.props.metrics ||
+      nextProps.domain !== this.props.domain
+    ) {
+      this.fetchComponents(nextProps);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchComponents = (props: Props) => {
+    const { branchLike, component, domain, metrics } = props;
+    if (isFileType(component)) {
+      this.setState({ components: [], paging: undefined });
+      return;
+    }
+    const { x, y, size, colors } = getBubbleMetrics(domain, metrics);
+    const metricsKey = [x.key, y.key, size.key];
+    if (colors) {
+      metricsKey.push(...colors.map(metric => metric.key));
+    }
+    const options = {
+      ...getBranchLikeQuery(branchLike),
+      s: 'metric',
+      metricSort: size.key,
+      asc: false,
+      ps: BUBBLES_LIMIT
+    };
+
+    this.props.updateLoading({ bubbles: true });
+    getComponentLeaves(component.key, metricsKey, options).then(
+      r => {
+        if (domain === this.props.domain) {
+          if (this.mounted) {
+            this.setState({
+              components: r.components.map(component =>
+                enhanceComponent(component, undefined, metrics)
+              ),
+              paging: r.paging
+            });
+          }
+          this.props.updateLoading({ bubbles: false });
+        }
+      },
+      () => this.props.updateLoading({ bubbles: false })
+    );
+  };
+
+  renderContent() {
+    const { branchLike, component } = this.props;
+    if (isFileType(component)) {
+      return (
+        <div className="measure-details-viewer">
+          <SourceViewer branchLike={branchLike} component={component.key} />
+        </div>
+      );
+    }
+
+    return (
+      <BubbleChart
+        component={this.props.component}
+        components={this.state.components}
+        domain={this.props.domain}
+        metrics={this.props.metrics}
+        updateSelected={this.props.updateSelected}
+      />
+    );
+  }
+
+  render() {
+    const { branchLike, component, currentUser, leakPeriod, rootComponent } = this.props;
+    const isLoggedIn = currentUser && currentUser.isLoggedIn;
+    const isFile = isFileType(component);
+    return (
+      <div className={this.props.className}>
+        <div className="layout-page-header-panel layout-page-main-header">
+          <div className="layout-page-header-panel-inner layout-page-main-header-inner">
+            <div className="layout-page-main-inner">
+              <Breadcrumbs
+                backToFirst={true}
+                branchLike={branchLike}
+                className="measure-breadcrumbs spacer-right text-ellipsis"
+                component={component}
+                handleSelect={this.props.updateSelected}
+                rootComponent={rootComponent}
+              />
+              {component.key !== rootComponent.key &&
+                isLoggedIn && (
+                  <MeasureFavoriteContainer
+                    className="measure-favorite spacer-right"
+                    component={component.key}
+                  />
+                )}
+              <PageActions
+                current={this.state.components.length}
+                isFile={isFile}
+                paging={this.state.paging}
+              />
+            </div>
+          </div>
+        </div>
+        <div className="layout-page-main-inner measure-details-content">
+          <div className="clearfix big-spacer-bottom">
+            {leakPeriod && (
+              <LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />
+            )}
+          </div>
+          <DeferredSpinner loading={this.props.loading} />
+          {!this.props.loading && this.renderContent()}
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js
deleted file mode 100644 (file)
index db4caed..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import MeasureOverview from './MeasureOverview';
-import { getComponentShow } from '../../../api/components';
-import { getProjectUrl } from '../../../helpers/urls';
-import { isViewType } from '../utils';
-import { getBranchLikeQuery } from '../../../helpers/branches';
-/*:: import type { Component, Period, Query } from '../types'; */
-/*:: import type { RawQuery } from '../../../helpers/query'; */
-/*:: import type { Metric } from '../../../app/flow-types'; */
-
-/*:: type Props = {|
-  branchLike?: { id?: string; name: string },
-  className?: string,
-  rootComponent: Component,
-  currentUser: { isLoggedIn: boolean },
-  domain: string,
-  leakPeriod: Period,
-  metrics: { [string]: Metric },
-  router: {
-    push: ({ pathname: string, query?: RawQuery }) => void
-  },
-  selected: ?string,
-  updateQuery: Query => void
-|}; */
-
-/*:: type State = {
-  component: ?Component,
-  loading: {
-    component: boolean,
-    bubbles: boolean
-  }
-}; */
-
-export default class MeasureOverviewContainer extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  state /*: State */ = {
-    component: null,
-    loading: {
-      component: false,
-      bubbles: false
-    }
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchComponent(this.props);
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    const { component } = this.state;
-    const componentChanged =
-      !component ||
-      nextProps.rootComponent.key !== component.key ||
-      nextProps.selected !== component.key;
-    if (componentChanged || nextProps.domain !== this.props.domain) {
-      this.fetchComponent(nextProps);
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchComponent = ({ branchLike, rootComponent, selected } /*: Props */) => {
-    if (!selected || rootComponent.key === selected) {
-      this.setState({ component: rootComponent });
-      this.updateLoading({ component: false });
-      return;
-    }
-    this.updateLoading({ component: true });
-    getComponentShow({ component: selected, ...getBranchLikeQuery(branchLike) }).then(
-      ({ component }) => {
-        if (this.mounted) {
-          this.setState({ component });
-          this.updateLoading({ component: false });
-        }
-      },
-      () => this.updateLoading({ component: false })
-    );
-  };
-
-  updateLoading = (loading /*: { [string]: boolean } */) => {
-    if (this.mounted) {
-      this.setState(state => ({ loading: { ...state.loading, ...loading } }));
-    }
-  };
-
-  updateSelected = (component /*: string */) => {
-    if (this.state.component && isViewType(this.state.component)) {
-      this.props.router.push(getProjectUrl(component));
-    } else {
-      this.props.updateQuery({
-        selected: component !== this.props.rootComponent.key ? component : null
-      });
-    }
-  };
-
-  render() {
-    if (!this.state.component) {
-      return null;
-    }
-
-    return (
-      <MeasureOverview
-        branchLike={this.props.branchLike}
-        className={this.props.className}
-        component={this.state.component}
-        currentUser={this.props.currentUser}
-        domain={this.props.domain}
-        leakPeriod={this.props.leakPeriod}
-        loading={this.state.loading.component || this.state.loading.bubbles}
-        metrics={this.props.metrics}
-        rootComponent={this.props.rootComponent}
-        updateLoading={this.updateLoading}
-        updateSelected={this.updateSelected}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
new file mode 100644 (file)
index 0000000..642cde7
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { InjectedRouter } from 'react-router';
+import MeasureOverview from './MeasureOverview';
+import { getComponentShow } from '../../../api/components';
+import { getProjectUrl } from '../../../helpers/urls';
+import { isViewType, Query } from '../utils';
+import { getBranchLikeQuery } from '../../../helpers/branches';
+import { BranchLike, ComponentMeasure, CurrentUser, Metric, Period } from '../../../app/types';
+
+interface Props {
+  branchLike?: BranchLike;
+  className?: string;
+  currentUser: CurrentUser;
+  domain: string;
+  leakPeriod?: Period;
+  metrics: { [metric: string]: Metric };
+  rootComponent: ComponentMeasure;
+  router: InjectedRouter;
+  selected?: string;
+  updateQuery: (query: Partial<Query>) => void;
+}
+
+interface LoadingState {
+  bubbles: boolean;
+  component: boolean;
+}
+
+interface State {
+  component?: ComponentMeasure;
+  loading: LoadingState;
+}
+
+export default class MeasureOverviewContainer extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  state: State = {
+    loading: { bubbles: false, component: false }
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchComponent(this.props);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    const { component } = this.state;
+    const componentChanged =
+      !component ||
+      nextProps.rootComponent.key !== component.key ||
+      nextProps.selected !== component.key;
+    if (componentChanged || nextProps.domain !== this.props.domain) {
+      this.fetchComponent(nextProps);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchComponent = ({ branchLike, rootComponent, selected }: Props) => {
+    if (!selected || rootComponent.key === selected) {
+      this.setState({ component: rootComponent });
+      this.updateLoading({ component: false });
+      return;
+    }
+    this.updateLoading({ component: true });
+    getComponentShow({ component: selected, ...getBranchLikeQuery(branchLike) }).then(
+      ({ component }) => {
+        if (this.mounted) {
+          this.setState({ component });
+          this.updateLoading({ component: false });
+        }
+      },
+      () => this.updateLoading({ component: false })
+    );
+  };
+
+  updateLoading = (loading: Partial<LoadingState>) => {
+    if (this.mounted) {
+      this.setState(state => ({ loading: { ...state.loading, ...loading } }));
+    }
+  };
+
+  updateSelected = (component: string) => {
+    if (this.state.component && isViewType(this.state.component)) {
+      this.props.router.push(getProjectUrl(component));
+    } else {
+      this.props.updateQuery({
+        selected: component !== this.props.rootComponent.key ? component : undefined
+      });
+    }
+  };
+
+  render() {
+    if (!this.state.component) {
+      return null;
+    }
+
+    return (
+      <MeasureOverview
+        branchLike={this.props.branchLike}
+        className={this.props.className}
+        component={this.state.component}
+        currentUser={this.props.currentUser}
+        domain={this.props.domain}
+        leakPeriod={this.props.leakPeriod}
+        loading={this.state.loading.component || this.state.loading.bubbles}
+        metrics={this.props.metrics}
+        rootComponent={this.props.rootComponent}
+        updateLoading={this.updateLoading}
+        updateSelected={this.updateSelected}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js b/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js
deleted file mode 100644 (file)
index e512707..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { translate } from '../../../helpers/l10n';
-
-export default function MetricNotFound({ className } /*: { className?: string } */) {
-  return (
-    <div className={className}>
-      <div className="alert alert-danger">{translate('component_measures.not_found')}</div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.tsx
new file mode 100644 (file)
index 0000000..6009d74
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default function MetricNotFound({ className }: { className?: string }) {
+  return (
+    <div className={className}>
+      <div className="alert alert-danger">{translate('component_measures.not_found')}</div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js b/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js
deleted file mode 100644 (file)
index 4b26994..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import FilesCounter from './FilesCounter';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Paging } from '../types'; */
-
-/*:: type Props = {|
-  current: ?number,
-  isFile: ?boolean,
-  paging: ?Paging,
-  totalLoadedComponents?: number,
-  view?: string
-|}; */
-
-export default function PageActions(props /*: Props */) {
-  const { isFile, paging, totalLoadedComponents } = props;
-  const showShortcuts = ['list', 'tree'].includes(props.view);
-  return (
-    <div className="pull-right">
-      {!isFile && showShortcuts && renderShortcuts()}
-      {isFile && paging && renderFileShortcuts()}
-      <div className="measure-details-page-actions">
-        {paging != null && (
-          <FilesCounter
-            className="spacer-left"
-            current={props.current}
-            total={isFile && totalLoadedComponents != null ? totalLoadedComponents : paging.total}
-          />
-        )}
-      </div>
-    </div>
-  );
-}
-
-function renderShortcuts() {
-  return (
-    <span className="note big-spacer-right">
-      <span className="big-spacer-right">
-        <span className="shortcut-button little-spacer-right">↑</span>
-        <span className="shortcut-button little-spacer-right">↓</span>
-        {translate('component_measures.to_select_files')}
-      </span>
-
-      <span>
-        <span className="shortcut-button little-spacer-right">←</span>
-        <span className="shortcut-button little-spacer-right">→</span>
-        {translate('component_measures.to_navigate')}
-      </span>
-    </span>
-  );
-}
-
-function renderFileShortcuts() {
-  return (
-    <span className="note spacer-right">
-      <span>
-        <span className="shortcut-button little-spacer-right">j</span>
-        <span className="shortcut-button little-spacer-right">k</span>
-        {translate('component_measures.to_navigate_files')}
-      </span>
-    </span>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.tsx
new file mode 100644 (file)
index 0000000..75078ed
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import FilesCounter from './FilesCounter';
+import { translate } from '../../../helpers/l10n';
+import { Paging } from '../../../app/types';
+
+interface Props {
+  current?: number;
+  isFile?: boolean;
+  paging?: Paging;
+  totalLoadedComponents?: number;
+  view?: string;
+}
+
+export default function PageActions(props: Props) {
+  const { isFile, paging, totalLoadedComponents } = props;
+  const showShortcuts = props.view && ['list', 'tree'].includes(props.view);
+  return (
+    <div className="pull-right">
+      {!isFile && showShortcuts && renderShortcuts()}
+      {isFile && paging && renderFileShortcuts()}
+      <div className="measure-details-page-actions">
+        {paging != null && (
+          <FilesCounter
+            className="spacer-left"
+            current={props.current}
+            total={isFile && totalLoadedComponents != null ? totalLoadedComponents : paging.total}
+          />
+        )}
+      </div>
+    </div>
+  );
+}
+
+function renderShortcuts() {
+  return (
+    <span className="note big-spacer-right">
+      <span className="big-spacer-right">
+        <span className="shortcut-button little-spacer-right">↑</span>
+        <span className="shortcut-button little-spacer-right">↓</span>
+        {translate('component_measures.to_select_files')}
+      </span>
+
+      <span>
+        <span className="shortcut-button little-spacer-right">←</span>
+        <span className="shortcut-button little-spacer-right">→</span>
+        {translate('component_measures.to_navigate')}
+      </span>
+    </span>
+  );
+}
+
+function renderFileShortcuts() {
+  return (
+    <span className="note spacer-right">
+      <span>
+        <span className="shortcut-button little-spacer-right">j</span>
+        <span className="shortcut-button little-spacer-right">k</span>
+        {translate('component_measures.to_navigate_files')}
+      </span>
+    </span>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js
deleted file mode 100644 (file)
index 5b19497..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import App from '../App';
-
-const METRICS = {
-  lines_to_cover: {
-    key: 'lines_to_cover',
-    type: 'INT',
-    name: 'Lines to Cover',
-    domain: 'Coverage'
-  },
-  coverage: { key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
-  duplicated_lines_density: {
-    key: 'duplicated_lines_density',
-    type: 'PERCENT',
-    name: 'Duplicated Lines (%)',
-    domain: 'Duplications'
-  },
-  new_bugs: { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
-};
-
-const PROPS = {
-  branch: { isMain: true, name: 'master' },
-  component: { key: 'foo' },
-  location: { pathname: '/component_measures', query: { metric: 'coverage' } },
-  fetchMeasures: () => Promise.resolve({ measures: [] }),
-  fetchMetrics: () => {},
-  metrics: METRICS,
-  metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'],
-  router: { push: () => {} }
-};
-
-it('should render correctly', () => {
-  const wrapper = shallow(<App {...PROPS} />);
-  expect(wrapper.find('.spinner')).toHaveLength(1);
-  wrapper.setState({ loading: false });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render a measure overview', () => {
-  const wrapper = shallow(
-    <App
-      {...PROPS}
-      location={{ pathname: '/component_measures', query: { metric: 'Reliability' } }}
-    />
-  );
-  expect(wrapper.find('.spinner')).toHaveLength(1);
-  wrapper.setState({ loading: false });
-  expect(wrapper.find('MeasureOverviewContainer')).toHaveLength(1);
-});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx
new file mode 100644 (file)
index 0000000..f020259
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+/* eslint-disable camelcase */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import App from '../App';
+
+const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' };
+
+const METRICS = {
+  lines_to_cover: {
+    id: '1',
+    key: 'lines_to_cover',
+    type: 'INT',
+    name: 'Lines to Cover',
+    domain: 'Coverage'
+  },
+  coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
+  duplicated_lines_density: {
+    id: '3',
+    key: 'duplicated_lines_density',
+    type: 'PERCENT',
+    name: 'Duplicated Lines (%)',
+    domain: 'Duplications'
+  },
+  new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
+};
+
+const PROPS = {
+  branch: { isMain: true, name: 'master' },
+  component: COMPONENT,
+  currentUser: { isLoggedIn: false },
+  location: { pathname: '/component_measures', query: { metric: 'coverage' } },
+  fetchMeasures: jest.fn().mockResolvedValue({ component: COMPONENT, measures: [] }),
+  fetchMetrics: jest.fn(),
+  metrics: METRICS,
+  metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'],
+  router: { push: jest.fn() } as any
+};
+
+it('should render correctly', () => {
+  const wrapper = shallow(<App {...PROPS} />);
+  expect(wrapper.find('.spinner')).toHaveLength(1);
+  wrapper.setState({ loading: false });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render a measure overview', () => {
+  const wrapper = shallow(
+    <App
+      {...PROPS}
+      location={{ pathname: '/component_measures', query: { metric: 'Reliability' } }}
+    />
+  );
+  expect(wrapper.find('.spinner')).toHaveLength(1);
+  wrapper.setState({ loading: false });
+  expect(wrapper.find('MeasureOverviewContainer')).toHaveLength(1);
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js
deleted file mode 100644 (file)
index 1cedf4a..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import FilesCounter from '../FilesCounter';
-
-it('should display x files on y total', () => {
-  expect(shallow(<FilesCounter current={12} total={123455} />)).toMatchSnapshot();
-});
-
-it('should display only total of files', () => {
-  expect(shallow(<FilesCounter current={null} total={123455} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.tsx
new file mode 100644 (file)
index 0000000..8d02bc6
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import FilesCounter from '../FilesCounter';
+
+it('should display x files on y total', () => {
+  expect(shallow(<FilesCounter current={12} total={123455} />)).toMatchSnapshot();
+});
+
+it('should display only total of files', () => {
+  expect(shallow(<FilesCounter current={undefined} total={123455} />)).toMatchSnapshot();
+});
index 475b6e17bc6ee4148c8a815b87a341d5286c8b50..be616b62017ecf3f612dbf998e70256ef8e2677f 100644 (file)
@@ -20,9 +20,8 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import LeakPeriodLegend from '../LeakPeriodLegend';
-import { PeriodMode, Period } from '../../../../helpers/periods';
 import { differenceInDays } from '../../../../helpers/dates';
-import { ComponentMeasure } from '../../../../app/types';
+import { ComponentMeasure, Period, PeriodMode } from '../../../../app/types';
 
 jest.mock('../../../../helpers/dates', () => {
   const dates = require.requireActual('../../../../helpers/dates');
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
deleted file mode 100644 (file)
index 5582f01..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import MeasureHeader from '../MeasureHeader';
-
-const METRIC = {
-  key: 'reliability_rating',
-  type: 'RATING',
-  name: 'Reliability Rating'
-};
-
-const MEASURE = {
-  value: '3.0',
-  periods: [{ index: 1, value: '0.0' }],
-  metric: METRIC,
-  leak: '0.0'
-};
-
-const LEAK_METRIC = {
-  key: 'new_reliability_rating',
-  type: 'RATING',
-  name: 'Reliability Rating on New Code'
-};
-
-const LEAK_MEASURE = {
-  periods: [{ index: 1, value: '3.0' }],
-  metric: LEAK_METRIC,
-  leak: '3.0'
-};
-
-const SECONDARY = {
-  value: 'java=175123;js=26382',
-  metric: {
-    key: 'ncloc_language_distribution',
-    type: 'DATA',
-    name: 'Lines of Code Per Language'
-  },
-  leak: null
-};
-
-const PROPS = {
-  component: { key: 'foo', qualifier: 'TRK' },
-  components: [],
-  handleSelect: () => {},
-  leakPeriod: {
-    date: '2017-05-16T13:50:02+0200',
-    index: 1,
-    mode: 'previous_version',
-    parameter: '6,4'
-  },
-  measure: MEASURE,
-  metric: METRIC,
-  paging: null,
-  secondaryMeasure: null,
-  selectedIdx: null
-};
-
-it('should render correctly', () => {
-  expect(shallow(<MeasureHeader {...PROPS} />)).toMatchSnapshot();
-});
-
-it('should render correctly for leak', () => {
-  expect(
-    shallow(<MeasureHeader {...PROPS} measure={LEAK_MEASURE} metric={LEAK_METRIC} />)
-  ).toMatchSnapshot();
-});
-
-it('should render with branch', () => {
-  const shortBranch = { isMain: false, name: 'feature', mergeBranch: '', type: 'SHORT' };
-  expect(
-    shallow(<MeasureHeader branchLike={shortBranch} {...PROPS} />).find('Link')
-  ).toMatchSnapshot();
-});
-
-it('should not render link to activity page for files', () => {
-  expect(
-    shallow(<MeasureHeader {...PROPS} />)
-      .find('HistoryIcon')
-      .exists()
-  ).toBeTruthy();
-
-  expect(
-    shallow(<MeasureHeader {...PROPS} component={{ ...PROPS.component, qualifier: 'FIL' }} />)
-      .find('HistoryIcon')
-      .exists()
-  ).toBeFalsy();
-});
-
-it('should display secondary measure too', () => {
-  const wrapper = shallow(<MeasureHeader {...PROPS} secondaryMeasure={SECONDARY} />);
-  expect(wrapper.find('Connect(LanguageDistribution)')).toHaveLength(1);
-});
-
-it('should display correctly for open file', () => {
-  const wrapper = shallow(
-    <MeasureHeader
-      {...PROPS}
-      component={{ key: 'bar', qualifier: 'FIL' }}
-      components={[{ key: 'foo' }, { key: 'bar' }, { key: 'baz' }]}
-      selectedIdx={1}
-    />
-  );
-  expect(wrapper.find('.measure-details-primary-actions')).toMatchSnapshot();
-  wrapper.setProps({ components: [{ key: 'foo' }, { key: 'bar' }] });
-  expect(wrapper.find('.measure-details-primary-actions')).toMatchSnapshot();
-});
-
-it('should work with measure without value', () => {
-  expect(shallow(<MeasureHeader {...PROPS} measure={undefined} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx
new file mode 100644 (file)
index 0000000..965af1b
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import MeasureHeader from '../MeasureHeader';
+import { PeriodMode } from '../../../../app/types';
+
+const METRIC = {
+  id: '1',
+  key: 'reliability_rating',
+  type: 'RATING',
+  name: 'Reliability Rating'
+};
+
+const MEASURE = {
+  value: '3.0',
+  periods: [{ index: 1, value: '0.0' }],
+  metric: METRIC,
+  leak: '0.0'
+};
+
+const LEAK_METRIC = {
+  id: '2',
+  key: 'new_reliability_rating',
+  type: 'RATING',
+  name: 'Reliability Rating on New Code'
+};
+
+const LEAK_MEASURE = {
+  periods: [{ index: 1, value: '3.0' }],
+  metric: LEAK_METRIC,
+  leak: '3.0'
+};
+
+const SECONDARY = {
+  value: 'java=175123;js=26382',
+  metric: {
+    id: '3',
+    key: 'ncloc_language_distribution',
+    type: 'DATA',
+    name: 'Lines of Code Per Language'
+  }
+};
+
+const PROPS = {
+  component: { key: 'foo', name: 'Foo', qualifier: 'TRK' },
+  leakPeriod: {
+    date: '2017-05-16T13:50:02+0200',
+    index: 1,
+    mode: PeriodMode.PreviousVersion,
+    parameter: '6,4'
+  },
+  measure: MEASURE,
+  metric: METRIC
+};
+
+it('should render correctly', () => {
+  expect(shallow(<MeasureHeader {...PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly for leak', () => {
+  expect(
+    shallow(<MeasureHeader {...PROPS} measure={LEAK_MEASURE} metric={LEAK_METRIC} />)
+  ).toMatchSnapshot();
+});
+
+it('should render with branch', () => {
+  const shortBranch = { isMain: false, name: 'feature', mergeBranch: '', type: 'SHORT' };
+  expect(
+    shallow(<MeasureHeader branchLike={shortBranch} {...PROPS} />).find('Link')
+  ).toMatchSnapshot();
+});
+
+it('should not render link to activity page for files', () => {
+  expect(
+    shallow(<MeasureHeader {...PROPS} />)
+      .find('HistoryIcon')
+      .exists()
+  ).toBeTruthy();
+
+  expect(
+    shallow(<MeasureHeader {...PROPS} component={{ ...PROPS.component, qualifier: 'FIL' }} />)
+      .find('HistoryIcon')
+      .exists()
+  ).toBeFalsy();
+});
+
+it('should display secondary measure too', () => {
+  const wrapper = shallow(<MeasureHeader {...PROPS} secondaryMeasure={SECONDARY} />);
+  expect(wrapper.find('Connect(LanguageDistribution)')).toHaveLength(1);
+});
+
+it('should work with measure without value', () => {
+  expect(shallow(<MeasureHeader {...PROPS} measure={undefined} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js
deleted file mode 100644 (file)
index 067ecb4..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import PageActions from '../PageActions';
-
-it('should display correctly for a project', () => {
-  expect(
-    shallow(<PageActions isFile={false} totalLoadedComponents={20} view="list" />)
-  ).toMatchSnapshot();
-});
-
-it('should display correctly for a file', () => {
-  const wrapper = shallow(<PageActions isFile={true} totalLoadedComponents={10} view="tree" />);
-  expect(wrapper).toMatchSnapshot();
-  wrapper.setProps({ paging: { total: 100 } });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should not display shortcuts for treemap', () => {
-  expect(
-    shallow(<PageActions isFile={false} totalLoadedComponents={20} view="treemap" />)
-  ).toMatchSnapshot();
-});
-
-it('should display the total of files', () => {
-  expect(
-    shallow(
-      <PageActions
-        current={12}
-        isFile={false}
-        paging={{ total: 120 }}
-        totalLoadedComponents={20}
-        view="treemap"
-      />
-    )
-  ).toMatchSnapshot();
-  expect(
-    shallow(
-      <PageActions
-        current={12}
-        isFile={true}
-        paging={{ total: 120 }}
-        totalLoadedComponents={20}
-        view="list"
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.tsx
new file mode 100644 (file)
index 0000000..016e36d
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import PageActions from '../PageActions';
+
+const PAGING = {
+  pageIndex: 1,
+  pageSize: 100,
+  total: 120
+};
+
+it('should display correctly for a project', () => {
+  expect(
+    shallow(<PageActions isFile={false} totalLoadedComponents={20} view="list" />)
+  ).toMatchSnapshot();
+});
+
+it('should display correctly for a file', () => {
+  const wrapper = shallow(<PageActions isFile={true} totalLoadedComponents={10} view="tree" />);
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setProps({ paging: { total: 100 } });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should not display shortcuts for treemap', () => {
+  expect(
+    shallow(<PageActions isFile={false} totalLoadedComponents={20} view="treemap" />)
+  ).toMatchSnapshot();
+});
+
+it('should display the total of files', () => {
+  expect(
+    shallow(
+      <PageActions
+        current={12}
+        isFile={false}
+        paging={PAGING}
+        totalLoadedComponents={20}
+        view="treemap"
+      />
+    )
+  ).toMatchSnapshot();
+  expect(
+    shallow(
+      <PageActions
+        current={12}
+        isFile={true}
+        paging={PAGING}
+        totalLoadedComponents={20}
+        view="list"
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
deleted file mode 100644 (file)
index 70583de..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
-  className="layout-page"
-  id="component-measures"
->
-  <Suggestions
-    suggestions="component_measures"
-  />
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="Coverage"
-  />
-  <ScreenPositionHelper
-    className="layout-page-side-outer"
-  />
-  <MeasureContentContainer
-    className="layout-page-main"
-    fetchMeasures={[Function]}
-    leakPeriod={null}
-    metric={
-      Object {
-        "domain": "Coverage",
-        "key": "coverage",
-        "name": "Coverage",
-        "type": "PERCENT",
-      }
-    }
-    metrics={
-      Object {
-        "coverage": Object {
-          "domain": "Coverage",
-          "key": "coverage",
-          "name": "Coverage",
-          "type": "PERCENT",
-        },
-        "duplicated_lines_density": Object {
-          "domain": "Duplications",
-          "key": "duplicated_lines_density",
-          "name": "Duplicated Lines (%)",
-          "type": "PERCENT",
-        },
-        "lines_to_cover": Object {
-          "domain": "Coverage",
-          "key": "lines_to_cover",
-          "name": "Lines to Cover",
-          "type": "INT",
-        },
-        "new_bugs": Object {
-          "domain": "Reliability",
-          "key": "new_bugs",
-          "name": "New Bugs",
-          "type": "INT",
-        },
-      }
-    }
-    rootComponent={
-      Object {
-        "key": "foo",
-      }
-    }
-    router={
-      Object {
-        "push": [Function],
-      }
-    }
-    selected=""
-    updateQuery={[Function]}
-    view="list"
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap
new file mode 100644 (file)
index 0000000..b0436ae
--- /dev/null
@@ -0,0 +1,106 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="layout-page"
+  id="component-measures"
+>
+  <Suggestions
+    suggestions="component_measures"
+  />
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="Coverage"
+  />
+  <ScreenPositionHelper
+    className="layout-page-side-outer"
+  />
+  <MeasureContentContainer
+    className="layout-page-main"
+    currentUser={
+      Object {
+        "isLoggedIn": false,
+      }
+    }
+    fetchMeasures={
+      [MockFunction] {
+        "calls": Array [
+          Array [
+            "foo",
+            Array [
+              "lines_to_cover",
+              "coverage",
+              "duplicated_lines_density",
+              "new_bugs",
+            ],
+            undefined,
+          ],
+        ],
+        "results": Array [
+          Object {
+            "isThrow": false,
+            "value": Promise {},
+          },
+        ],
+      }
+    }
+    metric={
+      Object {
+        "domain": "Coverage",
+        "id": "2",
+        "key": "coverage",
+        "name": "Coverage",
+        "type": "PERCENT",
+      }
+    }
+    metrics={
+      Object {
+        "coverage": Object {
+          "domain": "Coverage",
+          "id": "2",
+          "key": "coverage",
+          "name": "Coverage",
+          "type": "PERCENT",
+        },
+        "duplicated_lines_density": Object {
+          "domain": "Duplications",
+          "id": "3",
+          "key": "duplicated_lines_density",
+          "name": "Duplicated Lines (%)",
+          "type": "PERCENT",
+        },
+        "lines_to_cover": Object {
+          "domain": "Coverage",
+          "id": "1",
+          "key": "lines_to_cover",
+          "name": "Lines to Cover",
+          "type": "INT",
+        },
+        "new_bugs": Object {
+          "domain": "Reliability",
+          "id": "4",
+          "key": "new_bugs",
+          "name": "New Bugs",
+          "type": "INT",
+        },
+      }
+    }
+    rootComponent={
+      Object {
+        "key": "foo",
+        "name": "Foo",
+        "qualifier": "TRK",
+      }
+    }
+    router={
+      Object {
+        "push": [MockFunction],
+      }
+    }
+    selected=""
+    updateQuery={[Function]}
+    view="list"
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap
deleted file mode 100644 (file)
index bb01a61..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display only total of files 1`] = `
-<span>
-  <strong>
-    123,455
-  </strong>
-   
-  component_measures.files
-</span>
-`;
-
-exports[`should display x files on y total 1`] = `
-<span>
-  <strong>
-    <span>
-      12
-       / 
-    </span>
-    123,455
-  </strong>
-   
-  component_measures.files
-</span>
-`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.tsx.snap
new file mode 100644 (file)
index 0000000..bb01a61
--- /dev/null
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display only total of files 1`] = `
+<span>
+  <strong>
+    123,455
+  </strong>
+   
+  component_measures.files
+</span>
+`;
+
+exports[`should display x files on y total 1`] = `
+<span>
+  <strong>
+    <span>
+      12
+       / 
+    </span>
+    123,455
+  </strong>
+   
+  component_measures.files
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap
deleted file mode 100644 (file)
index 49f756b..0000000
+++ /dev/null
@@ -1,268 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display correctly for open file 1`] = `
-<div
-  className="measure-details-primary-actions"
->
-  <LeakPeriodLegend
-    className="spacer-left"
-    component={
-      Object {
-        "key": "bar",
-        "qualifier": "FIL",
-      }
-    }
-    period={
-      Object {
-        "date": "2017-05-16T13:50:02+0200",
-        "index": 1,
-        "mode": "previous_version",
-        "parameter": "6,4",
-      }
-    }
-  />
-</div>
-`;
-
-exports[`should display correctly for open file 2`] = `
-<div
-  className="measure-details-primary-actions"
->
-  <LeakPeriodLegend
-    className="spacer-left"
-    component={
-      Object {
-        "key": "bar",
-        "qualifier": "FIL",
-      }
-    }
-    period={
-      Object {
-        "date": "2017-05-16T13:50:02+0200",
-        "index": 1,
-        "mode": "previous_version",
-        "parameter": "6,4",
-      }
-    }
-  />
-</div>
-`;
-
-exports[`should render correctly 1`] = `
-<div
-  className="measure-details-header big-spacer-bottom"
->
-  <div
-    className="measure-details-primary"
-  >
-    <div
-      className="measure-details-metric"
-    >
-      <IssueTypeIcon
-        className="little-spacer-right text-text-bottom"
-        query="reliability_rating"
-      />
-      Reliability Rating
-      <span
-        className="measure-details-value spacer-left"
-      >
-        <strong>
-          <Measure
-            metricKey="reliability_rating"
-            metricType="RATING"
-            value="3.0"
-          />
-        </strong>
-      </span>
-      <Tooltip
-        overlay="component_measures.show_metric_history"
-      >
-        <Link
-          className="js-show-history spacer-left button button-small"
-          onlyActiveOnIndex={false}
-          style={Object {}}
-          to={
-            Object {
-              "pathname": "/project/activity",
-              "query": Object {
-                "custom_metrics": "reliability_rating",
-                "graph": "custom",
-                "id": "foo",
-              },
-            }
-          }
-        >
-          <HistoryIcon />
-        </Link>
-      </Tooltip>
-    </div>
-    <div
-      className="measure-details-primary-actions"
-    >
-      <LeakPeriodLegend
-        className="spacer-left"
-        component={
-          Object {
-            "key": "foo",
-            "qualifier": "TRK",
-          }
-        }
-        period={
-          Object {
-            "date": "2017-05-16T13:50:02+0200",
-            "index": 1,
-            "mode": "previous_version",
-            "parameter": "6,4",
-          }
-        }
-      />
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correctly for leak 1`] = `
-<div
-  className="measure-details-header big-spacer-bottom"
->
-  <div
-    className="measure-details-primary"
-  >
-    <div
-      className="measure-details-metric"
-    >
-      <IssueTypeIcon
-        className="little-spacer-right text-text-bottom"
-        query="new_reliability_rating"
-      />
-      Reliability Rating on New Code
-      <span
-        className="measure-details-value spacer-left"
-      >
-        <strong>
-          <Measure
-            className="domain-measures-leak"
-            metricKey="new_reliability_rating"
-            metricType="RATING"
-            value="3.0"
-          />
-        </strong>
-      </span>
-    </div>
-    <div
-      className="measure-details-primary-actions"
-    >
-      <LeakPeriodLegend
-        className="spacer-left"
-        component={
-          Object {
-            "key": "foo",
-            "qualifier": "TRK",
-          }
-        }
-        period={
-          Object {
-            "date": "2017-05-16T13:50:02+0200",
-            "index": 1,
-            "mode": "previous_version",
-            "parameter": "6,4",
-          }
-        }
-      />
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render with branch 1`] = `
-<Link
-  className="js-show-history spacer-left button button-small"
-  onlyActiveOnIndex={false}
-  style={Object {}}
-  to={
-    Object {
-      "pathname": "/project/activity",
-      "query": Object {
-        "branch": "feature",
-        "custom_metrics": "reliability_rating",
-        "graph": "custom",
-        "id": "foo",
-      },
-    }
-  }
->
-  <HistoryIcon />
-</Link>
-`;
-
-exports[`should work with measure without value 1`] = `
-<div
-  className="measure-details-header big-spacer-bottom"
->
-  <div
-    className="measure-details-primary"
-  >
-    <div
-      className="measure-details-metric"
-    >
-      <IssueTypeIcon
-        className="little-spacer-right text-text-bottom"
-        query="reliability_rating"
-      />
-      Reliability Rating
-      <span
-        className="measure-details-value spacer-left"
-      >
-        <strong>
-          <Measure
-            metricKey="reliability_rating"
-            metricType="RATING"
-          />
-        </strong>
-      </span>
-      <Tooltip
-        overlay="component_measures.show_metric_history"
-      >
-        <Link
-          className="js-show-history spacer-left button button-small"
-          onlyActiveOnIndex={false}
-          style={Object {}}
-          to={
-            Object {
-              "pathname": "/project/activity",
-              "query": Object {
-                "custom_metrics": "reliability_rating",
-                "graph": "custom",
-                "id": "foo",
-              },
-            }
-          }
-        >
-          <HistoryIcon />
-        </Link>
-      </Tooltip>
-    </div>
-    <div
-      className="measure-details-primary-actions"
-    >
-      <LeakPeriodLegend
-        className="spacer-left"
-        component={
-          Object {
-            "key": "foo",
-            "qualifier": "TRK",
-          }
-        }
-        period={
-          Object {
-            "date": "2017-05-16T13:50:02+0200",
-            "index": 1,
-            "mode": "previous_version",
-            "parameter": "6,4",
-          }
-        }
-      />
-    </div>
-  </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap
new file mode 100644 (file)
index 0000000..2ccafb9
--- /dev/null
@@ -0,0 +1,223 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="measure-details-header big-spacer-bottom"
+>
+  <div
+    className="measure-details-primary"
+  >
+    <div
+      className="measure-details-metric"
+    >
+      <IssueTypeIcon
+        className="little-spacer-right text-text-bottom"
+        query="reliability_rating"
+      />
+      Reliability Rating
+      <span
+        className="measure-details-value spacer-left"
+      >
+        <strong>
+          <Measure
+            metricKey="reliability_rating"
+            metricType="RATING"
+            value="3.0"
+          />
+        </strong>
+      </span>
+      <Tooltip
+        overlay="component_measures.show_metric_history"
+      >
+        <Link
+          className="js-show-history spacer-left button button-small"
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/project/activity",
+              "query": Object {
+                "custom_metrics": "reliability_rating",
+                "graph": "custom",
+                "id": "foo",
+              },
+            }
+          }
+        >
+          <HistoryIcon />
+        </Link>
+      </Tooltip>
+    </div>
+    <div
+      className="measure-details-primary-actions"
+    >
+      <LeakPeriodLegend
+        className="spacer-left"
+        component={
+          Object {
+            "key": "foo",
+            "name": "Foo",
+            "qualifier": "TRK",
+          }
+        }
+        period={
+          Object {
+            "date": "2017-05-16T13:50:02+0200",
+            "index": 1,
+            "mode": "previous_version",
+            "parameter": "6,4",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correctly for leak 1`] = `
+<div
+  className="measure-details-header big-spacer-bottom"
+>
+  <div
+    className="measure-details-primary"
+  >
+    <div
+      className="measure-details-metric"
+    >
+      <IssueTypeIcon
+        className="little-spacer-right text-text-bottom"
+        query="new_reliability_rating"
+      />
+      Reliability Rating on New Code
+      <span
+        className="measure-details-value spacer-left"
+      >
+        <strong>
+          <Measure
+            className="domain-measures-leak"
+            metricKey="new_reliability_rating"
+            metricType="RATING"
+            value="3.0"
+          />
+        </strong>
+      </span>
+    </div>
+    <div
+      className="measure-details-primary-actions"
+    >
+      <LeakPeriodLegend
+        className="spacer-left"
+        component={
+          Object {
+            "key": "foo",
+            "name": "Foo",
+            "qualifier": "TRK",
+          }
+        }
+        period={
+          Object {
+            "date": "2017-05-16T13:50:02+0200",
+            "index": 1,
+            "mode": "previous_version",
+            "parameter": "6,4",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render with branch 1`] = `
+<Link
+  className="js-show-history spacer-left button button-small"
+  onlyActiveOnIndex={false}
+  style={Object {}}
+  to={
+    Object {
+      "pathname": "/project/activity",
+      "query": Object {
+        "branch": "feature",
+        "custom_metrics": "reliability_rating",
+        "graph": "custom",
+        "id": "foo",
+      },
+    }
+  }
+>
+  <HistoryIcon />
+</Link>
+`;
+
+exports[`should work with measure without value 1`] = `
+<div
+  className="measure-details-header big-spacer-bottom"
+>
+  <div
+    className="measure-details-primary"
+  >
+    <div
+      className="measure-details-metric"
+    >
+      <IssueTypeIcon
+        className="little-spacer-right text-text-bottom"
+        query="reliability_rating"
+      />
+      Reliability Rating
+      <span
+        className="measure-details-value spacer-left"
+      >
+        <strong>
+          <Measure
+            metricKey="reliability_rating"
+            metricType="RATING"
+          />
+        </strong>
+      </span>
+      <Tooltip
+        overlay="component_measures.show_metric_history"
+      >
+        <Link
+          className="js-show-history spacer-left button button-small"
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/project/activity",
+              "query": Object {
+                "custom_metrics": "reliability_rating",
+                "graph": "custom",
+                "id": "foo",
+              },
+            }
+          }
+        >
+          <HistoryIcon />
+        </Link>
+      </Tooltip>
+    </div>
+    <div
+      className="measure-details-primary-actions"
+    >
+      <LeakPeriodLegend
+        className="spacer-left"
+        component={
+          Object {
+            "key": "foo",
+            "name": "Foo",
+            "qualifier": "TRK",
+          }
+        }
+        period={
+          Object {
+            "date": "2017-05-16T13:50:02+0200",
+            "index": 1,
+            "mode": "previous_version",
+            "parameter": "6,4",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap
deleted file mode 100644 (file)
index 9849310..0000000
+++ /dev/null
@@ -1,144 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display correctly for a file 1`] = `
-<div
-  className="pull-right"
->
-  <div
-    className="measure-details-page-actions"
-  />
-</div>
-`;
-
-exports[`should display correctly for a file 2`] = `
-<div
-  className="pull-right"
->
-  <span
-    className="note spacer-right"
-  >
-    <span>
-      <span
-        className="shortcut-button little-spacer-right"
-      >
-        j
-      </span>
-      <span
-        className="shortcut-button little-spacer-right"
-      >
-        k
-      </span>
-      component_measures.to_navigate_files
-    </span>
-  </span>
-  <div
-    className="measure-details-page-actions"
-  >
-    <FilesCounter
-      className="spacer-left"
-      total={10}
-    />
-  </div>
-</div>
-`;
-
-exports[`should display correctly for a project 1`] = `
-<div
-  className="pull-right"
->
-  <span
-    className="note big-spacer-right"
-  >
-    <span
-      className="big-spacer-right"
-    >
-      <span
-        className="shortcut-button little-spacer-right"
-      >
-        ↑
-      </span>
-      <span
-        className="shortcut-button little-spacer-right"
-      >
-        ↓
-      </span>
-      component_measures.to_select_files
-    </span>
-    <span>
-      <span
-        className="shortcut-button little-spacer-right"
-      >
-        ←
-      </span>
-      <span
-        className="shortcut-button little-spacer-right"
-      >
-        →
-      </span>
-      component_measures.to_navigate
-    </span>
-  </span>
-  <div
-    className="measure-details-page-actions"
-  />
-</div>
-`;
-
-exports[`should display the total of files 1`] = `
-<div
-  className="pull-right"
->
-  <div
-    className="measure-details-page-actions"
-  >
-    <FilesCounter
-      className="spacer-left"
-      current={12}
-      total={120}
-    />
-  </div>
-</div>
-`;
-
-exports[`should display the total of files 2`] = `
-<div
-  className="pull-right"
->
-  <span
-    className="note spacer-right"
-  >
-    <span>
-      <span
-        className="shortcut-button little-spacer-right"
-      >
-        j
-      </span>
-      <span
-        className="shortcut-button little-spacer-right"
-      >
-        k
-      </span>
-      component_measures.to_navigate_files
-    </span>
-  </span>
-  <div
-    className="measure-details-page-actions"
-  >
-    <FilesCounter
-      className="spacer-left"
-      current={12}
-      total={20}
-    />
-  </div>
-</div>
-`;
-
-exports[`should not display shortcuts for treemap 1`] = `
-<div
-  className="pull-right"
->
-  <div
-    className="measure-details-page-actions"
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.tsx.snap
new file mode 100644 (file)
index 0000000..9849310
--- /dev/null
@@ -0,0 +1,144 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display correctly for a file 1`] = `
+<div
+  className="pull-right"
+>
+  <div
+    className="measure-details-page-actions"
+  />
+</div>
+`;
+
+exports[`should display correctly for a file 2`] = `
+<div
+  className="pull-right"
+>
+  <span
+    className="note spacer-right"
+  >
+    <span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        j
+      </span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        k
+      </span>
+      component_measures.to_navigate_files
+    </span>
+  </span>
+  <div
+    className="measure-details-page-actions"
+  >
+    <FilesCounter
+      className="spacer-left"
+      total={10}
+    />
+  </div>
+</div>
+`;
+
+exports[`should display correctly for a project 1`] = `
+<div
+  className="pull-right"
+>
+  <span
+    className="note big-spacer-right"
+  >
+    <span
+      className="big-spacer-right"
+    >
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ↑
+      </span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ↓
+      </span>
+      component_measures.to_select_files
+    </span>
+    <span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ←
+      </span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        →
+      </span>
+      component_measures.to_navigate
+    </span>
+  </span>
+  <div
+    className="measure-details-page-actions"
+  />
+</div>
+`;
+
+exports[`should display the total of files 1`] = `
+<div
+  className="pull-right"
+>
+  <div
+    className="measure-details-page-actions"
+  >
+    <FilesCounter
+      className="spacer-left"
+      current={12}
+      total={120}
+    />
+  </div>
+</div>
+`;
+
+exports[`should display the total of files 2`] = `
+<div
+  className="pull-right"
+>
+  <span
+    className="note spacer-right"
+  >
+    <span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        j
+      </span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        k
+      </span>
+      component_measures.to_navigate_files
+    </span>
+  </span>
+  <div
+    className="measure-details-page-actions"
+  >
+    <FilesCounter
+      className="spacer-left"
+      current={12}
+      total={20}
+    />
+  </div>
+</div>
+`;
+
+exports[`should not display shortcuts for treemap 1`] = `
+<div
+  className="pull-right"
+>
+  <div
+    className="measure-details-page-actions"
+  />
+</div>
+`;
index 99c1b0e8e8c67e1bee4e2fb587f272f748cfa3d4..0b432457a9021e5ec7628bd76a9d07089eae8748 100644 (file)
@@ -23,7 +23,7 @@ export const bubbles: {
     y: string;
     size: string;
     colors?: string[];
-    yDomain?: number[];
+    yDomain?: [number, number];
   };
 } = {
   Reliability: {
index d7022b680b447a9158629bb05eac81cad7860410..fe45dd98450d26f7e2fe3323b71e99da75150701 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-export const domains: { [domain: string]: { categories?: string[]; order: string[] } } = {
+
+interface Domains {
+  [domain: string]: { categories?: string[]; order: string[] };
+}
+
+export const domains: Domains = {
   Reliability: {
     categories: ['new_code_category', 'overall_category'],
     order: [
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js
deleted file mode 100644 (file)
index 7a6fe32..0000000
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import EmptyResult from './EmptyResult';
-import OriginalBubbleChart from '../../../components/charts/BubbleChart';
-import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
-import {
-  getLocalizedMetricDomain,
-  getLocalizedMetricName,
-  translate,
-  translateWithParameters
-} from '../../../helpers/l10n';
-import { getBubbleMetrics, getBubbleYDomain, isProjectOverview } from '../utils';
-import { RATING_COLORS } from '../../../helpers/constants';
-/*:: import type { Component, ComponentEnhanced } from '../types'; */
-/*:: import type { Metric } from '../../../app/flow-types'; */
-
-const HEIGHT = 500;
-
-/*:: type Props = {|
-  component: Component,
-  components: Array<ComponentEnhanced>,
-  domain: string,
-  metrics: { [string]: Metric },
-  updateSelected: string => void
-|}; */
-
-export default class BubbleChart extends React.PureComponent {
-  /*:: props: Props; */
-
-  getMeasureVal = (component /*: ComponentEnhanced */, metric /*: Metric */) => {
-    const measure = component.measures.find(measure => measure.metric.key === metric.key);
-    if (measure) {
-      return Number(isDiffMetric(metric.key) ? measure.leak : measure.value);
-    }
-  };
-
-  getTooltip(
-    componentName /*: string */,
-    values /*: {
-      x: number,
-      y: number,
-      size: number,
-      colors: ?Array<?number>
-    }*/,
-    metrics /*: {
-      x: Metric ,
-      y: Metric ,
-      size: Metric ,
-      colors: ?Array<Metric>
-    }*/
-  ) {
-    const inner = [
-      componentName,
-      `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
-      `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
-      `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`
-    ];
-    if (values.colors && metrics.colors) {
-      metrics.colors.forEach((metric, idx) => {
-        // $FlowFixMe colors is always defined at this point
-        const colorValue = values.colors[idx];
-        if (colorValue || colorValue === 0) {
-          inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
-        }
-      });
-    }
-    return (
-      <div className="text-left">
-        {inner.map((line, index) => (
-          <React.Fragment key={index}>
-            {line}
-            {index < inner.length - 1 && <br />}
-          </React.Fragment>
-        ))}
-      </div>
-    );
-  }
-
-  handleBubbleClick = (component /*: ComponentEnhanced */) =>
-    this.props.updateSelected(component.refKey || component.key);
-
-  getDescription(domain /*: string */) {
-    const description = `component_measures.overview.${domain}.description`;
-    const translatedDescription = translate(description);
-    if (description === translatedDescription) {
-      return null;
-    }
-    return translatedDescription;
-  }
-
-  renderBubbleChart(
-    metrics /*: {
-      x: Metric ,
-      y: Metric ,
-      size: Metric ,
-      colors: ?Array<Metric>
-    }*/
-  ) {
-    const items = this.props.components
-      .map(component => {
-        const x = this.getMeasureVal(component, metrics.x);
-        const y = this.getMeasureVal(component, metrics.y);
-        const size = this.getMeasureVal(component, metrics.size);
-        const colors =
-          metrics.colors && metrics.colors.map(metric => this.getMeasureVal(component, metric));
-        if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
-          return null;
-        }
-        return {
-          x,
-          y,
-          size,
-          color:
-            colors != null ? RATING_COLORS[Math.max(...colors.filter(Boolean)) - 1] : undefined,
-          link: component,
-          tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics)
-        };
-      })
-      .filter(Boolean);
-
-    const formatXTick = tick => formatMeasure(tick, metrics.x.type);
-    const formatYTick = tick => formatMeasure(tick, metrics.y.type);
-
-    return (
-      <OriginalBubbleChart
-        formatXTick={formatXTick}
-        formatYTick={formatYTick}
-        height={HEIGHT}
-        items={items}
-        onBubbleClick={this.handleBubbleClick}
-        padding={[25, 60, 50, 60]}
-        yDomain={getBubbleYDomain(this.props.domain)}
-      />
-    );
-  }
-
-  renderChartHeader(
-    domain /*: string */,
-    sizeMetric /*: Metric */,
-    colorsMetric /*: ?Array<Metric> */
-  ) {
-    const title = isProjectOverview(domain)
-      ? translate('component_measures.overview', domain, 'title')
-      : translateWithParameters(
-          'component_measures.domain_x_overview',
-          getLocalizedMetricDomain(domain)
-        );
-    return (
-      <div className="measure-overview-bubble-chart-header">
-        <span className="measure-overview-bubble-chart-title">
-          <span className="text-middle">{title}</span>
-          <HelpTooltip className="spacer-left" overlay={this.getDescription(domain)} />
-        </span>
-        <span className="measure-overview-bubble-chart-legend">
-          <span className="note">
-            {colorsMetric && (
-              <span className="spacer-right">
-                {translateWithParameters(
-                  'component_measures.legend.color_x',
-                  colorsMetric.length > 1
-                    ? translateWithParameters(
-                        'component_measures.legend.worse_of_x_y',
-                        ...colorsMetric.map(metric => getLocalizedMetricName(metric))
-                      )
-                    : getLocalizedMetricName(colorsMetric[0])
-                )}
-              </span>
-            )}
-            {translateWithParameters(
-              'component_measures.legend.size_x',
-              getLocalizedMetricName(sizeMetric)
-            )}
-          </span>
-          {colorsMetric && <ColorRatingsLegend className="spacer-top" />}
-        </span>
-      </div>
-    );
-  }
-
-  render() {
-    if (this.props.components.length <= 0) {
-      return <EmptyResult />;
-    }
-    const { domain } = this.props;
-    const metrics = getBubbleMetrics(domain, this.props.metrics);
-
-    return (
-      <div className="measure-overview-bubble-chart">
-        {this.renderChartHeader(domain, metrics.size, metrics.colors)}
-        <div className="measure-overview-bubble-chart-content">
-          {this.renderBubbleChart(metrics)}
-        </div>
-        <div className="measure-overview-bubble-chart-axis x">
-          {getLocalizedMetricName(metrics.x)}
-        </div>
-        <div className="measure-overview-bubble-chart-axis y">
-          {getLocalizedMetricName(metrics.y)}
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx
new file mode 100644 (file)
index 0000000..0c07b5a
--- /dev/null
@@ -0,0 +1,202 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import EmptyResult from './EmptyResult';
+import OriginalBubbleChart, { BubbleItem } from '../../../components/charts/BubbleChart';
+import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import {
+  getLocalizedMetricDomain,
+  getLocalizedMetricName,
+  translate,
+  translateWithParameters
+} from '../../../helpers/l10n';
+import { getBubbleMetrics, getBubbleYDomain, isProjectOverview } from '../utils';
+import { RATING_COLORS } from '../../../helpers/constants';
+import { ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types';
+
+const HEIGHT = 500;
+
+interface Props {
+  component: ComponentMeasure;
+  components: ComponentMeasureEnhanced[];
+  domain: string;
+  metrics: { [metric: string]: Metric };
+  updateSelected: (component: string) => void;
+}
+
+export default class BubbleChart extends React.PureComponent<Props> {
+  getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => {
+    const measure = component.measures.find(measure => measure.metric.key === metric.key);
+    if (!measure) {
+      return undefined;
+    }
+    return Number(isDiffMetric(metric.key) ? measure.leak : measure.value);
+  };
+
+  getTooltip(
+    componentName: string,
+    values: { x: number; y: number; size: number; colors?: Array<number | undefined> },
+    metrics: { x: Metric; y: Metric; size: Metric; colors?: Array<Metric> }
+  ) {
+    const inner = [
+      componentName,
+      `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
+      `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
+      `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`
+    ];
+    const { colors: valuesColors } = values;
+    const { colors: metricColors } = metrics;
+    if (valuesColors && metricColors) {
+      metricColors.forEach((metric, idx) => {
+        const colorValue = valuesColors[idx];
+        if (colorValue || colorValue === 0) {
+          inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
+        }
+      });
+    }
+    return (
+      <div className="text-left">
+        {inner.map((line, index) => (
+          <React.Fragment key={index}>
+            {line}
+            {index < inner.length - 1 && <br />}
+          </React.Fragment>
+        ))}
+      </div>
+    );
+  }
+
+  handleBubbleClick = (component: ComponentMeasureEnhanced) =>
+    this.props.updateSelected(component.refKey || component.key);
+
+  getDescription(domain: string) {
+    const description = `component_measures.overview.${domain}.description`;
+    const translatedDescription = translate(description);
+    if (description === translatedDescription) {
+      return null;
+    }
+    return translatedDescription;
+  }
+
+  renderBubbleChart(metrics: { x: Metric; y: Metric; size: Metric; colors?: Metric[] }) {
+    const items = this.props.components
+      .map(component => {
+        const x = this.getMeasureVal(component, metrics.x);
+        const y = this.getMeasureVal(component, metrics.y);
+        const size = this.getMeasureVal(component, metrics.size);
+        const colors =
+          metrics.colors && metrics.colors.map(metric => this.getMeasureVal(component, metric));
+        if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
+          return undefined;
+        }
+        return {
+          x,
+          y,
+          size,
+          color:
+            colors !== undefined
+              ? RATING_COLORS[Math.max(...colors.filter(Boolean) as number[]) - 1]
+              : undefined,
+          data: component,
+          tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics)
+        };
+      })
+      .filter(Boolean) as BubbleItem<ComponentMeasureEnhanced>[];
+
+    const formatXTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.x.type);
+    const formatYTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.y.type);
+
+    return (
+      <OriginalBubbleChart<ComponentMeasureEnhanced>
+        formatXTick={formatXTick}
+        formatYTick={formatYTick}
+        height={HEIGHT}
+        items={items}
+        onBubbleClick={this.handleBubbleClick}
+        padding={[25, 60, 50, 60]}
+        yDomain={getBubbleYDomain(this.props.domain)}
+      />
+    );
+  }
+
+  renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric?: Metric[]) {
+    const title = isProjectOverview(domain)
+      ? translate('component_measures.overview', domain, 'title')
+      : translateWithParameters(
+          'component_measures.domain_x_overview',
+          getLocalizedMetricDomain(domain)
+        );
+    return (
+      <div className="measure-overview-bubble-chart-header">
+        <span className="measure-overview-bubble-chart-title">
+          <span className="text-middle">{title}</span>
+          <HelpTooltip className="spacer-left" overlay={this.getDescription(domain)} />
+        </span>
+        <span className="measure-overview-bubble-chart-legend">
+          <span className="note">
+            {colorsMetric && (
+              <span className="spacer-right">
+                {translateWithParameters(
+                  'component_measures.legend.color_x',
+                  colorsMetric.length > 1
+                    ? translateWithParameters(
+                        'component_measures.legend.worse_of_x_y',
+                        ...colorsMetric.map(metric => getLocalizedMetricName(metric))
+                      )
+                    : getLocalizedMetricName(colorsMetric[0])
+                )}
+              </span>
+            )}
+            {translateWithParameters(
+              'component_measures.legend.size_x',
+              getLocalizedMetricName(sizeMetric)
+            )}
+          </span>
+          {colorsMetric && <ColorRatingsLegend className="spacer-top" />}
+        </span>
+      </div>
+    );
+  }
+
+  render() {
+    if (this.props.components.length <= 0) {
+      return <EmptyResult />;
+    }
+    const { domain } = this.props;
+    const metrics = getBubbleMetrics(domain, this.props.metrics);
+
+    return (
+      <div className="measure-overview-bubble-chart">
+        {this.renderChartHeader(domain, metrics.size, metrics.colors)}
+        <div className="measure-overview-bubble-chart-content">
+          {this.renderBubbleChart(metrics)}
+        </div>
+        <div className="measure-overview-bubble-chart-axis x">
+          {getLocalizedMetricName(metrics.x)}
+        </div>
+        <div className="measure-overview-bubble-chart-axis y">
+          {getLocalizedMetricName(metrics.y)}
+        </div>
+      </div>
+    );
+  }
+}
index 6f74390695b2599678d268121272f9a59913d53a..a07eb4710c217931bdb79746781270bc297a8e1a 100644 (file)
 import * as React from 'react';
 import * as key from 'keymaster';
 import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import { BranchLike, ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types';
-import { Period } from '../../../helpers/periods';
+import {
+  BranchLike,
+  ComponentMeasure,
+  ComponentMeasureEnhanced,
+  Metric,
+  Period
+} from '../../../app/types';
 
 interface Props {
   branchLike?: BranchLike;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js
deleted file mode 100644 (file)
index 6ec4749..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { Link } from 'react-router';
-import LinkIcon from '../../../components/icons-components/LinkIcon';
-import QualifierIcon from '../../../components/icons-components/QualifierIcon';
-import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon';
-import { splitPath } from '../../../helpers/path';
-import {
-  getPathUrlAsString,
-  getBranchLikeUrl,
-  getLongLivingBranchUrl,
-  getComponentDrilldownUrlWithSelection
-} from '../../../helpers/urls';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Component, ComponentEnhanced } from '../types'; */
-/*:: import type { Metric } from '../../../app/flow-types'; */
-
-/*:: type Props = {
-  branchLike?: { id?: string; name: string },
-  component: ComponentEnhanced,
-  onClick: string => void,
-  metric: Metric,
-  rootComponent: Component
-}; */
-
-export default class ComponentCell extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleClick = (e /*: MouseEvent */) => {
-    const isLeftClickEvent = e.button === 0;
-    const isModifiedEvent = !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
-
-    if (isLeftClickEvent && !isModifiedEvent) {
-      e.preventDefault();
-      this.props.onClick(this.props.component.key);
-    }
-  };
-
-  renderInner() {
-    const { component } = this.props;
-    let head = '';
-    let tail = component.name;
-    let branch = null;
-
-    if (['DIR', 'FIL', 'UTS'].includes(component.qualifier)) {
-      const parts = splitPath(component.path);
-      ({ head, tail } = parts);
-    }
-
-    if (this.props.rootComponent.qualifier === 'APP') {
-      branch = (
-        <React.Fragment>
-          {component.branch ? (
-            <React.Fragment>
-              <LongLivingBranchIcon className="spacer-left little-spacer-right" />
-              <span className="note">{component.branch}</span>
-            </React.Fragment>
-          ) : (
-            <span className="spacer-left outline-badge">{translate('branches.main_branch')}</span>
-          )}
-        </React.Fragment>
-      );
-    }
-    return (
-      <span title={component.refKey || component.key}>
-        <QualifierIcon qualifier={component.qualifier} />
-        &nbsp;
-        {head.length > 0 && <span className="note">{head}/</span>}
-        <span>{tail}</span>
-        {branch}
-      </span>
-    );
-  }
-
-  render() {
-    const { branchLike, component, metric, rootComponent } = this.props;
-    const to =
-      this.props.rootComponent.qualifier === 'APP'
-        ? getLongLivingBranchUrl(component.refKey, component.branch)
-        : getBranchLikeUrl(component.refKey, branchLike);
-    return (
-      <td className="measure-details-component-cell">
-        <div className="text-ellipsis">
-          {component.refKey == null ? (
-            <a
-              className="link-no-underline"
-              href={getPathUrlAsString(
-                getComponentDrilldownUrlWithSelection(
-                  rootComponent.key,
-                  component.key,
-                  metric.key,
-                  branchLike
-                )
-              )}
-              id={'component-measures-component-link-' + component.key}
-              onClick={this.handleClick}>
-              {this.renderInner()}
-            </a>
-          ) : (
-            <Link
-              className="link-no-underline"
-              id={'component-measures-component-link-' + component.key}
-              to={to}>
-              <span className="big-spacer-right">
-                <LinkIcon />
-              </span>
-              {this.renderInner()}
-            </Link>
-          )}
-        </div>
-      </td>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx
new file mode 100644 (file)
index 0000000..4ddcd66
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import LinkIcon from '../../../components/icons-components/LinkIcon';
+import QualifierIcon from '../../../components/icons-components/QualifierIcon';
+import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon';
+import { splitPath } from '../../../helpers/path';
+import {
+  getPathUrlAsString,
+  getBranchLikeUrl,
+  getComponentDrilldownUrlWithSelection,
+  getProjectUrl
+} from '../../../helpers/urls';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike, ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: ComponentMeasureEnhanced;
+  onClick: (component: string) => void;
+  metric: Metric;
+  rootComponent: ComponentMeasure;
+}
+
+export default class ComponentCell extends React.PureComponent<Props> {
+  handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    const isLeftClickEvent = event.button === 0;
+    const isModifiedEvent = !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
+
+    if (isLeftClickEvent && !isModifiedEvent) {
+      event.preventDefault();
+      this.props.onClick(this.props.component.key);
+    }
+  };
+
+  renderInner(componentKey: string) {
+    const { component } = this.props;
+    let head = '';
+    let tail = component.name;
+    let branchComponent = null;
+
+    if (['DIR', 'FIL', 'UTS'].includes(component.qualifier) && component.path) {
+      ({ head, tail } = splitPath(component.path));
+    }
+
+    if (this.props.rootComponent.qualifier === 'APP') {
+      branchComponent = (
+        <>
+          {component.branch ? (
+            <>
+              <LongLivingBranchIcon className="spacer-left little-spacer-right" />
+              <span className="note">{component.branch}</span>
+            </>
+          ) : (
+            <span className="spacer-left outline-badge">{translate('branches.main_branch')}</span>
+          )}
+        </>
+      );
+    }
+    return (
+      <span title={componentKey}>
+        <QualifierIcon qualifier={component.qualifier} />
+        &nbsp;
+        {head.length > 0 && <span className="note">{head}/</span>}
+        <span>{tail}</span>
+        {branchComponent}
+      </span>
+    );
+  }
+
+  render() {
+    const { branchLike, component, metric, rootComponent } = this.props;
+    return (
+      <td className="measure-details-component-cell">
+        <div className="text-ellipsis">
+          {!component.refKey ? (
+            <a
+              className="link-no-underline"
+              href={getPathUrlAsString(
+                getComponentDrilldownUrlWithSelection(
+                  rootComponent.key,
+                  component.key,
+                  metric.key,
+                  branchLike
+                )
+              )}
+              id={'component-measures-component-link-' + component.key}
+              onClick={this.handleClick}>
+              {this.renderInner(component.key)}
+            </a>
+          ) : (
+            <Link
+              className="link-no-underline"
+              id={'component-measures-component-link-' + component.refKey}
+              to={
+                this.props.rootComponent.qualifier === 'APP'
+                  ? getProjectUrl(component.refKey, component.branch)
+                  : getBranchLikeUrl(component.refKey, branchLike)
+              }>
+              <span className="big-spacer-right">
+                <LinkIcon />
+              </span>
+              {this.renderInner(component.refKey)}
+            </Link>
+          )}
+        </div>
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js
deleted file mode 100644 (file)
index 49024fe..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import ComponentCell from './ComponentCell';
-import MeasureCell from './MeasureCell';
-/*:: import type { Component, ComponentEnhanced } from '../types'; */
-/*:: import type { Metric } from '../../../app/flow-types'; */
-
-/*:: type Props = {|
-  branchLike?: { id?: string; name: string },
-  component: ComponentEnhanced,
-  isSelected: boolean,
-  onClick: string => void,
-  otherMetrics: Array<Metric>,
-  metric: Metric,
-  rootComponent: Component
-|}; */
-
-export default function ComponentsListRow(props /*: Props */) {
-  const { branchLike, component, rootComponent } = props;
-  const otherMeasures = props.otherMetrics.map(metric => {
-    const measure = component.measures.find(measure => measure.metric.key === metric.key);
-    return { ...measure, metric };
-  });
-  const rowClass = classNames('measure-details-component-row', {
-    selected: props.isSelected
-  });
-  return (
-    <tr className={rowClass}>
-      <ComponentCell
-        branchLike={branchLike}
-        component={component}
-        metric={props.metric}
-        onClick={props.onClick}
-        rootComponent={rootComponent}
-      />
-
-      <MeasureCell component={component} metric={props.metric} />
-
-      {otherMeasures.map(measure => (
-        <MeasureCell
-          component={component}
-          key={measure.metric.key}
-          measure={measure}
-          metric={measure.metric}
-        />
-      ))}
-    </tr>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx
new file mode 100644 (file)
index 0000000..6ae4c80
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import ComponentCell from './ComponentCell';
+import MeasureCell from './MeasureCell';
+import { ComponentMeasure, Metric, ComponentMeasureEnhanced, BranchLike } from '../../../app/types';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: ComponentMeasureEnhanced;
+  isSelected: boolean;
+  onClick: (component: string) => void;
+  otherMetrics: Metric[];
+  metric: Metric;
+  rootComponent: ComponentMeasure;
+}
+
+export default function ComponentsListRow(props: Props) {
+  const { branchLike, component, rootComponent } = props;
+  const otherMeasures = props.otherMetrics.map(metric => {
+    const measure = component.measures.find(measure => measure.metric.key === metric.key);
+    return { ...measure, metric };
+  });
+  const rowClass = classNames('measure-details-component-row', {
+    selected: props.isSelected
+  });
+  return (
+    <tr className={rowClass}>
+      <ComponentCell
+        branchLike={branchLike}
+        component={component}
+        metric={props.metric}
+        onClick={props.onClick}
+        rootComponent={rootComponent}
+      />
+
+      <MeasureCell component={component} metric={props.metric} />
+
+      {otherMeasures.map(measure => (
+        <MeasureCell
+          component={component}
+          key={measure.metric.key}
+          measure={measure}
+          metric={measure.metric}
+        />
+      ))}
+    </tr>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js
deleted file mode 100644 (file)
index 8a984c5..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import Measure from '../../../components/measure/Measure';
-import { isDiffMetric } from '../../../helpers/measures';
-/*:: import type { ComponentEnhanced } from '../types'; */
-/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
-/*:: import type { Metric } from '../../../app/flow-types'; */
-
-/*:: type Props = {
-  component: ComponentEnhanced,
-  measure?: MeasureEnhanced,
-  metric: Metric
-}; */
-
-export default function MeasureCell({ component, measure, metric } /*: Props */) {
-  const getValue = (item /*: { leak?: ?string; value?: string } */) =>
-    isDiffMetric(metric.key) ? item.leak : item.value;
-
-  const value = getValue(measure || component);
-
-  return (
-    <td className="thin nowrap text-right">
-      <span id={`component-measures-component-measure-${component.key}-${metric.key}`}>
-        <Measure metricKey={metric.key} metricType={metric.type} value={value} />
-      </span>
-    </td>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx
new file mode 100644 (file)
index 0000000..951bb59
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Measure from '../../../components/measure/Measure';
+import { isDiffMetric } from '../../../helpers/measures';
+import { Metric, MeasureEnhanced, ComponentMeasureEnhanced } from '../../../app/types';
+
+interface Props {
+  component: ComponentMeasureEnhanced;
+  measure?: MeasureEnhanced;
+  metric: Metric;
+}
+
+export default function MeasureCell({ component, measure, metric }: Props) {
+  const getValue = (item: { leak?: string; value?: string }) =>
+    isDiffMetric(metric.key) ? item.leak : item.value;
+
+  const value = getValue(measure || component);
+
+  return (
+    <td className="thin nowrap text-right">
+      <span id={`component-measures-component-measure-${component.key}-${metric.key}`}>
+        <Measure metricKey={metric.key} metricType={metric.type} value={value} />
+      </span>
+    </td>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.js
deleted file mode 100644 (file)
index 6897ed8..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import MeasureCell from '../MeasureCell';
-
-describe('should correctly take the value', () => {
-  const renderAndTakeValue = props =>
-    shallow(<MeasureCell {...props} />)
-      .find('Measure')
-      .prop('value');
-
-  it('absolute value', () => {
-    const component = { value: '123' };
-    const metric = { key: 'coverage' };
-    const measure = { value: '567' };
-
-    expect(renderAndTakeValue({ component, metric })).toEqual('123');
-    expect(renderAndTakeValue({ component, metric, measure })).toEqual('567');
-  });
-
-  it('leak value', () => {
-    const component = { leak: '234' };
-    const metric = { key: 'new_coverage' };
-    const measure = { leak: '678' };
-
-    expect(renderAndTakeValue({ component, metric })).toEqual('234');
-    expect(renderAndTakeValue({ component, metric, measure })).toEqual('678');
-  });
-});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.tsx
new file mode 100644 (file)
index 0000000..cd0772f
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import MeasureCell from '../MeasureCell';
+
+describe('should correctly take the value', () => {
+  const renderAndTakeValue = (props: any) =>
+    shallow(<MeasureCell {...props} />)
+      .find('Measure')
+      .prop('value');
+
+  it('absolute value', () => {
+    const component = { value: '123' };
+    const metric = { id: '1', key: 'coverage' };
+    const measure = { value: '567' };
+
+    expect(renderAndTakeValue({ component, metric })).toEqual('123');
+    expect(renderAndTakeValue({ component, metric, measure })).toEqual('567');
+  });
+
+  it('leak value', () => {
+    const component = { leak: '234' };
+    const metric = { id: '1', key: 'new_coverage' };
+    const measure = { leak: '678' };
+
+    expect(renderAndTakeValue({ component, metric })).toEqual('234');
+    expect(renderAndTakeValue({ component, metric, measure })).toEqual('678');
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js
deleted file mode 100644 (file)
index 47338d7..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import FacetMeasureValue from './FacetMeasureValue';
-import BubblesIcon from '../../../components/icons-components/BubblesIcon';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import {
-  addMeasureCategories,
-  filterMeasures,
-  hasBubbleChart,
-  hasFacetStat,
-  sortMeasures
-} from '../utils';
-import {
-  getLocalizedCategoryMetricName,
-  getLocalizedMetricDomain,
-  getLocalizedMetricName,
-  translate
-} from '../../../helpers/l10n';
-/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
-
-/*:: type Props = {|
-  onChange: (metric: string) => void,
-  onToggle: (property: string) => void,
-  open: boolean,
-  domain: { name: string, measures: Array<MeasureEnhanced> },
-  selected: string
-|}; */
-
-export default class DomainFacet extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleHeaderClick = () => this.props.onToggle(this.props.domain.name);
-
-  hasFacetSelected = (
-    domain /*: { name: string } */,
-    measures /*: Array<MeasureEnhanced> */,
-    selected /*: string */
-  ) => {
-    const measureSelected = measures.find(measure => measure.metric.key === selected);
-    const overviewSelected = domain.name === selected && hasBubbleChart(domain.name);
-    return measureSelected || overviewSelected;
-  };
-
-  getValues = () => {
-    const { domain, selected } = this.props;
-    const measureSelected = domain.measures.find(measure => measure.metric.key === selected);
-    const overviewSelected = domain.name === selected && hasBubbleChart(domain.name);
-    if (measureSelected) {
-      return [getLocalizedMetricName(measureSelected.metric)];
-    }
-    return overviewSelected ? [translate('component_measures.domain_overview')] : [];
-  };
-
-  renderItemFacetStat = (item /*: MeasureEnhanced */) =>
-    hasFacetStat(item.metric.key) ? <FacetMeasureValue measure={item} /> : null;
-
-  renderItemsFacet = () => {
-    const { domain, selected } = this.props;
-    const items = addMeasureCategories(domain.name, filterMeasures(domain.measures));
-    const hasCategories = items.some(item => typeof item === 'string');
-    const translateMetric = hasCategories ? getLocalizedCategoryMetricName : getLocalizedMetricName;
-    let sortedItems = sortMeasures(domain.name, items);
-
-    sortedItems = sortedItems.filter((item, index) => {
-      return (
-        typeof item !== 'string' ||
-        (index + 1 !== sortedItems.length && typeof sortedItems[index + 1] !== 'string')
-      );
-    });
-
-    return sortedItems.map(
-      item =>
-        typeof item === 'string' ? (
-          <span className="facet search-navigator-facet facet-category" key={item}>
-            <span className="facet-name">
-              {translate('component_measures.facet_category', item)}
-            </span>
-          </span>
-        ) : (
-          <FacetItem
-            active={item.metric.key === selected}
-            disabled={false}
-            key={item.metric.key}
-            name={
-              <span className="big-spacer-left" id={`measure-${item.metric.key}-name`}>
-                {translateMetric(item.metric)}
-              </span>
-            }
-            onClick={this.props.onChange}
-            stat={this.renderItemFacetStat(item)}
-            value={item.metric.key}
-          />
-        )
-    );
-  };
-
-  renderOverviewFacet = () => {
-    const { domain, selected } = this.props;
-    if (!hasBubbleChart(domain.name)) {
-      return null;
-    }
-    return (
-      <FacetItem
-        active={domain.name === selected}
-        disabled={false}
-        key={domain.name}
-        name={
-          <span id={`measure-overview-${domain.name}-name`}>
-            {translate('component_measures.domain_overview')}
-          </span>
-        }
-        onClick={this.props.onChange}
-        stat={<BubblesIcon size={14} />}
-        value={domain.name}
-      />
-    );
-  };
-
-  render() {
-    const { domain } = this.props;
-    const helper = `component_measures.domain_facets.${domain.name}.help`;
-    const translatedHelper = translate(helper);
-    return (
-      <FacetBox property={domain.name}>
-        <FacetHeader
-          helper={helper !== translatedHelper ? translatedHelper : undefined}
-          name={getLocalizedMetricDomain(domain.name)}
-          onClick={this.handleHeaderClick}
-          open={this.props.open}
-          values={this.getValues()}
-        />
-
-        {this.props.open && (
-          <FacetItemsList>
-            {this.renderOverviewFacet()}
-            {this.renderItemsFacet()}
-          </FacetItemsList>
-        )}
-      </FacetBox>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx
new file mode 100644 (file)
index 0000000..3257089
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import FacetMeasureValue from './FacetMeasureValue';
+import BubblesIcon from '../../../components/icons-components/BubblesIcon';
+import FacetBox from '../../../components/facet/FacetBox';
+import FacetHeader from '../../../components/facet/FacetHeader';
+import FacetItem from '../../../components/facet/FacetItem';
+import FacetItemsList from '../../../components/facet/FacetItemsList';
+import {
+  addMeasureCategories,
+  filterMeasures,
+  hasBubbleChart,
+  hasFacetStat,
+  sortMeasures
+} from '../utils';
+import {
+  getLocalizedCategoryMetricName,
+  getLocalizedMetricDomain,
+  getLocalizedMetricName,
+  translate
+} from '../../../helpers/l10n';
+import { MeasureEnhanced } from '../../../app/types';
+
+interface Props {
+  domain: { name: string; measures: MeasureEnhanced[] };
+  onChange: (metric: string) => void;
+  onToggle: (property: string) => void;
+  open: boolean;
+  selected: string;
+}
+
+export default class DomainFacet extends React.PureComponent<Props> {
+  handleHeaderClick = () => {
+    this.props.onToggle(this.props.domain.name);
+  };
+
+  hasFacetSelected = (domain: { name: string }, measures: MeasureEnhanced[], selected: string) => {
+    const measureSelected = measures.find(measure => measure.metric.key === selected);
+    const overviewSelected = domain.name === selected && hasBubbleChart(domain.name);
+    return measureSelected || overviewSelected;
+  };
+
+  getValues = () => {
+    const { domain, selected } = this.props;
+    const measureSelected = domain.measures.find(measure => measure.metric.key === selected);
+    const overviewSelected = domain.name === selected && hasBubbleChart(domain.name);
+    if (measureSelected) {
+      return [getLocalizedMetricName(measureSelected.metric)];
+    }
+    return overviewSelected ? [translate('component_measures.domain_overview')] : [];
+  };
+
+  renderItemFacetStat = (item: MeasureEnhanced) => {
+    return hasFacetStat(item.metric.key) ? <FacetMeasureValue measure={item} /> : null;
+  };
+
+  renderItemsFacet = () => {
+    const { domain, selected } = this.props;
+    const items = addMeasureCategories(domain.name, filterMeasures(domain.measures));
+    const hasCategories = items.some(item => typeof item === 'string');
+    const translateMetric = hasCategories ? getLocalizedCategoryMetricName : getLocalizedMetricName;
+    let sortedItems = sortMeasures(domain.name, items);
+
+    sortedItems = sortedItems.filter((item, index) => {
+      return (
+        typeof item !== 'string' ||
+        (index + 1 !== sortedItems.length && typeof sortedItems[index + 1] !== 'string')
+      );
+    });
+
+    return sortedItems.map(
+      item =>
+        typeof item === 'string' ? (
+          <span className="facet search-navigator-facet facet-category" key={item}>
+            <span className="facet-name">
+              {translate('component_measures.facet_category', item)}
+            </span>
+          </span>
+        ) : (
+          <FacetItem
+            active={item.metric.key === selected}
+            disabled={false}
+            key={item.metric.key}
+            name={
+              <span className="big-spacer-left" id={`measure-${item.metric.key}-name`}>
+                {translateMetric(item.metric)}
+              </span>
+            }
+            onClick={this.props.onChange}
+            stat={this.renderItemFacetStat(item)}
+            tooltip={translateMetric(item.metric)}
+            value={item.metric.key}
+          />
+        )
+    );
+  };
+
+  renderOverviewFacet = () => {
+    const { domain, selected } = this.props;
+    if (!hasBubbleChart(domain.name)) {
+      return null;
+    }
+    return (
+      <FacetItem
+        active={domain.name === selected}
+        disabled={false}
+        key={domain.name}
+        name={
+          <span id={`measure-overview-${domain.name}-name`}>
+            {translate('component_measures.domain_overview')}
+          </span>
+        }
+        onClick={this.props.onChange}
+        stat={<BubblesIcon size={14} />}
+        tooltip={translate('component_measures.domain_overview')}
+        value={domain.name}
+      />
+    );
+  };
+
+  render() {
+    const { domain } = this.props;
+    const helper = `component_measures.domain_facets.${domain.name}.help`;
+    const translatedHelper = translate(helper);
+    return (
+      <FacetBox property={domain.name}>
+        <FacetHeader
+          helper={helper !== translatedHelper ? translatedHelper : undefined}
+          name={getLocalizedMetricDomain(domain.name)}
+          onClick={this.handleHeaderClick}
+          open={this.props.open}
+          values={this.getValues()}
+        />
+
+        {this.props.open && (
+          <FacetItemsList>
+            {this.renderOverviewFacet()}
+            {this.renderItemsFacet()}
+          </FacetItemsList>
+        )}
+      </FacetBox>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js
deleted file mode 100644 (file)
index 7095335..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import Measure from '../../../components/measure/Measure';
-import { isDiffMetric } from '../../../helpers/measures';
-/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
-
-export default function FacetMeasureValue({ measure } /*: { measure: MeasureEnhanced } */) {
-  if (isDiffMetric(measure.metric.key)) {
-    return (
-      <div
-        className="domain-measures-value domain-measures-leak"
-        id={`measure-${measure.metric.key}-leak`}>
-        <Measure
-          metricKey={measure.metric.key}
-          metricType={measure.metric.type}
-          value={measure.leak}
-        />
-      </div>
-    );
-  }
-
-  return (
-    <div className="domain-measures-value" id={`measure-${measure.metric.key}-value`}>
-      <Measure
-        metricKey={measure.metric.key}
-        metricType={measure.metric.type}
-        value={measure.value}
-      />
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx
new file mode 100644 (file)
index 0000000..1829547
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Measure from '../../../components/measure/Measure';
+import { isDiffMetric } from '../../../helpers/measures';
+import { MeasureEnhanced } from '../../../app/types';
+
+interface Props {
+  measure: MeasureEnhanced;
+}
+
+export default function FacetMeasureValue({ measure }: Props) {
+  if (isDiffMetric(measure.metric.key)) {
+    return (
+      <div
+        className="domain-measures-value domain-measures-leak"
+        id={`measure-${measure.metric.key}-leak`}>
+        <Measure
+          metricKey={measure.metric.key}
+          metricType={measure.metric.type}
+          value={measure.leak}
+        />
+      </div>
+    );
+  }
+
+  return (
+    <div className="domain-measures-value" id={`measure-${measure.metric.key}-value`}>
+      <Measure
+        metricKey={measure.metric.key}
+        metricType={measure.metric.type}
+        value={measure.value}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js
deleted file mode 100644 (file)
index c087708..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import { translate } from '../../../helpers/l10n';
-
-/*:: type Props = {|
-  onChange: (metric: string) => void,
-  selected: string,
-  value: string
-|}; */
-
-export default function ProjectOverviewFacet({ value, selected, onChange } /*: Props */) {
-  const facetName = translate('component_measures.overview', value, 'facet');
-  return (
-    <FacetBox property={value}>
-      <FacetItemsList>
-        <FacetItem
-          active={value === selected}
-          disabled={false}
-          key={value}
-          name={
-            <strong id={`measure-overview-${value}-name`} title={facetName}>
-              {facetName}
-            </strong>
-          }
-          onClick={onChange}
-          value={value}
-        />
-      </FacetItemsList>
-    </FacetBox>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx
new file mode 100644 (file)
index 0000000..a91c658
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import FacetBox from '../../../components/facet/FacetBox';
+import FacetItem from '../../../components/facet/FacetItem';
+import FacetItemsList from '../../../components/facet/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  onChange: (metric: string) => void;
+  selected: string;
+  value: string;
+}
+
+export default function ProjectOverviewFacet({ value, selected, onChange }: Props) {
+  const facetName = translate('component_measures.overview', value, 'facet');
+  return (
+    <FacetBox property={value}>
+      <FacetItemsList>
+        <FacetItem
+          active={value === selected}
+          disabled={false}
+          key={value}
+          name={<strong id={`measure-overview-${value}-name`}>{facetName}</strong>}
+          onClick={onChange}
+          tooltip={facetName}
+          value={value}
+        />
+      </FacetItemsList>
+    </FacetBox>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js
deleted file mode 100644 (file)
index 183875d..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import ProjectOverviewFacet from './ProjectOverviewFacet';
-import DomainFacet from './DomainFacet';
-import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW } from '../utils';
-/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
-/*:: import type { Query } from '../types'; */
-
-/*:: type Props = {|
-  measures: Array<MeasureEnhanced>,
-  selectedMetric: string,
-  updateQuery: Query => void
-|}; */
-
-/*:: type State = {|
-  openFacets: { [string]: boolean }
-|}; */
-
-export default class Sidebar extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = { openFacets: this.getOpenFacets({}, props) };
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    if (nextProps.selectedMetric !== this.props.selectedMetric) {
-      this.setState(state => this.getOpenFacets(state.openFacets, nextProps));
-    }
-  }
-
-  getOpenFacets = (
-    openFacets /*: { [string]: boolean } */,
-    { measures, selectedMetric } /*: Props */
-  ) => {
-    const newOpenFacets = { ...openFacets };
-    const measure = measures.find(measure => measure.metric.key === selectedMetric);
-    if (measure && measure.metric && measure.metric.domain) {
-      newOpenFacets[measure.metric.domain] = true;
-    } else if (KNOWN_DOMAINS.includes(selectedMetric)) {
-      newOpenFacets[selectedMetric] = true;
-    }
-    return newOpenFacets;
-  };
-
-  toggleFacet = (name /*: string */) => {
-    this.setState(({ openFacets } /*: State */) => ({
-      openFacets: { ...openFacets, [name]: !openFacets[name] }
-    }));
-  };
-
-  resetSelection = (metric /*: string */) => ({ selected: null, view: getDefaultView(metric) });
-
-  changeMetric = (metric /*: string */) =>
-    this.props.updateQuery({ metric, ...this.resetSelection(metric) });
-
-  render() {
-    return (
-      <div>
-        <ProjectOverviewFacet
-          onChange={this.changeMetric}
-          selected={this.props.selectedMetric}
-          value={PROJECT_OVERVEW}
-        />
-        {groupByDomains(this.props.measures).map(domain => (
-          <DomainFacet
-            domain={domain}
-            key={domain.name}
-            onChange={this.changeMetric}
-            onToggle={this.toggleFacet}
-            open={this.state.openFacets[domain.name] === true}
-            selected={this.props.selectedMetric}
-          />
-        ))}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
new file mode 100644 (file)
index 0000000..027d359
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import ProjectOverviewFacet from './ProjectOverviewFacet';
+import DomainFacet from './DomainFacet';
+import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW, Query } from '../utils';
+import { MeasureEnhanced } from '../../../app/types';
+
+interface Props {
+  measures: MeasureEnhanced[];
+  selectedMetric: string;
+  updateQuery: (query: Query) => void;
+}
+
+interface State {
+  openFacets: { [metric: string]: boolean };
+}
+
+export default class Sidebar extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = { openFacets: this.getOpenFacets({}, props) };
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (nextProps.selectedMetric !== this.props.selectedMetric) {
+      this.setState(({ openFacets }) => ({
+        openFacets: this.getOpenFacets(openFacets, nextProps)
+      }));
+    }
+  }
+
+  getOpenFacets = (
+    openFacets: { [metric: string]: boolean },
+    { measures, selectedMetric }: Props
+  ) => {
+    const newOpenFacets = { ...openFacets };
+    const measure = measures.find(measure => measure.metric.key === selectedMetric);
+    if (measure && measure.metric && measure.metric.domain) {
+      newOpenFacets[measure.metric.domain] = true;
+    } else if (KNOWN_DOMAINS.includes(selectedMetric)) {
+      newOpenFacets[selectedMetric] = true;
+    }
+    return newOpenFacets;
+  };
+
+  toggleFacet = (name: string) => {
+    this.setState(({ openFacets }) => ({
+      openFacets: { ...openFacets, [name]: !openFacets[name] }
+    }));
+  };
+
+  resetSelection = (metric: string) => ({ selected: undefined, view: getDefaultView(metric) });
+
+  changeMetric = (metric: string) =>
+    this.props.updateQuery({ metric, ...this.resetSelection(metric) });
+
+  render() {
+    return (
+      <div>
+        <ProjectOverviewFacet
+          onChange={this.changeMetric}
+          selected={this.props.selectedMetric}
+          value={PROJECT_OVERVEW}
+        />
+        {groupByDomains(this.props.measures).map(domain => (
+          <DomainFacet
+            domain={domain}
+            key={domain.name}
+            onChange={this.changeMetric}
+            onToggle={this.toggleFacet}
+            open={this.state.openFacets[domain.name] === true}
+            selected={this.props.selectedMetric}
+          />
+        ))}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js
deleted file mode 100644 (file)
index 551d118..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { shallow } from 'enzyme';
-import DomainFacet from '../DomainFacet';
-
-const DOMAIN = {
-  name: 'Reliability',
-  measures: [
-    {
-      metric: {
-        key: 'bugs',
-        type: 'INT',
-        name: 'Bugs',
-        domain: 'Reliability'
-      },
-      value: '5',
-      periods: [{ index: 1, value: '5' }],
-      leak: '5'
-    },
-    {
-      metric: {
-        key: 'new_bugs',
-        type: 'INT',
-        name: 'New Bugs',
-        domain: 'Reliability'
-      },
-      periods: [{ index: 1, value: '5' }],
-      leak: '5'
-    }
-  ]
-};
-
-const PROPS = {
-  onChange: () => {},
-  onToggle: () => {},
-  open: true,
-  domain: DOMAIN,
-  selected: 'foo'
-};
-
-it('should display facet item list', () => {
-  expect(shallow(<DomainFacet {...PROPS} />)).toMatchSnapshot();
-});
-
-it('should display facet item list with bugs selected', () => {
-  expect(shallow(<DomainFacet {...PROPS} selected="bugs" />)).toMatchSnapshot();
-});
-
-it('should render closed', () => {
-  const wrapper = shallow(<DomainFacet {...PROPS} open={false} />);
-  expect(wrapper.find('FacetItemsList')).toHaveLength(0);
-});
-
-it('should not display subtitles of new measures if there is none', () => {
-  const domain = {
-    name: 'Reliability',
-    measures: [
-      {
-        metric: { key: 'bugs', type: 'INT', name: 'Bugs', domain: 'Reliability' },
-        value: '5'
-      }
-    ]
-  };
-
-  expect(
-    shallow(
-      <DomainFacet
-        domain={domain}
-        onChange={() => {}}
-        onToggle={() => {}}
-        open={true}
-        selected={'foo'}
-      />
-    )
-  ).toMatchSnapshot();
-});
-
-it('should not display subtitles of new measures if there is none, even on last line', () => {
-  const domain = {
-    name: 'Reliability',
-    measures: [
-      {
-        metric: { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' },
-        value: '5'
-      }
-    ]
-  };
-
-  expect(
-    shallow(
-      <DomainFacet
-        domain={domain}
-        onChange={() => {}}
-        onToggle={() => {}}
-        open={true}
-        selected={'foo'}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.tsx
new file mode 100644 (file)
index 0000000..590536b
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import DomainFacet from '../DomainFacet';
+
+const DOMAIN = {
+  name: 'Reliability',
+  measures: [
+    {
+      metric: {
+        id: '1',
+        key: 'bugs',
+        type: 'INT',
+        name: 'Bugs',
+        domain: 'Reliability'
+      },
+      value: '5',
+      periods: [{ index: 1, value: '5' }],
+      leak: '5'
+    },
+    {
+      metric: {
+        id: '2',
+        key: 'new_bugs',
+        type: 'INT',
+        name: 'New Bugs',
+        domain: 'Reliability'
+      },
+      periods: [{ index: 1, value: '5' }],
+      leak: '5'
+    }
+  ]
+};
+
+const PROPS = {
+  onChange: () => {},
+  onToggle: () => {},
+  open: true,
+  domain: DOMAIN,
+  selected: 'foo'
+};
+
+it('should display facet item list', () => {
+  expect(shallow(<DomainFacet {...PROPS} />)).toMatchSnapshot();
+});
+
+it('should display facet item list with bugs selected', () => {
+  expect(shallow(<DomainFacet {...PROPS} selected="bugs" />)).toMatchSnapshot();
+});
+
+it('should render closed', () => {
+  const wrapper = shallow(<DomainFacet {...PROPS} open={false} />);
+  expect(wrapper.find('FacetItemsList')).toHaveLength(0);
+});
+
+it('should not display subtitles of new measures if there is none', () => {
+  const domain = {
+    name: 'Reliability',
+    measures: [
+      {
+        metric: { id: '1', key: 'bugs', type: 'INT', name: 'Bugs', domain: 'Reliability' },
+        value: '5'
+      }
+    ]
+  };
+
+  expect(
+    shallow(
+      <DomainFacet
+        domain={domain}
+        onChange={() => {}}
+        onToggle={() => {}}
+        open={true}
+        selected={'foo'}
+      />
+    )
+  ).toMatchSnapshot();
+});
+
+it('should not display subtitles of new measures if there is none, even on last line', () => {
+  const domain = {
+    name: 'Reliability',
+    measures: [
+      {
+        metric: { id: '2', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' },
+        value: '5'
+      }
+    ]
+  };
+
+  expect(
+    shallow(
+      <DomainFacet
+        domain={domain}
+        onChange={() => {}}
+        onToggle={() => {}}
+        open={true}
+        selected={'foo'}
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.js
deleted file mode 100644 (file)
index 542fc42..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { shallow } from 'enzyme';
-import FacetMeasureValue from '../FacetMeasureValue';
-
-const MEASURE = {
-  metric: {
-    key: 'bugs',
-    type: 'INT',
-    name: 'Bugs',
-    domain: 'Reliability'
-  },
-  value: '5',
-  periods: [{ index: 1, value: '5' }],
-  leak: '5'
-};
-const LEAK_MEASURE = {
-  metric: {
-    key: 'new_bugs',
-    type: 'INT',
-    name: 'New Bugs',
-    domain: 'Reliability'
-  },
-  periods: [{ index: 1, value: '5' }],
-  leak: '5'
-};
-
-it('should display measure value', () => {
-  expect(shallow(<FacetMeasureValue measure={MEASURE} />)).toMatchSnapshot();
-});
-
-it('should display leak measure value', () => {
-  expect(shallow(<FacetMeasureValue measure={LEAK_MEASURE} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.tsx
new file mode 100644 (file)
index 0000000..971be2b
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import FacetMeasureValue from '../FacetMeasureValue';
+
+const MEASURE = {
+  metric: {
+    id: '1',
+    key: 'bugs',
+    type: 'INT',
+    name: 'Bugs',
+    domain: 'Reliability'
+  },
+  value: '5',
+  periods: [{ index: 1, value: '5' }],
+  leak: '5'
+};
+const LEAK_MEASURE = {
+  metric: {
+    id: '2',
+    key: 'new_bugs',
+    type: 'INT',
+    name: 'New Bugs',
+    domain: 'Reliability'
+  },
+  periods: [{ index: 1, value: '5' }],
+  leak: '5'
+};
+
+it('should display measure value', () => {
+  expect(shallow(<FacetMeasureValue measure={MEASURE} />)).toMatchSnapshot();
+});
+
+it('should display leak measure value', () => {
+  expect(shallow(<FacetMeasureValue measure={LEAK_MEASURE} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js
deleted file mode 100644 (file)
index e19a464..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import Sidebar from '../Sidebar';
-
-const MEASURES = [
-  {
-    metric: {
-      key: 'lines_to_cover',
-      type: 'INT',
-      name: 'Lines to Cover',
-      domain: 'Coverage'
-    },
-    value: '431',
-    periods: [{ index: 1, value: '70' }],
-    leak: '70'
-  },
-  {
-    metric: {
-      key: 'coverage',
-      type: 'PERCENT',
-      name: 'Coverage',
-      domain: 'Coverage'
-    },
-    value: '99.3',
-    periods: [{ index: 1, value: '0.0999999999999943' }],
-    leak: '0.0999999999999943'
-  },
-  {
-    metric: {
-      key: 'duplicated_lines_density',
-      type: 'PERCENT',
-      name: 'Duplicated Lines (%)',
-      domain: 'Duplications'
-    },
-    value: '3.2',
-    periods: [{ index: 1, value: '0.0' }],
-    leak: '0.0'
-  }
-];
-
-const PROPS = {
-  measures: MEASURES,
-  selectedMetric: 'duplicated_lines_density',
-  updateQuery: () => {}
-};
-
-it('should display two facets', () => {
-  expect(shallow(<Sidebar {...PROPS} />)).toMatchSnapshot();
-});
-
-it('should correctly toggle facets', () => {
-  const wrapper = shallow(<Sidebar {...PROPS} />);
-  expect(wrapper.state('openFacets').bugs).toBeUndefined();
-  wrapper.instance().toggleFacet('bugs');
-  expect(wrapper.state('openFacets').bugs).toBeTruthy();
-  wrapper.instance().toggleFacet('bugs');
-  expect(wrapper.state('openFacets').bugs).toBeFalsy();
-});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx
new file mode 100644 (file)
index 0000000..9b62999
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Sidebar from '../Sidebar';
+
+const MEASURES = [
+  {
+    metric: {
+      id: '1',
+      key: 'lines_to_cover',
+      type: 'INT',
+      name: 'Lines to Cover',
+      domain: 'Coverage'
+    },
+    value: '431',
+    periods: [{ index: 1, value: '70' }],
+    leak: '70'
+  },
+  {
+    metric: {
+      id: '2',
+      key: 'coverage',
+      type: 'PERCENT',
+      name: 'Coverage',
+      domain: 'Coverage'
+    },
+    value: '99.3',
+    periods: [{ index: 1, value: '0.0999999999999943' }],
+    leak: '0.0999999999999943'
+  },
+  {
+    metric: {
+      id: '3',
+      key: 'duplicated_lines_density',
+      type: 'PERCENT',
+      name: 'Duplicated Lines (%)',
+      domain: 'Duplications'
+    },
+    value: '3.2',
+    periods: [{ index: 1, value: '0.0' }],
+    leak: '0.0'
+  }
+];
+
+const PROPS = {
+  measures: MEASURES,
+  selectedMetric: 'duplicated_lines_density',
+  updateQuery: () => {}
+};
+
+it('should display two facets', () => {
+  expect(shallow(<Sidebar {...PROPS} />)).toMatchSnapshot();
+});
+
+it('should correctly toggle facets', () => {
+  const wrapper = shallow(<Sidebar {...PROPS} />);
+  expect(wrapper.state('openFacets').bugs).toBeUndefined();
+  (wrapper.instance() as Sidebar).toggleFacet('bugs');
+  expect(wrapper.state('openFacets').bugs).toBeTruthy();
+  (wrapper.instance() as Sidebar).toggleFacet('bugs');
+  expect(wrapper.state('openFacets').bugs).toBeFalsy();
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap
deleted file mode 100644 (file)
index 64c9678..0000000
+++ /dev/null
@@ -1,427 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display facet item list 1`] = `
-<FacetBox
-  property="Reliability"
->
-  <FacetHeader
-    name="Reliability"
-    onClick={[Function]}
-    open={true}
-    values={Array []}
-  />
-  <FacetItemsList>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="Reliability"
-      loading={false}
-      name={
-        <span
-          id="measure-overview-Reliability-name"
-        >
-          component_measures.domain_overview
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <BubblesIcon
-          size={14}
-        />
-      }
-      value="Reliability"
-    />
-    <span
-      className="facet search-navigator-facet facet-category"
-      key="new_code_category"
-    >
-      <span
-        className="facet-name"
-      >
-        component_measures.facet_category.new_code_category
-      </span>
-    </span>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="new_bugs"
-      loading={false}
-      name={
-        <span
-          className="big-spacer-left"
-          id="measure-new_bugs-name"
-        >
-          New Bugs
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <FacetMeasureValue
-          measure={
-            Object {
-              "leak": "5",
-              "metric": Object {
-                "domain": "Reliability",
-                "key": "new_bugs",
-                "name": "New Bugs",
-                "type": "INT",
-              },
-              "periods": Array [
-                Object {
-                  "index": 1,
-                  "value": "5",
-                },
-              ],
-            }
-          }
-        />
-      }
-      value="new_bugs"
-    />
-    <span
-      className="facet search-navigator-facet facet-category"
-      key="overall_category"
-    >
-      <span
-        className="facet-name"
-      >
-        component_measures.facet_category.overall_category
-      </span>
-    </span>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="bugs"
-      loading={false}
-      name={
-        <span
-          className="big-spacer-left"
-          id="measure-bugs-name"
-        >
-          Bugs
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <FacetMeasureValue
-          measure={
-            Object {
-              "leak": "5",
-              "metric": Object {
-                "domain": "Reliability",
-                "key": "bugs",
-                "name": "Bugs",
-                "type": "INT",
-              },
-              "periods": Array [
-                Object {
-                  "index": 1,
-                  "value": "5",
-                },
-              ],
-              "value": "5",
-            }
-          }
-        />
-      }
-      value="bugs"
-    />
-  </FacetItemsList>
-</FacetBox>
-`;
-
-exports[`should display facet item list with bugs selected 1`] = `
-<FacetBox
-  property="Reliability"
->
-  <FacetHeader
-    name="Reliability"
-    onClick={[Function]}
-    open={true}
-    values={
-      Array [
-        "Bugs",
-      ]
-    }
-  />
-  <FacetItemsList>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="Reliability"
-      loading={false}
-      name={
-        <span
-          id="measure-overview-Reliability-name"
-        >
-          component_measures.domain_overview
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <BubblesIcon
-          size={14}
-        />
-      }
-      value="Reliability"
-    />
-    <span
-      className="facet search-navigator-facet facet-category"
-      key="new_code_category"
-    >
-      <span
-        className="facet-name"
-      >
-        component_measures.facet_category.new_code_category
-      </span>
-    </span>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="new_bugs"
-      loading={false}
-      name={
-        <span
-          className="big-spacer-left"
-          id="measure-new_bugs-name"
-        >
-          New Bugs
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <FacetMeasureValue
-          measure={
-            Object {
-              "leak": "5",
-              "metric": Object {
-                "domain": "Reliability",
-                "key": "new_bugs",
-                "name": "New Bugs",
-                "type": "INT",
-              },
-              "periods": Array [
-                Object {
-                  "index": 1,
-                  "value": "5",
-                },
-              ],
-            }
-          }
-        />
-      }
-      value="new_bugs"
-    />
-    <span
-      className="facet search-navigator-facet facet-category"
-      key="overall_category"
-    >
-      <span
-        className="facet-name"
-      >
-        component_measures.facet_category.overall_category
-      </span>
-    </span>
-    <FacetItem
-      active={true}
-      disabled={false}
-      halfWidth={false}
-      key="bugs"
-      loading={false}
-      name={
-        <span
-          className="big-spacer-left"
-          id="measure-bugs-name"
-        >
-          Bugs
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <FacetMeasureValue
-          measure={
-            Object {
-              "leak": "5",
-              "metric": Object {
-                "domain": "Reliability",
-                "key": "bugs",
-                "name": "Bugs",
-                "type": "INT",
-              },
-              "periods": Array [
-                Object {
-                  "index": 1,
-                  "value": "5",
-                },
-              ],
-              "value": "5",
-            }
-          }
-        />
-      }
-      value="bugs"
-    />
-  </FacetItemsList>
-</FacetBox>
-`;
-
-exports[`should not display subtitles of new measures if there is none 1`] = `
-<FacetBox
-  property="Reliability"
->
-  <FacetHeader
-    name="Reliability"
-    onClick={[Function]}
-    open={true}
-    values={Array []}
-  />
-  <FacetItemsList>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="Reliability"
-      loading={false}
-      name={
-        <span
-          id="measure-overview-Reliability-name"
-        >
-          component_measures.domain_overview
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <BubblesIcon
-          size={14}
-        />
-      }
-      value="Reliability"
-    />
-    <span
-      className="facet search-navigator-facet facet-category"
-      key="overall_category"
-    >
-      <span
-        className="facet-name"
-      >
-        component_measures.facet_category.overall_category
-      </span>
-    </span>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="bugs"
-      loading={false}
-      name={
-        <span
-          className="big-spacer-left"
-          id="measure-bugs-name"
-        >
-          Bugs
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <FacetMeasureValue
-          measure={
-            Object {
-              "metric": Object {
-                "domain": "Reliability",
-                "key": "bugs",
-                "name": "Bugs",
-                "type": "INT",
-              },
-              "value": "5",
-            }
-          }
-        />
-      }
-      value="bugs"
-    />
-  </FacetItemsList>
-</FacetBox>
-`;
-
-exports[`should not display subtitles of new measures if there is none, even on last line 1`] = `
-<FacetBox
-  property="Reliability"
->
-  <FacetHeader
-    name="Reliability"
-    onClick={[Function]}
-    open={true}
-    values={Array []}
-  />
-  <FacetItemsList>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="Reliability"
-      loading={false}
-      name={
-        <span
-          id="measure-overview-Reliability-name"
-        >
-          component_measures.domain_overview
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <BubblesIcon
-          size={14}
-        />
-      }
-      value="Reliability"
-    />
-    <span
-      className="facet search-navigator-facet facet-category"
-      key="new_code_category"
-    >
-      <span
-        className="facet-name"
-      >
-        component_measures.facet_category.new_code_category
-      </span>
-    </span>
-    <FacetItem
-      active={false}
-      disabled={false}
-      halfWidth={false}
-      key="new_bugs"
-      loading={false}
-      name={
-        <span
-          className="big-spacer-left"
-          id="measure-new_bugs-name"
-        >
-          New Bugs
-        </span>
-      }
-      onClick={[Function]}
-      stat={
-        <FacetMeasureValue
-          measure={
-            Object {
-              "metric": Object {
-                "domain": "Reliability",
-                "key": "new_bugs",
-                "name": "New Bugs",
-                "type": "INT",
-              },
-              "value": "5",
-            }
-          }
-        />
-      }
-      value="new_bugs"
-    />
-  </FacetItemsList>
-</FacetBox>
-`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.tsx.snap
new file mode 100644 (file)
index 0000000..0a4c859
--- /dev/null
@@ -0,0 +1,443 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display facet item list 1`] = `
+<FacetBox
+  property="Reliability"
+>
+  <FacetHeader
+    name="Reliability"
+    onClick={[Function]}
+    open={true}
+    values={Array []}
+  />
+  <FacetItemsList>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="Reliability"
+      loading={false}
+      name={
+        <span
+          id="measure-overview-Reliability-name"
+        >
+          component_measures.domain_overview
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <BubblesIcon
+          size={14}
+        />
+      }
+      tooltip="component_measures.domain_overview"
+      value="Reliability"
+    />
+    <span
+      className="facet search-navigator-facet facet-category"
+      key="new_code_category"
+    >
+      <span
+        className="facet-name"
+      >
+        component_measures.facet_category.new_code_category
+      </span>
+    </span>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="new_bugs"
+      loading={false}
+      name={
+        <span
+          className="big-spacer-left"
+          id="measure-new_bugs-name"
+        >
+          New Bugs
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <FacetMeasureValue
+          measure={
+            Object {
+              "leak": "5",
+              "metric": Object {
+                "domain": "Reliability",
+                "id": "2",
+                "key": "new_bugs",
+                "name": "New Bugs",
+                "type": "INT",
+              },
+              "periods": Array [
+                Object {
+                  "index": 1,
+                  "value": "5",
+                },
+              ],
+            }
+          }
+        />
+      }
+      tooltip="New Bugs"
+      value="new_bugs"
+    />
+    <span
+      className="facet search-navigator-facet facet-category"
+      key="overall_category"
+    >
+      <span
+        className="facet-name"
+      >
+        component_measures.facet_category.overall_category
+      </span>
+    </span>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="bugs"
+      loading={false}
+      name={
+        <span
+          className="big-spacer-left"
+          id="measure-bugs-name"
+        >
+          Bugs
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <FacetMeasureValue
+          measure={
+            Object {
+              "leak": "5",
+              "metric": Object {
+                "domain": "Reliability",
+                "id": "1",
+                "key": "bugs",
+                "name": "Bugs",
+                "type": "INT",
+              },
+              "periods": Array [
+                Object {
+                  "index": 1,
+                  "value": "5",
+                },
+              ],
+              "value": "5",
+            }
+          }
+        />
+      }
+      tooltip="Bugs"
+      value="bugs"
+    />
+  </FacetItemsList>
+</FacetBox>
+`;
+
+exports[`should display facet item list with bugs selected 1`] = `
+<FacetBox
+  property="Reliability"
+>
+  <FacetHeader
+    name="Reliability"
+    onClick={[Function]}
+    open={true}
+    values={
+      Array [
+        "Bugs",
+      ]
+    }
+  />
+  <FacetItemsList>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="Reliability"
+      loading={false}
+      name={
+        <span
+          id="measure-overview-Reliability-name"
+        >
+          component_measures.domain_overview
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <BubblesIcon
+          size={14}
+        />
+      }
+      tooltip="component_measures.domain_overview"
+      value="Reliability"
+    />
+    <span
+      className="facet search-navigator-facet facet-category"
+      key="new_code_category"
+    >
+      <span
+        className="facet-name"
+      >
+        component_measures.facet_category.new_code_category
+      </span>
+    </span>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="new_bugs"
+      loading={false}
+      name={
+        <span
+          className="big-spacer-left"
+          id="measure-new_bugs-name"
+        >
+          New Bugs
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <FacetMeasureValue
+          measure={
+            Object {
+              "leak": "5",
+              "metric": Object {
+                "domain": "Reliability",
+                "id": "2",
+                "key": "new_bugs",
+                "name": "New Bugs",
+                "type": "INT",
+              },
+              "periods": Array [
+                Object {
+                  "index": 1,
+                  "value": "5",
+                },
+              ],
+            }
+          }
+        />
+      }
+      tooltip="New Bugs"
+      value="new_bugs"
+    />
+    <span
+      className="facet search-navigator-facet facet-category"
+      key="overall_category"
+    >
+      <span
+        className="facet-name"
+      >
+        component_measures.facet_category.overall_category
+      </span>
+    </span>
+    <FacetItem
+      active={true}
+      disabled={false}
+      halfWidth={false}
+      key="bugs"
+      loading={false}
+      name={
+        <span
+          className="big-spacer-left"
+          id="measure-bugs-name"
+        >
+          Bugs
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <FacetMeasureValue
+          measure={
+            Object {
+              "leak": "5",
+              "metric": Object {
+                "domain": "Reliability",
+                "id": "1",
+                "key": "bugs",
+                "name": "Bugs",
+                "type": "INT",
+              },
+              "periods": Array [
+                Object {
+                  "index": 1,
+                  "value": "5",
+                },
+              ],
+              "value": "5",
+            }
+          }
+        />
+      }
+      tooltip="Bugs"
+      value="bugs"
+    />
+  </FacetItemsList>
+</FacetBox>
+`;
+
+exports[`should not display subtitles of new measures if there is none 1`] = `
+<FacetBox
+  property="Reliability"
+>
+  <FacetHeader
+    name="Reliability"
+    onClick={[Function]}
+    open={true}
+    values={Array []}
+  />
+  <FacetItemsList>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="Reliability"
+      loading={false}
+      name={
+        <span
+          id="measure-overview-Reliability-name"
+        >
+          component_measures.domain_overview
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <BubblesIcon
+          size={14}
+        />
+      }
+      tooltip="component_measures.domain_overview"
+      value="Reliability"
+    />
+    <span
+      className="facet search-navigator-facet facet-category"
+      key="overall_category"
+    >
+      <span
+        className="facet-name"
+      >
+        component_measures.facet_category.overall_category
+      </span>
+    </span>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="bugs"
+      loading={false}
+      name={
+        <span
+          className="big-spacer-left"
+          id="measure-bugs-name"
+        >
+          Bugs
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <FacetMeasureValue
+          measure={
+            Object {
+              "metric": Object {
+                "domain": "Reliability",
+                "id": "1",
+                "key": "bugs",
+                "name": "Bugs",
+                "type": "INT",
+              },
+              "value": "5",
+            }
+          }
+        />
+      }
+      tooltip="Bugs"
+      value="bugs"
+    />
+  </FacetItemsList>
+</FacetBox>
+`;
+
+exports[`should not display subtitles of new measures if there is none, even on last line 1`] = `
+<FacetBox
+  property="Reliability"
+>
+  <FacetHeader
+    name="Reliability"
+    onClick={[Function]}
+    open={true}
+    values={Array []}
+  />
+  <FacetItemsList>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="Reliability"
+      loading={false}
+      name={
+        <span
+          id="measure-overview-Reliability-name"
+        >
+          component_measures.domain_overview
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <BubblesIcon
+          size={14}
+        />
+      }
+      tooltip="component_measures.domain_overview"
+      value="Reliability"
+    />
+    <span
+      className="facet search-navigator-facet facet-category"
+      key="new_code_category"
+    >
+      <span
+        className="facet-name"
+      >
+        component_measures.facet_category.new_code_category
+      </span>
+    </span>
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={false}
+      key="new_bugs"
+      loading={false}
+      name={
+        <span
+          className="big-spacer-left"
+          id="measure-new_bugs-name"
+        >
+          New Bugs
+        </span>
+      }
+      onClick={[Function]}
+      stat={
+        <FacetMeasureValue
+          measure={
+            Object {
+              "metric": Object {
+                "domain": "Reliability",
+                "id": "2",
+                "key": "new_bugs",
+                "name": "New Bugs",
+                "type": "INT",
+              },
+              "value": "5",
+            }
+          }
+        />
+      }
+      tooltip="New Bugs"
+      value="new_bugs"
+    />
+  </FacetItemsList>
+</FacetBox>
+`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap
deleted file mode 100644 (file)
index 228f8a3..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display leak measure value 1`] = `
-<div
-  className="domain-measures-value domain-measures-leak"
-  id="measure-new_bugs-leak"
->
-  <Measure
-    metricKey="new_bugs"
-    metricType="INT"
-    value="5"
-  />
-</div>
-`;
-
-exports[`should display measure value 1`] = `
-<div
-  className="domain-measures-value"
-  id="measure-bugs-value"
->
-  <Measure
-    metricKey="bugs"
-    metricType="INT"
-    value="5"
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap
new file mode 100644 (file)
index 0000000..228f8a3
--- /dev/null
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display leak measure value 1`] = `
+<div
+  className="domain-measures-value domain-measures-leak"
+  id="measure-new_bugs-leak"
+>
+  <Measure
+    metricKey="new_bugs"
+    metricType="INT"
+    value="5"
+  />
+</div>
+`;
+
+exports[`should display measure value 1`] = `
+<div
+  className="domain-measures-value"
+  id="measure-bugs-value"
+>
+  <Measure
+    metricKey="bugs"
+    metricType="INT"
+    value="5"
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap
deleted file mode 100644 (file)
index f526bbb..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display two facets 1`] = `
-<div>
-  <ProjectOverviewFacet
-    onChange={[Function]}
-    selected="duplicated_lines_density"
-    value="project_overview"
-  />
-  <DomainFacet
-    domain={
-      Object {
-        "measures": Array [
-          Object {
-            "leak": "70",
-            "metric": Object {
-              "domain": "Coverage",
-              "key": "lines_to_cover",
-              "name": "Lines to Cover",
-              "type": "INT",
-            },
-            "periods": Array [
-              Object {
-                "index": 1,
-                "value": "70",
-              },
-            ],
-            "value": "431",
-          },
-          Object {
-            "leak": "0.0999999999999943",
-            "metric": Object {
-              "domain": "Coverage",
-              "key": "coverage",
-              "name": "Coverage",
-              "type": "PERCENT",
-            },
-            "periods": Array [
-              Object {
-                "index": 1,
-                "value": "0.0999999999999943",
-              },
-            ],
-            "value": "99.3",
-          },
-        ],
-        "name": "Coverage",
-      }
-    }
-    key="Coverage"
-    onChange={[Function]}
-    onToggle={[Function]}
-    open={false}
-    selected="duplicated_lines_density"
-  />
-  <DomainFacet
-    domain={
-      Object {
-        "measures": Array [
-          Object {
-            "leak": "0.0",
-            "metric": Object {
-              "domain": "Duplications",
-              "key": "duplicated_lines_density",
-              "name": "Duplicated Lines (%)",
-              "type": "PERCENT",
-            },
-            "periods": Array [
-              Object {
-                "index": 1,
-                "value": "0.0",
-              },
-            ],
-            "value": "3.2",
-          },
-        ],
-        "name": "Duplications",
-      }
-    }
-    key="Duplications"
-    onChange={[Function]}
-    onToggle={[Function]}
-    open={true}
-    selected="duplicated_lines_density"
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
new file mode 100644 (file)
index 0000000..09f7788
--- /dev/null
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display two facets 1`] = `
+<div>
+  <ProjectOverviewFacet
+    onChange={[Function]}
+    selected="duplicated_lines_density"
+    value="project_overview"
+  />
+  <DomainFacet
+    domain={
+      Object {
+        "measures": Array [
+          Object {
+            "leak": "70",
+            "metric": Object {
+              "domain": "Coverage",
+              "id": "1",
+              "key": "lines_to_cover",
+              "name": "Lines to Cover",
+              "type": "INT",
+            },
+            "periods": Array [
+              Object {
+                "index": 1,
+                "value": "70",
+              },
+            ],
+            "value": "431",
+          },
+          Object {
+            "leak": "0.0999999999999943",
+            "metric": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "periods": Array [
+              Object {
+                "index": 1,
+                "value": "0.0999999999999943",
+              },
+            ],
+            "value": "99.3",
+          },
+        ],
+        "name": "Coverage",
+      }
+    }
+    key="Coverage"
+    onChange={[Function]}
+    onToggle={[Function]}
+    open={false}
+    selected="duplicated_lines_density"
+  />
+  <DomainFacet
+    domain={
+      Object {
+        "measures": Array [
+          Object {
+            "leak": "0.0",
+            "metric": Object {
+              "domain": "Duplications",
+              "id": "3",
+              "key": "duplicated_lines_density",
+              "name": "Duplicated Lines (%)",
+              "type": "PERCENT",
+            },
+            "periods": Array [
+              Object {
+                "index": 1,
+                "value": "0.0",
+              },
+            ],
+            "value": "3.2",
+          },
+        ],
+        "name": "Duplications",
+      }
+    }
+    key="Duplications"
+    onChange={[Function]}
+    onToggle={[Function]}
+    open={true}
+    selected="duplicated_lines_density"
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/types.js b/server/sonar-web/src/main/js/apps/component-measures/types.js
deleted file mode 100644 (file)
index 4ce7d76..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-/*:: import type { Measure, MeasureEnhanced } from '../../components/measure/types'; */
-
-/*:: type ComponentIntern = {
-  isFavorite?: boolean,
-  isRecentlyBrowsed?: boolean,
-  key: string,
-  match?: string,
-  name: string,
-  organization?: string,
-  project?: string,
-  qualifier: string
-}; */
-
-/*:: export type Component = ComponentIntern & { measures?: Array<Measure> }; */
-
-/*:: export type ComponentEnhanced = ComponentIntern & {
-  value?: ?string,
-  leak?: ?string,
-  measures: Array<MeasureEnhanced>
-}; */
-
-/*:: export type Paging = {
-  pageIndex: number,
-  pageSize: number,
-  total: number
-}; */
-
-/*:: export type Period = {
-  index: number,
-  date: string,
-  mode: string,
-  parameter?: string
-}; */
-
-/*:: export type Query = {
-  metric: ?string,
-  selected: ?string,
-  view: string
-}; */
index a02181c7e95c1c1d854a78fc5e281f050dda4a76..74befd184f03263858a3cf6c7db56e73acd414ec 100644 (file)
@@ -79,10 +79,7 @@ export function sortMeasures(
   ]);
 }
 
-export function addMeasureCategories(
-  domainName: string,
-  measures: MeasureEnhanced[]
-) /*: Array<any> */ {
+export function addMeasureCategories(domainName: string, measures: MeasureEnhanced[]) {
   const categories = domains[domainName] && domains[domainName].categories;
   if (categories && categories.length > 0) {
     return [...categories, ...measures];
@@ -121,7 +118,7 @@ export const groupByDomains = memoize((measures: MeasureEnhanced[]) => {
   }));
 
   return sortBy(domains, [
-    (domain: { name: string; measure: MeasureEnhanced[] }) => {
+    (domain: { name: string; measures: MeasureEnhanced[] }) => {
       const idx = KNOWN_DOMAINS.indexOf(domain.name);
       return idx >= 0 ? idx : KNOWN_DOMAINS.length;
     },
@@ -162,7 +159,7 @@ export function getBubbleMetrics(domain: string, metrics: { [key: string]: Metri
     x: metrics[conf.x],
     y: metrics[conf.y],
     size: metrics[conf.size],
-    colors: conf.colors ? conf.colors.map(color => metrics[color]) : null
+    colors: conf.colors && conf.colors.map(color => metrics[color])
   };
 }
 
index 90b9b56931183b793df2c49e182605611dda34e5..7e1c17777dd71e430b470c33f3b3a0671144043e 100644 (file)
@@ -23,9 +23,10 @@ import DateFromNow from '../../../components/intl/DateFromNow';
 import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import Tooltip from '../../../components/controls/Tooltip';
-import { getPeriodDate, getPeriodLabel, Period, PeriodMode } from '../../../helpers/periods';
+import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods';
 import { translateWithParameters } from '../../../helpers/l10n';
 import { differenceInDays } from '../../../helpers/dates';
+import { Period, PeriodMode } from '../../../app/types';
 
 interface Props {
   period: Period;
index 3f6982b46968045eaa89d1f5ade9a51495a7c0db..f6e58af8c0b78db003d10660ef232ac4643182c7 100644 (file)
@@ -32,7 +32,7 @@ import { getMeasuresAndMeta } from '../../../api/measures';
 import { getAllTimeMachineData, History } from '../../../api/time-machine';
 import { parseDate } from '../../../helpers/dates';
 import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';
-import { getLeakPeriod, Period } from '../../../helpers/periods';
+import { getLeakPeriod } from '../../../helpers/periods';
 import { get } from '../../../helpers/storage';
 import { METRICS, HISTORY_METRICS_LIST } from '../utils';
 import {
@@ -48,7 +48,7 @@ import {
 } from '../../../helpers/branches';
 import { fetchMetrics } from '../../../store/rootActions';
 import { getMetrics, Store } from '../../../store/rootReducer';
-import { BranchLike, Component, Metric, MeasureEnhanced } from '../../../app/types';
+import { BranchLike, Component, Metric, MeasureEnhanced, Period } from '../../../app/types';
 import { translate } from '../../../helpers/l10n';
 import '../styles.css';
 
index fa6f4c952fe62b07056c0ba2e2e3fa8bf14b5eb4..77a2ddc8cca1be00e18e7908e674774eb6a9ebde 100644 (file)
@@ -20,8 +20,8 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import LeakPeriodLegend from '../LeakPeriodLegend';
-import { PeriodMode, Period } from '../../../../helpers/periods';
 import { differenceInDays } from '../../../../helpers/dates';
+import { Period, PeriodMode } from '../../../../app/types';
 
 jest.mock('../../../../helpers/dates', () => {
   const dates = require.requireActual('../../../../helpers/dates');
index 44aecab78af59826558ee22182aa8ddb263037d1..e0306ea0a7ef926b2c9a0a32d9a7bb8788dea56b 100644 (file)
@@ -33,13 +33,13 @@ import {
   getRatingTooltip
 } from '../../../helpers/measures';
 import { getLocalizedMetricName } from '../../../helpers/l10n';
-import { getPeriodDate, Period } from '../../../helpers/periods';
+import { getPeriodDate } from '../../../helpers/periods';
 import {
   getComponentDrilldownUrl,
   getComponentIssuesUrl,
   getMeasureHistoryUrl
 } from '../../../helpers/urls';
-import { Component, BranchLike, MeasureEnhanced } from '../../../app/types';
+import { Component, BranchLike, MeasureEnhanced, Period } from '../../../app/types';
 import { History } from '../../../api/time-machine';
 import { getBranchLikeQuery } from '../../../helpers/branches';
 
index 67d4b26c17a9f99940b9f1b67a6102037ef0a95c..62c153454d7366e66034f18a9c3137dfc8963c05 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+/* eslint-disable camelcase */
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import ProjectCardLeak from '../ProjectCardLeak';
index 85b04e98266d64a3f1313cd30741531686183bd6..021f46232bf978c43084b15770175f8e507fa60e 100644 (file)
@@ -48,7 +48,6 @@ import {
   SourceViewerFile
 } from '../../app/types';
 import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches';
-import { parseDate } from '../../helpers/dates';
 import { translate } from '../../helpers/l10n';
 import './styles.css';
 
index f636a57ee145afd68f7b698687a9a8f83db8b715..7810f4503d85d42d7a6ea964fb14e65f6bf5f78b 100644 (file)
@@ -28,14 +28,16 @@ import { event, select } from 'd3-selection';
 import { sortBy, uniq } from 'lodash';
 import Tooltip from '../controls/Tooltip';
 import { translate } from '../../helpers/l10n';
+import { Location } from '../../helpers/urls';
 import './BubbleChart.css';
 
 const TICKS_COUNT = 5;
 
-interface BubbleProps {
+interface BubbleProps<T> {
   color?: string;
-  link?: string;
-  onClick?: (link?: string) => void;
+  link?: string | Location;
+  onClick?: (ref?: T) => void;
+  data?: T;
   r: number;
   scale: number;
   tooltip?: string | React.ReactNode;
@@ -43,12 +45,12 @@ interface BubbleProps {
   y: number;
 }
 
-export class Bubble extends React.PureComponent<BubbleProps> {
+export class Bubble<T> extends React.PureComponent<BubbleProps<T>> {
   handleClick = (event: React.MouseEvent<SVGCircleElement>) => {
     if (this.props.onClick) {
       event.stopPropagation();
       event.preventDefault();
-      this.props.onClick(this.props.link);
+      this.props.onClick(this.props.data);
     }
   };
 
@@ -75,17 +77,18 @@ export class Bubble extends React.PureComponent<BubbleProps> {
   }
 }
 
-interface Item {
+export interface BubbleItem<T> {
   color?: string;
   key?: string;
-  link?: any;
+  link?: string | Location;
+  data?: T;
   size: number;
   tooltip?: React.ReactNode;
   x: number;
   y: number;
 }
 
-interface Props {
+interface Props<T> {
   displayXGrid?: boolean;
   displayXTicks?: boolean;
   displayYGrid?: boolean;
@@ -93,8 +96,8 @@ interface Props {
   formatXTick?: (tick: number) => string;
   formatYTick?: (tick: number) => string;
   height: number;
-  items: Item[];
-  onBubbleClick?: (link?: string) => void;
+  items: BubbleItem<T>[];
+  onBubbleClick?: (ref?: T) => void;
   padding?: [number, number, number, number];
   sizeDomain?: [number, number];
   sizeRange?: [number, number];
@@ -108,7 +111,7 @@ interface State {
 
 type Scale = ScaleLinear<number, number>;
 
-export default class BubbleChart extends React.Component<Props, State> {
+export default class BubbleChart<T> extends React.Component<Props<T>, State> {
   node: SVGSVGElement | null = null;
   selection: any = null;
   transform: any = null;
@@ -122,7 +125,7 @@ export default class BubbleChart extends React.Component<Props, State> {
     sizeRange: [5, 45]
   };
 
-  constructor(props: Props) {
+  constructor(props: Props<T>) {
     super(props);
     this.state = { transform: { x: 0, y: 0, k: 1 } };
   }
@@ -317,6 +320,7 @@ export default class BubbleChart extends React.Component<Props, State> {
           key={item.key || index}
           link={item.link}
           onClick={this.props.onBubbleClick}
+          data={item.data}
           r={sizeScale(item.size)}
           scale={1 / transform.k}
           tooltip={item.tooltip}
index 07c73da2b87e0e216679e894ee8471174d17a887..d98be168b61069f8a6a25ab385e2d31d5dc3e636 100644 (file)
@@ -35,7 +35,7 @@ it('should render bubble links', () => {
 
 it('should render bubbles with click handlers', () => {
   const onClick = jest.fn();
-  const items = [{ x: 1, y: 10, size: 7, link: 'foo' }, { x: 2, y: 30, size: 5, link: 'bar' }];
+  const items = [{ x: 1, y: 10, size: 7, data: 'foo' }, { x: 2, y: 30, size: 5, data: 'bar' }];
   const chart = mount(<BubbleChart height={100} items={items} onBubbleClick={onClick} />);
   chart.find(Bubble).forEach(bubble => expect(bubble).toMatchSnapshot());
 });
index 89b36a4c5ea58cfe8484990bfecb8b08cd16e6a7..9d69488fbd0c2d458272cee0a3fe661142bc92d8 100644 (file)
@@ -130,8 +130,8 @@ exports[`should render bubble links 2`] = `
 
 exports[`should render bubbles with click handlers 1`] = `
 <Bubble
+  data="foo"
   key="0"
-  link="foo"
   onClick={[MockFunction]}
   r={45}
   scale={1}
@@ -159,8 +159,8 @@ exports[`should render bubbles with click handlers 1`] = `
 
 exports[`should render bubbles with click handlers 2`] = `
 <Bubble
+  data="bar"
   key="1"
-  link="bar"
   onClick={[MockFunction]}
   r={33.57142857142857}
   scale={1}
index e45496fbd1ae3436efaac290590cc0369637a2ea..e5851a5f95666aa3a660d3e86b20ecd91ecbcee5 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { getRatingTooltip as nextGetRatingTooltip, isDiffMetric } from '../../helpers/measures';
 import { Metric, Measure, MeasureEnhanced } from '../../app/types';
+import { getLeakPeriod } from '../../helpers/periods';
 
 const KNOWN_RATINGS = ['sqale_rating', 'reliability_rating', 'security_rating'];
 
@@ -37,7 +38,7 @@ export function getLeakValue(measure: Measure | undefined): string | undefined {
   if (!measure || !measure.periods) {
     return undefined;
   }
-  const period = measure.periods.find(period => period.index === 1);
+  const period = getLeakPeriod(measure.periods);
   return period && period.value;
 }
 
index 1c4138e75a57da08741bc4075e48cb7d221acb5f..c9a257f90fba0640e5721eabac734446358ea892 100644 (file)
@@ -87,16 +87,12 @@ export function fileFromPath(path: string | null): string | null {
   }
 }
 
-export function splitPath(path: string): { head: string; tail: string } | null {
-  if (typeof path === 'string') {
-    const tokens = path.split('/');
-    return {
-      head: tokens.slice(0, -1).join('/'),
-      tail: tokens[tokens.length - 1]
-    };
-  } else {
-    return null;
-  }
+export function splitPath(path: string) {
+  const tokens = path.split('/');
+  return {
+    head: tokens.slice(0, -1).join('/'),
+    tail: tokens[tokens.length - 1]
+  };
 }
 
 export function limitComponentName(str: string, limit = 30): string {
index d278a904b9d49a0f7640fe75951b49b74158a7b6..baa0651439d2c413870f0584809753620834f929 100644 (file)
  */
 import { translate, translateWithParameters } from './l10n';
 import { parseDate } from './dates';
+import { Period, PeriodMode, PeriodMeasure } from '../app/types';
 
-export enum PeriodMode {
-  Days = 'days',
-  Date = 'date',
-  Version = 'version',
-  PreviousAnalysis = 'previous_analysis',
-  PreviousVersion = 'previous_version'
-}
-
-export interface Period {
-  date: string;
-  index: number;
-  mode: PeriodMode;
-  modeParam?: string;
-  parameter?: string;
-}
-
-function getPeriod(periods: Period[] | undefined, index: number) {
+function getPeriod<T extends Period | PeriodMeasure>(periods: T[] | undefined, index: number) {
   if (!Array.isArray(periods)) {
     return undefined;
   }
-
   return periods.find(period => period.index === index);
 }
 
-export function getLeakPeriod(periods: Period[] | undefined) {
+export function getLeakPeriod<T extends Period | PeriodMeasure>(periods: T[] | undefined) {
   return getPeriod(periods, 1);
 }