From af99676ca6fd08f4bc03faa0b78f4c900c2d93dc Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 6 Jun 2019 11:38:41 +0200 Subject: [PATCH] SONAR-12156 Add security review rating to portfolio overview --- .../js/apps/portfolio/components/Activity.tsx | 116 ------ .../main/js/apps/portfolio/components/App.tsx | 117 +++--- .../js/apps/portfolio/components/Effort.tsx | 4 +- .../components/HistoryButtonLink.tsx | 8 +- .../components/MaintainabilityBox.tsx | 54 --- .../components/MeasuresButtonLink.tsx | 10 +- .../apps/portfolio/components/MetricBox.tsx | 115 ++++++ .../portfolio/components/ReleasabilityBox.tsx | 71 ---- .../portfolio/components/ReliabilityBox.tsx | 54 --- .../js/apps/portfolio/components/Report.tsx | 70 ++-- .../apps/portfolio/components/SecurityBox.tsx | 54 --- .../portfolio/components/Subscription.tsx | 113 ++---- .../components/SubscriptionContainer.tsx | 28 -- .../js/apps/portfolio/components/Summary.tsx | 75 ---- .../portfolio/components/WorstProjects.tsx | 6 +- .../components/__tests__/Activity-test.tsx | 72 ---- .../components/__tests__/App-test.tsx | 18 +- .../__tests__/MaintainabilityBox-test.tsx | 31 -- ...abilityBox-test.tsx => MetricBox-test.tsx} | 31 +- .../__tests__/ReleasabilityBox-test.tsx | 31 -- .../components/__tests__/SecurityBox-test.tsx | 31 -- .../__tests__/Subscription-test.tsx | 84 +++-- .../__snapshots__/Activity-test.tsx.snap | 30 -- .../__tests__/__snapshots__/App-test.tsx.snap | 351 +++++++----------- .../__snapshots__/Effort-test.tsx.snap | 2 +- .../HistoryButtonLink-test.tsx.snap | 5 +- .../MaintainabilityBox-test.tsx.snap | 40 -- .../MeasuresButtonLink-test.tsx.snap | 7 +- .../__snapshots__/MetricBox-test.tsx.snap | 181 +++++++++ .../ReleasabilityBox-test.tsx.snap | 67 ---- .../ReliabilityBox-test.tsx.snap | 40 -- .../__snapshots__/Report-test.tsx.snap | 85 +++-- .../__snapshots__/SecurityBox-test.tsx.snap | 40 -- .../__snapshots__/Subscription-test.tsx.snap | 64 +--- .../__snapshots__/Summary-test.tsx.snap | 86 ----- .../__snapshots__/WorstProjects-test.tsx.snap | 31 +- .../src/main/js/apps/portfolio/styles.css | 146 ++++++-- .../src/main/js/apps/portfolio/utils.ts | 53 +++ .../icons-components/MeasuresIcon.tsx} | 20 +- .../resources/org/sonar/l10n/core.properties | 26 ++ 40 files changed, 971 insertions(+), 1496 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx create mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx rename server/sonar-web/src/main/js/apps/portfolio/components/__tests__/{ReliabilityBox-test.tsx => MetricBox-test.tsx} (54%) delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap rename server/sonar-web/src/main/js/{apps/portfolio/components/__tests__/Summary-test.tsx => components/icons-components/MeasuresIcon.tsx} (71%) 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 index 4e08fb2e46f..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx +++ /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; -} - -interface State { - history?: { - [metric: string]: Array<{ date: Date; value?: string }>; - }; - loading: boolean; -} - -export default class Activity extends React.PureComponent { - 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> = {}; - 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 = () =>
{translate('component_measures.no_history')}
; - - render() { - return ( -
-

{translate('project_activity.page')}

- - {this.state.loading ? ( - - ) : ( - this.state.history !== undefined && ( - - ) - )} -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx index eb8b5f89ec1..c1b063059e3 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx @@ -19,23 +19,21 @@ */ 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 { ); } - 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 { } return ( -
+
+
+ +
+

{translate('portfolio.health_factors')}

- - - - + + + + + +
+ +

{translate('portfolio.breakdown')}

+
+
+

{translate('portfolio.number_of_projects')}

+
+ +
+
+
+ +
+
+
+
+

{translate('portfolio.number_of_lines')}

+
+ +
+
+
+ + {translate('portfolio.language_breakdown_link')} + +
+
+
{subComponents !== undefined && totalSubComponents !== undefined && ( @@ -171,49 +215,6 @@ export class App extends React.PureComponent {
); } - - render() { - const { component } = this.props; - const { loading, measures } = this.state; - - if (loading) { - return this.renderSpinner(); - } - - return ( -
-
-
{this.renderMain()}
- - -
-
- ); - } } const mapDispatchToProps: DispatchToProps = { fetchMetrics }; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx index 6762b42105b..e576fb8fd90 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx @@ -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')} ), diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx index 756b31c2af8..4232584f30b 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx @@ -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 ( - - + + + {translate('portfolio.activity_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 index 29c58da3fea..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx +++ /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; -} - -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 ( -
-

- {translate('metric_domain.Maintainability')} - - -

- - {rating && } - - - - {effort && } -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx index 5a6a90d3805..258eed0869e 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx @@ -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 ( - - + + + {translate('portfolio.measures_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 index 00000000000..f8f607458a8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx @@ -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; + 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 ( +
+

+ {translate(keys.label)} + +

+ + {rating ? ( + + ) : ( +
+ — +
+ )} + + {rating && ( + <> +

{translate('portfolio.metric_trend')}

+ + + )} + + {metricKey === 'releasability' + ? Number(effort) > 0 && ( + <> +

{translate('portfolio.lowest_rated_projects')}

+
+ + + + {Number(effort) === 1 + ? translate('project_singular') + : translate('project_plural')} + + {' '} + +
+ + ) + : effort && ( + <> +

{translate('portfolio.lowest_rated_projects')}

+ + + )} + +
+
+ +
+
+ +
+
+
+ ); +} 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 index 4fb6e1ad37a..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx +++ /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; -} - -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 ( -
-

{translate('metric_domain.Releasability')}

- - {rating && ( - - - - )} - - - - {effort && Number(effort) > 0 && ( -
- - - - {Number(effort) === 1 ? 'project' : 'projects'} - - {' '} - -
- )} -
- ); -} 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 index 2e2c1893d11..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx +++ /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; -} - -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 ( -
-

- {translate('metric_domain.Reliability')} - - -

- - {rating && } - - - - {effort && } -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx index 84751d46250..87ddf6b25e9 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx @@ -18,7 +18,10 @@ * 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 { 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 { } } ); - } - - renderHeader = () =>

{translate('report.page')}

; + }; render() { const { component } = this.props; const { status, loading } = this.state; - if (loading) { - return ( -
- {this.renderHeader()} - -
- ); - } - - if (!status) { + if (loading || !status) { return null; } - return ( -
- {this.renderHeader()} - - {!status.canDownload && ( -
{translate('report.cant_download')}
- )} - - {status.canDownload && ( -
- {translate('report.can_download')} -
+ return status.canSubscribe ? ( + +
  • {translate('report.print')} -
  • -
    - )} - - {status.canSubscribe && } -
    + +
  • + +
  • + + } + tagName="li"> + + + ) : ( + + {translate('report.print')} + ); } } 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 index b8101d7f8f9..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx +++ /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; -} - -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 ( -
    -

    - {translate('metric_domain.Security')} - - -

    - - {rating && } - - - - {effort && } -
    - ); -} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx index 476df52806d..12ff0e52a15 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx @@ -18,115 +18,72 @@ * 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 { - 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 { 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 && ; - - renderWhenSubscribed = () => ( -
    -
    - -
    - {translateWithParameters('report.subscribed', this.getEffectiveFrequencyText())} -
    -
    - - {this.renderLoading()} -
    - ); - - renderWhenNotSubscribed = () => ( -
    -

    - {translateWithParameters('report.unsubscribed', this.getEffectiveFrequencyText())} -

    - - {this.renderLoading()} -
    - ); - 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 =

    {translate('report.no_email_to_subscribe')}

    ; + const { status } = this.props; + + if (!hasEmail) { + return {translate('report.no_email_to_subscribe')}; } - return
    {inner}
    ; + return status.subscribed ? ( + + {translateWithParameters('report.unsubscribe_x', this.getFrequencyText())} + + ) : ( + + {translateWithParameters('report.subscribe_x', this.getFrequencyText())} + + ); } } + +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 index 461fee991e9..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx +++ /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 index b396276a003..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx +++ /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; -} - -export default function Summary({ component, measures }: Props) { - const { projects, ncloc } = measures; - const nclocDistribution = measures['ncloc_language_distribution']; - - return ( -
    - {component.description &&
    {component.description}
    } - -
      -
    • -
      - {projects ? ( - - - - ) : ( - '0' - )} -
      -
      {translate('projects')}
      -
    • -
    • -
      - {ncloc ? ( - - - - ) : ( - '0' - )} -
      -
      {translate('metric.ncloc.name')}
      -
    • -
    - - {nclocDistribution && ( -
    - -
    - )} -
    - ); -} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx index 2d9f11e89df..344fd501672 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx @@ -59,7 +59,10 @@ export default function WorstProjects({ component, subComponents, total }: Props {translate('metric_domain.Reliability')} - {translate('metric_domain.Security')} + {translate('portfolio.metric_domain.vulnerabilities')} + + + {translate('portfolio.metric_domain.security_hotspots')} {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)} 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 index d7a67e3c275..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx +++ /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(); - 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(); - expect(getAllTimeMachineData).toBeCalledWith({ component: 'foo', metrics: 'coverage' }); -}); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx index 8d50d655409..43647e64921 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx @@ -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 index 7bc9830a92d..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx +++ /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()).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__/MetricBox-test.tsx similarity index 54% rename from server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx rename to server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MetricBox-test.tsx index 6198a9bbdda..ada57c03b2c 100644 --- 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__/MetricBox-test.tsx @@ -19,13 +19,38 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import ReliabilityBox from '../ReliabilityBox'; +import MetricBox from '../MetricBox'; -it('renders', () => { +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()).toMatchSnapshot(); + expect( + shallow() + ).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() + ).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() + ).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 index 6a7b387e230..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx +++ /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()).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 index 91feb1bd2a3..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx +++ /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()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx index d1aed9996c4..e5e33c6ace1 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx @@ -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; const unsubscribe = require('../../../../api/report').unsubscribe as jest.Mock; -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() - ).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot(); }); it('renders when not subscribed', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); + expect(shallowRender({}, { subscribed: false })).toMatchSnapshot(); }); it('renders when no email', () => { - expect( - shallow() - ).toMatchSnapshot(); + expect(shallowRender({ currentUser: { isLoggedIn: false } })).toMatchSnapshot(); }); it('changes subscription', async () => { - const wrapper = mount(); - 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( + + ); + + 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 = {}, + statusOverrides: Partial = {} +) { + const status = { + canDownload: true, + canSubscribe: true, + componentFrequency: 'montly', + globalFrequency: 'weekly', + subscribed: true, + ...statusOverrides + }; + + const currentUser = { isLoggedIn: true, email: 'foo@example.com' }; + + return shallow( + + ); +} 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 index 66a2f952e56..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
    -

    - project_activity.page -

    - -
    -`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap index bee7a5e17b9..9c400a1ff41 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap @@ -2,251 +2,174 @@ exports[`renders 1`] = `
    + +
    +

    + portfolio.health_factors +

    +
    + + + + + +
    +

    + portfolio.breakdown +

    +
    -
    -
    - - - - + portfolio.number_of_projects + +
    + +
    +
    +
    +
    -
    -
    - +
    +
    `; exports[`renders when portfolio is empty 1`] = `
    -
    -
    -
    -

    - portfolio.empty -

    -
    -
    - -
    +

    + portfolio.empty +

    `; exports[`renders when portfolio is not computed 1`] = `
    -
    -
    -
    -

    - portfolio.not_computed -

    -
    -
    - -
    +

    + portfolio.not_computed +

    `; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap index ad120207978..8ee2d0d5355 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap @@ -30,7 +30,7 @@ exports[`renders 1`] = ` metricType="SHORT_INT" value="3" /> - projects_ + project_plural , "rating": + + portfolio.activity_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 index eaf8d2609a4..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
    -

    - metric_domain.Maintainability - - -

    - - - -
    -`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap index b4afa69ade1..5b5aff8aff9 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap @@ -2,7 +2,6 @@ exports[`renders 1`] = ` - + + portfolio.measures_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 index 00000000000..0642438cda9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap @@ -0,0 +1,181 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
    +

    + metric_domain.Reliability + +

    + +

    + portfolio.metric_trend +

    + +

    + portfolio.lowest_rated_projects +

    + +
    +
    + +
    +
    + +
    +
    +
    +`; + +exports[`should render correctly for releasability 1`] = ` +
    +

    + metric_domain.Releasability + +

    + +

    + portfolio.metric_trend +

    + +

    + portfolio.lowest_rated_projects +

    +
    + + + + project_plural + + + + +
    +
    +
    + +
    +
    + +
    +
    +
    +`; + +exports[`should render correctly when no effort 1`] = ` +
    +

    + metric_domain.Releasability + +

    + +

    + portfolio.metric_trend +

    + +
    +
    + +
    +
    + +
    +
    +
    +`; 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 index 4ab3d79f908..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
    -

    - metric_domain.Releasability -

    - - - - -
    - - - - projects - - - - -
    -
    -`; 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 index 426f6290acd..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
    -

    - metric_domain.Reliability - - -

    - - - -
    -`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap index d955ec60f55..d35590311e2 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap @@ -1,49 +1,48 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders 1`] = ` -
    -

    - report.page -

    - -
    -`; +exports[`renders 1`] = `""`; exports[`renders 2`] = ` -
    -

    - report.page -

    -
    - report.can_download - -
    - -
    +
  • + + report.print + +
  • +
  • + +
  • + + } + tagName="li" +> + + `; 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 index 3795a1b585f..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
    -

    - metric_domain.Security - - -

    - - - -
    -`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap index 321b77ff5a9..a3df93ebe6e 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap @@ -1,63 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders when no email 1`] = ` -
    -

    - report.no_email_to_subscribe -

    -
    + report.no_email_to_subscribe + `; exports[`renders when not subscribed 1`] = ` -
    -
    -

    - report.unsubscribed.report.frequency.montly.effective -

    - -
    -
    + report.subscribe_x.report.frequency.montly + `; exports[`renders when subscribed 1`] = ` -
    -
    -
    - -
    - report.subscribed.report.frequency.montly.effective -
    -
    - -
    -
    + report.unsubscribe_x.report.frequency.montly + `; 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 index 5c828719017..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap +++ /dev/null @@ -1,86 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
    -
    - blabla -
    -
      -
    • -
      - - - -
      -
      - projects -
      -
    • -
    • -
      - - - -
      -
      - metric.ncloc.name -
      -
    • -
    -
    - -
    -
    -`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap index 736ab248060..ee5e59a8907 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap @@ -26,7 +26,12 @@ exports[`renders 1`] = ` - metric_domain.Security + portfolio.metric_domain.vulnerabilities + + + portfolio.metric_domain.security_hotspots + + + @@ -181,6 +194,14 @@ exports[`renders 1`] = ` value="1" /> + + + @@ -269,6 +290,14 @@ exports[`renders 1`] = ` value="1" /> + + + diff --git a/server/sonar-web/src/main/js/apps/portfolio/styles.css b/server/sonar-web/src/main/js/apps/portfolio/styles.css index c64fc049184..3f810d2a794 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/styles.css +++ b/server/sonar-web/src/main/js/apps/portfolio/styles.css @@ -17,6 +17,14 @@ * 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; @@ -43,72 +51,166 @@ .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 { diff --git a/server/sonar-web/src/main/js/apps/portfolio/utils.ts b/server/sonar-web/src/main/js/apps/portfolio/utils.ts index ea66cfb7398..a7c80c00e90 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/utils.ts +++ b/server/sonar-web/src/main/js/apps/portfolio/utils.ts @@ -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 = { + 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/apps/portfolio/components/__tests__/Summary-test.tsx b/server/sonar-web/src/main/js/components/icons-components/MeasuresIcon.tsx similarity index 71% rename from server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx rename to server/sonar-web/src/main/js/components/icons-components/MeasuresIcon.tsx index 14efe424dd8..4747e582eab 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/MeasuresIcon.tsx @@ -18,16 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { shallow } from 'enzyme'; -import Summary from '../Summary'; +import Icon, { IconProps } from './Icon'; -it('renders', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); +export default function MeasuresIcon({ className, fill = 'currentColor', size }: IconProps) { + return ( + + + + ); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index b5cd959f4ea..164e7135f9e 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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. #------------------------------------------------------------------------------ # -- 2.39.5