]> source.dussan.org Git - sonarqube.git/commitdiff
Migrate parts of overview app to TS
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 17 Jan 2018 10:29:05 +0000 (11:29 +0100)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 25 Jan 2018 14:16:50 +0000 (15:16 +0100)
118 files changed:
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/projectActivity.ts
server/sonar-web/src/main/js/api/projectLinks.ts
server/sonar-web/src/main/js/api/quality-gates.ts
server/sonar-web/src/main/js/api/time-machine.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js
server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap
server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx
server/sonar-web/src/main/js/apps/overview/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js [deleted file]
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/Timeline.js [deleted file]
server/sonar-web/src/main/js/apps/overview/components/Timeline.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js [deleted file]
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/Analysis.js [deleted file]
server/sonar-web/src/main/js/apps/overview/events/Analysis.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/Event.js [deleted file]
server/sonar-web/src/main/js/apps/overview/events/Event.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js [deleted file]
server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js [deleted file]
server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/main/Coverage.js
server/sonar-web/src/main/js/apps/overview/main/Duplications.js
server/sonar-web/src/main/js/apps/overview/main/enhance.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main/enhance.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/Meta.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/MetaLink.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js [deleted file]
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js [deleted file]
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js [deleted file]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js [deleted file]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx
server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx
server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx
server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.tsx
server/sonar-web/src/main/js/components/common/BubblePopup.tsx
server/sonar-web/src/main/js/components/common/MultiSelect.js [deleted file]
server/sonar-web/src/main/js/components/common/MultiSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/MultiSelectOption.js [deleted file]
server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.js [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
server/sonar-web/src/main/js/components/measure/Measure.tsx
server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx
server/sonar-web/src/main/js/components/measure/utils.ts
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/drilldown-link.js [deleted file]
server/sonar-web/src/main/js/components/tags/TagsSelector.js [deleted file]
server/sonar-web/src/main/js/components/tags/TagsSelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js [deleted file]
server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts
server/sonar-web/src/main/js/helpers/measures.ts
server/sonar-web/src/main/js/helpers/periods.ts

index 5ed1cf5a25895674c20bd0b16225362684020ae2..db1e32cc64335b4b630c0c33f7c83ab6d8f75d2f 100644 (file)
@@ -79,7 +79,7 @@ export function createProject(data: {
 }
 
 export function searchProjectTags(data?: { ps?: number; q?: string }): Promise<any> {
-  return getJSON('/api/project_tags/search', data);
+  return getJSON('/api/project_tags/search', data).catch(throwGlobalError);
 }
 
 export function setProjectTags(data: { project: string; tags: string }): Promise<void> {
index d78ff17681e61ec9742266501e82762f95b449b9..b931ea5a6f61daf7ec53b73c0a1e7805eaab39f3 100644 (file)
  */
 import { getJSON, postJSON, post, RequestData } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
+import { Paging } from '../app/types';
 
-interface GetProjectActivityResponse {
-  analyses: any[];
-  paging: {
-    total: number;
-    pageIndex: number;
-    pageSize: number;
-  };
+export interface Event {
+  key: string;
+  name: string;
+  category: string;
+  description?: string;
+}
+
+export interface Analysis {
+  key: string;
+  date: string;
+  events: Event[];
 }
 
 export function getProjectActivity(data: {
@@ -35,7 +40,7 @@ export function getProjectActivity(data: {
   category?: string;
   p?: number;
   ps?: number;
-}): Promise<GetProjectActivityResponse> {
+}): Promise<{ analyses: Analysis[]; paging: Paging }> {
   return getJSON('/api/project_analyses/search', data).catch(throwGlobalError);
 }
 
index 16a492bca9fa7ab0d43c55307bf5517d67738db7..e91b200c4fec107c66f5a091dfd5506c841e3bd8 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { getJSON, post, postJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
 
-export function getProjectLinks(projectKey: string): Promise<any> {
+export interface ProjectLink {
+  id: string;
+  name: string;
+  type: string;
+  url: string;
+}
+
+export function getProjectLinks(projectKey: string): Promise<ProjectLink[]> {
   const url = '/api/project_links/search';
   const data = { projectKey };
-  return getJSON(url, data).then(r => r.links);
+  return getJSON(url, data).then(r => r.links, throwGlobalError);
 }
 
 export function deleteLink(linkId: string): Promise<void> {
index 5a50564b36f6dd5dc4b76d0180a07c9a53d5b437..b40e6130b28a8ed12b704b1c90275cc96c748112 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { getJSON, post, postJSON } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
+import { Metric } from '../app/types';
 
 export interface ConditionBase {
   error: string;
@@ -148,9 +149,33 @@ export function dissociateGateWithProject(data: {
   return post('/api/qualitygates/deselect', data).catch(throwGlobalError);
 }
 
+export interface ConditionAnalysis {
+  comparator: string;
+  errorThreshold?: string;
+  metric: string;
+  periodIndex?: number;
+  onLeak?: boolean;
+  status: string;
+  value: string;
+  warningThreshold?: string;
+}
+
+export interface ApplicationProject {
+  key: string;
+  name: string;
+  status: string;
+  conditions: ConditionAnalysis[];
+}
+
+export interface ApplicationQualityGate {
+  metrics: Metric[];
+  projects: ApplicationProject[];
+  status: string;
+}
+
 export function getApplicationQualityGate(data: {
   application: string;
   organization?: string;
-}): Promise<void | Response> {
+}): Promise<ApplicationQualityGate> {
   return getJSON('/api/qualitygates/application_status', data).catch(throwGlobalError);
 }
index e6c01eba852bcdb12763ed6d8a917fdf098db42d..2f25912b191f94b0a4d7d3c7291f0ba93ce500f5 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { getJSON } from '../helpers/request';
 import { Paging } from '../app/types';
+import throwGlobalError from '../app/utils/throwGlobalError';
 
 export interface HistoryItem {
   date: Date;
@@ -47,7 +48,7 @@ export function getTimeMachineData(
     metrics: metrics.join(),
     ps: 1000,
     ...other
-  });
+  }).catch(throwGlobalError);
 }
 
 export function getAllTimeMachineData(
index e109a1428610cede2d7c1d585c82944d44162168..de3102a3957f10e9dad045ac5e824161b18e03ea 100644 (file)
@@ -43,7 +43,7 @@ interface Props {
 interface State {
   branches: Branch[];
   loading: boolean;
-  component: Component | null;
+  component?: Component;
   currentTask?: Task;
   isInProgress?: boolean;
   isPending?: boolean;
@@ -54,7 +54,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
 
   constructor(props: Props) {
     super(props);
-    this.state = { branches: [], loading: true, component: null };
+    this.state = { branches: [], loading: true };
   }
 
   componentDidMount() {
index 7a2a64d4e2605938adac5f41f459ebf965ba29d6..dedfabe68c2d2a059c2b0c2ee1e63f084fdf87d5 100644 (file)
@@ -68,19 +68,25 @@ export interface Breadcrumb {
   qualifier: string;
 }
 
-export interface Component {
+export interface LightComponent {
+  key: string;
+  organization: string;
+  qualifier: string;
+}
+
+export interface Component extends LightComponent {
   analysisDate?: string;
   breadcrumbs: Breadcrumb[];
   configuration?: ComponentConfiguration;
   description?: string;
   extensions?: Extension[];
   isFavorite?: boolean;
-  key: string;
   name: string;
-  organization: string;
   path?: string;
-  qualifier: string;
   refKey?: string;
+  qualityProfiles?: { key: string; language: string; name: string }[];
+  qualityGate?: { isDefault?: boolean; key: string; name: string };
+  tags?: string[];
   version?: string;
   visibility?: string;
 }
@@ -105,7 +111,7 @@ export interface Metric {
   domain?: string;
   hidden?: boolean;
   key: string;
-  name?: string;
+  name: string;
   qualitative?: boolean;
   type: string;
 }
index d0bdd18e3034b9188b0d5d9c38dcc5d90450c86e..bc900211a0ee8932154610108fc55e2f744cbe0b 100644 (file)
@@ -42,7 +42,5 @@ export default function ComponentMeasure({ component, metricKey, metricType }: P
     return <span />;
   }
 
-  return (
-    <Measure measure={{ ...measure, metric: { key: finalMetricKey, type: finalMetricType } }} />
-  );
+  return <Measure value={measure.value} metricKey={finalMetricKey} metricType={finalMetricType} />;
 }
index f59b58ee56f6fc6c56d77359761c682df66c7aa3..c7964eb56a1bc17ae9f5f5ddc3872edb569a17a9 100644 (file)
@@ -44,7 +44,7 @@ import { isDiffMetric } from '../../../helpers/measures';
 
 export default function MeasureHeader(props /*: Props*/) {
   const { branch, component, leakPeriod, measure, secondaryMeasure } = props;
-  const metric = measure.metric;
+  const { metric } = measure;
   const isDiff = isDiffMetric(metric.key);
   return (
     <div className="measure-details-header big-spacer-bottom">
@@ -55,9 +55,14 @@ export default function MeasureHeader(props /*: Props*/) {
           <span className="measure-details-value spacer-left">
             <strong>
               {isDiff ? (
-                <Measure className="domain-measures-leak" measure={measure} metric={metric} />
+                <Measure
+                  className="domain-measures-leak"
+                  value={measure.leak}
+                  metricKey={metric.key}
+                  metricType={metric.type}
+                />
               ) : (
-                <Measure measure={measure} metric={metric} />
+                <Measure value={measure.value} metricKey={metric.key} metricType={metric.type} />
               )}
             </strong>
           </span>
index e58ba900cd7802864742dc27dd96a605ff7a9106..d6c9bceb55af2ca0be704c3ab364031265061a31 100644 (file)
@@ -68,30 +68,9 @@ exports[`should render correctly 1`] = `
       >
         <strong>
           <Measure
-            measure={
-              Object {
-                "leak": "0.0",
-                "metric": Object {
-                  "key": "reliability_rating",
-                  "name": "Reliability Rating",
-                  "type": "RATING",
-                },
-                "periods": Array [
-                  Object {
-                    "index": 1,
-                    "value": "0.0",
-                  },
-                ],
-                "value": "3.0",
-              }
-            }
-            metric={
-              Object {
-                "key": "reliability_rating",
-                "name": "Reliability Rating",
-                "type": "RATING",
-              }
-            }
+            metricKey="reliability_rating"
+            metricType="RATING"
+            value="3.0"
           />
         </strong>
       </span>
@@ -165,29 +144,9 @@ exports[`should render correctly for leak 1`] = `
         <strong>
           <Measure
             className="domain-measures-leak"
-            measure={
-              Object {
-                "leak": "3.0",
-                "metric": Object {
-                  "key": "new_reliability_rating",
-                  "name": "Reliability Rating on New Code",
-                  "type": "RATING",
-                },
-                "periods": Array [
-                  Object {
-                    "index": 1,
-                    "value": "3.0",
-                  },
-                ],
-              }
-            }
-            metric={
-              Object {
-                "key": "new_reliability_rating",
-                "name": "Reliability Rating on New Code",
-                "type": "RATING",
-              }
-            }
+            metricKey="new_reliability_rating"
+            metricType="RATING"
+            value="3.0"
           />
         </strong>
       </span>
index 30b74e4aab4f7a59c1c5b83a2a3ce296b1f0bb37..e87b224967f374436ad8c55577dedc1b61c69e5a 100644 (file)
@@ -52,11 +52,8 @@ export default function ComponentsListRow(props /*: Props */) {
       {otherMeasures.map(measure => (
         <MeasureCell
           key={measure.metric.key}
-          component={{
-            ...component,
-            value: measure.value,
-            leak: measure.leak
-          }}
+          component={component}
+          measure={measure}
           metric={measure.metric}
         />
       ))}
index d5109091646cc11d3ea5f8e97e5250d1c2ba78eb..0934587ccf22151c4e34b44242decba2ffd6768d 100644 (file)
@@ -20,6 +20,7 @@
 // @flow
 import React from 'react';
 import Measure from '../../../components/measure/Measure';
+import { isDiffMetric } from '../../../helpers/measures';
 /*:: import type { Component } from '../types'; */
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
@@ -32,7 +33,11 @@ export default function MeasureCell({ component, metric } /*: Props */) {
   return (
     <td className="thin nowrap text-right">
       <span id={`component-measures-component-measure-${component.key}-${metric.key}`}>
-        <Measure measure={{ metric, value: component.value, leak: component.leak }} />
+        <Measure
+          value={isDiffMetric(metric.key) ? component.leak : component.value}
+          metricKey={metric.key}
+          metricType={metric.type}
+        />
       </span>
     </td>
   );
index d4060ac29c7c0f55992777999900a57b2fcd62ff..93d7ff584931ce696ba396d9226d023a1c23072a 100644 (file)
@@ -29,14 +29,22 @@ export default function FacetMeasureValue({ measure } /*: { measure: MeasureEnha
       <div
         id={`measure-${measure.metric.key}-leak`}
         className="domain-measures-value domain-measures-leak">
-        <Measure measure={measure} />
+        <Measure
+          value={measure.leak}
+          metricKey={measure.metric.key}
+          metricType={measure.metric.type}
+        />
       </div>
     );
   }
 
   return (
     <div id={`measure-${measure.metric.key}-value`} className="domain-measures-value">
-      <Measure measure={measure} />
+      <Measure
+        value={measure.value}
+        metricKey={measure.metric.key}
+        metricType={measure.metric.type}
+      />
     </div>
   );
 }
index 9bd6a3e7e00e7d7667b6ba6af6633905a92a2d90..228f8a3e6d9ff55e8987ffce1106bc2d5e56d464 100644 (file)
@@ -6,23 +6,9 @@ exports[`should display leak measure value 1`] = `
   id="measure-new_bugs-leak"
 >
   <Measure
-    measure={
-      Object {
-        "leak": "5",
-        "metric": Object {
-          "domain": "Reliability",
-          "key": "new_bugs",
-          "name": "New Bugs",
-          "type": "INT",
-        },
-        "periods": Array [
-          Object {
-            "index": 1,
-            "value": "5",
-          },
-        ],
-      }
-    }
+    metricKey="new_bugs"
+    metricType="INT"
+    value="5"
   />
 </div>
 `;
@@ -33,24 +19,9 @@ exports[`should display measure value 1`] = `
   id="measure-bugs-value"
 >
   <Measure
-    measure={
-      Object {
-        "leak": "5",
-        "metric": Object {
-          "domain": "Reliability",
-          "key": "bugs",
-          "name": "Bugs",
-          "type": "INT",
-        },
-        "periods": Array [
-          Object {
-            "index": 1,
-            "value": "5",
-          },
-        ],
-        "value": "5",
-      }
-    }
+    metricKey="bugs"
+    metricType="INT"
+    value="5"
   />
 </div>
 `;
index d8d23b2182c288276961a6cfc63dee574b458c23..7e4e0daf24944785a2e02cdf43ee26942319ec0f 100644 (file)
@@ -27,7 +27,7 @@ import { translate } from '../../../helpers/l10n';
 import './styles.css';
 
 interface Props {
-  branch: string;
+  branch?: string;
   project: string;
 }
 
diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx
new file mode 100644 (file)
index 0000000..03d898d
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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 PropTypes from 'prop-types';
+import OverviewApp from './OverviewApp';
+import EmptyOverview from './EmptyOverview';
+import { getBranchName, isShortLivingBranch } from '../../../helpers/branches';
+import { getProjectBranchUrl, getCodeUrl } from '../../../helpers/urls';
+import { Branch, Component } from '../../../app/types';
+
+interface Props {
+  branch?: Branch;
+  component: Component;
+  isInProgress?: boolean;
+  isPending?: boolean;
+  onComponentChange: (changes: Partial<Component>) => void;
+}
+
+export default class App extends React.PureComponent<Props> {
+  static contextTypes = {
+    router: PropTypes.object
+  };
+
+  componentDidMount() {
+    const { branch, component } = this.props;
+
+    if (this.isPortfolio()) {
+      this.context.router.replace({
+        pathname: '/portfolio',
+        query: { id: component.key }
+      });
+    } else if (this.isFile()) {
+      this.context.router.replace(
+        getCodeUrl(component.breadcrumbs[0].key, getBranchName(branch), component.key)
+      );
+    } else if (isShortLivingBranch(branch)) {
+      this.context.router.replace(getProjectBranchUrl(component.key, branch));
+    }
+  }
+
+  isPortfolio = () => ['VW', 'SVW'].includes(this.props.component.qualifier);
+
+  isFile = () => ['FIL', 'UTS'].includes(this.props.component.qualifier);
+
+  render() {
+    const { branch, component } = this.props;
+
+    if (this.isPortfolio() || this.isFile() || isShortLivingBranch(branch)) {
+      return null;
+    }
+
+    if (!component.analysisDate) {
+      return (
+        <EmptyOverview
+          component={component.key}
+          showWarning={!this.props.isPending && !this.props.isInProgress}
+        />
+      );
+    }
+
+    return (
+      <OverviewApp
+        branch={branch}
+        component={component}
+        onComponentChange={this.props.onComponentChange}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
deleted file mode 100644 (file)
index 0271e1e..0000000
+++ /dev/null
@@ -1,202 +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 { uniq } from 'lodash';
-import QualityGate from '../qualityGate/QualityGate';
-import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate';
-import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities';
-import CodeSmells from '../main/CodeSmells';
-import Coverage from '../main/Coverage';
-import Duplications from '../main/Duplications';
-import Meta from '../meta/Meta';
-import throwGlobalError from '../../../app/utils/throwGlobalError';
-import { getMeasuresAndMeta } from '../../../api/measures';
-import { getAllTimeMachineData } from '../../../api/time-machine';
-import { parseDate } from '../../../helpers/dates';
-import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';
-import { getLeakPeriod } from '../../../helpers/periods';
-import { getCustomGraph, getGraph } from '../../../helpers/storage';
-import { METRICS, HISTORY_METRICS_LIST } from '../utils';
-import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils';
-import { getBranchName } from '../../../helpers/branches';
-/*:: import type { Component, History, MeasuresList, Period } from '../types'; */
-import '../styles.css';
-
-/*::
-type Props = {
-  branch?: { name: string },
-  component: Component,
-  onComponentChange: {} => void
-};
-*/
-
-/*::
-type State = {
-  history?: History,
-  historyStartDate?: Date,
-  loading: boolean,
-  measures: MeasuresList,
-  periods?: Array<Period>
-};
-*/
-
-export default class OverviewApp extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  state /*: State */ = {
-    loading: true,
-    measures: []
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadMeasures().then(this.loadHistory);
-  }
-
-  componentDidUpdate(prevProps /*: Props */) {
-    if (
-      this.props.component.key !== prevProps.component.key ||
-      this.props.branch !== prevProps.branch
-    ) {
-      this.loadMeasures().then(this.loadHistory);
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  loadMeasures() {
-    const { branch, component } = this.props;
-    this.setState({ loading: true });
-
-    return getMeasuresAndMeta(component.key, METRICS, {
-      additionalFields: 'metrics,periods',
-      branch: branch && getBranchName(branch)
-    }).then(
-      r => {
-        if (this.mounted) {
-          this.setState({
-            loading: false,
-            measures: enhanceMeasuresWithMetrics(r.component.measures, r.metrics),
-            periods: r.periods
-          });
-        }
-      },
-      error => {
-        throwGlobalError(error);
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  }
-
-  loadHistory = () => {
-    const { branch, component } = this.props;
-
-    let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
-    if (!graphMetrics || graphMetrics.length <= 0) {
-      graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
-    }
-
-    const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics));
-    return getAllTimeMachineData(component.key, metrics, {
-      branch: branch && getBranchName(branch)
-    }).then(r => {
-      if (this.mounted) {
-        const history /*: History */ = {};
-        r.measures.forEach(measure => {
-          const measureHistory = measure.history.map(analysis => ({
-            date: parseDate(analysis.date),
-            value: analysis.value
-          }));
-          history[measure.metric] = measureHistory;
-        });
-        const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date;
-        this.setState({ history, historyStartDate });
-      }
-    }, throwGlobalError);
-  };
-
-  getApplicationLeakPeriod = () =>
-    this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null;
-
-  renderLoading() {
-    return (
-      <div className="text-center">
-        <i className="spinner spinner-margin" />
-      </div>
-    );
-  }
-
-  render() {
-    const { branch, component } = this.props;
-    const { loading, measures, periods, history, historyStartDate } = this.state;
-
-    if (loading) {
-      return this.renderLoading();
-    }
-
-    const leakPeriod =
-      component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods);
-    const branchName = branch && getBranchName(branch);
-    const domainProps = {
-      branch: branchName,
-      component,
-      measures,
-      leakPeriod,
-      history,
-      historyStartDate
-    };
-
-    return (
-      <div className="page page-limited">
-        <div className="overview page-with-sidebar">
-          <div className="overview-main page-main">
-            {component.qualifier === 'APP' ? (
-              <ApplicationQualityGate component={component} />
-            ) : (
-              <QualityGate branch={branchName} component={component} measures={measures} />
-            )}
-
-            <div className="overview-domains-list">
-              <BugsAndVulnerabilities {...domainProps} />
-              <CodeSmells {...domainProps} />
-              <Coverage {...domainProps} />
-              <Duplications {...domainProps} />
-            </div>
-          </div>
-
-          <div className="page-sidebar-fixed">
-            <Meta
-              branch={branchName}
-              component={component}
-              history={history}
-              measures={measures}
-              onComponentChange={this.props.onComponentChange}
-            />
-          </div>
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
new file mode 100644 (file)
index 0000000..07d2201
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+ * 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 { uniq } from 'lodash';
+import QualityGate from '../qualityGate/QualityGate';
+import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate';
+import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities';
+import CodeSmells from '../main/CodeSmells';
+import Coverage from '../main/Coverage';
+import Duplications from '../main/Duplications';
+import Meta from '../meta/Meta';
+import throwGlobalError from '../../../app/utils/throwGlobalError';
+import { getMeasuresAndMeta } from '../../../api/measures';
+import { getAllTimeMachineData, History } from '../../../api/time-machine';
+import { parseDate } from '../../../helpers/dates';
+import { enhanceMeasuresWithMetrics, MeasureEnhanced } from '../../../helpers/measures';
+import { getLeakPeriod, Period } from '../../../helpers/periods';
+import { getCustomGraph, getGraph } from '../../../helpers/storage';
+import { METRICS, HISTORY_METRICS_LIST } from '../utils';
+import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils';
+import { getBranchName } from '../../../helpers/branches';
+import { Branch, Component } from '../../../app/types';
+import '../styles.css';
+
+interface Props {
+  branch?: Branch;
+  component: Component;
+  onComponentChange: (changes: {}) => void;
+}
+
+interface State {
+  history?: History;
+  historyStartDate?: Date;
+  loading: boolean;
+  measures: MeasureEnhanced[];
+  periods?: Period[];
+}
+
+export default class OverviewApp extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { loading: true, measures: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadMeasures().then(this.loadHistory, () => {});
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (
+      this.props.component.key !== prevProps.component.key ||
+      this.props.branch !== prevProps.branch
+    ) {
+      this.loadMeasures().then(this.loadHistory, () => {});
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  loadMeasures() {
+    const { branch, component } = this.props;
+    this.setState({ loading: true });
+
+    return getMeasuresAndMeta(component.key, METRICS, {
+      additionalFields: 'metrics,periods',
+      branch: getBranchName(branch)
+    }).then(
+      r => {
+        if (this.mounted && r.metrics) {
+          this.setState({
+            loading: false,
+            measures: enhanceMeasuresWithMetrics(r.component.measures, r.metrics),
+            periods: r.periods
+          });
+        }
+      },
+      error => {
+        throwGlobalError(error);
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  }
+
+  loadHistory = () => {
+    const { branch, component } = this.props;
+
+    let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
+    if (!graphMetrics || graphMetrics.length <= 0) {
+      graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
+    }
+
+    const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics));
+    return getAllTimeMachineData(component.key, metrics, { branch: getBranchName(branch) }).then(
+      r => {
+        if (this.mounted) {
+          const history: History = {};
+          r.measures.forEach(measure => {
+            const measureHistory = measure.history.map(analysis => ({
+              date: parseDate(analysis.date),
+              value: analysis.value
+            }));
+            history[measure.metric] = measureHistory;
+          });
+          const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date;
+          this.setState({ history, historyStartDate });
+        }
+      }
+    );
+  };
+
+  getApplicationLeakPeriod = () =>
+    this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null;
+
+  renderLoading() {
+    return (
+      <div className="text-center">
+        <i className="spinner spinner-margin" />
+      </div>
+    );
+  }
+
+  render() {
+    const { branch, component } = this.props;
+    const { loading, measures, periods, history, historyStartDate } = this.state;
+
+    if (loading) {
+      return this.renderLoading();
+    }
+
+    const leakPeriod =
+      component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods);
+    const branchName = getBranchName(branch);
+    const domainProps = {
+      branch: branchName,
+      component,
+      measures,
+      leakPeriod,
+      history,
+      historyStartDate
+    };
+
+    return (
+      <div className="page page-limited">
+        <div className="overview page-with-sidebar">
+          <div className="overview-main page-main">
+            {component.qualifier === 'APP' ? (
+              <ApplicationQualityGate component={component} />
+            ) : (
+              <QualityGate branch={branchName} component={component} measures={measures} />
+            )}
+
+            <div className="overview-domains-list">
+              <BugsAndVulnerabilities {...domainProps} />
+              <CodeSmells {...domainProps} />
+              <Coverage {...domainProps} />
+              <Duplications {...domainProps} />
+            </div>
+          </div>
+
+          <div className="page-sidebar-fixed">
+            <Meta
+              branch={branchName}
+              component={component}
+              history={history}
+              measures={measures}
+              onComponentChange={this.props.onComponentChange}
+            />
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/Timeline.js b/server/sonar-web/src/main/js/apps/overview/components/Timeline.js
deleted file mode 100644 (file)
index 15dde52..0000000
+++ /dev/null
@@ -1,68 +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 PropTypes from 'prop-types';
-import { max } from 'd3-array';
-import { LineChart } from '../../../components/charts/line-chart';
-
-const HEIGHT = 80;
-
-export default class Timeline extends React.PureComponent {
-  static propTypes = {
-    history: PropTypes.arrayOf(PropTypes.object).isRequired,
-    before: PropTypes.object,
-    after: PropTypes.object
-  };
-
-  filterSnapshots() {
-    const { history, before, after } = this.props;
-
-    return history.filter(s => {
-      const matchBefore = !before || s.date <= before;
-      const matchAfter = !after || s.date >= after;
-      return matchBefore && matchAfter;
-    });
-  }
-
-  render() {
-    const snapshots = this.filterSnapshots();
-
-    if (snapshots.length < 2) {
-      return null;
-    }
-
-    const data = snapshots.map((snapshot, index) => {
-      return { x: index, y: snapshot.value };
-    });
-    const domain = [0, max(this.props.history, d => parseFloat(d.value))];
-    return (
-      <LineChart
-        data={data}
-        domain={domain}
-        interpolate="basis"
-        displayBackdrop={true}
-        displayPoints={false}
-        displayVerticalGrid={false}
-        height={HEIGHT}
-        padding={[0, 0, 0, 0]}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/Timeline.tsx b/server/sonar-web/src/main/js/apps/overview/components/Timeline.tsx
new file mode 100644 (file)
index 0000000..1b2bd3d
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 { max } from 'd3-array';
+import { LineChart } from '../../../components/charts/line-chart';
+import { HistoryItem } from '../../../api/time-machine';
+
+const HEIGHT = 80;
+
+interface Props {
+  history: HistoryItem[];
+  before?: Date;
+  after?: Date;
+}
+
+export default class Timeline extends React.PureComponent<Props> {
+  filterSnapshots() {
+    const { history, before, after } = this.props;
+
+    return history.filter(s => {
+      const matchBefore = !before || s.date <= before;
+      const matchAfter = !after || s.date >= after;
+      return matchBefore && matchAfter;
+    });
+  }
+
+  render() {
+    const snapshots = this.filterSnapshots();
+
+    if (snapshots.length < 2) {
+      return null;
+    }
+
+    const data = snapshots.map((snapshot, index) => {
+      return { x: index, y: snapshot.value };
+    });
+    const domain = [0, max(this.props.history, d => parseFloat(d.value))];
+    return (
+      <LineChart
+        data={data}
+        domain={domain}
+        interpolate="basis"
+        displayBackdrop={true}
+        displayPoints={false}
+        displayVerticalGrid={false}
+        height={HEIGHT}
+        padding={[0, 0, 0, 0]}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
new file mode 100644 (file)
index 0000000..4edd63f
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 { mount, shallow } from 'enzyme';
+import App from '../App';
+import OverviewApp from '../OverviewApp';
+import EmptyOverview from '../EmptyOverview';
+import { BranchType, LongLivingBranch } from '../../../../app/types';
+
+const component = {
+  key: 'foo',
+  analysisDate: '2016-01-01',
+  breadcrumbs: [],
+  name: 'Foo',
+  organization: 'org',
+  qualifier: 'TRK',
+  version: '0.0.1'
+};
+
+it('should render OverviewApp', () => {
+  expect(getWrapper().type()).toBe(OverviewApp);
+});
+
+it('should render EmptyOverview', () => {
+  const output = getWrapper({ component: { key: 'foo' } });
+  expect(output.type()).toBe(EmptyOverview);
+});
+
+it('redirects on Code page for files', () => {
+  const branch: LongLivingBranch = { isMain: false, name: 'b', type: BranchType.LONG };
+  const newComponent = {
+    ...component,
+    breadcrumbs: [
+      { key: 'project', name: 'Project', qualifier: 'TRK' },
+      { key: 'foo', name: 'Foo', qualifier: 'DIR' }
+    ],
+    qualifier: 'FIL'
+  };
+  const replace = jest.fn();
+  mount(<App branch={branch} component={newComponent} onComponentChange={jest.fn()} />, {
+    context: { router: { replace } }
+  });
+  expect(replace).toBeCalledWith({
+    pathname: '/code',
+    query: { branch: 'b', id: 'project', selected: 'foo' }
+  });
+});
+
+function getWrapper(props = {}) {
+  return shallow(<App component={component} onComponentChange={jest.fn()} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
deleted file mode 100644 (file)
index bff7894..0000000
+++ /dev/null
@@ -1,155 +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 Analysis from './Analysis';
-import { getAllMetrics } from '../../../api/metrics';
-import { getProjectActivity } from '../../../api/projectActivity';
-import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Analysis as AnalysisType } from '../../projectActivity/types'; */
-/*:: import type { History, Metric } from '../types'; */
-
-/*::
-type Props = {
-  branch?: string,
-  component: Object,
-  history: ?History,
-  qualifier: string
-};
-*/
-
-/*::
-type State = {
-  analyses: Array<AnalysisType>,
-  loading: boolean,
-  metrics: Array<Metric>
-};
-*/
-
-const PAGE_SIZE = 3;
-
-export default class AnalysesList extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  state /*: State */ = { analyses: [], loading: true, metrics: [] };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchData();
-  }
-
-  componentDidUpdate(prevProps /*: Props */) {
-    if (prevProps.component !== this.props.component) {
-      this.fetchData();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  getTopLevelComponent = () => {
-    const { component } = this.props;
-    let current = component.breadcrumbs.length - 1;
-    while (
-      current > 0 &&
-      !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
-    ) {
-      current--;
-    }
-    return component.breadcrumbs[current].key;
-  };
-
-  fetchData() {
-    this.setState({ loading: true });
-    Promise.all([
-      getProjectActivity({
-        branch: this.props.branch,
-        project: this.getTopLevelComponent(),
-        ps: PAGE_SIZE
-      }),
-      getAllMetrics()
-    ]).then(
-      response => {
-        if (this.mounted) {
-          this.setState({
-            analyses: response[0].analyses,
-            metrics: response[1],
-            loading: false
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  }
-
-  renderList(analyses /*: Array<AnalysisType> */) {
-    if (!analyses.length) {
-      return <p className="spacer-top note">{translate('no_results')}</p>;
-    }
-
-    return (
-      <ul className="spacer-top">
-        {analyses.map(analysis => (
-          <Analysis key={analysis.key} analysis={analysis} qualifier={this.props.qualifier} />
-        ))}
-      </ul>
-    );
-  }
-
-  render() {
-    const { analyses, loading } = this.state;
-
-    if (loading) {
-      return null;
-    }
-
-    return (
-      <div className="overview-meta-card">
-        <h4 className="overview-meta-header">{translate('project_activity.page')}</h4>
-
-        <PreviewGraph
-          branch={this.props.branch}
-          history={this.props.history}
-          project={this.props.component.key}
-          metrics={this.state.metrics}
-        />
-
-        {this.renderList(analyses)}
-
-        <div className="spacer-top small">
-          <Link
-            to={{
-              pathname: '/project/activity',
-              query: { id: this.props.component.key, branch: this.props.branch }
-            }}>
-            {translate('show_more')}
-          </Link>
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx
new file mode 100644 (file)
index 0000000..30a3d98
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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 Analysis from './Analysis';
+import { getAllMetrics } from '../../../api/metrics';
+import { getProjectActivity, Analysis as IAnalysis } from '../../../api/projectActivity';
+import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
+import { translate } from '../../../helpers/l10n';
+import { Metric, Component } from '../../../app/types';
+import { History } from '../../../api/time-machine';
+
+interface Props {
+  branch?: string;
+  component: Component;
+  history?: History;
+  qualifier: string;
+}
+
+interface State {
+  analyses: IAnalysis[];
+  loading: boolean;
+  metrics: Metric[];
+}
+
+const PAGE_SIZE = 3;
+
+export default class AnalysesList extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { analyses: [], loading: true, metrics: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchData();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.component !== this.props.component) {
+      this.fetchData();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  getTopLevelComponent = () => {
+    const { component } = this.props;
+    let current = component.breadcrumbs.length - 1;
+    while (
+      current > 0 &&
+      !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
+    ) {
+      current--;
+    }
+    return component.breadcrumbs[current].key;
+  };
+
+  fetchData = () => {
+    this.setState({ loading: true });
+    Promise.all([
+      getProjectActivity({
+        branch: this.props.branch,
+        project: this.getTopLevelComponent(),
+        ps: PAGE_SIZE
+      }),
+      getAllMetrics()
+    ]).then(
+      ([{ analyses }, metrics]) => {
+        if (this.mounted) {
+          this.setState({ analyses, metrics, loading: false });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  renderList(analyses: IAnalysis[]) {
+    if (!analyses.length) {
+      return <p className="spacer-top note">{translate('no_results')}</p>;
+    }
+
+    return (
+      <ul className="spacer-top">
+        {analyses.map(analysis => (
+          <Analysis key={analysis.key} analysis={analysis} qualifier={this.props.qualifier} />
+        ))}
+      </ul>
+    );
+  }
+
+  render() {
+    const { analyses, loading } = this.state;
+
+    if (loading) {
+      return null;
+    }
+
+    return (
+      <div className="overview-meta-card">
+        <h4 className="overview-meta-header">{translate('project_activity.page')}</h4>
+
+        <PreviewGraph
+          branch={this.props.branch}
+          history={this.props.history}
+          project={this.props.component.key}
+          metrics={this.state.metrics}
+        />
+
+        {this.renderList(analyses)}
+
+        <div className="spacer-top small">
+          <Link
+            to={{
+              pathname: '/project/activity',
+              query: { id: this.props.component.key, branch: this.props.branch }
+            }}>
+            {translate('show_more')}
+          </Link>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js
deleted file mode 100644 (file)
index 3addf41..0000000
+++ /dev/null
@@ -1,65 +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 { sortBy } from 'lodash';
-import Event from './Event';
-import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Analysis as AnalysisType, Event as EventType } from '../../projectActivity/types'; */
-
-/*::
-type Props = {
-  analysis: AnalysisType,
-  qualifier: string
-};
-*/
-
-export default function Analysis(props /*: Props */) {
-  const { analysis } = props;
-  const sortedEvents /*: Array<EventType> */ = sortBy(
-    analysis.events,
-    // versions first
-    (event /*: EventType */) => (event.category === 'VERSION' ? 0 : 1),
-    // then the rest sorted by category
-    'category'
-  );
-
-  // use `TRK` for all components but applications
-  const qualifier = props.qualifier === 'APP' ? 'APP' : 'TRK';
-
-  return (
-    <li className="overview-analysis">
-      <div className="small little-spacer-bottom">
-        <strong>
-          <DateTooltipFormatter date={analysis.date} placement="right" />
-        </strong>
-      </div>
-
-      {sortedEvents.length > 0 ? (
-        <div className="project-activity-events">
-          {sortedEvents.map(event => <Event event={event} key={event.key} />)}
-        </div>
-      ) : (
-        <span className="note">{translate('project_activity.analyzed', qualifier)}</span>
-      )}
-    </li>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.tsx b/server/sonar-web/src/main/js/apps/overview/events/Analysis.tsx
new file mode 100644 (file)
index 0000000..f80ce56
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * 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 { sortBy } from 'lodash';
+import Event from './Event';
+import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter';
+import { Analysis as IAnalysis, Event as IEvent } from '../../../api/projectActivity';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  analysis: IAnalysis;
+  qualifier: string;
+}
+
+export default function Analysis({ analysis, ...props }: Props) {
+  const sortedEvents: Array<IEvent> = sortBy(
+    analysis.events,
+    // versions first
+    (event: IEvent) => (event.category === 'VERSION' ? 0 : 1),
+    // then the rest sorted by category
+    'category'
+  );
+
+  // use `TRK` for all components but applications
+  const qualifier = props.qualifier === 'APP' ? 'APP' : 'TRK';
+
+  return (
+    <li className="overview-analysis">
+      <div className="small little-spacer-bottom">
+        <strong>
+          <DateTooltipFormatter date={analysis.date} placement="right" />
+        </strong>
+      </div>
+
+      {sortedEvents.length > 0 ? (
+        <div className="project-activity-events">
+          {sortedEvents.map(event => <Event event={event} key={event.key} />)}
+        </div>
+      ) : (
+        <span className="note">{translate('project_activity.analyzed', qualifier)}</span>
+      )}
+    </li>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.js b/server/sonar-web/src/main/js/apps/overview/events/Event.js
deleted file mode 100644 (file)
index ca2982c..0000000
+++ /dev/null
@@ -1,49 +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 Tooltip from '../../../components/controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Event as EventType } from '../../projectActivity/types'; */
-
-export default function Event(props /*: { event: EventType } */) {
-  const { event } = props;
-
-  if (event.category === 'VERSION') {
-    return (
-      <Tooltip overlay={`${translate('version')} ${props.event.name}`} mouseEnterDelay={0.5}>
-        <span className="overview-analysis-event badge">{props.event.name}</span>
-      </Tooltip>
-    );
-  }
-
-  return (
-    <div className="overview-analysis-event">
-      <span className="note">{translate('event.category', event.category)}:</span>{' '}
-      {event.description ? (
-        <Tooltip overlay={event.description} placement="left" mouseEnterDelay={0.5}>
-          <strong>{event.name}</strong>
-        </Tooltip>
-      ) : (
-        <strong>{event.name}</strong>
-      )}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.tsx b/server/sonar-web/src/main/js/apps/overview/events/Event.tsx
new file mode 100644 (file)
index 0000000..f90d318
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 Tooltip from '../../../components/controls/Tooltip';
+import { Event as IEvent } from '../../../api/projectActivity';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  event: IEvent;
+}
+
+export default function Event({ event }: Props) {
+  if (event.category === 'VERSION') {
+    return (
+      <Tooltip overlay={`${translate('version')} ${event.name}`} mouseEnterDelay={0.5}>
+        <span className="overview-analysis-event badge">{event.name}</span>
+      </Tooltip>
+    );
+  }
+
+  return (
+    <div className="overview-analysis-event">
+      <span className="note">{translate('event.category', event.category)}:</span>{' '}
+      {event.description ? (
+        <Tooltip overlay={event.description} placement="left" mouseEnterDelay={0.5}>
+          <strong>{event.name}</strong>
+        </Tooltip>
+      ) : (
+        <strong>{event.name}</strong>
+      )}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js
deleted file mode 100644 (file)
index 7c6fdb4..0000000
+++ /dev/null
@@ -1,35 +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 Analysis from '../Analysis';
-
-const ANALYSIS = {
-  key: '1',
-  date: '2017-06-10T16:10:59+0200',
-  events: [
-    { key: '1', category: 'OTHER', name: 'test' },
-    { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' }
-  ]
-};
-
-it('should sort the events with version first', () => {
-  expect(shallow(<Analysis analysis={ANALYSIS} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.tsx b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.tsx
new file mode 100644 (file)
index 0000000..16c481b
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 Analysis from '../Analysis';
+
+const ANALYSIS = {
+  key: '1',
+  date: '2017-06-10T16:10:59+0200',
+  events: [
+    { key: '1', category: 'OTHER', name: 'test' },
+    { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' }
+  ]
+};
+
+it('should sort the events with version first', () => {
+  expect(shallow(<Analysis analysis={ANALYSIS} qualifier="TRK" />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js
deleted file mode 100644 (file)
index 496bf25..0000000
+++ /dev/null
@@ -1,33 +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 Event from '../Event';
-
-const EVENT = { key: '1', category: 'OTHER', name: 'test' };
-const VERSION = { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' };
-
-it('should render an event correctly', () => {
-  expect(shallow(<Event event={EVENT} />)).toMatchSnapshot();
-});
-
-it('should render a version correctly', () => {
-  expect(shallow(<Event event={VERSION} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.tsx b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.tsx
new file mode 100644 (file)
index 0000000..e15be76
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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 Event from '../Event';
+
+const EVENT = { key: '1', category: 'OTHER', name: 'test' };
+const VERSION = { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' };
+
+it('should render an event correctly', () => {
+  expect(shallow(<Event event={EVENT} />)).toMatchSnapshot();
+});
+
+it('should render a version correctly', () => {
+  expect(shallow(<Event event={VERSION} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap
deleted file mode 100644 (file)
index a929618..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should sort the events with version first 1`] = `
-<li
-  className="overview-analysis"
->
-  <div
-    className="small little-spacer-bottom"
-  >
-    <strong>
-      <DateTooltipFormatter
-        date="2017-06-10T16:10:59+0200"
-        placement="right"
-      />
-    </strong>
-  </div>
-  <div
-    className="project-activity-events"
-  >
-    <Event
-      event={
-        Object {
-          "category": "VERSION",
-          "key": "2",
-          "name": "6.5-SNAPSHOT",
-        }
-      }
-      key="2"
-    />
-    <Event
-      event={
-        Object {
-          "category": "OTHER",
-          "key": "1",
-          "name": "test",
-        }
-      }
-      key="1"
-    />
-  </div>
-</li>
-`;
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.tsx.snap
new file mode 100644 (file)
index 0000000..a929618
--- /dev/null
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should sort the events with version first 1`] = `
+<li
+  className="overview-analysis"
+>
+  <div
+    className="small little-spacer-bottom"
+  >
+    <strong>
+      <DateTooltipFormatter
+        date="2017-06-10T16:10:59+0200"
+        placement="right"
+      />
+    </strong>
+  </div>
+  <div
+    className="project-activity-events"
+  >
+    <Event
+      event={
+        Object {
+          "category": "VERSION",
+          "key": "2",
+          "name": "6.5-SNAPSHOT",
+        }
+      }
+      key="2"
+    />
+    <Event
+      event={
+        Object {
+          "category": "OTHER",
+          "key": "1",
+          "name": "test",
+        }
+      }
+      key="1"
+    />
+  </div>
+</li>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap
deleted file mode 100644 (file)
index f6b5bdf..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render a version correctly 1`] = `
-<Tooltip
-  mouseEnterDelay={0.5}
-  overlay="version 6.5-SNAPSHOT"
-  placement="bottom"
->
-  <span
-    className="overview-analysis-event badge"
-  >
-    6.5-SNAPSHOT
-  </span>
-</Tooltip>
-`;
-
-exports[`should render an event correctly 1`] = `
-<div
-  className="overview-analysis-event"
->
-  <span
-    className="note"
-  >
-    event.category.OTHER
-    :
-  </span>
-   
-  <strong>
-    test
-  </strong>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.tsx.snap
new file mode 100644 (file)
index 0000000..f6b5bdf
--- /dev/null
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render a version correctly 1`] = `
+<Tooltip
+  mouseEnterDelay={0.5}
+  overlay="version 6.5-SNAPSHOT"
+  placement="bottom"
+>
+  <span
+    className="overview-analysis-event badge"
+  >
+    6.5-SNAPSHOT
+  </span>
+</Tooltip>
+`;
+
+exports[`should render an event correctly 1`] = `
+<div
+  className="overview-analysis-event"
+>
+  <span
+    className="note"
+  >
+    event.category.OTHER
+    :
+  </span>
+   
+  <strong>
+    test
+  </strong>
+</div>
+`;
index d8f4a118c43fdd35314b4e10cc2f6ec8cbbc6d0b..ecc0874352784f4fb6b0ec843a93bf5dbd2da14d 100644 (file)
@@ -19,7 +19,7 @@
  */
 import React from 'react';
 import enhance from './enhance';
-import { DrilldownLink } from '../../../components/shared/drilldown-link';
+import DrilldownLink from '../../../components/shared/DrilldownLink';
 import { getMetricName } from '../helpers/metrics';
 import { formatMeasure, getPeriodValue } from '../../../helpers/measures';
 import { translate } from '../../../helpers/l10n';
index 3a04c3cc306de7be9422bc68aa3f95c7e973fee3..e668ff8efc2b023d94ad8649abc7e3a10a964fe5 100644 (file)
@@ -19,7 +19,7 @@
  */
 import React from 'react';
 import enhance from './enhance';
-import { DrilldownLink } from '../../../components/shared/drilldown-link';
+import DrilldownLink from '../../../components/shared/DrilldownLink';
 import { getMetricName } from '../helpers/metrics';
 import { formatMeasure, getPeriodValue } from '../../../helpers/measures';
 import { translate } from '../../../helpers/l10n';
diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js
deleted file mode 100644 (file)
index 8aac3e3..0000000
+++ /dev/null
@@ -1,219 +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 { Link } from 'react-router';
-import { DrilldownLink } from '../../../components/shared/drilldown-link';
-import BubblesIcon from '../../../components/icons-components/BubblesIcon';
-import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
-import HistoryIcon from '../../../components/icons-components/HistoryIcon';
-import Rating from './../../../components/ui/Rating';
-import Timeline from '../components/Timeline';
-import Tooltip from '../../../components/controls/Tooltip';
-import {
-  formatMeasure,
-  formatMeasureVariation,
-  isDiffMetric,
-  getPeriodValue,
-  getShortType,
-  getRatingTooltip
-} from '../../../helpers/measures';
-import { translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n';
-import { getPeriodDate } from '../../../helpers/periods';
-import {
-  getComponentDrilldownUrl,
-  getComponentIssuesUrl,
-  getMeasureHistoryUrl
-} from '../../../helpers/urls';
-
-export default function enhance(ComposedComponent) {
-  return class extends React.PureComponent {
-    static displayName = `enhance(${ComposedComponent.displayName})}`;
-
-    getValue = measure => {
-      const { leakPeriod } = this.props;
-
-      if (!measure) {
-        return 0;
-      }
-
-      return isDiffMetric(measure.metric.key)
-        ? getPeriodValue(measure, leakPeriod.index)
-        : measure.value;
-    };
-
-    renderHeader = (domain, label) => {
-      const { branch, component } = this.props;
-      return (
-        <div className="overview-card-header">
-          <div className="overview-title">
-            <span>{label}</span>
-            <Link
-              className="button button-small spacer-left text-text-bottom"
-              to={getComponentDrilldownUrl(component.key, domain, branch)}>
-              <BubblesIcon size={14} />
-            </Link>
-          </div>
-        </div>
-      );
-    };
-
-    renderMeasure = metricKey => {
-      const { branch, measures, component } = this.props;
-      const measure = measures.find(measure => measure.metric.key === metricKey);
-
-      if (measure == null) {
-        return null;
-      }
-
-      return (
-        <div className="overview-domain-measure">
-          <div className="overview-domain-measure-value">
-            <DrilldownLink branch={branch} component={component.key} metric={metricKey}>
-              <span className="js-overview-main-tests">
-                {formatMeasure(measure.value, getShortType(measure.metric.type))}
-              </span>
-            </DrilldownLink>
-          </div>
-
-          <div className="overview-domain-measure-label offset-left">
-            {getLocalizedMetricName(measure.metric)}
-            {this.renderHistoryLink(measure.metric.key)}
-          </div>
-        </div>
-      );
-    };
-
-    renderMeasureVariation = (metricKey, customLabel) => {
-      const NO_VALUE = '—';
-      const { measures, leakPeriod } = this.props;
-      const measure = measures.find(measure => measure.metric.key === metricKey);
-      const periodValue = getPeriodValue(measure, leakPeriod.index);
-      const formatted =
-        periodValue != null
-          ? formatMeasureVariation(periodValue, getShortType(measure.metric.type))
-          : NO_VALUE;
-      return (
-        <div className="overview-domain-measure">
-          <div className="overview-domain-measure-value">{formatted}</div>
-
-          <div className="overview-domain-measure-label">{customLabel || measure.metric.name}</div>
-        </div>
-      );
-    };
-
-    renderRating = metricKey => {
-      const { branch, component, measures } = this.props;
-      const measure = measures.find(measure => measure.metric.key === metricKey);
-      if (!measure) {
-        return null;
-      }
-      const value = this.getValue(measure);
-      const title = getRatingTooltip(metricKey, value);
-      return (
-        <Tooltip overlay={title} placement="top">
-          <div className="overview-domain-measure-sup">
-            <DrilldownLink
-              branch={branch}
-              className="link-no-underline"
-              component={component.key}
-              metric={metricKey}>
-              <Rating value={value} />
-            </DrilldownLink>
-          </div>
-        </Tooltip>
-      );
-    };
-
-    renderIssues = (metric, type) => {
-      const { branch, measures, component } = this.props;
-      const measure = measures.find(measure => measure.metric.key === metric);
-      const value = this.getValue(measure);
-      const params = { branch, resolved: 'false', types: type };
-      if (isDiffMetric(metric)) {
-        Object.assign(params, { sinceLeakPeriod: 'true' });
-      }
-
-      const tooltip = (
-        <DateTimeFormatter date={component.analysisDate}>
-          {formattedAnalysisDate => (
-            <span>
-              {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)}
-            </span>
-          )}
-        </DateTimeFormatter>
-      );
-
-      return (
-        <Tooltip overlay={tooltip} placement="top">
-          <Link to={getComponentIssuesUrl(component.key, params)}>
-            {formatMeasure(value, 'SHORT_INT')}
-          </Link>
-        </Tooltip>
-      );
-    };
-
-    renderHistoryLink = metricKey => {
-      const linkClass = 'button button-small spacer-left overview-domain-measure-history-link';
-      return (
-        <Link
-          className={linkClass}
-          to={getMeasureHistoryUrl(this.props.component.key, metricKey, this.props.branch)}>
-          <HistoryIcon />
-        </Link>
-      );
-    };
-
-    renderTimeline = (metricKey, range, children) => {
-      if (!this.props.history) {
-        return null;
-      }
-      const history = this.props.history[metricKey];
-      if (!history) {
-        return null;
-      }
-      const props = {
-        history,
-        [range]: getPeriodDate(this.props.leakPeriod)
-      };
-      return (
-        <div className="overview-domain-timeline">
-          <Timeline {...props} />
-          {children}
-        </div>
-      );
-    };
-
-    render() {
-      return (
-        <ComposedComponent
-          {...this.props}
-          getValue={this.getValue}
-          renderHeader={this.renderHeader}
-          renderHistoryLink={this.renderHistoryLink}
-          renderMeasure={this.renderMeasure}
-          renderMeasureVariation={this.renderMeasureVariation}
-          renderRating={this.renderRating}
-          renderIssues={this.renderIssues}
-          renderTimeline={this.renderTimeline}
-        />
-      );
-    }
-  };
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx b/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx
new file mode 100644 (file)
index 0000000..142cdf3
--- /dev/null
@@ -0,0 +1,220 @@
+/*
+ * 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 DrilldownLink from '../../../components/shared/DrilldownLink';
+import BubblesIcon from '../../../components/icons-components/BubblesIcon';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
+import HistoryIcon from '../../../components/icons-components/HistoryIcon';
+import Rating from './../../../components/ui/Rating';
+import Timeline from '../components/Timeline';
+import Tooltip from '../../../components/controls/Tooltip';
+import {
+  formatMeasure,
+  isDiffMetric,
+  getPeriodValue,
+  getShortType,
+  getRatingTooltip,
+  MeasureEnhanced
+} from '../../../helpers/measures';
+import { translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n';
+import { getPeriodDate } from '../../../helpers/periods';
+import {
+  getComponentDrilldownUrl,
+  getComponentIssuesUrl,
+  getMeasureHistoryUrl
+} from '../../../helpers/urls';
+import { Component } from '../../../app/types';
+import { History } from '../../../api/time-machine';
+
+export interface EnhanceProps {
+  branch?: string;
+  component: Component;
+  measures: MeasureEnhanced[];
+  leakPeriod?: { index: number; date?: string };
+  history?: History;
+  historyStartDate?: Date;
+}
+
+export interface ComposedProps extends EnhanceProps {
+  getValue: (measure: MeasureEnhanced) => string | undefined;
+  renderHeader: (domain: string, label: string) => React.ReactNode;
+  renderMeasure: (metricKey: string) => React.ReactNode;
+  renderRating: (metricKey: string) => React.ReactNode;
+  renderIssues: (metric: string, type: string) => React.ReactNode;
+  renderHistoryLink: (metricKey: string) => React.ReactNode;
+  renderTimeline: (metricKey: string, range: string, children?: React.ReactNode) => React.ReactNode;
+}
+
+export default function enhance(ComposedComponent: React.ComponentType<ComposedProps>) {
+  return class extends React.PureComponent<EnhanceProps> {
+    static displayName = `enhance(${ComposedComponent.displayName})}`;
+
+    getValue = (measure: MeasureEnhanced) => {
+      const { leakPeriod } = this.props;
+      if (!measure) {
+        return '0';
+      }
+      return isDiffMetric(measure.metric.key)
+        ? getPeriodValue(measure, leakPeriod ? leakPeriod.index : 0)
+        : measure.value;
+    };
+
+    renderHeader = (domain: string, label: string) => {
+      const { branch, component } = this.props;
+      return (
+        <div className="overview-card-header">
+          <div className="overview-title">
+            <span>{label}</span>
+            <Link
+              className="button button-small spacer-left text-text-bottom"
+              to={getComponentDrilldownUrl(component.key, domain, branch)}>
+              <BubblesIcon size={14} />
+            </Link>
+          </div>
+        </div>
+      );
+    };
+
+    renderMeasure = (metricKey: string) => {
+      const { branch, measures, component } = this.props;
+      const measure = measures.find(measure => measure.metric.key === metricKey);
+      if (!measure) {
+        return null;
+      }
+
+      return (
+        <div className="overview-domain-measure">
+          <div className="overview-domain-measure-value">
+            <DrilldownLink branch={branch} component={component.key} metric={metricKey}>
+              <span className="js-overview-main-tests">
+                {formatMeasure(measure.value, getShortType(measure.metric.type))}
+              </span>
+            </DrilldownLink>
+          </div>
+
+          <div className="overview-domain-measure-label offset-left">
+            {getLocalizedMetricName(measure.metric)}
+            {this.renderHistoryLink(measure.metric.key)}
+          </div>
+        </div>
+      );
+    };
+
+    renderRating = (metricKey: string) => {
+      const { branch, component, measures } = this.props;
+      const measure = measures.find(measure => measure.metric.key === metricKey);
+      if (!measure) {
+        return null;
+      }
+
+      const value = this.getValue(measure);
+      const title = value && getRatingTooltip(metricKey, value);
+      return (
+        <Tooltip overlay={title} placement="top">
+          <div className="overview-domain-measure-sup">
+            <DrilldownLink
+              branch={branch}
+              className="link-no-underline"
+              component={component.key}
+              metric={metricKey}>
+              <Rating value={value} />
+            </DrilldownLink>
+          </div>
+        </Tooltip>
+      );
+    };
+
+    renderIssues = (metric: string, type: string) => {
+      const { branch, measures, component } = this.props;
+      const measure = measures.find(measure => measure.metric.key === metric);
+      if (!measure) {
+        return null;
+      }
+
+      const value = this.getValue(measure);
+      const params = { branch, resolved: 'false', types: type };
+      if (isDiffMetric(metric)) {
+        Object.assign(params, { sinceLeakPeriod: 'true' });
+      }
+
+      const tooltip = component.analysisDate && (
+        <DateTimeFormatter date={component.analysisDate}>
+          {formattedAnalysisDate => (
+            <span>
+              {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)}
+            </span>
+          )}
+        </DateTimeFormatter>
+      );
+
+      return (
+        <Tooltip overlay={tooltip} placement="top">
+          <Link to={getComponentIssuesUrl(component.key, params)}>
+            {formatMeasure(value, 'SHORT_INT')}
+          </Link>
+        </Tooltip>
+      );
+    };
+
+    renderHistoryLink = (metricKey: string) => {
+      const linkClass = 'button button-small spacer-left overview-domain-measure-history-link';
+      return (
+        <Link
+          className={linkClass}
+          to={getMeasureHistoryUrl(this.props.component.key, metricKey, this.props.branch)}>
+          <HistoryIcon />
+        </Link>
+      );
+    };
+
+    renderTimeline = (metricKey: string, range: 'before' | 'after', children?: React.ReactNode) => {
+      if (!this.props.history) {
+        return null;
+      }
+      const history = this.props.history[metricKey];
+      if (!history) {
+        return null;
+      }
+      const props = { history, [range]: getPeriodDate(this.props.leakPeriod) };
+      return (
+        <div className="overview-domain-timeline">
+          <Timeline {...props} />
+          {children}
+        </div>
+      );
+    };
+
+    render() {
+      return (
+        <ComposedComponent
+          {...this.props}
+          getValue={this.getValue}
+          renderHeader={this.renderHeader}
+          renderHistoryLink={this.renderHistoryLink}
+          renderMeasure={this.renderMeasure}
+          renderRating={this.renderRating}
+          renderIssues={this.renderIssues}
+          renderTimeline={this.renderTimeline}
+        />
+      );
+    }
+  };
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
deleted file mode 100644 (file)
index 0422440..0000000
+++ /dev/null
@@ -1,109 +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 { connect } from 'react-redux';
-import MetaKey from './MetaKey';
-import MetaOrganizationKey from './MetaOrganizationKey';
-import MetaLinks from './MetaLinks';
-import MetaQualityGate from './MetaQualityGate';
-import MetaQualityProfiles from './MetaQualityProfiles';
-import AnalysesList from '../events/AnalysesList';
-import MetaSize from './MetaSize';
-import MetaTags from './MetaTags';
-import BadgesModal from '../badges/BadgesModal';
-import { areThereCustomOrganizations, getGlobalSettingValue } from '../../../store/rootReducer';
-import { Visibility } from '../../../app/types';
-
-const Meta = ({
-  branch,
-  component,
-  history,
-  measures,
-  areThereCustomOrganizations,
-  onComponentChange,
-  onSonarCloud
-}) => {
-  const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;
-
-  const isProject = qualifier === 'TRK';
-  const isPrivate = visibility === Visibility.Private;
-
-  const hasDescription = !!description;
-  const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
-  const hasQualityGate = !!qualityGate;
-
-  const shouldShowQualityProfiles = isProject && hasQualityProfiles;
-  const shouldShowQualityGate = isProject && hasQualityGate;
-  const hasOrganization = component.organization != null && areThereCustomOrganizations;
-
-  return (
-    <div className="overview-meta">
-      {hasDescription && (
-        <div className="overview-meta-card overview-meta-description">{description}</div>
-      )}
-
-      <MetaSize branch={branch} component={component} measures={measures} />
-
-      {isProject && <MetaTags component={component} onComponentChange={onComponentChange} />}
-
-      <AnalysesList
-        branch={branch}
-        component={component}
-        qualifier={component.qualifier}
-        history={history}
-      />
-
-      {shouldShowQualityGate && (
-        <MetaQualityGate
-          gate={qualityGate}
-          organization={hasOrganization && component.organization}
-        />
-      )}
-
-      {shouldShowQualityProfiles && (
-        <MetaQualityProfiles
-          component={component}
-          customOrganizations={areThereCustomOrganizations}
-          profiles={qualityProfiles}
-        />
-      )}
-
-      {isProject && <MetaLinks component={component} />}
-
-      <MetaKey component={component} />
-
-      {hasOrganization && <MetaOrganizationKey component={component} />}
-
-      {onSonarCloud &&
-        isProject &&
-        !isPrivate && <BadgesModal branch={branch} project={component.key} />}
-    </div>
-  );
-};
-
-const mapStateToProps = state => {
-  const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');
-  return {
-    areThereCustomOrganizations: areThereCustomOrganizations(state),
-    onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true')
-  };
-};
-
-export default connect(mapStateToProps)(Meta);
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx
new file mode 100644 (file)
index 0000000..bfffecd
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * 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 { connect } from 'react-redux';
+import MetaKey from './MetaKey';
+import MetaOrganizationKey from './MetaOrganizationKey';
+import MetaLinks from './MetaLinks';
+import MetaQualityGate from './MetaQualityGate';
+import MetaQualityProfiles from './MetaQualityProfiles';
+import AnalysesList from '../events/AnalysesList';
+import MetaSize from './MetaSize';
+import MetaTags from './MetaTags';
+import BadgesModal from '../badges/BadgesModal';
+import { areThereCustomOrganizations, getGlobalSettingValue } from '../../../store/rootReducer';
+import { Visibility, Component } from '../../../app/types';
+import { History } from '../../../api/time-machine';
+import { MeasureEnhanced } from '../../../helpers/measures';
+
+interface OwnProps {
+  branch?: string;
+  component: Component;
+  history?: History;
+  measures: MeasureEnhanced[];
+  onComponentChange: (changes: {}) => void;
+}
+
+interface StateToProps {
+  areThereCustomOrganizations: boolean;
+  onSonarCloud: boolean;
+}
+
+export function Meta(props: OwnProps & StateToProps) {
+  const { branch, component, areThereCustomOrganizations } = props;
+  const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;
+
+  const isProject = qualifier === 'TRK';
+  const isPrivate = visibility === Visibility.Private;
+
+  const hasDescription = !!description;
+  const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
+  const hasQualityGate = !!qualityGate;
+
+  const shouldShowQualityProfiles = isProject && hasQualityProfiles;
+  const shouldShowQualityGate = isProject && hasQualityGate;
+  const hasOrganization = component.organization != null && areThereCustomOrganizations;
+
+  return (
+    <div className="overview-meta">
+      {hasDescription && (
+        <div className="overview-meta-card overview-meta-description">{description}</div>
+      )}
+
+      <MetaSize branch={branch} component={component} measures={props.measures} />
+
+      {isProject && <MetaTags component={component} onComponentChange={props.onComponentChange} />}
+
+      <AnalysesList
+        branch={branch}
+        component={component}
+        qualifier={component.qualifier}
+        history={props.history}
+      />
+
+      {shouldShowQualityGate && (
+        <MetaQualityGate
+          gate={qualityGate}
+          organization={hasOrganization && component.organization}
+        />
+      )}
+
+      {shouldShowQualityProfiles && (
+        <MetaQualityProfiles
+          component={component}
+          customOrganizations={areThereCustomOrganizations}
+          profiles={qualityProfiles}
+        />
+      )}
+
+      {isProject && <MetaLinks component={component} />}
+
+      <MetaKey component={component} />
+
+      {hasOrganization && <MetaOrganizationKey component={component} />}
+
+      {props.onSonarCloud &&
+        isProject &&
+        !isPrivate && <BadgesModal branch={branch} project={component.key} />}
+    </div>
+  );
+}
+
+const mapStateToProps = (state: any): StateToProps => {
+  const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');
+  return {
+    areThereCustomOrganizations: areThereCustomOrganizations(state),
+    onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true')
+  };
+};
+
+export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(Meta);
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.js
deleted file mode 100644 (file)
index a59e818..0000000
+++ /dev/null
@@ -1,96 +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 { isProvided, isClickable } from '../../project-admin/links/utils';
-import BugTrackerIcon from '../../../components/ui/BugTrackerIcon';
-
-/*::
-type Link = {
-  id: string,
-  name: string,
-  url: string,
-  type: string
-};
-*/
-
-/*::
-type State = {|
-  expanded: boolean
-|};
-*/
-
-export default class MetaLink extends React.PureComponent {
-  /*:: props: {
-    link: Link
-  };
-*/
-
-  state /*: State */ = {
-    expanded: false
-  };
-
-  handleClick = (e /*: Object */) => {
-    e.preventDefault();
-    e.target.blur();
-    this.setState((s /*: State */) => ({ expanded: !s.expanded }));
-  };
-
-  renderLinkIcon(link /*: Link */) {
-    if (link.type === 'issue') {
-      return <BugTrackerIcon />;
-    }
-
-    return isProvided(link) ? (
-      <i className={`icon-color-link icon-${link.type}`} />
-    ) : (
-      <i className="icon-color-link icon-detach" />
-    );
-  }
-
-  render() {
-    const { link } = this.props;
-
-    return (
-      <li>
-        <a
-          className="link-with-icon"
-          href={link.url}
-          target="_blank"
-          onClick={!isClickable(link) ? this.handleClick : undefined}>
-          {this.renderLinkIcon(link)}
-          &nbsp;
-          {link.name}
-        </a>
-        {this.state.expanded && (
-          <div className="little-spacer-top">
-            <input
-              type="text"
-              className="overview-key"
-              value={link.url}
-              readOnly={true}
-              onClick={e => e.target.select()}
-            />
-          </div>
-        )}
-      </li>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx
new file mode 100644 (file)
index 0000000..b704420
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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 { isProvided, isClickable } from '../../project-admin/links/utils';
+import BugTrackerIcon from '../../../components/ui/BugTrackerIcon';
+import { ProjectLink } from '../../../api/projectLinks';
+
+interface Props {
+  link: ProjectLink;
+}
+
+interface State {
+  expanded: boolean;
+}
+
+export default class MetaLink extends React.PureComponent<Props, State> {
+  state: State = { expanded: false };
+
+  handleClick = (e: React.SyntheticEvent<HTMLAnchorElement>) => {
+    e.preventDefault();
+    e.currentTarget.blur();
+    this.setState((s: State) => ({ expanded: !s.expanded }));
+  };
+
+  handleInputClick = (e: React.SyntheticEvent<HTMLInputElement>) => {
+    e.currentTarget.select();
+  };
+
+  renderLinkIcon = (link: ProjectLink) => {
+    if (link.type === 'issue') {
+      return <BugTrackerIcon />;
+    }
+
+    return isProvided(link) ? (
+      <i className={`icon-color-link icon-${link.type}`} />
+    ) : (
+      <i className="icon-color-link icon-detach" />
+    );
+  };
+
+  render() {
+    const { link } = this.props;
+
+    return (
+      <li>
+        <a
+          className="link-with-icon"
+          href={link.url}
+          target="_blank"
+          onClick={!isClickable(link) ? this.handleClick : undefined}>
+          {this.renderLinkIcon(link)}
+          &nbsp;
+          {link.name}
+        </a>
+        {this.state.expanded && (
+          <div className="little-spacer-top">
+            <input
+              type="text"
+              className="overview-key"
+              value={link.url}
+              readOnly={true}
+              onClick={this.handleInputClick}
+            />
+          </div>
+        )}
+      </li>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js
deleted file mode 100644 (file)
index 7e2ca4c..0000000
+++ /dev/null
@@ -1,73 +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 PropTypes from 'prop-types';
-import MetaLink from './MetaLink';
-import { getProjectLinks } from '../../../api/projectLinks';
-import { orderLinks } from '../../project-admin/links/utils';
-
-export default class MetaLinks extends React.PureComponent {
-  static propTypes = {
-    component: PropTypes.object.isRequired
-  };
-
-  state = {};
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadLinks();
-  }
-
-  componentDidUpdate(prevProps) {
-    if (prevProps.component.key !== this.props.component.key) {
-      this.loadLinks();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  loadLinks() {
-    getProjectLinks(this.props.component.key).then(links => {
-      if (this.mounted) {
-        this.setState({ links });
-      }
-    });
-  }
-
-  render() {
-    const { links } = this.state;
-
-    if (links == null || links.length === 0) {
-      return null;
-    }
-
-    const orderedLinks = orderLinks(links);
-
-    return (
-      <div className="overview-meta-card">
-        <ul className="overview-meta-list">
-          {orderedLinks.map(link => <MetaLink key={link.id} link={link} />)}
-        </ul>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx
new file mode 100644 (file)
index 0000000..66980dd
--- /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 MetaLink from './MetaLink';
+import { getProjectLinks, ProjectLink } from '../../../api/projectLinks';
+import { orderLinks } from '../../project-admin/links/utils';
+import { LightComponent } from '../../../app/types';
+
+interface Props {
+  component: LightComponent;
+}
+
+interface State {
+  links?: ProjectLink[];
+}
+
+export default class MetaLinks extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = {};
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadLinks();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.component.key !== this.props.component.key) {
+      this.loadLinks();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  loadLinks = () =>
+    getProjectLinks(this.props.component.key).then(
+      links => {
+        if (this.mounted) {
+          this.setState({ links });
+        }
+      },
+      () => {}
+    );
+
+  render() {
+    const { links } = this.state;
+
+    if (!links || links.length === 0) {
+      return null;
+    }
+
+    const orderedLinks = orderLinks(links);
+
+    return (
+      <div className="overview-meta-card">
+        <ul className="overview-meta-list">
+          {orderedLinks.map(link => <MetaLink key={link.id} link={link} />)}
+        </ul>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js
deleted file mode 100644 (file)
index 9d34ec9..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.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import { DrilldownLink } from '../../../components/shared/drilldown-link';
-import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
-import SizeRating from '../../../components/ui/SizeRating';
-import { formatMeasure } from '../../../helpers/measures';
-import { getMetricName } from '../helpers/metrics';
-import { translate } from '../../../helpers/l10n';
-
-export default class MetaSize extends React.PureComponent {
-  static propTypes = {
-    branch: PropTypes.string,
-    component: PropTypes.object.isRequired,
-    measures: PropTypes.array.isRequired
-  };
-
-  renderLoC = ncloc => (
-    <div
-      id="overview-ncloc"
-      className={classNames('overview-meta-size-ncloc', {
-        'is-half-width': this.props.component.qualifier === 'APP'
-      })}>
-      <span className="spacer-right">
-        <SizeRating value={ncloc.value} />
-      </span>
-      <DrilldownLink branch={this.props.branch} component={this.props.component.key} metric="ncloc">
-        {formatMeasure(ncloc.value, 'SHORT_INT')}
-      </DrilldownLink>
-      <div className="spacer-top text-muted">{getMetricName('ncloc')}</div>
-    </div>
-  );
-
-  renderLoCDistribution = () => {
-    const languageDistribution = this.props.measures.find(
-      measure => measure.metric.key === 'ncloc_language_distribution'
-    );
-
-    const className =
-      this.props.component.qualifier === 'TRK' ? 'overview-meta-size-lang-dist' : 'big-spacer-top';
-
-    return languageDistribution ? (
-      <div id="overview-language-distribution" className={className}>
-        <LanguageDistributionContainer distribution={languageDistribution.value} width={160} />
-      </div>
-    ) : null;
-  };
-
-  renderProjects = () => {
-    const projects = this.props.measures.find(measure => measure.metric.key === 'projects');
-
-    return projects ? (
-      <div id="overview-projects" className="overview-meta-size-ncloc is-half-width">
-        <DrilldownLink
-          branch={this.props.branch}
-          component={this.props.component.key}
-          metric="projects">
-          {formatMeasure(projects.value, 'SHORT_INT')}
-        </DrilldownLink>
-        <div className="spacer-top text-muted">{translate('metric.projects.name')}</div>
-      </div>
-    ) : null;
-  };
-
-  render() {
-    const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc');
-
-    if (ncloc == null) {
-      return null;
-    }
-
-    return (
-      <div id="overview-size" className="overview-meta-card">
-        {this.props.component.qualifier === 'APP' && this.renderProjects()}
-        {this.renderLoC(ncloc)}
-        {this.renderLoCDistribution()}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx
new file mode 100644 (file)
index 0000000..6465c86
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * 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 DrilldownLink from '../../../components/shared/DrilldownLink';
+import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
+import SizeRating from '../../../components/ui/SizeRating';
+import { formatMeasure, MeasureEnhanced } from '../../../helpers/measures';
+import { getMetricName } from '../helpers/metrics';
+import { translate } from '../../../helpers/l10n';
+import { LightComponent } from '../../../app/types';
+
+interface Props {
+  branch?: string;
+  component: LightComponent;
+  measures: MeasureEnhanced[];
+}
+
+export default class MetaSize extends React.PureComponent<Props> {
+  renderLoC = (ncloc: MeasureEnhanced) => (
+    <div
+      id="overview-ncloc"
+      className={classNames('overview-meta-size-ncloc', {
+        'is-half-width': this.props.component.qualifier === 'APP'
+      })}>
+      <span className="spacer-right">
+        <SizeRating value={Number(ncloc.value)} />
+      </span>
+      <DrilldownLink branch={this.props.branch} component={this.props.component.key} metric="ncloc">
+        {formatMeasure(ncloc.value, 'SHORT_INT')}
+      </DrilldownLink>
+      <div className="spacer-top text-muted">{getMetricName('ncloc')}</div>
+    </div>
+  );
+
+  renderLoCDistribution = () => {
+    const languageDistribution = this.props.measures.find(
+      measure => measure.metric.key === 'ncloc_language_distribution'
+    );
+
+    const className =
+      this.props.component.qualifier === 'TRK' ? 'overview-meta-size-lang-dist' : 'big-spacer-top';
+
+    return languageDistribution ? (
+      <div id="overview-language-distribution" className={className}>
+        <LanguageDistributionContainer distribution={languageDistribution.value} width={160} />
+      </div>
+    ) : null;
+  };
+
+  renderProjects = () => {
+    const projects = this.props.measures.find(measure => measure.metric.key === 'projects');
+
+    return projects ? (
+      <div id="overview-projects" className="overview-meta-size-ncloc is-half-width">
+        <DrilldownLink
+          branch={this.props.branch}
+          component={this.props.component.key}
+          metric="projects">
+          {formatMeasure(projects.value, 'SHORT_INT')}
+        </DrilldownLink>
+        <div className="spacer-top text-muted">{translate('metric.projects.name')}</div>
+      </div>
+    ) : null;
+  };
+
+  render() {
+    const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc');
+
+    if (ncloc == null) {
+      return null;
+    }
+
+    return (
+      <div id="overview-size" className="overview-meta-card">
+        {this.props.component.qualifier === 'APP' && this.renderProjects()}
+        {this.renderLoC(ncloc)}
+        {this.renderLoCDistribution()}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
deleted file mode 100644 (file)
index 409f304..0000000
+++ /dev/null
@@ -1,149 +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 { setProjectTags } from '../../../api/components';
-import { translate } from '../../../helpers/l10n';
-import TagsList from '../../../components/tags/TagsList';
-import MetaTagsSelector from './MetaTagsSelector';
-
-/*::
-type Props = {
-  component: {
-    key: string,
-    tags: Array<string>,
-    configuration?: {
-      showSettings?: boolean
-    }
-  },
-  onComponentChange: {} => void
-};
-*/
-
-/*::
-type State = {
-  popupOpen: boolean,
-  popupPosition: { top: number, right: number }
-};
-*/
-
-export default class MetaTags extends React.PureComponent {
-  /*:: card: HTMLElement; */
-  /*:: tagsList: HTMLElement; */
-  /*:: tagsSelector: HTMLElement; */
-  /*:: props: Props; */
-  state /*: State */ = {
-    popupOpen: false,
-    popupPosition: {
-      top: 0,
-      right: 0
-    }
-  };
-
-  componentDidMount() {
-    if (this.canUpdateTags()) {
-      const buttonPos = this.tagsList.getBoundingClientRect();
-      const cardPos = this.card.getBoundingClientRect();
-      this.setState({ popupPosition: this.getPopupPos(buttonPos, cardPos) });
-
-      window.addEventListener('keydown', this.handleKey, false);
-      window.addEventListener('click', this.handleOutsideClick, false);
-    }
-  }
-
-  componentWillUnmount() {
-    window.removeEventListener('keydown', this.handleKey);
-    window.removeEventListener('click', this.handleOutsideClick);
-  }
-
-  handleKey = (evt /*: KeyboardEvent */) => {
-    // Escape key
-    if (evt.keyCode === 27) {
-      this.setState({ popupOpen: false });
-    }
-  };
-
-  handleOutsideClick = (evt /*: SyntheticInputEvent */) => {
-    if (!this.tagsSelector || !this.tagsSelector.contains(evt.target)) {
-      this.setState({ popupOpen: false });
-    }
-  };
-
-  handleClick = (evt /*: MouseEvent */) => {
-    evt.stopPropagation();
-    this.setState(state => ({ popupOpen: !state.popupOpen }));
-  };
-
-  canUpdateTags() {
-    const { configuration } = this.props.component;
-    return configuration && configuration.showSettings;
-  }
-
-  getPopupPos(
-    eltPos /*: { height: number, width: number } */,
-    containerPos /*: { width: number } */
-  ) {
-    return {
-      top: eltPos.height,
-      right: containerPos.width - eltPos.width
-    };
-  }
-
-  handleSetProjectTags = (tags /*: Array<string> */) => {
-    setProjectTags({ project: this.props.component.key, tags: tags.join(',') }).then(
-      () => this.props.onComponentChange({ tags }),
-      () => {}
-    );
-  };
-
-  render() {
-    const { tags, key } = this.props.component;
-    const { popupOpen, popupPosition } = this.state;
-
-    if (this.canUpdateTags()) {
-      return (
-        <div className="overview-meta-card overview-meta-tags" ref={card => (this.card = card)}>
-          <button
-            className="button-link"
-            onClick={this.handleClick}
-            ref={tagsList => (this.tagsList = tagsList)}>
-            <TagsList tags={tags.length ? tags : [translate('no_tags')]} allowUpdate={true} />
-          </button>
-          {popupOpen && (
-            <div ref={tagsSelector => (this.tagsSelector = tagsSelector)}>
-              <MetaTagsSelector
-                position={popupPosition}
-                project={key}
-                selectedTags={tags}
-                setProjectTags={this.handleSetProjectTags}
-              />
-            </div>
-          )}
-        </div>
-      );
-    } else {
-      return (
-        <div className="overview-meta-card overview-meta-tags">
-          <TagsList tags={tags.length ? tags : [translate('no_tags')]} allowUpdate={false} />
-        </div>
-      );
-    }
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
new file mode 100644 (file)
index 0000000..f08c082
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * 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 { setProjectTags } from '../../../api/components';
+import { translate } from '../../../helpers/l10n';
+import TagsList from '../../../components/tags/TagsList';
+import MetaTagsSelector from './MetaTagsSelector';
+import { BubblePopupPosition } from '../../../components/common/BubblePopup';
+import { Component } from '../../../app/types';
+
+interface Props {
+  component: Component;
+  onComponentChange: (changes: {}) => void;
+}
+
+interface State {
+  popupOpen: boolean;
+  popupPosition: BubblePopupPosition;
+}
+
+export default class MetaTags extends React.PureComponent<Props, State> {
+  card: HTMLDivElement | null;
+  tagsList: HTMLButtonElement | null;
+  tagsSelector: HTMLDivElement | null;
+  state: State = { popupOpen: false, popupPosition: { top: 0, right: 0 } };
+
+  componentDidMount() {
+    if (this.canUpdateTags() && this.tagsList && this.card) {
+      const buttonPos = this.tagsList.getBoundingClientRect();
+      const cardPos = this.card.getBoundingClientRect();
+      this.setState({ popupPosition: this.getPopupPos(buttonPos, cardPos) });
+
+      window.addEventListener('keydown', this.handleKey, false);
+      window.addEventListener('click', this.handleOutsideClick, false);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('keydown', this.handleKey);
+    window.removeEventListener('click', this.handleOutsideClick);
+  }
+
+  handleKey = (evt: KeyboardEvent) => {
+    // Escape key
+    if (evt.keyCode === 27) {
+      this.setState({ popupOpen: false });
+    }
+  };
+
+  handleOutsideClick = (evt: Event) => {
+    if (!this.tagsSelector || !this.tagsSelector.contains(evt.target as Node)) {
+      this.setState({ popupOpen: false });
+    }
+  };
+
+  handleClick = (evt: React.SyntheticEvent<HTMLButtonElement>) => {
+    evt.stopPropagation();
+    this.setState(state => ({ popupOpen: !state.popupOpen }));
+  };
+
+  canUpdateTags = () => {
+    const { configuration } = this.props.component;
+    return configuration && configuration.showSettings;
+  };
+
+  getPopupPos = (eltPos: ClientRect, containerPos: ClientRect) => ({
+    top: eltPos.height,
+    right: containerPos.width - eltPos.width
+  });
+
+  handleSetProjectTags = (tags: string[]) => {
+    setProjectTags({ project: this.props.component.key, tags: tags.join(',') }).then(
+      () => this.props.onComponentChange({ tags }),
+      () => {}
+    );
+  };
+
+  render() {
+    const { key } = this.props.component;
+    const { popupOpen, popupPosition } = this.state;
+    const tags = this.props.component.tags || [];
+
+    if (this.canUpdateTags()) {
+      return (
+        <div className="overview-meta-card overview-meta-tags" ref={card => (this.card = card)}>
+          <button
+            className="button-link"
+            onClick={this.handleClick}
+            ref={tagsList => (this.tagsList = tagsList)}>
+            <TagsList tags={tags.length ? tags : [translate('no_tags')]} allowUpdate={true} />
+          </button>
+          {popupOpen && (
+            <div ref={tagsSelector => (this.tagsSelector = tagsSelector)}>
+              <MetaTagsSelector
+                position={popupPosition}
+                project={key}
+                selectedTags={tags}
+                setProjectTags={this.handleSetProjectTags}
+              />
+            </div>
+          )}
+        </div>
+      );
+    } else {
+      return (
+        <div className="overview-meta-card overview-meta-tags">
+          <TagsList tags={tags.length ? tags : [translate('no_tags')]} allowUpdate={false} />
+        </div>
+      );
+    }
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js
deleted file mode 100644 (file)
index f728a15..0000000
+++ /dev/null
@@ -1,81 +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 { without } from 'lodash';
-import TagsSelector from '../../../components/tags/TagsSelector';
-import { searchProjectTags } from '../../../api/components';
-
-/*::
-type Props = {
-  position: {},
-  project: string,
-  selectedTags: Array<string>,
-  setProjectTags: (Array<string>) => void
-};
-*/
-
-/*::
-type State = {
-  searchResult: Array<string>
-};
-*/
-
-const LIST_SIZE = 10;
-
-export default class MetaTagsSelector extends React.PureComponent {
-  /*:: props: Props; */
-  state /*: State */ = { searchResult: [] };
-
-  componentDidMount() {
-    this.onSearch('');
-  }
-
-  onSearch = (query /*: string */) => {
-    searchProjectTags({
-      q: query,
-      ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100)
-    }).then(result => {
-      this.setState({ searchResult: result.tags });
-    });
-  };
-
-  onSelect = (tag /*: string */) => {
-    this.props.setProjectTags([...this.props.selectedTags, tag]);
-  };
-
-  onUnselect = (tag /*: string */) => {
-    this.props.setProjectTags(without(this.props.selectedTags, tag));
-  };
-
-  render() {
-    return (
-      <TagsSelector
-        position={this.props.position}
-        tags={this.state.searchResult}
-        selectedTags={this.props.selectedTags}
-        listSize={LIST_SIZE}
-        onSearch={this.onSearch}
-        onSelect={this.onSelect}
-        onUnselect={this.onUnselect}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
new file mode 100644 (file)
index 0000000..2e2bb77
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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 { without } from 'lodash';
+import TagsSelector from '../../../components/tags/TagsSelector';
+import { BubblePopupPosition } from '../../../components/common/BubblePopup';
+import { searchProjectTags } from '../../../api/components';
+
+interface Props {
+  position: BubblePopupPosition;
+  project: string;
+  selectedTags: string[];
+  setProjectTags: (tags: string[]) => void;
+}
+
+interface State {
+  searchResult: string[];
+}
+
+const LIST_SIZE = 10;
+
+export default class MetaTagsSelector extends React.PureComponent<Props, State> {
+  state: State = { searchResult: [] };
+
+  componentDidMount() {
+    this.onSearch('');
+  }
+
+  onSearch = (query: string) => {
+    searchProjectTags({
+      q: query,
+      ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100)
+    }).then(result => this.setState({ searchResult: result.tags }), () => {});
+  };
+
+  onSelect = (tag: string) => {
+    this.props.setProjectTags([...this.props.selectedTags, tag]);
+  };
+
+  onUnselect = (tag: string) => {
+    this.props.setProjectTags(without(this.props.selectedTags, tag));
+  };
+
+  render() {
+    return (
+      <TagsSelector
+        position={this.props.position}
+        tags={this.state.searchResult}
+        selectedTags={this.props.selectedTags}
+        listSize={LIST_SIZE}
+        onSearch={this.onSearch}
+        onSelect={this.onSelect}
+        onUnselect={this.onUnselect}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.js b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.js
deleted file mode 100644 (file)
index 2499c2e..0000000
+++ /dev/null
@@ -1,54 +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 MetaLink from '../MetaLink';
-import { click } from '../../../../helpers/testUtils';
-
-it('should match snapshot', () => {
-  const link = {
-    id: '1',
-    name: 'Foo',
-    url: 'http://example.com',
-    type: 'foo'
-  };
-
-  expect(shallow(<MetaLink link={link} />)).toMatchSnapshot();
-});
-
-it('should expand and collapse link', () => {
-  const link = {
-    id: '1',
-    name: 'Foo',
-    url: 'scm:git:git@github.com',
-    type: 'foo'
-  };
-
-  const wrapper = shallow(<MetaLink link={link} />);
-  expect(wrapper).toMatchSnapshot();
-
-  // expand
-  click(wrapper.find('a'));
-  expect(wrapper).toMatchSnapshot();
-
-  // collapse
-  click(wrapper.find('a'));
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx
new file mode 100644 (file)
index 0000000..0bea708
--- /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 MetaLink from '../MetaLink';
+import { click } from '../../../../helpers/testUtils';
+
+it('should match snapshot', () => {
+  const link = {
+    id: '1',
+    name: 'Foo',
+    url: 'http://example.com',
+    type: 'foo'
+  };
+
+  expect(shallow(<MetaLink link={link} />)).toMatchSnapshot();
+});
+
+it('should expand and collapse link', () => {
+  const link = {
+    id: '1',
+    name: 'Foo',
+    url: 'scm:git:git@github.com',
+    type: 'foo'
+  };
+
+  const wrapper = shallow(<MetaLink link={link} />);
+  expect(wrapper).toMatchSnapshot();
+
+  // expand
+  click(wrapper.find('a'));
+  expect(wrapper).toMatchSnapshot();
+
+  // collapse
+  click(wrapper.find('a'));
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.js b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.js
deleted file mode 100644 (file)
index 9a621ce..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 { click } from '../../../../helpers/testUtils';
-import MetaTags from '../MetaTags';
-
-const component = {
-  key: 'my-project',
-  tags: [],
-  configuration: {
-    showSettings: false
-  }
-};
-
-const componentWithTags = {
-  key: 'my-second-project',
-  tags: ['foo', 'bar'],
-  configuration: {
-    showSettings: true
-  }
-};
-
-it('should render without tags and admin rights', () => {
-  expect(
-    shallow(<MetaTags component={component} />, { disableLifecycleMethods: true })
-  ).toMatchSnapshot();
-});
-
-it('should render with tags and admin rights', () => {
-  expect(
-    shallow(<MetaTags component={componentWithTags} />, { disableLifecycleMethods: true })
-  ).toMatchSnapshot();
-});
-
-it('should open the tag selector on click', () => {
-  const wrapper = shallow(<MetaTags component={componentWithTags} />, {
-    disableLifecycleMethods: true
-  });
-  expect(wrapper).toMatchSnapshot();
-
-  // open
-  click(wrapper.find('button'));
-  expect(wrapper).toMatchSnapshot();
-
-  // close
-  click(wrapper.find('button'));
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx
new file mode 100644 (file)
index 0000000..1479225
--- /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 { shallow } from 'enzyme';
+import { click } from '../../../../helpers/testUtils';
+import MetaTags from '../MetaTags';
+
+const component = {
+  key: 'my-project',
+  tags: [],
+  configuration: {
+    showSettings: false
+  },
+  organization: 'foo',
+  qualifier: 'TRK',
+  name: 'MyProject',
+  breadcrumbs: []
+};
+
+const componentWithTags = {
+  key: 'my-second-project',
+  tags: ['foo', 'bar'],
+  configuration: {
+    showSettings: true
+  },
+  organization: 'foo',
+  qualifier: 'TRK',
+  name: 'MySecondProject',
+  breadcrumbs: []
+};
+
+it('should render without tags and admin rights', () => {
+  expect(
+    shallow(<MetaTags component={component} onComponentChange={jest.fn()} />, {
+      disableLifecycleMethods: true
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render with tags and admin rights', () => {
+  expect(
+    shallow(<MetaTags component={componentWithTags} onComponentChange={jest.fn()} />, {
+      disableLifecycleMethods: true
+    })
+  ).toMatchSnapshot();
+});
+
+it('should open the tag selector on click', () => {
+  const wrapper = shallow(
+    <MetaTags component={componentWithTags} onComponentChange={jest.fn()} />,
+    {
+      disableLifecycleMethods: true
+    }
+  );
+  expect(wrapper).toMatchSnapshot();
+
+  // open
+  click(wrapper.find('button'));
+  expect(wrapper).toMatchSnapshot();
+
+  // close
+  click(wrapper.find('button'));
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js
deleted file mode 100644 (file)
index 2f7a364..0000000
+++ /dev/null
@@ -1,62 +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.
- */
-/* eslint-disable import/order, import/first */
-import React from 'react';
-import { mount, shallow } from 'enzyme';
-import MetaTagsSelector from '../MetaTagsSelector';
-
-jest.mock('../../../../api/components', () => ({
-  searchProjectTags: jest.fn()
-}));
-
-jest.mock('lodash', () => {
-  const lodash = require.requireActual('lodash');
-  lodash.debounce = jest.fn(fn => fn);
-  return lodash;
-});
-
-import { searchProjectTags } from '../../../../api/components';
-
-it('searches tags on mount', () => {
-  searchProjectTags.mockImplementation(() => Promise.resolve({ tags: ['foo', 'bar'] }));
-  mount(
-    <MetaTagsSelector position={{}} project="foo" selectedTags={[]} setProjectTags={jest.fn()} />
-  );
-  expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' });
-});
-
-it('selects and deselects tags', () => {
-  const setProjectTags = jest.fn();
-  const wrapper = shallow(
-    <MetaTagsSelector
-      position={{}}
-      project="foo"
-      selectedTags={['foo', 'bar']}
-      setProjectTags={setProjectTags}
-    />
-  );
-
-  wrapper.find('TagsSelector').prop('onSelect')('baz');
-  expect(setProjectTags).toHaveBeenLastCalledWith(['foo', 'bar', 'baz']);
-
-  // note that the `selectedTags` is a prop and so it wasn't changed
-  wrapper.find('TagsSelector').prop('onUnselect')('bar');
-  expect(setProjectTags).toHaveBeenLastCalledWith(['foo']);
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx
new file mode 100644 (file)
index 0000000..aefeb8d
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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 import/order, import/first */
+import * as React from 'react';
+import { mount, shallow } from 'enzyme';
+import MetaTagsSelector from '../MetaTagsSelector';
+
+jest.mock('../../../../api/components', () => ({
+  searchProjectTags: jest.fn()
+}));
+
+jest.mock('lodash', () => {
+  const lodash = require.requireActual('lodash');
+  lodash.debounce = jest.fn(fn => fn);
+  return lodash;
+});
+
+import { searchProjectTags } from '../../../../api/components';
+
+it('searches tags on mount', () => {
+  (searchProjectTags as jest.Mock).mockImplementation(() =>
+    Promise.resolve({ tags: ['foo', 'bar'] })
+  );
+  mount(
+    <MetaTagsSelector
+      position={{ top: 0, right: 0 }}
+      project="foo"
+      selectedTags={[]}
+      setProjectTags={jest.fn()}
+    />
+  );
+  expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' });
+});
+
+it('selects and deselects tags', () => {
+  const setProjectTags = jest.fn();
+  const wrapper = shallow(
+    <MetaTagsSelector
+      position={{ top: 0, right: 0 }}
+      project="foo"
+      selectedTags={['foo', 'bar']}
+      setProjectTags={setProjectTags}
+    />
+  );
+
+  const tagSelect: any = wrapper.find('TagsSelector');
+  tagSelect.prop('onSelect')('baz');
+  expect(setProjectTags).toHaveBeenLastCalledWith(['foo', 'bar', 'baz']);
+
+  // note that the `selectedTags` is a prop and so it wasn't changed
+  tagSelect.prop('onUnselect')('bar');
+  expect(setProjectTags).toHaveBeenLastCalledWith(['foo']);
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.js.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.js.snap
deleted file mode 100644 (file)
index 20c6a30..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should expand and collapse link 1`] = `
-<li>
-  <a
-    className="link-with-icon"
-    href="scm:git:git@github.com"
-    onClick={[Function]}
-    target="_blank"
-  >
-    <i
-      className="icon-color-link icon-detach"
-    />
-     
-    Foo
-  </a>
-</li>
-`;
-
-exports[`should expand and collapse link 2`] = `
-<li>
-  <a
-    className="link-with-icon"
-    href="scm:git:git@github.com"
-    onClick={[Function]}
-    target="_blank"
-  >
-    <i
-      className="icon-color-link icon-detach"
-    />
-     
-    Foo
-  </a>
-  <div
-    className="little-spacer-top"
-  >
-    <input
-      className="overview-key"
-      onClick={[Function]}
-      readOnly={true}
-      type="text"
-      value="scm:git:git@github.com"
-    />
-  </div>
-</li>
-`;
-
-exports[`should expand and collapse link 3`] = `
-<li>
-  <a
-    className="link-with-icon"
-    href="scm:git:git@github.com"
-    onClick={[Function]}
-    target="_blank"
-  >
-    <i
-      className="icon-color-link icon-detach"
-    />
-     
-    Foo
-  </a>
-</li>
-`;
-
-exports[`should match snapshot 1`] = `
-<li>
-  <a
-    className="link-with-icon"
-    href="http://example.com"
-    target="_blank"
-  >
-    <i
-      className="icon-color-link icon-detach"
-    />
-     
-    Foo
-  </a>
-</li>
-`;
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap
new file mode 100644 (file)
index 0000000..20c6a30
--- /dev/null
@@ -0,0 +1,79 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should expand and collapse link 1`] = `
+<li>
+  <a
+    className="link-with-icon"
+    href="scm:git:git@github.com"
+    onClick={[Function]}
+    target="_blank"
+  >
+    <i
+      className="icon-color-link icon-detach"
+    />
+     
+    Foo
+  </a>
+</li>
+`;
+
+exports[`should expand and collapse link 2`] = `
+<li>
+  <a
+    className="link-with-icon"
+    href="scm:git:git@github.com"
+    onClick={[Function]}
+    target="_blank"
+  >
+    <i
+      className="icon-color-link icon-detach"
+    />
+     
+    Foo
+  </a>
+  <div
+    className="little-spacer-top"
+  >
+    <input
+      className="overview-key"
+      onClick={[Function]}
+      readOnly={true}
+      type="text"
+      value="scm:git:git@github.com"
+    />
+  </div>
+</li>
+`;
+
+exports[`should expand and collapse link 3`] = `
+<li>
+  <a
+    className="link-with-icon"
+    href="scm:git:git@github.com"
+    onClick={[Function]}
+    target="_blank"
+  >
+    <i
+      className="icon-color-link icon-detach"
+    />
+     
+    Foo
+  </a>
+</li>
+`;
+
+exports[`should match snapshot 1`] = `
+<li>
+  <a
+    className="link-with-icon"
+    href="http://example.com"
+    target="_blank"
+  >
+    <i
+      className="icon-color-link icon-detach"
+    />
+     
+    Foo
+  </a>
+</li>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
deleted file mode 100644 (file)
index 8753024..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should open the tag selector on click 1`] = `
-<div
-  className="overview-meta-card overview-meta-tags"
->
-  <button
-    className="button-link"
-    onClick={[Function]}
-  >
-    <TagsList
-      allowUpdate={true}
-      tags={
-        Array [
-          "foo",
-          "bar",
-        ]
-      }
-    />
-  </button>
-</div>
-`;
-
-exports[`should open the tag selector on click 2`] = `
-<div
-  className="overview-meta-card overview-meta-tags"
->
-  <button
-    className="button-link"
-    onClick={[Function]}
-  >
-    <TagsList
-      allowUpdate={true}
-      tags={
-        Array [
-          "foo",
-          "bar",
-        ]
-      }
-    />
-  </button>
-  <div>
-    <MetaTagsSelector
-      position={
-        Object {
-          "right": 0,
-          "top": 0,
-        }
-      }
-      project="my-second-project"
-      selectedTags={
-        Array [
-          "foo",
-          "bar",
-        ]
-      }
-      setProjectTags={[Function]}
-    />
-  </div>
-</div>
-`;
-
-exports[`should open the tag selector on click 3`] = `
-<div
-  className="overview-meta-card overview-meta-tags"
->
-  <button
-    className="button-link"
-    onClick={[Function]}
-  >
-    <TagsList
-      allowUpdate={true}
-      tags={
-        Array [
-          "foo",
-          "bar",
-        ]
-      }
-    />
-  </button>
-</div>
-`;
-
-exports[`should render with tags and admin rights 1`] = `
-<div
-  className="overview-meta-card overview-meta-tags"
->
-  <button
-    className="button-link"
-    onClick={[Function]}
-  >
-    <TagsList
-      allowUpdate={true}
-      tags={
-        Array [
-          "foo",
-          "bar",
-        ]
-      }
-    />
-  </button>
-</div>
-`;
-
-exports[`should render without tags and admin rights 1`] = `
-<div
-  className="overview-meta-card overview-meta-tags"
->
-  <TagsList
-    allowUpdate={false}
-    tags={
-      Array [
-        "no_tags",
-      ]
-    }
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap
new file mode 100644 (file)
index 0000000..8753024
--- /dev/null
@@ -0,0 +1,118 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should open the tag selector on click 1`] = `
+<div
+  className="overview-meta-card overview-meta-tags"
+>
+  <button
+    className="button-link"
+    onClick={[Function]}
+  >
+    <TagsList
+      allowUpdate={true}
+      tags={
+        Array [
+          "foo",
+          "bar",
+        ]
+      }
+    />
+  </button>
+</div>
+`;
+
+exports[`should open the tag selector on click 2`] = `
+<div
+  className="overview-meta-card overview-meta-tags"
+>
+  <button
+    className="button-link"
+    onClick={[Function]}
+  >
+    <TagsList
+      allowUpdate={true}
+      tags={
+        Array [
+          "foo",
+          "bar",
+        ]
+      }
+    />
+  </button>
+  <div>
+    <MetaTagsSelector
+      position={
+        Object {
+          "right": 0,
+          "top": 0,
+        }
+      }
+      project="my-second-project"
+      selectedTags={
+        Array [
+          "foo",
+          "bar",
+        ]
+      }
+      setProjectTags={[Function]}
+    />
+  </div>
+</div>
+`;
+
+exports[`should open the tag selector on click 3`] = `
+<div
+  className="overview-meta-card overview-meta-tags"
+>
+  <button
+    className="button-link"
+    onClick={[Function]}
+  >
+    <TagsList
+      allowUpdate={true}
+      tags={
+        Array [
+          "foo",
+          "bar",
+        ]
+      }
+    />
+  </button>
+</div>
+`;
+
+exports[`should render with tags and admin rights 1`] = `
+<div
+  className="overview-meta-card overview-meta-tags"
+>
+  <button
+    className="button-link"
+    onClick={[Function]}
+  >
+    <TagsList
+      allowUpdate={true}
+      tags={
+        Array [
+          "foo",
+          "bar",
+        ]
+      }
+    />
+  </button>
+</div>
+`;
+
+exports[`should render without tags and admin rights 1`] = `
+<div
+  className="overview-meta-card overview-meta-tags"
+>
+  <TagsList
+    allowUpdate={false}
+    tags={
+      Array [
+        "no_tags",
+      ]
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js
deleted file mode 100644 (file)
index 6528d5a..0000000
+++ /dev/null
@@ -1,124 +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 { keyBy } from 'lodash';
-import ApplicationQualityGateProject from './ApplicationQualityGateProject';
-import Level from '../../../components/ui/Level';
-import { getApplicationQualityGate } from '../../../api/quality-gates';
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = {
-  component: { key: string, organization?: string }
-};
-*/
-
-/*::
-type State = {
-  loading: boolean,
-  metrics?: { [string]: Object },
-  projects?: Array<{
-    conditions: Array<Object>,
-    key: string,
-    name: string,
-    status: string
-  }>,
-  status?: string
-};
-*/
-
-export default class ApplicationQualityGate extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  state /*: State */ = {
-    loading: true
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchDetails();
-  }
-
-  componentDidUpdate(prevProps /*: Props */) {
-    if (prevProps.component.key !== this.props.component.key) {
-      this.fetchDetails();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchDetails = () => {
-    const { component } = this.props;
-    this.setState({ loading: true });
-    getApplicationQualityGate({
-      application: component.key,
-      organization: component.organization
-    }).then(
-      ({ status, projects, metrics }) => {
-        if (this.mounted) {
-          this.setState({
-            loading: false,
-            metrics: keyBy(metrics, 'key'),
-            status,
-            projects
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  render() {
-    const { metrics, status, projects } = this.state;
-
-    return (
-      <div className="overview-quality-gate" id="overview-quality-gate">
-        <h2 className="overview-title">
-          {translate('overview.quality_gate')}
-          {this.state.loading && <i className="spinner spacer-left" />}
-          {status != null && <Level level={status} />}
-        </h2>
-
-        {projects != null && (
-          <div
-            id="overview-quality-gate-conditions-list"
-            className="overview-quality-gate-conditions-list clearfix">
-            {projects
-              .filter(project => project.status !== 'OK')
-              .map(project => (
-                <ApplicationQualityGateProject
-                  key={project.key}
-                  metrics={metrics}
-                  project={project}
-                />
-              ))}
-          </div>
-        )}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx
new file mode 100644 (file)
index 0000000..3d8202f
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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 { keyBy } from 'lodash';
+import ApplicationQualityGateProject from './ApplicationQualityGateProject';
+import Level from '../../../components/ui/Level';
+import { getApplicationQualityGate, ApplicationProject } from '../../../api/quality-gates';
+import { translate } from '../../../helpers/l10n';
+import { LightComponent, Metric } from '../../../app/types';
+
+interface Props {
+  component: LightComponent;
+}
+
+type State = {
+  loading: boolean;
+  metrics?: { [key: string]: Metric };
+  projects?: ApplicationProject[];
+  status?: string;
+};
+
+export default class ApplicationQualityGate extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchDetails();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.component.key !== this.props.component.key) {
+      this.fetchDetails();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchDetails = () => {
+    const { component } = this.props;
+    this.setState({ loading: true });
+    getApplicationQualityGate({
+      application: component.key,
+      organization: component.organization
+    }).then(
+      ({ status, projects, metrics }) => {
+        if (this.mounted) {
+          this.setState({
+            loading: false,
+            metrics: keyBy(metrics, 'key'),
+            status,
+            projects
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  render() {
+    const { metrics, status, projects } = this.state;
+
+    return (
+      <div className="overview-quality-gate" id="overview-quality-gate">
+        <h2 className="overview-title">
+          {translate('overview.quality_gate')}
+          {this.state.loading && <i className="spinner spacer-left" />}
+          {status != null && <Level level={status} />}
+        </h2>
+
+        {projects &&
+          metrics && (
+            <div
+              id="overview-quality-gate-conditions-list"
+              className="overview-quality-gate-conditions-list clearfix">
+              {projects
+                .filter(project => project.status !== 'OK')
+                .map(project => (
+                  <ApplicationQualityGateProject
+                    key={project.key}
+                    metrics={metrics}
+                    project={project}
+                  />
+                ))}
+            </div>
+          )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js
deleted file mode 100644 (file)
index 451c808..0000000
+++ /dev/null
@@ -1,105 +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 classNames from 'classnames';
-import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
-import { getProjectUrl } from '../../../helpers/urls';
-import './ApplicationQualityGateProject.css';
-
-/*::
-type Condition = {
-  comparator: string,
-  errorThreshold?: string,
-  metric: string,
-  onLeak: boolean,
-  status: string,
-  value: string,
-  warningThreshold?: string
-};
-*/
-
-/*::
-type Props = {
-  metrics: {
-    [string]: {
-      key: string,
-      name: string,
-      type: string
-    }
-  },
-  project: {
-    conditions: Array<Condition>,
-    key: string,
-    name: string,
-    status: string
-  }
-};
-*/
-
-export default class ApplicationQualityGateProject extends React.PureComponent {
-  /*:: props: Props; */
-
-  renderCondition = (condition /*: Condition */) => {
-    const metric = this.props.metrics[condition.metric];
-    const metricName = getLocalizedMetricName(metric);
-    const threshold = condition.errorThreshold || condition.warningThreshold;
-    const isDiff = isDiffMetric(condition.metric);
-
-    return (
-      <li className={classNames({ 'is-on-leak': isDiff })} key={condition.metric}>
-        <span className="text-limited">
-          <strong>{formatMeasure(condition.value, metric.type)}</strong> {metricName}
-          {!isDiff && condition.onLeak && ' ' + translate('quality_gates.conditions.leak')}
-        </span>
-        <span
-          className={classNames('big-spacer-left', {
-            'text-danger': condition.status === 'ERROR',
-            'text-warning': condition.status === 'WARN'
-          })}>
-          {translate('quality_gates.operator', condition.comparator, 'short')}{' '}
-          {formatMeasure(threshold, metric.type)}
-        </span>
-      </li>
-    );
-  };
-
-  render() {
-    const { project } = this.props;
-
-    return (
-      <Link
-        className={classNames(
-          'overview-quality-gate-condition',
-          'overview-quality-gate-condition-' + project.status.toLowerCase()
-        )}
-        to={getProjectUrl(project.key)}>
-        <div className="application-quality-gate-project">
-          <h4>{project.name}</h4>
-          <ul className="application-quality-gate-project-conditions">
-            {project.conditions.filter(c => c.status !== 'OK').map(this.renderCondition)}
-          </ul>
-        </div>
-      </Link>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.tsx
new file mode 100644 (file)
index 0000000..388b770
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * 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 { Link } from 'react-router';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { getProjectUrl } from '../../../helpers/urls';
+import './ApplicationQualityGateProject.css';
+import { Metric } from '../../../app/types';
+import { ApplicationProject, ConditionAnalysis } from '../../../api/quality-gates';
+
+interface Props {
+  metrics: { [key: string]: Metric };
+  project: ApplicationProject;
+}
+
+export default class ApplicationQualityGateProject extends React.PureComponent<Props> {
+  renderCondition = (condition: ConditionAnalysis) => {
+    const metric = this.props.metrics[condition.metric];
+    const metricName = getLocalizedMetricName(metric);
+    const threshold = condition.errorThreshold || condition.warningThreshold;
+    const isDiff = isDiffMetric(condition.metric);
+
+    return (
+      <li className={classNames({ 'is-on-leak': isDiff })} key={condition.metric}>
+        <span className="text-limited">
+          <strong>{formatMeasure(condition.value, metric.type)}</strong> {metricName}
+          {!isDiff && condition.onLeak && ' ' + translate('quality_gates.conditions.leak')}
+        </span>
+        <span
+          className={classNames('big-spacer-left', {
+            'text-danger': condition.status === 'ERROR',
+            'text-warning': condition.status === 'WARN'
+          })}>
+          {translate('quality_gates.operator', condition.comparator, 'short')}{' '}
+          {formatMeasure(threshold, metric.type)}
+        </span>
+      </li>
+    );
+  };
+
+  render() {
+    const { project } = this.props;
+
+    return (
+      <Link
+        className={classNames(
+          'overview-quality-gate-condition',
+          'overview-quality-gate-condition-' + project.status.toLowerCase()
+        )}
+        to={getProjectUrl(project.key)}>
+        <div className="application-quality-gate-project">
+          <h4>{project.name}</h4>
+          <ul className="application-quality-gate-project-conditions">
+            {project.conditions.filter(c => c.status !== 'OK').map(this.renderCondition)}
+          </ul>
+        </div>
+      </Link>
+    );
+  }
+}
index 71eeccc29321728e986ebe81440ebcd635a04b7d..3301c33ef6fb96d616bb329103cae1fc0a7cefc9 100644 (file)
@@ -21,7 +21,7 @@
 import React from 'react';
 import classNames from 'classnames';
 import { Link } from 'react-router';
-import { DrilldownLink } from '../../../components/shared/drilldown-link';
+import DrilldownLink from '../../../components/shared/DrilldownLink';
 import Measure from '../../../components/measure/Measure';
 import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
 import { getPeriodValue, isDiffMetric, formatMeasure } from '../../../helpers/measures';
@@ -146,7 +146,12 @@ export default class QualityGateCondition extends React.PureComponent {
     return this.wrapWithLink(
       <div className="overview-quality-gate-condition-container">
         <div className="overview-quality-gate-condition-value">
-          <Measure measure={{ ...measure, value: actual, leak: actual }} decimals={decimals} />
+          <Measure
+            decimals={decimals}
+            value={actual}
+            metricKey={measure.metric.key}
+            metricType={measure.metric.type}
+          />
         </div>
 
         <div>
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js
deleted file mode 100644 (file)
index 5d6170c..0000000
+++ /dev/null
@@ -1,39 +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 ApplicationQualityGate from '../ApplicationQualityGate';
-
-it('renders', () => {
-  const wrapper = shallow(<ApplicationQualityGate component={{ key: 'foo' }} />);
-  expect(wrapper).toMatchSnapshot();
-  wrapper.setState({
-    loading: false,
-    metrics: {},
-    status: 'ERROR',
-    projects: [
-      { conditions: [], key: 'project1', name: 'project1', status: 'ERROR' },
-      { conditions: [], key: 'project2', name: 'project2', status: 'OK' },
-      { conditions: [], key: 'project3', name: 'project3', status: 'WARN' }
-    ]
-  });
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.tsx
new file mode 100644 (file)
index 0000000..fc15b30
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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 ApplicationQualityGate from '../ApplicationQualityGate';
+
+it('renders', () => {
+  const wrapper = shallow(
+    <ApplicationQualityGate component={{ key: 'foo', organization: 'foo', qualifier: 'TRK' }} />
+  );
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setState({
+    loading: false,
+    metrics: {},
+    status: 'ERROR',
+    projects: [
+      { conditions: [], key: 'project1', name: 'project1', status: 'ERROR' },
+      { conditions: [], key: 'project2', name: 'project2', status: 'OK' },
+      { conditions: [], key: 'project3', name: 'project3', status: 'WARN' }
+    ]
+  });
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js
deleted file mode 100644 (file)
index 87d7f69..0000000
+++ /dev/null
@@ -1,73 +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 ApplicationQualityGateProject from '../ApplicationQualityGateProject';
-
-const metrics = {
-  bugs: { key: 'bugs', name: 'Bugs', type: 'INT' },
-  new_coverage: { key: 'new_coverage', name: 'Coverage on New Code', type: 'PERCENT' },
-  skipped_tests: { key: 'skipped_tests', name: 'Skipped Tests', type: 'INT' }
-};
-
-it('renders', () => {
-  const project = {
-    key: 'foo',
-    name: 'Foo',
-    status: 'ERROR',
-    conditions: [
-      {
-        status: 'ERROR',
-        metric: 'new_coverage',
-        comparator: 'LT',
-        onLeak: true,
-        errorThreshold: '85',
-        value: '82.50562381034781'
-      },
-      {
-        status: 'WARN',
-        metric: 'bugs',
-        comparator: 'GT',
-        onLeak: false,
-        warningThreshold: '0',
-        value: '17'
-      },
-      {
-        status: 'ERROR',
-        metric: 'bugs',
-        comparator: 'GT',
-        onLeak: true,
-        warningThreshold: '0',
-        value: '3'
-      },
-      {
-        status: 'OK',
-        metric: 'skipped_tests',
-        comparator: 'GT',
-        onLeak: false,
-        warningThreshold: '0',
-        value: '0'
-      }
-    ]
-  };
-  const wrapper = shallow(<ApplicationQualityGateProject metrics={metrics} project={project} />);
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.tsx
new file mode 100644 (file)
index 0000000..3f7904c
--- /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 ApplicationQualityGateProject from '../ApplicationQualityGateProject';
+
+const metrics = {
+  bugs: { key: 'bugs', name: 'Bugs', type: 'INT' },
+  new_coverage: { key: 'new_coverage', name: 'Coverage on New Code', type: 'PERCENT' },
+  skipped_tests: { key: 'skipped_tests', name: 'Skipped Tests', type: 'INT' }
+};
+
+it('renders', () => {
+  const project = {
+    key: 'foo',
+    name: 'Foo',
+    status: 'ERROR',
+    conditions: [
+      {
+        status: 'ERROR',
+        metric: 'new_coverage',
+        comparator: 'LT',
+        onLeak: true,
+        errorThreshold: '85',
+        value: '82.50562381034781'
+      },
+      {
+        status: 'WARN',
+        metric: 'bugs',
+        comparator: 'GT',
+        onLeak: false,
+        warningThreshold: '0',
+        value: '17'
+      },
+      {
+        status: 'ERROR',
+        metric: 'bugs',
+        comparator: 'GT',
+        onLeak: true,
+        warningThreshold: '0',
+        value: '3'
+      },
+      {
+        status: 'OK',
+        metric: 'skipped_tests',
+        comparator: 'GT',
+        onLeak: false,
+        warningThreshold: '0',
+        value: '0'
+      }
+    ]
+  };
+  const wrapper = shallow(<ApplicationQualityGateProject metrics={metrics} project={project} />);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap
deleted file mode 100644 (file)
index 5a7f39c..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
-  className="overview-quality-gate"
-  id="overview-quality-gate"
->
-  <h2
-    className="overview-title"
-  >
-    overview.quality_gate
-    <i
-      className="spinner spacer-left"
-    />
-  </h2>
-</div>
-`;
-
-exports[`renders 2`] = `
-<div
-  className="overview-quality-gate"
-  id="overview-quality-gate"
->
-  <h2
-    className="overview-title"
-  >
-    overview.quality_gate
-    <Level
-      level="ERROR"
-    />
-  </h2>
-  <div
-    className="overview-quality-gate-conditions-list clearfix"
-    id="overview-quality-gate-conditions-list"
-  >
-    <ApplicationQualityGateProject
-      key="project1"
-      metrics={Object {}}
-      project={
-        Object {
-          "conditions": Array [],
-          "key": "project1",
-          "name": "project1",
-          "status": "ERROR",
-        }
-      }
-    />
-    <ApplicationQualityGateProject
-      key="project3"
-      metrics={Object {}}
-      project={
-        Object {
-          "conditions": Array [],
-          "key": "project3",
-          "name": "project3",
-          "status": "WARN",
-        }
-      }
-    />
-  </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap
new file mode 100644 (file)
index 0000000..5a7f39c
--- /dev/null
@@ -0,0 +1,62 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+  className="overview-quality-gate"
+  id="overview-quality-gate"
+>
+  <h2
+    className="overview-title"
+  >
+    overview.quality_gate
+    <i
+      className="spinner spacer-left"
+    />
+  </h2>
+</div>
+`;
+
+exports[`renders 2`] = `
+<div
+  className="overview-quality-gate"
+  id="overview-quality-gate"
+>
+  <h2
+    className="overview-title"
+  >
+    overview.quality_gate
+    <Level
+      level="ERROR"
+    />
+  </h2>
+  <div
+    className="overview-quality-gate-conditions-list clearfix"
+    id="overview-quality-gate-conditions-list"
+  >
+    <ApplicationQualityGateProject
+      key="project1"
+      metrics={Object {}}
+      project={
+        Object {
+          "conditions": Array [],
+          "key": "project1",
+          "name": "project1",
+          "status": "ERROR",
+        }
+      }
+    />
+    <ApplicationQualityGateProject
+      key="project3"
+      metrics={Object {}}
+      project={
+        Object {
+          "conditions": Array [],
+          "key": "project3",
+          "name": "project3",
+          "status": "WARN",
+        }
+      }
+    />
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap
deleted file mode 100644 (file)
index 64d2bf8..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Link
-  className="overview-quality-gate-condition overview-quality-gate-condition-error"
-  onlyActiveOnIndex={false}
-  style={Object {}}
-  to={
-    Object {
-      "pathname": "/dashboard",
-      "query": Object {
-        "branch": undefined,
-        "id": "foo",
-      },
-    }
-  }
->
-  <div
-    className="application-quality-gate-project"
-  >
-    <h4>
-      Foo
-    </h4>
-    <ul
-      className="application-quality-gate-project-conditions"
-    >
-      <li
-        className="is-on-leak"
-        key="new_coverage"
-      >
-        <span
-          className="text-limited"
-        >
-          <strong>
-            82.5%
-          </strong>
-           
-          Coverage on New Code
-        </span>
-        <span
-          className="big-spacer-left text-danger"
-        >
-          quality_gates.operator.LT.short
-           
-          85.0%
-        </span>
-      </li>
-      <li
-        className=""
-        key="bugs"
-      >
-        <span
-          className="text-limited"
-        >
-          <strong>
-            17
-          </strong>
-           
-          Bugs
-        </span>
-        <span
-          className="big-spacer-left text-warning"
-        >
-          quality_gates.operator.GT.short
-           
-          0
-        </span>
-      </li>
-      <li
-        className=""
-        key="bugs"
-      >
-        <span
-          className="text-limited"
-        >
-          <strong>
-            3
-          </strong>
-           
-          Bugs
-           quality_gates.conditions.leak
-        </span>
-        <span
-          className="big-spacer-left text-danger"
-        >
-          quality_gates.operator.GT.short
-           
-          0
-        </span>
-      </li>
-    </ul>
-  </div>
-</Link>
-`;
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap
new file mode 100644 (file)
index 0000000..64d2bf8
--- /dev/null
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Link
+  className="overview-quality-gate-condition overview-quality-gate-condition-error"
+  onlyActiveOnIndex={false}
+  style={Object {}}
+  to={
+    Object {
+      "pathname": "/dashboard",
+      "query": Object {
+        "branch": undefined,
+        "id": "foo",
+      },
+    }
+  }
+>
+  <div
+    className="application-quality-gate-project"
+  >
+    <h4>
+      Foo
+    </h4>
+    <ul
+      className="application-quality-gate-project-conditions"
+    >
+      <li
+        className="is-on-leak"
+        key="new_coverage"
+      >
+        <span
+          className="text-limited"
+        >
+          <strong>
+            82.5%
+          </strong>
+           
+          Coverage on New Code
+        </span>
+        <span
+          className="big-spacer-left text-danger"
+        >
+          quality_gates.operator.LT.short
+           
+          85.0%
+        </span>
+      </li>
+      <li
+        className=""
+        key="bugs"
+      >
+        <span
+          className="text-limited"
+        >
+          <strong>
+            17
+          </strong>
+           
+          Bugs
+        </span>
+        <span
+          className="big-spacer-left text-warning"
+        >
+          quality_gates.operator.GT.short
+           
+          0
+        </span>
+      </li>
+      <li
+        className=""
+        key="bugs"
+      >
+        <span
+          className="text-limited"
+        >
+          <strong>
+            3
+          </strong>
+           
+          Bugs
+           quality_gates.conditions.leak
+        </span>
+        <span
+          className="big-spacer-left text-danger"
+        >
+          quality_gates.operator.GT.short
+           
+          0
+        </span>
+      </li>
+    </ul>
+  </div>
+</Link>
+`;
index 2c31f8e7d5b27e3cca9052628bcdc8c9e5c4fa01..e286b3da99d7346414e4305077ec517ec97938e1 100644 (file)
@@ -26,23 +26,9 @@ exports[`new_maintainability_rating 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "3",
-            "metric": Object {
-              "key": "new_maintainability_rating",
-              "name": "new_maintainability_rating",
-              "type": "RATING",
-            },
-            "periods": Array [
-              Object {
-                "index": 1,
-                "value": "3",
-              },
-            ],
-            "value": "3",
-          }
-        }
+        metricKey="new_maintainability_rating"
+        metricType="RATING"
+        value="3"
       />
     </div>
     <div>
@@ -82,23 +68,9 @@ exports[`new_open_issues 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "10",
-            "metric": Object {
-              "key": "new_open_issues",
-              "name": "new_open_issues",
-              "type": "INT",
-            },
-            "periods": Array [
-              Object {
-                "index": 1,
-                "value": "10",
-              },
-            ],
-            "value": "10",
-          }
-        }
+        metricKey="new_open_issues"
+        metricType="INT"
+        value="10"
       />
     </div>
     <div>
@@ -150,23 +122,9 @@ exports[`new_reliability_rating 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "3",
-            "metric": Object {
-              "key": "new_reliability_rating",
-              "name": "new_reliability_rating",
-              "type": "RATING",
-            },
-            "periods": Array [
-              Object {
-                "index": 1,
-                "value": "3",
-              },
-            ],
-            "value": "3",
-          }
-        }
+        metricKey="new_reliability_rating"
+        metricType="RATING"
+        value="3"
       />
     </div>
     <div>
@@ -218,23 +176,9 @@ exports[`new_security_rating 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "3",
-            "metric": Object {
-              "key": "new_security_rating",
-              "name": "new_security_rating",
-              "type": "RATING",
-            },
-            "periods": Array [
-              Object {
-                "index": 1,
-                "value": "3",
-              },
-            ],
-            "value": "3",
-          }
-        }
+        metricKey="new_security_rating"
+        metricType="RATING"
+        value="3"
       />
     </div>
     <div>
@@ -274,17 +218,9 @@ exports[`open_issues 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "10",
-            "metric": Object {
-              "key": "open_issues",
-              "name": "Open open_issues",
-              "type": "INT",
-            },
-            "value": "10",
-          }
-        }
+        metricKey="open_issues"
+        metricType="INT"
+        value="10"
       />
     </div>
     <div>
@@ -335,17 +271,9 @@ exports[`reliability_rating 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "3",
-            "metric": Object {
-              "key": "reliability_rating",
-              "name": "reliability_rating",
-              "type": "RATING",
-            },
-            "value": "3",
-          }
-        }
+        metricKey="reliability_rating"
+        metricType="RATING"
+        value="3"
       />
     </div>
     <div>
@@ -396,17 +324,9 @@ exports[`security_rating 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "3",
-            "metric": Object {
-              "key": "security_rating",
-              "name": "security_rating",
-              "type": "RATING",
-            },
-            "value": "3",
-          }
-        }
+        metricKey="security_rating"
+        metricType="RATING"
+        value="3"
       />
     </div>
     <div>
@@ -457,17 +377,9 @@ exports[`should work with branch 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "3",
-            "metric": Object {
-              "key": "new_maintainability_rating",
-              "name": "new_maintainability_rating",
-              "type": "RATING",
-            },
-            "value": "3",
-          }
-        }
+        metricKey="new_maintainability_rating"
+        metricType="RATING"
+        value="3"
       />
     </div>
     <div>
@@ -517,17 +429,9 @@ exports[`sqale_rating 1`] = `
     >
       <Measure
         decimals={null}
-        measure={
-          Object {
-            "leak": "3",
-            "metric": Object {
-              "key": "sqale_rating",
-              "name": "sqale_rating",
-              "type": "RATING",
-            },
-            "value": "3",
-          }
-        }
+        metricKey="sqale_rating"
+        metricType="RATING"
+        value="3"
       />
     </div>
     <div>
index 69bd9c4b254941dafb7b41540cc2454c7e8487b5..4029dbe7a383a1fa26f7f8a15cfc0eda92d58ddf 100644 (file)
@@ -42,11 +42,11 @@ export default function Effort({ component, effort, metricKey }: Props) {
             <Link to={getComponentDrilldownUrl(component, metricKey)}>
               <span>
                 <Measure
-                  measure={{
-                    metric: { key: 'projects', type: 'SHORT_INT' },
-                    value: String(effort.projects)
-                  }}
-                />{' '}
+                  className="little-spacer-right"
+                  metricKey="projects"
+                  metricType="SHORT_INT"
+                  value={String(effort.projects)}
+                />
                 {translate('projects_')}
               </span>
             </Link>
index 5a57b31cd9dd876e9d44efc2f459b39b236dda3d..ff7a2dd2f157bd9eba4e1957d08fb954b1c0b8ef 100644 (file)
@@ -55,8 +55,11 @@ export default function ReleasabilityBox({ component, measures }: Props) {
             <Link to={getComponentDrilldownUrl(component, 'alert_status')}>
               <span>
                 <Measure
-                  measure={{ metric: { key: 'projects', type: 'SHORT_INT' }, value: effort }}
-                />{' '}
+                  className="little-spacer-right"
+                  metricKey="projects"
+                  metricType="SHORT_INT"
+                  value={effort}
+                />
                 {Number(effort) === 1 ? 'project' : 'projects'}
               </span>
             </Link>{' '}
index 3279b530d70853aad6ee8c8ce9f37f4db2257d1f..75ef3f327237c715b6ab71c364623f9f239a7ae1 100644 (file)
@@ -41,9 +41,7 @@ export default function Summary({ component, measures }: Props) {
         <li>
           <div className="portfolio-measure-secondary-value">
             <Link to={getComponentDrilldownUrl(component.key, 'projects')}>
-              <Measure
-                measure={{ metric: { key: 'projects', type: 'SHORT_INT' }, value: projects }}
-              />
+              <Measure metricKey="projects" metricType="SHORT_INT" value={projects} />
             </Link>
           </div>
           <div className="spacer-top text-muted">{translate('projects')}</div>
@@ -51,7 +49,7 @@ export default function Summary({ component, measures }: Props) {
         <li>
           <div className="portfolio-measure-secondary-value">
             <Link to={getComponentDrilldownUrl(component.key, 'ncloc')}>
-              <Measure measure={{ metric: { key: 'ncloc', type: 'SHORT_INT' }, value: ncloc }} />
+              <Measure metricKey="ncloc" metricType="SHORT_INT" value={ncloc} />
             </Link>
           </div>
           <div className="spacer-top text-muted">{translate('metric.ncloc.name')}</div>
index 3e29710488240ec3db33b43674b1599ca5fcfeac..2dc04f3278b2c54ae35f4cee5a93ceec687568e8 100644 (file)
@@ -110,7 +110,7 @@ export default function WorstProjects({ component, subComponents, total }: Props
 function renderCell(measures: { [key: string]: string | undefined }, metric: string, type: string) {
   return (
     <td className="text-center">
-      <Measure measure={{ metric: { key: metric, type }, value: measures[metric] }} />
+      <Measure metricKey={metric} metricType={type} value={measures[metric]} />
     </td>
   );
 }
@@ -121,12 +121,7 @@ function renderNcloc(measures: { [key: string]: string | undefined }, maxLoc: nu
   return (
     <td className="text-right">
       <span className="note">
-        <Measure
-          measure={{
-            metric: { key: 'ncloc', type: 'SHORT_INT' },
-            value: measures['ncloc']
-          }}
-        />
+        <Measure metricKey="ncloc" metricType="SHORT_INT" value={measures['ncloc']} />
       </span>
       {maxLoc > 0 && (
         <svg width="50" height="16" className="spacer-left">
index 989531cad1e51817fff8419b3d239a4b9bb18106..8918c9e4b99d48c365bce9d96c1446e79d9af8d2 100644 (file)
@@ -25,17 +25,11 @@ exports[`renders 1`] = `
         >
           <span>
             <Measure
-              measure={
-                Object {
-                  "metric": Object {
-                    "key": "projects",
-                    "type": "SHORT_INT",
-                  },
-                  "value": "3",
-                }
-              }
+              className="little-spacer-right"
+              metricKey="projects"
+              metricType="SHORT_INT"
+              value="3"
             />
-             
             projects_
           </span>
         </Link>,
index 3db58b7d539d04c2b0516326ff79fd8c5ce9b1be..c2120360b568db2797b83e0866fc43caae0e3f35 100644 (file)
@@ -50,17 +50,11 @@ exports[`renders 1`] = `
     >
       <span>
         <Measure
-          measure={
-            Object {
-              "metric": Object {
-                "key": "projects",
-                "type": "SHORT_INT",
-              },
-              "value": "7",
-            }
-          }
+          className="little-spacer-right"
+          metricKey="projects"
+          metricType="SHORT_INT"
+          value="7"
         />
-         
         projects
       </span>
     </Link>
index f8f2706777dd8f3c63d9a259de92a7032290a1ba..149c818b286ce9f1540d98458e384191809b6fb3 100644 (file)
@@ -32,15 +32,9 @@ exports[`renders 1`] = `
           }
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "projects",
-                  "type": "SHORT_INT",
-                },
-                "value": "15",
-              }
-            }
+            metricKey="projects"
+            metricType="SHORT_INT"
+            value="15"
           />
         </Link>
       </div>
@@ -69,15 +63,9 @@ exports[`renders 1`] = `
           }
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "ncloc",
-                  "type": "SHORT_INT",
-                },
-                "value": "1234",
-              }
-            }
+            metricKey="ncloc"
+            metricType="SHORT_INT"
+            value="1234"
           />
         </Link>
       </div>
index c36279d6709f2afab64eb7b70eab5a4d83cb5d0c..736ab24806037a3c2bc3c6a2b5acb82176635d18 100644 (file)
@@ -70,60 +70,36 @@ exports[`renders 1`] = `
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "releasability_rating",
-                  "type": "RATING",
-                },
-                "value": "3",
-              }
-            }
+            metricKey="releasability_rating"
+            metricType="RATING"
+            value="3"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "reliability_rating",
-                  "type": "RATING",
-                },
-                "value": "2",
-              }
-            }
+            metricKey="reliability_rating"
+            metricType="RATING"
+            value="2"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "security_rating",
-                  "type": "RATING",
-                },
-                "value": "1",
-              }
-            }
+            metricKey="security_rating"
+            metricType="RATING"
+            value="1"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "sqale_rating",
-                  "type": "RATING",
-                },
-                "value": "4",
-              }
-            }
+            metricKey="sqale_rating"
+            metricType="RATING"
+            value="4"
           />
         </td>
         <td
@@ -133,15 +109,9 @@ exports[`renders 1`] = `
             className="note"
           >
             <Measure
-              measure={
-                Object {
-                  "metric": Object {
-                    "key": "ncloc",
-                    "type": "SHORT_INT",
-                  },
-                  "value": "200",
-                }
-              }
+              metricKey="ncloc"
+              metricType="SHORT_INT"
+              value="200"
             />
           </span>
           <svg
@@ -188,60 +158,36 @@ exports[`renders 1`] = `
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "alert_status",
-                  "type": "LEVEL",
-                },
-                "value": "ERROR",
-              }
-            }
+            metricKey="alert_status"
+            metricType="LEVEL"
+            value="ERROR"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "reliability_rating",
-                  "type": "RATING",
-                },
-                "value": "2",
-              }
-            }
+            metricKey="reliability_rating"
+            metricType="RATING"
+            value="2"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "security_rating",
-                  "type": "RATING",
-                },
-                "value": "1",
-              }
-            }
+            metricKey="security_rating"
+            metricType="RATING"
+            value="1"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "sqale_rating",
-                  "type": "RATING",
-                },
-                "value": "4",
-              }
-            }
+            metricKey="sqale_rating"
+            metricType="RATING"
+            value="4"
           />
         </td>
         <td
@@ -251,15 +197,9 @@ exports[`renders 1`] = `
             className="note"
           >
             <Measure
-              measure={
-                Object {
-                  "metric": Object {
-                    "key": "ncloc",
-                    "type": "SHORT_INT",
-                  },
-                  "value": "100",
-                }
-              }
+              metricKey="ncloc"
+              metricType="SHORT_INT"
+              value="100"
             />
           </span>
           <svg
@@ -306,60 +246,36 @@ exports[`renders 1`] = `
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "alert_status",
-                  "type": "LEVEL",
-                },
-                "value": "WARN",
-              }
-            }
+            metricKey="alert_status"
+            metricType="LEVEL"
+            value="WARN"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "reliability_rating",
-                  "type": "RATING",
-                },
-                "value": "2",
-              }
-            }
+            metricKey="reliability_rating"
+            metricType="RATING"
+            value="2"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "security_rating",
-                  "type": "RATING",
-                },
-                "value": "1",
-              }
-            }
+            metricKey="security_rating"
+            metricType="RATING"
+            value="1"
           />
         </td>
         <td
           className="text-center"
         >
           <Measure
-            measure={
-              Object {
-                "metric": Object {
-                  "key": "sqale_rating",
-                  "type": "RATING",
-                },
-                "value": "4",
-              }
-            }
+            metricKey="sqale_rating"
+            metricType="RATING"
+            value="4"
           />
         </td>
         <td
@@ -369,15 +285,9 @@ exports[`renders 1`] = `
             className="note"
           >
             <Measure
-              measure={
-                Object {
-                  "metric": Object {
-                    "key": "ncloc",
-                    "type": "SHORT_INT",
-                  },
-                  "value": "150",
-                }
-              }
+              metricKey="ncloc"
+              metricType="SHORT_INT"
+              value="150"
             />
           </span>
           <svg
index 25157da7d59bab33aac7353c66663857c7e48bad..51d28646604cabe66787a2cb8d54f03a3f0ffa3b 100644 (file)
@@ -198,7 +198,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
             value: analysis.value
           }))
         })),
-      throwGlobalError
+      () => {}
     );
   };
 
index 4efd5855aff12b73d7ee51b777aaf3014895c3f4..50c440bda9ca32ffd5d954709c0d219df4d48819 100644 (file)
@@ -37,10 +37,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
           <div className="project-card-measure-number">
             <Measure
               className="spacer-right"
-              measure={{
-                metric: { key: 'new_bugs', type: 'SHORT_INT' },
-                leak: measures['new_bugs']
-              }}
+              metricKey="new_bugs"
+              metricType="SHORT_INT"
+              value={measures['new_bugs']}
             />
             <Rating value={measures['new_reliability_rating']} />
           </div>
@@ -56,10 +55,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
           <div className="project-card-measure-number">
             <Measure
               className="spacer-right"
-              measure={{
-                metric: { key: 'new_vulnerabilities', type: 'SHORT_INT' },
-                leak: measures['new_vulnerabilities']
-              }}
+              metricKey="new_vulnerabilities"
+              metricType="SHORT_INT"
+              value={measures['new_vulnerabilities']}
             />
             <Rating value={measures['new_security_rating']} />
           </div>
@@ -75,10 +73,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
           <div className="project-card-measure-number">
             <Measure
               className="spacer-right"
-              measure={{
-                metric: { key: 'new_code_smells', type: 'SHORT_INT' },
-                leak: measures['new_code_smells']
-              }}
+              metricKey="new_code_smells"
+              metricType="SHORT_INT"
+              value={measures['new_code_smells']}
             />
             <Rating value={measures['new_maintainability_rating']} />
           </div>
@@ -93,10 +90,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
         <div className="project-card-measure-inner">
           <div className="project-card-measure-number">
             <Measure
-              measure={{
-                metric: { key: 'new_coverage', type: 'PERCENT' },
-                leak: measures['new_coverage']
-              }}
+              metricKey="new_coverage"
+              metricType="PERCENT"
+              value={measures['new_coverage']}
             />
           </div>
           <div className="project-card-measure-label">{translate('metric.coverage.name')}</div>
@@ -107,10 +103,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
         <div className="project-card-measure-inner">
           <div className="project-card-measure-number">
             <Measure
-              measure={{
-                metric: { key: 'new_duplicated_lines_density', type: 'PERCENT' },
-                leak: measures['new_duplicated_lines_density']
-              }}
+              metricKey="new_duplicated_lines_density"
+              metricType="PERCENT"
+              value={measures['new_duplicated_lines_density']}
             />
           </div>
           <div className="project-card-measure-label">
@@ -122,12 +117,7 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
       <div className="project-card-measure smaller-card project-card-ncloc" data-key="new_lines">
         <div className="project-card-measure-inner">
           <div className="project-card-measure-number">
-            <Measure
-              measure={{
-                metric: { key: 'new_lines', type: 'SHORT_INT' },
-                leak: measures['new_lines']
-              }}
-            />
+            <Measure metricKey="new_lines" metricType="SHORT_INT" value={measures['new_lines']} />
           </div>
           <div className="project-card-measure-label">{translate('metric.lines.name')}</div>
         </div>
index 63dba2d25f4b52968dfed7189d411e483a05df53..e11c8e5b80dd54ecaabb395e23c5deef4dfcfc4e 100644 (file)
@@ -45,10 +45,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
           <div className="project-card-measure-number">
             <Measure
               className="spacer-right"
-              measure={{
-                metric: { key: 'bugs', type: 'SHORT_INT' },
-                value: measures['bugs']
-              }}
+              metricKey="bugs"
+              metricType="SHORT_INT"
+              value={measures['bugs']}
             />
             <Rating value={measures['reliability_rating']} />
           </div>
@@ -64,10 +63,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
           <div className="project-card-measure-number">
             <Measure
               className="spacer-right"
-              measure={{
-                metric: { key: 'vulnerabilities', type: 'SHORT_INT' },
-                value: measures['vulnerabilities']
-              }}
+              metricKey="vulnerabilities"
+              metricType="SHORT_INT"
+              value={measures['vulnerabilities']}
             />
             <Rating value={measures['security_rating']} />
           </div>
@@ -83,10 +81,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
           <div className="project-card-measure-number">
             <Measure
               className="spacer-right"
-              measure={{
-                metric: { key: 'code_smells', type: 'SHORT_INT' },
-                value: measures['code_smells']
-              }}
+              metricKey="code_smells"
+              metricType="SHORT_INT"
+              value={measures['code_smells']}
             />
             <Rating value={measures['sqale_rating']} />
           </div>
@@ -105,12 +102,7 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
                 <CoverageRating value={measures['coverage']} />
               </span>
             )}
-            <Measure
-              measure={{
-                metric: { key: 'coverage', type: 'PERCENT' },
-                value: measures['coverage']
-              }}
-            />
+            <Measure metricKey="coverage" metricType="PERCENT" value={measures['coverage']} />
           </div>
           <div className="project-card-measure-label">{translate('metric.coverage.name')}</div>
         </div>
@@ -125,10 +117,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
               </span>
             )}
             <Measure
-              measure={{
-                metric: { key: 'duplicated_lines_density', type: 'PERCENT' },
-                value: measures['duplicated_lines_density']
-              }}
+              metricKey="duplicated_lines_density"
+              metricType="PERCENT"
+              value={measures['duplicated_lines_density']}
             />
           </div>
           <div className="project-card-measure-label">
@@ -141,12 +132,7 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
         <div className="project-card-measure project-card-ncloc" data-key="ncloc">
           <div className="project-card-measure-inner pull-right">
             <div className="project-card-measure-number">
-              <Measure
-                measure={{
-                  metric: { key: 'ncloc', type: 'SHORT_INT' },
-                  value: measures['ncloc']
-                }}
-              />
+              <Measure metricKey="ncloc" metricType="SHORT_INT" value={measures['ncloc']} />
               <span className="spacer-left">
                 <SizeRating value={Number(measures['ncloc'])} />
               </span>
index 0ccdace41b14095d4c805a5ed98719fe3136b77f..5dbdbd4a07406e44bff3bd01a04623c5636ebc09 100644 (file)
@@ -16,15 +16,9 @@ exports[`should render correctly with all data 1`] = `
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "leak": "8",
-              "metric": Object {
-                "key": "new_bugs",
-                "type": "SHORT_INT",
-              },
-            }
-          }
+          metricKey="new_bugs"
+          metricType="SHORT_INT"
+          value="8"
         />
         <Rating
           value="1.0"
@@ -52,15 +46,9 @@ exports[`should render correctly with all data 1`] = `
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "leak": "2",
-              "metric": Object {
-                "key": "new_vulnerabilities",
-                "type": "SHORT_INT",
-              },
-            }
-          }
+          metricKey="new_vulnerabilities"
+          metricType="SHORT_INT"
+          value="2"
         />
         <Rating
           value="2.0"
@@ -88,15 +76,9 @@ exports[`should render correctly with all data 1`] = `
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "leak": "0",
-              "metric": Object {
-                "key": "new_code_smells",
-                "type": "SHORT_INT",
-              },
-            }
-          }
+          metricKey="new_code_smells"
+          metricType="SHORT_INT"
+          value="0"
         />
         <Rating
           value="1.0"
@@ -123,15 +105,9 @@ exports[`should render correctly with all data 1`] = `
         className="project-card-measure-number"
       >
         <Measure
-          measure={
-            Object {
-              "leak": "26.55",
-              "metric": Object {
-                "key": "new_coverage",
-                "type": "PERCENT",
-              },
-            }
-          }
+          metricKey="new_coverage"
+          metricType="PERCENT"
+          value="26.55"
         />
       </div>
       <div
@@ -152,15 +128,9 @@ exports[`should render correctly with all data 1`] = `
         className="project-card-measure-number"
       >
         <Measure
-          measure={
-            Object {
-              "leak": "0.55",
-              "metric": Object {
-                "key": "new_duplicated_lines_density",
-                "type": "PERCENT",
-              },
-            }
-          }
+          metricKey="new_duplicated_lines_density"
+          metricType="PERCENT"
+          value="0.55"
         />
       </div>
       <div
@@ -181,15 +151,9 @@ exports[`should render correctly with all data 1`] = `
         className="project-card-measure-number"
       >
         <Measure
-          measure={
-            Object {
-              "leak": "87",
-              "metric": Object {
-                "key": "new_lines",
-                "type": "SHORT_INT",
-              },
-            }
-          }
+          metricKey="new_lines"
+          metricType="SHORT_INT"
+          value="87"
         />
       </div>
       <div
@@ -218,15 +182,9 @@ exports[`should render no data style new coverage, new duplications and new line
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "leak": "8",
-              "metric": Object {
-                "key": "new_bugs",
-                "type": "SHORT_INT",
-              },
-            }
-          }
+          metricKey="new_bugs"
+          metricType="SHORT_INT"
+          value="8"
         />
         <Rating
           value="1.0"
@@ -254,15 +212,9 @@ exports[`should render no data style new coverage, new duplications and new line
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "leak": "2",
-              "metric": Object {
-                "key": "new_vulnerabilities",
-                "type": "SHORT_INT",
-              },
-            }
-          }
+          metricKey="new_vulnerabilities"
+          metricType="SHORT_INT"
+          value="2"
         />
         <Rating
           value="2.0"
@@ -290,15 +242,9 @@ exports[`should render no data style new coverage, new duplications and new line
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "leak": "0",
-              "metric": Object {
-                "key": "new_code_smells",
-                "type": "SHORT_INT",
-              },
-            }
-          }
+          metricKey="new_code_smells"
+          metricType="SHORT_INT"
+          value="0"
         />
         <Rating
           value="1.0"
@@ -325,15 +271,8 @@ exports[`should render no data style new coverage, new duplications and new line
         className="project-card-measure-number"
       >
         <Measure
-          measure={
-            Object {
-              "leak": undefined,
-              "metric": Object {
-                "key": "new_coverage",
-                "type": "PERCENT",
-              },
-            }
-          }
+          metricKey="new_coverage"
+          metricType="PERCENT"
         />
       </div>
       <div
@@ -354,15 +293,8 @@ exports[`should render no data style new coverage, new duplications and new line
         className="project-card-measure-number"
       >
         <Measure
-          measure={
-            Object {
-              "leak": undefined,
-              "metric": Object {
-                "key": "new_duplicated_lines_density",
-                "type": "PERCENT",
-              },
-            }
-          }
+          metricKey="new_duplicated_lines_density"
+          metricType="PERCENT"
         />
       </div>
       <div
@@ -383,15 +315,8 @@ exports[`should render no data style new coverage, new duplications and new line
         className="project-card-measure-number"
       >
         <Measure
-          measure={
-            Object {
-              "leak": undefined,
-              "metric": Object {
-                "key": "new_lines",
-                "type": "SHORT_INT",
-              },
-            }
-          }
+          metricKey="new_lines"
+          metricType="SHORT_INT"
         />
       </div>
       <div
index 74ef1f08cfeeaeeb580a5c5868c3f3ab94ae5bf7..2c300ec6258c5d3ad3f6e2e0fa5d4f63987f9f38 100644 (file)
@@ -12,15 +12,8 @@ exports[`should not render coverage 1`] = `
       className="project-card-measure-number"
     >
       <Measure
-        measure={
-          Object {
-            "metric": Object {
-              "key": "coverage",
-              "type": "PERCENT",
-            },
-            "value": undefined,
-          }
-        }
+        metricKey="coverage"
+        metricType="PERCENT"
       />
     </div>
     <div
@@ -44,15 +37,8 @@ exports[`should not render duplications 1`] = `
       className="project-card-measure-number"
     >
       <Measure
-        measure={
-          Object {
-            "metric": Object {
-              "key": "duplicated_lines_density",
-              "type": "PERCENT",
-            },
-            "value": undefined,
-          }
-        }
+        metricKey="duplicated_lines_density"
+        metricType="PERCENT"
       />
     </div>
     <div
@@ -80,15 +66,9 @@ exports[`should render correctly with all data 1`] = `
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "metric": Object {
-                "key": "bugs",
-                "type": "SHORT_INT",
-              },
-              "value": "17",
-            }
-          }
+          metricKey="bugs"
+          metricType="SHORT_INT"
+          value="17"
         />
         <Rating
           value="1.0"
@@ -116,15 +96,9 @@ exports[`should render correctly with all data 1`] = `
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "metric": Object {
-                "key": "vulnerabilities",
-                "type": "SHORT_INT",
-              },
-              "value": "0",
-            }
-          }
+          metricKey="vulnerabilities"
+          metricType="SHORT_INT"
+          value="0"
         />
         <Rating
           value="1.0"
@@ -152,15 +126,9 @@ exports[`should render correctly with all data 1`] = `
       >
         <Measure
           className="spacer-right"
-          measure={
-            Object {
-              "metric": Object {
-                "key": "code_smells",
-                "type": "SHORT_INT",
-              },
-              "value": "132",
-            }
-          }
+          metricKey="code_smells"
+          metricType="SHORT_INT"
+          value="132"
         />
         <Rating
           value="1.0"
@@ -194,15 +162,9 @@ exports[`should render correctly with all data 1`] = `
           />
         </span>
         <Measure
-          measure={
-            Object {
-              "metric": Object {
-                "key": "coverage",
-                "type": "PERCENT",
-              },
-              "value": "88.3",
-            }
-          }
+          metricKey="coverage"
+          metricType="PERCENT"
+          value="88.3"
         />
       </div>
       <div
@@ -230,15 +192,9 @@ exports[`should render correctly with all data 1`] = `
           />
         </span>
         <Measure
-          measure={
-            Object {
-              "metric": Object {
-                "key": "duplicated_lines_density",
-                "type": "PERCENT",
-              },
-              "value": "9.8",
-            }
-          }
+          metricKey="duplicated_lines_density"
+          metricType="PERCENT"
+          value="9.8"
         />
       </div>
       <div
@@ -259,15 +215,9 @@ exports[`should render correctly with all data 1`] = `
         className="project-card-measure-number"
       >
         <Measure
-          measure={
-            Object {
-              "metric": Object {
-                "key": "ncloc",
-                "type": "SHORT_INT",
-              },
-              "value": "2053",
-            }
-          }
+          metricKey="ncloc"
+          metricType="SHORT_INT"
+          value="2053"
         />
         <span
           className="spacer-left"
@@ -299,15 +249,9 @@ exports[`should render ncloc correctly 1`] = `
       className="project-card-measure-number"
     >
       <Measure
-        measure={
-          Object {
-            "metric": Object {
-              "key": "ncloc",
-              "type": "SHORT_INT",
-            },
-            "value": "16549887",
-          }
-        }
+        metricKey="ncloc"
+        metricType="SHORT_INT"
+        value="16549887"
       />
       <span
         className="spacer-left"
index 091bd3c95b5f741788571f6d3daf531be4e35422..963333ba5c1ada0bcf7a4c8fdd2c7eff386eb14d 100644 (file)
@@ -82,11 +82,14 @@ export default class TagsFilter extends React.PureComponent<Props, State> {
       searchProjectTags({
         q: search,
         ps: size(this.props.facet || {}) + LIST_SIZE
-      }).then(result => {
-        if (this.mounted) {
-          this.setState({ isLoading: false, tags: result.tags });
-        }
-      });
+      }).then(
+        result => {
+          if (this.mounted) {
+            this.setState({ isLoading: false, tags: result.tags });
+          }
+        },
+        () => {}
+      );
     }
   };
 
index deb5f3d7882b5067a8a70dbd3f93ee97d673e7a2..18b7dbd6f95f16a295f4cbbbcf7e47f7aedd41d3 100644 (file)
 import * as React from 'react';
 import * as classNames from 'classnames';
 
+export interface BubblePopupPosition {
+  top: number;
+  right: number;
+}
+
 interface Props {
   customClass?: string;
   children: React.ReactNode;
-  position: { top: number; right: number };
+  position: BubblePopupPosition;
 }
 
 export default function BubblePopup(props: Props) {
diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.js b/server/sonar-web/src/main/js/components/common/MultiSelect.js
deleted file mode 100644 (file)
index 2921ab1..0000000
+++ /dev/null
@@ -1,281 +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 { difference } from 'lodash';
-import MultiSelectOption from './MultiSelectOption';
-import SearchBox from '../controls/SearchBox';
-import { translate } from '../../helpers/l10n';
-
-/*::
-type Props = {
-  selectedElements: Array<string>,
-  elements: Array<string>,
-  listSize: number,
-  onSearch: string => void,
-  onSelect: string => void,
-  onUnselect: string => void,
-  validateSearchInput: string => string,
-  placeholder: string
-};
-*/
-
-/*::
-type State = {
-  query: string,
-  selectedElements: Array<string>,
-  unselectedElements: Array<string>,
-  activeIdx: number
-};
-*/
-
-export default class MultiSelect extends React.PureComponent {
-  /*:: container: HTMLElement; */
-  /*:: searchInput: HTMLInputElement; */
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  static defaultProps = {
-    listSize: 10,
-    validateSearchInput: (value /*: string */) => value
-  };
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      query: '',
-      selectedElements: [],
-      unselectedElements: [],
-      activeIdx: 0
-    };
-  }
-
-  componentDidMount() {
-    this.updateSelectedElements(this.props);
-    this.updateUnselectedElements(this.props);
-    if (this.container) {
-      this.container.addEventListener('keydown', this.handleKeyboard, true);
-    }
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    if (
-      this.props.elements !== nextProps.elements ||
-      this.props.selectedElements !== nextProps.selectedElements
-    ) {
-      this.updateSelectedElements(nextProps);
-      this.updateUnselectedElements(nextProps);
-
-      const totalElements = this.getAllElements(nextProps, this.state).length;
-      if (this.state.activeIdx >= totalElements) {
-        this.setState({ activeIdx: totalElements - 1 });
-      }
-    }
-  }
-
-  componentDidUpdate() {
-    this.searchInput && this.searchInput.focus();
-  }
-
-  componentWillUnmount() {
-    this.container.removeEventListener('keydown', this.handleKeyboard);
-  }
-
-  handleSelectChange = (item /*: string */, selected /*: boolean */) => {
-    if (selected) {
-      this.onSelectItem(item);
-    } else {
-      this.onUnselectItem(item);
-    }
-  };
-
-  handleSearchChange = (value /*: string */) => {
-    this.onSearchQuery(this.props.validateSearchInput(value));
-  };
-
-  handleElementHover = (element /*: string */) => {
-    this.setState((prevState, props) => {
-      return { activeIdx: this.getAllElements(props, prevState).indexOf(element) };
-    });
-  };
-
-  handleKeyboard = (evt /*: KeyboardEvent */) => {
-    switch (evt.keyCode) {
-      case 40: // down
-        this.setState(this.selectNextElement);
-        evt.stopPropagation();
-        evt.preventDefault();
-        break;
-      case 38: // up
-        this.setState(this.selectPreviousElement);
-        evt.stopPropagation();
-        evt.preventDefault();
-        break;
-      case 37: // left
-      case 39: // right
-        evt.stopPropagation();
-        break;
-      case 13: // enter
-        if (this.state.activeIdx >= 0) {
-          this.toggleSelect(this.getAllElements(this.props, this.state)[this.state.activeIdx]);
-        }
-        break;
-    }
-  };
-
-  onSearchQuery(query /*: string */) {
-    this.setState({ query, activeIdx: 0 });
-    this.props.onSearch(query);
-  }
-
-  onSelectItem(item /*: string */) {
-    if (this.isNewElement(item, this.props)) {
-      this.onSearchQuery('');
-    }
-    this.props.onSelect(item);
-  }
-
-  onUnselectItem(item /*: string */) {
-    this.props.onUnselect(item);
-  }
-
-  isNewElement(elem /*: string */, { selectedElements, elements } /*: Props */) {
-    return elem && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1;
-  }
-
-  updateSelectedElements(props /*: Props */) {
-    this.setState((state /*: State */) => {
-      if (state.query) {
-        return {
-          selectedElements: [...props.selectedElements.filter(elem => elem.includes(state.query))]
-        };
-      } else {
-        return { selectedElements: [...props.selectedElements] };
-      }
-    });
-  }
-
-  updateUnselectedElements(props /*: Props */) {
-    this.setState((state /*: State */) => {
-      if (props.listSize < state.selectedElements.length) {
-        return { unselectedElements: [] };
-      } else {
-        return {
-          unselectedElements: difference(props.elements, props.selectedElements).slice(
-            0,
-            props.listSize - state.selectedElements.length
-          )
-        };
-      }
-    });
-  }
-
-  getAllElements(props /*: Props */, state /*: State */) {
-    if (this.isNewElement(state.query, props)) {
-      return [...state.selectedElements, ...state.unselectedElements, state.query];
-    } else {
-      return [...state.selectedElements, ...state.unselectedElements];
-    }
-  }
-
-  setElementActive(idx /*: number */) {
-    this.setState({ activeIdx: idx });
-  }
-
-  selectNextElement = (state /*: State */, props /*: Props */) => {
-    const { activeIdx } = state;
-    const allElements = this.getAllElements(props, state);
-    if (activeIdx < 0 || activeIdx >= allElements.length - 1) {
-      return { activeIdx: 0 };
-    } else {
-      return { activeIdx: activeIdx + 1 };
-    }
-  };
-
-  selectPreviousElement = (state /*: State */, props /*: Props */) => {
-    const { activeIdx } = state;
-    const allElements = this.getAllElements(props, state);
-    if (activeIdx <= 0) {
-      const lastIdx = allElements.length - 1;
-      return { activeIdx: lastIdx };
-    } else {
-      return { activeIdx: activeIdx - 1 };
-    }
-  };
-
-  toggleSelect(item /*: string */) {
-    if (this.props.selectedElements.indexOf(item) === -1) {
-      this.onSelectItem(item);
-    } else {
-      this.onUnselectItem(item);
-    }
-  }
-
-  render() {
-    const { query, activeIdx, selectedElements, unselectedElements } = this.state;
-    const activeElement = this.getAllElements(this.props, this.state)[activeIdx];
-
-    return (
-      <div className="multi-select" ref={div => (this.container = div)}>
-        <div className="menu-search">
-          <SearchBox
-            autoFocus={true}
-            onChange={this.handleSearchChange}
-            placeholder={this.props.placeholder}
-            value={query}
-          />
-        </div>
-        <ul className="menu">
-          {selectedElements.length > 0 &&
-            selectedElements.map(element => (
-              <MultiSelectOption
-                key={element}
-                element={element}
-                selected={true}
-                active={activeElement === element}
-                onSelectChange={this.handleSelectChange}
-                onHover={this.handleElementHover}
-              />
-            ))}
-          {unselectedElements.length > 0 &&
-            unselectedElements.map(element => (
-              <MultiSelectOption
-                key={element}
-                element={element}
-                active={activeElement === element}
-                onSelectChange={this.handleSelectChange}
-                onHover={this.handleElementHover}
-              />
-            ))}
-          {this.isNewElement(query, this.props) && (
-            <MultiSelectOption
-              key={query}
-              element={query}
-              custom={true}
-              active={activeElement === query}
-              onSelectChange={this.handleSelectChange}
-              onHover={this.handleElementHover}
-            />
-          )}
-        </ul>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx
new file mode 100644 (file)
index 0000000..b88ccd4
--- /dev/null
@@ -0,0 +1,279 @@
+/*
+ * 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 { difference } from 'lodash';
+import MultiSelectOption from './MultiSelectOption';
+import SearchBox from '../controls/SearchBox';
+
+interface Props {
+  selectedElements: Array<string>;
+  elements: Array<string>;
+  listSize?: number;
+  onSearch: (query: string) => void;
+  onSelect: (item: string) => void;
+  onUnselect: (item: string) => void;
+  validateSearchInput?: (value: string) => string;
+  placeholder: string;
+}
+
+interface State {
+  query: string;
+  selectedElements: Array<string>;
+  unselectedElements: Array<string>;
+  activeIdx: number;
+}
+
+interface DefaultProps {
+  listSize: number;
+  validateSearchInput: (value: string) => string;
+}
+
+type PropsWithDefault = Props & DefaultProps;
+
+export default class MultiSelect extends React.PureComponent<Props, State> {
+  container: HTMLDivElement | null;
+  searchInput: HTMLInputElement | null;
+
+  static defaultProps: DefaultProps = {
+    listSize: 10,
+    validateSearchInput: (value: string) => value
+  };
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      query: '',
+      selectedElements: [],
+      unselectedElements: [],
+      activeIdx: 0
+    };
+  }
+
+  componentDidMount() {
+    this.updateSelectedElements(this.props);
+    this.updateUnselectedElements(this.props as PropsWithDefault);
+    if (this.container) {
+      this.container.addEventListener('keydown', this.handleKeyboard, true);
+    }
+  }
+
+  componentWillReceiveProps(nextProps: PropsWithDefault) {
+    if (
+      this.props.elements !== nextProps.elements ||
+      this.props.selectedElements !== nextProps.selectedElements
+    ) {
+      this.updateSelectedElements(nextProps);
+      this.updateUnselectedElements(nextProps);
+
+      const totalElements = this.getAllElements(nextProps, this.state).length;
+      if (this.state.activeIdx >= totalElements) {
+        this.setState({ activeIdx: totalElements - 1 });
+      }
+    }
+  }
+
+  componentDidUpdate() {
+    if (this.searchInput) {
+      this.searchInput.focus();
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.container) {
+      this.container.removeEventListener('keydown', this.handleKeyboard);
+    }
+  }
+
+  handleSelectChange = (item: string, selected: boolean) => {
+    if (selected) {
+      this.onSelectItem(item);
+    } else {
+      this.onUnselectItem(item);
+    }
+  };
+
+  handleSearchChange = (value: string) => {
+    this.onSearchQuery((this.props as PropsWithDefault).validateSearchInput(value));
+  };
+
+  handleElementHover = (element: string) => {
+    this.setState((prevState, props) => {
+      return { activeIdx: this.getAllElements(props, prevState).indexOf(element) };
+    });
+  };
+
+  handleKeyboard = (evt: KeyboardEvent) => {
+    switch (evt.keyCode) {
+      case 40: // down
+        this.setState(this.selectNextElement);
+        evt.stopPropagation();
+        evt.preventDefault();
+        break;
+      case 38: // up
+        this.setState(this.selectPreviousElement);
+        evt.stopPropagation();
+        evt.preventDefault();
+        break;
+      case 37: // left
+      case 39: // right
+        evt.stopPropagation();
+        break;
+      case 13: // enter
+        if (this.state.activeIdx >= 0) {
+          this.toggleSelect(this.getAllElements(this.props, this.state)[this.state.activeIdx]);
+        }
+        break;
+    }
+  };
+
+  onSearchQuery = (query: string) => {
+    this.setState({ query, activeIdx: 0 });
+    this.props.onSearch(query);
+  };
+
+  onSelectItem = (item: string) => {
+    if (this.isNewElement(item, this.props)) {
+      this.onSearchQuery('');
+    }
+    this.props.onSelect(item);
+  };
+
+  onUnselectItem = (item: string) => this.props.onUnselect(item);
+
+  isNewElement = (elem: string, { selectedElements, elements }: Props) =>
+    elem && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1;
+
+  updateSelectedElements = (props: Props) => {
+    this.setState((state: State) => {
+      if (state.query) {
+        return {
+          selectedElements: [...props.selectedElements.filter(elem => elem.includes(state.query))]
+        };
+      } else {
+        return { selectedElements: [...props.selectedElements] };
+      }
+    });
+  };
+
+  updateUnselectedElements = (props: PropsWithDefault) => {
+    this.setState((state: State) => {
+      if (props.listSize < state.selectedElements.length) {
+        return { unselectedElements: [] };
+      } else {
+        return {
+          unselectedElements: difference(props.elements, props.selectedElements).slice(
+            0,
+            props.listSize - state.selectedElements.length
+          )
+        };
+      }
+    });
+  };
+
+  getAllElements = (props: Props, state: State) => {
+    if (this.isNewElement(state.query, props)) {
+      return [...state.selectedElements, ...state.unselectedElements, state.query];
+    } else {
+      return [...state.selectedElements, ...state.unselectedElements];
+    }
+  };
+
+  setElementActive = (idx: number) => this.setState({ activeIdx: idx });
+
+  selectNextElement = (state: State, props: Props) => {
+    const { activeIdx } = state;
+    const allElements = this.getAllElements(props, state);
+    if (activeIdx < 0 || activeIdx >= allElements.length - 1) {
+      return { activeIdx: 0 };
+    } else {
+      return { activeIdx: activeIdx + 1 };
+    }
+  };
+
+  selectPreviousElement = (state: State, props: Props) => {
+    const { activeIdx } = state;
+    const allElements = this.getAllElements(props, state);
+    if (activeIdx <= 0) {
+      const lastIdx = allElements.length - 1;
+      return { activeIdx: lastIdx };
+    } else {
+      return { activeIdx: activeIdx - 1 };
+    }
+  };
+
+  toggleSelect = (item: string) => {
+    if (this.props.selectedElements.indexOf(item) === -1) {
+      this.onSelectItem(item);
+    } else {
+      this.onUnselectItem(item);
+    }
+  };
+
+  render() {
+    const { query, activeIdx, selectedElements, unselectedElements } = this.state;
+    const activeElement = this.getAllElements(this.props, this.state)[activeIdx];
+
+    return (
+      <div className="multi-select" ref={div => (this.container = div)}>
+        <div className="menu-search">
+          <SearchBox
+            autoFocus={true}
+            onChange={this.handleSearchChange}
+            placeholder={this.props.placeholder}
+            value={query}
+          />
+        </div>
+        <ul className="menu">
+          {selectedElements.length > 0 &&
+            selectedElements.map(element => (
+              <MultiSelectOption
+                key={element}
+                element={element}
+                selected={true}
+                active={activeElement === element}
+                onSelectChange={this.handleSelectChange}
+                onHover={this.handleElementHover}
+              />
+            ))}
+          {unselectedElements.length > 0 &&
+            unselectedElements.map(element => (
+              <MultiSelectOption
+                key={element}
+                element={element}
+                active={activeElement === element}
+                onSelectChange={this.handleSelectChange}
+                onHover={this.handleElementHover}
+              />
+            ))}
+          {this.isNewElement(query, this.props) && (
+            <MultiSelectOption
+              key={query}
+              element={query}
+              custom={true}
+              active={activeElement === query}
+              onSelectChange={this.handleSelectChange}
+              onHover={this.handleElementHover}
+            />
+          )}
+        </ul>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/common/MultiSelectOption.js b/server/sonar-web/src/main/js/components/common/MultiSelectOption.js
deleted file mode 100644 (file)
index ad144f5..0000000
+++ /dev/null
@@ -1,75 +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';
-
-/*::
-type Props = {
-  element: string,
-  selected: boolean,
-  custom: boolean,
-  active: boolean,
-  onSelectChange: (string, boolean) => void,
-  onHover: string => void
-};
-*/
-
-export default class MultiSelectOption extends React.PureComponent {
-  /*:: props: Props; */
-
-  static defaultProps = {
-    selected: false,
-    custom: false,
-    active: false
-  };
-
-  handleSelect = (evt /*: SyntheticInputEvent */) => {
-    evt.stopPropagation();
-    evt.preventDefault();
-    evt.target.blur();
-    this.props.onSelectChange(this.props.element, !this.props.selected);
-  };
-
-  handleHover = () => {
-    this.props.onHover(this.props.element);
-  };
-
-  render() {
-    const className = classNames('icon-checkbox', {
-      'icon-checkbox-checked': this.props.selected
-    });
-    const activeClass = classNames({ active: this.props.active });
-
-    return (
-      <li>
-        <a
-          href="#"
-          className={activeClass}
-          onClick={this.handleSelect}
-          onMouseOver={this.handleHover}
-          onFocus={this.handleHover}>
-          <i className={className} /> {this.props.custom && '+ '}
-          {this.props.element}
-        </a>
-      </li>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx b/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx
new file mode 100644 (file)
index 0000000..8942942
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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';
+
+interface Props {
+  element: string;
+  selected?: boolean;
+  custom?: boolean;
+  active?: boolean;
+  onSelectChange: (elem: string, selected: boolean) => void;
+  onHover: (elem: string) => void;
+}
+
+export default class MultiSelectOption extends React.PureComponent<Props> {
+  handleSelect = (evt: React.SyntheticEvent<HTMLAnchorElement>) => {
+    evt.stopPropagation();
+    evt.preventDefault();
+    evt.currentTarget.blur();
+    this.props.onSelectChange(this.props.element, !this.props.selected);
+  };
+
+  handleHover = () => this.props.onHover(this.props.element);
+
+  render() {
+    const className = classNames('icon-checkbox', {
+      'icon-checkbox-checked': this.props.selected
+    });
+    const activeClass = classNames({ active: this.props.active });
+
+    return (
+      <li>
+        <a
+          href="#"
+          className={activeClass}
+          onClick={this.handleSelect}
+          onMouseOver={this.handleHover}
+          onFocus={this.handleHover}>
+          <i className={className} /> {this.props.custom && '+ '}
+          {this.props.element}
+        </a>
+      </li>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js
deleted file mode 100644 (file)
index c843a79..0000000
+++ /dev/null
@@ -1,51 +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 { shallow, mount } from 'enzyme';
-import React from 'react';
-import MultiSelect from '../MultiSelect';
-
-const props = {
-  selectedElements: ['bar'],
-  elements: [],
-  onSearch: () => {},
-  onSelect: () => {},
-  onUnselect: () => {},
-  placeholder: ''
-};
-
-const elements = ['foo', 'bar', 'baz'];
-
-it('should render multiselect with selected elements', () => {
-  const multiselect = shallow(<MultiSelect {...props} />);
-  // Will not only the selected element
-  expect(multiselect).toMatchSnapshot();
-
-  multiselect.setProps({ elements });
-  expect(multiselect).toMatchSnapshot();
-  multiselect.setState({ activeIdx: 2 });
-  expect(multiselect).toMatchSnapshot();
-  multiselect.setState({ query: 'test' });
-  expect(multiselect).toMatchSnapshot();
-});
-
-it('should render with the focus inside the search input', () => {
-  const multiselect = mount(<MultiSelect {...props} />);
-  expect(multiselect.find('input').getDOMNode()).toBe(document.activeElement);
-});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx
new file mode 100644 (file)
index 0000000..13bb165
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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, mount } from 'enzyme';
+import MultiSelect from '../MultiSelect';
+
+const props = {
+  selectedElements: ['bar'],
+  elements: [],
+  onSearch: () => {},
+  onSelect: () => {},
+  onUnselect: () => {},
+  placeholder: ''
+};
+
+const elements = ['foo', 'bar', 'baz'];
+
+it('should render multiselect with selected elements', () => {
+  const multiselect = shallow(<MultiSelect {...props} />);
+  // Will not only the selected element
+  expect(multiselect).toMatchSnapshot();
+
+  multiselect.setProps({ elements });
+  expect(multiselect).toMatchSnapshot();
+  multiselect.setState({ activeIdx: 2 });
+  expect(multiselect).toMatchSnapshot();
+  multiselect.setState({ query: 'test' });
+  expect(multiselect).toMatchSnapshot();
+});
+
+it('should render with the focus inside the search input', () => {
+  const multiselect = mount(<MultiSelect {...props} />);
+  expect(multiselect.find('input').getDOMNode()).toBe(document.activeElement);
+});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.js b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.js
deleted file mode 100644 (file)
index a15475b..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 { shallow } from 'enzyme';
-import React from 'react';
-import MultiSelectOption from '../MultiSelectOption';
-
-const props = {
-  element: 'mytag',
-  selected: false,
-  custom: false,
-  active: false,
-  onSelectChange: () => {},
-  onHover: () => {}
-};
-
-it('should render standard tag', () => {
-  expect(shallow(<MultiSelectOption {...props} />)).toMatchSnapshot();
-});
-
-it('should render selected tag', () => {
-  expect(shallow(<MultiSelectOption {...props} selected={true} />)).toMatchSnapshot();
-});
-
-it('should render custom tag', () => {
-  expect(shallow(<MultiSelectOption {...props} custom={true} />)).toMatchSnapshot();
-});
-
-it('should render active tag', () => {
-  expect(shallow(<MultiSelectOption {...props} selected={true} active={true} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx
new file mode 100644 (file)
index 0000000..5b72d18
--- /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 { shallow } from 'enzyme';
+import MultiSelectOption from '../MultiSelectOption';
+
+const props = {
+  element: 'mytag',
+  onSelectChange: () => {},
+  onHover: () => {}
+};
+
+it('should render standard tag', () => {
+  expect(shallow(<MultiSelectOption {...props} />)).toMatchSnapshot();
+});
+
+it('should render selected tag', () => {
+  expect(shallow(<MultiSelectOption {...props} selected={true} />)).toMatchSnapshot();
+});
+
+it('should render custom tag', () => {
+  expect(shallow(<MultiSelectOption {...props} custom={true} />)).toMatchSnapshot();
+});
+
+it('should render active tag', () => {
+  expect(shallow(<MultiSelectOption {...props} selected={true} active={true} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap
deleted file mode 100644 (file)
index 5af710f..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render multiselect with selected elements 1`] = `
-<div
-  className="multi-select"
->
-  <div
-    className="menu-search"
-  >
-    <SearchBox
-      autoFocus={true}
-      onChange={[Function]}
-      placeholder=""
-      value=""
-    />
-  </div>
-  <ul
-    className="menu"
-  >
-    <MultiSelectOption
-      active={true}
-      custom={false}
-      element="bar"
-      key="bar"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={true}
-    />
-  </ul>
-</div>
-`;
-
-exports[`should render multiselect with selected elements 2`] = `
-<div
-  className="multi-select"
->
-  <div
-    className="menu-search"
-  >
-    <SearchBox
-      autoFocus={true}
-      onChange={[Function]}
-      placeholder=""
-      value=""
-    />
-  </div>
-  <ul
-    className="menu"
-  >
-    <MultiSelectOption
-      active={true}
-      custom={false}
-      element="bar"
-      key="bar"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={true}
-    />
-    <MultiSelectOption
-      active={false}
-      custom={false}
-      element="foo"
-      key="foo"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={false}
-    />
-    <MultiSelectOption
-      active={false}
-      custom={false}
-      element="baz"
-      key="baz"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={false}
-    />
-  </ul>
-</div>
-`;
-
-exports[`should render multiselect with selected elements 3`] = `
-<div
-  className="multi-select"
->
-  <div
-    className="menu-search"
-  >
-    <SearchBox
-      autoFocus={true}
-      onChange={[Function]}
-      placeholder=""
-      value=""
-    />
-  </div>
-  <ul
-    className="menu"
-  >
-    <MultiSelectOption
-      active={false}
-      custom={false}
-      element="bar"
-      key="bar"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={true}
-    />
-    <MultiSelectOption
-      active={false}
-      custom={false}
-      element="foo"
-      key="foo"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={false}
-    />
-    <MultiSelectOption
-      active={true}
-      custom={false}
-      element="baz"
-      key="baz"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={false}
-    />
-  </ul>
-</div>
-`;
-
-exports[`should render multiselect with selected elements 4`] = `
-<div
-  className="multi-select"
->
-  <div
-    className="menu-search"
-  >
-    <SearchBox
-      autoFocus={true}
-      onChange={[Function]}
-      placeholder=""
-      value="test"
-    />
-  </div>
-  <ul
-    className="menu"
-  >
-    <MultiSelectOption
-      active={false}
-      custom={false}
-      element="bar"
-      key="bar"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={true}
-    />
-    <MultiSelectOption
-      active={false}
-      custom={false}
-      element="foo"
-      key="foo"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={false}
-    />
-    <MultiSelectOption
-      active={true}
-      custom={false}
-      element="baz"
-      key="baz"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={false}
-    />
-    <MultiSelectOption
-      active={false}
-      custom={true}
-      element="test"
-      key="test"
-      onHover={[Function]}
-      onSelectChange={[Function]}
-      selected={false}
-    />
-  </ul>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap
new file mode 100644 (file)
index 0000000..32dac98
--- /dev/null
@@ -0,0 +1,167 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render multiselect with selected elements 1`] = `
+<div
+  className="multi-select"
+>
+  <div
+    className="menu-search"
+  >
+    <SearchBox
+      autoFocus={true}
+      onChange={[Function]}
+      placeholder=""
+      value=""
+    />
+  </div>
+  <ul
+    className="menu"
+  >
+    <MultiSelectOption
+      active={true}
+      element="bar"
+      key="bar"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+      selected={true}
+    />
+  </ul>
+</div>
+`;
+
+exports[`should render multiselect with selected elements 2`] = `
+<div
+  className="multi-select"
+>
+  <div
+    className="menu-search"
+  >
+    <SearchBox
+      autoFocus={true}
+      onChange={[Function]}
+      placeholder=""
+      value=""
+    />
+  </div>
+  <ul
+    className="menu"
+  >
+    <MultiSelectOption
+      active={true}
+      element="bar"
+      key="bar"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+      selected={true}
+    />
+    <MultiSelectOption
+      active={false}
+      element="foo"
+      key="foo"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+    />
+    <MultiSelectOption
+      active={false}
+      element="baz"
+      key="baz"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+    />
+  </ul>
+</div>
+`;
+
+exports[`should render multiselect with selected elements 3`] = `
+<div
+  className="multi-select"
+>
+  <div
+    className="menu-search"
+  >
+    <SearchBox
+      autoFocus={true}
+      onChange={[Function]}
+      placeholder=""
+      value=""
+    />
+  </div>
+  <ul
+    className="menu"
+  >
+    <MultiSelectOption
+      active={false}
+      element="bar"
+      key="bar"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+      selected={true}
+    />
+    <MultiSelectOption
+      active={false}
+      element="foo"
+      key="foo"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+    />
+    <MultiSelectOption
+      active={true}
+      element="baz"
+      key="baz"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+    />
+  </ul>
+</div>
+`;
+
+exports[`should render multiselect with selected elements 4`] = `
+<div
+  className="multi-select"
+>
+  <div
+    className="menu-search"
+  >
+    <SearchBox
+      autoFocus={true}
+      onChange={[Function]}
+      placeholder=""
+      value="test"
+    />
+  </div>
+  <ul
+    className="menu"
+  >
+    <MultiSelectOption
+      active={false}
+      element="bar"
+      key="bar"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+      selected={true}
+    />
+    <MultiSelectOption
+      active={false}
+      element="foo"
+      key="foo"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+    />
+    <MultiSelectOption
+      active={true}
+      element="baz"
+      key="baz"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+    />
+    <MultiSelectOption
+      active={false}
+      custom={true}
+      element="test"
+      key="test"
+      onHover={[Function]}
+      onSelectChange={[Function]}
+    />
+  </ul>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.js.snap
deleted file mode 100644 (file)
index b7b9f2a..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render active tag 1`] = `
-<li>
-  <a
-    className="active"
-    href="#"
-    onClick={[Function]}
-    onFocus={[Function]}
-    onMouseOver={[Function]}
-  >
-    <i
-      className="icon-checkbox icon-checkbox-checked"
-    />
-     
-    mytag
-  </a>
-</li>
-`;
-
-exports[`should render custom tag 1`] = `
-<li>
-  <a
-    className=""
-    href="#"
-    onClick={[Function]}
-    onFocus={[Function]}
-    onMouseOver={[Function]}
-  >
-    <i
-      className="icon-checkbox"
-    />
-     
-    + 
-    mytag
-  </a>
-</li>
-`;
-
-exports[`should render selected tag 1`] = `
-<li>
-  <a
-    className=""
-    href="#"
-    onClick={[Function]}
-    onFocus={[Function]}
-    onMouseOver={[Function]}
-  >
-    <i
-      className="icon-checkbox icon-checkbox-checked"
-    />
-     
-    mytag
-  </a>
-</li>
-`;
-
-exports[`should render standard tag 1`] = `
-<li>
-  <a
-    className=""
-    href="#"
-    onClick={[Function]}
-    onFocus={[Function]}
-    onMouseOver={[Function]}
-  >
-    <i
-      className="icon-checkbox"
-    />
-     
-    mytag
-  </a>
-</li>
-`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap
new file mode 100644 (file)
index 0000000..b7b9f2a
--- /dev/null
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render active tag 1`] = `
+<li>
+  <a
+    className="active"
+    href="#"
+    onClick={[Function]}
+    onFocus={[Function]}
+    onMouseOver={[Function]}
+  >
+    <i
+      className="icon-checkbox icon-checkbox-checked"
+    />
+     
+    mytag
+  </a>
+</li>
+`;
+
+exports[`should render custom tag 1`] = `
+<li>
+  <a
+    className=""
+    href="#"
+    onClick={[Function]}
+    onFocus={[Function]}
+    onMouseOver={[Function]}
+  >
+    <i
+      className="icon-checkbox"
+    />
+     
+    + 
+    mytag
+  </a>
+</li>
+`;
+
+exports[`should render selected tag 1`] = `
+<li>
+  <a
+    className=""
+    href="#"
+    onClick={[Function]}
+    onFocus={[Function]}
+    onMouseOver={[Function]}
+  >
+    <i
+      className="icon-checkbox icon-checkbox-checked"
+    />
+     
+    mytag
+  </a>
+</li>
+`;
+
+exports[`should render standard tag 1`] = `
+<li>
+  <a
+    className=""
+    href="#"
+    onClick={[Function]}
+    onFocus={[Function]}
+    onMouseOver={[Function]}
+  >
+    <i
+      className="icon-checkbox"
+    />
+     
+    mytag
+  </a>
+</li>
+`;
index 30c93818fffff153149181f259bb1fc2885349df..c334412d5c0a68a46a408ad0d393d592fa96e982 100644 (file)
@@ -77,7 +77,6 @@ export default class SetIssueTagsPopup extends React.PureComponent {
 
   render() {
     return (
-      // $FlowFixMe `this.props.popupPosition` is passed from `BabelPopupHelper`
       <TagsSelector
         position={this.props.popupPosition}
         tags={this.state.searchResult}
index 200035f80169e9adebf419e32bab44eb1b7896c8..ae12c4c480f6a701e95aeee3f46628fa029eaa3b 100644 (file)
@@ -21,39 +21,32 @@ import * as React from 'react';
 import Rating from '../ui/Rating';
 import Level from '../ui/Level';
 import Tooltips from '../controls/Tooltip';
-import { formatMeasure, isDiffMetric, MeasureEnhanced } from '../../helpers/measures';
-import { formatLeak, getRatingTooltip } from './utils';
+import { formatMeasure } from '../../helpers/measures';
+import { getRatingTooltip } from './utils';
 
 interface Props {
   className?: string;
   decimals?: number | null;
-  measure?: MeasureEnhanced;
+  value?: string;
+  metricKey: string;
+  metricType: string;
 }
 
-export default function Measure({ className, decimals, measure }: Props) {
-  if (measure === undefined) {
-    return <span>{'–'}</span>;
-  }
-
-  const { metric } = measure;
-  const value = isDiffMetric(metric.key) ? measure.leak : measure.value;
-
+export default function Measure({ className, decimals, metricKey, metricType, value }: Props) {
   if (value === undefined) {
     return <span>{'–'}</span>;
   }
 
-  if (metric.type === 'LEVEL') {
+  if (metricType === 'LEVEL') {
     return <Level className={className} level={value} />;
   }
 
-  if (metric.type !== 'RATING') {
-    const formattedValue = isDiffMetric(metric.key)
-      ? formatLeak(measure.leak, metric.key, metric.type, { decimals })
-      : formatMeasure(measure.value, metric.type, { decimals });
+  if (metricType !== 'RATING') {
+    const formattedValue = formatMeasure(value, metricType, { decimals });
     return <span className={className}>{formattedValue != null ? formattedValue : '–'}</span>;
   }
 
-  const tooltip = getRatingTooltip(metric.key, Number(value));
+  const tooltip = getRatingTooltip(metricKey, Number(value));
   const rating = <Rating value={value} />;
   if (tooltip) {
     return (
index a62f16c513db5d4daec6175e703000205c1968d6..84aaf15fdb3d835d44a7b8afe6f600c8269f620c 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.
  */
-/* eslint-disable import/first */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Measure from '../Measure';
+
 jest.mock('../../../helpers/measures', () => {
   const measures = require.requireActual('../../../helpers/measures');
   measures.getRatingTooltip = jest.fn(() => 'tooltip');
   return measures;
 });
 
-import * as React from 'react';
-import { shallow } from 'enzyme';
-import Measure from '../Measure';
-
 it('renders trivial measure', () => {
-  const measure = { metric: { key: 'coverage', name: 'Coverage', type: 'PERCENT' }, value: '73.0' };
-  expect(shallow(<Measure measure={measure} />)).toMatchSnapshot();
+  expect(
+    shallow(<Measure metricKey="coverage" metricType="PERCENT" value="73.0" />)
+  ).toMatchSnapshot();
 });
 
 it('renders leak measure', () => {
-  const measure = {
-    metric: { key: 'new_coverage', name: 'Coverage on New Code', type: 'PERCENT' },
-    leak: '36.0'
-  };
-  expect(shallow(<Measure measure={measure} />)).toMatchSnapshot();
+  expect(
+    shallow(<Measure metricKey="new_coverage" metricType="PERCENT" value="36.0" />)
+  ).toMatchSnapshot();
 });
 
 it('renders LEVEL', () => {
-  const measure = {
-    metric: { key: 'quality_gate_status', name: 'Quality Gate', type: 'LEVEL' },
-    value: 'ERROR'
-  };
-  expect(shallow(<Measure measure={measure} />)).toMatchSnapshot();
+  expect(
+    shallow(<Measure metricKey="quality_gate_status" metricType="LEVEL" value="ERROR" />)
+  ).toMatchSnapshot();
 });
 
 it('renders known RATING', () => {
-  const measure = {
-    metric: { key: 'sqale_rating', name: 'Maintainability Rating', type: 'RATING' },
-    value: '3'
-  };
-  expect(shallow(<Measure measure={measure} />)).toMatchSnapshot();
+  expect(
+    shallow(<Measure metricKey="sqale_rating" metricType="RATING" value="3" />)
+  ).toMatchSnapshot();
 });
 
 it('renders unknown RATING', () => {
-  const measure = {
-    metric: { key: 'foo_rating', name: 'Foo Rating', type: 'RATING' },
-    value: '4'
-  };
-  expect(shallow(<Measure measure={measure} />)).toMatchSnapshot();
+  expect(
+    shallow(<Measure metricKey="foo_rating" metricType="RATING" value="4" />)
+  ).toMatchSnapshot();
 });
 
 it('renders undefined measure', () => {
-  const measure = { metric: { key: 'foo', name: 'Foo', type: 'PERCENT' } };
-  expect(shallow(<Measure measure={measure} />)).toMatchSnapshot();
+  expect(shallow(<Measure metricKey="foo" metricType="PERCENT" />)).toMatchSnapshot();
 });
index 8e932892152f538d3ce60989bf60ecc92d6420d3..626c65f552f3c2e120eed33bd0d688b00b97dd02 100644 (file)
@@ -18,8 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import {
-  formatMeasure,
-  formatMeasureVariation,
   getRatingTooltip as nextGetRatingTooltip,
   isDiffMetric,
   Measure,
@@ -40,19 +38,6 @@ export function enhanceMeasure(
   };
 }
 
-export function formatLeak(
-  value: string | undefined,
-  metricKey: string,
-  metricType: string,
-  options: any
-): string {
-  if (isDiffMetric(metricKey)) {
-    return formatMeasure(value, metricType, options);
-  } else {
-    return formatMeasureVariation(value, metricType, options);
-  }
-}
-
 export function getLeakValue(measure: Measure | undefined): string | undefined {
   if (!measure || !measure.periods) {
     return undefined;
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts
new file mode 100644 (file)
index 0000000..744eed0
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 { History } from '../../api/time-machine';
+import { Metric } from '../../app/types';
+
+interface Props {
+  branch?: string;
+  history?: History;
+  metrics: Metric[];
+  project: string;
+  renderWhenEmpty?: () => void;
+}
+
+export default class PreviewGraph extends React.Component<Props> {}
diff --git a/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx b/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx
new file mode 100644 (file)
index 0000000..1065e91
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * 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 { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls';
+
+const ISSUE_MEASURES = [
+  'violations',
+  'new_violations',
+  'blocker_violations',
+  'critical_violations',
+  'major_violations',
+  'minor_violations',
+  'info_violations',
+  'new_blocker_violations',
+  'new_critical_violations',
+  'new_major_violations',
+  'new_minor_violations',
+  'new_info_violations',
+  'open_issues',
+  'reopened_issues',
+  'confirmed_issues',
+  'false_positive_issues',
+  'code_smells',
+  'new_code_smells',
+  'bugs',
+  'new_bugs',
+  'vulnerabilities',
+  'new_vulnerabilities'
+];
+
+interface Props {
+  branch?: string;
+  children?: React.ReactNode;
+  className?: string;
+  component: string;
+  metric: string;
+  sinceLeakPeriod?: boolean;
+}
+
+export default class DrilldownLink extends React.PureComponent<Props> {
+  isIssueMeasure = () => {
+    return ISSUE_MEASURES.indexOf(this.props.metric) !== -1;
+  };
+
+  propsToIssueParams = () => {
+    const params: { [key: string]: string | boolean } = {};
+
+    if (this.props.sinceLeakPeriod) {
+      params.sinceLeakPeriod = true;
+    }
+
+    switch (this.props.metric) {
+      case 'blocker_violations':
+      case 'new_blocker_violations':
+        Object.assign(params, { resolved: 'false', severities: 'BLOCKER' });
+        break;
+      case 'critical_violations':
+      case 'new_critical_violations':
+        Object.assign(params, { resolved: 'false', severities: 'CRITICAL' });
+        break;
+      case 'major_violations':
+      case 'new_major_violations':
+        Object.assign(params, { resolved: 'false', severities: 'MAJOR' });
+        break;
+      case 'minor_violations':
+      case 'new_minor_violations':
+        Object.assign(params, { resolved: 'false', severities: 'MINOR' });
+        break;
+      case 'info_violations':
+      case 'new_info_violations':
+        Object.assign(params, { resolved: 'false', severities: 'INFO' });
+        break;
+      case 'open_issues':
+        Object.assign(params, { resolved: 'false', statuses: 'OPEN' });
+        break;
+      case 'reopened_issues':
+        Object.assign(params, { resolved: 'false', statuses: 'REOPENED' });
+        break;
+      case 'confirmed_issues':
+        Object.assign(params, { resolved: 'false', statuses: 'CONFIRMED' });
+        break;
+      case 'false_positive_issues':
+        Object.assign(params, { resolutions: 'FALSE-POSITIVE' });
+        break;
+      case 'code_smells':
+      case 'new_code_smells':
+        Object.assign(params, { resolved: 'false', types: 'CODE_SMELL' });
+        break;
+      case 'bugs':
+      case 'new_bugs':
+        Object.assign(params, { resolved: 'false', types: 'BUG' });
+        break;
+      case 'vulnerabilities':
+      case 'new_vulnerabilities':
+        Object.assign(params, { resolved: 'false', types: 'VULNERABILITY' });
+        break;
+      default:
+        Object.assign(params, { resolved: 'false' });
+    }
+    return params;
+  };
+
+  renderIssuesLink = () => {
+    const url = getComponentIssuesUrl(this.props.component, {
+      ...this.propsToIssueParams(),
+      branch: this.props.branch
+    });
+
+    return (
+      <Link to={url} className={this.props.className}>
+        {this.props.children}
+      </Link>
+    );
+  };
+
+  render() {
+    if (this.isIssueMeasure()) {
+      return this.renderIssuesLink();
+    }
+
+    const url = getComponentDrilldownUrl(
+      this.props.component,
+      this.props.metric,
+      this.props.branch
+    );
+    return (
+      <Link to={url} className={this.props.className}>
+        {this.props.children}
+      </Link>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/shared/drilldown-link.js b/server/sonar-web/src/main/js/components/shared/drilldown-link.js
deleted file mode 100644 (file)
index cdff767..0000000
+++ /dev/null
@@ -1,150 +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 PropTypes from 'prop-types';
-import { Link } from 'react-router';
-import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls';
-
-const ISSUE_MEASURES = [
-  'violations',
-  'new_violations',
-  'blocker_violations',
-  'critical_violations',
-  'major_violations',
-  'minor_violations',
-  'info_violations',
-  'new_blocker_violations',
-  'new_critical_violations',
-  'new_major_violations',
-  'new_minor_violations',
-  'new_info_violations',
-  'open_issues',
-  'reopened_issues',
-  'confirmed_issues',
-  'false_positive_issues',
-  'code_smells',
-  'new_code_smells',
-  'bugs',
-  'new_bugs',
-  'vulnerabilities',
-  'new_vulnerabilities'
-];
-
-export class DrilldownLink extends React.PureComponent {
-  static propTypes = {
-    branch: PropTypes.string,
-    children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
-    className: PropTypes.string,
-    component: PropTypes.string.isRequired,
-    metric: PropTypes.string.isRequired,
-    sinceLeakPeriod: PropTypes.bool
-  };
-  isIssueMeasure = () => {
-    return ISSUE_MEASURES.indexOf(this.props.metric) !== -1;
-  };
-
-  propsToIssueParams = () => {
-    const params = {};
-
-    if (this.props.sinceLeakPeriod) {
-      params.sinceLeakPeriod = true;
-    }
-
-    switch (this.props.metric) {
-      case 'blocker_violations':
-      case 'new_blocker_violations':
-        Object.assign(params, { resolved: 'false', severities: 'BLOCKER' });
-        break;
-      case 'critical_violations':
-      case 'new_critical_violations':
-        Object.assign(params, { resolved: 'false', severities: 'CRITICAL' });
-        break;
-      case 'major_violations':
-      case 'new_major_violations':
-        Object.assign(params, { resolved: 'false', severities: 'MAJOR' });
-        break;
-      case 'minor_violations':
-      case 'new_minor_violations':
-        Object.assign(params, { resolved: 'false', severities: 'MINOR' });
-        break;
-      case 'info_violations':
-      case 'new_info_violations':
-        Object.assign(params, { resolved: 'false', severities: 'INFO' });
-        break;
-      case 'open_issues':
-        Object.assign(params, { resolved: 'false', statuses: 'OPEN' });
-        break;
-      case 'reopened_issues':
-        Object.assign(params, { resolved: 'false', statuses: 'REOPENED' });
-        break;
-      case 'confirmed_issues':
-        Object.assign(params, { resolved: 'false', statuses: 'CONFIRMED' });
-        break;
-      case 'false_positive_issues':
-        Object.assign(params, { resolutions: 'FALSE-POSITIVE' });
-        break;
-      case 'code_smells':
-      case 'new_code_smells':
-        Object.assign(params, { resolved: 'false', types: 'CODE_SMELL' });
-        break;
-      case 'bugs':
-      case 'new_bugs':
-        Object.assign(params, { resolved: 'false', types: 'BUG' });
-        break;
-      case 'vulnerabilities':
-      case 'new_vulnerabilities':
-        Object.assign(params, { resolved: 'false', types: 'VULNERABILITY' });
-        break;
-      default:
-        Object.assign(params, { resolved: 'false' });
-    }
-    return params;
-  };
-
-  renderIssuesLink = () => {
-    const url = getComponentIssuesUrl(this.props.component, {
-      ...this.propsToIssueParams(),
-      branch: this.props.branch
-    });
-
-    return (
-      <Link to={url} className={this.props.className}>
-        {this.props.children}
-      </Link>
-    );
-  };
-
-  render() {
-    if (this.isIssueMeasure()) {
-      return this.renderIssuesLink();
-    }
-
-    const url = getComponentDrilldownUrl(
-      this.props.component,
-      this.props.metric,
-      this.props.branch
-    );
-    return (
-      <Link to={url} className={this.props.className}>
-        {this.props.children}
-      </Link>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/tags/TagsSelector.js b/server/sonar-web/src/main/js/components/tags/TagsSelector.js
deleted file mode 100644 (file)
index c257c31..0000000
+++ /dev/null
@@ -1,61 +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 BubblePopup from '../common/BubblePopup';
-import MultiSelect from '../common/MultiSelect';
-import { translate } from '../../helpers/l10n';
-import './TagsList.css';
-
-/*::
-type Props = {
-  position: {},
-  tags: Array<string>,
-  selectedTags: Array<string>,
-  listSize: number,
-  onSearch: string => void,
-  onSelect: string => void,
-  onUnselect: string => void
-};
-*/
-
-export default function TagsSelector(props /*: Props */) {
-  return (
-    <BubblePopup
-      position={props.position}
-      customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300">
-      <MultiSelect
-        elements={props.tags}
-        selectedElements={props.selectedTags}
-        listSize={props.listSize}
-        onSearch={props.onSearch}
-        onSelect={props.onSelect}
-        onUnselect={props.onUnselect}
-        validateSearchInput={validateTag}
-        placeholder={translate('search.search_for_tags')}
-      />
-    </BubblePopup>
-  );
-}
-
-export function validateTag(value /*: string */) {
-  // Allow only a-z, 0-9, '+', '-', '#', '.'
-  return value.toLowerCase().replace(/[^a-z0-9\+\-#.]/gi, '');
-}
diff --git a/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx b/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx
new file mode 100644 (file)
index 0000000..f9ed202
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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 BubblePopup, { BubblePopupPosition } from '../common/BubblePopup';
+import MultiSelect from '../common/MultiSelect';
+import { translate } from '../../helpers/l10n';
+import './TagsList.css';
+
+interface Props {
+  position: BubblePopupPosition;
+  tags: string[];
+  selectedTags: string[];
+  listSize: number;
+  onSearch: (query: string) => void;
+  onSelect: (item: string) => void;
+  onUnselect: (item: string) => void;
+}
+
+export default function TagsSelector(props: Props) {
+  return (
+    <BubblePopup
+      position={props.position}
+      customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300">
+      <MultiSelect
+        elements={props.tags}
+        selectedElements={props.selectedTags}
+        listSize={props.listSize}
+        onSearch={props.onSearch}
+        onSelect={props.onSelect}
+        onUnselect={props.onUnselect}
+        validateSearchInput={validateTag}
+        placeholder={translate('search.search_for_tags')}
+      />
+    </BubblePopup>
+  );
+}
+
+export function validateTag(value: string) {
+  // Allow only a-z, 0-9, '+', '-', '#', '.'
+  return value.toLowerCase().replace(/[^a-z0-9\+\-#.]/gi, '');
+}
diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js b/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js
deleted file mode 100644 (file)
index 98186da..0000000
+++ /dev/null
@@ -1,49 +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 { shallow } from 'enzyme';
-import React from 'react';
-import TagsSelector, { validateTag } from '../TagsSelector';
-
-const props = {
-  position: { left: 0, top: 0 },
-  tags: ['foo', 'bar', 'baz'],
-  selectedTags: ['bar'],
-  onSearch: () => {},
-  onSelect: () => {},
-  onUnselect: () => {}
-};
-
-it('should render with selected tags', () => {
-  const tagsSelector = shallow(<TagsSelector {...props} />);
-  expect(tagsSelector).toMatchSnapshot();
-});
-
-it('should render without tags at all', () => {
-  expect(shallow(<TagsSelector {...props} tags={[]} selectedTags={[]} />)).toMatchSnapshot();
-});
-
-it('should validate tags correctly', () => {
-  const validChars = 'abcdefghijklmnopqrstuvwxyz0123456789+-#.';
-  expect(validateTag('test')).toBe('test');
-  expect(validateTag(validChars)).toBe(validChars);
-  expect(validateTag(validChars.toUpperCase())).toBe(validChars);
-  expect(validateTag('T E$ST')).toBe('test');
-  expect(validateTag('T E$st!^àéèing1')).toBe('testing1');
-});
diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx b/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx
new file mode 100644 (file)
index 0000000..1b5169a
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 TagsSelector, { validateTag } from '../TagsSelector';
+
+const props = {
+  position: { right: 0, top: 0 },
+  listSize: 10,
+  tags: ['foo', 'bar', 'baz'],
+  selectedTags: ['bar'],
+  onSearch: () => {},
+  onSelect: () => {},
+  onUnselect: () => {}
+};
+
+it('should render with selected tags', () => {
+  const tagsSelector = shallow(<TagsSelector {...props} />);
+  expect(tagsSelector).toMatchSnapshot();
+});
+
+it('should render without tags at all', () => {
+  expect(shallow(<TagsSelector {...props} tags={[]} selectedTags={[]} />)).toMatchSnapshot();
+});
+
+it('should validate tags correctly', () => {
+  const validChars = 'abcdefghijklmnopqrstuvwxyz0123456789+-#.';
+  expect(validateTag('test')).toBe('test');
+  expect(validateTag(validChars)).toBe(validChars);
+  expect(validateTag(validChars.toUpperCase())).toBe(validChars);
+  expect(validateTag('T E$ST')).toBe('test');
+  expect(validateTag('T E$st!^àéèing1')).toBe('testing1');
+});
diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap
deleted file mode 100644 (file)
index e2d5756..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render with selected tags 1`] = `
-<BubblePopup
-  customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
-  position={
-    Object {
-      "left": 0,
-      "top": 0,
-    }
-  }
->
-  <MultiSelect
-    elements={
-      Array [
-        "foo",
-        "bar",
-        "baz",
-      ]
-    }
-    listSize={10}
-    onSearch={[Function]}
-    onSelect={[Function]}
-    onUnselect={[Function]}
-    placeholder="search.search_for_tags"
-    selectedElements={
-      Array [
-        "bar",
-      ]
-    }
-    validateSearchInput={[Function]}
-  />
-</BubblePopup>
-`;
-
-exports[`should render without tags at all 1`] = `
-<BubblePopup
-  customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
-  position={
-    Object {
-      "left": 0,
-      "top": 0,
-    }
-  }
->
-  <MultiSelect
-    elements={Array []}
-    listSize={10}
-    onSearch={[Function]}
-    onSelect={[Function]}
-    onUnselect={[Function]}
-    placeholder="search.search_for_tags"
-    selectedElements={Array []}
-    validateSearchInput={[Function]}
-  />
-</BubblePopup>
-`;
diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap
new file mode 100644 (file)
index 0000000..c2d4623
--- /dev/null
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render with selected tags 1`] = `
+<BubblePopup
+  customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
+  position={
+    Object {
+      "right": 0,
+      "top": 0,
+    }
+  }
+>
+  <MultiSelect
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    listSize={10}
+    onSearch={[Function]}
+    onSelect={[Function]}
+    onUnselect={[Function]}
+    placeholder="search.search_for_tags"
+    selectedElements={
+      Array [
+        "bar",
+      ]
+    }
+    validateSearchInput={[Function]}
+  />
+</BubblePopup>
+`;
+
+exports[`should render without tags at all 1`] = `
+<BubblePopup
+  customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
+  position={
+    Object {
+      "right": 0,
+      "top": 0,
+    }
+  }
+>
+  <MultiSelect
+    elements={Array []}
+    listSize={10}
+    onSearch={[Function]}
+    onSelect={[Function]}
+    onUnselect={[Function]}
+    placeholder="search.search_for_tags"
+    selectedElements={Array []}
+    validateSearchInput={[Function]}
+  />
+</BubblePopup>
+`;
index 68ae35138a75857a10c4a52c9f4dde533af8918b..6cf02992461e04f68baebedee6e5c89369c0bcb2 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { resetBundle } from '../l10n';
-import { formatMeasure, formatMeasureVariation } from '../measures';
+import { formatMeasure } from '../measures';
 
 const HOURS_IN_DAY = 8;
 const ONE_MINUTE = 1;
@@ -170,92 +170,3 @@ describe('#formatMeasure()', () => {
     expect(formatMeasure(undefined, 'INT')).toBe('');
   });
 });
-
-describe('#formatMeasureVariation()', () => {
-  it('should format INT', () => {
-    expect(formatMeasureVariation(0, 'INT')).toBe('+0');
-    expect(formatMeasureVariation(1, 'INT')).toBe('+1');
-    expect(formatMeasureVariation(-1, 'INT')).toBe('-1');
-    expect(formatMeasureVariation(1529, 'INT')).toBe('+1,529');
-    expect(formatMeasureVariation(-1529, 'INT')).toBe('-1,529');
-  });
-
-  it('should format SHORT_INT', () => {
-    expect(formatMeasureVariation(0, 'SHORT_INT')).toBe('+0');
-    expect(formatMeasureVariation(1, 'SHORT_INT')).toBe('+1');
-    expect(formatMeasureVariation(-1, 'SHORT_INT')).toBe('-1');
-    expect(formatMeasureVariation(1529, 'SHORT_INT')).toBe('+1.5k');
-    expect(formatMeasureVariation(-1529, 'SHORT_INT')).toBe('-1.5k');
-    expect(formatMeasureVariation(10678, 'SHORT_INT')).toBe('+11k');
-    expect(formatMeasureVariation(-10678, 'SHORT_INT')).toBe('-11k');
-  });
-
-  it('should format FLOAT', () => {
-    expect(formatMeasureVariation(0.0, 'FLOAT')).toBe('+0.0');
-    expect(formatMeasureVariation(1.0, 'FLOAT')).toBe('+1.0');
-    expect(formatMeasureVariation(-1.0, 'FLOAT')).toBe('-1.0');
-    expect(formatMeasureVariation(50.89, 'FLOAT')).toBe('+50.89');
-    expect(formatMeasureVariation(-50.89, 'FLOAT')).toBe('-50.89');
-  });
-
-  it('should respect FLOAT precision', () => {
-    expect(formatMeasureVariation(0.1, 'FLOAT')).toBe('+0.1');
-    expect(formatMeasureVariation(0.12, 'FLOAT')).toBe('+0.12');
-    expect(formatMeasureVariation(0.12345, 'FLOAT')).toBe('+0.12345');
-    expect(formatMeasureVariation(0.123456, 'FLOAT')).toBe('+0.12346');
-  });
-
-  it('should format PERCENT', () => {
-    expect(formatMeasureVariation(0.0, 'PERCENT')).toBe('+0.0%');
-    expect(formatMeasureVariation(1.0, 'PERCENT')).toBe('+1.0%');
-    expect(formatMeasureVariation(-1.0, 'PERCENT')).toBe('-1.0%');
-    expect(formatMeasureVariation(50.89, 'PERCENT')).toBe('+50.9%');
-    expect(formatMeasureVariation(-50.89, 'PERCENT')).toBe('-50.9%');
-  });
-
-  it('should format WORK_DUR', () => {
-    expect(formatMeasureVariation(0, 'WORK_DUR')).toBe('+0');
-    expect(formatMeasureVariation(5 * ONE_DAY, 'WORK_DUR')).toBe('+5d');
-    expect(formatMeasureVariation(2 * ONE_HOUR, 'WORK_DUR')).toBe('+2h');
-    expect(formatMeasureVariation(ONE_MINUTE, 'WORK_DUR')).toBe('+1min');
-    expect(formatMeasureVariation(-5 * ONE_DAY, 'WORK_DUR')).toBe('-5d');
-    expect(formatMeasureVariation(-2 * ONE_HOUR, 'WORK_DUR')).toBe('-2h');
-    expect(formatMeasureVariation(-1 * ONE_MINUTE, 'WORK_DUR')).toBe('-1min');
-  });
-
-  it('should format SHORT_WORK_DUR', () => {
-    expect(formatMeasureVariation(0, 'SHORT_WORK_DUR')).toBe('+0');
-    expect(formatMeasureVariation(5 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('+5d');
-    expect(formatMeasureVariation(2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('+2h');
-    expect(formatMeasureVariation(ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+1min');
-    expect(formatMeasureVariation(30 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+30min');
-    expect(formatMeasureVariation(58 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+1h');
-    expect(formatMeasureVariation(5 * ONE_DAY + 2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('+5d');
-    expect(formatMeasureVariation(2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+2h');
-    expect(formatMeasureVariation(ONE_HOUR + 55 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+2h');
-    expect(formatMeasureVariation(3 * ONE_DAY + 6 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('+4d');
-    expect(formatMeasureVariation(7 * ONE_HOUR + 59 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+1d');
-    expect(formatMeasureVariation(5 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe(
-      '+5d'
-    );
-    expect(formatMeasureVariation(15 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe(
-      '+15d'
-    );
-    expect(formatMeasureVariation(7 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+7min');
-    expect(formatMeasureVariation(-5 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('-5d');
-    expect(formatMeasureVariation(-2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('-2h');
-    expect(formatMeasureVariation(-1 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('-1min');
-
-    expect(formatMeasureVariation(1529 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('+1.5kd');
-    expect(formatMeasureVariation(1234567 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('+1Md');
-    expect(formatMeasureVariation(1234567 * ONE_DAY + 2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('+1Md');
-  });
-
-  it('should not format unknown type', () => {
-    expect(formatMeasureVariation('random value', 'RANDOM_TYPE')).toBe('random value');
-  });
-
-  it('should not fail with undefined', () => {
-    expect(formatMeasureVariation(undefined, 'INT')).toBe('');
-  });
-});
index edb05d8ce851f5e8d28a9947aa71cff4bd443dc8..b6354557ad12374b41f93fffc50ebb35dd98abfb 100644 (file)
@@ -55,16 +55,6 @@ export function formatMeasure(
   return useFormatter(value, formatter, options);
 }
 
-/** Format a measure variation for a given type */
-export function formatMeasureVariation(
-  value: string | number | undefined,
-  type: string,
-  options?: any
-): string {
-  const formatter = getVariationFormatter(type);
-  return useFormatter(value, formatter, options);
-}
-
 /** Return a localized metric name */
 export function localizeMetric(metricKey: string): string {
   return translate('metric', metricKey, 'name');
@@ -91,7 +81,10 @@ export function enhanceMeasuresWithMetrics(
 }
 
 /** Get period value of a measure */
-export function getPeriodValue(measure: Measure, periodIndex: number): string | undefined {
+export function getPeriodValue(
+  measure: Measure | MeasureEnhanced,
+  periodIndex: number
+): string | undefined {
   const { periods } = measure;
   const period = periods && periods.find(period => period.index === periodIndex);
   return period ? period.value : undefined;
@@ -125,21 +118,6 @@ function getFormatter(type: string): Formatter {
   return FORMATTERS[type] || noFormatter;
 }
 
-function getVariationFormatter(type: string): Formatter {
-  const FORMATTERS: { [type: string]: Formatter } = {
-    INT: intVariationFormatter,
-    SHORT_INT: shortIntVariationFormatter,
-    FLOAT: floatVariationFormatter,
-    PERCENT: percentVariationFormatter,
-    WORK_DUR: durationVariationFormatter,
-    SHORT_WORK_DUR: shortDurationVariationFormatter,
-    RATING: emptyFormatter,
-    LEVEL: emptyFormatter,
-    MILLISEC: millisecondsVariationFormatter
-  };
-  return FORMATTERS[type] || noFormatter;
-}
-
 function numberFormatter(
   value: number,
   minimumFractionDigits = 0,
@@ -156,19 +134,10 @@ function noFormatter(value: string | number): string | number {
   return value;
 }
 
-function emptyFormatter(): string {
-  return '';
-}
-
 function intFormatter(value: number): string {
   return numberFormatter(value);
 }
 
-function intVariationFormatter(value: number): string {
-  const prefix = value < 0 ? '-' : '+';
-  return prefix + intFormatter(Math.abs(value));
-}
-
 function shortIntFormatter(value: number): string {
   if (value >= 1e9) {
     return numberFormatter(value / 1e9) + translate('short_number_suffix.g');
@@ -183,20 +152,10 @@ function shortIntFormatter(value: number): string {
   }
 }
 
-function shortIntVariationFormatter(value: number): string {
-  const formatted = shortIntFormatter(Math.abs(value));
-  return value < 0 ? `-${formatted}` : `+${formatted}`;
-}
-
 function floatFormatter(value: number): string {
   return numberFormatter(value, 1, 5);
 }
 
-function floatVariationFormatter(value: number): string {
-  const prefix = value < 0 ? '-' : '+';
-  return prefix + floatFormatter(Math.abs(value));
-}
-
 function percentFormatter(value: string | number, options: { decimals?: number } = {}): string {
   if (typeof value === 'string') {
     value = parseFloat(value);
@@ -207,17 +166,6 @@ function percentFormatter(value: string | number, options: { decimals?: number }
   return value === 100 ? '100%' : numberFormatter(value, 1) + '%';
 }
 
-function percentVariationFormatter(
-  value: string | number,
-  options: { decimals?: number } = {}
-): string {
-  if (typeof value === 'string') {
-    value = parseFloat(value);
-  }
-  const prefix = value < 0 ? '-' : '+';
-  return prefix + percentFormatter(Math.abs(value), options);
-}
-
 function ratingFormatter(value: string | number): string {
   if (typeof value === 'string') {
     value = parseInt(value, 10);
@@ -247,12 +195,6 @@ function millisecondsFormatter(value: number): string {
   }
 }
 
-function millisecondsVariationFormatter(value: number): string {
-  const absValue = Math.abs(value);
-  const formattedValue = millisecondsFormatter(absValue);
-  return value < 0 ? `-${formattedValue}` : `+${formattedValue}`;
-}
-
 /*
  * Debt Formatters
  */
@@ -362,22 +304,6 @@ function shortDurationFormatter(value: string | number): string {
   return formatDurationShort(isNegative, days, hours, remainingValue);
 }
 
-function durationVariationFormatter(value: string | number): string {
-  if (value === 0 || value === '0') {
-    return '+0';
-  }
-  const formatted = durationFormatter(value);
-  return formatted[0] !== '-' ? '+' + formatted : formatted;
-}
-
-function shortDurationVariationFormatter(value: string | number): string {
-  if (value === 0 || value === '0') {
-    return '+0';
-  }
-  const formatted = shortDurationFormatter(value);
-  return formatted[0] !== '-' ? '+' + formatted : formatted;
-}
-
 function getRatingGrid(): string {
   // workaround cyclic dependencies
   const getStore = require('../app/utils/getStore').default;
@@ -429,12 +355,12 @@ function getMaintainabilityRatingTooltip(rating: number): string {
   );
 }
 
-export function getRatingTooltip(metricKey: string, value: number): string {
+export function getRatingTooltip(metricKey: string, value: number | string): string {
   const ratingLetter = formatMeasure(value, 'RATING');
 
-  const finalMetricKey = metricKey.startsWith('new_') ? metricKey.substr(4) : metricKey;
+  const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey;
 
   return finalMetricKey === 'sqale_rating' || finalMetricKey === 'maintainability_rating'
-    ? getMaintainabilityRatingTooltip(value)
+    ? getMaintainabilityRatingTooltip(Number(value))
     : translate('metric', finalMetricKey, 'tooltip', ratingLetter);
 }
index 110eb67cf2addf84c6f18497ca3c58f380be9060..afc0edf1c49c12422aacdd42aeeb9df0a543a85d 100644 (file)
@@ -54,8 +54,8 @@ export function getPeriodLabel(period: Period | undefined): string | undefined {
   return translateWithParameters(`overview.period.${period.mode}`, parameter);
 }
 
-export function getPeriodDate(period: Period | undefined): Date | undefined {
-  return period ? parseDate(period.date) : undefined;
+export function getPeriodDate(period?: { date?: string }): Date | undefined {
+  return period && period.date ? parseDate(period.date) : undefined;
 }
 
 export function getLeakPeriodLabel(periods: Period[]): string | undefined {