]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12156 Add security review rating to portfolio overview
authorJeremy Davis <jeremy.davis@sonarsource.com>
Thu, 6 Jun 2019 09:38:41 +0000 (11:38 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 14 Jun 2019 18:21:12 +0000 (20:21 +0200)
42 files changed:
server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx
server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx
server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx
server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx
server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx
server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MetricBox-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap
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__/HistoryButtonLink-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/styles.css
server/sonar-web/src/main/js/apps/portfolio/utils.ts
server/sonar-web/src/main/js/components/icons-components/MeasuresIcon.tsx [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
deleted file mode 100644 (file)
index 4e08fb2..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 {
-  getDisplayedHistoryMetrics,
-  DEFAULT_GRAPH,
-  getProjectActivityGraph
-} from '../../projectActivity/utils';
-import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
-import { getAllTimeMachineData } from '../../../api/time-machine';
-import { parseDate } from '../../../helpers/dates';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  component: string;
-  metrics: T.Dict<T.Metric>;
-}
-
-interface State {
-  history?: {
-    [metric: string]: Array<{ date: Date; value?: string }>;
-  };
-  loading: boolean;
-}
-
-export default class Activity extends React.PureComponent<Props> {
-  mounted = false;
-  state: State = { loading: true };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchHistory();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.component !== this.props.component) {
-      this.fetchHistory();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchHistory = () => {
-    const { component } = this.props;
-
-    const { graph, customGraphs } = getProjectActivityGraph(component);
-    let graphMetrics = getDisplayedHistoryMetrics(graph, customGraphs);
-    if (!graphMetrics || graphMetrics.length <= 0) {
-      graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
-    }
-
-    this.setState({ loading: true });
-    return getAllTimeMachineData({ component, metrics: graphMetrics.join() }).then(
-      timeMachine => {
-        if (this.mounted) {
-          const history: T.Dict<Array<{ date: Date; value?: string }>> = {};
-          timeMachine.measures.forEach(measure => {
-            const measureHistory = measure.history.map(analysis => ({
-              date: parseDate(analysis.date),
-              value: analysis.value
-            }));
-            history[measure.metric] = measureHistory;
-          });
-          this.setState({ history, loading: false });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  renderWhenEmpty = () => <div className="note">{translate('component_measures.no_history')}</div>;
-
-  render() {
-    return (
-      <div className="big-spacer-bottom">
-        <h4>{translate('project_activity.page')}</h4>
-
-        {this.state.loading ? (
-          <i className="spinner" />
-        ) : (
-          this.state.history !== undefined && (
-            <PreviewGraph
-              history={this.state.history}
-              metrics={this.props.metrics}
-              project={this.props.component}
-              renderWhenEmpty={this.renderWhenEmpty}
-            />
-          )
-        )}
-      </div>
-    );
-  }
-}
index eb8b5f89ec1b14098693dc38be769fe73a9add8b..c1b063059e3303bd508d22a531676e0eb4a37d02 100644 (file)
  */
 import * as React from 'react';
 import { connect } from 'react-redux';
-import Summary from './Summary';
+import { Link } from 'react-router';
+import MeasuresButtonLink from './MeasuresButtonLink';
+import MetricBox from './MetricBox';
 import Report from './Report';
 import WorstProjects from './WorstProjects';
-import ReleasabilityBox from './ReleasabilityBox';
-import ReliabilityBox from './ReliabilityBox';
-import SecurityBox from './SecurityBox';
-import MaintainabilityBox from './MaintainabilityBox';
-import Activity from './Activity';
 import { SubComponent } from '../types';
 import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../utils';
-import { getMeasures } from '../../../api/measures';
+import Measure from '../../../components/measure/Measure';
 import { getChildren } from '../../../api/components';
+import { getMeasures } from '../../../api/measures';
 import { translate } from '../../../helpers/l10n';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
 import { fetchMetrics } from '../../../store/rootActions';
 import { getMetrics, Store } from '../../../store/rootReducer';
 import '../styles.css';
-import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
 
 interface OwnProps {
   component: T.Component;
@@ -140,9 +138,13 @@ export class App extends React.PureComponent<Props, State> {
     );
   }
 
-  renderMain() {
+  render() {
     const { component } = this.props;
-    const { measures, subComponents, totalSubComponents } = this.state;
+    const { loading, measures, subComponents, totalSubComponents } = this.state;
+
+    if (loading) {
+      return this.renderSpinner();
+    }
 
     if (this.isEmpty()) {
       return this.renderEmpty();
@@ -153,12 +155,54 @@ export class App extends React.PureComponent<Props, State> {
     }
 
     return (
-      <div>
+      <div className="page page-limited portfolio-overview">
+        <div className="page-actions">
+          <Report component={component} />
+        </div>
+        <h1>{translate('portfolio.health_factors')}</h1>
         <div className="portfolio-boxes">
-          <ReleasabilityBox component={component.key} measures={measures!} />
-          <ReliabilityBox component={component.key} measures={measures!} />
-          <SecurityBox component={component.key} measures={measures!} />
-          <MaintainabilityBox component={component.key} measures={measures!} />
+          <MetricBox component={component.key} measures={measures!} metricKey="releasability" />
+          <MetricBox component={component.key} measures={measures!} metricKey="reliability" />
+          <MetricBox component={component.key} measures={measures!} metricKey="vulnerabilities" />
+          <MetricBox component={component.key} measures={measures!} metricKey="security_hotspots" />
+          <MetricBox component={component.key} measures={measures!} metricKey="maintainability" />
+        </div>
+
+        <h1>{translate('portfolio.breakdown')}</h1>
+        <div className="portfolio-breakdown">
+          <div className="portfolio-breakdown-box">
+            <h2>{translate('portfolio.number_of_projects')}</h2>
+            <div className="portfolio-breakdown-metric">
+              <Measure
+                metricKey="projects"
+                metricType="SHORT_INT"
+                value={(measures && measures.projects) || '0'}
+              />
+            </div>
+            <div className="portfolio-breakdown-box-link">
+              <div>
+                <MeasuresButtonLink component={component.key} metric="projects" />
+              </div>
+            </div>
+          </div>
+          <div className="portfolio-breakdown-box">
+            <h2>{translate('portfolio.number_of_lines')}</h2>
+            <div className="portfolio-breakdown-metric">
+              <Measure
+                metricKey="ncloc"
+                metricType="SHORT_INT"
+                value={(measures && measures.ncloc) || '0'}
+              />
+            </div>
+            <div className="portfolio-breakdown-box-link">
+              <div>
+                <Link
+                  to={getComponentDrilldownUrl({ componentKey: component.key, metric: 'ncloc' })}>
+                  <span>{translate('portfolio.language_breakdown_link')}</span>
+                </Link>
+              </div>
+            </div>
+          </div>
         </div>
 
         {subComponents !== undefined && totalSubComponents !== undefined && (
@@ -171,49 +215,6 @@ export class App extends React.PureComponent<Props, State> {
       </div>
     );
   }
-
-  render() {
-    const { component } = this.props;
-    const { loading, measures } = this.state;
-
-    if (loading) {
-      return this.renderSpinner();
-    }
-
-    return (
-      <div className="page page-limited">
-        <div className="page-with-sidebar">
-          <div className="page-main">{this.renderMain()}</div>
-
-          <aside className="page-sidebar-fixed">
-            <div className="portfolio-meta-card">
-              <h4 className="portfolio-meta-header">
-                {translate('overview.about_this_portfolio')}
-                {component.visibility && (
-                  <PrivacyBadgeContainer
-                    className="spacer-left pull-right"
-                    organization={component.organization}
-                    qualifier={component.qualifier}
-                    tooltipProps={{ projectKey: component.key }}
-                    visibility={component.visibility}
-                  />
-                )}
-              </h4>
-              <Summary component={component} measures={measures || {}} />
-            </div>
-
-            <div className="portfolio-meta-card">
-              <Activity component={component.key} metrics={this.props.metrics} />
-            </div>
-
-            <div className="portfolio-meta-card">
-              <Report component={component} />
-            </div>
-          </aside>
-        </div>
-      </div>
-    );
-  }
 }
 
 const mapDispatchToProps: DispatchToProps = { fetchMetrics };
index 6762b42105b2fb8433d8fe61d5047c7b60fd76f7..e576fb8fd90813d22339fb3451815ac9976b7481 100644 (file)
@@ -52,7 +52,9 @@ export default function Effort({ component, effort, metricKey }: Props) {
                   metricType="SHORT_INT"
                   value={String(effort.projects)}
                 />
-                {translate('projects_')}
+                {effort.projects === 1
+                  ? translate('project_singular')
+                  : translate('project_plural')}
               </span>
             </Link>
           ),
index 756b31c2af8d3c667c03309713726f709adbeef9..4232584f30bed53bdc7efe597153089f8eef7797 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { Link } from 'react-router';
 import HistoryIcon from '../../../components/icons-components/HistoryIcon';
+import { translate } from '../../../helpers/l10n';
 import { getMeasureHistoryUrl } from '../../../helpers/urls';
 
 interface Props {
@@ -29,10 +30,9 @@ interface Props {
 
 export default function HistoryButtonLink({ component, metric }: Props) {
   return (
-    <Link
-      className="button button-small spacer-left text-text-bottom"
-      to={getMeasureHistoryUrl(component, metric)}>
-      <HistoryIcon size={14} />
+    <Link to={getMeasureHistoryUrl(component, metric)}>
+      <HistoryIcon className="little-spacer-right" size={14} />
+      <span>{translate('portfolio.activity_link')}</span>
     </Link>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx
deleted file mode 100644 (file)
index 29c58da..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 Effort from './Effort';
-import MainRating from './MainRating';
-import MeasuresButtonLink from './MeasuresButtonLink';
-import HistoryButtonLink from './HistoryButtonLink';
-import RatingFreshness from './RatingFreshness';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  component: string;
-  measures: T.Dict<string | undefined>;
-}
-
-export default function MaintainabilityBox({ component, measures }: Props) {
-  const rating = measures['sqale_rating'];
-  const lastMaintainabilityChange = measures['last_change_on_maintainability_rating'];
-  const rawEffort = measures['maintainability_rating_effort'];
-  const effort = rawEffort ? JSON.parse(rawEffort) : undefined;
-
-  return (
-    <div className="portfolio-box portfolio-maintainability">
-      <h2 className="portfolio-box-title">
-        {translate('metric_domain.Maintainability')}
-        <MeasuresButtonLink component={component} metric="Maintainability" />
-        <HistoryButtonLink component={component} metric="sqale_rating" />
-      </h2>
-
-      {rating && <MainRating component={component} metric={'sqale_rating'} value={rating} />}
-
-      <RatingFreshness lastChange={lastMaintainabilityChange} rating={rating} />
-
-      {effort && <Effort component={component} effort={effort} metricKey={'sqale_rating'} />}
-    </div>
-  );
-}
index 5a6a90d38050170f29fcf46cfc92d2398533fd6d..258eed0869e636b849e1a7be59b1dcc28fee7d11 100644 (file)
@@ -19,7 +19,8 @@
  */
 import * as React from 'react';
 import { Link } from 'react-router';
-import BubblesIcon from '../../../components/icons-components/BubblesIcon';
+import MeasuresIcon from '../../../components/icons-components/MeasuresIcon';
+import { translate } from '../../../helpers/l10n';
 import { getComponentDrilldownUrl } from '../../../helpers/urls';
 
 interface Props {
@@ -29,10 +30,9 @@ interface Props {
 
 export default function MeasuresButtonLink({ component, metric }: Props) {
   return (
-    <Link
-      className="button button-small spacer-left text-text-bottom"
-      to={getComponentDrilldownUrl({ componentKey: component, metric })}>
-      <BubblesIcon size={14} />
+    <Link to={getComponentDrilldownUrl({ componentKey: component, metric })}>
+      <MeasuresIcon className="little-spacer-right" size={14} />
+      <span>{translate('portfolio.measures_link')}</span>
     </Link>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx
new file mode 100644 (file)
index 0000000..f8f6074
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 Effort from './Effort';
+import HistoryButtonLink from './HistoryButtonLink';
+import MainRating from './MainRating';
+import MeasuresButtonLink from './MeasuresButtonLink';
+import RatingFreshness from './RatingFreshness';
+import { METRICS_PER_TYPE } from '../utils';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import Level from '../../../components/ui/Level';
+import Measure from '../../../components/measure/Measure';
+import { translate } from '../../../helpers/l10n';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
+
+interface Props {
+  component: string;
+  measures: T.Dict<string | undefined>;
+  metricKey: string;
+}
+
+export default function MetricBox({ component, measures, metricKey }: Props) {
+  const keys = METRICS_PER_TYPE[metricKey];
+  const rating = measures[keys.rating];
+  const lastReliabilityChange = measures[keys.last_change];
+  const rawEffort = measures[keys.effort];
+  const effort = rawEffort ? JSON.parse(rawEffort) : undefined;
+
+  return (
+    <div className="portfolio-box">
+      <h2 className="portfolio-box-title">
+        {translate(keys.label)}
+        <HelpTooltip
+          className="little-spacer-left"
+          overlay={translate('portfolio.metric_domain', metricKey, 'help')}
+        />
+      </h2>
+
+      {rating ? (
+        <MainRating component={component} metric={keys.rating} value={rating} />
+      ) : (
+        <div className="portfolio-box-rating">
+          <span className="rating no-rating">—</span>
+        </div>
+      )}
+
+      {rating && (
+        <>
+          <h3>{translate('portfolio.metric_trend')}</h3>
+          <RatingFreshness lastChange={lastReliabilityChange} rating={rating} />
+        </>
+      )}
+
+      {metricKey === 'releasability'
+        ? Number(effort) > 0 && (
+            <>
+              <h3>{translate('portfolio.lowest_rated_projects')}</h3>
+              <div className="portfolio-effort">
+                <Link
+                  to={getComponentDrilldownUrl({
+                    componentKey: component,
+                    metric: 'alert_status'
+                  })}>
+                  <span>
+                    <Measure
+                      className="little-spacer-right"
+                      metricKey="projects"
+                      metricType="SHORT_INT"
+                      value={effort}
+                    />
+                    {Number(effort) === 1
+                      ? translate('project_singular')
+                      : translate('project_plural')}
+                  </span>
+                </Link>{' '}
+                <Level level="ERROR" small={true} />
+              </div>
+            </>
+          )
+        : effort && (
+            <>
+              <h3>{translate('portfolio.lowest_rated_projects')}</h3>
+              <Effort component={component} effort={effort} metricKey={keys.rating} />
+            </>
+          )}
+
+      <div className="portfolio-box-links">
+        <div>
+          <MeasuresButtonLink component={component} metric={keys.measuresMetric} />
+        </div>
+        <div>
+          <HistoryButtonLink component={component} metric={keys.activity || keys.rating} />
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx
deleted file mode 100644 (file)
index 4fb6e1a..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 RatingFreshness from './RatingFreshness';
-import Rating from '../../../components/ui/Rating';
-import Measure from '../../../components/measure/Measure';
-import Level from '../../../components/ui/Level';
-import { translate } from '../../../helpers/l10n';
-import { getComponentDrilldownUrl } from '../../../helpers/urls';
-
-interface Props {
-  component: string;
-  measures: T.Dict<string | undefined>;
-}
-
-export default function ReleasabilityBox({ component, measures }: Props) {
-  const rating = measures['releasability_rating'];
-  const lastReleasabilityChange = measures['last_change_on_releasability_rating'];
-  const effort = measures['releasability_effort'];
-
-  return (
-    <div className="portfolio-box portfolio-releasability">
-      <h2 className="portfolio-box-title">{translate('metric_domain.Releasability')}</h2>
-
-      {rating && (
-        <Link
-          className="portfolio-box-rating"
-          to={getComponentDrilldownUrl({ componentKey: component, metric: 'alert_status' })}>
-          <Rating value={rating} />
-        </Link>
-      )}
-
-      <RatingFreshness lastChange={lastReleasabilityChange} rating={rating} />
-
-      {effort && Number(effort) > 0 && (
-        <div className="portfolio-effort">
-          <Link to={getComponentDrilldownUrl({ componentKey: component, metric: 'alert_status' })}>
-            <span>
-              <Measure
-                className="little-spacer-right"
-                metricKey="projects"
-                metricType="SHORT_INT"
-                value={effort}
-              />
-              {Number(effort) === 1 ? 'project' : 'projects'}
-            </span>
-          </Link>{' '}
-          <Level level="ERROR" small={true} />
-        </div>
-      )}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx
deleted file mode 100644 (file)
index 2e2c189..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 Effort from './Effort';
-import MeasuresButtonLink from './MeasuresButtonLink';
-import HistoryButtonLink from './HistoryButtonLink';
-import MainRating from './MainRating';
-import RatingFreshness from './RatingFreshness';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  component: string;
-  measures: T.Dict<string | undefined>;
-}
-
-export default function ReliabilityBox({ component, measures }: Props) {
-  const rating = measures['reliability_rating'];
-  const lastReliabilityChange = measures['last_change_on_reliability_rating'];
-  const rawEffort = measures['reliability_rating_effort'];
-  const effort = rawEffort ? JSON.parse(rawEffort) : undefined;
-
-  return (
-    <div className="portfolio-box portfolio-reliability">
-      <h2 className="portfolio-box-title">
-        {translate('metric_domain.Reliability')}
-        <MeasuresButtonLink component={component} metric="Reliability" />
-        <HistoryButtonLink component={component} metric="reliability_rating" />
-      </h2>
-
-      {rating && <MainRating component={component} metric="reliability_rating" value={rating} />}
-
-      <RatingFreshness lastChange={lastReliabilityChange} rating={rating} />
-
-      {effort && <Effort component={component} effort={effort} metricKey="reliability_rating" />}
-    </div>
-  );
-}
index 84751d4625066c42ff3758463843071106aa32ab..87ddf6b25e9392170f36bb2226a316c6e7fcf84f 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import SubscriptionContainer from './SubscriptionContainer';
+import Subscription from './Subscription';
+import { Button } from '../../../components/ui/buttons';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import Dropdown from '../../../components/controls/Dropdown';
 import { getReportStatus, ReportStatus, getReportUrl } from '../../../api/report';
 import { translate } from '../../../helpers/l10n';
 
@@ -44,7 +47,7 @@ export default class Report extends React.PureComponent<Props, State> {
     this.mounted = false;
   }
 
-  loadStatus() {
+  loadStatus = () => {
     getReportStatus(this.props.component.key).then(
       status => {
         if (this.mounted) {
@@ -57,52 +60,51 @@ export default class Report extends React.PureComponent<Props, State> {
         }
       }
     );
-  }
-
-  renderHeader = () => <h4>{translate('report.page')}</h4>;
+  };
 
   render() {
     const { component } = this.props;
     const { status, loading } = this.state;
 
-    if (loading) {
-      return (
-        <div>
-          {this.renderHeader()}
-          <i className="spinner" />
-        </div>
-      );
-    }
-
-    if (!status) {
+    if (loading || !status) {
       return null;
     }
 
-    return (
-      <div>
-        {this.renderHeader()}
-
-        {!status.canDownload && (
-          <div className="note js-report-cant-download">{translate('report.cant_download')}</div>
-        )}
-
-        {status.canDownload && (
-          <div className="js-report-can-download">
-            {translate('report.can_download')}
-            <div className="spacer-top">
+    return status.canSubscribe ? (
+      <Dropdown
+        overlay={
+          <ul className="menu">
+            <li>
               <a
-                className="button js-report-download"
                 download={component.name + ' - Executive Report.pdf'}
                 href={getReportUrl(component.key)}
                 target="_blank">
                 {translate('report.print')}
               </a>
-            </div>
-          </div>
-        )}
-
-        {status.canSubscribe && <SubscriptionContainer component={component.key} status={status} />}
-      </div>
+            </li>
+            <li>
+              <Subscription
+                component={component.key}
+                onSubscribe={this.loadStatus}
+                status={status}
+              />
+            </li>
+          </ul>
+        }
+        tagName="li">
+        <Button className="dropdown-toggle">
+          {translate('portfolio.pdf_report')}
+          <DropdownIcon className="spacer-left icon-half-transparent" />
+        </Button>
+      </Dropdown>
+    ) : (
+      <a
+        className="button"
+        download={component.name + ' - Executive Report.pdf'}
+        href={getReportUrl(component.key)}
+        target="_blank">
+        {translate('report.print')}
+      </a>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx
deleted file mode 100644 (file)
index b8101d7..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 Effort from './Effort';
-import MeasuresButtonLink from './MeasuresButtonLink';
-import HistoryButtonLink from './HistoryButtonLink';
-import RatingFreshness from './RatingFreshness';
-import MainRating from './MainRating';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  component: string;
-  measures: T.Dict<string | undefined>;
-}
-
-export default function SecurityBox({ component, measures }: Props) {
-  const rating = measures['security_rating'];
-  const lastSecurityChange = measures['last_change_on_security_rating'];
-  const rawEffort = measures['security_rating_effort'];
-  const effort = rawEffort ? JSON.parse(rawEffort) : undefined;
-
-  return (
-    <div className="portfolio-box portfolio-security">
-      <h2 className="portfolio-box-title">
-        {translate('metric_domain.Security')}
-        <MeasuresButtonLink component={component} metric="Security" />
-        <HistoryButtonLink component={component} metric="security_rating" />
-      </h2>
-
-      {rating && <MainRating component={component} metric="security_rating" value={rating} />}
-
-      <RatingFreshness lastChange={lastSecurityChange} rating={rating} />
-
-      {effort && <Effort component={component} effort={effort} metricKey="security_rating" />}
-    </div>
-  );
-}
index 476df52806d638f6163f740101382d08d8c0af32..12ff0e52a15d21fd916e0f207febdc29ca968d3d 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
+import { connect } from 'react-redux';
 import { ReportStatus, subscribe, unsubscribe } from '../../../api/report';
+import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage';
+import throwGlobalError from '../../../app/utils/throwGlobalError';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Button } from '../../../components/ui/buttons';
 import { isLoggedIn } from '../../../helpers/users';
+import { getCurrentUser, Store } from '../../../store/rootReducer';
 
 interface Props {
   component: string;
   currentUser: T.CurrentUser;
+  onSubscribe: () => void;
   status: ReportStatus;
 }
 
-interface State {
-  loading: boolean;
-  subscribed?: boolean;
-}
-
-export default class Subscription extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-    this.state = { subscribed: props.status.subscribed, loading: false };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillReceiveProps(nextProps: Props) {
-    if (nextProps.status.subscribed !== this.props.status.subscribed) {
-      this.setState({ subscribed: nextProps.status.subscribed });
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  stopLoading = () => {
-    if (this.mounted) {
-      this.setState({ loading: false });
-    }
-  };
-
+export class Subscription extends React.PureComponent<Props> {
   handleSubscription = (subscribed: boolean) => {
-    if (this.mounted) {
-      this.setState({ loading: false, subscribed });
-    }
+    addGlobalSuccessMessage(
+      subscribed
+        ? translateWithParameters('report.subscribe_x_success', this.getFrequencyText())
+        : translateWithParameters('report.unsubscribe_x_success', this.getFrequencyText())
+    );
+    this.props.onSubscribe();
   };
 
   handleSubscribe = () => {
-    this.setState({ loading: true });
     subscribe(this.props.component)
       .then(() => this.handleSubscription(true))
-      .catch(this.stopLoading);
+      .catch(throwGlobalError);
   };
 
   handleUnsubscribe = () => {
-    this.setState({ loading: true });
     unsubscribe(this.props.component)
       .then(() => this.handleSubscription(false))
-      .catch(this.stopLoading);
+      .catch(throwGlobalError);
   };
 
-  getEffectiveFrequencyText = () => {
+  getFrequencyText = () => {
     const effectiveFrequency =
       this.props.status.componentFrequency || this.props.status.globalFrequency;
-    return translate('report.frequency', effectiveFrequency, 'effective');
+    return translate('report.frequency', effectiveFrequency);
   };
 
-  renderLoading = () => this.state.loading && <i className="spacer-left spinner" />;
-
-  renderWhenSubscribed = () => (
-    <div className="js-subscribed">
-      <div className="spacer-bottom">
-        <AlertSuccessIcon className="pull-left spacer-right" />
-        <div className="overflow-hidden">
-          {translateWithParameters('report.subscribed', this.getEffectiveFrequencyText())}
-        </div>
-      </div>
-      <Button onClick={this.handleUnsubscribe}>{translate('report.unsubscribe')}</Button>
-      {this.renderLoading()}
-    </div>
-  );
-
-  renderWhenNotSubscribed = () => (
-    <div className="js-not-subscribed">
-      <p className="spacer-bottom">
-        {translateWithParameters('report.unsubscribed', this.getEffectiveFrequencyText())}
-      </p>
-      <Button className="js-report-subscribe" onClick={this.handleSubscribe}>
-        {translate('report.subscribe')}
-      </Button>
-      {this.renderLoading()}
-    </div>
-  );
-
   render() {
     const hasEmail = isLoggedIn(this.props.currentUser) && !!this.props.currentUser.email;
-    const { subscribed } = this.state;
 
-    let inner;
-    if (hasEmail) {
-      inner = subscribed ? this.renderWhenSubscribed() : this.renderWhenNotSubscribed();
-    } else {
-      inner = <p className="note js-no-email">{translate('report.no_email_to_subscribe')}</p>;
+    const { status } = this.props;
+
+    if (!hasEmail) {
+      return <span className="text-muted-2">{translate('report.no_email_to_subscribe')}</span>;
     }
 
-    return <div className="big-spacer-top js-report-subscription">{inner}</div>;
+    return status.subscribed ? (
+      <a href="#" onClick={this.handleUnsubscribe}>
+        {translateWithParameters('report.unsubscribe_x', this.getFrequencyText())}
+      </a>
+    ) : (
+      <a href="#" onClick={this.handleSubscribe}>
+        {translateWithParameters('report.subscribe_x', this.getFrequencyText())}
+      </a>
+    );
   }
 }
+
+const mapStateToProps = (state: Store) => ({
+  currentUser: getCurrentUser(state)
+});
+
+export default connect(mapStateToProps)(Subscription);
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx
deleted file mode 100644 (file)
index 461fee9..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { connect } from 'react-redux';
-import Subscription from './Subscription';
-import { getCurrentUser, Store } from '../../../store/rootReducer';
-
-const mapStateToProps = (state: Store) => ({
-  currentUser: getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(Subscription);
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx
deleted file mode 100644 (file)
index b396276..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
-import Measure from '../../../components/measure/Measure';
-import { translate } from '../../../helpers/l10n';
-import { getComponentDrilldownUrl } from '../../../helpers/urls';
-
-interface Props {
-  component: { description?: string; key: string };
-  measures: T.Dict<string | undefined>;
-}
-
-export default function Summary({ component, measures }: Props) {
-  const { projects, ncloc } = measures;
-  const nclocDistribution = measures['ncloc_language_distribution'];
-
-  return (
-    <section className="big-spacer-bottom" id="portfolio-summary">
-      {component.description && <div className="big-spacer-bottom">{component.description}</div>}
-
-      <ul className="portfolio-grid">
-        <li>
-          <div className="portfolio-measure-secondary-value">
-            {projects ? (
-              <Link
-                to={getComponentDrilldownUrl({ componentKey: component.key, metric: 'projects' })}>
-                <Measure metricKey="projects" metricType="SHORT_INT" value={projects} />
-              </Link>
-            ) : (
-              '0'
-            )}
-          </div>
-          <div className="spacer-top text-muted">{translate('projects')}</div>
-        </li>
-        <li>
-          <div className="portfolio-measure-secondary-value">
-            {ncloc ? (
-              <Link to={getComponentDrilldownUrl({ componentKey: component.key, metric: 'ncloc' })}>
-                <Measure metricKey="ncloc" metricType="SHORT_INT" value={ncloc} />
-              </Link>
-            ) : (
-              '0'
-            )}
-          </div>
-          <div className="spacer-top text-muted">{translate('metric.ncloc.name')}</div>
-        </li>
-      </ul>
-
-      {nclocDistribution && (
-        <div className="big-spacer-top">
-          <LanguageDistributionContainer distribution={nclocDistribution} width={260} />
-        </div>
-      )}
-    </section>
-  );
-}
index 2d9f11e89df0b49c3abbf8e98601b65690f4a4a1..344fd501672563b112b173f196eeb6b5b9da8d58 100644 (file)
@@ -59,7 +59,10 @@ export default function WorstProjects({ component, subComponents, total }: Props
               {translate('metric_domain.Reliability')}
             </th>
             <th className="text-center portfolio-sub-components-cell">
-              {translate('metric_domain.Security')}
+              {translate('portfolio.metric_domain.vulnerabilities')}
+            </th>
+            <th className="text-center portfolio-sub-components-cell">
+              {translate('portfolio.metric_domain.security_hotspots')}
             </th>
             <th className="text-center portfolio-sub-components-cell">
               {translate('metric_domain.Maintainability')}
@@ -84,6 +87,7 @@ export default function WorstProjects({ component, subComponents, total }: Props
                 : renderCell(component.measures, 'releasability_rating', 'RATING')}
               {renderCell(component.measures, 'reliability_rating', 'RATING')}
               {renderCell(component.measures, 'security_rating', 'RATING')}
+              {renderCell(component.measures, 'security_review_rating', 'RATING')}
               {renderCell(component.measures, 'sqale_rating', 'RATING')}
               {renderNcloc(component.measures, maxLoc)}
             </tr>
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
deleted file mode 100644 (file)
index d7a67e3..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 Activity from '../Activity';
-import { getAllTimeMachineData } from '../../../../api/time-machine';
-import { getProjectActivityGraph } from '../../../projectActivity/utils';
-
-jest.mock('../../../projectActivity/utils', () => {
-  const utils = require.requireActual('../../../projectActivity/utils');
-  utils.getProjectActivityGraph = jest
-    .fn()
-    .mockReturnValue({ graph: 'custom', customGraphs: ['coverage'] });
-  return utils;
-});
-
-jest.mock('../../../../api/time-machine', () => ({
-  getAllTimeMachineData: jest.fn().mockResolvedValue({
-    measures: [
-      {
-        metric: 'coverage',
-        history: [
-          { date: '2017-01-01T00:00:00.000Z', value: '73' },
-          { date: '2017-01-02T00:00:00.000Z', value: '82' }
-        ]
-      }
-    ]
-  })
-}));
-
-beforeEach(() => {
-  (getAllTimeMachineData as jest.Mock).mockClear();
-  (getProjectActivityGraph as jest.Mock).mockClear();
-});
-
-it('renders', () => {
-  const wrapper = shallow(<Activity component="foo" metrics={{}} />);
-  wrapper.setState({
-    history: {
-      coverage: [
-        { date: '2017-01-01T00:00:00.000Z', value: '73' },
-        { date: '2017-01-02T00:00:00.000Z', value: '82' }
-      ]
-    },
-    loading: false,
-    metrics: [{ key: 'coverage' }]
-  });
-  expect(wrapper).toMatchSnapshot();
-  expect(getProjectActivityGraph).toBeCalledWith('foo');
-});
-
-it('fetches history', () => {
-  mount(<Activity component="foo" metrics={{}} />);
-  expect(getAllTimeMachineData).toBeCalledWith({ component: 'foo', metrics: 'coverage' });
-});
index 8d50d6554094c2aff2bb51b546c7e30be8830b99..43647e64921770b6431e65dafa03d4f30acf7398 100644 (file)
@@ -26,21 +26,6 @@ jest.mock('../../../../api/components', () => ({
   getChildren: jest.fn(() => Promise.resolve({ components: [], paging: { total: 0 } }))
 }));
 
-// mock Activity to not deal with localstorage
-jest.mock('../Activity', () => ({
-  // eslint-disable-next-line
-  default: function Activity() {
-    return null;
-  }
-}));
-
-jest.mock('../Report', () => ({
-  // eslint-disable-next-line
-  default: function Report() {
-    return null;
-  }
-}));
-
 import * as React from 'react';
 import { shallow, mount } from 'enzyme';
 import { App } from '../App';
@@ -80,7 +65,7 @@ it('fetches measures and children components', () => {
   expect(getMeasures).toBeCalledWith({
     component: 'foo',
     metricKeys:
-      'projects,ncloc,ncloc_language_distribution,releasability_rating,releasability_effort,sqale_rating,maintainability_rating_effort,reliability_rating,reliability_rating_effort,security_rating,security_rating_effort,last_change_on_releasability_rating,last_change_on_maintainability_rating,last_change_on_security_rating,last_change_on_reliability_rating'
+      'projects,ncloc,ncloc_language_distribution,releasability_rating,releasability_effort,sqale_rating,maintainability_rating_effort,reliability_rating,reliability_rating_effort,security_rating,security_rating_effort,security_review_rating,security_review_rating_effort,last_change_on_releasability_rating,last_change_on_maintainability_rating,last_change_on_security_rating,last_change_on_security_review_rating,last_change_on_reliability_rating'
   });
   expect(getChildren).toBeCalledWith(
     'foo',
@@ -88,6 +73,7 @@ it('fetches measures and children components', () => {
       'ncloc',
       'releasability_rating',
       'security_rating',
+      'security_review_rating',
       'reliability_rating',
       'sqale_rating',
       'alert_status'
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx
deleted file mode 100644 (file)
index 7bc9830..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 MaintainabilityBox from '../MaintainabilityBox';
-
-it('renders', () => {
-  const measures = {
-    sqale_rating: '3',
-    last_change_on_maintainability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
-    maintainability_rating_effort: '{"rating":3,"projects":1}'
-  };
-  expect(shallow(<MaintainabilityBox component="foo" measures={measures} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MetricBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MetricBox-test.tsx
new file mode 100644 (file)
index 0000000..ada57c0
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 MetricBox from '../MetricBox';
+
+it('should render correctly', () => {
+  const measures = {
+    reliability_rating: '3',
+    last_change_on_reliability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
+    reliability_rating_effort: '{"rating":3,"projects":1}'
+  };
+  expect(
+    shallow(<MetricBox component="foo" measures={measures} metricKey="reliability" />)
+  ).toMatchSnapshot();
+});
+
+it('should render correctly for releasability', () => {
+  const measures = {
+    releasability_rating: '2',
+    last_change_on_releasability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
+    releasability_effort: '5'
+  };
+  expect(
+    shallow(<MetricBox component="foo" measures={measures} metricKey="releasability" />)
+  ).toMatchSnapshot();
+});
+
+it('should render correctly when no effort', () => {
+  const measures = {
+    releasability_rating: '2',
+    last_change_on_releasability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
+    releasability_effort: '0'
+  };
+
+  expect(
+    shallow(<MetricBox component="foo" measures={measures} metricKey="releasability" />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx
deleted file mode 100644 (file)
index 6a7b387..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 ReleasabilityBox from '../ReleasabilityBox';
-
-it('renders', () => {
-  const measures = {
-    releasability_rating: '3',
-    last_change_on_releasability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
-    releasability_effort: '7'
-  };
-  expect(shallow(<ReleasabilityBox component="foo" measures={measures} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx
deleted file mode 100644 (file)
index 6198a9b..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 ReliabilityBox from '../ReliabilityBox';
-
-it('renders', () => {
-  const measures = {
-    reliability_rating: '3',
-    last_change_on_reliability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
-    reliability_rating_effort: '{"rating":3,"projects":1}'
-  };
-  expect(shallow(<ReliabilityBox component="foo" measures={measures} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx
deleted file mode 100644 (file)
index 91feb1b..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 SecurityBox from '../SecurityBox';
-
-it('renders', () => {
-  const measures = {
-    security_rating: '3',
-    last_change_on_security_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
-    security_rating_effort: '{"rating":3,"projects":1}'
-  };
-  expect(shallow(<SecurityBox component="foo" measures={measures} />)).toMatchSnapshot();
-});
index d1aed9996c49b9d94a34ac5a8b701c1cd251901b..e5e33c6ace1ccb90b6c686476567efe6fda20eca 100644 (file)
@@ -26,59 +26,83 @@ jest.mock('../../../../api/report', () => {
 });
 
 import * as React from 'react';
-import { mount, shallow } from 'enzyme';
-import Subscription from '../Subscription';
+import { shallow, mount } from 'enzyme';
+import { Subscription } from '../Subscription';
 import { click, waitAndUpdate } from '../../../../helpers/testUtils';
+import { ReportStatus } from '../../../../api/report';
 
 const subscribe = require('../../../../api/report').subscribe as jest.Mock<any>;
 const unsubscribe = require('../../../../api/report').unsubscribe as jest.Mock<any>;
 
-const status = {
-  canDownload: true,
-  canSubscribe: true,
-  componentFrequency: 'montly',
-  globalFrequency: 'weekly',
-  subscribed: true
-};
-
-const currentUser = { isLoggedIn: true, email: 'foo@example.com' };
-
 beforeEach(() => {
   subscribe.mockClear();
   unsubscribe.mockClear();
 });
 
 it('renders when subscribed', () => {
-  expect(
-    shallow(<Subscription component="foo" currentUser={currentUser} status={status} />)
-  ).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot();
 });
 
 it('renders when not subscribed', () => {
-  expect(
-    shallow(
-      <Subscription
-        component="foo"
-        currentUser={currentUser}
-        status={{ ...status, subscribed: false }}
-      />
-    )
-  ).toMatchSnapshot();
+  expect(shallowRender({}, { subscribed: false })).toMatchSnapshot();
 });
 
 it('renders when no email', () => {
-  expect(
-    shallow(<Subscription component="foo" currentUser={{ isLoggedIn: false }} status={status} />)
-  ).toMatchSnapshot();
+  expect(shallowRender({ currentUser: { isLoggedIn: false } })).toMatchSnapshot();
 });
 
 it('changes subscription', async () => {
-  const wrapper = mount(<Subscription component="foo" currentUser={currentUser} status={status} />);
-  click(wrapper.find('button'));
+  const status = {
+    canDownload: true,
+    canSubscribe: true,
+    componentFrequency: 'montly',
+    globalFrequency: 'weekly',
+    subscribed: true
+  };
+
+  const currentUser = { isLoggedIn: true, email: 'foo@example.com' };
+
+  const wrapper = mount(
+    <Subscription
+      component="foo"
+      currentUser={currentUser}
+      onSubscribe={jest.fn()}
+      status={status}
+    />
+  );
+
+  click(wrapper.find('a'));
   expect(unsubscribe).toBeCalledWith('foo');
 
+  wrapper.setProps({ status: { ...status, subscribed: false } });
   await waitAndUpdate(wrapper);
 
-  click(wrapper.find('button'));
+  click(wrapper.find('a'));
   expect(subscribe).toBeCalledWith('foo');
 });
+
+function shallowRender(
+  props: Partial<Subscription['props']> = {},
+  statusOverrides: Partial<ReportStatus> = {}
+) {
+  const status = {
+    canDownload: true,
+    canSubscribe: true,
+    componentFrequency: 'montly',
+    globalFrequency: 'weekly',
+    subscribed: true,
+    ...statusOverrides
+  };
+
+  const currentUser = { isLoggedIn: true, email: 'foo@example.com' };
+
+  return shallow<Subscription>(
+    <Subscription
+      component="foo"
+      currentUser={currentUser}
+      onSubscribe={jest.fn()}
+      status={status}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx
deleted file mode 100644 (file)
index 14efe42..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 Summary from '../Summary';
-
-it('renders', () => {
-  expect(
-    shallow(
-      <Summary
-        component={{ description: 'blabla', key: 'foo' }}
-        measures={{ ncloc: '1234', ncloc_language_distribution: 'java=13;js=17', projects: '15' }}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
deleted file mode 100644 (file)
index 66a2f95..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
-  className="big-spacer-bottom"
->
-  <h4>
-    project_activity.page
-  </h4>
-  <withRouter(PreviewGraph)
-    history={
-      Object {
-        "coverage": Array [
-          Object {
-            "date": "2017-01-01T00:00:00.000Z",
-            "value": "73",
-          },
-          Object {
-            "date": "2017-01-02T00:00:00.000Z",
-            "value": "82",
-          },
-        ],
-      }
-    }
-    metrics={Object {}}
-    project="foo"
-    renderWhenEmpty={[Function]}
-  />
-</div>
-`;
index bee7a5e17b9f6b3c31dea5a12df98dec55fbe48c..9c400a1ff417848cfea9457a44fcfffc7ebd849b 100644 (file)
 
 exports[`renders 1`] = `
 <div
-  className="page page-limited"
+  className="page page-limited portfolio-overview"
 >
   <div
-    className="page-with-sidebar"
+    className="page-actions"
+  >
+    <Report
+      component={
+        Object {
+          "key": "foo",
+          "name": "Foo",
+          "qualifier": "TRK",
+        }
+      }
+    />
+  </div>
+  <h1>
+    portfolio.health_factors
+  </h1>
+  <div
+    className="portfolio-boxes"
+  >
+    <MetricBox
+      component="foo"
+      measures={
+        Object {
+          "ncloc": "173",
+          "reliability_rating": "1",
+        }
+      }
+      metricKey="releasability"
+    />
+    <MetricBox
+      component="foo"
+      measures={
+        Object {
+          "ncloc": "173",
+          "reliability_rating": "1",
+        }
+      }
+      metricKey="reliability"
+    />
+    <MetricBox
+      component="foo"
+      measures={
+        Object {
+          "ncloc": "173",
+          "reliability_rating": "1",
+        }
+      }
+      metricKey="vulnerabilities"
+    />
+    <MetricBox
+      component="foo"
+      measures={
+        Object {
+          "ncloc": "173",
+          "reliability_rating": "1",
+        }
+      }
+      metricKey="security_hotspots"
+    />
+    <MetricBox
+      component="foo"
+      measures={
+        Object {
+          "ncloc": "173",
+          "reliability_rating": "1",
+        }
+      }
+      metricKey="maintainability"
+    />
+  </div>
+  <h1>
+    portfolio.breakdown
+  </h1>
+  <div
+    className="portfolio-breakdown"
   >
     <div
-      className="page-main"
+      className="portfolio-breakdown-box"
     >
-      <div>
-        <div
-          className="portfolio-boxes"
-        >
-          <ReleasabilityBox
-            component="foo"
-            measures={
-              Object {
-                "ncloc": "173",
-                "reliability_rating": "1",
-              }
-            }
-          />
-          <ReliabilityBox
-            component="foo"
-            measures={
-              Object {
-                "ncloc": "173",
-                "reliability_rating": "1",
-              }
-            }
-          />
-          <SecurityBox
-            component="foo"
-            measures={
-              Object {
-                "ncloc": "173",
-                "reliability_rating": "1",
-              }
-            }
-          />
-          <MaintainabilityBox
+      <h2>
+        portfolio.number_of_projects
+      </h2>
+      <div
+        className="portfolio-breakdown-metric"
+      >
+        <Measure
+          metricKey="projects"
+          metricType="SHORT_INT"
+          value="0"
+        />
+      </div>
+      <div
+        className="portfolio-breakdown-box-link"
+      >
+        <div>
+          <MeasuresButtonLink
             component="foo"
-            measures={
-              Object {
-                "ncloc": "173",
-                "reliability_rating": "1",
-              }
-            }
+            metric="projects"
           />
         </div>
-        <WorstProjects
-          component="foo"
-          subComponents={Array []}
-          total={0}
-        />
       </div>
     </div>
-    <aside
-      className="page-sidebar-fixed"
+    <div
+      className="portfolio-breakdown-box"
     >
+      <h2>
+        portfolio.number_of_lines
+      </h2>
       <div
-        className="portfolio-meta-card"
+        className="portfolio-breakdown-metric"
       >
-        <h4
-          className="portfolio-meta-header"
-        >
-          overview.about_this_portfolio
-        </h4>
-        <Summary
-          component={
-            Object {
-              "key": "foo",
-              "name": "Foo",
-              "qualifier": "TRK",
-            }
-          }
-          measures={
-            Object {
-              "ncloc": "173",
-              "reliability_rating": "1",
-            }
-          }
+        <Measure
+          metricKey="ncloc"
+          metricType="SHORT_INT"
+          value="173"
         />
       </div>
       <div
-        className="portfolio-meta-card"
+        className="portfolio-breakdown-box-link"
       >
-        <Activity
-          component="foo"
-          metrics={Object {}}
-        />
-      </div>
-      <div
-        className="portfolio-meta-card"
-      >
-        <Report
-          component={
-            Object {
-              "key": "foo",
-              "name": "Foo",
-              "qualifier": "TRK",
+        <div>
+          <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/component_measures",
+                "query": Object {
+                  "id": "foo",
+                  "metric": "ncloc",
+                },
+              }
             }
-          }
-        />
+          >
+            <span>
+              portfolio.language_breakdown_link
+            </span>
+          </Link>
+        </div>
       </div>
-    </aside>
+    </div>
   </div>
+  <WorstProjects
+    component="foo"
+    subComponents={Array []}
+    total={0}
+  />
 </div>
 `;
 
 exports[`renders when portfolio is empty 1`] = `
 <div
-  className="page page-limited"
+  className="empty-search"
 >
-  <div
-    className="page-with-sidebar"
-  >
-    <div
-      className="page-main"
-    >
-      <div
-        className="empty-search"
-      >
-        <h3>
-          portfolio.empty
-        </h3>
-      </div>
-    </div>
-    <aside
-      className="page-sidebar-fixed"
-    >
-      <div
-        className="portfolio-meta-card"
-      >
-        <h4
-          className="portfolio-meta-header"
-        >
-          overview.about_this_portfolio
-        </h4>
-        <Summary
-          component={
-            Object {
-              "key": "foo",
-              "name": "Foo",
-              "qualifier": "TRK",
-            }
-          }
-          measures={
-            Object {
-              "reliability_rating": "1",
-            }
-          }
-        />
-      </div>
-      <div
-        className="portfolio-meta-card"
-      >
-        <Activity
-          component="foo"
-          metrics={Object {}}
-        />
-      </div>
-      <div
-        className="portfolio-meta-card"
-      >
-        <Report
-          component={
-            Object {
-              "key": "foo",
-              "name": "Foo",
-              "qualifier": "TRK",
-            }
-          }
-        />
-      </div>
-    </aside>
-  </div>
+  <h3>
+    portfolio.empty
+  </h3>
 </div>
 `;
 
 exports[`renders when portfolio is not computed 1`] = `
 <div
-  className="page page-limited"
+  className="empty-search"
 >
-  <div
-    className="page-with-sidebar"
-  >
-    <div
-      className="page-main"
-    >
-      <div
-        className="empty-search"
-      >
-        <h3>
-          portfolio.not_computed
-        </h3>
-      </div>
-    </div>
-    <aside
-      className="page-sidebar-fixed"
-    >
-      <div
-        className="portfolio-meta-card"
-      >
-        <h4
-          className="portfolio-meta-header"
-        >
-          overview.about_this_portfolio
-        </h4>
-        <Summary
-          component={
-            Object {
-              "key": "foo",
-              "name": "Foo",
-              "qualifier": "TRK",
-            }
-          }
-          measures={
-            Object {
-              "ncloc": "173",
-            }
-          }
-        />
-      </div>
-      <div
-        className="portfolio-meta-card"
-      >
-        <Activity
-          component="foo"
-          metrics={Object {}}
-        />
-      </div>
-      <div
-        className="portfolio-meta-card"
-      >
-        <Report
-          component={
-            Object {
-              "key": "foo",
-              "name": "Foo",
-              "qualifier": "TRK",
-            }
-          }
-        />
-      </div>
-    </aside>
-  </div>
+  <h3>
+    portfolio.not_computed
+  </h3>
 </div>
 `;
index ad1202079787af486ce187c1e01753ccbcb42fbd..8ee2d0d535502221e86dcefe640f16824cb0e44a 100644 (file)
@@ -30,7 +30,7 @@ exports[`renders 1`] = `
               metricType="SHORT_INT"
               value="3"
             />
-            projects_
+            project_plural
           </span>
         </Link>,
         "rating": <Rating
index 4b12c321e5f6787cc742ba7fb9683ba181d8e349..7cf000e0a10a69697eda9baccec1ab4ab1f7a745 100644 (file)
@@ -2,7 +2,6 @@
 
 exports[`renders 1`] = `
 <Link
-  className="button button-small spacer-left text-text-bottom"
   onlyActiveOnIndex={false}
   style={Object {}}
   to={
@@ -17,7 +16,11 @@ exports[`renders 1`] = `
   }
 >
   <HistoryIcon
+    className="little-spacer-right"
     size={14}
   />
+  <span>
+    portfolio.activity_link
+  </span>
 </Link>
 `;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap
deleted file mode 100644 (file)
index eaf8d26..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
-  className="portfolio-box portfolio-maintainability"
->
-  <h2
-    className="portfolio-box-title"
-  >
-    metric_domain.Maintainability
-    <MeasuresButtonLink
-      component="foo"
-      metric="Maintainability"
-    />
-    <HistoryButtonLink
-      component="foo"
-      metric="sqale_rating"
-    />
-  </h2>
-  <MainRating
-    component="foo"
-    metric="sqale_rating"
-    value="3"
-  />
-  <RatingFreshness
-    lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
-    rating="3"
-  />
-  <Effort
-    component="foo"
-    effort={
-      Object {
-        "projects": 1,
-        "rating": 3,
-      }
-    }
-    metricKey="sqale_rating"
-  />
-</div>
-`;
index b4afa69ade1b7e49fbb234c75ca6bd222ba8083a..5b5aff8aff9e08f2099afa27017c7f0665e79d1c 100644 (file)
@@ -2,7 +2,6 @@
 
 exports[`renders 1`] = `
 <Link
-  className="button button-small spacer-left text-text-bottom"
   onlyActiveOnIndex={false}
   style={Object {}}
   to={
@@ -15,8 +14,12 @@ exports[`renders 1`] = `
     }
   }
 >
-  <BubblesIcon
+  <MeasuresIcon
+    className="little-spacer-right"
     size={14}
   />
+  <span>
+    portfolio.measures_link
+  </span>
 </Link>
 `;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap
new file mode 100644 (file)
index 0000000..0642438
--- /dev/null
@@ -0,0 +1,181 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="portfolio-box"
+>
+  <h2
+    className="portfolio-box-title"
+  >
+    metric_domain.Reliability
+    <HelpTooltip
+      className="little-spacer-left"
+      overlay="portfolio.metric_domain.reliability.help"
+    />
+  </h2>
+  <MainRating
+    component="foo"
+    metric="reliability_rating"
+    value="3"
+  />
+  <h3>
+    portfolio.metric_trend
+  </h3>
+  <RatingFreshness
+    lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
+    rating="3"
+  />
+  <h3>
+    portfolio.lowest_rated_projects
+  </h3>
+  <Effort
+    component="foo"
+    effort={
+      Object {
+        "projects": 1,
+        "rating": 3,
+      }
+    }
+    metricKey="reliability_rating"
+  />
+  <div
+    className="portfolio-box-links"
+  >
+    <div>
+      <MeasuresButtonLink
+        component="foo"
+        metric="Reliability"
+      />
+    </div>
+    <div>
+      <HistoryButtonLink
+        component="foo"
+        metric="reliability_rating"
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correctly for releasability 1`] = `
+<div
+  className="portfolio-box"
+>
+  <h2
+    className="portfolio-box-title"
+  >
+    metric_domain.Releasability
+    <HelpTooltip
+      className="little-spacer-left"
+      overlay="portfolio.metric_domain.releasability.help"
+    />
+  </h2>
+  <MainRating
+    component="foo"
+    metric="releasability_rating"
+    value="2"
+  />
+  <h3>
+    portfolio.metric_trend
+  </h3>
+  <RatingFreshness
+    lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
+    rating="2"
+  />
+  <h3>
+    portfolio.lowest_rated_projects
+  </h3>
+  <div
+    className="portfolio-effort"
+  >
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/component_measures",
+          "query": Object {
+            "id": "foo",
+            "metric": "alert_status",
+          },
+        }
+      }
+    >
+      <span>
+        <Measure
+          className="little-spacer-right"
+          metricKey="projects"
+          metricType="SHORT_INT"
+          value={5}
+        />
+        project_plural
+      </span>
+    </Link>
+     
+    <Level
+      level="ERROR"
+      small={true}
+    />
+  </div>
+  <div
+    className="portfolio-box-links"
+  >
+    <div>
+      <MeasuresButtonLink
+        component="foo"
+        metric="Releasability"
+      />
+    </div>
+    <div>
+      <HistoryButtonLink
+        component="foo"
+        metric="releasability_rating"
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correctly when no effort 1`] = `
+<div
+  className="portfolio-box"
+>
+  <h2
+    className="portfolio-box-title"
+  >
+    metric_domain.Releasability
+    <HelpTooltip
+      className="little-spacer-left"
+      overlay="portfolio.metric_domain.releasability.help"
+    />
+  </h2>
+  <MainRating
+    component="foo"
+    metric="releasability_rating"
+    value="2"
+  />
+  <h3>
+    portfolio.metric_trend
+  </h3>
+  <RatingFreshness
+    lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
+    rating="2"
+  />
+  <div
+    className="portfolio-box-links"
+  >
+    <div>
+      <MeasuresButtonLink
+        component="foo"
+        metric="Releasability"
+      />
+    </div>
+    <div>
+      <HistoryButtonLink
+        component="foo"
+        metric="releasability_rating"
+      />
+    </div>
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap
deleted file mode 100644 (file)
index 4ab3d79..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
-  className="portfolio-box portfolio-releasability"
->
-  <h2
-    className="portfolio-box-title"
-  >
-    metric_domain.Releasability
-  </h2>
-  <Link
-    className="portfolio-box-rating"
-    onlyActiveOnIndex={false}
-    style={Object {}}
-    to={
-      Object {
-        "pathname": "/component_measures",
-        "query": Object {
-          "id": "foo",
-          "metric": "alert_status",
-        },
-      }
-    }
-  >
-    <Rating
-      value="3"
-    />
-  </Link>
-  <RatingFreshness
-    lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
-    rating="3"
-  />
-  <div
-    className="portfolio-effort"
-  >
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to={
-        Object {
-          "pathname": "/component_measures",
-          "query": Object {
-            "id": "foo",
-            "metric": "alert_status",
-          },
-        }
-      }
-    >
-      <span>
-        <Measure
-          className="little-spacer-right"
-          metricKey="projects"
-          metricType="SHORT_INT"
-          value="7"
-        />
-        projects
-      </span>
-    </Link>
-     
-    <Level
-      level="ERROR"
-      small={true}
-    />
-  </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap
deleted file mode 100644 (file)
index 426f629..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
-  className="portfolio-box portfolio-reliability"
->
-  <h2
-    className="portfolio-box-title"
-  >
-    metric_domain.Reliability
-    <MeasuresButtonLink
-      component="foo"
-      metric="Reliability"
-    />
-    <HistoryButtonLink
-      component="foo"
-      metric="reliability_rating"
-    />
-  </h2>
-  <MainRating
-    component="foo"
-    metric="reliability_rating"
-    value="3"
-  />
-  <RatingFreshness
-    lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
-    rating="3"
-  />
-  <Effort
-    component="foo"
-    effort={
-      Object {
-        "projects": 1,
-        "rating": 3,
-      }
-    }
-    metricKey="reliability_rating"
-  />
-</div>
-`;
index d955ec60f55ea4e1a9f74d7cfc9f9b0be8d4344a..d35590311e2acefec9449b0fc9f445e9c7e8db08 100644 (file)
@@ -1,49 +1,48 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`renders 1`] = `
-<div>
-  <h4>
-    report.page
-  </h4>
-  <i
-    className="spinner"
-  />
-</div>
-`;
+exports[`renders 1`] = `""`;
 
 exports[`renders 2`] = `
-<div>
-  <h4>
-    report.page
-  </h4>
-  <div
-    className="js-report-can-download"
-  >
-    report.can_download
-    <div
-      className="spacer-top"
+<Dropdown
+  overlay={
+    <ul
+      className="menu"
     >
-      <a
-        className="button js-report-download"
-        download="Foo - Executive Report.pdf"
-        href="/api/governance_reports/download?componentKey=foo"
-        target="_blank"
-      >
-        report.print
-      </a>
-    </div>
-  </div>
-  <Connect(Subscription)
-    component="foo"
-    status={
-      Object {
-        "canDownload": true,
-        "canSubscribe": true,
-        "componentFrequency": "montly",
-        "globalFrequency": "weekly",
-        "subscribed": true,
-      }
-    }
-  />
-</div>
+      <li>
+        <a
+          download="Foo - Executive Report.pdf"
+          href="/api/governance_reports/download?componentKey=foo"
+          target="_blank"
+        >
+          report.print
+        </a>
+      </li>
+      <li>
+        <Connect(Subscription)
+          component="foo"
+          onSubscribe={[Function]}
+          status={
+            Object {
+              "canDownload": true,
+              "canSubscribe": true,
+              "componentFrequency": "montly",
+              "globalFrequency": "weekly",
+              "subscribed": true,
+            }
+          }
+        />
+      </li>
+    </ul>
+  }
+  tagName="li"
+>
+  <Button
+    className="dropdown-toggle"
+  >
+    portfolio.pdf_report
+    <DropdownIcon
+      className="spacer-left icon-half-transparent"
+    />
+  </Button>
+</Dropdown>
 `;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap
deleted file mode 100644 (file)
index 3795a1b..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
-  className="portfolio-box portfolio-security"
->
-  <h2
-    className="portfolio-box-title"
-  >
-    metric_domain.Security
-    <MeasuresButtonLink
-      component="foo"
-      metric="Security"
-    />
-    <HistoryButtonLink
-      component="foo"
-      metric="security_rating"
-    />
-  </h2>
-  <MainRating
-    component="foo"
-    metric="security_rating"
-    value="3"
-  />
-  <RatingFreshness
-    lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
-    rating="3"
-  />
-  <Effort
-    component="foo"
-    effort={
-      Object {
-        "projects": 1,
-        "rating": 3,
-      }
-    }
-    metricKey="security_rating"
-  />
-</div>
-`;
index 321b77ff5a932063eec1079365c39a8dc2070af2..a3df93ebe6ec3975f2dcee5759de2a3767eae998 100644 (file)
@@ -1,63 +1,27 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`renders when no email 1`] = `
-<div
-  className="big-spacer-top js-report-subscription"
+<span
+  className="text-muted-2"
 >
-  <p
-    className="note js-no-email"
-  >
-    report.no_email_to_subscribe
-  </p>
-</div>
+  report.no_email_to_subscribe
+</span>
 `;
 
 exports[`renders when not subscribed 1`] = `
-<div
-  className="big-spacer-top js-report-subscription"
+<a
+  href="#"
+  onClick={[Function]}
 >
-  <div
-    className="js-not-subscribed"
-  >
-    <p
-      className="spacer-bottom"
-    >
-      report.unsubscribed.report.frequency.montly.effective
-    </p>
-    <Button
-      className="js-report-subscribe"
-      onClick={[Function]}
-    >
-      report.subscribe
-    </Button>
-  </div>
-</div>
+  report.subscribe_x.report.frequency.montly
+</a>
 `;
 
 exports[`renders when subscribed 1`] = `
-<div
-  className="big-spacer-top js-report-subscription"
+<a
+  href="#"
+  onClick={[Function]}
 >
-  <div
-    className="js-subscribed"
-  >
-    <div
-      className="spacer-bottom"
-    >
-      <AlertSuccessIcon
-        className="pull-left spacer-right"
-      />
-      <div
-        className="overflow-hidden"
-      >
-        report.subscribed.report.frequency.montly.effective
-      </div>
-    </div>
-    <Button
-      onClick={[Function]}
-    >
-      report.unsubscribe
-    </Button>
-  </div>
-</div>
+  report.unsubscribe_x.report.frequency.montly
+</a>
 `;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap
deleted file mode 100644 (file)
index 5c82871..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<section
-  className="big-spacer-bottom"
-  id="portfolio-summary"
->
-  <div
-    className="big-spacer-bottom"
-  >
-    blabla
-  </div>
-  <ul
-    className="portfolio-grid"
-  >
-    <li>
-      <div
-        className="portfolio-measure-secondary-value"
-      >
-        <Link
-          onlyActiveOnIndex={false}
-          style={Object {}}
-          to={
-            Object {
-              "pathname": "/component_measures",
-              "query": Object {
-                "id": "foo",
-                "metric": "projects",
-              },
-            }
-          }
-        >
-          <Measure
-            metricKey="projects"
-            metricType="SHORT_INT"
-            value="15"
-          />
-        </Link>
-      </div>
-      <div
-        className="spacer-top text-muted"
-      >
-        projects
-      </div>
-    </li>
-    <li>
-      <div
-        className="portfolio-measure-secondary-value"
-      >
-        <Link
-          onlyActiveOnIndex={false}
-          style={Object {}}
-          to={
-            Object {
-              "pathname": "/component_measures",
-              "query": Object {
-                "id": "foo",
-                "metric": "ncloc",
-              },
-            }
-          }
-        >
-          <Measure
-            metricKey="ncloc"
-            metricType="SHORT_INT"
-            value="1234"
-          />
-        </Link>
-      </div>
-      <div
-        className="spacer-top text-muted"
-      >
-        metric.ncloc.name
-      </div>
-    </li>
-  </ul>
-  <div
-    className="big-spacer-top"
-  >
-    <Connect(LanguageDistribution)
-      distribution="java=13;js=17"
-      width={260}
-    />
-  </div>
-</section>
-`;
index 736ab24806037a3c2bc3c6a2b5acb82176635d18..ee5e59a8907b0e9a32da7a5fa4cacd4976fcfc41 100644 (file)
@@ -26,7 +26,12 @@ exports[`renders 1`] = `
         <th
           className="text-center portfolio-sub-components-cell"
         >
-          metric_domain.Security
+          portfolio.metric_domain.vulnerabilities
+        </th>
+        <th
+          className="text-center portfolio-sub-components-cell"
+        >
+          portfolio.metric_domain.security_hotspots
         </th>
         <th
           className="text-center portfolio-sub-components-cell"
@@ -93,6 +98,14 @@ exports[`renders 1`] = `
             value="1"
           />
         </td>
+        <td
+          className="text-center"
+        >
+          <Measure
+            metricKey="security_review_rating"
+            metricType="RATING"
+          />
+        </td>
         <td
           className="text-center"
         >
@@ -181,6 +194,14 @@ exports[`renders 1`] = `
             value="1"
           />
         </td>
+        <td
+          className="text-center"
+        >
+          <Measure
+            metricKey="security_review_rating"
+            metricType="RATING"
+          />
+        </td>
         <td
           className="text-center"
         >
@@ -269,6 +290,14 @@ exports[`renders 1`] = `
             value="1"
           />
         </td>
+        <td
+          className="text-center"
+        >
+          <Measure
+            metricKey="security_review_rating"
+            metricType="RATING"
+          />
+        </td>
         <td
           className="text-center"
         >
index c64fc049184f0e5b50fc9d5a3ecb3d0836dd5461..3f810d2a79417b9a828fb88a768438ce9bb8a469 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.
  */
+.portfolio-overview > h1 {
+  font-weight: normal;
+}
+
+.portfolio-overview > .page-actions {
+  margin-bottom: 0;
+}
+
 .portfolio-measure-secondary-value {
   line-height: var(--controlHeight);
   font-size: 18px;
 
 .portfolio-freshness {
   line-height: var(--controlHeight);
-  margin-top: 12px;
-  color: var(--secondFontColor);
   font-size: var(--smallFontSize);
   white-space: nowrap;
 }
 
-.portfolio-effort {
-  margin-top: 12px;
-  padding-top: 12px;
-  border-top: 1px solid var(--barBorderColor);
-}
-
 .portfolio-boxes {
   display: flex;
   justify-content: space-between;
   align-items: stretch;
   margin-bottom: 20px;
   padding: 15px 0;
-  border: 1px solid var(--barBorderColor);
-  background-color: #fff;
+  width: 100%;
 }
 
 .portfolio-box {
+  flex: 1 0 10%;
   position: relative;
-  width: 25%;
-  padding: 0 5px;
-  border-radius: 3px;
+  padding: 0 calc(2 * var(--gridSize)) 66px;
+  margin: 0 var(--gridSize);
+  border: 1px solid var(--barBorderColor);
+  background-color: #fff;
   box-sizing: border-box;
-  text-align: center;
+}
+
+.portfolio-box:first-child {
+  margin-left: 0;
+}
+
+.portfolio-box:last-child {
+  margin-right: 0;
 }
 
 .portfolio-box-title {
-  margin-bottom: 25px;
+  padding: var(--gridSize) 0 calc(2 * var(--gridSize));
+  margin: var(--gridSize) 0 calc(2 * var(--gridSize));
   font-size: var(--bigFontSize);
+  line-height: var(--bigFontSize);
+  border-bottom: 1px solid var(--barBorderColor);
+  white-space: nowrap;
 }
 
 .portfolio-box-title > .button-small > svg {
   margin-top: 0;
 }
 
+.portfolio-box > h3 {
+  color: var(--secondFontColor);
+  font-size: 12px;
+  font-weight: normal;
+  margin-top: var(--gridSize);
+}
+
 .portfolio-box-rating,
 .portfolio-box-rating .rating {
   display: block;
-  width: 120px;
-  height: 120px;
-  line-height: 120px;
+  width: 80px;
+  height: 80px;
+  line-height: 80px;
 }
 
 .portfolio-box-rating {
-  margin: 0 auto;
+  margin: calc(2 * var(--gridSize)) auto;
   border: none;
 }
 
 .portfolio-box-rating .rating {
-  border-radius: 120px;
-  font-size: 60px;
+  border-radius: 80px;
+  font-size: 48px;
   text-align: center;
 }
 
+.portfolio-box-rating .rating.no-rating {
+  color: var(--secondFontColor);
+}
+
+.portfolio-box-links {
+  border-top: 1px solid var(--barBorderColor);
+  text-align: center;
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}
+
+.portfolio-box-links > div {
+  display: inline-block;
+  padding: calc(1.5 * var(--gridSize)) 0;
+  width: 50%;
+  box-sizing: border-box;
+}
+
+.portfolio-box-links > div:first-child {
+  border-right: 1px solid var(--barBorderColor);
+}
+
+.portfolio-box-links a,
+.portfolio-breakdown-box-link a {
+  border: none;
+}
+
+.portfolio-box-links svg,
+.portfolio-breakdown-box-link svg {
+  vertical-align: middle;
+}
+
+.portfolio-box-links a > span,
+.portfolio-breakdown-box-link a > span {
+  border-bottom: 1px solid #cae3f2;
+}
+
+.portfolio-breakdown {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+}
+
+.portfolio-breakdown-box {
+  flex: 0 0 auto;
+  background-color: white;
+  border: 1px solid var(--barBorderColor);
+  margin: var(--gridSize) var(--gridSize) calc(2 * var(--gridSize));
+  padding: 0 var(--gridSize) 66px;
+  position: relative;
+}
+
+.portfolio-breakdown-box:first-child {
+  margin-left: 0;
+}
+
+.portfolio-breakdown-box:last-child {
+  margin-right: 0;
+}
+
+.portfolio-breakdown-box > h2 {
+  color: var(--secondFontColor);
+  margin: var(--gridSize);
+  font-size: 12px;
+}
+
+.portfolio-breakdown-box > .portfolio-breakdown-metric {
+  font-size: var(--hugeFontSize);
+  margin-left: var(--gridSize);
+}
+
+.portfolio-breakdown-box-link {
+  border-top: 1px solid var(--barBorderColor);
+  padding: calc(2 * var(--gridSize));
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}
+
 .portfolio-sub-components table.data > thead > tr > th {
   font-size: var(--baseFontSize);
   text-transform: none;
+  vertical-align: middle;
 }
 
 .portfolio-sub-components-cell {
-  width: 90px;
+  width: 110px;
 }
 
 .portfolio-meta-header {
index ea66cfb73983f4ff4e7d2b6b678f847064c4447e..a7c80c00e9062e089ebf6ab6dee76f660a316a31 100644 (file)
@@ -34,16 +34,69 @@ export const PORTFOLIO_METRICS = [
   'security_rating',
   'security_rating_effort',
 
+  'security_review_rating',
+  'security_review_rating_effort',
+
   'last_change_on_releasability_rating',
   'last_change_on_maintainability_rating',
   'last_change_on_security_rating',
+  'last_change_on_security_review_rating',
   'last_change_on_reliability_rating'
 ];
 
+export interface MetricKeys {
+  activity?: string;
+  effort: string;
+  measuresMetric: string;
+  label: string;
+  last_change: string;
+  rating: string;
+}
+
+export const METRICS_PER_TYPE: T.Dict<MetricKeys> = {
+  releasability: {
+    measuresMetric: 'Releasability',
+    label: 'metric_domain.Releasability',
+    rating: 'releasability_rating',
+    effort: 'releasability_effort',
+    last_change: 'last_change_on_releasability_rating'
+  },
+  reliability: {
+    measuresMetric: 'Reliability',
+    label: 'metric_domain.Reliability',
+    rating: 'reliability_rating',
+    effort: 'reliability_rating_effort',
+    last_change: 'last_change_on_reliability_rating'
+  },
+  vulnerabilities: {
+    measuresMetric: 'Security',
+    label: 'portfolio.metric_domain.vulnerabilities',
+    rating: 'security_rating',
+    effort: 'security_rating_effort',
+    last_change: 'last_change_on_security_rating',
+    activity: 'security_rating,vulnerabilities'
+  },
+  security_hotspots: {
+    measuresMetric: 'security_review_rating',
+    label: 'portfolio.metric_domain.security_hotspots',
+    rating: 'security_review_rating',
+    effort: 'security_review_rating_effort',
+    last_change: 'last_change_on_security_review_rating'
+  },
+  maintainability: {
+    measuresMetric: 'Maintainability',
+    label: 'metric_domain.Maintainability',
+    rating: 'sqale_rating',
+    effort: 'maintainability_rating_effort',
+    last_change: 'last_change_on_maintainability_rating'
+  }
+};
+
 export const SUB_COMPONENTS_METRICS = [
   'ncloc',
   'releasability_rating',
   'security_rating',
+  'security_review_rating',
   'reliability_rating',
   'sqale_rating',
   'alert_status'
diff --git a/server/sonar-web/src/main/js/components/icons-components/MeasuresIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/MeasuresIcon.tsx
new file mode 100644 (file)
index 0000000..4747e58
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 Icon, { IconProps } from './Icon';
+
+export default function MeasuresIcon({ className, fill = 'currentColor', size }: IconProps) {
+  return (
+    <Icon className={className} size={size} style={{ fillRule: 'nonzero' }}>
+      <path d="M3.33 6.13h2v6.54h-2zm3.74-2.8h1.86v9.34H7.07zm3.73 5.34h1.87v4H10.8z" fill={fill} />
+    </Icon>
+  );
+}
index b5cd959f4ea0815294a22f661470e990b092f257..164e7135f9e7cb9aa2b52def44d2cca521e62114 100644 (file)
@@ -137,6 +137,8 @@ plugin=Plugin
 project=Project
 projects=Projects
 projects_=project(s)
+project_singular=project
+project_plural=projects
 projects_management=Projects Management
 quality_profile=Quality Profile
 raw=Raw
@@ -3014,7 +3016,31 @@ portfolio.no_lines_of_code=All projects in this portfolio are empty
 portfolio.not_computed=This portfolio is not yet computed.
 portfolio.app.empty=This application is empty.
 portfolio.app.no_lines_of_code=All projects in this application are empty
+portfolio.metric_trend=Metric trend
+portfolio.lowest_rated_projects=Lowest rated projects
+portfolio.health_factors=Portfolio health factors
+portfolio.activity_link=Activity
+portfolio.measures_link=Measures
+portfolio.language_breakdown_link=Language breakdown
+portfolio.breakdown=Portfolio breakdown
+portfolio.pdf_report=Portfolio PDF Report
+portfolio.number_of_projects=Number of projects
+portfolio.number_of_lines=Number of lines of code
 
+portfolio.metric_domain.vulnerabilities=Security Vulnerabilities
+portfolio.metric_domain.security_hotspots=Security Hotspots Review
+
+#------------------------------------------------------------------------------
+#
+# METRIC DOMAINS HELP TEXT
+#
+#------------------------------------------------------------------------------
+
+portfolio.metric_domain.releasability.help=Ratio of projects in the Portfolio that have passed the Quality Gate.
+portfolio.metric_domain.reliability.help=Average Reliability rating for all projects in the portfolio.
+portfolio.metric_domain.vulnerabilities.help=Average security rating for all projects in the portfolio.
+portfolio.metric_domain.security_hotspots.help=Ratio of To Review or In Review Security Hotspots per 1k lines of code.
+portfolio.metric_domain.maintainability.help=Average maintainability rating for all projects in the portfolio.
 
 #------------------------------------------------------------------------------
 #