diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-10-28 10:25:13 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-10-30 10:46:02 +0100 |
commit | 1e5830fb652fda2da6f18460c92f82d93c52130c (patch) | |
tree | 8383c08cc17ed71555b1cd8468292968f9bf07f1 /server/sonar-web/src | |
parent | a3528799883487e180ce90985e96cf87281e645f (diff) | |
download | sonarqube-1e5830fb652fda2da6f18460c92f82d93c52130c.tar.gz sonarqube-1e5830fb652fda2da6f18460c92f82d93c52130c.zip |
SONAR-6331 improve UX of the project overview page
Diffstat (limited to 'server/sonar-web/src')
54 files changed, 1029 insertions, 1197 deletions
diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js index 5d2259e6855..76a54463b7d 100644 --- a/server/sonar-web/src/main/js/api/issues.js +++ b/server/sonar-web/src/main/js/api/issues.js @@ -1,7 +1,9 @@ import _ from 'underscore'; + import { getJSON } from '../helpers/request.js'; -function getFacet (query, facet) { + +export function getFacet (query, facet) { let url = baseUrl + '/api/issues/search'; let data = _.extend({}, query, { facets: facet, ps: 1, additionalFields: '_all' }); return getJSON(url, data).then(r => { @@ -9,14 +11,17 @@ function getFacet (query, facet) { }); } + export function getSeverities (query) { return getFacet(query, 'severities').then(r => r.facet); } + export function getTags (query) { return getFacet(query, 'tags').then(r => r.facet); } + export function getAssignees (query) { return getFacet(query, 'assignees').then(r => { return r.facet.map(item => { @@ -25,3 +30,12 @@ export function getAssignees (query) { }); }); } + + +export function getIssuesCount (query) { + let url = baseUrl + '/api/issues/search'; + let data = _.extend({}, query, { ps: 1, facetMode: 'debt' }); + return getJSON(url, data).then(r => { + return { issues: r.total, debt: r.debtTotal }; + }); +} diff --git a/server/sonar-web/src/main/js/api/measures.js b/server/sonar-web/src/main/js/api/measures.js index e416e3a8882..0916c4998c1 100644 --- a/server/sonar-web/src/main/js/api/measures.js +++ b/server/sonar-web/src/main/js/api/measures.js @@ -1,5 +1,6 @@ import { getJSON } from '../helpers/request.js'; + export function getMeasures (componentKey, metrics) { let url = baseUrl + '/api/resources/index'; let data = { resource: componentKey, metrics: metrics.join(',') }; @@ -12,3 +13,22 @@ export function getMeasures (componentKey, metrics) { return measures; }); } + + +export function getMeasuresAndVariations (componentKey, metrics) { + let url = baseUrl + '/api/resources/index'; + let data = { resource: componentKey, metrics: metrics.join(','), includetrends: 'true' }; + return getJSON(url, data).then(r => { + let msr = r[0].msr || []; + let measures = {}; + msr.forEach(measure => { + measures[measure.key] = { + value: measure.val != null ? measure.val : measure.data, + var1: measure.var1, + var2: measure.var2, + var3: measure.var3 + }; + }); + return measures; + }); +} 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 f2912244670..418784e1405 100644 --- a/server/sonar-web/src/main/js/apps/overview/app.js +++ b/server/sonar-web/src/main/js/apps/overview/app.js @@ -2,15 +2,26 @@ import $ from 'jquery'; import _ from 'underscore'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Overview } from './main'; + +import { Overview, EmptyOverview } from './overview'; + + +const LEAK_PERIOD = '1'; + class App { start (options) { let opts = _.extend({}, options, window.sonarqube.overview); _.extend(opts.component, options.component); + $('html').toggleClass('dashboard-page', opts.component.hasSnapshot); let el = document.querySelector(opts.el); - ReactDOM.render(<Overview {...opts}/>, el); + + if (opts.component.hasSnapshot) { + ReactDOM.render(<Overview {...opts} leakPeriodIndex={LEAK_PERIOD}/>, el); + } else { + ReactDOM.render(<EmptyOverview/>, el); + } } } diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/main.js b/server/sonar-web/src/main/js/apps/overview/coverage/main.js index 084a7f94ea0..a4e8f90fa37 100644 --- a/server/sonar-web/src/main/js/apps/overview/coverage/main.js +++ b/server/sonar-web/src/main/js/apps/overview/coverage/main.js @@ -9,7 +9,14 @@ import { CoverageTreemap } from './treemap'; export default class extends React.Component { render () { - return <div className="overview-domain"> + return <div className="overview-detailed-page"> + <div className="overview-domain-header"> + <h2 className="overview-title">Coverage & Tests</h2> + </div> + + <a className="overview-detailed-page-back" href="#"> + <i className="icon-chevron-left"/> + </a> <CoverageTimeline {...this.props}/> diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/main.js b/server/sonar-web/src/main/js/apps/overview/duplications/main.js index a029f6ebb32..02aa9eceecc 100644 --- a/server/sonar-web/src/main/js/apps/overview/duplications/main.js +++ b/server/sonar-web/src/main/js/apps/overview/duplications/main.js @@ -8,7 +8,15 @@ import { DuplicationsTreemap } from './treemap'; export default class extends React.Component { render () { - return <div className="overview-domain"> + return <div className="overview-detailed-page"> + <div className="overview-domain-header"> + <h2 className="overview-title">Duplications</h2> + </div> + + <a className="overview-detailed-page-back" href="#"> + <i className="icon-chevron-left"/> + </a> + <DuplicationsTimeline {...this.props}/> <div className="flex-columns"> <div className="flex-column flex-column-half"> diff --git a/server/sonar-web/src/main/js/apps/overview/formatting.js b/server/sonar-web/src/main/js/apps/overview/formatting.js deleted file mode 100644 index 7f05531226c..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/formatting.js +++ /dev/null @@ -1,78 +0,0 @@ -const METRIC_TYPES = { - 'violations': 'SHORT_INT', - 'blocker_violations': 'SHORT_INT', - 'critical_violations': 'SHORT_INT', - 'major_violations': 'SHORT_INT', - 'minor_violations': 'SHORT_INT', - 'info_violations': 'SHORT_INT', - 'confirmed_issues': 'SHORT_INT', - 'false_positive_issues': 'SHORT_INT', - 'open_issues': 'SHORT_INT', - 'reopened_issues': 'SHORT_INT', - 'sqale_index': 'SHORT_WORK_DUR', - 'sqale_debt_ratio': 'PERCENT', - 'sqale_rating': 'RATING', - - 'coverage': 'PERCENT', - 'line_coverage': 'PERCENT', - 'branch_coverage': 'PERCENT', - 'lines_to_cover': 'SHORT_INT', - 'conditions_to_cover': 'SHORT_INT', - 'uncovered_lines': 'SHORT_INT', - 'uncovered_conditions': 'SHORT_INT', - - 'it_coverage': 'PERCENT', - 'it_line_coverage': 'PERCENT', - 'it_branch_coverage': 'PERCENT', - 'it_lines_to_cover': 'SHORT_INT', - 'it_conditions_to_cover': 'SHORT_INT', - 'it_uncovered_lines': 'SHORT_INT', - 'it_uncovered_conditions': 'SHORT_INT', - - 'overall_coverage': 'PERCENT', - 'overall_line_coverage': 'PERCENT', - 'overall_branch_coverage': 'PERCENT', - 'overall_lines_to_cover': 'SHORT_INT', - 'overall_conditions_to_cover': 'SHORT_INT', - 'overall_uncovered_lines': 'SHORT_INT', - 'overall_uncovered_conditions': 'SHORT_INT', - - 'tests': 'SHORT_INT', - 'skipped_tests': 'SHORT_INT', - 'test_errors': 'SHORT_INT', - 'test_failures': 'SHORT_INT', - 'test_execution_time': 'MILLISEC', - 'test_success_density': 'PERCENT', - - 'duplicated_blocks': 'INT', - 'duplicated_files': 'INT', - 'duplicated_lines': 'INT', - 'duplicated_lines_density': 'PERCENT', - - 'ncloc': 'SHORT_INT', - 'classes': 'SHORT_INT', - 'lines': 'SHORT_INT', - 'generated_ncloc': 'SHORT_INT', - 'generated_lines': 'SHORT_INT', - 'directories': 'SHORT_INT', - 'files': 'SHORT_INT', - 'functions': 'SHORT_INT', - 'statements': 'SHORT_INT', - 'public_api': 'SHORT_INT', - - 'complexity': 'SHORT_INT', - 'class_complexity': 'SHORT_INT', - 'file_complexity': 'SHORT_INT', - 'function_complexity': 'SHORT_INT', - - 'comment_lines_density': 'PERCENT', - 'comment_lines': 'SHORT_INT', - 'commented_out_code_lines': 'SHORT_INT', - 'public_documented_api_density': 'PERCENT', - 'public_undocumented_api': 'SHORT_INT' -}; - -export function formatMeasure (value, metric) { - let type = METRIC_TYPES[metric]; - return type ? window.formatMeasure(value, type) : value; -} 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 new file mode 100644 index 00000000000..5699a79aeeb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js @@ -0,0 +1,41 @@ +import React from 'react'; + +import Measure from './../helpers/measure'; +import { getPeriodLabel, getPeriodDate } from './../helpers/period-label'; +import DrilldownLink from './../helpers/drilldown-link'; + + +export default React.createClass({ + render() { + let metricName = window.t('metric', this.props.condition.metric.name, 'name'), + threshold = this.props.condition.level === 'ERROR' ? + this.props.condition.error : this.props.condition.warning, + period = this.props.condition.period ? + getPeriodLabel(this.props.component.periods, this.props.condition.period) : null, + periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period); + + let 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>{window.t('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-conditions.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js new file mode 100644 index 00000000000..adefecbded4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js @@ -0,0 +1,16 @@ +import React from 'react'; +import GateCondition from './gate-condition'; + +export default React.createClass({ + propTypes: { + gate: React.PropTypes.object.isRequired, + component: React.PropTypes.object.isRequired + }, + + render() { + let 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>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js index fb74bfa8a12..61347185593 100644 --- a/server/sonar-web/src/main/js/apps/overview/general/gate-empty.js +++ b/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js @@ -7,7 +7,7 @@ export default React.createClass({ return ( <div className="overview-gate"> <h2 className="overview-title">{window.t('overview.quality_gate')}</h2> - <p className="overview-paragraph big-spacer-top"> + <p className="overview-gate-warning"> You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p> </div> ); diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate.js b/server/sonar-web/src/main/js/apps/overview/gate/gate.js index 2436d2d468e..076cbcd376a 100644 --- a/server/sonar-web/src/main/js/apps/overview/general/gate.js +++ b/server/sonar-web/src/main/js/apps/overview/gate/gate.js @@ -1,15 +1,17 @@ import React from 'react'; + import GateConditions from './gate-conditions'; import GateEmpty from './gate-empty'; + export default React.createClass({ render() { if (!this.props.gate || !this.props.gate.level) { return this.props.component.qualifier === 'TRK' ? <GateEmpty/> : null; } - let - badgeClassName = 'badge badge-' + this.props.gate.level.toLowerCase(), + let level = this.props.gate.level.toLowerCase(), + badgeClassName = 'badge badge-' + level, badgeText = window.t('overview.gate', this.props.gate.level); return ( diff --git a/server/sonar-web/src/main/js/apps/overview/general/card.js b/server/sonar-web/src/main/js/apps/overview/general/card.js deleted file mode 100644 index 4d5eb06adf6..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/card.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - - -export default React.createClass({ - render() { - return <li className="overview-card">{this.props.children}</li>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/cards.js b/server/sonar-web/src/main/js/apps/overview/general/cards.js deleted file mode 100644 index 3d69cf8bf3a..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/cards.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -export default React.createClass({ - render() { - return <ul className="overview-cards">{this.props.children}</ul>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/components.js b/server/sonar-web/src/main/js/apps/overview/general/components.js new file mode 100644 index 00000000000..441f6d12e01 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/general/components.js @@ -0,0 +1,126 @@ +import moment from 'moment'; +import React from 'react'; + +import { Timeline } from './timeline'; + + +export const Domain = React.createClass({ + render () { + return <div className="overview-domain">{this.props.children}</div>; + } +}); + + +export const DomainTitle = React.createClass({ + render () { + return <div className="overview-title">{this.props.children}</div>; + } +}); + + +export const DomainLeakTitle = React.createClass({ + render() { + if (!this.props.label || !this.props.date) { + return null; + } + let momentDate = moment(this.props.date); + let fromNow = momentDate.fromNow(); + let tooltip = 'Started ' + fromNow + ', ' + momentDate.format('LLL'); + return <span title={tooltip} data-toggle="tooltip">Water Leak: {this.props.label}</span>; + } +}); + + +export const DomainHeader = React.createClass({ + render () { + return <div className="overview-domain-header"> + <DomainTitle>{this.props.title}</DomainTitle> + <DomainLeakTitle label={this.props.leakPeriodLabel} date={this.props.leakPeriodDate}/> + </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 = { + renderTimeline(range) { + if (!this.props.history) { + return null; + } + let props = { history: this.props.history }; + props[range] = this.props.leakPeriodDate; + return <div className="overview-domain-timeline"> + <Timeline {...props}/> + </div>; + }, + + hasLeakPeriod () { + return this.props.leakPeriodDate != null; + } +}; diff --git a/server/sonar-web/src/main/js/apps/overview/general/coverage.js b/server/sonar-web/src/main/js/apps/overview/general/coverage.js new file mode 100644 index 00000000000..bec2f468bf1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/general/coverage.js @@ -0,0 +1,64 @@ +import React from 'react'; + +import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components'; +import DrilldownLink from '../helpers/drilldown-link'; +import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; +import { getMetricName } from '../helpers/metrics'; + + +export const GeneralCoverage = React.createClass({ + mixins: [TooltipsMixin, DomainMixin], + + propTypes: { + measures: React.PropTypes.object.isRequired, + leakPeriodLabel: React.PropTypes.string, + leakPeriodDate: React.PropTypes.object + }, + + renderLeak () { + if (!this.hasLeakPeriod()) { + return null; + } + + return <DomainLeak> + <MeasuresList> + <Measure label={getMetricName('new_coverage')}> + <DrilldownLink component={this.props.component.key} metric="new_overall_coverage" period="1"> + {window.formatMeasure(this.props.leak['new_overall_coverage'], 'PERCENT')} + </DrilldownLink> + </Measure> + </MeasuresList> + {this.renderTimeline('after')} + </DomainLeak>; + }, + + render () { + if (this.props.measures['overall_coverage'] == null) { + return null; + } + + return <Domain> + <DomainHeader title="Tests" + leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/> + + <DomainPanel domain="coverage"> + <DomainNutshell> + <MeasuresList> + <Measure label={getMetricName('coverage')}> + <DrilldownLink component={this.props.component.key} metric="overall_coverage"> + {window.formatMeasure(this.props.measures['overall_coverage'], 'PERCENT')} + </DrilldownLink> + </Measure> + <Measure label={getMetricName('tests')}> + <DrilldownLink component={this.props.component.key} metric="tests"> + {window.formatMeasure(this.props.measures['tests'], 'SHORT_INT')} + </DrilldownLink> + </Measure> + </MeasuresList> + {this.renderTimeline('before')} + </DomainNutshell> + {this.renderLeak()} + </DomainPanel> + </Domain>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/details-link.js b/server/sonar-web/src/main/js/apps/overview/general/details-link.js deleted file mode 100644 index dca106dd3dd..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/details-link.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; - -export default React.createClass({ - handleClick(e) { - e.preventDefault(); - this.props.onRoute(this.props.linkTo); - }, - - render() { - let classes = classNames('overview-card', 'overview-card-section', { - 'active': this.props.active - }); - return <li className={classes}> - <a onClick={this.handleClick}>{window.t('overview.domain', this.props.linkTo)}</a> - </li>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/details.js b/server/sonar-web/src/main/js/apps/overview/general/details.js deleted file mode 100644 index f5ee24bc4bb..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/details.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import Cards from './cards'; -import DetailsLink from './details-link'; - - -function checkMeasureForDomain (domain, measures) { - if (domain === 'coverage' && measures.coverage == null) { - return false; - } - if (domain === 'duplications' && measures.duplications == null) { - return false; - } - return true; -} - - -export default React.createClass({ - render() { - let domains = ['issues', 'coverage', 'duplications', 'size'].map(domain => { - if (!checkMeasureForDomain(domain, this.props.measures)) { - return null; - } - let active = domain === this.props.section; - return <DetailsLink key={domain} linkTo={domain} onRoute={this.props.onRoute} active={active}/>; - }); - - return ( - <div className="overview-more"> - <h2 className="overview-title">More Details</h2> - <Cards>{domains}</Cards> - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/duplications.js b/server/sonar-web/src/main/js/apps/overview/general/duplications.js new file mode 100644 index 00000000000..d316eea8ba6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/general/duplications.js @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components'; +import DrilldownLink from '../helpers/drilldown-link'; +import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; +import { getMetricName } from '../helpers/metrics'; + + +export const GeneralDuplications = React.createClass({ + mixins: [TooltipsMixin, DomainMixin], + + propTypes: { + leakPeriodLabel: React.PropTypes.string, + leakPeriodDate: React.PropTypes.object + }, + + renderLeak () { + if (!this.hasLeakPeriod()) { + return null; + } + return <DomainLeak> + <MeasuresList> + <Measure label={getMetricName('duplications')}> + {window.formatMeasureVariation(this.props.leak['duplicated_lines_density'], 'PERCENT')} + </Measure> + </MeasuresList> + {this.renderTimeline('after')} + </DomainLeak>; + }, + + render () { + return <Domain> + <DomainHeader title="Duplications" + leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/> + + <DomainPanel domain="duplications"> + <DomainNutshell> + <MeasuresList> + <Measure label={getMetricName('duplications')}> + <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density"> + {window.formatMeasure(this.props.measures['duplicated_lines_density'], 'PERCENT')} + </DrilldownLink> + </Measure> + <Measure label={getMetricName('duplicated_blocks')}> + <DrilldownLink component={this.props.component.key} metric="duplicated_blocks"> + {window.formatMeasure(this.props.measures['duplicated_blocks'], 'SHORT_INT')} + </DrilldownLink> + </Measure> + </MeasuresList> + {this.renderTimeline('before')} + </DomainNutshell> + {this.renderLeak()} + </DomainPanel> + </Domain>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/empty.js b/server/sonar-web/src/main/js/apps/overview/general/empty.js deleted file mode 100644 index 78c5320a1f8..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/empty.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -export default React.createClass({ - render() { - return ( - <div className="panel"> - <div className="alert alert-warning"> - {window.t('provisioning.no_analysis')} - </div> - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js deleted file mode 100644 index 32727972d03..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -import Measure from './../helpers/measure'; -import { periodLabel, getPeriodDate } from './../helpers/period-label'; -import DrilldownLink from './../helpers/drilldown-link'; - - -export default React.createClass({ - render() { - let metricName = window.t('metric', this.props.condition.metric.name, 'name'), - threshold = this.props.condition.level === 'ERROR' ? - this.props.condition.error : this.props.condition.warning, - period = this.props.condition.period ? - `(${periodLabel(this.props.component.periods, this.props.condition.period)})` : null, - periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period); - - let classes = 'alert_' + this.props.condition.level.toUpperCase(); - - return ( - <div> - <h4 className="overview-gate-condition-metric">{metricName}<br/><span className="nowrap">{period}</span></h4> - <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> - <span className="overview-gate-condition-itself"> - {window.t('quality_gates.operator', this.props.condition.op, 'short')} - <Measure value={threshold} type={this.props.condition.metric.type}/> - </span> - </div> - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js deleted file mode 100644 index 65b92e42a71..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import Cards from './cards'; -import Card from './card'; -import GateCondition from './gate-condition'; - -export default React.createClass({ - render() { - let conditions = this.props.gate.conditions - .filter((c) => { - return c.level !== 'OK'; - }) - .map((c) => { - return ( - <Card key={c.metric.name}> - <GateCondition condition={c} component={this.props.component}/> - </Card> - ); - }); - return <Cards>{conditions}</Cards>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/issues.js b/server/sonar-web/src/main/js/apps/overview/general/issues.js new file mode 100644 index 00000000000..b97b0db57f9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/general/issues.js @@ -0,0 +1,129 @@ +import moment from 'moment'; +import React from 'react'; + +import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components'; +import Rating from './../helpers/rating'; +import IssuesLink from '../helpers/issues-link'; +import DrilldownLink from '../helpers/drilldown-link'; +import SeverityHelper from '../../../components/shared/severity-helper'; +import SeverityIcon from '../../../components/shared/severity-icon'; +import StatusIcon from '../../../components/shared/status-icon'; +import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; +import { getMetricName } from '../helpers/metrics'; +import { SEVERITIES } from '../../../helpers/constants'; + + +export const GeneralIssues = React.createClass({ + mixins: [TooltipsMixin, DomainMixin], + + propTypes: { + leakPeriodLabel: React.PropTypes.string, + leakPeriodDate: React.PropTypes.object + }, + + renderSeverities() { + let severities = SEVERITIES.map((s, index) => { + let measure = this.props.measures.issuesSeverities[index]; + return <tr key={s}> + <td> + <SeverityHelper severity={s}/> + </td> + <td className="thin nowrap text-right"> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: s }}> + {window.formatMeasure(measure, 'SHORT_INT')} + </IssuesLink> + </td> + </tr>; + }); + + return <div style={{ width: 120 }}> + <table className="data"> + <tbody>{severities}</tbody> + </table> + </div>; + }, + + renderLeak () { + if (!this.hasLeakPeriod()) { + return null; + } + + let createdAfter = moment(this.props.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ'); + + return <DomainLeak> + <MeasuresList> + <Measure label={getMetricName('new_issues')}> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: createdAfter }}> + {window.formatMeasureVariation(this.props.leak.issues, 'SHORT_INT')} + </IssuesLink> + </Measure> + <Measure label={getMetricName('new_debt')}> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: createdAfter, facetMode: 'debt' }}> + {window.formatMeasureVariation(this.props.leak.debt, 'SHORT_WORK_DUR')} + </IssuesLink> + </Measure> + </MeasuresList> + <MeasuresList> + <Measure label={getMetricName('new_blocker_issues')}> + <span className="spacer-right"><SeverityIcon severity="BLOCKER"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', severities: 'BLOCKER', createdAfter: createdAfter }}> + {window.formatMeasureVariation(this.props.leak.issuesSeverities[0], 'SHORT_INT')} + </IssuesLink> + </Measure> + <Measure label={getMetricName('new_critical_issues')}> + <span className="spacer-right"><SeverityIcon severity="CRITICAL"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', severities: 'CRITICAL', createdAfter: createdAfter }}> + {window.formatMeasureVariation(this.props.leak.issuesSeverities[1], 'SHORT_INT')} + </IssuesLink> + </Measure> + <Measure label={getMetricName('new_open_issues')}> + <span className="spacer-right"><StatusIcon status="OPEN"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', statuses: 'OPEN,REOPENED', createdAfter: createdAfter }}> + {window.formatMeasureVariation(this.props.leak.issuesStatuses[0] + this.props.leak.issuesStatuses[1], + 'SHORT_INT')} + </IssuesLink> + </Measure> + </MeasuresList> + {this.renderTimeline('after')} + </DomainLeak>; + }, + + render () { + return <Domain> + <DomainHeader title="Technical Debt" + leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/> + + <DomainPanel domain="issues"> + <DomainNutshell> + <MeasuresList> + <Measure> + <DrilldownLink component={this.props.component.key} metric="sqale_rating"> + <Rating value={this.props.measures['sqale_rating']}/> + </DrilldownLink> + </Measure> + <Measure label={getMetricName('issues')}> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}> + {window.formatMeasure(this.props.measures.issues, 'SHORT_INT')} + </IssuesLink> + </Measure> + <Measure label={getMetricName('debt')}> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}> + {window.formatMeasure(this.props.measures.debt, 'SHORT_WORK_DUR')} + </IssuesLink> + </Measure> + <Measure composite={true}> + {this.renderSeverities()} + </Measure> + </MeasuresList> + {this.renderTimeline('before')} + </DomainNutshell> + {this.renderLeak()} + </DomainPanel> + </Domain>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js b/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js deleted file mode 100644 index 23e3fabd7e1..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import Card from './card'; -import Measure from './../helpers/measure'; -import MeasureVariation from './../helpers/measure-variation'; -import DrilldownLink from './../helpers/drilldown-link'; -import Donut from './../helpers/donut'; - -export default React.createClass({ - render() { - let - newCoverage = parseInt(this.props.leak.newCoverage, 10), - tests = this.props.leak.tests, - donutData = [ - { value: newCoverage, fill: '#85bb43' }, - { value: 100 - newCoverage, fill: '#d4333f' } - ]; - - if (newCoverage == null || isNaN(newCoverage)) { - return null; - } - - return ( - <Card> - <div className="measures"> - <div className="measures-chart"> - <Donut data={donutData} size="47"/> - </div> - <div className="measure measure-big" data-metric="new_coverage"> - <span className="measure-name">{window.t('overview.metric.new_coverage')}</span> - <span className="measure-value"> - <DrilldownLink component={this.props.component.key} metric="new_coverage" period="1"> - <Measure value={newCoverage} type="PERCENT"/> - </DrilldownLink> - </span> - </div> - </div> - <ul className="list-inline big-spacer-top measures-chart-indent"> - <li> - <span><MeasureVariation value={tests} type="SHORT_INT"/></span> - <span>{window.t('overview.metric.tests')}</span> - </li> - </ul> - </Card> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js b/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js deleted file mode 100644 index f59e996c967..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import Card from './card'; -import MeasureVariation from './../helpers/measure-variation'; -import Donut from './../helpers/donut'; - -export default React.createClass({ - render() { - let - density = this.props.leak.duplications, - lines = this.props.leak.duplicatedLines, - donutData = [ - { value: density, fill: '#f3ca8e' }, - { value: 100 - density, fill: '#e6e6e6' } - ]; - - if (density == null) { - return null; - } - - return ( - <Card> - <div className="measures"> - <div className="measures-chart"> - <Donut data={donutData} size="47"/> - </div> - <div className="measure measure-big" data-metric="duplicated_lines_density"> - <span className="measure-name">{window.t('overview.metric.duplications')}</span> - <span className="measure-value"> - <MeasureVariation value={density} type="PERCENT"/> - </span> - </div> - </div> - <ul className="list-inline big-spacer-top measures-chart-indent"> - <li> - <span><MeasureVariation value={lines} type="SHORT_INT"/></span> - <span>{window.t('overview.metric.duplicated_lines')}</span> - </li> - </ul> - </Card> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js b/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js deleted file mode 100644 index 29574509109..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js +++ /dev/null @@ -1,69 +0,0 @@ -import moment from 'moment'; -import React from 'react'; -import Card from './card'; -import Measure from './../helpers/measure'; -import MeasureVariation from './../helpers/measure-variation'; -import IssuesLink from './../helpers/issues-link'; -import SeverityIcon from '../../../components/shared/severity-icon'; -import StatusIcon from '../../../components/shared/status-icon'; -import {getPeriodDate} from './../helpers/period-label'; - -export default React.createClass({ - render() { - let - newDebt = this.props.leak.newDebt, - issues = this.props.leak.newIssues, - blockerIssues = this.props.leak.newBlockerIssues, - criticalIssues = this.props.leak.newCriticalIssues, - issuesToReview = this.props.leak.newOpenIssues + this.props.leak.newReopenedIssues, - periodDate = moment(getPeriodDate(this.props.component.periods, '1')).format('YYYY-MM-DDTHH:mm:ssZZ'); - - return ( - <Card> - <div className="measures"> - <div className="measure measure-big" data-metric="sqale_index"> - <span className="measure-name">{window.t('overview.metric.new_debt')}</span> - <span className="measure-value"> - <IssuesLink component={this.props.component.key} - params={{ resolved: 'false', createdAfter: periodDate, facetMode: 'debt' }}> - <Measure value={newDebt} type="SHORT_WORK_DUR"/> - </IssuesLink> - </span> - </div> - <div className="measure measure-big" data-metric="violations"> - <span className="measure-name">{window.t('overview.metric.new_issues')}</span> - <span className="measure-value"> - <IssuesLink component={this.props.component.key} - params={{ resolved: 'false', createdAfter: periodDate }}> - <Measure value={issues} type="SHORT_INT"/> - </IssuesLink> - </span> - </div> - </div> - <ul className="list-inline big-spacer-top"> - <li> - <span><SeverityIcon severity="BLOCKER"/></span> - <IssuesLink component={this.props.component.key} - params={{ resolved: 'false', createdAfter: periodDate, severities: 'BLOCKER' }}> - <MeasureVariation value={blockerIssues} type="SHORT_INT"/> - </IssuesLink> - </li> - <li> - <span><SeverityIcon severity="CRITICAL"/></span> - <IssuesLink component={this.props.component.key} - params={{ resolved: 'false', createdAfter: periodDate, severities: 'CRITICAL' }}> - <MeasureVariation value={criticalIssues} type="SHORT_INT"/> - </IssuesLink> - </li> - <li> - <span><StatusIcon status="OPEN"/></span> - <IssuesLink component={this.props.component.key} - params={{ resolved: 'false', createdAfter: periodDate, statuses: 'OPEN,REOPENED' }}> - <MeasureVariation value={issuesToReview} type="SHORT_INT"/> - </IssuesLink> - </li> - </ul> - </Card> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-size.js b/server/sonar-web/src/main/js/apps/overview/general/leak-size.js deleted file mode 100644 index 5b09ddf0eed..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/leak-size.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import Card from './card'; -import MeasureVariation from './../helpers/measure-variation'; - -export default React.createClass({ - render() { - let - lines = this.props.leak.lines, - files = this.props.leak.files; - - return ( - <Card> - <div className="measures"> - <div className="measure measure-big" data-metric="lines"> - <span className="measure-name">{window.t('overview.metric.lines')}</span> - <span className="measure-value"> - <MeasureVariation value={lines} type="SHORT_INT"/> - </span> - </div> - <div className="measure measure-big" data-metric="files"> - <span className="measure-name">{window.t('overview.metric.files')}</span> - <span className="measure-value"> - <MeasureVariation value={files} type="SHORT_INT"/> - </span> - </div> - </div> - </Card> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak.js b/server/sonar-web/src/main/js/apps/overview/general/leak.js deleted file mode 100644 index 1cef6139954..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/leak.js +++ /dev/null @@ -1,37 +0,0 @@ -import _ from 'underscore'; -import moment from 'moment'; -import React from 'react'; - -import Cards from './cards'; -import LeakIssues from './leak-issues'; -import LeakCoverage from './leak-coverage'; -import LeakSize from './leak-size'; -import LeakDups from './leak-dups'; -import { periodLabel, getPeriodDate } from './../helpers/period-label'; - - -export default React.createClass({ - render() { - if (_.size(this.props.component.periods) < 1) { - return null; - } - - let period = periodLabel(this.props.component.periods, '1'); - let periodDate = getPeriodDate(this.props.component.periods, '1'); - - return ( - <div className="overview-leak"> - <h2 className="overview-title"> - {window.t('overview.water_leak')} - <span className="overview-leak-period">{period} / {moment(periodDate).format('LL')}</span> - </h2> - <Cards> - <LeakIssues component={this.props.component} leak={this.props.leak} measures={this.props.measures}/> - <LeakCoverage component={this.props.component} leak={this.props.leak}/> - <LeakDups component={this.props.component} leak={this.props.leak}/> - <LeakSize component={this.props.component} leak={this.props.leak}/> - </Cards> - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/main.js b/server/sonar-web/src/main/js/apps/overview/general/main.js index 233e111063e..dd0d748788c 100644 --- a/server/sonar-web/src/main/js/apps/overview/general/main.js +++ b/server/sonar-web/src/main/js/apps/overview/general/main.js @@ -1,99 +1,189 @@ -import $ from 'jquery'; import _ from 'underscore'; import moment from 'moment'; import React from 'react'; -import Gate from './gate'; -import Leak from './leak'; -import Nutshell from './nutshell'; -import MoreDetails from './details'; -import { getPeriodDate } from './../helpers/period-label'; + +import { GeneralIssues } from './issues'; +import { GeneralCoverage } from './coverage'; +import { GeneralDuplications } from './duplications'; +import { GeneralSize } from './size'; +import { getPeriodLabel, getPeriodDate } from './../helpers/period-label'; +import { getMeasuresAndVariations } from '../../../api/measures'; +import { getFacet, getIssuesCount } from '../../../api/issues'; +import { getTimeMachineData } from '../../../api/time-machine'; +import { SEVERITIES, STATUSES } from '../../../helpers/constants'; + + +const METRICS_LIST = [ + 'sqale_rating', + 'overall_coverage', + 'new_overall_coverage', + 'tests', + 'duplicated_lines_density', + 'duplicated_blocks', + 'ncloc', + 'files' +]; + +const HISTORY_METRICS_LIST = [ + 'violations', + 'overall_coverage', + 'duplicated_lines_density', + 'ncloc' +]; + + +function getFacetValue (facet, key) { + return _.findWhere(facet, { val: key }).count; +} + export default React.createClass({ + propTypes: { + leakPeriodIndex: React.PropTypes.string.isRequired + }, + getInitialState() { - return { leak: this.props.leak, measures: this.props.measures }; + return { + ready: false, + history: {}, + leakPeriodLabel: getPeriodLabel(this.props.component.periods, this.props.leakPeriodIndex), + leakPeriodDate: getPeriodDate(this.props.component.periods, this.props.leakPeriodIndex) + }; }, componentDidMount() { - if (this._hasWaterLeak()) { - this.requestLeakIssues(); - this.requestLeakDebt(); - } - this.requestNutshellIssues(); - this.requestNutshellDebt(); - }, + Promise.all([ + this.requestMeasures(), + this.requestIssuesAndDebt(), + this.requestIssuesSeverities(), + this.requestLeakIssuesAndDebt(), + this.requestIssuesLeakSeverities(), + this.requestIssuesLeakStatuses() + ]).then(responses => { + let measures = this.getMeasuresValues(responses[0], 'value'); + measures.issues = responses[1].issues; + measures.debt = responses[1].debt; + measures.issuesSeverities = SEVERITIES.map(s => getFacetValue(responses[2].facet, s)); - _hasWaterLeak() { - return !!_.findWhere(this.props.component.periods, { index: '1' }); + let leak; + if (this.state.leakPeriodLabel) { + leak = this.getMeasuresValues(responses[0], 'var' + this.props.leakPeriodIndex); + leak.issues = responses[3].issues; + leak.debt = responses[3].debt; + leak.issuesSeverities = SEVERITIES.map(s => getFacetValue(responses[4].facet, s)); + leak.issuesStatuses = STATUSES.map(s => getFacetValue(responses[5].facet, s)); + } + + this.setState({ + ready: true, + measures: measures, + leak: leak + }, this.requestHistory); + }); }, - _requestIssues(data) { - let url = `${baseUrl}/api/issues/search`; - data.ps = 1; - data.componentUuids = this.props.component.id; - return $.get(url, data); + requestMeasures () { + return getMeasuresAndVariations(this.props.component.key, METRICS_LIST); }, - requestLeakIssues() { - let createdAfter = moment(getPeriodDate(this.props.component.periods, '1')).format('YYYY-MM-DDTHH:mm:ssZZ'); - this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities,statuses' }).done(r => { - let - severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values, - statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values; + getMeasuresValues (measures, fieldKey) { + let values = {}; + Object.keys(measures).forEach(measureKey => { + values[measureKey] = measures[measureKey][fieldKey]; + }); + return values; + }, - this.setState({ - leak: _.extend({}, this.state.leak, { - newIssues: r.total, - newBlockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count, - newCriticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count, - newOpenIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count, - newReopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count - }) - }); + requestIssuesAndDebt() { + // FIXME requesting severities facet only to get debtTotal + return getIssuesCount({ + componentUuids: this.props.component.id, + resolved: 'false', + facets: 'severities' }); }, - requestNutshellIssues() { - this._requestIssues({ resolved: 'false', facets: 'severities,statuses' }).done(r => { - let - severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values, - statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values; + requestLeakIssuesAndDebt() { + if (!this.state.leakPeriodLabel) { + return Promise.resolve(); + } - this.setState({ - measures: _.extend({}, this.state.measures, { - issues: r.total, - blockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count, - criticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count, - openIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count, - reopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count - }) - }); + let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ'); + + // FIXME requesting severities facet only to get debtTotal + return getIssuesCount({ + componentUuids: this.props.component.id, + createdAfter: createdAfter, + resolved: 'false', + facets: 'severities' }); }, - requestLeakDebt() { - let createdAfter = moment(getPeriodDate(this.props.component.periods, '1')).format('YYYY-MM-DDTHH:mm:ssZZ'); - this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities', facetMode: 'debt' }).done(r => { - this.setState({ - leak: _.extend({}, this.state.leak, { newDebt: r.debtTotal }) - }); - }); + requestIssuesSeverities() { + return getFacet({ componentUuids: this.props.component.id, resolved: 'false' }, 'severities'); }, - requestNutshellDebt() { - this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => { - this.setState({ - measures: _.extend({}, this.state.measures, { debt: r.debtTotal }) + requestIssuesLeakSeverities() { + if (!this.state.leakPeriodLabel) { + return Promise.resolve(); + } + + let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ'); + + return getFacet({ + componentUuids: this.props.component.id, + createdAfter: createdAfter, + resolved: 'false' + }, 'severities'); + }, + + requestIssuesLeakStatuses() { + if (!this.state.leakPeriodLabel) { + return Promise.resolve(); + } + + let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ'); + + return getFacet({ + componentUuids: this.props.component.id, + createdAfter: createdAfter, + resolved: 'false' + }, 'statuses'); + }, + + requestHistory () { + let metrics = HISTORY_METRICS_LIST.join(','); + return getTimeMachineData(this.props.component.key, metrics).then(r => { + let history = {}; + r[0].cols.forEach((col, index) => { + history[col.metric] = r[0].cells.map(cell => { + let date = moment(cell.d).toDate(); + let value = cell.v[index] || 0; + return { date, value }; + }); }); + this.setState({ history }); }); }, + renderLoading () { + return <div className="text-center"> + <i className="spinner spinner-margin"/> + </div>; + }, + render() { - return <div> - <Gate component={this.props.component} gate={this.props.gate}/> - <Leak component={this.props.component} leak={this.state.leak} measures={this.state.measures}/> - <Nutshell component={this.props.component} measures={this.state.measures} section={this.props.section}/> - <MoreDetails component={this.props.component} measures={this.state.measures} - section={this.props.section} onRoute={this.props.onRoute}/> + if (!this.state.ready) { + return this.renderLoading(); + } + + let props = _.extend({}, this.props, this.state); + + return <div className="overview-domains"> + <GeneralIssues {...props} history={this.state.history['violations']}/> + <GeneralCoverage {...props} history={this.state.history['overall_coverage']}/> + <GeneralDuplications {...props} history={this.state.history['duplicated_lines_density']}/> + <GeneralSize {...props} history={this.state.history['ncloc']}/> </div>; } }); diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js deleted file mode 100644 index dabcb76cc6b..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import Card from './card'; -import Measure from './../helpers/measure'; -import DrilldownLink from './../helpers/drilldown-link'; -import Donut from './../helpers/donut'; - -export default React.createClass({ - render() { - let - coverage = this.props.measures.coverage, - tests = this.props.measures.tests, - donutData = [ - { value: coverage, fill: '#85bb43' }, - { value: 100 - coverage, fill: '#d4333f' } - ]; - - if (coverage == null) { - return null; - } - - return ( - <Card> - <div className="measures"> - <div className="measures-chart"> - <Donut data={donutData} size="47"/> - </div> - <div className="measure measure-big"> - <span className="measure-name">{window.t('overview.metric.coverage')}</span> - <span className="measure-value"> - <DrilldownLink component={this.props.component.key} metric="overall_coverage"> - <Measure value={coverage} type="PERCENT"/> - </DrilldownLink> - </span> - </div> - </div> - <ul className="list-inline big-spacer-top measures-chart-indent"> - <li> - <DrilldownLink component={this.props.component.key} metric="tests"> - <Measure value={tests} type="SHORT_INT"/> - </DrilldownLink> - <span>{window.t('overview.metric.tests')}</span> - </li> - </ul> - </Card> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js deleted file mode 100644 index 6c4bb0b1c3a..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import Card from './card'; -import Measure from './../helpers/measure'; -import DrilldownLink from './../helpers/drilldown-link'; -import Donut from './../helpers/donut'; - -export default React.createClass({ - render() { - let - density = this.props.measures.duplications, - lines = this.props.measures.duplicatedLines, - donutData = [ - { value: density, fill: '#f3ca8e' }, - { value: 100 - density, fill: '#e6e6e6' } - ]; - - if (density == null) { - return null; - } - - return ( - <Card> - <div className="measures"> - <div className="measures-chart"> - <Donut data={donutData} size="47"/> - </div> - <div className="measure measure-big"> - <span className="measure-name">{window.t('overview.metric.duplications')}</span> - <span className="measure-value"> - <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density"> - <Measure value={density} type="PERCENT"/> - </DrilldownLink> - </span> - </div> - </div> - <ul className="list-inline big-spacer-top measures-chart-indent"> - <li> - <DrilldownLink component={this.props.component.key} metric="duplicated_lines"> - <Measure value={lines} type="SHORT_INT"/> - </DrilldownLink> - <span>{window.t('overview.metric.duplicated_lines')}</span> - </li> - </ul> - </Card> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js deleted file mode 100644 index 3cad21fdc41..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import Card from './card'; -import Measure from './../helpers/measure'; -import Rating from './../helpers/rating'; -import IssuesLink from './../helpers/issues-link'; -import DrilldownLink from './../helpers/drilldown-link'; -import SeverityIcon from '../../../components/shared/severity-icon'; -import StatusIcon from '../../../components/shared/status-icon'; - -export default React.createClass({ - render() { - let - debt = this.props.measures.debt, - rating = this.props.measures.sqaleRating, - issues = this.props.measures.issues, - blockerIssues = this.props.measures.blockerIssues, - criticalIssues = this.props.measures.criticalIssues, - issuesToReview = this.props.measures.openIssues + this.props.measures.reopenedIssues; - - return ( - <Card> - <div className="measures"> - <div className="measure measure-big" data-metric="sqale_rating"> - <DrilldownLink component={this.props.component.key} metric="sqale_rating"> - <Rating value={rating}/> - </DrilldownLink> - </div> - <div className="measure measure-big" data-metric="sqale_index"> - <span className="measure-name">{window.t('overview.metric.debt')}</span> - <span className="measure-value"> - <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}> - <Measure value={debt} type="SHORT_WORK_DUR"/> - </IssuesLink> - </span> - </div> - <div className="measure measure-big" data-metric="violations"> - <span className="measure-name">{window.t('overview.metric.issues')}</span> - <span className="measure-value"> - <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}> - <Measure value={issues} type="SHORT_INT"/> - </IssuesLink> - </span> - </div> - </div> - <ul className="list-inline big-spacer-top"> - <li> - <span><SeverityIcon severity="BLOCKER"/></span> - <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'BLOCKER' }}> - <Measure value={blockerIssues} type="SHORT_INT"/> - </IssuesLink> - </li> - <li> - <span><SeverityIcon severity="CRITICAL"/></span> - <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'CRITICAL' }}> - <Measure value={criticalIssues} type="SHORT_INT"/> - </IssuesLink> - </li> - <li> - <span><StatusIcon status="OPEN"/></span> - <IssuesLink component={this.props.component.key} params={{ resolved: 'false', statuses: 'OPEN,REOPENED' }}> - <Measure value={issuesToReview} type="SHORT_INT"/> - </IssuesLink> - </li> - </ul> - </Card> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js deleted file mode 100644 index 845088f9491..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import Card from './card'; -import Measure from './../helpers/measure'; -import DrilldownLink from './../helpers/drilldown-link'; - -export default React.createClass({ - render() { - let - lines = this.props.measures['lines'], - files = this.props.measures['files']; - - return ( - <Card> - <div className="measures"> - <div className="measure measure-big" data-metric="lines"> - <span className="measure-name">{window.t('overview.metric.lines')}</span> - <span className="measure-value"> - <DrilldownLink component={this.props.component.key} metric="lines"> - <Measure value={lines} type="SHORT_INT"/> - </DrilldownLink> - </span> - </div> - <div className="measure measure-big" data-metric="files"> - <span className="measure-name">{window.t('overview.metric.files')}</span> - <span className="measure-value"> - <DrilldownLink component={this.props.component.key} metric="files"> - <Measure value={files} type="SHORT_INT"/> - </DrilldownLink> - </span> - </div> - </div> - </Card> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell.js deleted file mode 100644 index e538149958e..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/general/nutshell.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import Cards from './cards'; -import NutshellIssues from './nutshell-issues'; -import NutshellCoverage from './nutshell-coverage'; -import NutshellSize from './nutshell-size'; -import NutshellDups from './nutshell-dups'; - -export default React.createClass({ - render() { - let props = { - measures: this.props.measures, - component: this.props.component - }; - return ( - <div className="overview-nutshell"> - <h2 className="overview-title">{window.t('overview.project_in_a_nutshell')}</h2> - <Cards> - <NutshellIssues {...props}/> - <NutshellCoverage {...props}/> - <NutshellDups {...props}/> - <NutshellSize {...props}/> - </Cards> - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/size.js b/server/sonar-web/src/main/js/apps/overview/general/size.js new file mode 100644 index 00000000000..ac43ab6b001 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/general/size.js @@ -0,0 +1,60 @@ +import React from 'react'; + +import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components'; +import DrilldownLink from '../helpers/drilldown-link'; +import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; +import { getMetricName } from '../helpers/metrics'; + + +export const GeneralSize = React.createClass({ + mixins: [TooltipsMixin, DomainMixin], + + propTypes: { + leakPeriodLabel: React.PropTypes.string, + leakPeriodDate: React.PropTypes.object + }, + + renderLeak () { + if (!this.hasLeakPeriod()) { + return null; + } + + return <DomainLeak> + <MeasuresList> + <Measure label={getMetricName('ncloc')}> + {window.formatMeasureVariation(this.props.leak['ncloc'], 'SHORT_INT')} + </Measure> + <Measure label={getMetricName('files')}> + {window.formatMeasureVariation(this.props.leak['files'], 'SHORT_INT')} + </Measure> + </MeasuresList> + {this.renderTimeline('after')} + </DomainLeak>; + }, + + render () { + return <Domain> + <DomainHeader title="Size" + leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/> + + <DomainPanel domain="size"> + <DomainNutshell> + <MeasuresList> + <Measure label={getMetricName('ncloc')}> + <DrilldownLink component={this.props.component.key} metric="ncloc"> + {window.formatMeasure(this.props.measures['ncloc'], 'SHORT_INT')} + </DrilldownLink> + </Measure> + <Measure label={getMetricName('files')}> + <DrilldownLink component={this.props.component.key} metric="files"> + {window.formatMeasure(this.props.measures['files'], 'SHORT_INT')} + </DrilldownLink> + </Measure> + </MeasuresList> + {this.renderTimeline('before')} + </DomainNutshell> + {this.renderLeak()} + </DomainPanel> + </Domain>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/general/timeline.js b/server/sonar-web/src/main/js/apps/overview/general/timeline.js new file mode 100644 index 00000000000..3938fff0330 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/general/timeline.js @@ -0,0 +1,47 @@ +import d3 from 'd3'; +import React from 'react'; + +import { LineChart } from '../../../components/charts/line-chart'; + + +const HEIGHT = 80; + + +export class Timeline extends React.Component { + filterSnapshots () { + return this.props.history.filter(s => { + let matchBefore = !this.props.before || s.date <= this.props.before; + let matchAfter = !this.props.after || s.date >= this.props.after; + return matchBefore && matchAfter; + }); + } + + render () { + let snapshots = this.filterSnapshots(); + + if (snapshots.length < 2) { + return null; + } + + let data = snapshots.map((snapshot, index) => { + return { x: index, y: snapshot.value }; + }); + + let 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]}/>; + } +} + +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/helpers/issues-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js index d3118579036..f1331395671 100644 --- a/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js +++ b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js @@ -5,7 +5,7 @@ export default React.createClass({ let params = Object.keys(this.props.params).map((key) => { return `${key}=${encodeURIComponent(this.props.params[key])}`; }).join('|'), - url = `${baseUrl}/component_issues/index?id=${encodeURIComponent(this.props.component)}#${params}`; + url = `${window.baseUrl}/component_issues/index?id=${encodeURIComponent(this.props.component)}#${params}`; return <a href={url}>{this.props.children}</a>; } }); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js b/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js index 1a1dcefcc47..f556841abe3 100644 --- a/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js +++ b/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js @@ -19,3 +19,8 @@ export function filterMetricsForDomains (metrics, domains) { return hasRightDomain(metric, domains) && isNotHidden(metric) && hasSimpleType(metric) && isNotDifferential(metric); }); } + + +export function getMetricName (metricKey) { + return window.t('overview.metric', metricKey); +} diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js index 109a9df9a57..07cfcd43b9f 100644 --- a/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js +++ b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import moment from 'moment'; -export let periodLabel = (periods, periodIndex) => { +export let getPeriodLabel = (periods, periodIndex) => { let period = _.findWhere(periods, { index: periodIndex }); if (!period) { return null; diff --git a/server/sonar-web/src/main/js/apps/overview/issues/assignees.js b/server/sonar-web/src/main/js/apps/overview/issues/assignees.js index af445cc981b..2d0905c8030 100644 --- a/server/sonar-web/src/main/js/apps/overview/issues/assignees.js +++ b/server/sonar-web/src/main/js/apps/overview/issues/assignees.js @@ -1,7 +1,6 @@ import React from 'react'; import Assignee from '../../../components/shared/assignee-helper'; import { DomainHeader } from '../domain/header'; -import { formatMeasure } from '../formatting'; import { componentIssuesUrl } from '../../../helpers/Url'; export default class extends React.Component { @@ -13,7 +12,7 @@ export default class extends React.Component { <Assignee user={s.user}/> </td> <td className="thin text-right"> - <a href={href}>{formatMeasure(s.count, 'violations')}</a> + <a href={href}>{window.formatMeasure(s.count, 'SHORT_INT')}</a> </td> </tr>; }); diff --git a/server/sonar-web/src/main/js/apps/overview/issues/main.js b/server/sonar-web/src/main/js/apps/overview/issues/main.js index e55a2f0cf2b..d615b8d44a3 100644 --- a/server/sonar-web/src/main/js/apps/overview/issues/main.js +++ b/server/sonar-web/src/main/js/apps/overview/issues/main.js @@ -18,7 +18,7 @@ export default class OverviewDomain extends React.Component { componentDidMount () { Promise.all([ - this.requestSeverities(), + this.requestIssuesSeverities(), this.requestTags(), this.requestAssignees() ]).then(responses => { @@ -43,7 +43,15 @@ export default class OverviewDomain extends React.Component { } render () { - return <div className="overview-domain"> + return <div className="overview-detailed-page"> + + <div className="overview-domain-header"> + <h2 className="overview-title">Issues & Technical Debt</h2> + </div> + + <a className="overview-detailed-page-back" href="#"> + <i className="icon-chevron-left"/> + </a> <IssuesTimeline {...this.props}/> diff --git a/server/sonar-web/src/main/js/apps/overview/issues/severities.js b/server/sonar-web/src/main/js/apps/overview/issues/severities.js index be2911b9889..5ccb3098b86 100644 --- a/server/sonar-web/src/main/js/apps/overview/issues/severities.js +++ b/server/sonar-web/src/main/js/apps/overview/issues/severities.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import React from 'react'; import SeverityHelper from '../../../components/shared/severity-helper'; import { DomainHeader } from '../domain/header'; -import { formatMeasure } from '../formatting'; import { componentIssuesUrl } from '../../../helpers/Url'; export default class extends React.Component { @@ -19,7 +18,7 @@ export default class extends React.Component { </td> <td className="thin text-right"> <a className="cell-link" href={href}> - {formatMeasure(s.count, 'violations')} + {window.formatMeasure(s.count, 'SHORT_INT')} </a> </td> </tr>; diff --git a/server/sonar-web/src/main/js/apps/overview/main.js b/server/sonar-web/src/main/js/apps/overview/main.js deleted file mode 100644 index 5c19f4282f5..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import offset from 'document-offset'; - -import GeneralMain from './general/main'; -import IssuesMain from './issues/main'; -import CoverageMain from './coverage/main'; -import DuplicationsMain from './duplications/main'; -import SizeMain from './size/main'; -import Meta from './meta'; -import Empty from './general/empty'; - -import { getMetrics } from '../../api/metrics'; - - -export const Overview = React.createClass({ - getInitialState () { - let hash = window.location.hash; - return { section: hash.length ? hash.substr(1) : null }; - }, - - componentWillMount () { - window.addEventListener('hashchange', this.handleHashChange); - }, - - componentDidMount () { - if (this.props.component.hasSnapshot) { - this.requestMetrics(); - } - }, - - componentWillUnmount () { - window.removeEventListener('hashchange', this.handleHashChange); - }, - - requestMetrics () { - return getMetrics().then(metrics => this.setState({ metrics })); - }, - - handleRoute (section) { - if (section !== this.state.section) { - let el = document.querySelector('.overview-more'); - this.setState({ section }, () => this.scrollToEl(el)); - window.location.href = '#' + section; - } else { - this.setState({ section: null }); - window.location.href = '#'; - } - }, - - handleHashChange () { - let hash = window.location.hash; - this.setState({ section: hash.substr(1) }); - }, - - scrollToEl (el) { - let top = offset(el).top; - window.scrollTo(0, top); - }, - - render () { - if (!this.props.component.hasSnapshot) { - return <div className="overview"><Empty/></div>; - } - - if (!this.state.metrics) { - return null; - } - - let child; - switch (this.state.section) { - case 'issues': - child = <IssuesMain {...this.props} {...this.state}/>; - break; - case 'coverage': - child = <CoverageMain {...this.props} {...this.state}/>; - break; - case 'duplications': - child = <DuplicationsMain {...this.props} {...this.state}/>; - break; - case 'size': - child = <SizeMain {...this.props} {...this.state}/>; - break; - default: - child = null; - } - - return <div className="overview"> - <div className="overview-main"> - <GeneralMain {...this.props} section={this.state.section} onRoute={this.handleRoute}/> - {child} - </div> - <Meta component={this.props.component}/> - </div>; - } -}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta.js b/server/sonar-web/src/main/js/apps/overview/meta.js index 310aadd8ae6..e3e632a30fa 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta.js @@ -25,26 +25,26 @@ export default React.createClass({ }); let descriptionCard = this.props.component.description ? ( - <div className="overview-card"> + <div className="overview-meta-card"> <div className="overview-meta-description">{this.props.component.description}</div> </div> ) : null, linksCard = _.size(this.props.component.links) > 0 ? ( - <div className="overview-card"> + <div className="overview-meta-card"> <ul className="overview-meta-list">{links}</ul> </div> ) : null, profilesCard = _.size(this.props.component.profiles) > 0 ? ( - <div className="overview-card"> + <div className="overview-meta-card"> <h4 className="overview-meta-header">{window.t('overview.quality_profiles')}</h4> <ul className="overview-meta-list">{profiles}</ul> </div> ) : null, gateCard = this.props.component.gate ? ( - <div className="overview-card"> + <div className="overview-meta-card"> <h4 className="overview-meta-header">{window.t('overview.quality_gate')}</h4> <ul className="overview-meta-list"> <li> @@ -60,8 +60,8 @@ export default React.createClass({ <div className="overview-meta"> {descriptionCard} {linksCard} - {profilesCard} {gateCard} + {profilesCard} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/overview.js b/server/sonar-web/src/main/js/apps/overview/overview.js new file mode 100644 index 00000000000..f03573a2677 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/overview.js @@ -0,0 +1,54 @@ +import React from 'react'; + +import Gate from './gate/gate'; +import GeneralMain from './general/main'; +import Meta from './meta'; +import { getMetrics } from '../../api/metrics'; + + +export const Overview = React.createClass({ + getInitialState () { + return { ready: false }; + }, + + componentDidMount () { + this.requestMetrics(); + }, + + requestMetrics () { + return getMetrics().then(metrics => this.setState({ ready: true, metrics })); + }, + + renderLoading () { + return <div className="text-center"> + <i className="spinner spinner-margin"/> + </div>; + }, + + render () { + if (!this.state.ready) { + return this.renderLoading(); + } + + return <div className="overview"> + <div className="overview-main"> + <Gate component={this.props.component} gate={this.props.gate}/> + <GeneralMain {...this.props} {...this.state}/> + </div> + <Meta component={this.props.component}/> + </div>; + } +}); + + +export const EmptyOverview = React.createClass({ + render() { + return ( + <div className="page"> + <div className="alert alert-warning"> + {window.t('provisioning.no_analysis')} + </div> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/size/main.js b/server/sonar-web/src/main/js/apps/overview/size/main.js index 72f892c9db6..9e037e136c9 100644 --- a/server/sonar-web/src/main/js/apps/overview/size/main.js +++ b/server/sonar-web/src/main/js/apps/overview/size/main.js @@ -11,7 +11,15 @@ import { SizeTreemap } from './treemap'; export default class extends React.Component { render () { - return <div className="overview-domain"> + return <div className="overview-detailed-page"> + <div className="overview-domain-header"> + <h2 className="overview-title">Size</h2> + </div> + + <a className="overview-detailed-page-back" href="#"> + <i className="icon-chevron-left"/> + </a> + <SizeTimeline {...this.props}/> <div className="flex-columns"> diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js index c7444fedcb9..f82a9c7cda2 100644 --- a/server/sonar-web/src/main/js/components/charts/bar-chart.js +++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js @@ -2,7 +2,7 @@ import d3 from 'd3'; import React from 'react'; import { ResizeMixin } from './mixins/resize-mixin'; -import { TooltipsMixin } from './mixins/tooltips-mixin'; +import { TooltipsMixin } from './../mixins/tooltips-mixin'; export const BarChart = React.createClass({ mixins: [ResizeMixin, TooltipsMixin], diff --git a/server/sonar-web/src/main/js/components/charts/bubble-chart.js b/server/sonar-web/src/main/js/components/charts/bubble-chart.js index e388a2baecb..9e3facaa74d 100644 --- a/server/sonar-web/src/main/js/components/charts/bubble-chart.js +++ b/server/sonar-web/src/main/js/components/charts/bubble-chart.js @@ -2,7 +2,7 @@ import d3 from 'd3'; import React from 'react'; import { ResizeMixin } from './mixins/resize-mixin'; -import { TooltipsMixin } from './mixins/tooltips-mixin'; +import { TooltipsMixin } from './../mixins/tooltips-mixin'; export const Bubble = React.createClass({ diff --git a/server/sonar-web/src/main/js/components/charts/line-chart.js b/server/sonar-web/src/main/js/components/charts/line-chart.js index f7c560bd3e1..eeaed5e88a9 100644 --- a/server/sonar-web/src/main/js/components/charts/line-chart.js +++ b/server/sonar-web/src/main/js/components/charts/line-chart.js @@ -2,7 +2,7 @@ import d3 from 'd3'; import React from 'react'; import { ResizeMixin } from './mixins/resize-mixin'; -import { TooltipsMixin } from './mixins/tooltips-mixin'; +import { TooltipsMixin } from './../mixins/tooltips-mixin'; export const LineChart = React.createClass({ @@ -124,14 +124,21 @@ export const LineChart = React.createClass({ let availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3]; let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2]; - let maxY = d3.max(this.props.data, d => d.y); + let maxY; let xScale = d3.scale.linear() .domain(d3.extent(this.props.data, d => d.x)) .range([0, availableWidth]); let yScale = d3.scale.linear() - .domain([0, maxY]) .range([availableHeight, 0]); + if (this.props.domain) { + maxY = this.props.domain[1]; + yScale.domain(this.props.domain); + } else { + maxY = d3.max(this.props.data, d => d.y); + yScale.domain([0, maxY]); + } + return <svg className="line-chart" width={this.state.width} height={this.state.height}> <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}> {this.renderVerticalGrid(xScale, yScale, maxY)} diff --git a/server/sonar-web/src/main/js/components/charts/treemap.js b/server/sonar-web/src/main/js/components/charts/treemap.js index be054fdc055..24bcff49ce2 100644 --- a/server/sonar-web/src/main/js/components/charts/treemap.js +++ b/server/sonar-web/src/main/js/components/charts/treemap.js @@ -3,7 +3,7 @@ import d3 from 'd3'; import React from 'react'; import { ResizeMixin } from './mixins/resize-mixin'; -import { TooltipsMixin } from './mixins/tooltips-mixin'; +import { TooltipsMixin } from './../mixins/tooltips-mixin'; const SIZE_SCALE = d3.scale.linear() diff --git a/server/sonar-web/src/main/js/components/charts/word-cloud.js b/server/sonar-web/src/main/js/components/charts/word-cloud.js index ed59b040fbf..1edbc2274ec 100644 --- a/server/sonar-web/src/main/js/components/charts/word-cloud.js +++ b/server/sonar-web/src/main/js/components/charts/word-cloud.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import d3 from 'd3'; import React from 'react'; -import { TooltipsMixin } from './mixins/tooltips-mixin'; +import { TooltipsMixin } from './../mixins/tooltips-mixin'; export const Word = React.createClass({ propTypes: { diff --git a/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js index 240edee02c5..240edee02c5 100644 --- a/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js +++ b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js diff --git a/server/sonar-web/src/main/js/helpers/constants.js b/server/sonar-web/src/main/js/helpers/constants.js new file mode 100644 index 00000000000..392adf2202a --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/constants.js @@ -0,0 +1,2 @@ +export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; +export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED']; diff --git a/server/sonar-web/src/main/js/libs/application.js b/server/sonar-web/src/main/js/libs/application.js index 8692a73f9cb..8d2236d9710 100644 --- a/server/sonar-web/src/main/js/libs/application.js +++ b/server/sonar-web/src/main/js/libs/application.js @@ -281,7 +281,7 @@ function closeModalWindow () { function shortIntVariationFormatter (value) { if (value === 0) { - return '0'; + return '+0'; } var format = '+0,0'; if (Math.abs(value) >= 1000) { @@ -354,21 +354,6 @@ function closeModalWindow () { } /** - * Check if about sign be displayed for a work duration - * @param {number} days - * @param {number} hours - * @param {number} minutes - * @returns {boolean} - */ - function shouldDisplayAbout (days, hours, minutes) { - var hasDays = days > 0, - fewDays = days < 5, - hasHours = hours > 0, - hasMinutes = minutes > 0; - return (hasDays && fewDays && hasHours) || (!hasDays && hasHours && hasMinutes); - } - - /** * Format a work duration based on parameters * @param {bool} isNegative * @param {number} days @@ -414,9 +399,6 @@ function closeModalWindow () { formatted = addSpaceIfNeeded(formatted); formatted += tp('work_duration.x_minutes', isNegative && formatted.length === 0 ? -1 * minutes : minutes); } - if (shouldDisplayAbout(days, hours, minutes)) { - formatted = tp('work_duration.about', formatted); - } return formatted; } @@ -432,9 +414,9 @@ function closeModalWindow () { var hoursInDay = window.SS.hoursInDay || 8, isNegative = value < 0, absValue = Math.abs(value); - var days = Math.floor(absValue / hoursInDay / 60); + var days = Math.round(absValue / hoursInDay / 60); var remainingValue = absValue - days * hoursInDay * 60; - var hours = Math.floor(remainingValue / 60); + var hours = Math.round(remainingValue / 60); remainingValue -= hours * 60; return formatDuration(isNegative, days, hours, remainingValue); }; @@ -462,6 +444,7 @@ function closeModalWindow () { /** * Format a work duration variation * @param {number} value + * @returns {string} */ var durationVariationFormatter = function (value) { if (value === 0) { @@ -472,6 +455,19 @@ function closeModalWindow () { }; /** + * Format a work duration variation + * @param {number} value + * @returns {string} + */ + var shortDurationVariationFormatter = function (value) { + if (value === 0) { + return '0'; + } + var formatted = shortDurationFormatter(value); + return formatted[0] !== '-' ? '+' + formatted : formatted; + }; + + /** * Format a rating measure * @param {number} value */ @@ -552,9 +548,10 @@ function closeModalWindow () { return value === 0 ? '0' : numeral(value).format('+0,0.0'); }, 'PERCENT': function (value) { - return value === 0 ? '0%' : numeral(+value / 100).format('+0,0.0%'); + return value === 0 ? '+0%' : numeral(+value / 100).format('+0,0.0%'); }, - 'WORK_DUR': durationVariationFormatter + 'WORK_DUR': durationVariationFormatter, + 'SHORT_WORK_DUR': shortDurationVariationFormatter }; if (measure != null && type != null) { formatted = formatters[type] != null ? formatters[type](measure) : measure; diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less index 072a4cea062..2e519588943 100644 --- a/server/sonar-web/src/main/less/pages/overview.less +++ b/server/sonar-web/src/main/less/pages/overview.less @@ -3,208 +3,105 @@ @import (reference) "../init/type"; @import (reference) "../init/links"; -@side-padding: 30px; +@side-padding: 20px; .overview { display: flex; + flex-wrap: wrap; + justify-content: space-between; width: 100%; min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})"; -} - -.overview > .panel { - flex: 1; + overflow-x: hidden; + animation: fadeIn 0.5s forwards; } .overview-main { - flex: 1; + flex: 4; box-sizing: border-box; - background-color: #fff; + background-color: @barBackgroundColor; + transition: transform 0.5s ease, opacity 0.5s ease; } -.overview-gate { - .clearfix; - padding: 50px 0 25px; -} +/* + * Gate + */ -.overview-gate-box { - float: left; - .size(120px, 70px); - padding: 10px; - .box-sizing(border-box); - line-height: 24px; - color: #fff; - font-size: 16px; - font-weight: 300; -} - -.overview-gate-box-error { - background-color: @red; -} +.overview-gate { + margin-right: 20px; + padding: 15px 0; + border-bottom: 1px solid @barBorderColor; + background-color: @barBackgroundColor; -.overview-gate-box-warn { - background-color: @orange; + .overview-title { + margin: 0 @side-padding; + } } -.overview-gate-box-ok { - background-color: @green; +.overview-gate-conditions-list { + display: flex; + flex-wrap: wrap; } -.overview-gate-conditions { - line-height: 70px; - font-size: 0; - white-space: nowrap; - overflow: hidden; - - & > li { - display: inline-block; - vertical-align: middle; - padding: 0 20px; - .box-sizing(border-box); - font-size: @baseFontSize; - line-height: 1; - } +.overview-gate-condition { + padding: 10px @side-padding; } .overview-gate-condition-metric { - //color: mix(@baseFontColor, @barBackgroundColor, 70%); - font-size: 15px; - font-weight: 400; - //letter-spacing: 0.03em; + } .overview-gate-condition-value { - margin-top: 8px; + margin-right: 4px; font-weight: 300; - font-size: 22px; + font-size: 24px; } -.overview-gate-condition-itself { - padding-left: 4px; - color: mix(@baseFontColor, @barBackgroundColor, 70%); - font-size: 13px; - font-weight: 400; +.overview-gate-warning { + margin: 15px @side-padding 0; } -.overview-gate-condition-level { - margin-top: 8px; -} - -.overview-leak { - padding: 50px 0 25px; - border-top: 1px solid @barBorderColor; - border-bottom: 1px solid @barBorderColor; -} +/* + * Title + */ .overview-title { - padding: 0 @side-padding; - font-size: 18px; + font-size: 16px; font-weight: 400; & > .badge { position: relative; top: -2px; margin-left: 15px; - padding: 8px 15px; - font-size: 16px; - letter-spacing: 0.04em; + padding: 6px 12px; + font-size: 14px; + letter-spacing: 0.05em; } } -.overview-leak-period { - margin-left: 10px; - font-size: 14px; -} - -.overview-nutshell { - padding: 50px 0 25px; -} +/* + * Cards + * TODO drop it + */ .overview-cards { display: flex; + flex-wrap: wrap; } -.overview-card { - flex: 1 0 25%; - padding: 25px @side-padding; - box-sizing: border-box; - - .overview-gate & { - flex-grow: 0; - } - - .overview-main & { - font-size: 14px; - } - - .measures-chart { - width: auto; - text-align: left; - } - - .measures-chart-indent { - padding-left: 67px; - } - - .measure-big + .measure-big { - margin-left: @side-padding; - } - - .measure-big .measure-name { - margin-top: 0; - margin-bottom: 2px; - } - - .list-inline { - margin-left: -10px; - margin-right: -10px; - - & > li { - padding-left: 10px; - padding-right: 10px; - } - } -} - -.overview-card-section { - padding: 0; - text-align: center; - - a { - display: block; - padding: 25px @side-padding; - .link-no-underline; - cursor: pointer; - transition: none; - } -} - -.overview-card-section.active a, -.overview-card-section a:hover { - background-color: #2c3946; - color: mix(#fff, #2c3946, 75%); -} - -.overview-measure { - font-size: 28px; -} - -.overview-measure-label { - font-size: 16px; -} +/* + * Meta + */ .overview-meta { - width: 240px; - border-left: 1px solid @barBorderColor; + flex: 1; box-sizing: border-box; background-color: @barBackgroundColor; - - .panel { - border: none !important; - } } -.overview-meta .overview-card { - width: auto; +.overview-meta-card { + min-width: 200px; + padding: @side-padding; + box-sizing: border-box; } .overview-meta-description { @@ -222,165 +119,140 @@ } } -.overview-domain { - margin-top: -25px; -} - -.overview-domain-dark { - background-color: #2c3946; - color: mix(#fff, #2c3946, 75%); - - a { - color: @blue; - border-bottom-color: @darkBlue; - - &:hover, &:focus { - border-bottom-color: @blue; - } - } +/* + * Domain + */ - .overview-title { - color: mix(#fff, #2c3946, 75%); - } - - table.data.zebra > tbody > tr:nth-child(odd) { - background-color: mix(#fff, #2c3946, 5%);; - } +.overview-domains { + animation: fadeIn 0.5s forwards; } -.overview-domain-section { - padding: 50px @side-padding; - - .overview-title { - margin-bottom: 25px; - padding-left: 0; - padding-right: 0; - } +.overview-domain { + margin: 30px @side-padding; } .overview-domain-header { display: flex; align-items: baseline; - margin-bottom: 20px;; - padding: 50px @side-padding 0; + justify-content: space-between; + margin-bottom: 10px; .overview-title { flex: 1; - margin: 0; - padding: 0; } } -.overview-timeline { +.overview-domain-panel { + display: flex; + margin-top: 10px; + border: 1px solid @barBorderColor; + background-color: #fff; + overflow: hidden; +} + +.overview-domain-nutshell, +.overview-domain-leak { position: relative; + padding: 30px 10px; +} - .line-chart { +.overview-domain-nutshell { + flex: 2; + .line-chart-backdrop { + fill: #e5f1f9; } +} - .line-chart-grid { - shape-rendering: crispedges; - stroke: #384653; - } +.overview-domain-leak { + flex: 1; + background-color: #fffae7; - .line-chart-path { - fill: none; - stroke-width: 2; - stroke: @blue; + .line-chart-backdrop { + fill: #f1ecd1; } +} - .line-chart-point { - fill: @blue; - stroke: none; - } +.overview-domain-measures { + position: relative; + z-index: 2; + display: flex; + flex: 1; + justify-content: space-around; + align-items: center; +} + +.overview-domain-measures + .overview-domain-measures { + margin-top: 30px; - .line-chart-tick { - fill: mix(#fff, #2c3946); - font-size: 11px; - text-anchor: middle; + .overview-domain-measure-value { + font-size: 14px; + font-weight: 400; } - .line-chart-backdrop { - fill: #4b9fd5; - fill-opacity: 0.2; + .overview-domain-measure-label { + margin-top: 4px; } } -.overview-timeline-select { - height: @formControlHeight; - border: 1px solid mix(#fff, #2c3946); - background-color: transparent; - color: mix(#fff, #2c3946); -} +.overview-domain-measure { -.overview-bubble-chart { - .bubble-chart-tick { - fill: mix(#fff, #2c3946); - font-size: 11px; - text-anchor: middle; - } +} - .bubble-chart-tick-y { - text-anchor: end; - } +.overview-domain-measure-value { + line-height: 1; + font-size: 36px; + font-weight: 300; + text-align: center; +} - .bubble-chart-bubble { - stroke: @blue; - fill: @blue; - fill-opacity: 0.2; - transition: fill-opacity 0.2s ease; +.overview-domain-measure-label { + margin-top: 10px; + text-align: center; +} - &:hover { - fill-opacity: 0.5; - } - } +.overview-domain-timeline { + position: absolute; + z-index: 1; + bottom: 0; + left: 0; + right: 0; + animation: fadeIn 0.5s forwards; - .bubble-chart-grid { - stroke: #ccc; + .line-chart-path { + fill: none; + stroke: none; } } -.overview-bar-chart { - .bar-chart-bar { - fill: @blue; - } - .bar-chart-tick { - fill: @baseFontColor; - font-size: 11px; - text-anchor: middle; + +/* + * Responsive Stuff + */ + +@media (max-width: 1200px) { + .overview { + display: block; } -} -.overview-treemap { - .overview-domain-header { - padding-top: 0; - padding-left: 0; - padding-right: 0; + .overview-meta { + display: flex; + justify-content: flex-start; } -} -.overview-chart-placeholder { - display: flex; - justify-content: center; - align-items: center; - align-content: center; + .overview-meta .overview-meta-card { + max-width: 25%; + } } -.overview-paragraph { - padding: 0 @side-padding; -} -.overview-more { - padding-top: 50px; - padding-bottom: 25px; - border-top: 1px solid @barBorderColor; - .overview-title { - padding-bottom: 25px; - } +/* + * Animations + */ - .overview-card + .overview-card { - border-left: 1px solid @barBorderColor; - } +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } } 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 c86e998a518..b9447a88eb5 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 @@ -119,61 +119,9 @@ var gate = null; <% end %> - var measures = { - <% if @snapshot %> - - // issues - <% if @snapshot.measure('sqale_rating') %> - sqaleRating: '<%= @snapshot.measure('sqale_rating').value -%>', - <% else %> - sqaleRating: 'A', - <% end %> - - // coverage - <% if @snapshot.measure('overall_coverage') %> - coverage: '<%= @snapshot.measure('overall_coverage').value -%>', - <% end %> - <% if @snapshot.measure('tests') %> - tests: '<%= @snapshot.measure('tests').value -%>', - <% end %> - - // duplications - duplications: '<%= @snapshot.measure('duplicated_lines_density').value -%>', - duplicatedLines: '<%= @snapshot.measure('duplicated_lines').value -%>', - duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').value -%>', - - // size - lines: '<%= @snapshot.measure('lines').value -%>', - files: '<%= @snapshot.measure('files').value -%>' - <% end %> - }; - - var leak = { - <% if @snapshot %> - // coverage - <% if @snapshot.measure('new_overall_coverage') %> - newCoverage: '<%= @snapshot.measure('new_overall_coverage').variation(1) -%>', - <% end %> - <% if @snapshot.measure('tests') %> - tests: '<%= @snapshot.measure('tests').variation(1) -%>', - <% end %> - - // duplications - duplications: '<%= @snapshot.measure('duplicated_lines_density').variation(1) -%>', - duplicatedLines: '<%= @snapshot.measure('duplicated_lines').variation(1) -%>', - duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').variation(1) -%>', - - // size - lines: '<%= @snapshot.measure('lines').variation(1) -%>', - files: '<%= @snapshot.measure('files').variation(1) -%>' - <% end %> - }; - window.sonarqube.overview = { component: component, - gate: gate, - measures: measures, - leak: leak + gate: gate }; })(); </script> |