"browserify": "11.2.0",
"browserify-shim": "3.8.10",
"chai": "3.3.0",
+ "classnames": "^2.2.0",
"del": "2.0.2",
+ "document-offset": "^1.0.4",
"event-stream": "3.3.1",
"glob": "5.0.15",
"gulp": "3.9.0",
+import { getJSON } from '../helpers/request.js';
import $ from 'jquery';
export function getComponents (data) {
options.type = 'POST';
return $.ajax(options);
}
+
+export function getChildren (componentKey, metrics = []) {
+ let url = baseUrl + '/api/resources/index';
+ let data = { resource: componentKey, metrics: metrics.join(','), depth: 1 };
+ return getJSON(url, data);
+}
+
+export function getFiles (componentKey, metrics = []) {
+ // due to the limitation of the WS we can not ask qualifiers=FIL,
+ // in this case the WS does not return measures
+ // so the filtering by a qualifier is done manually
+
+ let url = baseUrl + '/api/resources/index';
+ let data = { resource: componentKey, metrics: metrics.join(','), depth: -1 };
+ return getJSON(url, data).then(r => {
+ return r.filter(component => component.qualifier === 'FIL');
+ });
+}
--- /dev/null
+import { getJSON } from '../helpers/request.js';
+
+export function getEvents (componentKey, categories) {
+ let url = baseUrl + '/api/events';
+ let data = { resource: componentKey, categories };
+ return getJSON(url, data);
+}
--- /dev/null
+import _ from 'underscore';
+import { getJSON } from '../helpers/request.js';
+
+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 => {
+ return { facet: r.facets[0].values, response: r };
+ });
+}
+
+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 => {
+ let user = _.findWhere(r.response.users, { login: item.val });
+ return _.extend(item, { user });
+ });
+ });
+}
--- /dev/null
+import { getJSON } from '../helpers/request.js';
+
+export function getTimeMachineData (componentKey, metrics) {
+ let url = baseUrl + '/api/timemachine/index';
+ let data = { resource: componentKey, metrics };
+ return getJSON(url, data);
+}
import _ from 'underscore';
import React from 'react';
import Main from './main';
-import Empty from './empty';
class App {
- start(options) {
+ start (options) {
let opts = _.extend({}, options, window.sonarqube.overview);
_.extend(opts.component, options.component);
$('html').toggleClass('dashboard-page', opts.component.hasSnapshot);
- window.requestMessages().done(() => {
- let el = document.querySelector(opts.el);
- let inner = opts.component.hasSnapshot ? (
- <Main
- component={opts.component}
- gate={opts.gate}
- measures={opts.measures}
- leak={opts.leak}/>
- ) : <Empty/>;
- React.render(inner, el);
- });
+ let el = document.querySelector(opts.el);
+ React.render(<Main {...opts}/>, el);
}
}
+++ /dev/null
-import React from 'react';
-
-export default React.createClass({
- render() {
- return <li className="overview-card">{this.props.children}</li>;
- }
-});
+++ /dev/null
-import React from 'react';
-
-export default React.createClass({
- render() {
- return <ul className="overview-cards">{this.props.children}</ul>;
- }
-});
--- /dev/null
+import React from 'react';
+
+export class DomainHeader extends React.Component {
+ render () {
+ return <h2 className="overview-title">{this.props.title}</h2>;
+ }
+}
+++ /dev/null
-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>
- );
- }
-});
--- /dev/null
+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',
+ 'lines': 'SHORT_INT'
+};
+
+export function formatMeasure (value, metric) {
+ let type = METRIC_TYPES[metric];
+ return type ? window.formatMeasure(value, type) : value;
+}
+++ /dev/null
-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,
- iconClassName = 'icon-alert-' + this.props.condition.level.toLowerCase(),
- 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);
-
- return (
- <div>
- <h4 className="overview-gate-condition-metric">{metricName}<br/><span className="nowrap">{period}</span></h4>
- <div className="overview-gate-condition-value">
- <i className={iconClassName}/>
- <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
- period={this.props.condition.period} periodDate={periodDate}>
- <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
- </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>
- );
- }
-});
+++ /dev/null
-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>;
- }
-});
+++ /dev/null
-import React from 'react';
-
-export default React.createClass({
- render() {
- let qualityGatesUrl = window.baseUrl + '/quality_gates';
-
- return (
- <div className="overview-gate">
- <h2 className="overview-title">{window.t('overview.quality_gate')}</h2>
- <p className="big-spacer-top">You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p>
- </div>
- );
- }
-});
+++ /dev/null
-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(),
- badgeText = window.t('overview.gate', this.props.gate.level);
-
- return (
- <div className="overview-gate">
- <h2 className="overview-title">
- {window.t('overview.quality_gate')}
- <span className={badgeClassName}>{badgeText}</span>
- </h2>
- <GateConditions gate={this.props.gate} component={this.props.component}/>
- </div>
- );
- }
-});
--- /dev/null
+import React from 'react';
+import classNames from 'classnames';
+
+export default React.createClass({
+ handleClick() {
+ if (this.props.linkTo) {
+ let tab = React.findDOMNode(this);
+ this.props.onRoute(this.props.linkTo, tab);
+ }
+ },
+
+ render() {
+ let classes = classNames('overview-card', {
+ 'overview-card-section': this.props.linkTo,
+ 'active': this.props.active
+ });
+ return <li onClick={this.handleClick} className={classes}>{this.props.children}</li>;
+ }
+});
--- /dev/null
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ return <ul className="overview-cards">{this.props.children}</ul>;
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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,
+ iconClassName = 'icon-alert-' + this.props.condition.level.toLowerCase(),
+ 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);
+
+ return (
+ <div>
+ <h4 className="overview-gate-condition-metric">{metricName}<br/><span className="nowrap">{period}</span></h4>
+ <div className="overview-gate-condition-value">
+ <i className={iconClassName}/>
+ <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
+ period={this.props.condition.period} periodDate={periodDate}>
+ <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
+ </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>
+ );
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ let qualityGatesUrl = window.baseUrl + '/quality_gates';
+
+ return (
+ <div className="overview-gate">
+ <h2 className="overview-title">{window.t('overview.quality_gate')}</h2>
+ <p className="big-spacer-top">You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p>
+ </div>
+ );
+ }
+});
--- /dev/null
+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(),
+ badgeText = window.t('overview.gate', this.props.gate.level);
+
+ return (
+ <div className="overview-gate">
+ <h2 className="overview-title">
+ {window.t('overview.quality_gate')}
+ <span className={badgeClassName}>{badgeText}</span>
+ </h2>
+ <GateConditions gate={this.props.gate} component={this.props.component}/>
+ </div>
+ );
+ }
+});
--- /dev/null
+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-value">
+ <DrilldownLink component={this.props.component.key} metric="new_coverage" period="3">
+ <Measure value={newCoverage} type="PERCENT"/>
+ </DrilldownLink>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.new_coverage')}</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>
+ );
+ }
+});
--- /dev/null
+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-value">
+ <MeasureVariation value={density} type="PERCENT"/>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.duplications')}</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>
+ );
+ }
+});
--- /dev/null
+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, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+ return (
+ <Card>
+ <div className="measures">
+ <div className="measure measure-big" data-metric="sqale_index">
+ <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>
+ <span className="measure-name">{window.t('overview.metric.new_debt')}</span>
+ </div>
+ <div className="measure measure-big" data-metric="violations">
+ <span className="measure-value">
+ <IssuesLink component={this.props.component.key}
+ params={{ resolved: 'false', createdAfter: periodDate }}>
+ <Measure value={issues} type="SHORT_INT"/>
+ </IssuesLink>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.new_issues')}</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>
+ );
+ }
+});
--- /dev/null
+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-value">
+ <MeasureVariation value={lines} type="SHORT_INT"/>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.lines')}</span>
+ </div>
+ <div className="measure measure-big" data-metric="files">
+ <span className="measure-value">
+ <MeasureVariation value={files} type="SHORT_INT"/>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.files')}</span>
+ </div>
+ </div>
+ </Card>
+ );
+ }
+});
--- /dev/null
+import _ from 'underscore';
+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} from './../helpers/period-label';
+
+export default React.createClass({
+ render() {
+ if (_.size(this.props.component.periods) < 3) {
+ return null;
+ }
+
+ let period = periodLabel(this.props.component.periods, '3');
+
+ return (
+ <div className="overview-leak">
+ <h2 className="overview-title">
+ {window.t('overview.water_leak')}
+ <span className="overview-leak-period">{period}</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>
+ );
+ }
+});
--- /dev/null
+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 {getPeriodDate} from './../helpers/period-label';
+
+export default React.createClass({
+ getInitialState() {
+ return { leak: this.props.leak, measures: this.props.measures };
+ },
+
+ componentDidMount() {
+ if (this._hasWaterLeak()) {
+ this.requestLeakIssues();
+ this.requestLeakDebt();
+ }
+ this.requestNutshellIssues();
+ this.requestNutshellDebt();
+ },
+
+ _hasWaterLeak() {
+ return !!_.findWhere(this.props.component.periods, { index: '3' });
+ },
+
+ _requestIssues(data) {
+ let url = `${baseUrl}/api/issues/search`;
+ data.ps = 1;
+ data.componentUuids = this.props.component.id;
+ return $.get(url, data);
+ },
+
+ requestLeakIssues() {
+ let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).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;
+
+ 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
+ })
+ });
+ });
+ },
+
+ 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;
+
+ 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
+ })
+ });
+ });
+ },
+
+ requestLeakDebt() {
+ let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).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 })
+ });
+ });
+ },
+
+ requestNutshellDebt() {
+ this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => {
+ this.setState({
+ measures: _.extend({}, this.state.measures, { debt: r.debtTotal })
+ });
+ });
+ },
+
+ 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}
+ onRoute={this.props.onRoute}/>
+ </div>;
+ }
+});
--- /dev/null
+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-value">
+ <DrilldownLink component={this.props.component.key} metric="overall_coverage">
+ <Measure value={coverage} type="PERCENT"/>
+ </DrilldownLink>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.coverage')}</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>
+ );
+ }
+});
--- /dev/null
+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-value">
+ <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density">
+ <Measure value={density} type="PERCENT"/>
+ </DrilldownLink>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.duplications')}</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>
+ );
+ }
+});
--- /dev/null
+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;
+
+ let active = this.props.section === 'issues';
+
+ return (
+ <Card linkTo="issues" active={active} onRoute={this.props.onRoute}>
+ <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-value">
+ <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}>
+ <Measure value={debt} type="SHORT_WORK_DUR"/>
+ </IssuesLink>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.debt')}</span>
+ </div>
+ <div className="measure measure-big" data-metric="violations">
+ <span className="measure-value">
+ <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
+ <Measure value={issues} type="SHORT_INT"/>
+ </IssuesLink>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.issues')}</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>
+ );
+ }
+});
--- /dev/null
+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-value">
+ <DrilldownLink component={this.props.component.key} metric="lines">
+ <Measure value={lines} type="SHORT_INT"/>
+ </DrilldownLink>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.lines')}</span>
+ </div>
+ <div className="measure measure-big" data-metric="files">
+ <span className="measure-value">
+ <DrilldownLink component={this.props.component.key} metric="files">
+ <Measure value={files} type="SHORT_INT"/>
+ </DrilldownLink>
+ </span>
+ <span className="measure-name">{window.t('overview.metric.files')}</span>
+ </div>
+ </div>
+ </Card>
+ );
+ }
+});
--- /dev/null
+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,
+ section: this.props.section,
+ onRoute: this.props.onRoute
+ };
+ 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>
+ );
+ }
+});
--- /dev/null
+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 {
+ render () {
+ let rows = this.props.assignees.map(s => {
+ let href = componentIssuesUrl(this.props.component.key, { statuses: 'OPEN,REOPENED', assignees: s.val });
+ return <tr key={s.val}>
+ <td>
+ <Assignee user={s.user}/>
+ </td>
+ <td className="thin text-right">
+ <a href={href}>{formatMeasure(s.count, 'violations')}</a>
+ </td>
+ </tr>;
+ });
+
+ return <div className="overview-domain-section">
+ <DomainHeader title="Issues to Review"/>
+ <table className="data zebra">
+ <tbody>{rows}</tbody>
+ </table>
+ </div>;
+ }
+}
--- /dev/null
+import _ from 'underscore';
+import React from 'react';
+import { BubbleChart } from '../../../components/charts/bubble-chart';
+import { getProjectUrl } from '../../../helpers/Url';
+import { getFiles } from '../../../api/components';
+
+
+const X_METRIC = 'violations';
+const Y_METRIC = 'sqale_index';
+const SIZE_METRIC_1 = 'blocker_violations';
+const SIZE_METRIC_2 = 'critical_violations';
+const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC_1, SIZE_METRIC_2];
+const HEIGHT = 360;
+
+
+function formatIssues (d) {
+ return window.formatMeasure(d, 'SHORT_INT');
+}
+
+function formatDebt (d) {
+ return window.formatMeasure(d, 'SHORT_WORK_DUR');
+}
+
+function getMeasure (component, metric) {
+ return component.measures[metric] || 0;
+}
+
+
+export class IssuesBubbleChart extends React.Component {
+ constructor () {
+ super();
+ this.state = { loading: true, files: [] };
+ }
+
+ componentDidMount () {
+ this.requestFiles();
+ }
+
+ requestFiles () {
+ return getFiles(this.props.component.key, COMPONENTS_METRICS).then(r => {
+ let files = r.map(file => {
+ let measures = {};
+ (file.msr || []).forEach(measure => {
+ measures[measure.key] = measure.val;
+ });
+ return _.extend(file, { measures });
+ });
+ this.setState({ loading: false, files });
+ });
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderBubbleChart () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ let items = this.state.files.map(component => {
+ return {
+ x: getMeasure(component, X_METRIC),
+ y: getMeasure(component, Y_METRIC),
+ size: getMeasure(component, SIZE_METRIC_1) + getMeasure(component, SIZE_METRIC_2),
+ link: getProjectUrl(component.key)
+ };
+ });
+ let xGrid = this.state.files.map(component => component.measures[X_METRIC]);
+ let tooltips = this.state.files.map(component => {
+ let inner = [
+ component.name,
+ `Issues: ${formatIssues(getMeasure(component, X_METRIC))}`,
+ `Technical Debt: ${formatDebt(getMeasure(component, Y_METRIC))}`,
+ `Blocker & Critical Issues: ${formatIssues(getMeasure(component, SIZE_METRIC_1) + getMeasure(component, SIZE_METRIC_2))}`
+ ].join('<br>');
+ return `<div class="text-left">${inner}</div>`;
+ });
+ return <BubbleChart items={items}
+ xGrid={xGrid}
+ tooltips={tooltips}
+ height={HEIGHT}
+ padding={[25, 30, 50, 60]}
+ formatXTick={formatIssues}
+ formatYTick={formatDebt}/>;
+ }
+
+ render () {
+ return <div className="overview-bubble-chart overview-domain-dark">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Project Files</h2>
+ <ul className="list-inline small">
+ <li>X: Issues</li>
+ <li>Y: Technical Debt</li>
+ <li>Size: Blocker & Critical Issues</li>
+ </ul>
+ </div>
+ <div>
+ {this.renderBubbleChart()}
+ </div>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import IssuesSeverities from './severities';
+import IssuesAssignees from './assignees';
+import IssuesTags from './tags';
+import { IssuesBubbleChart } from './bubble-chart';
+import { IssuesTimeline } from './timeline';
+import { IssuesTreemap } from './treemap';
+
+import { getSeverities, getTags, getAssignees } from '../../../api/issues';
+
+
+export default class OverviewDomain extends React.Component {
+ constructor () {
+ super();
+ this.state = { severities: [], tags: [], assignees: [] };
+ }
+
+ componentDidMount () {
+ Promise.all([
+ this.requestSeverities(),
+ this.requestTags(),
+ this.requestAssignees()
+ ]).then(responses => {
+ this.setState({
+ severities: responses[0],
+ tags: responses[1],
+ assignees: responses[2]
+ });
+ });
+ }
+
+ requestSeverities () {
+ return getSeverities({ resolved: 'false', componentUuids: this.props.component.id });
+ }
+
+ requestTags () {
+ return getTags({ resolved: 'false', componentUuids: this.props.component.id });
+ }
+
+ requestAssignees () {
+ return getAssignees({ statuses: 'OPEN,REOPENED', componentUuids: this.props.component.id });
+ }
+
+ render () {
+ return <div className="overview-domain">
+
+ <IssuesTimeline {...this.props}/>
+
+ <div className="flex-columns">
+ <div className="flex-column flex-column-third">
+ <IssuesSeverities {...this.props} severities={this.state.severities}/>
+ </div>
+ <div className="flex-column flex-column-third">
+ <IssuesTags {...this.props} tags={this.state.tags}/>
+ </div>
+ <div className="flex-column flex-column-third">
+ <IssuesAssignees {...this.props} assignees={this.state.assignees}/>
+ </div>
+ </div>
+
+ <IssuesBubbleChart {...this.props}/>
+ <IssuesTreemap {...this.props}/>
+ </div>;
+ }
+}
--- /dev/null
+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 {
+ sortedSeverities () {
+ return _.sortBy(this.props.severities, s => window.severityComparator(s.val));
+ }
+
+ render () {
+ let rows = this.sortedSeverities().map(s => {
+ let href = componentIssuesUrl(this.props.component.key, { resolved: 'false', severities: s.val });
+ return <tr key={s.val}>
+ <td>
+ <SeverityHelper severity={s.val}/>
+ </td>
+ <td className="thin text-right">
+ <a className="cell-link" href={href}>
+ {formatMeasure(s.count, 'violations')}
+ </a>
+ </td>
+ </tr>;
+ });
+
+ return <div className="overview-domain-section">
+ <DomainHeader title="Prioritized Issues"/>
+ <table className="data zebra">
+ <tbody>{rows}</tbody>
+ </table>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+import { DomainHeader } from '../domain/header';
+import { WordCloud } from '../../../components/charts/word-cloud';
+import { componentIssuesUrl } from '../../../helpers/Url';
+
+export default class extends React.Component {
+ renderWordCloud () {
+ let tags = this.props.tags.map(tag => {
+ let link = componentIssuesUrl(this.props.component.key, { resolved: 'false', tags: tag.val });
+ let tooltip = `Issues: ${window.formatMeasure(tag.count, 'SHORT_INT')}`;
+ return { text: tag.val, size: tag.count, link, tooltip };
+ });
+ return <WordCloud items={tags}/>;
+ }
+
+ render () {
+ return <div className="overview-domain-section">
+ <DomainHeader title="Issues By Tag"/>
+ <div>
+ {this.renderWordCloud()}
+ </div>
+ </div>;
+ }
+}
--- /dev/null
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { LineChart } from '../../../components/charts/line-chart';
+import { formatMeasure } from '../formatting';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+
+
+const ISSUES_METRICS = [
+ 'violations',
+ 'blocker_violations',
+ 'critical_violations',
+ 'major_violations',
+ 'minor_violations',
+ 'info_violations',
+ 'confirmed_issues',
+ 'false_positive_issues',
+ 'open_issues',
+ 'reopened_issues'
+];
+
+const DEBT_METRICS = [
+ 'sqale_index',
+ 'sqale_debt_ratio'
+];
+
+const HEIGHT = 280;
+
+
+export class IssuesTimeline extends React.Component {
+ constructor () {
+ super();
+ this.state = { loading: true, currentMetric: ISSUES_METRICS[0] };
+ }
+
+ componentDidMount () {
+ Promise.all([
+ this.requestTimeMachineData(),
+ this.requestEvents()
+ ]).then(() => this.setState({ loading: false }));
+ }
+
+ requestTimeMachineData () {
+ return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
+ let snapshots = r[0].cells.map(cell => {
+ return { date: moment(cell.d).toDate(), value: cell.v[0] };
+ });
+ this.setState({ snapshots });
+ });
+ }
+
+ requestEvents () {
+ return getEvents(this.props.component.key, 'Version').then(r => {
+ let events = r.map(event => {
+ return { version: event.n, date: moment(event.dt).toDate() };
+ });
+ events = _.sortBy(events, 'date');
+ this.setState({ events });
+ });
+ }
+
+ prepareEvents () {
+ let events = this.state.events;
+ let snapshots = this.state.snapshots;
+ return events
+ .map(event => {
+ let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
+ event.value = snapshot && snapshot.value;
+ return event;
+ })
+ .filter(event => event.value != null);
+ }
+
+ handleMetricChange () {
+ let metric = React.findDOMNode(this.refs.metricSelect).value;
+ this.setState({ currentMetric: metric }, this.requestTimeMachineData);
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderLineChart () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ let events = this.prepareEvents();
+
+ let data = events.map((event, index) => {
+ return { x: index, y: event.value };
+ });
+
+ let xTicks = events.map(event => event.version.substr(0, 6));
+
+ let xValues = events.map(event => formatMeasure(event.value, this.state.currentMetric));
+
+ // TODO use leak period
+ let backdropConstraints = [
+ this.state.events.length - 2,
+ this.state.events.length - 1
+ ];
+
+ return <LineChart data={data}
+ xTicks={xTicks}
+ xValues={xValues}
+ backdropConstraints={backdropConstraints}
+ height={HEIGHT}
+ interpolate="linear"
+ padding={[25, 30, 50, 30]}/>;
+ }
+
+ renderTimelineMetricSelect () {
+ if (this.state.loading) {
+ return null;
+ }
+
+ let issueOptions = ISSUES_METRICS
+ .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
+ let debtOptions = DEBT_METRICS
+ .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
+
+ return <select ref="metricSelect"
+ className="overview-timeline-select"
+ onChange={this.handleMetricChange.bind(this)}
+ value={this.state.currentMetric}>
+ <optgroup label="Issues">{issueOptions}</optgroup>
+ <optgroup label="Technical Debt">{debtOptions}</optgroup>
+ </select>;
+ }
+
+ render () {
+ return <div className="overview-timeline overview-domain-dark">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Project History</h2>
+ {this.renderTimelineMetricSelect()}
+ </div>
+ <div>
+ {this.renderLineChart()}
+ </div>
+ </div>;
+ }
+}
--- /dev/null
+import _ from 'underscore';
+import React from 'react';
+
+import { Treemap } from '../../../components/charts/treemap';
+import { formatMeasure } from '../formatting';
+import { getChildren } from '../../../api/components';
+
+const COMPONENTS_METRICS = [
+ 'lines',
+ 'sqale_rating'
+];
+
+const HEIGHT = 360;
+
+export class IssuesTreemap extends React.Component {
+ constructor () {
+ super();
+ this.state = { loading: true, components: [] };
+ }
+
+ componentDidMount () {
+ this.requestComponents();
+ }
+
+ requestComponents () {
+ return getChildren(this.props.component.key, COMPONENTS_METRICS).then(r => {
+ let components = r.map(component => {
+ let measures = {};
+ component.msr.forEach(measure => {
+ measures[measure.key] = measure.val;
+ });
+ return _.extend(component, { measures });
+ });
+ this.setState({ loading: false, components });
+ });
+ }
+
+ // TODO use css
+ getRatingColor (rating) {
+ switch (rating) {
+ case 1:
+ return '#00AA00';
+ case 2:
+ return '#80CC00';
+ case 3:
+ return '#FFEE00';
+ case 4:
+ return '#F77700';
+ case 5:
+ return '#EE0000';
+ default:
+ return '#777';
+ }
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderTreemap () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ let items = this.state.components.map(component => {
+ return {
+ size: component.measures['lines'],
+ color: this.getRatingColor(component.measures['sqale_rating'])
+ };
+ });
+ let labels = this.state.components.map(component => component.name);
+ let tooltips = this.state.components.map(component => {
+ let inner = [
+ component.name,
+ `Lines: ${formatMeasure(component.measures['lines'], 'lines')}`,
+ `SQALE Rating: ${formatMeasure(component.measures['sqale_rating'], 'sqale_rating')}`
+ ].join('<br>');
+ return `<div class="text-left">${inner}</div>`;
+ });
+ return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
+ }
+
+ render () {
+ return <div className="overview-domain-section overview-treemap">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Project Components</h2>
+ <ul className="list-inline small">
+ <li>Size: Lines</li>
+ <li>Color: SQALE Rating</li>
+ </ul>
+ </div>
+ <div>
+ {this.renderTreemap()}
+ </div>
+ </div>;
+ }
+}
+++ /dev/null
-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-value">
- <DrilldownLink component={this.props.component.key} metric="new_coverage" period="3">
- <Measure value={newCoverage} type="PERCENT"/>
- </DrilldownLink>
- </span>
- <span className="measure-name">{window.t('overview.metric.new_coverage')}</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>
- );
- }
-});
+++ /dev/null
-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-value">
- <MeasureVariation value={density} type="PERCENT"/>
- </span>
- <span className="measure-name">{window.t('overview.metric.duplications')}</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>
- );
- }
-});
+++ /dev/null
-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, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
-
- return (
- <Card>
- <div className="measures">
- <div className="measure measure-big" data-metric="sqale_index">
- <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>
- <span className="measure-name">{window.t('overview.metric.new_debt')}</span>
- </div>
- <div className="measure measure-big" data-metric="violations">
- <span className="measure-value">
- <IssuesLink component={this.props.component.key}
- params={{ resolved: 'false', createdAfter: periodDate }}>
- <Measure value={issues} type="SHORT_INT"/>
- </IssuesLink>
- </span>
- <span className="measure-name">{window.t('overview.metric.new_issues')}</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>
- );
- }
-});
+++ /dev/null
-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-value">
- <MeasureVariation value={lines} type="SHORT_INT"/>
- </span>
- <span className="measure-name">{window.t('overview.metric.lines')}</span>
- </div>
- <div className="measure measure-big" data-metric="files">
- <span className="measure-value">
- <MeasureVariation value={files} type="SHORT_INT"/>
- </span>
- <span className="measure-name">{window.t('overview.metric.files')}</span>
- </div>
- </div>
- </Card>
- );
- }
-});
+++ /dev/null
-import _ from 'underscore';
-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} from './helpers/period-label';
-
-export default React.createClass({
- render() {
- if (_.size(this.props.component.periods) < 3) {
- return null;
- }
-
- let period = periodLabel(this.props.component.periods, '3');
-
- return (
- <div className="overview-leak">
- <h2 className="overview-title">
- {window.t('overview.water_leak')}
- <span className="overview-leak-period">{period}</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>
- );
- }
-});
-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 offset from 'document-offset';
+import GeneralMain from './general/main';
+import IssuesMain from './issues/main';
import Meta from './meta';
-import {getPeriodDate} from './helpers/period-label';
-export default React.createClass({
- getInitialState() {
- return { leak: this.props.leak, measures: this.props.measures };
- },
-
- componentDidMount() {
- if (this._hasWaterLeak()) {
- this.requestLeakIssues();
- this.requestLeakDebt();
- }
- this.requestNutshellIssues();
- this.requestNutshellDebt();
- },
-
- _hasWaterLeak() {
- return !!_.findWhere(this.props.component.periods, { index: '3' });
- },
-
- _requestIssues(data) {
- let url = `${baseUrl}/api/issues/search`;
- data.ps = 1;
- data.componentUuids = this.props.component.id;
- return $.get(url, data);
- },
-
- requestLeakIssues() {
- let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).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;
-
- 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
- })
- });
- });
- },
-
- 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;
+export default class Overview extends React.Component {
+ constructor () {
+ super();
+ let hash = window.location.hash;
+ this.state = { section: hash.length ? hash.substr(1) : null };
+ }
- 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
- })
- });
- });
- },
+ handleRoute (section, el) {
+ this.setState({ section }, () => this.scrollToEl(el));
+ window.location.href = '#' + section;
+ }
- requestLeakDebt() {
- let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).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 })
- });
- });
- },
+ scrollToEl (el) {
+ let top = offset(el).top - el.getBoundingClientRect().height;
+ window.scrollTo(0, top);
+ }
- requestNutshellDebt() {
- this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => {
- this.setState({
- measures: _.extend({}, this.state.measures, { debt: r.debtTotal })
- });
- });
- },
+ render () {
+ let child;
+ switch (this.state.section) {
+ case 'issues':
+ child = <IssuesMain {...this.props}/>;
+ break;
+ default:
+ child = null;
+ }
- render() {
- return (
- <div className="overview">
- <div className="overview-main">
- <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}/>
- </div>
- <Meta component={this.props.component}/>
- </div>
- );
+ return <div className="overview">
+ <div className="overview-main">
+ <GeneralMain {...this.props} section={this.state.section} onRoute={this.handleRoute.bind(this)}/>
+ {child}
+ </div>
+ <Meta component={this.props.component}/>
+ </div>;
}
-});
+}
+++ /dev/null
-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-value">
- <DrilldownLink component={this.props.component.key} metric="overall_coverage">
- <Measure value={coverage} type="PERCENT"/>
- </DrilldownLink>
- </span>
- <span className="measure-name">{window.t('overview.metric.coverage')}</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>
- );
- }
-});
+++ /dev/null
-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-value">
- <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density">
- <Measure value={density} type="PERCENT"/>
- </DrilldownLink>
- </span>
- <span className="measure-name">{window.t('overview.metric.duplications')}</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>
- );
- }
-});
+++ /dev/null
-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-value">
- <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}>
- <Measure value={debt} type="SHORT_WORK_DUR"/>
- </IssuesLink>
- </span>
- <span className="measure-name">{window.t('overview.metric.debt')}</span>
- </div>
- <div className="measure measure-big" data-metric="violations">
- <span className="measure-value">
- <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
- <Measure value={issues} type="SHORT_INT"/>
- </IssuesLink>
- </span>
- <span className="measure-name">{window.t('overview.metric.issues')}</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>
- );
- }
-});
+++ /dev/null
-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-value">
- <DrilldownLink component={this.props.component.key} metric="lines">
- <Measure value={lines} type="SHORT_INT"/>
- </DrilldownLink>
- </span>
- <span className="measure-name">{window.t('overview.metric.lines')}</span>
- </div>
- <div className="measure measure-big" data-metric="files">
- <span className="measure-value">
- <DrilldownLink component={this.props.component.key} metric="files">
- <Measure value={files} type="SHORT_INT"/>
- </DrilldownLink>
- </span>
- <span className="measure-name">{window.t('overview.metric.files')}</span>
- </div>
- </div>
- </Card>
- );
- }
-});
+++ /dev/null
-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>
- );
- }
-});
--- /dev/null
+import $ from 'jquery';
+import d3 from 'd3';
+import React from 'react';
+
+export class Bubble extends React.Component {
+ handleClick () {
+ if (this.props.link) {
+ window.location = this.props.link;
+ }
+ }
+
+ render () {
+ let tooltipAttrs = {};
+ if (this.props.tooltip) {
+ tooltipAttrs = {
+ 'data-toggle': 'tooltip',
+ 'title': this.props.tooltip
+ };
+ }
+ return <circle onClick={this.handleClick.bind(this)} className="bubble-chart-bubble"
+ r={this.props.r} {...tooltipAttrs}
+ transform={`translate(${this.props.x}, ${this.props.y})`}/>;
+ }
+}
+
+
+export class BubbleChart extends React.Component {
+ constructor (props) {
+ super();
+ this.state = { width: props.width, height: props.height };
+ }
+
+ componentDidMount () {
+ if (!this.props.width || !this.props.height) {
+ this.handleResize();
+ window.addEventListener('resize', this.handleResize.bind(this));
+ }
+ this.initTooltips();
+ }
+
+ componentDidUpdate () {
+ this.initTooltips();
+ }
+
+ componentWillUnmount () {
+ if (!this.props.width || !this.props.height) {
+ window.removeEventListener('resize', this.handleResize.bind(this));
+ }
+ }
+
+ handleResize () {
+ let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+ let newWidth = this.props.width || boundingClientRect.width;
+ let newHeight = this.props.height || boundingClientRect.height;
+ this.setState({ width: newWidth, height: newHeight });
+ }
+
+ initTooltips () {
+ $('[data-toggle="tooltip"]', React.findDOMNode(this))
+ .tooltip({ container: 'body', placement: 'bottom', html: true });
+ }
+
+ getXRange (xScale, sizeScale, availableWidth) {
+ var minX = d3.min(this.props.items, d => xScale(d.x) - sizeScale(d.size)),
+ maxX = d3.max(this.props.items, d => xScale(d.x) + sizeScale(d.size)),
+ dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0],
+ dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
+ return [dMinX, availableWidth - dMaxX];
+ }
+
+ getYRange (yScale, sizeScale, availableHeight) {
+ var minY = d3.min(this.props.items, d => yScale(d.y) - sizeScale(d.size)),
+ maxY = d3.max(this.props.items, d => yScale(d.y) + sizeScale(d.size)),
+ dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1],
+ dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
+ return [availableHeight - dMaxY, dMinY];
+ }
+
+ renderXGrid (xScale, yScale) {
+ if (!this.props.displayXGrid) {
+ return null;
+ }
+
+ let lines = xScale.ticks().map((tick, index) => {
+ let x = xScale(tick);
+ let y1 = yScale.range()[0];
+ let y2 = yScale.range()[1];
+
+ // TODO extract styling
+ return <line key={index} x1={x} x2={x} y1={y1} y2={y2}
+ shapeRendering="crispEdges" strokeWidth="0.3" stroke="#ccc"/>;
+ });
+
+ return <g ref="xGrid">{lines}</g>;
+ }
+
+ renderYGrid (xScale, yScale) {
+ if (!this.props.displayYGrid) {
+ return null;
+ }
+
+ let lines = yScale.ticks(5).map((tick, index) => {
+ let y = yScale(tick);
+ let x1 = xScale.range()[0];
+ let x2 = xScale.range()[1];
+
+ // TODO extract styling
+ return <line key={index} x1={x1} x2={x2} y1={y} y2={y}
+ shapeRendering="crispEdges" strokeWidth="0.3" stroke="#ccc"/>;
+ });
+
+ return <g ref="yGrid">{lines}</g>;
+ }
+
+ renderXTicks (xScale, yScale) {
+ if (!this.props.displayXTicks) {
+ return null;
+ }
+
+ let ticks = xScale.ticks().map((tick, index) => {
+ let x = xScale(tick);
+ let y = yScale.range()[0];
+ let text = this.props.formatXTick(tick);
+
+ // TODO extract styling
+ return <text key={index} className="bubble-chart-tick" x={x} y={y} dy="1.5em">{text}</text>;
+ });
+
+ return <g>{ticks}</g>;
+ }
+
+ renderYTicks (xScale, yScale) {
+ if (!this.props.displayYTicks) {
+ return null;
+ }
+
+ let ticks = yScale.ticks(5).map((tick, index) => {
+ let x = xScale.range()[0];
+ let y = yScale(tick);
+ let text = this.props.formatYTick(tick);
+
+ // TODO extract styling
+ return <text key={index} className="bubble-chart-tick bubble-chart-tick-y"
+ x={x} y={y} dx="-0.5em" dy="0.3em">{text}</text>;
+ });
+
+ return <g>{ticks}</g>;
+ }
+
+ render () {
+ if (!this.state.width || !this.state.height) {
+ return <div/>;
+ }
+
+ 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 xScale = d3.scale.linear()
+ .domain([0, d3.max(this.props.items, d => d.x)])
+ .range([0, availableWidth])
+ .nice();
+ let yScale = d3.scale.linear()
+ .domain([0, d3.max(this.props.items, d => d.y)])
+ .range([availableHeight, 0])
+ .nice();
+ let sizeScale = d3.scale.linear()
+ .domain([0, d3.max(this.props.items, d => d.size)])
+ .range(this.props.sizeRange);
+
+ xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
+ yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
+
+ let bubbles = this.props.items
+ .map((item, index) => {
+ let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null;
+ return <Bubble key={index}
+ tooltip={tooltip}
+ link={item.link}
+ x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>;
+ });
+
+ return <svg className="bubble-chart" width={this.state.width} height={this.state.height}>
+ <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+ {this.renderXGrid(xScale, yScale)}
+ {this.renderXTicks(xScale, yScale)}
+ {this.renderYGrid(xScale, yScale)}
+ {this.renderYTicks(xScale, yScale)}
+ {bubbles}
+ </g>
+ </svg>;
+ }
+}
+
+BubbleChart.defaultProps = {
+ sizeRange: [5, 45],
+ displayXGrid: true,
+ displayYGrid: true,
+ displayXTicks: true,
+ displayYTicks: true,
+ tooltips: [],
+ padding: [10, 10, 10, 10],
+ formatXTick: d => d,
+ formatYTick: d => d
+};
+
+BubbleChart.propTypes = {
+ sizeRange: React.PropTypes.arrayOf(React.PropTypes.number),
+ displayXGrid: React.PropTypes.bool,
+ displayYGrid: React.PropTypes.bool,
+ padding: React.PropTypes.arrayOf(React.PropTypes.number),
+ formatXTick: React.PropTypes.func,
+ formatYTick: React.PropTypes.func
+};
--- /dev/null
+import d3 from 'd3';
+import React from 'react';
+
+export class LineChart extends React.Component {
+ constructor (props) {
+ super();
+ this.state = { width: props.width, height: props.height };
+ }
+
+ componentDidMount () {
+ if (!this.props.width || !this.props.height) {
+ this.handleResize();
+ window.addEventListener('resize', this.handleResize.bind(this));
+ }
+ }
+
+ componentWillUnmount () {
+ if (!this.props.width || !this.props.height) {
+ window.removeEventListener('resize', this.handleResize.bind(this));
+ }
+ }
+
+ handleResize () {
+ let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+ let newWidth = this.props.width || boundingClientRect.width;
+ let newHeight = this.props.height || boundingClientRect.height;
+ this.setState({ width: newWidth, height: newHeight });
+ }
+
+ renderBackdrop (xScale, yScale) {
+ if (!this.props.displayBackdrop) {
+ return null;
+ }
+
+ let area = d3.svg.area()
+ .x(d => xScale(d.x))
+ .y0(yScale.range()[0])
+ .y1(d => yScale(d.y))
+ .interpolate(this.props.interpolate);
+
+ let data = this.props.data;
+ if (this.props.backdropConstraints) {
+ let c = this.props.backdropConstraints;
+ data = data.filter(d => c[0] <= d.x && d.x <= c[1]);
+ }
+
+ // TODO extract styling
+ return <path d={area(data)} fill="#4b9fd5" fillOpacity="0.2"/>;
+ }
+
+ renderPoints (xScale, yScale) {
+ if (!this.props.displayPoints) {
+ return null;
+ }
+ let points = this.props.data.map((point, index) => {
+ let x = xScale(point.x);
+ let y = yScale(point.y);
+ return <circle key={index} className="line-chart-point" r="3" cx={x} cy={y}/>;
+ });
+ return <g>{points}</g>;
+ }
+
+ renderVerticalGrid (xScale, yScale) {
+ if (!this.props.displayVerticalGrid) {
+ return null;
+ }
+ let lines = this.props.data.map((point, index) => {
+ let x = xScale(point.x);
+ let y1 = yScale.range()[0];
+ let y2 = yScale(point.y);
+ return <line key={index} className="line-chart-grid" x1={x} x2={x} y1={y1} y2={y2}/>;
+ });
+ return <g>{lines}</g>;
+ }
+
+ renderXTicks (xScale, yScale) {
+ if (!this.props.xTicks.length) {
+ return null;
+ }
+ let ticks = this.props.xTicks.map((tick, index) => {
+ let point = this.props.data[index];
+ let x = xScale(point.x);
+ let y = yScale.range()[0];
+ return <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em">{tick}</text>;
+ });
+ return <g>{ticks}</g>;
+ }
+
+ renderXValues (xScale, yScale) {
+ if (!this.props.xValues.length) {
+ return null;
+ }
+ let ticks = this.props.xValues.map((value, index) => {
+ let point = this.props.data[index];
+ let x = xScale(point.x);
+ let y = yScale(point.y);
+ return <text key={index} className="line-chart-tick" x={x} y={y} dy="-1em">{value}</text>;
+ });
+ return <g>{ticks}</g>;
+ }
+
+ renderLine (xScale, yScale) {
+ let path = d3.svg.line()
+ .x(d => xScale(d.x))
+ .y(d => yScale(d.y))
+ .interpolate(this.props.interpolate);
+
+ return <path className="line-chart-path" d={path(this.props.data)}/>;
+ }
+
+ render () {
+ if (!this.state.width || !this.state.height) {
+ return <div/>;
+ }
+
+ 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 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]);
+
+ 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)}
+ {this.renderBackdrop(xScale, yScale)}
+ {this.renderLine(xScale, yScale)}
+ {this.renderPoints(xScale, yScale)}
+ {this.renderXTicks(xScale, yScale)}
+ {this.renderXValues(xScale, yScale)}
+ </g>
+ </svg>;
+ }
+}
+
+LineChart.defaultProps = {
+ displayBackdrop: true,
+ displayPoints: true,
+ displayVerticalGrid: true,
+ xTicks: [],
+ xValues: [],
+ padding: [10, 10, 10, 10],
+ interpolate: 'basis'
+};
+
+LineChart.propTypes = {
+ data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
+ xValues: React.PropTypes.arrayOf(React.PropTypes.any),
+ padding: React.PropTypes.arrayOf(React.PropTypes.number),
+ backdropConstraints: React.PropTypes.arrayOf(React.PropTypes.number)
+};
--- /dev/null
+import d3 from 'd3';
+import React from 'react';
+
+export class Timeline extends React.Component {
+ constructor (props) {
+ super();
+ this.state = { width: props.width, height: props.height };
+ }
+
+ componentDidMount () {
+ if (!this.props.width || !this.props.height) {
+ this.handleResize();
+ window.addEventListener('resize', this.handleResize.bind(this));
+ }
+ }
+
+ componentWillUnmount () {
+ if (!this.props.width || !this.props.height) {
+ window.removeEventListener('resize', this.handleResize.bind(this));
+ }
+ }
+
+ handleResize () {
+ let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+ let newWidth = this.props.width || boundingClientRect.width;
+ let newHeight = this.props.height || boundingClientRect.height;
+ this.setState({ width: newWidth, height: newHeight });
+ }
+
+ renderBackdrop (xScale, yScale, maxY) {
+ if (!this.props.displayBackdrop) {
+ return null;
+ }
+
+ let area = d3.svg.area()
+ .x(d => xScale(d.date))
+ .y0(maxY)
+ .y1(d => yScale(d.value))
+ .interpolate(this.props.interpolate);
+
+ // TODO extract styling
+ return <path d={area(this.props.snapshots)} fill="#4b9fd5" fillOpacity="0.2"/>;
+ }
+
+ renderLine (xScale, yScale) {
+ let path = d3.svg.line()
+ .x(d => xScale(d.date))
+ .y(d => yScale(d.value))
+ .interpolate(this.props.interpolate);
+
+ // TODO extract styling
+ return <path d={path(this.props.snapshots)} stroke="#4b9fd5" strokeWidth={this.props.lineWidth} fill="none"/>;
+ }
+
+ render () {
+ if (!this.state.width || !this.state.height) {
+ return <div/>;
+ }
+
+ let maxY = d3.max(this.props.snapshots, d => d.value);
+ let xScale = d3.time.scale()
+ .domain(d3.extent(this.props.snapshots, d => d.date))
+ .range([0, this.state.width - this.props.lineWidth]);
+ let yScale = d3.scale.linear()
+ .domain([0, maxY])
+ .range([this.state.height, 0]);
+
+ return <svg width={this.state.width} height={this.state.height}>
+ <g transform={`translate(${this.props.lineWidth / 2}, ${this.props.lineWidth / 2})`}>
+ {this.renderBackdrop(xScale, yScale, maxY)}
+ {this.renderLine(xScale, yScale)}
+ </g>
+ </svg>;
+ }
+}
+
+Timeline.defaultProps = {
+ lineWidth: 2,
+ displayBackdrop: true,
+ interpolate: 'basis'
+};
+
+Timeline.propTypes = {
+ snapshots: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+};
--- /dev/null
+import $ from 'jquery';
+import _ from 'underscore';
+import d3 from 'd3';
+import React from 'react';
+
+
+const SIZE_SCALE = d3.scale.linear()
+ .domain([3, 15])
+ .range([11, 18])
+ .clamp(true);
+
+
+function mostCommitPrefix (strings) {
+ var sortedStrings = strings.slice(0).sort(),
+ firstString = sortedStrings[0],
+ firstStringLength = firstString.length,
+ lastString = sortedStrings[sortedStrings.length - 1],
+ i = 0;
+ while (i < firstStringLength && firstString.charAt(i) === lastString.charAt(i)) {
+ i++;
+ }
+ var prefix = firstString.substr(0, i),
+ lastPrefixPart = _.last(prefix.split(/[\s\\\/]/));
+ return prefix.substr(0, prefix.length - lastPrefixPart.length);
+}
+
+
+export class TreemapRect extends React.Component {
+ render () {
+ let tooltipAttrs = {};
+ if (this.props.tooltip) {
+ tooltipAttrs = {
+ 'data-toggle': 'tooltip',
+ 'title': this.props.tooltip
+ };
+ }
+ let cellStyles = {
+ left: this.props.x,
+ top: this.props.y,
+ width: this.props.width,
+ height: this.props.height,
+ backgroundColor: this.props.fill,
+ fontSize: SIZE_SCALE(this.props.width / this.props.label.length),
+ lineHeight: `${this.props.height}px`
+ };
+ return <div className="treemap-cell" {...tooltipAttrs} style={cellStyles}>
+ <div className="treemap-inner" dangerouslySetInnerHTML={{ __html: this.props.label }}
+ style={{ maxWidth: this.props.width }}/>
+ </div>;
+ }
+}
+
+TreemapRect.propTypes = {
+ x: React.PropTypes.number.isRequired,
+ y: React.PropTypes.number.isRequired,
+ width: React.PropTypes.number.isRequired,
+ height: React.PropTypes.number.isRequired,
+ fill: React.PropTypes.string.isRequired
+};
+
+
+export class Treemap extends React.Component {
+ constructor (props) {
+ super();
+ this.state = { width: props.width, height: props.height };
+ }
+
+ componentDidMount () {
+ if (!this.props.width || !this.props.height) {
+ this.handleResize();
+ window.addEventListener('resize', this.handleResize.bind(this));
+ }
+ this.initTooltips();
+ }
+
+ componentDidUpdate () {
+ this.initTooltips();
+ }
+
+ componentWillUnmount () {
+ if (!this.props.width || !this.props.height) {
+ window.removeEventListener('resize', this.handleResize.bind(this));
+ }
+ }
+
+ initTooltips () {
+ $('[data-toggle="tooltip"]', React.findDOMNode(this))
+ .tooltip({ container: 'body', placement: 'top', html: true });
+ }
+
+ handleResize () {
+ let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+ let newWidth = this.props.width || boundingClientRect.width;
+ let newHeight = this.props.height || boundingClientRect.height;
+ this.setState({ width: newWidth, height: newHeight });
+ }
+
+ render () {
+ if (!this.state.width || !this.state.height || !this.props.items.length) {
+ return <div> </div>;
+ }
+
+ let sizeScale = d3.scale.linear()
+ .domain([0, d3.max(this.props.items, d => d.size)])
+ .range([5, 45]);
+ let treemap = d3.layout.treemap()
+ .round(true)
+ .value(d => sizeScale(d.size))
+ .size([this.state.width, 360]);
+ let nodes = treemap
+ .nodes({ children: this.props.items })
+ .filter(d => !d.children);
+
+ let prefix = mostCommitPrefix(this.props.labels),
+ prefixLength = prefix.length;
+
+ let rectangles = nodes.map((node, index) => {
+ let label = prefixLength ? `${prefix}<br>${this.props.labels[index].substr(prefixLength)}` :
+ this.props.labels[index];
+ let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null;
+ return <TreemapRect key={index}
+ x={node.x}
+ y={node.y}
+ width={node.dx}
+ height={node.dy}
+ fill={node.color}
+ label={label}
+ prefix={prefix}
+ tooltip={tooltip}/>;
+ });
+
+ return <div className="sonar-d3">
+ <div className="treemap-container" style={{ width: this.state.width, height: this.state.height }}>
+ {rectangles}
+ </div>
+ </div>;
+ }
+}
+
+Treemap.propTypes = {
+ labels: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
+ tooltips: React.PropTypes.arrayOf(React.PropTypes.string)
+};
--- /dev/null
+import $ from 'jquery';
+import _ from 'underscore';
+import React from 'react';
+
+export class Word extends React.Component {
+ render () {
+ let tooltipAttrs = {};
+ if (this.props.tooltip) {
+ tooltipAttrs = {
+ 'data-toggle': 'tooltip',
+ 'title': this.props.tooltip
+ };
+ }
+ return <a {...tooltipAttrs} style={{ fontSize: this.props.size }} href={this.props.link}>{this.props.text}</a>;
+ }
+}
+
+
+export class WordCloud extends React.Component {
+ componentDidMount () {
+ this.initTooltips();
+ }
+
+ componentDidUpdate () {
+ this.initTooltips();
+ }
+
+ initTooltips () {
+ $('[data-toggle="tooltip"]', React.findDOMNode(this))
+ .tooltip({ container: 'body', placement: 'bottom', html: true });
+ }
+
+ render () {
+ let len = this.props.items.length;
+ let sortedItems = _.sortBy(this.props.items, (item, idx) => {
+ let index = len - idx;
+ return (index % 2) * (len - index) + index / 2;
+ });
+
+ let sizeScale = d3.scale.linear()
+ .domain([0, d3.max(this.props.items, d => d.size)])
+ .range(this.props.sizeRange);
+ let words = sortedItems
+ .map((item, index) => <Word key={index}
+ text={item.text}
+ size={sizeScale(item.size)}
+ link={item.link}
+ tooltip={item.tooltip}/>);
+ return <div className="word-cloud">{words}</div>;
+ }
+}
+
+WordCloud.defaultProps = {
+ sizeRange: [10, 24]
+};
+
+WordCloud.propTypes = {
+ sizeRange: React.PropTypes.arrayOf(React.PropTypes.number)
+};
--- /dev/null
+import React from 'react';
+import Avatar from './avatar';
+
+export default class Assignee extends React.Component {
+ render () {
+ let avatar = this.props.user ?
+ <span className="spacer-right"><Avatar email={this.props.user.email} size={16}/></span> : null;
+ let name = this.props.user ? this.props.user.name : window.t('unassigned');
+ return <span>{avatar}{name}</span>;
+ }
+}
if (!this.props.severity) {
return null;
}
- return (
- <span>
- <SeverityIcon severity={this.props.severity}/>
-
- {window.t('severity', this.props.severity)}
- </span>
- );
+ return <span>
+ <span className="spacer-right">
+ <SeverityIcon severity={this.props.severity}/>
+ </span>
+ {window.t('severity', this.props.severity)}
+ </span>;
}
});
-export function getProjectUrl(project) {
+export function getProjectUrl (project) {
if (typeof project !== 'string') {
throw new TypeError('Project ID or KEY should be passed');
}
- return `${window.baseUrl}/overview?id=${encodeURIComponent(project)}`;
+ return `${window.baseUrl}/dashboard?id=${encodeURIComponent(project)}`;
+}
+
+export function componentIssuesUrl (componentKey, query) {
+ let serializedQuery = Object.keys(query).map(criterion => {
+ return `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`;
+ }).join('|');
+ return window.baseUrl + '/component_issues?id=' + encodeURIComponent(componentKey) + '#' + serializedQuery;
}
padding: 0 10px;
.box-sizing(border-box);
}
+
+
+.flex-columns {
+ display: flex;
+}
+
+.flex-column + .flex-column {
+ margin-left: 20px;
+}
+
+.flex-column-half {
+ width: 50%;
+}
+
+.flex-column-third {
+ width: ~"calc(100% / 3)";
+}
@import (reference) "../variables";
@import (reference) "../mixins";
+@import (reference) "../init/links";
.sonar-d3 {
& > .icon-chevron-right { margin-right: 10px; }
}
+
+.word-cloud {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ align-items: center;
+
+ a {
+ padding: 5px;
+ .link-no-underline;
+ }
+}
text-decoration: none;
background-color: @background;
border-radius: 4px;
+ letter-spacing: 0.04em;
}
.tooltip-arrow {
@import (reference) "../variables";
@import (reference) "../mixins";
@import (reference) "../init/type";
+@import (reference) "../init/links";
.overview {
display: flex;
font-weight: 300;
}
-.overview-gate-box-error { background-color: @red; }
+.overview-gate-box-error {
+ background-color: @red;
+}
-.overview-gate-box-warn { background-color: @orange; }
+.overview-gate-box-warn {
+ background-color: @orange;
+}
-.overview-gate-box-ok { background-color: @green; }
+.overview-gate-box-ok {
+ background-color: @green;
+}
.overview-gate-conditions {
line-height: 70px;
}
.overview-title {
+ margin-bottom: 20px;
font-size: 18px;
font-weight: 400;
}
}
-.overview-title + .overview-cards:not(:empty) {
- margin-top: 20px;
-}
-
.overview-leak-period {
margin-left: 10px;
font-size: 14px;
}
.overview-nutshell {
- padding: 50px 30px;
+ padding: 50px 30px 0;
+
+ .overview-card {
+ padding-bottom: 25px;
+ }
}
.overview-cards {
}
}
+.overview-card-section {
+ cursor: pointer;
+
+ &:hover, &.active {
+ border-bottom: 4px solid #2c3946;
+ }
+}
+
.overview-measure {
font-size: 28px;
}
.text-ellipsis;
}
}
+
+.overview-domain-dark {
+ background-color: #2c3946;
+ color: mix(#fff, #2c3946, 75%);
+
+ a {
+ color: @blue;
+ border-bottom-color: @darkBlue;
+
+ &:hover, &:focus {
+ border-bottom-color: @blue;
+ }
+ }
+
+ .overview-title {
+ color: mix(#fff, #2c3946, 75%);
+ }
+
+ table.data.zebra > tbody > tr:nth-child(odd) {
+ background-color: mix(#fff, #2c3946, 5%);;
+ }
+}
+
+.overview-domain-section {
+ padding: 50px 30px;
+}
+
+.overview-domain-header {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: 20px;;
+ padding: 50px 30px 0;
+
+ .overview-title {
+ flex: 1;
+ margin: 0;
+ padding: 0;
+ }
+}
+
+.overview-timeline {
+ position: relative;
+
+ .line-chart {
+
+ }
+
+ .line-chart-grid {
+ shape-rendering: crispedges;
+ stroke: #384653;
+ }
+
+ .line-chart-path {
+ fill: none;
+ stroke-width: 2;
+ stroke: @blue;
+ }
+
+ .line-chart-point {
+ fill: @blue;
+ stroke: none;
+ }
+
+ .line-chart-tick {
+ fill: mix(#fff, #2c3946);
+ font-size: 11px;
+ text-anchor: middle;
+ }
+}
+
+.overview-timeline-select {
+ height: @formControlHeight;
+ border: 1px solid mix(#fff, #2c3946);
+ background-color: transparent;
+ color: mix(#fff, #2c3946);
+}
+
+.overview-bubble-chart {
+ .bubble-chart-tick {
+ fill: mix(#fff, #2c3946);
+ font-size: 11px;
+ text-anchor: middle;
+ }
+
+ .bubble-chart-tick-y {
+ text-anchor: end;
+ }
+
+ .bubble-chart-bubble {
+ stroke: @blue;
+ fill: @blue;
+ fill-opacity: 0.2;
+ transition: fill-opacity 0.2s ease;
+
+ &:hover {
+ fill-opacity: 0.5;
+ }
+ }
+}
+
+.overview-treemap {
+ .overview-domain-header {
+ padding-left: 0;
+ padding-right: 0;
+ }
+}
+
+.overview-chart-placeholder {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ align-content: center;
+}