diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-05-13 16:24:17 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-05-13 16:24:17 +0200 |
commit | 3efea992de3a883b22321a42c0c639364b3fbf11 (patch) | |
tree | ef25eaa2aee7e0c7b76f2173ca75283c0a1af84c /server | |
parent | f77e302dde33cd34ade4311e36c03aa898c61669 (diff) | |
download | sonarqube-3efea992de3a883b22321a42c0c639364b3fbf11.tar.gz sonarqube-3efea992de3a883b22321a42c0c639364b3fbf11.zip |
refactor project overview page (#908)
Diffstat (limited to 'server')
58 files changed, 2088 insertions, 2517 deletions
diff --git a/server/sonar-web/.jscsrc b/server/sonar-web/.jscsrc index 0b5067c2de7..4b865a25859 100644 --- a/server/sonar-web/.jscsrc +++ b/server/sonar-web/.jscsrc @@ -33,5 +33,6 @@ }, "requireSpaceBeforeDestructuredValues": true, "requireSpaceBeforeObjectValues": true, - "requireSpacesInsideObjectBrackets": "all" + "requireSpacesInsideObjectBrackets": "all", + "requirePaddingNewLinesBeforeLineComments": false } diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js index 67063af2737..07c90a9b306 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js @@ -20,9 +20,9 @@ import React from 'react'; import Measure from './../components/Measure'; -import LanguageDistribution from './../components/LanguageDistribution'; +import LanguageDistribution from '../../../components/charts/LanguageDistribution'; import LeakPeriodLegend from '../components/LeakPeriodLegend'; -import { ComplexityDistribution } from '../../overview/components/complexity-distribution'; +import { ComplexityDistribution } from '../../../components/shared/complexity-distribution'; import { isDiffMetric, formatLeak } from '../utils'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; import { getLocalizedMetricName } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/overview/app.js b/server/sonar-web/src/main/js/apps/overview/app.js index 6c9b8872b54..5d8ad1b4a17 100644 --- a/server/sonar-web/src/main/js/apps/overview/app.js +++ b/server/sonar-web/src/main/js/apps/overview/app.js @@ -18,26 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import ReactDOM from 'react-dom'; +import { render } from 'react-dom'; -import OverviewApp from './components/OverviewApp'; -import EmptyOverview from './components/EmptyOverview'; +import App from './components/App'; -const LEAK_PERIOD = '1'; - -class App { - start (options) { - const opts = { ...options, ...window.sonarqube.overview }; - Object.assign(opts.component, options.component); - - const el = document.querySelector(opts.el); - - if (opts.component.hasSnapshot) { - ReactDOM.render(<OverviewApp {...opts} leakPeriodIndex={LEAK_PERIOD}/>, el); - } else { - ReactDOM.render(<EmptyOverview {...opts}/>, el); - } - } -} - -window.sonarqube.appStarted.then(options => new App().start(options)); +window.sonarqube.appStarted.then(options => { + const el = document.querySelector(options.el); + const component = { ...options.component, ...window.sonarqube.overview.component }; + render(( + <App component={component}/> + ), el); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewMain.js b/server/sonar-web/src/main/js/apps/overview/components/App.js index 015c3cde255..d791a021882 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewMain.js +++ b/server/sonar-web/src/main/js/apps/overview/components/App.js @@ -18,21 +18,32 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; -import Gate from '../gate/gate'; -import GeneralMain from './../main/main'; -import Meta from './Meta'; +import OverviewApp from './OverviewApp'; +import EmptyOverview from './EmptyOverview'; +import { ComponentType } from '../propTypes'; -export default function OverviewMain (props) { - return ( - <div className="page page-limited"> - <div className="overview"> - <div className="overview-main"> - <Gate component={props.component} gate={props.gate}/> - <GeneralMain {...props}/> - </div> - <Meta component={props.component}/> - </div> - </div> - ); +export default class App extends React.Component { + static propTypes = { + component: ComponentType.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + const { component } = this.props; + + if (!component.snapshotDate) { + return <EmptyOverview {...this.props}/>; + } + + return ( + <OverviewApp + {...this.props} + leakPeriodIndex="1"/> + ); + } } diff --git a/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.js b/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.js index b39005a49e5..5d88e24b8d0 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.js +++ b/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.js @@ -21,7 +21,7 @@ import React from 'react'; import { translate } from '../../../helpers/l10n'; -export default function EmptyOverview ({ component }) { +const EmptyOverview = ({ component }) => { return ( <div className="page page-limited"> <div className="alert alert-warning"> @@ -33,4 +33,6 @@ export default function EmptyOverview ({ component }) { </div> </div> ); -} +}; + +export default EmptyOverview; diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/periods.js b/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js index 20bc9981ff0..a2129c56b13 100644 --- a/server/sonar-web/src/main/js/apps/overview/helpers/periods.js +++ b/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js @@ -17,25 +17,31 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import _ from 'underscore'; +import React from 'react'; import moment from 'moment'; -import { getPeriodLabel as getLabel } from '../../../helpers/periods'; +import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; +import { translateWithParameters } from '../../../helpers/l10n'; -export function getPeriodLabel (periods, periodIndex) { - const period = _.findWhere(periods, { index: periodIndex }); +const LeakPeriodLegend = ({ period }) => { + const leakPeriodLabel = getPeriodLabel(period); + const leakPeriodDate = getPeriodDate(period); - if (!period) { - return null; - } + const momentDate = moment(leakPeriodDate); + const fromNow = momentDate.fromNow(); + const tooltip = translateWithParameters( + 'overview.started_on_x', + momentDate.format('LL')); - return getLabel(period); -} + return ( + <div className="overview-legend" title={tooltip} data-toggle="tooltip"> + {translateWithParameters('overview.leak_period_x', leakPeriodLabel)} + <br/> + <span className="note"> + {translateWithParameters('overview.started_x', fromNow)} + </span> + </div> + ); +}; -export function getPeriodDate (periods, periodIndex) { - const period = _.findWhere(periods, { index: periodIndex }); - if (!period) { - return null; - } - return period.date ? moment(period.date).toDate() : null; -} +export default LeakPeriodLegend; diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js index 1bf027ac518..fdd667d396d 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -18,28 +18,137 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import moment from 'moment'; +import shallowCompare from 'react-addons-shallow-compare'; -import OverviewMain from './OverviewMain'; -import { getMetrics } from '../../../api/metrics'; +import QualityGate from '../qualityGate/QualityGate'; +import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities'; +import CodeSmells from '../main/CodeSmells'; +import Coverage from '../main/Coverage'; +import Duplications from '../main/Duplications'; +import Size from '../main/Size'; +import Meta from './../meta/Meta'; +import { getMeasuresAndMeta } from '../../../api/measures'; +import { getTimeMachineData } from '../../../api/time-machine'; +import { enhanceMeasuresWithMetrics } from '../../../helpers/measures'; +import { getLeakPeriod } from '../../../helpers/periods'; +import { ComponentType } from '../propTypes'; + +import '../styles.css'; + +const METRICS = [ + // quality gate + 'alert_status', + 'quality_gate_details', + + // bugs + 'bugs', + 'new_bugs', + 'reliability_rating', + + // vulnerabilities + 'vulnerabilities', + 'new_vulnerabilities', + 'security_rating', + + // code smells + 'code_smells', + 'new_code_smells', + 'sqale_rating', + 'sqale_index', + 'new_technical_debt', + + // coverage + 'overall_coverage', + 'new_overall_coverage', + 'coverage', + 'new_coverage', + 'it_coverage', + 'new_it_coverage', + 'tests', + + // duplications + 'duplicated_lines_density', + 'duplicated_blocks', + + // size + 'ncloc', + 'ncloc_language_distribution' +]; + +const HISTORY_METRICS_LIST = [ + 'sqale_index', + 'duplicated_lines_density', + 'ncloc', + 'overall_coverage', + 'it_coverage', + 'coverage' +]; export default class OverviewApp extends React.Component { - state = {}; + static propTypes = { + component: ComponentType.isRequired + }; + + state = { + loading: true + }; componentDidMount () { this.mounted = true; document.querySelector('html').classList.add('dashboard-page'); - this.requestMetrics(); + this.loadMeasures(this.props.component) + .then(() => this.loadHistory(this.props.component)); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentDidUpdate (nextProps) { + if (this.props.component !== nextProps.component) { + this.loadMeasures(nextProps.component) + .then(() => this.loadHistory(nextProps.component)); + } } componentWillUnmount () { this.mounted = false; - document.querySelector('html').classList.delete('dashboard-page'); + document.querySelector('html').classList.remove('dashboard-page'); + } + + loadMeasures (component) { + this.setState({ loading: true }); + + return getMeasuresAndMeta( + component.key, + METRICS, + { additionalFields: 'metrics,periods' } + ).then(r => { + if (this.mounted) { + this.setState({ + loading: false, + measures: enhanceMeasuresWithMetrics(r.component.measures, r.metrics), + periods: r.periods + }); + } + }); } - requestMetrics () { - return getMetrics().then(metrics => { + loadHistory (component) { + const metrics = HISTORY_METRICS_LIST.join(','); + return getTimeMachineData(component.key, metrics).then(r => { if (this.mounted) { - this.setState({ metrics }); + const history = {}; + r[0].cols.forEach((col, index) => { + history[col.metric] = r[0].cells.map(cell => { + const date = moment(cell.d).toDate(); + const value = cell.v[index] || 0; + return { date, value }; + }); + }); + const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date; + this.setState({ history, historyStartDate }); } }); } @@ -53,10 +162,37 @@ export default class OverviewApp extends React.Component { } render () { - if (!this.state.metrics) { + const { component } = this.props; + const { loading, measures, periods, history, historyStartDate } = this.state; + + if (loading) { return this.renderLoading(); } - return <OverviewMain {...this.props} metrics={this.state.metrics}/>; + const leakPeriod = getLeakPeriod(periods); + const domainProps = { component, measures, leakPeriod, history, historyStartDate }; + + return ( + <div className="page page-limited"> + <div className="overview"> + <div className="overview-main"> + <QualityGate + component={component} + measures={measures} + periods={periods}/> + + <div className="overview-domains-list"> + <BugsAndVulnerabilities {...domainProps}/> + <CodeSmells {...domainProps}/> + <Coverage {...domainProps}/> + <Duplications {...domainProps}/> + <Size {...domainProps}/> + </div> + </div> + + <Meta component={component}/> + </div> + </div> + ); } } diff --git a/server/sonar-web/src/main/js/apps/overview/main/timeline.js b/server/sonar-web/src/main/js/apps/overview/components/Timeline.js index 81f76ceb266..90a9728f4ca 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/timeline.js +++ b/server/sonar-web/src/main/js/apps/overview/components/Timeline.js @@ -19,16 +19,31 @@ */ import d3 from 'd3'; import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; import { LineChart } from '../../../components/charts/line-chart'; const HEIGHT = 80; -export class Timeline extends React.Component { +export default class Timeline extends React.Component { + static propTypes = { + history: React.PropTypes.arrayOf( + React.PropTypes.object + ).isRequired, + before: React.PropTypes.object, + after: React.PropTypes.object + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + filterSnapshots () { - return this.props.history.filter(s => { - const matchBefore = !this.props.before || s.date <= this.props.before; - const matchAfter = !this.props.after || s.date >= this.props.after; + const { history, before, after } = this.props; + + return history.filter(s => { + const matchBefore = !before || s.date <= before; + const matchAfter = !after || s.date >= after; return matchBefore && matchAfter; }); } @@ -46,19 +61,16 @@ export class Timeline extends React.Component { const domain = [0, d3.max(this.props.history, d => d.value)]; - return <LineChart data={data} - domain={domain} - interpolate="basis" - displayBackdrop={true} - displayPoints={false} - displayVerticalGrid={false} - height={HEIGHT} - padding={[0, 0, 0, 0]}/>; + return ( + <LineChart + data={data} + domain={domain} + interpolate="basis" + displayBackdrop={true} + displayPoints={false} + displayVerticalGrid={false} + height={HEIGHT} + padding={[0, 0, 0, 0]}/> + ); } } - -Timeline.propTypes = { - history: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - before: React.PropTypes.object, - after: React.PropTypes.object -}; diff --git a/server/sonar-web/src/main/js/apps/overview/components/coverage-selection-mixin.js b/server/sonar-web/src/main/js/apps/overview/components/coverage-selection-mixin.js deleted file mode 100644 index 1f5fb57f227..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/components/coverage-selection-mixin.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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. - */ -export const CoverageSelectionMixin = { - getCoverageMetricPrefix (measures) { - if (measures['coverage'] != null && measures['it_coverage'] != null && measures['overall_coverage'] != null) { - return 'overall_'; - } else if (measures['coverage'] != null) { - return ''; - } else { - return 'it_'; - } - } -}; diff --git a/server/sonar-web/src/main/js/apps/overview/components/language-distribution.js b/server/sonar-web/src/main/js/apps/overview/components/language-distribution.js deleted file mode 100644 index 73e568a94c6..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/components/language-distribution.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 _ from 'underscore'; -import React from 'react'; - -import { Histogram } from '../../../components/charts/histogram'; -import { formatMeasure } from '../../../helpers/measures'; -import { getLanguages } from '../../../api/languages'; -import { translate } from '../../../helpers/l10n'; - -export const LanguageDistribution = React.createClass({ - propTypes: { - distribution: React.PropTypes.string.isRequired, - lines: React.PropTypes.number.isRequired - }, - - componentDidMount () { - this.requestLanguages(); - }, - - requestLanguages () { - getLanguages().then(languages => this.setState({ languages })); - }, - - getLanguageName (langKey) { - if (this.state && this.state.languages) { - const lang = _.findWhere(this.state.languages, { key: langKey }); - return lang ? lang.name : translate('unknown'); - } else { - return langKey; - } - }, - - cutLanguageName (name) { - return name.length > 10 ? `${name.substr(0, 7)}...` : name; - }, - - renderBarChart () { - let data = this.props.distribution.split(';').map((point, index) => { - const tokens = point.split('='); - return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] }; - }); - - data = _.sortBy(data, d => -d.x); - - const yTicks = data.map(point => this.getLanguageName(point.value)).map(this.cutLanguageName); - const yValues = data.map(point => { - const percent = point.x / this.props.lines * 100; - return percent >= 0.1 ? formatMeasure(percent, 'PERCENT') : '<0.1%'; - }); - - return <Histogram data={data} - yTicks={yTicks} - yValues={yValues} - height={data.length * 25} - barsWidth={10} - padding={[0, 60, 0, 80]}/>; - }, - - render () { - const count = this.props.distribution.split(';').length; - const height = count * 25; - - return <div className="overview-bar-chart" style={{ height }}> - {this.renderBarChart()} - </div>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/components/event.js b/server/sonar-web/src/main/js/apps/overview/events/Event.js index 35832990704..acef6b789cb 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/event.js +++ b/server/sonar-web/src/main/js/apps/overview/events/Event.js @@ -20,32 +20,37 @@ import React from 'react'; import moment from 'moment'; -import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; +import { EventType } from '../propTypes'; +import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; import { translate } from '../../../helpers/l10n'; -export const Event = React.createClass({ - propTypes: { - event: React.PropTypes.shape({ - id: React.PropTypes.string.isRequired, - date: React.PropTypes.object.isRequired, - type: React.PropTypes.string.isRequired, - name: React.PropTypes.string.isRequired, - text: React.PropTypes.string - }) - }, +const Event = ({ event }) => { + return ( + <TooltipsContainer> + <li className="spacer-top"> + <p> + <strong className="js-event-type"> + {translate('event.category', event.type)} + </strong> + {': '} + <span className="js-event-name">{event.name}</span> + {event.text && ( + <i + className="spacer-left icon-help" + data-toggle="tooltip" + title={event.text}/> + )} + </p> + <p className="note little-spacer-top js-event-date"> + {moment(event.date).format('LL')} + </p> + </li> + </TooltipsContainer> + ); +}; - mixins: [TooltipsMixin], +Event.propTypes = { + event: EventType.isRequired +}; - render () { - const { event } = this.props; - return <li className="spacer-top"> - <p> - <strong className="js-event-type">{translate('event.category', event.type)}</strong> - : - <span className="js-event-name">{event.name}</span> - { event.text && <i className="spacer-left icon-help" data-toggle="tooltip" title={event.text}/> } - </p> - <p className="note little-spacer-top js-event-date">{moment(event.date).format('LL')}</p> - </li>; - } -}); +export default Event; diff --git a/server/sonar-web/src/main/js/apps/overview/components/EventsList.js b/server/sonar-web/src/main/js/apps/overview/events/EventsList.js index 7ce48f1c416..40ae9b25573 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/EventsList.js +++ b/server/sonar-web/src/main/js/apps/overview/events/EventsList.js @@ -19,9 +19,10 @@ */ import moment from 'moment'; import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; -import { Event } from './event'; -import { EventsListFilter } from './events-list-filter'; +import Event from './Event'; +import EventsListFilter from './EventsListFilter'; import { getEvents } from '../../../api/events'; import { translate } from '../../../helpers/l10n'; @@ -39,6 +40,10 @@ export default class EventsList extends React.Component { this.fetchEvents(); } + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + componentDidUpdate (nextProps) { if (nextProps.component !== this.props.component) { this.fetchEvents(); diff --git a/server/sonar-web/src/main/js/apps/overview/components/events-list-filter.js b/server/sonar-web/src/main/js/apps/overview/events/EventsListFilter.js index 2dcb0eaf398..cfd142fd3e8 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/events-list-filter.js +++ b/server/sonar-web/src/main/js/apps/overview/events/EventsListFilter.js @@ -23,28 +23,30 @@ import { translate } from '../../../helpers/l10n'; const TYPES = ['All', 'Version', 'Alert', 'Profile', 'Other']; -export const EventsListFilter = React.createClass({ - propTypes: { - onFilter: React.PropTypes.func.isRequired, - currentFilter: React.PropTypes.string.isRequired - }, +const EventsListFilter = ({ currentFilter, onFilter }) => { + const handleChange = selected => onFilter(selected.value); - handleChange(selected) { - this.props.onFilter(selected.value); - }, + const options = TYPES.map(type => { + return { + value: type, + label: translate('event.category', type) + }; + }); - render () { - const options = TYPES.map(type => { - return { - value: type, - label: translate('event.category', type) - }; - }); - return <Select value={this.props.currentFilter} - options={options} - clearable={false} - searchable={false} - onChange={this.handleChange} - style={{ width: '125px' }}/>; - } -}); + return ( + <Select + value={currentFilter} + options={options} + clearable={false} + searchable={false} + onChange={handleChange} + style={{ width: '125px' }}/> + ); +}; + +EventsListFilter.propTypes = { + onFilter: React.PropTypes.func.isRequired, + currentFilter: React.PropTypes.string.isRequired +}; + +export default EventsListFilter; diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js deleted file mode 100644 index 621102841ba..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; - -import { getPeriodLabel, getPeriodDate } from '../helpers/periods'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; -import { formatMeasure } from '../../../helpers/measures'; -import { translate } from '../../../helpers/l10n'; - -const Measure = React.createClass({ - render() { - if (this.props.value == null || isNaN(this.props.value)) { - return null; - } - const formatted = formatMeasure(this.props.value, this.props.type); - return <span>{formatted}</span>; - } -}); - -export default React.createClass({ - render() { - const metricName = translate('metric', this.props.condition.metric.name, 'name'); - const threshold = this.props.condition.level === 'ERROR' ? - this.props.condition.error : this.props.condition.warning; - const period = this.props.condition.period ? - getPeriodLabel(this.props.component.periods, this.props.condition.period) : null; - const periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period); - - const classes = 'alert_' + this.props.condition.level.toUpperCase(); - - return ( - <li className="overview-gate-condition"> - <div className="little-spacer-bottom">{period}</div> - - <div style={{ display: 'flex', alignItems: 'center' }}> - <div className="overview-gate-condition-value"> - <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name} - period={this.props.condition.period} periodDate={periodDate}> - <span className={classes}> - <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/> - </span> - </DrilldownLink> - </div> - - <div className="overview-gate-condition-metric"> - <div>{metricName}</div> - <div> - {translate('quality_gates.operator', this.props.condition.op, 'short')} - {' '} - <Measure value={threshold} type={this.props.condition.metric.type}/> - </div> - </div> - </div> - </li> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate.js b/server/sonar-web/src/main/js/apps/overview/gate/gate.js deleted file mode 100644 index 74d2671ee4d..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/gate/gate.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; - -import GateConditions from './gate-conditions'; -import GateEmpty from './gate-empty'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -export default React.createClass({ - renderGateConditions () { - return <GateConditions gate={this.props.gate} component={this.props.component}/>; - }, - - renderGateText () { - let text = ''; - if (this.props.gate.level === 'ERROR') { - text = translateWithParameters('overview.gate.view.errors', this.props.gate.text); - } else if (this.props.gate.level === 'WARN') { - text = translateWithParameters('overview.gate.view.warnings', this.props.gate.text); - } else { - text = translate('overview.gate.view.no_alert'); - } - return <div className="overview-card">{text}</div>; - }, - - render() { - if (!this.props.gate || !this.props.gate.level) { - return this.props.component.qualifier === 'TRK' ? <GateEmpty/> : null; - } - - const level = this.props.gate.level.toLowerCase(); - const badgeClassName = 'badge badge-' + level; - const badgeText = translate('overview.gate', this.props.gate.level); - - return ( - <div className="overview-gate"> - <h2 className="overview-title"> - {translate('overview.quality_gate')} - <span className={badgeClassName}>{badgeText}</span> - </h2> - {this.props.gate.conditions ? this.renderGateConditions() : this.renderGateText()} - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js new file mode 100644 index 00000000000..c78181b7af0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js @@ -0,0 +1,131 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +import enhance from './enhance'; +import LeakPeriodLegend from '../components/LeakPeriodLegend'; +import { getMetricName } from '../helpers/metrics'; +import { translate } from '../../../helpers/l10n'; + +class BugsAndVulnerabilities extends React.Component { + renderHeader () { + const { component } = this.props; + const bugsDomainUrl = window.baseUrl + '/component_measures/domain/Reliability?id=' + + encodeURIComponent(component.key); + const vulnerabilitiesDomainUrl = window.baseUrl + '/component_measures/domain/Security?id=' + + encodeURIComponent(component.key); + + return ( + <div className="overview-card-header"> + <div className="overview-title"> + <a href={bugsDomainUrl}> + {translate('metric.bugs.name')} + </a> + {' & '} + <a href={vulnerabilitiesDomainUrl}> + {translate('metric.vulnerabilities.name')} + </a> + </div> + </div> + ); + } + + renderLeak () { + const { leakPeriod } = this.props; + + if (leakPeriod == null) { + return null; + } + + return ( + <div className="overview-domain-leak"> + <LeakPeriodLegend period={leakPeriod}/> + + <div className="overview-domain-measures"> + <div className="overview-domain-measure"> + <div className="overview-domain-measure-value"> + {this.props.renderIssues('new_bugs', 'BUG')} + </div> + <div className="overview-domain-measure-label"> + {getMetricName('new_bugs')} + </div> + </div> + + <div className="overview-domain-measure"> + <div className="overview-domain-measure-value"> + {this.props.renderIssues('new_vulnerabilities', 'VULNERABILITY')} + </div> + <div className="overview-domain-measure-label"> + {getMetricName('new_vulnerabilities')} + </div> + </div> + </div> + </div> + ); + } + + renderNutshell () { + return ( + <div className="overview-domain-nutshell"> + <div className="overview-domain-measures"> + + <div className="overview-domain-measure"> + <div className="display-inline-block text-middle" style={{ paddingLeft: 56 }}> + <div className="overview-domain-measure-value"> + {this.props.renderIssues('bugs', 'BUG')} + {this.props.renderRating('reliability_rating')} + </div> + <div className="overview-domain-measure-label"> + {getMetricName('bugs')} + </div> + </div> + </div> + + <div className="overview-domain-measure"> + <div className="display-inline-block text-middle"> + <div className="overview-domain-measure-value"> + {this.props.renderIssues('vulnerabilities', 'VULNERABILITY')} + {this.props.renderRating('security_rating')} + </div> + <div className="overview-domain-measure-label"> + {getMetricName('vulnerabilities')} + </div> + </div> + </div> + </div> + </div> + ); + } + + render () { + return ( + <div className="overview-card overview-card-special"> + {this.renderHeader()} + + <div className="overview-domain-panel"> + {this.renderNutshell()} + {this.renderLeak()} + </div> + </div> + ); + } +} + +export default enhance(BugsAndVulnerabilities); diff --git a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js new file mode 100644 index 00000000000..16dcb18ea73 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js @@ -0,0 +1,157 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 moment from 'moment'; +import React from 'react'; + +import enhance from './enhance'; +import { IssuesLink } from '../../../components/shared/issues-link'; +import { getMetricName } from '../helpers/metrics'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; + +class CodeSmells extends React.Component { + renderHeader () { + return this.props.renderHeader( + 'Maintainability', + translate('metric.code_smells.name')); + } + + renderDebt (metric, type) { + const { measures, component } = this.props; + const measure = measures.find(measure => measure.metric.key === metric); + const value = this.props.getValue(measure); + const params = { resolved: 'false', facetMode: 'effort', types: type }; + + if (isDiffMetric(metric)) { + Object.assign(params, { sinceLeakPeriod: 'true' }); + } + + const formattedSnapshotDate = moment(component.snapshotDate).format('LLL'); + const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate); + + return ( + <IssuesLink component={component.key} params={params}> + <span title={tooltip} data-toggle="tooltip"> + {formatMeasure(value, 'SHORT_WORK_DUR')} + </span> + </IssuesLink> + ); + } + + renderTimelineStartDate () { + const momentDate = moment(this.props.historyStartDate); + const fromNow = momentDate.fromNow(); + return ( + <span className="overview-domain-timeline-date"> + {translateWithParameters('overview.started_x', fromNow)} + </span> + ); + } + + renderTimeline (range, displayDate) { + return this.props.renderTimeline( + 'sqale_index', + range, + displayDate ? this.renderTimelineStartDate() : null); + } + + renderLeak () { + const { leakPeriod } = this.props; + + if (leakPeriod == null) { + return null; + } + + return ( + <div className="overview-domain-leak"> + <div className="overview-domain-measures"> + <div className="overview-domain-measure"> + <div className="overview-domain-measure-value"> + {this.props.renderIssues('new_code_smells', 'CODE_SMELL')} + </div> + <div className="overview-domain-measure-label"> + {getMetricName('new_code_smells')} + </div> + </div> + + <div className="overview-domain-measure"> + <div className="overview-domain-measure-value"> + {this.renderDebt('new_effort', 'CODE_SMELL')} + </div> + <div className="overview-domain-measure-label"> + {getMetricName('new_effort')} + </div> + </div> + </div> + + {this.renderTimeline('after')} + </div> + ); + } + + renderNutshell () { + return ( + <div className="overview-domain-nutshell"> + <div className="overview-domain-measures"> + + <div className="overview-domain-measure"> + <div className="display-inline-block text-middle" style={{ paddingLeft: 56 }}> + <div className="overview-domain-measure-value"> + {this.props.renderIssues('code_smells', 'CODE_SMELL')} + {this.props.renderRating('sqale_rating')} + </div> + <div className="overview-domain-measure-label"> + {getMetricName('code_smells')} + </div> + </div> + </div> + + <div className="overview-domain-measure"> + <div className="display-inline-block text-middle"> + <div className="overview-domain-measure-value"> + {this.renderDebt('sqale_index', 'CODE_SMELL')} + </div> + <div className="overview-domain-measure-label"> + {getMetricName('effort')} + </div> + </div> + </div> + </div> + + {this.renderTimeline('before', true)} + </div> + ); + } + + render () { + return ( + <div className="overview-card"> + {this.renderHeader()} + + <div className="overview-domain-panel"> + {this.renderNutshell()} + {this.renderLeak()} + </div> + </div> + ); + } +} + +export default enhance(CodeSmells); diff --git a/server/sonar-web/src/main/js/apps/overview/main/Coverage.js b/server/sonar-web/src/main/js/apps/overview/main/Coverage.js new file mode 100644 index 00000000000..d86fb2b88ff --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main/Coverage.js @@ -0,0 +1,193 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +import enhance from './enhance'; +import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import { getMetricName } from '../helpers/metrics'; +import { formatMeasure, getPeriodValue } from '../../../helpers/measures'; +import { translate } from '../../../helpers/l10n'; + +class Coverage extends React.Component { + getCoverageMetricPrefix () { + const { measures } = this.props; + const hasOverallCoverage = !!measures + .find(measure => measure.metric.key === 'overall_coverage'); + const hasUTCoverage = !!measures + .find(measure => measure.metric.key === 'coverage'); + const hasITCoverage = !!measures + .find(measure => measure.metric.key === 'it_coverage'); + + if (hasOverallCoverage && hasUTCoverage && hasITCoverage) { + return 'overall_'; + } else if (hasITCoverage) { + return 'it_'; + } else { + return ''; + } + } + + getCoverage (prefix) { + const { measures } = this.props; + const { value } = measures + .find(measure => measure.metric.key === `${prefix}coverage`); + return Number(value); + } + + getNewCoverageMeasure (prefix) { + const { measures } = this.props; + return measures + .find(measure => measure.metric.key === `new_${prefix}coverage`); + } + + renderHeader () { + return this.props.renderHeader( + 'Coverage', + translate('metric.coverage.name')); + } + + renderTimeline (coverageMetricPrefix, range) { + const metricKey = `${coverageMetricPrefix}coverage`; + return this.props.renderTimeline(metricKey, range); + } + + renderTests () { + return this.props.renderMeasure('tests'); + } + + renderCoverageDonut (coverage) { + const data = [ + { value: coverage, fill: '#85bb43' }, + { value: 100 - coverage, fill: '#d4333f' } + ]; + return this.props.renderDonut(data); + } + + renderCoverage (coverageMetricPrefix) { + const { component } = this.props; + const metric = `${coverageMetricPrefix}coverage`; + const coverage = this.getCoverage(coverageMetricPrefix); + + return ( + <div className="overview-domain-measure"> + {this.renderCoverageDonut(coverage)} + + <div className="display-inline-block text-middle"> + <div className="overview-domain-measure-value"> + <DrilldownLink component={component.key} metric={metric}> + <span className="js-overview-main-coverage"> + {formatMeasure(coverage, 'PERCENT')} + </span> + </DrilldownLink> + </div> + + <div className="overview-domain-measure-label"> + {getMetricName('coverage')} + </div> + </div> + </div> + ); + } + + renderNewCoverage (coverageMetricPrefix) { + const { component, leakPeriod } = this.props; + const newCoverageMeasure = this.getNewCoverageMeasure(coverageMetricPrefix); + + const value = newCoverageMeasure ? ( + <DrilldownLink + component={component.key} + metric={newCoverageMeasure.metric.key} + period={leakPeriod.index}> + <span className="js-overview-main-new-coverage"> + {formatMeasure(getPeriodValue(newCoverageMeasure, leakPeriod.index), 'PERCENT')} + </span> + </DrilldownLink> + ) : ( + <span>—</span> + ); + + return ( + <div className="overview-domain-measure"> + <div className="overview-domain-measure-value"> + {value} + </div> + + <div className="overview-domain-measure-label"> + {getMetricName('new_coverage')} + </div> + </div> + ); + } + + renderNutshell (coverageMetricPrefix) { + return ( + <div className="overview-domain-nutshell"> + <div className="overview-domain-measures"> + {this.renderCoverage(coverageMetricPrefix)} + {this.renderTests()} + </div> + + {this.renderTimeline(coverageMetricPrefix, 'before')} + </div> + ); + } + + renderLeak (coverageMetricPrefix) { + const { leakPeriod } = this.props; + + if (leakPeriod == null) { + return null; + } + + return ( + <div className="overview-domain-leak"> + <div className="overview-domain-measures"> + {this.renderNewCoverage(coverageMetricPrefix)} + </div> + + {this.renderTimeline(coverageMetricPrefix, 'after')} + </div> + ); + } + + render () { + const { measures } = this.props; + const coverageMetricPrefix = this.getCoverageMetricPrefix(); + const coverageMeasure = + measures.find(measure => measure.metric.key === `${coverageMetricPrefix}coverage`); + + if (coverageMeasure == null) { + return null; + } + + return ( + <div className="overview-card"> + {this.renderHeader()} + + <div className="overview-domain-panel"> + {this.renderNutshell(coverageMetricPrefix)} + {this.renderLeak(coverageMetricPrefix)} + </div> + </div> + ); + } +} + +export default enhance(Coverage); diff --git a/server/sonar-web/src/main/js/apps/overview/main/Duplications.js b/server/sonar-web/src/main/js/apps/overview/main/Duplications.js new file mode 100644 index 00000000000..06d9c113942 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main/Duplications.js @@ -0,0 +1,132 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +import enhance from './enhance'; +import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import { getMetricName } from '../helpers/metrics'; +import { formatMeasure } from '../../../helpers/measures'; +import { translate } from '../../../helpers/l10n'; + +class Duplications extends React.Component { + renderHeader () { + return this.props.renderHeader( + 'Duplications', + translate('overview.domain.duplications')); + } + + renderTimeline (range) { + return this.props.renderTimeline('duplicated_lines_density', range); + } + + renderDuplicatedBlocks () { + return this.props.renderMeasure('duplicated_blocks'); + } + + renderDuplicationsDonut (duplications) { + const data = [ + { value: duplications, fill: '#f3ca8e' }, + { value: Math.max(0, 20 - duplications), fill: '#e6e6e6' } + ]; + return this.props.renderDonut(data); + } + + renderDuplications () { + const { component, measures } = this.props; + const measure = measures.find(measure => measure.metric.key === 'duplicated_lines_density'); + const duplications = Number(measure.value); + + return ( + <div className="overview-domain-measure"> + {this.renderDuplicationsDonut(duplications)} + + <div className="display-inline-block text-middle"> + <div className="overview-domain-measure-value"> + <DrilldownLink component={component.key} metric="duplicated_lines_density"> + {formatMeasure(duplications, 'PERCENT')} + </DrilldownLink> + </div> + + <div className="overview-domain-measure-label"> + {getMetricName('duplications')} + </div> + </div> + </div> + ); + } + + renderNutshell () { + return ( + <div className="overview-domain-nutshell"> + <div className="overview-domain-measures"> + {this.renderDuplications()} + {this.renderDuplicatedBlocks()} + </div> + + {this.renderTimeline('before')} + </div> + ); + } + + renderLeak () { + const { leakPeriod } = this.props; + + if (leakPeriod == null) { + return null; + } + + const measure = this.props.renderMeasureVariation( + 'duplicated_lines_density', + getMetricName('duplications')); + + return ( + <div className="overview-domain-leak"> + <div className="overview-domain-measures"> + {measure} + </div> + + {this.renderTimeline('after')} + </div> + ); + } + + render () { + const { measures } = this.props; + const duplications = + measures.find(measure => measure.metric.key === 'duplicated_lines_density'); + + if (duplications == null) { + return null; + } + + return ( + <div className="overview-card"> + {this.renderHeader()} + + <div className="overview-domain-panel"> + {this.renderNutshell()} + {this.renderLeak()} + </div> + </div> + ); + } +} + +export default enhance(Duplications); diff --git a/server/sonar-web/src/main/js/apps/overview/main/Size.js b/server/sonar-web/src/main/js/apps/overview/main/Size.js new file mode 100644 index 00000000000..e8f5f1e433c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main/Size.js @@ -0,0 +1,108 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +import enhance from './enhance'; +import LanguageDistribution from '../../../components/charts/LanguageDistribution'; +import { translate } from '../../../helpers/l10n'; + +class Size extends React.Component { + renderHeader () { + return this.props.renderHeader( + 'Size', + translate('overview.domain.structure')); + } + + renderTimeline (range) { + return this.props.renderTimeline('ncloc', range); + } + + renderLeak () { + const { leakPeriod } = this.props; + + if (leakPeriod == null) { + return null; + } + + return ( + <div className="overview-domain-leak"> + <div className="overview-domain-measures"> + {this.props.renderMeasureVariation('ncloc')} + </div> + + {this.renderTimeline('after')} + </div> + ); + } + + renderLanguageDistribution () { + const { measures } = this.props; + const distribution = + measures.find(measure => measure.metric.key === 'ncloc_language_distribution'); + + if (!distribution) { + return null; + } + + return ( + <div className="overview-domain-measure"> + <div style={{ width: 200 }}> + <LanguageDistribution distribution={distribution.value}/> + </div> + </div> + ); + } + + renderNutshell () { + return ( + <div className="overview-domain-nutshell"> + <div className="overview-domain-measures"> + {this.renderLanguageDistribution()} + {this.props.renderMeasure('ncloc')} + </div> + + {this.renderTimeline('before')} + </div> + ); + } + + render () { + const { measures } = this.props; + const linesOfCode = + measures.find(measure => measure.metric.key === 'ncloc'); + + if (!linesOfCode) { + return null; + } + + return ( + <div className="overview-card"> + {this.renderHeader()} + + <div className="overview-domain-panel"> + {this.renderNutshell()} + {this.renderLeak()} + </div> + </div> + ); + } +} + +export default enhance(Size); diff --git a/server/sonar-web/src/main/js/apps/overview/main/code-smells.js b/server/sonar-web/src/main/js/apps/overview/main/code-smells.js deleted file mode 100644 index cc7cba65253..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main/code-smells.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 moment from 'moment'; -import React from 'react'; - -import { - Domain, - DomainPanel, - DomainNutshell, - DomainLeak, - MeasuresList, - Measure, - DomainMixin -} from './components'; -import { Rating } from './../../../components/shared/rating'; -import { IssuesLink } from '../../../components/shared/issues-link'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; -import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; -import { getMetricName } from '../helpers/metrics'; -import { formatMeasure } from '../../../helpers/measures'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -export const CodeSmells = React.createClass({ - propTypes: { - leakPeriodLabel: React.PropTypes.string, - leakPeriodDate: React.PropTypes.object - }, - - mixins: [TooltipsMixin, DomainMixin], - - renderLeak () { - if (!this.hasLeakPeriod()) { - return null; - } - - const { snapshotDate } = this.props.component; - const formattedSnapshotDate = moment(snapshotDate).format('LLL'); - const newDebt = this.props.leak['new_technical_debt'] || 0; - const newCodeSmells = this.props.leak['new_code_smells'] || 0; - - return <DomainLeak> - <MeasuresList> - <Measure label={getMetricName('new_code_smells')}> - <IssuesLink - component={this.props.component.key} - params={{ resolved: 'false', types: 'CODE_SMELL', sinceLeakPeriod: 'true' }}> - <span - title={translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate)} - data-toggle="tooltip"> - {formatMeasure(newCodeSmells, 'SHORT_INT')} - </span> - </IssuesLink> - </Measure> - <Measure label={getMetricName('new_effort')}> - <IssuesLink - component={this.props.component.key} - params={{ resolved: 'false', types: 'CODE_SMELL', facetMode: 'effort', sinceLeakPeriod: 'true' }}> - <span - title={translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate)} - data-toggle="tooltip"> - {formatMeasure(newDebt, 'SHORT_WORK_DUR')} - </span> - </IssuesLink> - </Measure> - </MeasuresList> - {this.renderTimeline('after')} - </DomainLeak>; - }, - - render () { - const debt = this.props.measures['sqale_index'] || 0; - const codeSmells = this.props.measures['code_smells'] || 0; - const { snapshotDate } = this.props.component; - const formattedSnapshotDate = moment(snapshotDate).format('LLL'); - - const domainUrl = window.baseUrl + '/component_measures/domain/Maintainability?id=' + - encodeURIComponent(this.props.component.key); - - return <Domain> - <div className="overview-card-header"> - <div className="overview-title"> - <a href={domainUrl}> - {translate('metric.code_smells.name')} - </a> - </div> - </div> - - <DomainPanel> - <DomainNutshell> - <MeasuresList> - - <Measure composite={true}> - <div className="display-inline-block text-middle" style={{ paddingLeft: 56 }}> - <div className="overview-domain-measure-value"> - <IssuesLink - component={this.props.component.key} - params={{ resolved: 'false', types: 'CODE_SMELL' }}> - <span - title={translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate)} - data-toggle="tooltip"> - {formatMeasure(codeSmells, 'SHORT_INT')} - </span> - </IssuesLink> - <div className="overview-domain-measure-sup"> - <DrilldownLink component={this.props.component.key} metric="sqale_rating"> - <Rating value={this.props.measures['sqale_rating']}/> - </DrilldownLink> - </div> - </div> - <div className="overview-domain-measure-label">{getMetricName('code_smells')}</div> - </div> - </Measure> - - <Measure label={getMetricName('effort')}> - <IssuesLink - component={this.props.component.key} - params={{ resolved: 'false', types: 'CODE_SMELL', facetMode: 'effort' }}> - <span - title={translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate)} - data-toggle="tooltip"> - {formatMeasure(debt, 'SHORT_WORK_DUR')} - </span> - </IssuesLink> - </Measure> - </MeasuresList> - {this.renderTimeline('before', true)} - </DomainNutshell> - {this.renderLeak()} - </DomainPanel> - </Domain>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/main/components.js b/server/sonar-web/src/main/js/apps/overview/main/components.js deleted file mode 100644 index 8fe41b3014a..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main/components.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 moment from 'moment'; -import React from 'react'; - -import { Timeline } from './timeline'; -import { translateWithParameters } from '../../../helpers/l10n'; - -export const Domain = React.createClass({ - render () { - return <div className="overview-card">{this.props.children}</div>; - } -}); - -export const DomainTitle = React.createClass({ - render () { - return <div className="overview-title">{this.props.children}</div>; - } -}); - -export const DomainLeakTitle = React.createClass({ - renderInline (tooltip, fromNow) { - return <span className="overview-domain-leak-title" title={tooltip} data-toggle="tooltip"> - <span>{translateWithParameters('overview.leak_period_x', this.props.label)}</span> - <span className="note spacer-left">{translateWithParameters('overview.started_x', fromNow)}</span> - </span>; - }, - - render() { - if (!this.props.label || !this.props.date) { - return null; - } - const momentDate = moment(this.props.date); - const fromNow = momentDate.fromNow(); - const tooltip = 'Started on ' + momentDate.format('LL'); - if (this.props.inline) { - return this.renderInline(tooltip, fromNow); - } - return <span className="overview-domain-leak-title" title={tooltip} data-toggle="tooltip"> - <span>{translateWithParameters('overview.leak_period_x', this.props.label)}</span> - <br/> - <span className="note">{translateWithParameters('overview.started_x', fromNow)}</span> - </span>; - } -}); - -export const DomainHeader = React.createClass({ - render () { - return <div className="overview-card-header"> - <DomainTitle {...this.props}>{this.props.title}</DomainTitle> - </div>; - } -}); - -export const DomainPanel = React.createClass({ - propTypes: { - domain: React.PropTypes.string - }, - - render () { - return <div className="overview-domain-panel"> - {this.props.children} - </div>; - } -}); - -export const DomainNutshell = React.createClass({ - render () { - return <div className="overview-domain-nutshell">{this.props.children}</div>; - } -}); - -export const DomainLeak = React.createClass({ - render () { - return <div className="overview-domain-leak">{this.props.children}</div>; - } -}); - -export const MeasuresList = React.createClass({ - render () { - return <div className="overview-domain-measures">{this.props.children}</div>; - } -}); - -export const Measure = React.createClass({ - propTypes: { - label: React.PropTypes.string, - composite: React.PropTypes.bool - }, - - getDefaultProps() { - return { composite: false }; - }, - - renderValue () { - if (this.props.composite) { - return this.props.children; - } else { - return <div className="overview-domain-measure-value"> - {this.props.children} - </div>; - } - }, - - renderLabel() { - return this.props.label ? - <div className="overview-domain-measure-label">{this.props.label}</div> : null; - }, - - render () { - return <div className="overview-domain-measure"> - {this.renderValue()} - {this.renderLabel()} - </div>; - } -}); - -export const DomainMixin = { - renderTimelineStartDate() { - const momentDate = moment(this.props.historyStartDate); - const fromNow = momentDate.fromNow(); - return ( - <span className="overview-domain-timeline-date"> - {translateWithParameters('overview.started_x', fromNow)} - </span> - ); - }, - - renderTimeline(range, displayDate) { - if (!this.props.history) { - return null; - } - const props = { history: this.props.history }; - props[range] = this.props.leakPeriodDate; - return <div className="overview-domain-timeline"> - <Timeline {...props}/> - {displayDate ? this.renderTimelineStartDate(range) : null} - </div>; - }, - - hasLeakPeriod () { - return this.props.leakPeriodDate != null; - } -}; diff --git a/server/sonar-web/src/main/js/apps/overview/main/coverage.js b/server/sonar-web/src/main/js/apps/overview/main/coverage.js deleted file mode 100644 index 81639060a0d..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main/coverage.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; - -import { - Domain, - DomainPanel, - DomainNutshell, - DomainLeak, - MeasuresList, - Measure, - DomainMixin -} from './components'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; -import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; -import { DonutChart } from '../../../components/charts/donut-chart'; -import { getMetricName } from '../helpers/metrics'; -import { formatMeasure } from '../../../helpers/measures'; -import { translate } from '../../../helpers/l10n'; - -export const GeneralCoverage = React.createClass({ - propTypes: { - measures: React.PropTypes.object.isRequired, - leakPeriodLabel: React.PropTypes.string, - leakPeriodDate: React.PropTypes.object, - coverageMetricPrefix: React.PropTypes.string.isRequired - }, - - mixins: [TooltipsMixin, DomainMixin], - - getCoverageMetric () { - return this.props.coverageMetricPrefix + 'coverage'; - }, - - getNewCoverageMetric () { - return 'new_' + this.props.coverageMetricPrefix + 'coverage'; - }, - - renderNewCoverage () { - const newCoverageMetric = this.getNewCoverageMetric(); - - if (this.props.leak[newCoverageMetric] != null) { - return <DrilldownLink component={this.props.component.key} metric={newCoverageMetric} - period={this.props.leakPeriodIndex}> - <span className="js-overview-main-new-coverage"> - {formatMeasure(this.props.leak[newCoverageMetric], 'PERCENT')} - </span> - </DrilldownLink>; - } else { - return <span>—</span>; - } - }, - - renderLeak () { - if (!this.hasLeakPeriod()) { - return null; - } - - return <DomainLeak> - <MeasuresList> - <Measure label={getMetricName('new_coverage')}>{this.renderNewCoverage()}</Measure> - </MeasuresList> - {this.renderTimeline('after')} - </DomainLeak>; - }, - - renderTests() { - const tests = this.props.measures['tests']; - if (tests == null) { - return null; - } - return <Measure label={getMetricName('tests')}> - <DrilldownLink component={this.props.component.key} metric="tests"> - <span className="js-overview-main-tests">{formatMeasure(tests, 'SHORT_INT')}</span> - </DrilldownLink> - </Measure>; - }, - - render () { - const coverageMetric = this.getCoverageMetric(); - if (this.props.measures[coverageMetric] == null) { - return null; - } - - const donutData = [ - { value: this.props.measures[coverageMetric], fill: '#85bb43' }, - { value: 100 - this.props.measures[coverageMetric], fill: '#d4333f' } - ]; - - const domainUrl = window.baseUrl + '/component_measures/domain/Coverage?id=' + - encodeURIComponent(this.props.component.key); - - return <Domain> - <div className="overview-card-header"> - <div className="overview-title"> - <a href={domainUrl}> - {translate('metric.coverage.name')} - </a> - </div> - </div> - - <DomainPanel> - <DomainNutshell> - <MeasuresList> - - <Measure composite={true}> - <div className="display-inline-block text-middle big-spacer-right"> - <DonutChart width="40" - height="40" - thickness="4" - data={donutData}/> - </div> - <div className="display-inline-block text-middle"> - <div className="overview-domain-measure-value"> - <DrilldownLink component={this.props.component.key} metric={coverageMetric}> - <span className="js-overview-main-coverage"> - {formatMeasure(this.props.measures[coverageMetric], 'PERCENT')} - </span> - </DrilldownLink> - </div> - <div className="overview-domain-measure-label">{getMetricName('coverage')}</div> - </div> - </Measure> - - {this.renderTests()} - </MeasuresList> - {this.renderTimeline('before')} - </DomainNutshell> - {this.renderLeak()} - </DomainPanel> - </Domain>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/main/duplications.js b/server/sonar-web/src/main/js/apps/overview/main/duplications.js deleted file mode 100644 index 96c94d50e61..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main/duplications.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; - -import { Domain, - DomainPanel, - DomainNutshell, - DomainLeak, - MeasuresList, - Measure, - DomainMixin } from './components'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; -import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; -import { DonutChart } from '../../../components/charts/donut-chart'; -import { getMetricName } from '../helpers/metrics'; -import { formatMeasure, formatMeasureVariation } from '../../../helpers/measures'; -import { translate } from '../../../helpers/l10n'; - -export const GeneralDuplications = React.createClass({ - propTypes: { - leakPeriodLabel: React.PropTypes.string, - leakPeriodDate: React.PropTypes.object - }, - - mixins: [TooltipsMixin, DomainMixin], - - renderLeak () { - if (!this.hasLeakPeriod()) { - return null; - } - const measure = this.props.leak['duplicated_lines_density']; - const formatted = measure != null ? formatMeasureVariation(measure, 'PERCENT') : '—'; - return <DomainLeak> - <MeasuresList> - <Measure label={getMetricName('duplications')}> - {formatted} - </Measure> - </MeasuresList> - {this.renderTimeline('after')} - </DomainLeak>; - }, - - renderDuplicatedBlocks () { - if (this.props.measures['duplicated_blocks'] == null) { - return null; - } - return <Measure label={getMetricName('duplicated_blocks')}> - <DrilldownLink component={this.props.component.key} metric="duplicated_blocks"> - {formatMeasure(this.props.measures['duplicated_blocks'], 'SHORT_INT')} - </DrilldownLink> - </Measure>; - }, - - render () { - const donutData = [ - { value: this.props.measures['duplicated_lines_density'], fill: '#f3ca8e' }, - { value: Math.max(0, 20 - this.props.measures['duplicated_lines_density']), fill: '#e6e6e6' } - ]; - - const domainUrl = window.baseUrl + '/component_measures/domain/Duplications?id=' + - encodeURIComponent(this.props.component.key); - - return <Domain> - <div className="overview-card-header"> - <div className="overview-title"> - <a href={domainUrl}> - {translate('overview.domain.duplications')} - </a> - </div> - </div> - - <DomainPanel> - <DomainNutshell> - <MeasuresList> - - <Measure composite={true}> - <div className="display-inline-block text-middle big-spacer-right"> - <DonutChart width="40" - height="40" - thickness="4" - data={donutData}/> - </div> - <div className="display-inline-block text-middle"> - <div className="overview-domain-measure-value"> - <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density"> - {formatMeasure(this.props.measures['duplicated_lines_density'], 'PERCENT')} - </DrilldownLink> - </div> - <div className="overview-domain-measure-label">{getMetricName('duplications')}</div> - </div> - </Measure> - - {this.renderDuplicatedBlocks()} - </MeasuresList> - {this.renderTimeline('before')} - </DomainNutshell> - {this.renderLeak()} - </DomainPanel> - </Domain>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js new file mode 100644 index 00000000000..280a9dc799b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js @@ -0,0 +1,213 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import moment from 'moment'; +import shallowCompare from 'react-addons-shallow-compare'; + +import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import { IssuesLink } from '../../../components/shared/issues-link'; +import { DonutChart } from '../../../components/charts/donut-chart'; +import { Rating } from './../../../components/shared/rating'; +import Timeline from '../components/Timeline'; +import { + formatMeasure, + formatMeasureVariation, + isDiffMetric, + getPeriodValue, + getShortType +} from '../../../helpers/measures'; +import { translateWithParameters } from '../../../helpers/l10n'; +import { getPeriodDate } from '../../../helpers/periods'; + +export default function enhance (ComposedComponent) { + return class extends React.Component { + static displayName = `enhance(${ComposedComponent.displayName})}`; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + getValue (measure) { + const { leakPeriod } = this.props; + + if (!measure) { + return 0; + } + + return isDiffMetric(measure.metric.key) ? + getPeriodValue(measure, leakPeriod.index) : + measure.value; + } + + renderHeader (label, domain) { + const { component } = this.props; + const domainUrl = + window.baseUrl + + `/component_measures/domain/${domain}` + + `id=${encodeURIComponent(component.key)}`; + + return ( + <div className="overview-card-header"> + <div className="overview-title"> + <a href={domainUrl}>{label}</a> + </div> + </div> + ); + } + + renderMeasure (metricKey) { + const { measures, component } = this.props; + const measure = measures.find(measure => measure.metric.key === metricKey); + + if (measure == null) { + return null; + } + + return ( + <div className="overview-domain-measure"> + <div className="overview-domain-measure-value"> + <DrilldownLink component={component.key} metric={metricKey}> + <span className="js-overview-main-tests"> + {formatMeasure(measure.value, getShortType(measure.metric.type))} + </span> + </DrilldownLink> + </div> + + <div className="overview-domain-measure-label"> + {measure.metric.name} + </div> + </div> + ); + } + + renderMeasureVariation (metricKey, customLabel) { + const NO_VALUE = '—'; + const { measures, leakPeriod } = this.props; + + const measure = measures.find(measure => measure.metric.key === metricKey); + const periodValue = getPeriodValue(measure, leakPeriod.index); + const formatted = periodValue != null ? + formatMeasureVariation(periodValue, getShortType(measure.metric.type)) : + NO_VALUE; + + return ( + <div className="overview-domain-measure"> + <div className="overview-domain-measure-value"> + {formatted} + </div> + + <div className="overview-domain-measure-label"> + {customLabel || measure.metric.name} + </div> + </div> + ); + } + + renderDonut (data) { + return ( + <div className="display-inline-block text-middle big-spacer-right"> + <DonutChart + data={data} + width={40} + height={40} + thickness={4}/> + </div> + ); + } + + renderRating (metricKey) { + const { component, measures } = this.props; + const measure = measures.find(measure => measure.metric.key === metricKey); + + if (!measure) { + return null; + } + + return ( + <div className="overview-domain-measure-sup"> + <DrilldownLink component={component.key} metric={metricKey}> + <Rating value={measure.value}/> + </DrilldownLink> + </div> + ); + } + + renderIssues (metric, type) { + const { measures, component } = this.props; + const measure = measures.find(measure => measure.metric.key === metric); + const value = this.getValue(measure); + const params = { resolved: 'false', types: type }; + + if (isDiffMetric(metric)) { + Object.assign(params, { sinceLeakPeriod: 'true' }); + } + + const formattedSnapshotDate = moment(component.snapshotDate).format('LLL'); + const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate); + + return ( + <IssuesLink component={component.key} params={params}> + <span title={tooltip} data-toggle="tooltip"> + {formatMeasure(value, 'SHORT_INT')} + </span> + </IssuesLink> + ); + } + + renderTimeline (metricKey, range, children) { + if (!this.props.history) { + return null; + } + + const history = this.props.history[metricKey]; + + if (!history) { + return null; + } + + const props = { + history, + [range]: getPeriodDate(this.props.leakPeriod) + }; + + return ( + <div className="overview-domain-timeline"> + <Timeline {...props}/> + {children} + </div> + ); + } + + render () { + return ( + <ComposedComponent + {...this.props} + getValue={this.getValue.bind(this)} + renderHeader={this.renderHeader.bind(this)} + renderMeasure={this.renderMeasure.bind(this)} + renderMeasureVariation={this.renderMeasureVariation.bind(this)} + renderDonut={this.renderDonut.bind(this)} + renderRating={this.renderRating.bind(this)} + renderIssues={this.renderIssues.bind(this)} + renderTimeline={this.renderTimeline.bind(this)}/> + ); + } + }; +} diff --git a/server/sonar-web/src/main/js/apps/overview/main/main.js b/server/sonar-web/src/main/js/apps/overview/main/main.js deleted file mode 100644 index 540ce0b3e2a..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main/main.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 _ from 'underscore'; -import moment from 'moment'; -import React from 'react'; - -import { Risk } from './risk'; -import { CodeSmells } from './code-smells'; -import { GeneralCoverage } from './coverage'; -import { GeneralDuplications } from './duplications'; -import { GeneralStructure } from './structure'; -import { CoverageSelectionMixin } from '../components/coverage-selection-mixin'; -import { getPeriodLabel, getPeriodDate } from './../helpers/periods'; -import { getMeasures } from '../../../api/measures'; -import { getTimeMachineData } from '../../../api/time-machine'; - -const METRICS_LIST = [ - 'overall_coverage', - 'new_overall_coverage', - 'coverage', - 'new_coverage', - 'it_coverage', - 'new_it_coverage', - 'tests', - 'duplicated_lines_density', - 'duplicated_blocks', - 'ncloc', - 'ncloc_language_distribution', - - 'sqale_index', - 'new_technical_debt', - 'code_smells', - 'new_code_smells', - 'sqale_rating', - 'reliability_rating', - 'bugs', - 'new_bugs', - 'security_rating', - 'vulnerabilities', - 'new_vulnerabilities' -]; - -const HISTORY_METRICS_LIST = [ - 'sqale_index', - 'duplicated_lines_density', - 'ncloc' -]; - -export default React.createClass({ - propTypes: { - leakPeriodIndex: React.PropTypes.string.isRequired - }, - - mixins: [CoverageSelectionMixin], - - getInitialState() { - return { - ready: false, - history: {}, - leakPeriodLabel: getPeriodLabel(this.props.component.periods, this.props.leakPeriodIndex), - leakPeriodDate: getPeriodDate(this.props.component.periods, this.props.leakPeriodIndex) - }; - }, - - componentDidMount() { - this.requestMeasures().then(r => { - const measures = this.getMeasuresValues(r); - - let leak; - if (this.state.leakPeriodDate) { - leak = this.getMeasuresValues(r, Number(this.props.leakPeriodIndex)); - } - - this.setState({ - ready: true, - measures, - leak, - coverageMetricPrefix: this.getCoverageMetricPrefix(measures) - }, this.requestHistory); - }); - }, - - requestMeasures () { - return getMeasures(this.props.component.key, METRICS_LIST); - }, - - getMeasuresValues (measures, period) { - const values = {}; - measures.forEach(measure => { - const container = period ? _.findWhere(measure.periods, { index: period }) : measure; - if (container) { - values[measure.metric] = container.value; - } - }); - return values; - }, - - requestHistory () { - const coverageMetric = this.state.coverageMetricPrefix + 'coverage'; - const metrics = [].concat(HISTORY_METRICS_LIST, coverageMetric).join(','); - return getTimeMachineData(this.props.component.key, metrics).then(r => { - const history = {}; - r[0].cols.forEach((col, index) => { - history[col.metric] = r[0].cells.map(cell => { - const date = moment(cell.d).toDate(); - const value = cell.v[index] || 0; - return { date, value }; - }); - }); - const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date; - this.setState({ history, historyStartDate }); - }); - }, - - renderLoading () { - return <div className="text-center"> - <i className="spinner spinner-margin"/> - </div>; - }, - - render() { - if (!this.state.ready) { - return this.renderLoading(); - } - - const coverageMetric = this.state.coverageMetricPrefix + 'coverage'; - const props = _.extend({}, this.props, this.state); - - return <div className="overview-domains-list"> - <Risk {...props}/> - <CodeSmells {...props} history={this.state.history['sqale_index']}/> - <GeneralCoverage {...props} coverageMetricPrefix={this.state.coverageMetricPrefix} - history={this.state.history[coverageMetric]}/> - <GeneralDuplications {...props} history={this.state.history['duplicated_lines_density']}/> - <GeneralStructure {...props} history={this.state.history['ncloc']}/> - </div>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/main/risk.js b/server/sonar-web/src/main/js/apps/overview/main/risk.js deleted file mode 100644 index d6942d02056..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main/risk.js +++ /dev/null @@ -1,165 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 moment from 'moment'; -import React from 'react'; - -import { - DomainPanel, - DomainNutshell, - DomainLeak, - MeasuresList, - Measure, - DomainMixin -} from './components'; -import { Rating } from './../../../components/shared/rating'; -import { IssuesLink } from '../../../components/shared/issues-link'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; -import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; -import { Legend } from '../components/legend'; -import { getMetricName } from '../helpers/metrics'; -import { formatMeasure } from '../../../helpers/measures'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -export const Risk = React.createClass({ - propTypes: { - leakPeriodLabel: React.PropTypes.string, - leakPeriodDate: React.PropTypes.object - }, - - mixins: [TooltipsMixin, DomainMixin], - - renderLeak () { - if (!this.hasLeakPeriod()) { - return null; - } - - const { snapshotDate } = this.props.component; - const formattedSnapshotDate = moment(snapshotDate).format('LLL'); - const newBugs = this.props.leak['new_bugs'] || 0; - const newVulnerabilities = this.props.leak['new_vulnerabilities'] || 0; - - return <DomainLeak> - <Legend leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/> - - <MeasuresList> - <Measure label={getMetricName('new_bugs')}> - <IssuesLink - component={this.props.component.key} - params={{ resolved: 'false', types: 'BUG', sinceLeakPeriod: 'true' }}> - <span - title={translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate)} - data-toggle="tooltip"> - {formatMeasure(newBugs, 'SHORT_INT')} - </span> - </IssuesLink> - </Measure> - <Measure label={getMetricName('new_vulnerabilities')}> - <IssuesLink - component={this.props.component.key} - params={{ resolved: 'false', types: 'VULNERABILITY', sinceLeakPeriod: 'true' }}> - <span - title={translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate)} - data-toggle="tooltip"> - {formatMeasure(newVulnerabilities, 'SHORT_INT')} - </span> - </IssuesLink> - </Measure> - </MeasuresList> - </DomainLeak>; - }, - - render () { - const { snapshotDate } = this.props.component; - const formattedSnapshotDate = moment(snapshotDate).format('LLL'); - const bugs = this.props.measures['bugs'] || 0; - const vulnerabilities = this.props.measures['vulnerabilities'] || 0; - - const bugsDomainUrl = window.baseUrl + '/component_measures/domain/Reliability?id=' + - encodeURIComponent(this.props.component.key); - const vulnerabilitiesDomainUrl = window.baseUrl + '/component_measures/domain/Security?id=' + - encodeURIComponent(this.props.component.key); - - return <div className="overview-card overview-card-special"> - <div className="overview-card-header"> - <div className="overview-title"> - <a href={bugsDomainUrl}> - {translate('metric.bugs.name')} - </a> - {' & '} - <a href={vulnerabilitiesDomainUrl}> - {translate('metric.vulnerabilities.name')} - </a> - </div> - </div> - - <DomainPanel> - <DomainNutshell> - <MeasuresList> - - <Measure composite={true}> - <div className="display-inline-block text-middle" style={{ paddingLeft: 56 }}> - <div className="overview-domain-measure-value"> - <IssuesLink - component={this.props.component.key} - params={{ resolved: 'false', types: 'BUG' }}> - <span - title={translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate)} - data-toggle="tooltip"> - {formatMeasure(bugs, 'SHORT_INT')} - </span> - </IssuesLink> - <div className="overview-domain-measure-sup"> - <DrilldownLink component={this.props.component.key} metric="reliability_rating"> - <Rating value={this.props.measures['reliability_rating']}/> - </DrilldownLink> - </div> - </div> - <div className="overview-domain-measure-label">{getMetricName('bugs')}</div> - </div> - </Measure> - - <Measure composite={true}> - <div className="display-inline-block text-middle"> - <div className="overview-domain-measure-value"> - <IssuesLink - component={this.props.component.key} - params={{ resolved: 'false', types: 'VULNERABILITY' }}> - <span - title={translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate)} - data-toggle="tooltip"> - {formatMeasure(vulnerabilities, 'SHORT_INT')} - </span> - </IssuesLink> - <div className="overview-domain-measure-sup"> - <DrilldownLink component={this.props.component.key} metric="security_rating"> - <Rating value={this.props.measures['security_rating']}/> - </DrilldownLink> - </div> - </div> - <div className="overview-domain-measure-label">{getMetricName('vulnerabilities')}</div> - </div> - </Measure> - </MeasuresList> - </DomainNutshell> - {this.renderLeak()} - </DomainPanel> - </div>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/main/structure.js b/server/sonar-web/src/main/js/apps/overview/main/structure.js deleted file mode 100644 index fe0e43bd5e1..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main/structure.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; - -import { Domain, - DomainPanel, - DomainNutshell, - DomainLeak, - MeasuresList, - Measure, - DomainMixin } from './components'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; -import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; -import { getMetricName } from '../helpers/metrics'; -import { formatMeasure, formatMeasureVariation } from '../../../helpers/measures'; -import { LanguageDistribution } from '../components/language-distribution'; -import { translate } from '../../../helpers/l10n'; - -export const GeneralStructure = React.createClass({ - propTypes: { - leakPeriodLabel: React.PropTypes.string, - leakPeriodDate: React.PropTypes.object - }, - - mixins: [TooltipsMixin, DomainMixin], - - renderLeak () { - if (!this.hasLeakPeriod()) { - return null; - } - const measure = this.props.leak['ncloc']; - const formatted = measure != null ? formatMeasureVariation(measure, 'SHORT_INT') : '—'; - return <DomainLeak> - <MeasuresList> - <Measure label={getMetricName('ncloc')}>{formatted}</Measure> - </MeasuresList> - {this.renderTimeline('after')} - </DomainLeak>; - }, - - renderLanguageDistribution() { - if (!this.props.measures['ncloc'] || !this.props.measures['ncloc_language_distribution']) { - return null; - } - return <Measure composite={true}> - <div style={{ width: 200 }}> - <LanguageDistribution lines={Number(this.props.measures['ncloc'])} - distribution={this.props.measures['ncloc_language_distribution']}/> - </div> - </Measure>; - }, - - render () { - const domainUrl = window.baseUrl + '/component_measures/domain/Size?id=' + - encodeURIComponent(this.props.component.key); - - return <Domain> - <div className="overview-card-header"> - <div className="overview-title"> - <a href={domainUrl}> - {translate('overview.domain.structure')} - </a> - </div> - </div> - - <DomainPanel> - <DomainNutshell> - <MeasuresList> - {this.renderLanguageDistribution()} - <Measure label={getMetricName('ncloc')}> - <DrilldownLink component={this.props.component.key} metric="ncloc"> - {formatMeasure(this.props.measures['ncloc'], 'SHORT_INT')} - </DrilldownLink> - </Measure> - </MeasuresList> - {this.renderTimeline('before')} - </DomainNutshell> - {this.renderLeak()} - </DomainPanel> - </Domain>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/components/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index 77b5d3b6d75..c87e51e3010 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -23,9 +23,9 @@ import MetaKey from './MetaKey'; import MetaLinks from './MetaLinks'; import MetaQualityGate from './MetaQualityGate'; import MetaQualityProfiles from './MetaQualityProfiles'; -import EventsList from './EventsList'; +import EventsList from './../events/EventsList'; -export default function Meta ({ component }) { +const Meta = ({ component }) => { const { qualifier, description, links, profiles, gate } = component; const isView = qualifier === 'VW' || qualifier === 'SVW'; @@ -70,4 +70,6 @@ export default function Meta ({ component }) { <EventsList component={component}/> </div> ); -} +}; + +export default Meta; diff --git a/server/sonar-web/src/main/js/apps/overview/components/MetaKey.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaKey.js index 71d212891cc..9a7e67346c4 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/MetaKey.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaKey.js @@ -21,7 +21,7 @@ import React from 'react'; import { translate } from '../../../helpers/l10n'; -export default function MetaKey ({ component }) { +const MetaKey = ({ component }) => { return ( <div> <h4 className="overview-meta-header"> @@ -34,4 +34,6 @@ export default function MetaKey ({ component }) { readOnly={true}/> </div> ); -} +}; + +export default MetaKey; diff --git a/server/sonar-web/src/main/js/apps/overview/components/MetaLinks.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js index 0d44738ff1f..369846ccb28 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/MetaLinks.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js @@ -19,7 +19,7 @@ */ import React from 'react'; -export default function MetaLinks ({ links }) { +const MetaLinks = ({ links }) => { return ( <ul className="overview-meta-list big-spacer-bottom"> {links.map(link => ( @@ -36,4 +36,6 @@ export default function MetaLinks ({ links }) { ))} </ul> ); -} +}; + +export default MetaLinks; diff --git a/server/sonar-web/src/main/js/apps/overview/components/MetaQualityGate.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js index 8f5170d04ea..3b26eb80623 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/MetaQualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js @@ -22,7 +22,7 @@ import React from 'react'; import { QualityGateLink } from '../../../components/shared/quality-gate-link'; import { translate } from '../../../helpers/l10n'; -export default function MetaQualityGate ({ gate }) { +const MetaQualityGate = ({ gate }) => { return ( <div className="big-spacer-bottom"> <h4 className="overview-meta-header"> @@ -43,4 +43,6 @@ export default function MetaQualityGate ({ gate }) { </ul> </div> ); -} +}; + +export default MetaQualityGate; diff --git a/server/sonar-web/src/main/js/apps/overview/components/MetaQualityProfiles.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js index 1bb15965a5a..d889f815c2f 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/MetaQualityProfiles.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js @@ -22,7 +22,7 @@ import React from 'react'; import { QualityProfileLink } from '../../../components/shared/quality-profile-link'; import { translate } from '../../../helpers/l10n'; -export default function MetaQualityProfiles ({ profiles }) { +const MetaQualityProfiles = ({ profiles }) => { return ( <div> <h4 className="overview-meta-header"> @@ -43,4 +43,6 @@ export default function MetaQualityProfiles ({ profiles }) { </ul> </div> ); -} +}; + +export default MetaQualityProfiles; diff --git a/server/sonar-web/src/main/js/apps/overview/propTypes.js b/server/sonar-web/src/main/js/apps/overview/propTypes.js new file mode 100644 index 00000000000..50c3cffb978 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/propTypes.js @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 { PropTypes } from 'react'; + +const { shape, arrayOf, array, string, number, object } = PropTypes; + +export const ComponentType = shape({ + id: string.isRequired +}); + +export const MetricType = shape({ + key: string.isRequired, + name: string.isRequired, + type: string.isRequired +}); + +export const MeasureType = shape({ + metric: MetricType.isRequired, + value: string, + periods: array +}); + +export const MeasuresListType = arrayOf(MeasureType); + +export const ConditionType = shape({ + metric: string.isRequired +}); + +export const EnhancedConditionType = shape({ + measure: MeasureType.isRequired +}); + +export const ConditionsListType = arrayOf(ConditionType); + +export const EnhancedConditionsListType = arrayOf(EnhancedConditionType); + +export const PeriodType = shape({ + index: number.isRequired, + date: string.isRequired, + mode: string.isRequired, + parameter: string +}); + +export const PeriodsListType = arrayOf(PeriodType); + +export const EventType = shape({ + id: string.isRequired, + date: object.isRequired, + type: string.isRequired, + name: string.isRequired, + text: string +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/EmptyQualityGate.js index 75aee6e6144..56e607c05cb 100644 --- a/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/EmptyQualityGate.js @@ -18,18 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; + import { translate } from '../../../helpers/l10n'; -export default React.createClass({ - render() { - const qualityGatesUrl = window.baseUrl + '/quality_gates'; +const EmptyQualityGate = () => { + return ( + <div className="overview-quality-gate"> + <h2 className="overview-title"> + {translate('overview.quality_gate')} + </h2> + <p className="overview-quality-gate-warning"> + {translate('overview.you_should_define_quality_gate')} + </p> + </div> + ); +}; - return ( - <div className="overview-gate"> - <h2 className="overview-title">{translate('overview.quality_gate')}</h2> - <p className="overview-gate-warning"> - You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p> - </div> - ); - } -}); +export default EmptyQualityGate; diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js new file mode 100644 index 00000000000..6217905ae83 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +import QualityGateConditions from './QualityGateConditions'; +import EmptyQualityGate from './EmptyQualityGate'; +import { ComponentType, MeasuresListType, PeriodsListType } from '../propTypes'; +import { translate } from '../../../helpers/l10n'; + +function parseQualityGateDetails (rawDetails) { + return JSON.parse(rawDetails); +} + +function isProject (component) { + return component.qualifier === 'TRK'; +} + +const QualityGate = ({ component, measures, periods }) => { + const statusMeasure = + measures.find(measure => measure.metric.key === 'alert_status'); + const detailsMeasure = + measures.find(measure => measure.metric.key === 'quality_gate_details'); + + if (!statusMeasure) { + return isProject(component) ? <EmptyQualityGate/> : null; + } + + const level = statusMeasure.value; + + let conditions = []; + if (detailsMeasure) { + conditions = parseQualityGateDetails(detailsMeasure.value).conditions; + } + + return ( + <div className="overview-quality-gate" id="overview-quality-gate"> + <h2 className="overview-title"> + {translate('overview.quality_gate')} + <span className={'badge badge-' + level.toLowerCase()}> + {translate('overview.gate', level)} + </span> + </h2> + + {conditions.length > 0 && ( + <QualityGateConditions + component={component} + periods={periods} + conditions={conditions}/> + )} + </div> + ); +}; + +QualityGate.propTypes = { + component: ComponentType.isRequired, + measures: MeasuresListType.isRequired, + periods: PeriodsListType.isRequired +}; + +export default QualityGate; + diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js new file mode 100644 index 00000000000..7708f2f90a8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js @@ -0,0 +1,84 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +import { ComponentType, PeriodsListType, EnhancedConditionType } from '../propTypes'; +import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import { formatMeasure, getPeriodValue } from '../../../helpers/measures'; +import { translate } from '../../../helpers/l10n'; +import { getPeriod, getPeriodLabel, getPeriodDate } from '../../../helpers/periods'; + +const QualityGateCondition = ({ component, periods, condition }) => { + const { measure } = condition; + const { metric } = measure; + + const threshold = condition.level === 'ERROR' ? + condition.error : + condition.warning; + + const actual = condition.period ? + getPeriodValue(measure, condition.period) : + measure.value; + + const period = getPeriod(periods, condition.period); + const periodLabel = getPeriodLabel(period); + const periodDate = getPeriodDate(period); + + return ( + <li className="overview-quality-gate-condition"> + <div className="overview-quality-gate-condition-period"> + {periodLabel} + </div> + + <div className="overview-quality-gate-condition-container"> + <div className="overview-quality-gate-condition-value"> + <DrilldownLink + component={component.key} + metric={metric.name} + period={condition.period} + periodDate={periodDate}> + <span className={'alert_' + condition.level.toUpperCase()}> + {formatMeasure(actual, metric.type)} + </span> + </DrilldownLink> + </div> + + <div> + <div className="overview-quality-gate-condition-metric"> + {metric.name} + </div> + <div className="overview-quality-gate-condition-threshold"> + {translate('quality_gates.operator', condition.op, 'short')} + {' '} + {formatMeasure(threshold, metric.type)} + </div> + </div> + </div> + </li> + ); +}; + +QualityGateCondition.propTypes = { + component: ComponentType.isRequired, + periods: PeriodsListType.isRequired, + condition: EnhancedConditionType.isRequired +}; + +export default QualityGateCondition; diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js new file mode 100644 index 00000000000..d48614ce09d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js @@ -0,0 +1,116 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import sortBy from 'lodash/sortBy'; + +import QualityGateCondition from './QualityGateCondition'; +import { ComponentType, ConditionsListType } from '../propTypes'; +import { getMeasuresAndMeta } from '../../../api/measures'; +import { enhanceMeasuresWithMetrics } from '../../../helpers/measures'; + +const LEVEL_ORDER = ['ERROR', 'WARN']; + +function enhanceConditions (conditions, measures) { + return conditions.map(c => { + const measure = measures.find(measure => measure.metric.key === c.metric); + return { ...c, measure }; + }); +} + +export default class QualityGateConditions extends React.Component { + static propTypes = { + component: ComponentType.isRequired, + conditions: ConditionsListType.isRequired + }; + + state = { + loading: true + }; + + componentDidMount () { + this.mounted = true; + this.loadFailedMeasures(this.props); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentDidUpdate (nextProps) { + if (nextProps.conditions !== this.props.conditions || + nextProps.component !== this.props.component) { + this.loadFailedMeasures(nextProps); + } + } + + componentWillUnmount () { + this.mounted = false; + } + + loadFailedMeasures (props) { + const { component, conditions } = props; + const failedConditions = conditions.filter(c => c.level !== 'OK'); + const metrics = failedConditions.map(condition => condition.metric); + + getMeasuresAndMeta( + component.key, + metrics, + { additionalFields: 'metrics' } + ).then(r => { + if (this.mounted) { + const measures = enhanceMeasuresWithMetrics(r.component.measures, r.metrics); + this.setState({ + conditions: enhanceConditions(failedConditions, measures), + loading: false + }); + } + }); + } + + render () { + const { component, periods } = this.props; + const { loading, conditions } = this.state; + + if (loading) { + return null; + } + + const sortedConditions = sortBy( + conditions, + condition => LEVEL_ORDER.indexOf(condition.level), + condition => condition.metric.name + ); + + return ( + <ul + className="overview-quality-gate-conditions-list" + id="overview-quality-gate-conditions-list"> + {sortedConditions.map(condition => ( + <QualityGateCondition + key={condition.measure.metric.key} + component={component} + periods={periods} + condition={condition}/> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css new file mode 100644 index 00000000000..7e09cd5241c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -0,0 +1,292 @@ +.overview { + display: flex; + animation: fadeIn 0.5s forwards; +} + +.overview-main { + flex-grow: 1; + box-sizing: border-box; + background-color: #f3f3f3; + transition: transform 0.5s ease, opacity 0.5s ease; +} + +/* + * Title + */ + +.overview-title { + font-size: 16px; + font-weight: 400; +} + +.overview-title > .badge { + position: relative; + top: -2px; + margin-left: 15px; + padding: 6px 12px; + font-size: 14px; + letter-spacing: 0.05em; +} + +.overview-title > a { + border-bottom-color: #d0d0d0; + color: #444; +} + +.overview-title > a:hover, +.overview-title > a:focus { + border-bottom-color: #cae3f2; + color: #4b9fd5; +} + +/* + * Quality Gate + */ + +.overview-quality-gate { + padding-bottom: 15px; + border-bottom: 1px solid #e6e6e6; + background-color: #f3f3f3; +} + +.overview-quality-gate-conditions-list { + display: flex; + flex-wrap: wrap; + align-items: flex-end; +} + +.overview-quality-gate-condition { + padding: 10px 40px 10px 0; +} + +.overview-quality-gate-condition-period { + margin-bottom: 4px; +} + +.overview-quality-gate-condition-container { + display: flex; + align-items: center; +} + +.overview-quality-gate-condition-value { + margin-right: 8px; + font-size: 24px; + font-weight: 300; +} + +.overview-quality-gate-condition-metric { +} + +.overview-quality-gate-condition-threshold { +} + +.overview-quality-gate-warning { + margin: 15px 0 0; +} + +/* + * Domain + */ + +.overview-domains-list { + animation: fadeIn 0.5s forwards; +} + +.overview-card { + margin: 15px 0; +} + +.overview-card-special { + padding-bottom: 26px; + border-bottom: 1px solid #e6e6e6; +} + +.overview-card-header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 10px; + line-height: 24px; +} + +.overview-domain-panel { + display: flex; + margin-top: 10px; + border: 1px solid #e6e6e6; + background-color: #fff; +} + +.overview-domain-nutshell, +.overview-domain-leak { + position: relative; + display: flex; + padding: 15px 10px; +} + +.overview-domain-nutshell { + flex: 2; +} + +.overview-domain-nutshell .line-chart-backdrop { + fill: #e5f1f9; +} + +.overview-domain-leak { + flex: 1; + background-color: #fbf3d5; +} + +.overview-domain-leak .overview-domain-measures { + padding: 0; +} + +.overview-domain-leak .line-chart-backdrop { + fill: #efe7b8; +} + +.overview-domain-measures { + position: relative; + z-index: 2; + display: flex; + flex: 1; + align-items: center; + padding: 0 10%; +} + +.overview-domain-measures + .overview-domain-measures { + margin-top: 30px; +} + +.overview-domain-measures + .overview-domain-measures .overview-domain-measure-value { + font-size: 14px; + font-weight: 400; +} + +.overview-domain-measures + .overview-domain-measures .overview-domain-measure-label { + margin-top: 4px; +} + +.overview-domain-measure { + flex: 1; +} + +.overview-domain-measure + .overview-domain-measure { + padding-left: 15%; +} + +.overview-domain-measure-value { + line-height: 1; + font-size: 36px; + font-weight: 300; +} + +.overview-domain-leak .overview-domain-measure-value { + text-align: center; +} + +.overview-domain-measure-label { + margin-top: 10px; +} + +.overview-domain-leak .overview-domain-measure-label { + text-align: center; +} + +.overview-domain-measure-sup { + display: inline-block; + vertical-align: top; + margin-top: -4px; + margin-left: 6px; + font-size: 16px; +} + +.overview-domain-timeline { + position: absolute; + z-index: 1; + bottom: 0; + left: 0; + right: 0; + animation: fadeIn 0.5s forwards; +} + +.overview-domain-timeline .line-chart-path { + fill: none; + stroke: none; +} + +.overview-domain-timeline-date { + position: absolute; + bottom: 2px; + left: 5px; + color: rgba(119, 119, 119, 0.6); + font-size: 11px; +} + +/* + * Meta + */ + +.overview-meta { + flex-shrink: 0; + width: 260px; + padding-left: 40px; + background-color: #f3f3f3; +} + +.overview-meta-card { + min-width: 200px; + padding-bottom: 20px; + box-sizing: border-box; +} + +.overview-meta-description { + line-height: 1.5; +} + +.overview-meta-header { + color: #797979; +} + +.overview-meta-list > li { + padding-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* + * Other + */ + +.overview-legend { + position: absolute; + bottom: 100%; + left: 0; + right: -1px; + padding: 5px 0 2px; + border: 1px solid #e6e6e6; + background-color: #fbf3d5; + font-size: 14px; + text-align: center; + transform: translateY(-4px); +} + +.overview-key { + width: 100%; + padding: 0 !important; + border: none !important; + background-color: transparent !important; +} + +/* + * Animations + */ + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/LanguageDistribution.js b/server/sonar-web/src/main/js/components/charts/LanguageDistribution.js index fe8c4d6849f..5105fca04b8 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/LanguageDistribution.js +++ b/server/sonar-web/src/main/js/components/charts/LanguageDistribution.js @@ -20,23 +20,43 @@ import find from 'lodash/find'; import sortBy from 'lodash/sortBy'; import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; -import { Histogram } from '../../../components/charts/histogram'; -import { formatMeasure } from '../../../helpers/measures'; -import { getLanguages } from '../../../api/languages'; -import { translate } from '../../../helpers/l10n'; +import { Histogram } from './histogram'; +import { formatMeasure } from '../../helpers/measures'; +import { getLanguages } from '../../api/languages'; +import { translate } from '../../helpers/l10n'; export default class LanguageDistribution extends React.Component { + static propTypes = { + distribution: React.PropTypes.string.isRequired + }; + + state = {}; + componentDidMount () { + this.mounted = true; this.requestLanguages(); } + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentWillUnmount () { + this.mounted = false; + } + requestLanguages () { - getLanguages().then(languages => this.setState({ languages })); + getLanguages().then(languages => { + if (this.mounted) { + this.setState({ languages }); + } + }); } getLanguageName (langKey) { - if (this.state && this.state.languages) { + if (this.state.languages) { const lang = find(this.state.languages, { key: langKey }); return lang ? lang.name : translate('unknown'); } else { @@ -49,23 +69,27 @@ export default class LanguageDistribution extends React.Component { } render () { - let data = this.props.distribution.split(';').map((point, index) => { - const tokens = point.split('='); - return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] }; - }); + let data = this.props.distribution.split(';') + .map((point, index) => { + const tokens = point.split('='); + return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] }; + }); data = sortBy(data, d => -d.x); - const yTicks = data.map(point => this.getLanguageName(point.value)).map(this.cutLanguageName); + const yTicks = data + .map(point => this.getLanguageName(point.value)) + .map(this.cutLanguageName); const yValues = data.map(point => formatMeasure(point.x, 'SHORT_INT')); return ( - <Histogram data={data} - yTicks={yTicks} - yValues={yValues} - barsWidth={10} - height={data.length * 25} - padding={[0, 60, 0, 80]}/> + <Histogram + data={data} + yTicks={yTicks} + yValues={yValues} + barsWidth={10} + height={data.length * 25} + padding={[0, 60, 0, 80]}/> ); } } diff --git a/server/sonar-web/src/main/js/apps/overview/components/complexity-distribution.js b/server/sonar-web/src/main/js/components/shared/complexity-distribution.js index e57f7072e6c..4771ae96489 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/complexity-distribution.js +++ b/server/sonar-web/src/main/js/components/shared/complexity-distribution.js @@ -19,9 +19,9 @@ */ import React from 'react'; -import { BarChart } from '../../../components/charts/bar-chart'; -import { formatMeasure } from '../../../helpers/measures'; -import { translateWithParameters } from '../../../helpers/l10n'; +import { BarChart } from '../charts/bar-chart'; +import { formatMeasure } from '../../helpers/measures'; +import { translateWithParameters } from '../../helpers/l10n'; const HEIGHT = 80; @@ -57,8 +57,13 @@ export const ComplexityDistribution = React.createClass({ }, render () { - return <div className="overview-bar-chart" style={{ height: HEIGHT }}> - {this.renderBarChart()} - </div>; + // TODO remove inline styling + return ( + <div + className="overview-bar-chart" + style={{ height: HEIGHT, paddingTop: 10, paddingBottom: 15 }}> + {this.renderBarChart()} + </div> + ); } }); diff --git a/server/sonar-web/src/main/js/helpers/measures.js b/server/sonar-web/src/main/js/helpers/measures.js index 0a66ef903b7..bf6df21284a 100644 --- a/server/sonar-web/src/main/js/helpers/measures.js +++ b/server/sonar-web/src/main/js/helpers/measures.js @@ -80,6 +80,39 @@ export function getShortType (type) { return type; } +/** + * Map metrics + * @param {Array} measures + * @param {Array} metrics + * @returns {Array} + */ +export function enhanceMeasuresWithMetrics (measures, metrics) { + return measures.map(measure => { + const metric = metrics.find(metric => metric.key === measure.metric); + return { ...measure, metric }; + }); +} + +/** + * Get period value of a measure + * @param measure + * @param periodIndex + */ +export function getPeriodValue (measure, periodIndex) { + const { periods } = measure; + const period = periods.find(period => period.index === periodIndex); + return period ? period.value : null; +} + +/** + * Check if metric is differential + * @param {string} metricKey + * @returns {boolean} + */ +export function isDiffMetric (metricKey) { + return metricKey.indexOf('new_') === 0; +} + /* * Helpers */ diff --git a/server/sonar-web/src/main/js/widgets/complexity/index.js b/server/sonar-web/src/main/js/widgets/complexity/index.js index 246a2ac9245..ffa5a9fd551 100644 --- a/server/sonar-web/src/main/js/widgets/complexity/index.js +++ b/server/sonar-web/src/main/js/widgets/complexity/index.js @@ -20,7 +20,7 @@ import React from 'react'; import { render } from 'react-dom'; import { translate } from '../../helpers/l10n'; -import { ComplexityDistribution } from '../../apps/overview/components/complexity-distribution'; +import { ComplexityDistribution } from '../../components/shared/complexity-distribution'; const Widget = ({ value, of }) => { return ( diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less deleted file mode 100644 index 749e17de3c1..00000000000 --- a/server/sonar-web/src/main/less/pages/overview.less +++ /dev/null @@ -1,596 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 (reference) "../variables"; -@import (reference) "../mixins"; -@import (reference) "../init/type"; -@import (reference) "../init/links"; - -.overview { - display: flex; - min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})"; - animation: fadeIn 0.5s forwards; -} - -.overview-main { - flex-grow: 1; - box-sizing: border-box; - background-color: @barBackgroundColor; - transition: transform 0.5s ease, opacity 0.5s ease; -} - -/* - * Gate - */ - -.overview-gate { - padding-bottom: 15px; - border-bottom: 1px solid @barBorderColor; - background-color: @barBackgroundColor; -} - -.overview-gate-conditions-list { - display: flex; - flex-wrap: wrap; -} - -.overview-gate-condition { - padding: 10px 40px 10px 0; -} - -.overview-gate-condition-value { - margin-right: 4px; - font-weight: 300; - font-size: 24px; -} - -.overview-gate-warning { - margin: 15px 20px 0; -} - -/* - * Title - */ - -.overview-title { - font-size: 16px; - font-weight: 400; - - & > .badge { - position: relative; - top: -2px; - margin-left: 15px; - padding: 6px 12px; - font-size: 14px; - letter-spacing: 0.05em; - } - - & > a { - border-bottom-color: #d0d0d0; - color: @baseFontColor; - - &:hover, &:focus { - border-bottom-color: @lightBlue; - color: @blue; - } - } -} - -/* - * Meta - */ - -.overview-meta { - flex-shrink: 0; - width: 260px; - padding-left: 40px; - background-color: @barBackgroundColor; -} - -.overview-meta-card { - min-width: 200px; - padding-bottom: 20px; - box-sizing: border-box; -} - -.overview-meta-description { - line-height: 1.5; -} - -.overview-meta-header { - color: #797979; -} - -.overview-meta-list { - & > li { - padding-bottom: 4px; - .text-ellipsis; - } -} - -/* - * Domain - */ - -.overview-domains-list { - animation: fadeIn 0.5s forwards; -} - -.overview-cards-list { - display: flex; - - & > .overview-card, - & > .overview-domain-chart { - flex: 1; - } -} - -.overview-card { - margin: 15px 0; -} - -.overview-card-special { - padding-bottom: 26px; - border-bottom: 1px solid #e6e6e6; -} - -.overview-card-fixed-width { - max-width: 560px; -} - -.overview-card-header { - display: flex; - align-items: baseline; - justify-content: space-between; - margin-bottom: 10px; - line-height: @formControlHeight; - - .overview-title { - flex: 1; - } -} - -.overview-domain-panel { - display: flex; - margin-top: 10px; - border: 1px solid @barBorderColor; - background-color: #fff; - - .overview-bar-chart { - padding: 0; - } -} - -.overview-domain-nutshell, -.overview-domain-leak { - position: relative; - display: flex; - padding: 15px 10px; -} - -.overview-domain-nutshell { - flex: 2; - - .line-chart-backdrop { - fill: #e5f1f9; - } -} - -.overview-domain-leak { - flex: 1; - background-color: #fbf3d5; - - .line-chart-backdrop { - fill: #efe7b8; - } -} - -.overview-domain-measures { - position: relative; - z-index: 2; - display: flex; - flex: 1; - align-items: center; - padding: 0 10%; -} - -.overview-domain-measures + .overview-domain-measures { - margin-top: 30px; - - .overview-domain-measure-value { - font-size: 14px; - font-weight: 400; - } - - .overview-domain-measure-label { - margin-top: 4px; - } -} - -.overview-domain-leak .overview-domain-measures { - padding: 0; -} - -.overview-domain-measure { - flex: 1; -} - -.overview-domain-measure + .overview-domain-measure { - padding-left: 15%; -} - -.overview-domain-measure-value { - line-height: 1; - font-size: 36px; - font-weight: 300; - - .overview-domain-leak & { text-align: center; } -} - -.overview-domain-measure-label { - margin-top: 10px; - - .overview-domain-leak & { text-align: center; } -} - -.overview-domain-measure-sup { - display: inline-block; - vertical-align: top; - margin-top: -4px; - margin-left: 6px; - font-size: 16px; -} - -.overview-domain-timeline { - position: absolute; - z-index: 1; - bottom: 0; - left: 0; - right: 0; - animation: fadeIn 0.5s forwards; - - .line-chart-path { - fill: none; - stroke: none; - } -} - -.overview-domain-timeline-date { - position: absolute; - bottom: 2px; - left: 5px; - color: fade(@secondFontColor, 60%); - font-size: 11px; -} - -/* - * Detailed Pages - */ - -.overview-detailed-page { - flex: 1; - animation: fadeIn 0.5s forwards; - - .overview-domain-leak-title { - padding: 0 10px; - border: 1px solid @barBorderColor; - background: #fbf3d5; - } -} - -.overview-detailed-measures-list { - border: 1px solid @barBorderColor; - background-color: #fff; - overflow: hidden; -} - -.overview-detailed-measures-list + .overview-detailed-measures-list { - margin-top: 40px; -} - -.overview-detailed-measures-list-inline { - display: flex; - border: none; - background: none; - - .overview-detailed-measure { - flex: 1; - flex-direction: column; - border: 1px solid @barBorderColor; - } - - .overview-detailed-measure-name { - margin-bottom: 8px; - flex: 0 1 auto; - font-weight: 500; - text-align: center; - } - - .overview-detailed-measure + .overview-detailed-measure { - margin-left: 10px; - } - - .overview-detailed-measure-nutshell { - flex-flow: column nowrap; - justify-content: flex-start; - align-items: stretch; - flex: 3 0 auto; - - .overview-detailed-measure-value { - flex: 1 0 auto; - display: flex; - justify-content: center; - align-items: center; - } - } - - .overview-detailed-measure-leak { - flex: 0 1 auto; - } -} - -.overview-detailed-measure { - display: flex; - background-color: #fff; -} - -.overview-detailed-measure-rating { - border: none !important; - background: none; - - .overview-detailed-measure-value { font-size: 36px; } -} - -.overview-detailed-measure-nutshell, -.overview-detailed-measure-leak, -.overview-detailed-measure-chart { - position: relative; - padding: 7px 10px; - - .overview-detailed-measure-nutshell, - .overview-detailed-measure-leak, - .overview-detailed-measure-chart { - padding-right: 0; - } -} - -.overview-detailed-measure-nutshell { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-items: baseline; - flex: 3; -} - -.overview-detailed-measure-leak { - flex: 1; - background-color: #fbf3d5; - text-align: center; -} - -.overview-detailed-measure-name { - flex: 1; -} - -.overview-detailed-measure-value { - font-size: 16px; -} - -.overview-detailed-layout-size { - display: flex; - justify-content: space-between; - margin: 0 -10px; - - .overview-detailed-layout-column { - flex: 1; - max-width: 560px; - } -} - -.overview-detailed-layout-column { - padding: 0 10px; -} - -.overview-legend { - position: absolute; - bottom: 100%; - left: 0; - right: -1px; - padding: 5px 0 2px; - border: 1px solid @barBorderColor; - font-size: 14px; - text-align: center; - transform: translateY(-4px); -} - -/* - * Charts - */ - -.overview-domain-chart { - .overview-title { - display: inline-block; - margin-right: 20px; - } -} - -.overview-domain-chart + .overview-domain-chart { - margin-top: 60px; -} - -.overview-bar-chart { - width: 100%; - padding-top: 10px; - padding-bottom: 15px; - - svg { - position: absolute; - } -} - -.overview-timeline { - padding: 10px; - border: 1px solid @barBorderColor; - box-sizing: border-box; - background-color: #fff; - - svg { - position: absolute; - } - - .line-chart-backdrop { - fill: #fbf3d5; - } -} - -.overview-timeline-1 { - .line-chart-path { - stroke: @purple; - } - - .line-chart-point { - stroke: darken(@purple, 20%); - } -} - -.overview-timeline-sample { - display: inline-block; - vertical-align: middle; - width: 16px; - height: 2px; - margin-right: 8px; -} - -.overview-timeline-sample-0 { - background-color: @blue; -} - -.overview-timeline-sample-1 { - background-color: @purple; -} - -.overview-timeline-chart { - text-align: center; -} - -.overview-timeline-chart + .overview-timeline-chart { - margin-top: 40px; -} - -.overview-timeline-select { - width: 12em; - height: @formControlHeight; - line-height: @formControlHeight; - border: 1px solid #cdcdcd; - background: none; -} - -.overview-treemap { - & > div { - position: absolute; - } -} - -.overview-chart-placeholder { - display: flex; - justify-content: center; - align-items: center; -} - -.overview-bubble-chart { - padding: 10px; - border: 1px solid @barBorderColor; - background-color: #fff; - - svg { - position: absolute; - } - - .bubble-chart-bubble { - fill: @blue; - fill-opacity: 0.2; - stroke: @blue; - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - fill-opacity: 0.8; - } - } - - .bubble-chart-grid { - shape-rendering: crispedges; - stroke: #eee; - } - - .bubble-chart-tick { - fill: @secondFontColor; - font-size: 12px; - text-anchor: middle; - } - - .bubble-chart-tick-y { - text-anchor: end; - } -} - -.overview-donut-chart { - position: relative; - text-align: center; - - .overview-detailed-measure-value { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - } -} - -/* - * Misc - */ - -.overview-nutshell { - background-color: #fff; -} - -.overview-leak { - background-color: #fbf3d5; -} - -.overview-key { - width: 100%; - padding: 0 !important; - border: none !important; - background-color: transparent !important; -} - -/* - * Animations - */ - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} diff --git a/server/sonar-web/src/main/less/sonar.less b/server/sonar-web/src/main/less/sonar.less index abba2b8e055..0389a58b796 100644 --- a/server/sonar-web/src/main/less/sonar.less +++ b/server/sonar-web/src/main/less/sonar.less @@ -64,7 +64,6 @@ @import "pages/quality-gates"; @import "pages/maintenance"; @import "pages/login"; -@import "pages/overview"; @import 'style'; @import 'deprecated'; diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb index e5fc799417b..aecf8510253 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb @@ -48,23 +48,6 @@ id: '<%= escape_javascript @resource.uuid %>', key: '<%= escape_javascript @resource.key %>', description: '<%= escape_javascript @resource.description %>', - hasSnapshot: <%= @snapshot ? true : false %>, - periods: [ - <% - if @snapshot && @snapshot.project_snapshot.periods? - (1..5).each do |index| - if @snapshot.period_mode(index) - %> - { - index: '<%= index -%>', - mode: '<%= escape_javascript @snapshot.period_mode(index) -%>', - modeParam: '<%= escape_javascript @snapshot.period_param(index) -%>', - date: '<%= escape_javascript @snapshot.period_datetime(index) ? @snapshot.period_datetime(index).strftime('%FT%T%z') : "" -%>' - }, - <% end %> - <% end %> - <% end %> - ], links: [ <% @resource.project_links.sort.each_with_index do |link, index| %> { @@ -92,41 +75,8 @@ <% end %> }; - <% if m %> - var gate = { - level: '<%= m.alert_status -%>', - conditions: [ - <% conditions.sort_by {|condition| [ -condition['level'].length, metric(condition['metric']).short_name] }.each do |condition| %> - <% metric = metric(condition['metric']) %> - { - level: '<%= escape_javascript condition['level'] %>', - metric: { - name: '<%= escape_javascript metric.name %>', - type: '<%= escape_javascript metric.value_type %>' - }, - op: '<%= escape_javascript condition['op'] %>', - period: '<%= condition['period'] %>', - warning: '<%= escape_javascript condition['warning'] %>', - error: '<%= escape_javascript condition['error'] %>', - actual: '<%= escape_javascript condition['actual'] %>', - }, - <% end %> - ] - }; - <% else %> - <% if alert_status && !alert_status.alert_status.blank? %> - var gate = { - level: '<%= alert_status.alert_status -%>', - text: '<%= alert_status.alert_text -%>' - }; - <% else %> - var gate = null; - <% end %> - <% end %> - window.sonarqube.overview = { - component: component, - gate: gate + component: component }; })(); </script> diff --git a/server/sonar-web/tests/apps/overview/components/App-test.js b/server/sonar-web/tests/apps/overview/components/App-test.js new file mode 100644 index 00000000000..08488916e7a --- /dev/null +++ b/server/sonar-web/tests/apps/overview/components/App-test.js @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; + +import App from '../../../../src/main/js/apps/overview/components/App'; +import OverviewApp from '../../../../src/main/js/apps/overview/components/OverviewApp'; +import EmptyOverview from '../../../../src/main/js/apps/overview/components/EmptyOverview'; + +describe('Overview :: App', () => { + it('should render OverviewApp', () => { + const component = { + id: 'id', + snapshotDate: '2016-01-01' + }; + + const output = shallow( + <App component={component}/> + ); + + expect(output.type()) + .to.equal(OverviewApp); + }); + + it('should render EmptyOverview', () => { + const component = { id: 'id' }; + + const output = shallow( + <App component={component}/> + ); + + expect(output.type()) + .to.equal(EmptyOverview); + }); + + it('should pass leakPeriodIndex', () => { + const component = { + id: 'id', + snapshotDate: '2016-01-01' + }; + + const output = shallow( + <App component={component}/> + ); + + expect(output.prop('leakPeriodIndex')) + .to.equal('1'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js b/server/sonar-web/tests/apps/overview/components/EmptyOverview-test.js index 99dd41b43f6..65a1c0dae3c 100644 --- a/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js +++ b/server/sonar-web/tests/apps/overview/components/EmptyOverview-test.js @@ -18,18 +18,24 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import GateCondition from './gate-condition'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; -export default React.createClass({ - propTypes: { - gate: React.PropTypes.object.isRequired, - component: React.PropTypes.object.isRequired - }, +import EmptyOverview from '../../../../src/main/js/apps/overview/components/EmptyOverview'; - render() { - const conditions = this.props.gate.conditions - .filter(c => c.level !== 'OK') - .map(c => <GateCondition key={c.metric.name} condition={c} component={this.props.component}/>); - return <ul className="overview-gate-conditions-list">{conditions}</ul>; - } +describe('Overview :: EmptyOverview', () => { + it('should render component key', () => { + const component = { + id: 'id', + key: 'abcd', + snapshotDate: '2016-01-01' + }; + + const output = shallow( + <EmptyOverview component={component}/> + ); + + expect(output.find('code').text()) + .to.equal('abcd'); + }); }); diff --git a/server/sonar-web/tests/apps/overview/components/complexity-distribution-test.js b/server/sonar-web/tests/apps/overview/components/complexity-distribution-test.js deleted file mode 100644 index 0033010e562..00000000000 --- a/server/sonar-web/tests/apps/overview/components/complexity-distribution-test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import TestUtils from 'react-addons-test-utils'; - -import { ComplexityDistribution } from '../../../../src/main/js/apps/overview/components/complexity-distribution'; - - -const DISTRIBUTION = '1=11950;2=86;4=77;6=43;8=17;10=12;12=3'; - - -describe('ComplexityDistribution', function () { - let props; - - beforeEach(function () { - let renderer = TestUtils.createRenderer(); - renderer.render(<ComplexityDistribution distribution={DISTRIBUTION} of="function"/>); - let output = renderer.getRenderOutput(); - let child = React.Children.only(output.props.children); - props = child.props; - }); - - it('should pass right data', function () { - expect(props.data).to.deep.equal([ - { x: 0, y: 11950, value: 1, tooltip: 'overview.complexity_tooltip.function.11950.1' }, - { x: 1, y: 86, value: 2, tooltip: 'overview.complexity_tooltip.function.86.2' }, - { x: 2, y: 77, value: 4, tooltip: 'overview.complexity_tooltip.function.77.4' }, - { x: 3, y: 43, value: 6, tooltip: 'overview.complexity_tooltip.function.43.6' }, - { x: 4, y: 17, value: 8, tooltip: 'overview.complexity_tooltip.function.17.8' }, - { x: 5, y: 12, value: 10, tooltip: 'overview.complexity_tooltip.function.12.10' }, - { x: 6, y: 3, value: 12, tooltip: 'overview.complexity_tooltip.function.3.12' } - ]); - }); - - it('should pass right xTicks', function () { - expect(props.xTicks).to.deep.equal([1, 2, 4, 6, 8, 10, 12]); - }); - - it('should pass right xValues', function () { - expect(props.xValues).to.deep.equal(['11,950', '86', '77', '43', '17', '12', '3']); - }); -}); diff --git a/server/sonar-web/tests/apps/overview/components/event-test.js b/server/sonar-web/tests/apps/overview/components/event-test.js deleted file mode 100644 index 281898c6728..00000000000 --- a/server/sonar-web/tests/apps/overview/components/event-test.js +++ /dev/null @@ -1,23 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import { findDOMNode } from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; - -import { Event } from '../../../../src/main/js/apps/overview/components/event'; - - -describe('Overview :: Event', function () { - it('should render event', function () { - let output = TestUtils.renderIntoDocument( - <Event event={{ id: '1', name: '1.5', type: 'Version', date: new Date(2015, 0, 1) }}/>); - expect( - findDOMNode(TestUtils.findRenderedDOMComponentWithClass(output, 'js-event-date')).textContent - ).to.include('2015'); - expect( - findDOMNode(TestUtils.findRenderedDOMComponentWithClass(output, 'js-event-name')).textContent - ).to.include('1.5'); - expect( - findDOMNode(TestUtils.findRenderedDOMComponentWithClass(output, 'js-event-type')).textContent - ).to.include('Version'); - }); -}); diff --git a/server/sonar-web/tests/apps/overview/components/language-distribution-test.js b/server/sonar-web/tests/apps/overview/components/language-distribution-test.js deleted file mode 100644 index cf5740daa55..00000000000 --- a/server/sonar-web/tests/apps/overview/components/language-distribution-test.js +++ /dev/null @@ -1,49 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import TestUtils from 'react-addons-test-utils'; - -import { LanguageDistribution } from '../../../../src/main/js/apps/overview/components/language-distribution'; - - -const DISTRIBUTION = '<null>=17345;java=194342;js=20984'; -const LINES = 1000000; - - -describe('LanguageDistribution', function () { - let props; - - beforeEach(function () { - let renderer = TestUtils.createRenderer(); - renderer.render(<LanguageDistribution distribution={DISTRIBUTION} lines={LINES}/>); - let output = renderer.getRenderOutput(); - let child = React.Children.only(output.props.children); - props = child.props; - }); - - it('should pass right data', function () { - expect(props.data).to.deep.equal([ - { x: 194342, y: 1, value: 'java' }, - { x: 20984, y: 2, value: 'js' }, - { x: 17345, y: 0, value: '<null>' } - ]); - }); - - it('should pass right yTicks', function () { - expect(props.yTicks).to.deep.equal(['java', 'js', '<null>']); - }); - - it('should pass right yValues', function () { - expect(props.yValues).to.deep.equal(['19.4%', '2.1%', '1.7%']); - }); - - it('should correctly render very small values', function () { - const DISTRIBUTION_SMALL = 'java=194342;js=999'; - - let renderer = TestUtils.createRenderer(); - renderer.render(<LanguageDistribution distribution={DISTRIBUTION_SMALL} lines={LINES}/>); - let output = renderer.getRenderOutput(); - let child = React.Children.only(output.props.children); - - expect(child.props.yValues).to.deep.equal(['19.4%', '<0.1%']); - }); -}); diff --git a/server/sonar-web/tests/apps/overview/components/legend-test.js b/server/sonar-web/tests/apps/overview/components/legend-test.js deleted file mode 100644 index c32fc4c6323..00000000000 --- a/server/sonar-web/tests/apps/overview/components/legend-test.js +++ /dev/null @@ -1,19 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import TestUtils from 'react-addons-test-utils'; - -import { Legend } from '../../../../src/main/js/apps/overview/components/legend'; - - -const DATE = new Date(2015, 3, 7); -const LABEL = 'since 1.0'; - - -describe('Legend', function () { - it('should render', function () { - let renderer = TestUtils.createRenderer(); - renderer.render(<Legend leakPeriodDate={DATE} leakPeriodLabel={LABEL}/>); - let output = renderer.getRenderOutput(); - expect(output).to.not.be.null; - }); -}); diff --git a/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js b/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js deleted file mode 100644 index cbe1b80d83d..00000000000 --- a/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js +++ /dev/null @@ -1,97 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import TestUtils from 'react-addons-test-utils'; - -import Timeline from '../../../../src/main/js/components/charts/Timeline'; - - -const ZERO_DATA = [ - { x: new Date(2015, 0, 1), y: 0 }, - { x: new Date(2015, 0, 2), y: 0 }, - { x: new Date(2015, 0, 3), y: 0 }, - { x: new Date(2015, 0, 4), y: 0 } -]; - -const NULL_DATA = [ - { x: new Date(2015, 0, 1), y: null }, - { x: new Date(2015, 0, 2) }, - { x: new Date(2015, 0, 3), y: null }, - { x: new Date(2015, 0, 4) } -]; - -const FORMAT = (tick) => tick; - - -describe('TimelineChart', function () { - it('should work with LEVEL', function () { - const DATA = [ - { x: new Date(2015, 0, 1), y: 'OK' }, - { x: new Date(2015, 0, 2), y: 'WARN' }, - { x: new Date(2015, 0, 3), y: 'ERROR' }, - { x: new Date(2015, 0, 4), y: 'WARN' } - ]; - - let timeline = <Timeline width={100} - height={100} - data={DATA} - metricType="LEVEL" - events={[]} - formatValue={FORMAT} - formatYTick={FORMAT}/>; - let output = TestUtils.renderIntoDocument(timeline); - let ticks = TestUtils.scryRenderedDOMComponentsWithClass(output, 'line-chart-tick-x'); - expect(ticks).to.have.length(3); - expect(ticks[0].textContent).to.equal('ERROR'); - expect(ticks[1].textContent).to.equal('WARN'); - expect(ticks[2].textContent).to.equal('OK'); - }); - - it('should work with RATING', function () { - const DATA = [ - { x: new Date(2015, 0, 1), y: 1 }, - { x: new Date(2015, 0, 2), y: 3 }, - { x: new Date(2015, 0, 3), y: 1 }, - { x: new Date(2015, 0, 4), y: 4 } - ]; - - let timeline = <Timeline width={100} - height={100} - data={DATA} - metricType="RATING" - events={[]} - formatValue={FORMAT} - formatYTick={FORMAT}/>; - let output = TestUtils.renderIntoDocument(timeline); - let ticks = TestUtils.scryRenderedDOMComponentsWithClass(output, 'line-chart-tick-x'); - expect(ticks).to.have.length(5); - expect(ticks[0].textContent).to.equal('5'); - expect(ticks[1].textContent).to.equal('4'); - expect(ticks[2].textContent).to.equal('3'); - expect(ticks[3].textContent).to.equal('2'); - expect(ticks[4].textContent).to.equal('1'); - }); - - it('should display the zero Y tick if all values are zero', function () { - let timeline = <Timeline width={100} - height={100} - data={ZERO_DATA} - events={[]} - formatValue={FORMAT} - formatYTick={FORMAT}/>; - let output = TestUtils.renderIntoDocument(timeline); - let tick = TestUtils.findRenderedDOMComponentWithClass(output, 'line-chart-tick-x'); - expect(tick.textContent).to.equal('0'); - }); - - it('should display the zero Y tick if all values are undefined', function () { - let timeline = <Timeline width={100} - height={100} - data={NULL_DATA} - events={[]} - formatValue={FORMAT} - formatYTick={FORMAT}/>; - let output = TestUtils.renderIntoDocument(timeline); - let tick = TestUtils.findRenderedDOMComponentWithClass(output, 'line-chart-tick-x'); - expect(tick.textContent).to.equal('0'); - }); -}); diff --git a/server/sonar-web/tests/apps/overview/helpers/periods-test.js b/server/sonar-web/tests/apps/overview/helpers/periods-test.js deleted file mode 100644 index bd9b3a41e05..00000000000 --- a/server/sonar-web/tests/apps/overview/helpers/periods-test.js +++ /dev/null @@ -1,67 +0,0 @@ -import { expect } from 'chai'; - -import { getPeriodDate, getPeriodLabel } from '../../../../src/main/js/apps/overview/helpers/periods'; - - -const PERIOD = { - date: '2015-09-09T00:00:00+0200', - index: '1', - mode: 'previous_version', - modeParam: '1.7' -}; - -const PERIOD_WITHOUT_VERSION = { - date: '2015-09-09T00:00:00+0200', - index: '1', - mode: 'previous_version', - modeParam: '' -}; - -const PERIOD_WITHOUT_DATE = { - date: '', - index: '1', - mode: 'previous_version', - modeParam: '' -}; - - -describe('Overview Helpers', function () { - describe('Periods', function () { - - describe('#getPeriodDate', function () { - it('should return date', function () { - let result = getPeriodDate([PERIOD], PERIOD.index); - expect(result.getFullYear()).to.equal(2015); - }); - - it('should return null when can not find period', function () { - let result = getPeriodDate([], '1'); - expect(result).to.be.null; - }); - - it('should return null when date is empty', function () { - let result = getPeriodDate([PERIOD_WITHOUT_DATE], '1'); - expect(result).to.be.null; - }); - }); - - - describe('#getPeriodLabel', function () { - it('should return label', function () { - let result = getPeriodLabel([PERIOD], PERIOD.index); - expect(result).to.equal('overview.period.previous_version.1.7'); - }); - - it('should return "since previous version"', function () { - let result = getPeriodLabel([PERIOD_WITHOUT_VERSION], PERIOD_WITHOUT_VERSION.index); - expect(result).to.equal('overview.period.previous_version_only_date'); - }); - - it('should return null', function () { - let result = getPeriodLabel([], '1'); - expect(result).to.be.null; - }); - }); - - }); -}); diff --git a/server/sonar-web/tests/apps/overview/main/coverage-test.js b/server/sonar-web/tests/apps/overview/main/coverage-test.js deleted file mode 100644 index 631179a344d..00000000000 --- a/server/sonar-web/tests/apps/overview/main/coverage-test.js +++ /dev/null @@ -1,69 +0,0 @@ -import _ from 'underscore'; -import { expect } from 'chai'; -import React from 'react'; -import TestUtils from 'react-addons-test-utils'; - -import { GeneralCoverage } from '../../../../src/main/js/apps/overview/main/coverage'; - - -const COMPONENT = { key: 'component-key' }; - -const DATE = new Date(2015, 0, 1); - -const MEASURES = { - 'overall_coverage': 73.5, - 'coverage': 69.7, - 'it_coverage': 54.0, - 'tests': 137 -}; -const LEAK = { - 'new_overall_coverage': 72.5, - 'new_coverage': 68.7, - 'new_it_coverage': 53.0 -}; -const MEASURES_FOR_UT = _.omit(MEASURES, 'overall_coverage'); -const LEAK_FOR_UT = _.omit(LEAK, 'new_overall_coverage'); -const MEASURES_FOR_IT = _.omit(MEASURES_FOR_UT, 'coverage'); -const LEAK_FOR_IT = _.omit(LEAK_FOR_UT, 'new_coverage'); - - -describe('Overview :: GeneralCoverage', function () { - it('should display tests', function () { - let component = <GeneralCoverage measures={MEASURES} component={COMPONENT} coverageMetricPrefix=""/>; - let output = TestUtils.renderIntoDocument(component); - let coverageElement = TestUtils.findRenderedDOMComponentWithClass(output, 'js-overview-main-tests'); - expect(coverageElement.textContent).to.equal('137'); - }); - - it('should not display tests', function () { - let measuresWithoutTests = _.omit(MEASURES, 'tests'); - let component = <GeneralCoverage measures={measuresWithoutTests} component={COMPONENT} coverageMetricPrefix=""/>; - let output = TestUtils.renderIntoDocument(component); - let coverageElements = TestUtils.scryRenderedDOMComponentsWithClass(output, 'js-overview-main-tests'); - expect(coverageElements).to.be.empty; - }); - - it('should fall back to UT coverage', function () { - let component = <GeneralCoverage measures={MEASURES_FOR_UT} leak={LEAK_FOR_UT} component={COMPONENT} - leakPeriodDate={DATE} coverageMetricPrefix=""/>; - let output = TestUtils.renderIntoDocument(component); - - let coverageElement = TestUtils.findRenderedDOMComponentWithClass(output, 'js-overview-main-coverage'); - expect(coverageElement.textContent).to.equal('69.7%'); - - let newCoverageElement = TestUtils.findRenderedDOMComponentWithClass(output, 'js-overview-main-new-coverage'); - expect(newCoverageElement.textContent).to.equal('68.7%'); - }); - - it('should fall back to IT coverage', function () { - let component = <GeneralCoverage measures={MEASURES_FOR_IT} leak={LEAK_FOR_IT} component={COMPONENT} - leakPeriodDate={DATE} coverageMetricPrefix="it_"/>; - let output = TestUtils.renderIntoDocument(component); - - let coverageElement = TestUtils.findRenderedDOMComponentWithClass(output, 'js-overview-main-coverage'); - expect(coverageElement.textContent).to.equal('54.0%'); - - let newCoverageElement = TestUtils.findRenderedDOMComponentWithClass(output, 'js-overview-main-new-coverage'); - expect(newCoverageElement.textContent).to.equal('53.0%'); - }); -}); diff --git a/server/sonar-web/tests/apps/overview/overview-test.js b/server/sonar-web/tests/apps/overview/overview-test.js deleted file mode 100644 index 2b7ae01d8be..00000000000 --- a/server/sonar-web/tests/apps/overview/overview-test.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; -import { expect } from 'chai'; - -import Gate from '../../../src/main/js/apps/overview/gate/gate'; -import GateConditions from '../../../src/main/js/apps/overview/gate/gate-conditions'; -import GateCondition from '../../../src/main/js/apps/overview/gate/gate-condition'; - -describe('Overview', function () { - - describe('Quality Gate', function () { - it('should display a badge', function () { - let output = TestUtils.renderIntoDocument(<Gate gate={{ level: 'ERROR', conditions: [] }} component={{ }}/>); - TestUtils.findRenderedDOMComponentWithClass(output, 'badge-error'); - }); - - it('should not be displayed', function () { - let output = TestUtils.renderIntoDocument(<Gate component={{ }}/>); - expect(TestUtils.scryRenderedDOMComponentsWithClass(output, 'overview-gate')).to.be.empty; - }); - - it('should display empty gate', function () { - let output = TestUtils.renderIntoDocument(<Gate component={{ qualifier: 'TRK' }}/>); - TestUtils.findRenderedDOMComponentWithClass(output, 'overview-gate'); - TestUtils.findRenderedDOMComponentWithClass(output, 'overview-gate-warning'); - }); - - it('should filter out passed conditions', function () { - const conditions = [ - { level: 'OK' }, - { level: 'ERROR', metric: { name: 'error metric' } }, - { level: 'WARN', metric: { name: 'warn metric' } }, - { level: 'OK' } - ]; - - let renderer = TestUtils.createRenderer(); - renderer.render(<GateConditions gate={{ conditions }} component={{}}/>); - let output = renderer.getRenderOutput(); - expect(output.props.children).to.have.length(2); - }); - }); - - - describe('Helpers', function () { - describe('Periods', function () { - - }); - }); - -}); diff --git a/server/sonar-web/tests/mocha.opts b/server/sonar-web/tests/mocha.opts index c33427ae69d..083265fe8a5 100644 --- a/server/sonar-web/tests/mocha.opts +++ b/server/sonar-web/tests/mocha.opts @@ -1,3 +1,3 @@ --recursive ---compilers js:babel-register +--compilers js:babel-register,css:tests/null-compiler.js --require tests/jsdom-setup.js diff --git a/server/sonar-web/src/main/js/apps/overview/components/legend.js b/server/sonar-web/tests/null-compiler.js index 408b827ac9f..f0167414d7a 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/legend.js +++ b/server/sonar-web/tests/null-compiler.js @@ -17,14 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +function nothing () { + return null; +} -import { DomainLeakTitle } from '../main/components'; - -export const Legend = React.createClass({ - render() { - return <div className="overview-legend overview-leak"> - <DomainLeakTitle label={this.props.leakPeriodLabel} date={this.props.leakPeriodDate}/> - </div>; - } -}); +require.extensions['.css'] = nothing; |