@@ -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> | |||
); | |||
} | |||
} |
@@ -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 }; |
@@ -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> | |||
), |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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); |
@@ -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); |
@@ -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> | |||
); | |||
} |
@@ -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> |
@@ -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' }); | |||
}); |
@@ -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' |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -30,7 +30,7 @@ exports[`renders 1`] = ` | |||
metricType="SHORT_INT" | |||
value="3" | |||
/> | |||
projects_ | |||
project_plural | |||
</span> | |||
</Link>, | |||
"rating": <Rating |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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" | |||
> |
@@ -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 { |
@@ -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' |
@@ -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> | |||
); | |||
} |
@@ -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. | |||
#------------------------------------------------------------------------------ | |||
# |