import { getJSON } from '../helpers/request.js';
-export function getFacet (query, facet) {
+export function getFacets (query, facets) {
let url = baseUrl + '/api/issues/search';
- let data = _.extend({}, query, { facets: facet, ps: 1, additionalFields: '_all' });
+ let data = _.extend({}, query, { facets: facets.join(), ps: 1, additionalFields: '_all' });
return getJSON(url, data).then(r => {
- return { facet: r.facets[0].values, response: r };
+ return { facets: r.facets, response: r };
+ });
+}
+
+
+export function getFacet (query, facet) {
+ return getFacets(query, [facet]).then(r => {
+ return { facet: r.facets[0].values, response: r.response };
});
}
}
-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 });
- });
+export function extractAssignees (facet, response) {
+ return facet.map(item => {
+ let user = _.findWhere(response.users, { login: item.val });
+ return _.extend(item, { user });
});
}
+export function getAssignees (query) {
+ return getFacet(query, 'assignees').then(r => extractAssignees(r.facet, r.response));
+}
+
+
export function getIssuesCount (query) {
let url = baseUrl + '/api/issues/search';
let data = _.extend({}, query, { ps: 1, facetMode: 'debt' });
},
renderDonut (measure) {
- if (this.props.metric !== 'PERCENT') {
+ if (this.props.type !== 'PERCENT') {
return null;
}
export default React.createClass({
render() {
let url = getComponentIssuesUrl(this.props.component, this.props.params);
- return <a href={url}>{this.props.children}</a>;
+ return <a className={this.props.className} href={url}>{this.props.children}</a>;
}
});
</tr>;
});
- return <div className="overview-domain-section">
- <DomainHeader title="Issues to Review"/>
- <table className="data zebra">
- <tbody>{rows}</tbody>
- </table>
- </div>;
+ return <table className="data zebra">
+ <tbody>{rows}</tbody>
+ </table>;
}
}
+++ /dev/null
-import React from 'react';
-import { DomainBubbleChart } from '../domain/bubble-chart';
-
-
-export class IssuesBubbleChart extends React.Component {
- render () {
- return <DomainBubbleChart {...this.props}
- xMetric="violations"
- yMetric="sqale_index"
- sizeMetrics={['blocker_violations', 'critical_violations']}/>;
- }
-}
--- /dev/null
+import React from 'react';
+
+import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../../helpers/measures';
+import DrilldownLink from '../helpers/drilldown-link';
+import IssuesLink from '../helpers/issues-link';
+import { getShortType } from '../helpers/metrics';
+import SeverityHelper from '../../../components/shared/severity-helper';
+
+
+export const IssueMeasure = React.createClass({
+ renderLeak () {
+ if (!this.props.leakPeriodDate) {
+ return null;
+ }
+
+ let leak = this.props.leak[this.props.metric];
+ let added = this.props.leak[this.props.leakMetric];
+ let removed = added - leak;
+
+ return <div className="overview-detailed-measure-leak">
+ <ul className="list-inline">
+ <li className="text-danger">
+ <IssuesLink className="text-danger overview-detailed-measure-value"
+ component={this.props.component.key} params={{ resolved: 'false' }}>
+ +{formatMeasure(added, getShortType(this.props.type))}
+ </IssuesLink>
+ </li>
+ <li className="text-success">
+ <span className="text-success overview-detailed-measure-value">
+ -{formatMeasure(removed, getShortType(this.props.type))}
+ </span>
+ </li>
+ </ul>
+ </div>;
+ },
+
+ render () {
+ let measure = this.props.measures[this.props.metric];
+ if (measure == null) {
+ return null;
+ }
+
+ return <div className="overview-detailed-measure">
+ <div className="overview-detailed-measure-nutshell">
+ <span className="overview-detailed-measure-name">{localizeMetric(this.props.metric)}</span>
+ <span className="overview-detailed-measure-value">
+ <DrilldownLink component={this.props.component.key} metric={this.props.metric}>
+ {formatMeasure(measure, this.props.type)}
+ </DrilldownLink>
+ </span>
+ {this.props.children}
+ </div>
+ {this.renderLeak()}
+ </div>;
+ }
+});
+
+
+export const AddedRemovedMeasure = React.createClass({
+ renderLeak () {
+ if (!this.props.leakPeriodDate) {
+ return null;
+ }
+
+ let leak = this.props.leak[this.props.metric];
+ let added = this.props.leak[this.props.leakMetric];
+ let removed = added - leak;
+
+ return <div className="overview-detailed-measure-leak">
+ <ul>
+ <li style={{ display: 'flex', alignItems: 'baseline' }}>
+ <small className="flex-1 text-left">Added</small>
+ <IssuesLink className="text-danger"
+ component={this.props.component.key} params={{ resolved: 'false' }}>
+ <span className="overview-detailed-measure-value">
+ {formatMeasure(added, getShortType(this.props.type))}
+ </span>
+ </IssuesLink>
+ </li>
+ <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'baseline' }}>
+ <small className="flex-1 text-left">Removed</small>
+ <span className="text-success">
+ {formatMeasure(removed, getShortType(this.props.type))}
+ </span>
+ </li>
+ </ul>
+ </div>;
+ },
+
+ render () {
+ let measure = this.props.measures[this.props.metric];
+ if (measure == null) {
+ return null;
+ }
+
+ return <div className="overview-detailed-measure">
+ <div className="overview-detailed-measure-nutshell">
+ <span className="overview-detailed-measure-name">{localizeMetric(this.props.metric)}</span>
+ <span className="overview-detailed-measure-value">
+ <DrilldownLink component={this.props.component.key} metric={this.props.metric}>
+ {formatMeasure(measure, this.props.type)}
+ </DrilldownLink>
+ </span>
+ {this.props.children}
+ </div>
+ {this.renderLeak()}
+ </div>;
+ }
+});
+
+
+export const OnNewCodeMeasure = React.createClass({
+ renderLeak () {
+ if (!this.props.leakPeriodDate) {
+ return null;
+ }
+
+ let onNewCode = this.props.leak[this.props.leakMetric];
+
+ return <div className="overview-detailed-measure-leak">
+ <ul>
+ <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'center' }}>
+ <small className="flex-1 text-left">On New Code</small>
+ <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
+ <span className="overview-detailed-measure-value">
+ {formatMeasure(onNewCode, getShortType(this.props.type))}
+ </span>
+ </IssuesLink>
+ </li>
+ </ul>
+ </div>;
+ },
+
+ render () {
+ let measure = this.props.measures[this.props.metric];
+ if (measure == null) {
+ return null;
+ }
+
+ return <div className="overview-detailed-measure">
+ <div className="overview-detailed-measure-nutshell">
+ <span className="overview-detailed-measure-name">{localizeMetric(this.props.metric)}</span>
+ <span className="overview-detailed-measure-value">
+ <DrilldownLink component={this.props.component.key} metric={this.props.metric}>
+ {formatMeasure(measure, this.props.type)}
+ </DrilldownLink>
+ </span>
+ {this.props.children}
+ </div>
+ {this.renderLeak()}
+ </div>;
+ }
+});
+
+
+export const SeverityMeasure = React.createClass({
+ getMetric () {
+ return this.props.severity.toLowerCase() + '_violations';
+ },
+
+ getNewMetric () {
+ return 'new_' + this.props.severity.toLowerCase() + '_violations';
+ },
+
+
+ renderLeak () {
+ if (!this.props.leakPeriodDate) {
+ return null;
+ }
+
+ let leak = this.props.leak[this.getMetric()];
+ let added = this.props.leak[this.getNewMetric()];
+ let removed = added - leak;
+
+ return <div className="overview-detailed-measure-leak">
+ <ul>
+ <li style={{ display: 'flex', alignItems: 'baseline' }}>
+ <small className="flex-1 text-left">Added</small>
+ <IssuesLink className="text-danger"
+ component={this.props.component.key} params={{ resolved: 'false' }}>
+ <span className="overview-detailed-measure-value">
+ {formatMeasure(added, 'SHORT_INT')}
+ </span>
+ </IssuesLink>
+ </li>
+ <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'baseline' }}>
+ <small className="flex-1 text-left">Removed</small>
+ <span className="text-success">
+ {formatMeasure(removed, 'SHORT_INT')}
+ </span>
+ </li>
+ </ul>
+ </div>;
+ },
+
+ render () {
+ let measure = this.props.measures[this.getMetric()];
+ if (measure == null) {
+ return null;
+ }
+
+ return <div className="overview-detailed-measure">
+ <div className="overview-detailed-measure-nutshell">
+ <span className="overview-detailed-measure-name">
+ <SeverityHelper severity={this.props.severity}/>
+ </span>
+ <span className="overview-detailed-measure-value">
+ <DrilldownLink component={this.props.component.key} metric={this.getMetric()}>
+ {formatMeasure(measure, 'SHORT_INT')}
+ </DrilldownLink>
+ </span>
+ {this.props.children}
+ </div>
+ {this.renderLeak()}
+ </div>;
+ }
+});
+import _ from 'underscore';
+import d3 from 'd3';
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 { getMeasuresAndVariations } from '../../../api/measures';
+import { DetailedMeasure } from '../common-components';
+import { DomainTimeline } from '../timeline/domain-timeline';
+import { DomainTreemap } from '../domain/treemap';
+import { DomainBubbleChart } from '../domain/bubble-chart';
+import { getPeriodLabel, getPeriodDate } from './../helpers/period-label';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics';
+import { Legend } from '../common-components';
+import { CHART_COLORS_RANGE_PERCENT } from '../../../helpers/constants';
+import { IssueMeasure, AddedRemovedMeasure, OnNewCodeMeasure, SeverityMeasure } from './issue-measure';
+import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../../helpers/measures';
+import IssuesLink from '../helpers/issues-link';
+import { Measure } from '../main/components';
+import { getMetricName } from '../helpers/metrics';
+import Tags from './tags';
+import Assignees from './assignees';
+import { getFacets, extractAssignees } from '../../../api/issues';
+import StatusHelper from '../../../components/shared/status-helper';
+import Rating from '../helpers/rating';
+import DrilldownLink from '../helpers/drilldown-link';
-import { getSeverities, getTags, getAssignees } from '../../../api/issues';
+const KNOWN_METRICS = ['violations', 'sqale_index', 'sqale_rating', 'sqale_debt_ratio', 'blocker_violations',
+ 'critical_violations', 'major_violations', 'minor_violations', 'info_violations', 'confirmed_issues'];
-export default class OverviewDomain extends React.Component {
- constructor () {
- super();
- this.state = { severities: [], tags: [], assignees: [] };
- }
- componentDidMount () {
+export const IssuesMain = React.createClass({
+ mixins: [TooltipsMixin],
+
+ getInitialState() {
+ return {
+ ready: false,
+ leakPeriodLabel: getPeriodLabel(this.props.component.periods, this.props.leakPeriodIndex),
+ leakPeriodDate: getPeriodDate(this.props.component.periods, this.props.leakPeriodIndex)
+ };
+ },
+
+ componentDidMount() {
Promise.all([
- this.requestIssuesSeverities(),
- this.requestTags(),
- this.requestAssignees()
+ this.requestMeasures(),
+ this.requestIssues()
]).then(responses => {
- this.setState({
- severities: responses[0],
- tags: responses[1],
- assignees: responses[2]
- });
+ let measures = this.getMeasuresValues(responses[0], 'value');
+ let leak = this.getMeasuresValues(responses[0], 'var' + this.props.leakPeriodIndex);
+ let tags = this.getFacet(responses[1].facets, 'tags');
+ let assignees = extractAssignees(this.getFacet(responses[1].facets, 'assignees'), responses[1].response);
+ this.setState({ ready: true, measures, leak, tags, assignees });
});
- }
+ },
- requestSeverities () {
- return getSeverities({ resolved: 'false', componentUuids: this.props.component.id });
- }
+ getMeasuresValues (measures, fieldKey) {
+ let values = {};
+ Object.keys(measures).forEach(measureKey => {
+ values[measureKey] = measures[measureKey][fieldKey];
+ });
+ return values;
+ },
- requestTags () {
- return getTags({ resolved: 'false', componentUuids: this.props.component.id });
- }
+ getMetricsForDomain() {
+ return this.props.metrics
+ .filter(metric => ['Issues', 'Technical Debt'].indexOf(metric.domain) !== -1)
+ .map(metric => metric.key);
+ },
- requestAssignees () {
- return getAssignees({ statuses: 'OPEN,REOPENED', componentUuids: this.props.component.id });
- }
+ getMetricsForTimeline() {
+ return filterMetricsForDomains(this.props.metrics, ['Issues', 'Technical Debt']);
+ },
+
+ getAllMetricsForTimeline() {
+ return filterMetrics(this.props.metrics);
+ },
+
+ requestMeasures () {
+ return getMeasuresAndVariations(this.props.component.key, this.getMetricsForDomain());
+ },
+
+ getFacet (facets, facetKey) {
+ return _.findWhere(facets, { property: facetKey }).values;
+ },
+
+ requestIssues () {
+ return getFacets({
+ componentUuids: this.props.component.id,
+ resolved: 'false'
+ }, ['tags', 'assignees']);
+ },
+
+ renderLoading () {
+ return <div className="text-center">
+ <i className="spinner spinner-margin"/>
+ </div>;
+ },
+
+ renderLegend () {
+ return <Legend leakPeriodDate={this.state.leakPeriodDate} leakPeriodLabel={this.state.leakPeriodLabel}/>;
+ },
+
+ renderOtherMeasures() {
+ let metrics = filterMetricsForDomains(this.props.metrics, ['Issues', 'Technical Debt'])
+ .filter(metric => KNOWN_METRICS.indexOf(metric.key) === -1)
+ .map(metric => {
+ return <DetailedMeasure key={metric.key} {...this.props} {...this.state} metric={metric.key}
+ type={metric.type}/>;
+ });
+ return <div>{metrics}</div>;
+ },
render () {
+ if (!this.state.ready) {
+ return this.renderLoading();
+ }
+
+ let treemapScale = d3.scale.ordinal()
+ .domain([1, 2, 3, 4, 5])
+ .range(CHART_COLORS_RANGE_PERCENT);
+
+ let rating = formatMeasure(this.state.measures['sqale_rating'], 'RATING');
+
return <div className="overview-detailed-page">
+ <div className="overview-domain-charts">
+ <div className="overview-domain overview-domain-fixed-width">
+ <div className="overview-domain-header">
+ <div className="overview-title">Technical Debt Overview</div>
+ {this.renderLegend()}
+ </div>
- <div className="overview-domain-header">
- <h2 className="overview-title">Issues & Technical Debt</h2>
- </div>
+ <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
+ <div className="overview-detailed-measure">
+ <div className="overview-detailed-measure-nutshell">
+ <span className="overview-detailed-measure-name">SQALE Rating</span>
+ <span className="overview-detailed-measure-value">
+ <DrilldownLink component={this.props.component.key} metric="sqale_rating">
+ <Rating value={this.state.measures['sqale_rating']}/>
+ </DrilldownLink>
+ </span>
+ </div>
+ </div>
+ <AddedRemovedMeasure {...this.props} {...this.state}
+ metric="violations" leakMetric="new_violations" type="INT"/>
+ <AddedRemovedMeasure {...this.props} {...this.state}
+ metric="sqale_index" leakMetric="new_technical_debt" type="WORK_DUR"/>
+ <OnNewCodeMeasure {...this.props} {...this.state}
+ metric="sqale_debt_ratio" leakMetric="new_sqale_debt_ratio" type="PERCENT"/>
+ </div>
- <a className="overview-detailed-page-back" href="#">
- <i className="icon-chevron-left"/>
- </a>
+ <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
+ <SeverityMeasure {...this.props} {...this.state} severity="BLOCKER"/>
+ <SeverityMeasure {...this.props} {...this.state} severity="CRITICAL"/>
+ <SeverityMeasure {...this.props} {...this.state} severity="MAJOR"/>
+ <SeverityMeasure {...this.props} {...this.state} severity="MINOR"/>
+ <SeverityMeasure {...this.props} {...this.state} severity="INFO"/>
+ </div>
- <IssuesTimeline {...this.props}/>
+ <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
+ <div className="overview-detailed-measure">
+ <div className="overview-detailed-measure-nutshell">
+ <Tags {...this.props} tags={this.state.tags}/>
+ </div>
+ </div>
+ <div className="overview-detailed-measure">
+ <div className="overview-detailed-measure-nutshell">
+ <div className="overview-detailed-measure-name">
+ <StatusHelper status="OPEN"/> & <StatusHelper status="REOPENED"/> Issues
+ </div>
+ <div className="spacer-top">
+ <Assignees {...this.props} assignees={this.state.assignees}/>
+ </div>
+ </div>
+ </div>
+ </div>
- <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 className="overview-detailed-measures-list">
+ {this.renderOtherMeasures()}
+ </div>
</div>
+ <DomainBubbleChart {...this.props}
+ xMetric="violations"
+ yMetric="sqale_index"
+ sizeMetrics={['blocker_violations', 'critical_violations']}/>
</div>
- <IssuesBubbleChart {...this.props}/>
- <IssuesTreemap {...this.props}/>
+ <div className="overview-domain-charts">
+ <DomainTimeline {...this.props} {...this.state}
+ initialMetric="sqale_index"
+ metrics={this.getMetricsForTimeline()}
+ allMetrics={this.getAllMetricsForTimeline()}/>
+ <DomainTreemap {...this.props}
+ sizeMetric="ncloc"
+ colorMetric="sqale_rating"
+ scale={treemapScale}/>
+ </div>
</div>;
+
}
-}
+});
}
render () {
- return <div className="overview-domain-section">
- <DomainHeader title="Issues By Tag"/>
- <div>
- {this.renderWordCloud()}
- </div>
- </div>;
+ return this.renderWordCloud();
}
}
+++ /dev/null
-import React from 'react';
-
-import { DomainTimeline } from '../domain/timeline';
-import { filterMetricsForDomains } from '../helpers/metrics';
-
-
-const DOMAINS = ['Issues', 'Technical Debt'];
-
-
-export class IssuesTimeline extends React.Component {
- render () {
- return <DomainTimeline {...this.props}
- initialMetric="violations"
- metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
- }
-}
+++ /dev/null
-import d3 from 'd3';
-import React from 'react';
-
-import { DomainTreemap } from '../domain/treemap';
-
-
-const COLORS_5 = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
-
-
-export class IssuesTreemap extends React.Component {
- render () {
- let scale = d3.scale.ordinal()
- .domain([1, 2, 3, 4, 5])
- .range(COLORS_5);
- return <DomainTreemap {...this.props}
- sizeMetric="ncloc"
- colorMetric="sqale_rating"
- scale={scale}/>;
- }
-}
render () {
return <Domain>
- <DomainHeader title="Technical Debt"
+ <DomainHeader title="Technical Debt" linkTo="/issues"
leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
<DomainPanel domain="issues">
import { SizeMain } from './size/main';
import { DuplicationsMain } from './duplications/main';
import { CoverageMain } from './coverage/main';
+import { IssuesMain } from './issues/main';
import { getMetrics } from '../../api/metrics';
import { RouterMixin } from '../../components/router/router';
</div>;
},
+ renderIssues () {
+ return <div className="overview">
+ <IssuesMain {...this.props} {...this.state}/>
+ </div>;
+ },
+
render () {
if (!this.state.ready) {
return this.renderLoading();
return this.renderDuplications();
case '/tests':
return this.renderTests();
+ case '/issues':
+ return this.renderIssues();
default:
throw new Error('Unknown route: ' + this.state.route);
}
-define([
- 'libs/third-party/react',
- './status-icon'
-], function (React, StatusIcon) {
+import React from 'react';
+import StatusIcon from './status-icon';
- return React.createClass({
- render: function () {
- if (!this.props.status) {
- return null;
- }
- var resolution;
- if (this.props.resolution) {
- resolution = ' (' + window.t('issue.resolution', this.props.resolution) + ')';
- }
- return (
- <span>
+export default React.createClass({
+ render: function () {
+ if (!this.props.status) {
+ return null;
+ }
+ var resolution;
+ if (this.props.resolution) {
+ resolution = ' (' + window.t('issue.resolution', this.props.resolution) + ')';
+ }
+ return (
+ <span>
<StatusIcon status={this.props.status}/>
-
- {window.t('issue.status', this.props.status)}
- {resolution}
+
+ {window.t('issue.status', this.props.status)}
+ {resolution}
</span>
- );
- }
- });
-
+ );
+ }
});
background-color: @background;
border-radius: 4px;
letter-spacing: 0.04em;
+ overflow: hidden;
}
.tooltip-arrow {
margin-top: 40px;
}
+.overview-detailed-measures-list-inline {
+ display: flex;
+ border: none;
+ background: none;
+
+ .overview-detailed-measure {
+ flex: 1;
+ flex-direction: column;
+ border: 1px solid @barBorderColor;
+ }
+
+ .overview-detailed-measure-name {
+ margin-bottom: 8px;
+ flex: 0 1 auto;
+ font-weight: 500;
+ }
+
+ .overview-detailed-measure + .overview-detailed-measure {
+ margin-left: 10px;
+ }
+
+ .overview-detailed-measure-nutshell {
+ flex-flow: column nowrap;
+ justify-content: flex-start;
+ align-items: stretch;
+
+ .overview-detailed-measure-value {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+
+ .overview-detailed-measure-leak {
+ flex: 0 1 auto;
+ }
+}
+
.overview-detailed-measure {
display: flex;
background-color: #fff;
position: relative;
padding: 7px 10px;
- & & {
+ .overview-detailed-measure-nutshell,
+ .overview-detailed-measure-leak,
+ .overview-detailed-measure-chart {
padding-right: 0;
}
}