Browse Source

SONAR-12156 Add security review rating to portfolio overview

tags/7.8
Jeremy Davis 4 years ago
parent
commit
af99676ca6
40 changed files with 971 additions and 1496 deletions
  1. 0
    116
      server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
  2. 59
    58
      server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
  3. 3
    1
      server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx
  4. 4
    4
      server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx
  5. 0
    54
      server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx
  6. 5
    5
      server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx
  7. 115
    0
      server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx
  8. 0
    71
      server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx
  9. 0
    54
      server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx
  10. 36
    34
      server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx
  11. 0
    54
      server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx
  12. 35
    78
      server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx
  13. 0
    28
      server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx
  14. 0
    75
      server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx
  15. 5
    1
      server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
  16. 0
    72
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
  17. 2
    16
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
  18. 0
    31
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx
  19. 28
    3
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MetricBox-test.tsx
  20. 0
    31
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx
  21. 0
    31
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx
  22. 54
    30
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx
  23. 0
    30
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
  24. 137
    214
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap
  25. 1
    1
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap
  26. 4
    1
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap
  27. 0
    40
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap
  28. 5
    2
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap
  29. 181
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap
  30. 0
    67
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap
  31. 0
    40
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap
  32. 42
    43
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap
  33. 0
    40
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap
  34. 14
    50
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap
  35. 0
    86
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap
  36. 30
    1
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
  37. 124
    22
      server/sonar-web/src/main/js/apps/portfolio/styles.css
  38. 53
    0
      server/sonar-web/src/main/js/apps/portfolio/utils.ts
  39. 8
    12
      server/sonar-web/src/main/js/components/icons-components/MeasuresIcon.tsx
  40. 26
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 116
server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx View File

@@ -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>
);
}
}

+ 59
- 58
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx View File

@@ -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<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 };

+ 3
- 1
server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx View 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>
),

+ 4
- 4
server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx View 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>
);
}

+ 0
- 54
server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx View File

@@ -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>
);
}

+ 5
- 5
server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx View 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>
);
}

+ 115
- 0
server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx View File

@@ -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>
);
}

+ 0
- 71
server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx View File

@@ -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>
);
}

+ 0
- 54
server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx View File

@@ -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>
);
}

+ 36
- 34
server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx View File

@@ -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<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>
);
}
}

+ 0
- 54
server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx View File

@@ -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>
);
}

+ 35
- 78
server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx View File

@@ -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<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);

+ 0
- 28
server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx View File

@@ -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);

+ 0
- 75
server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx View File

@@ -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>
);
}

+ 5
- 1
server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx View 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>

+ 0
- 72
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx View File

@@ -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' });
});

+ 2
- 16
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx View 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'

+ 0
- 31
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx View File

@@ -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();
});

server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx → server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MetricBox-test.tsx View File

@@ -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(<ReliabilityBox component="foo" measures={measures} />)).toMatchSnapshot();
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();
});

+ 0
- 31
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx View File

@@ -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();
});

+ 0
- 31
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx View File

@@ -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();
});

+ 54
- 30
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx View 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}
/>
);
}

+ 0
- 30
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap View File

@@ -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>
`;

+ 137
- 214
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -2,251 +2,174 @@

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>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap View File

@@ -30,7 +30,7 @@ exports[`renders 1`] = `
metricType="SHORT_INT"
value="3"
/>
projects_
project_plural
</span>
</Link>,
"rating": <Rating

+ 4
- 1
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap View 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>
`;

+ 0
- 40
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap View File

@@ -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>
`;

+ 5
- 2
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap View 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>
`;

+ 181
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap View File

@@ -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>
`;

+ 0
- 67
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap View File

@@ -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>
`;

+ 0
- 40
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap View File

@@ -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>
`;

+ 42
- 43
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap View 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>
`;

+ 0
- 40
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap View File

@@ -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>
`;

+ 14
- 50
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap View 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>
`;

+ 0
- 86
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap View File

@@ -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>
`;

+ 30
- 1
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap View 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"
>

+ 124
- 22
server/sonar-web/src/main/js/apps/portfolio/styles.css View File

@@ -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 {

+ 53
- 0
server/sonar-web/src/main/js/apps/portfolio/utils.ts View 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'

server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx → server/sonar-web/src/main/js/components/icons-components/MeasuresIcon.tsx View File

@@ -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(
<Summary
component={{ description: 'blabla', key: 'foo' }}
measures={{ ncloc: '1234', ncloc_language_distribution: 'java=13;js=17', projects: '15' }}
/>
)
).toMatchSnapshot();
});
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>
);
}

+ 26
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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.

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save