Преглед изворни кода

SONAR-9401 Add the overview chart to the project activity page

tags/6.5-M2
Grégoire Aubert пре 7 година
родитељ
комит
5b03b1c707

+ 37
- 1
server/sonar-web/src/main/js/api/time-machine.js Прегледај датотеку

@@ -38,7 +38,7 @@ type Response = {
export const getTimeMachineData = (
component: string,
metrics: Array<string>,
other?: {}
other?: { p?: number, ps?: number, from?: string, to?: string }
): Promise<Response> =>
getJSON('/api/measures/search_history', {
component,
@@ -46,3 +46,39 @@ export const getTimeMachineData = (
ps: 1000,
...other
});

export const getAllTimeMachineData = (
component: string,
metrics: Array<string>,
other?: { p?: number, ps?: number, from?: string, to?: string },
prev?: Response
): Promise<Response> =>
getTimeMachineData(component, metrics, other).then((r: Response) => {
const result = prev
? {
measures: prev.measures.map((measure, idx) => ({
...measure,
history: measure.history.concat(r.measures[idx].history)
})),
paging: r.paging
}
: r;

if (
// TODO Remove the sameAsPrevious condition when the webservice paging is working correctly ?
// Or keep it to be sure to not have an infinite loop ?
result.measures.every((measure, idx) => {
const equalToTotal = measure.history.length >= result.paging.total;
const sameAsPrevious = prev && measure.history.length === prev.measures[idx].history.length;
return equalToTotal || sameAsPrevious;
})
) {
return result;
}
return getAllTimeMachineData(
component,
metrics,
{ ...other, p: result.paging.pageIndex + 1 },
result
);
});

+ 49
- 34
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js Прегледај датотеку

@@ -22,9 +22,10 @@ import React from 'react';
import { groupBy } from 'lodash';
import moment from 'moment';
import ProjectActivityAnalysis from './ProjectActivityAnalysis';
import ProjectActivityPageFooter from './ProjectActivityPageFooter';
import FormattedDate from '../../../components/ui/FormattedDate';
import { translate } from '../../../helpers/l10n';
import type { Analysis } from '../types';
import type { Analysis, Paging } from '../types';

type Props = {
addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>,
@@ -33,49 +34,63 @@ type Props = {
canAdmin: boolean,
changeEvent: (event: string, name: string) => Promise<*>,
deleteAnalysis: (analysis: string) => Promise<*>,
deleteEvent: (analysis: string, event: string) => Promise<*>
deleteEvent: (analysis: string, event: string) => Promise<*>,
fetchMoreActivity: () => void,
paging?: Paging
};

export default function ProjectActivityAnalysesList(props: Props) {
if (props.analyses.length === 0) {
return <div className="note">{translate('no_results')}</div>;
return (
<div className="layout-page-side-outer project-activity-page-side-outer">
<div className="boxed-group boxed-group-inner">
<div className="note">{translate('no_results')}</div>
</div>
</div>
);
}

const firstAnalysis = props.analyses[0];

const byDay = groupBy(props.analyses, analysis => moment(analysis.date).startOf('day').valueOf());

return (
<div className="boxed-group boxed-group-inner">
<ul className="project-activity-days-list">
{Object.keys(byDay).map(day => (
<li
key={day}
className="project-activity-day"
data-day={moment(Number(day)).format('YYYY-MM-DD')}>
<div className="project-activity-date">
<FormattedDate date={Number(day)} format="LL" />
</div>
<div className="layout-page-side-outer project-activity-page-side-outer">
<div className="boxed-group boxed-group-inner">
<ul className="project-activity-days-list">
{Object.keys(byDay).map(day => (
<li
key={day}
className="project-activity-day"
data-day={moment(Number(day)).format('YYYY-MM-DD')}>
<div className="project-activity-date">
<FormattedDate date={Number(day)} format="LL" />
</div>

<ul className="project-activity-analyses-list">
{byDay[day] != null &&
byDay[day].map(analysis => (
<ProjectActivityAnalysis
addCustomEvent={props.addCustomEvent}
addVersion={props.addVersion}
analysis={analysis}
canAdmin={props.canAdmin}
changeEvent={props.changeEvent}
deleteAnalysis={props.deleteAnalysis}
deleteEvent={props.deleteEvent}
isFirst={analysis === firstAnalysis}
key={analysis.key}
/>
))}
</ul>
</li>
))}
</ul>

<ul className="project-activity-analyses-list">
{byDay[day] != null &&
byDay[day].map(analysis => (
<ProjectActivityAnalysis
addCustomEvent={props.addCustomEvent}
addVersion={props.addVersion}
analysis={analysis}
canAdmin={props.canAdmin}
changeEvent={props.changeEvent}
deleteAnalysis={props.deleteAnalysis}
deleteEvent={props.deleteEvent}
isFirst={analysis === firstAnalysis}
key={analysis.key}
/>
))}
</ul>
</li>
))}
</ul>
<ProjectActivityPageFooter
analyses={props.analyses}
fetchMoreActivity={props.fetchMoreActivity}
paging={props.paging}
/>
</div>
</div>
);
}

+ 74
- 22
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js Прегледај датотеку

@@ -20,16 +20,19 @@
// @flow
import React from 'react';
import Helmet from 'react-helmet';
import moment from 'moment';
import ProjectActivityPageHeader from './ProjectActivityPageHeader';
import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
import ProjectActivityPageFooter from './ProjectActivityPageFooter';
import ProjectActivityGraphs from './ProjectActivityGraphs';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import * as api from '../../../api/projectActivity';
import * as actions from '../actions';
import { parseQuery, serializeQuery, serializeUrlQuery } from '../utils';
import { getAllTimeMachineData } from '../../../api/time-machine';
import { getMetrics } from '../../../api/metrics';
import { GRAPHS_METRICS, parseQuery, serializeQuery, serializeUrlQuery } from '../utils';
import { translate } from '../../../helpers/l10n';
import './projectActivity.css';
import type { Analysis, Query, Paging } from '../types';
import type { Analysis, LeakPeriod, MeasureHistory, Metric, Query, Paging } from '../types';
import type { RawQuery } from '../../../helpers/query';

type Props = {
@@ -40,7 +43,11 @@ type Props = {

export type State = {
analyses: Array<Analysis>,
leakPeriod?: LeakPeriod,
loading: boolean,
measures: Array<*>,
metrics: Array<Metric>,
measuresHistory: Array<MeasureHistory>,
paging?: Paging,
query: Query
};
@@ -52,7 +59,14 @@ export default class ProjectActivityApp extends React.PureComponent {

constructor(props: Props) {
super(props);
this.state = { analyses: [], loading: true, query: parseQuery(props.location.query) };
this.state = {
analyses: [],
loading: true,
measures: [],
measuresHistory: [],
metrics: [],
query: parseQuery(props.location.query)
};
}

componentDidMount() {
@@ -85,6 +99,21 @@ export default class ProjectActivityApp extends React.PureComponent {
return api.getProjectActivity(parameters).catch(throwGlobalError);
};

fetchMetrics = (): Promise<Array<Metric>> => getMetrics().catch(throwGlobalError);

fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> =>
getAllTimeMachineData(this.props.project.key, metrics)
.then(({ measures }) =>
measures.map(measure => ({
metric: measure.metric,
history: measure.history.map(analysis => ({
date: moment(analysis.date).toDate(),
value: analysis.value
}))
}))
)
.catch(throwGlobalError);

fetchMoreActivity = () => {
const { paging, query } = this.state;
if (!paging) {
@@ -136,15 +165,29 @@ export default class ProjectActivityApp extends React.PureComponent {
.then(() => this.mounted && this.setState(actions.deleteAnalysis(analysis)))
.catch(throwGlobalError);

getMetricType = () => {
const metricKey = GRAPHS_METRICS[this.state.query.graph][0];
const metric = this.state.metrics.find(metric => metric.key === metricKey);
return metric ? metric.type : 'INT';
};

handleQueryChange() {
const query = parseQuery(this.props.location.query);
const graphMetrics = GRAPHS_METRICS[query.graph];
this.setState({ loading: true, query });
this.fetchActivity(query).then(({ analyses, paging }) => {

Promise.all([
this.fetchActivity(query),
this.fetchMetrics(),
this.fetchMeasuresHistory(graphMetrics)
]).then(response => {
if (this.mounted) {
this.setState({
analyses,
analyses: response[0].analyses,
loading: false,
paging
metrics: response[1],
measuresHistory: response[2],
paging: response[0].paging
});
}
});
@@ -174,21 +217,30 @@ export default class ProjectActivityApp extends React.PureComponent {

<ProjectActivityPageHeader category={query.category} updateQuery={this.updateQuery} />

<ProjectActivityAnalysesList
addCustomEvent={this.addCustomEvent}
addVersion={this.addVersion}
analyses={this.state.analyses}
canAdmin={canAdmin}
changeEvent={this.changeEvent}
deleteAnalysis={this.deleteAnalysis}
deleteEvent={this.deleteEvent}
/>

<ProjectActivityPageFooter
analyses={this.state.analyses}
fetchMoreActivity={this.fetchMoreActivity}
paging={this.state.paging}
/>
<div className="layout-page project-activity-page">
<ProjectActivityAnalysesList
addCustomEvent={this.addCustomEvent}
addVersion={this.addVersion}
analyses={this.state.analyses}
canAdmin={canAdmin}
changeEvent={this.changeEvent}
deleteAnalysis={this.deleteAnalysis}
deleteEvent={this.deleteEvent}
fetchMoreActivity={this.fetchMoreActivity}
paging={this.state.paging}
/>

<ProjectActivityGraphs
analyses={this.state.analyses}
leakPeriod={this.state.leakPeriod}
loading={this.state.loading}
measuresHistory={this.state.measuresHistory}
metricsType={this.getMetricType()}
project={this.props.project.key}
query={query}
updateQuery={this.updateQuery}
/>
</div>
</div>
);
}

+ 52
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js Прегледај датотеку

@@ -0,0 +1,52 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
import StaticGraphs from './StaticGraphs';
import type { RawQuery } from '../../../helpers/query';
import type { Analysis, MeasureHistory, Query } from '../types';

type Props = {
analyses: Array<Analysis>,
loading: boolean,
measuresHistory: Array<MeasureHistory>,
metricsType: string,
project: string,
query: Query,
updateQuery: RawQuery => void
};

export default function ProjectActivityGraphs(props: Props) {
return (
<div className="project-activity-layout-page-main">
<div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
<ProjectActivityGraphsHeader graph={props.query.graph} updateQuery={props.updateQuery} />
<StaticGraphs
analyses={props.analyses}
loading={props.loading}
measuresHistory={props.measuresHistory}
metricsType={props.metricsType}
project={props.project}
/>
</div>
</div>
);
}

+ 60
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js Прегледај датотеку

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import Select from 'react-select';
import { GRAPH_TYPES } from '../utils';
import { translate } from '../../../helpers/l10n';
import type { RawQuery } from '../../../helpers/query';

type Props = {
updateQuery: RawQuery => void,
graph: string
};

export default class ProjectActivityGraphsHeader extends React.PureComponent {
props: Props;

handleGraphChange = (option: { value: string }) => {
if (option.value !== this.props.graph) {
this.props.updateQuery({ graph: option.value });
}
};

render() {
const selectOptions = GRAPH_TYPES.map(graph => ({
label: translate('project_activity.graphs', graph),
value: graph
}));

return (
<header className="page-header">
<Select
className="input-medium"
clearable={false}
searchable={false}
value={this.props.graph}
options={selectOptions}
onChange={this.handleGraphChange}
/>
</header>
);
}
}

+ 9
- 15
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js Прегледај датотеку

@@ -43,21 +43,15 @@ export default class ProjectActivityPageHeader extends React.PureComponent {

return (
<header className="page-header">
<div className="page-actions">
<Select
className="input-medium"
placeholder={translate('filter_verb') + '...'}
clearable={true}
searchable={false}
value={this.props.category}
options={selectOptions}
onChange={this.handleCategoryChange}
/>
</div>

<div className="page-description">
{translate('project_activity.page.description')}
</div>
<Select
className="input-medium"
placeholder={translate('project_activity.filter_events') + '...'}
clearable={true}
searchable={false}
value={this.props.category}
options={selectOptions}
onChange={this.handleCategoryChange}
/>
</header>
);
}

+ 113
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js Прегледај датотеку

@@ -0,0 +1,113 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import moment from 'moment';
import { some, sortBy } from 'lodash';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
import StaticGraphsLegend from './StaticGraphsLegend';
import ResizeHelper from '../../../components/common/ResizeHelper';
import { formatMeasure, getShortType } from '../../../helpers/measures';
import { translate } from '../../../helpers/l10n';
import type { Analysis, MeasureHistory } from '../types';

type Props = {
analyses: Array<Analysis>,
loading: boolean,
measuresHistory: Array<MeasureHistory>,
metricsType: string
};

export default class StaticGraphs extends React.PureComponent {
props: Props;

getEvents = () => {
const events = this.props.analyses.reduce((acc, analysis) => {
return acc.concat(
analysis.events.map(event => ({
className: event.category,
name: event.name,
date: moment(analysis.date).toDate()
}))
);
}, []);
return sortBy(events, 'date');
};

getSeries = () =>
sortBy(this.props.measuresHistory, 'metric').map(measure => ({
name: measure.metric,
data: measure.history.map(analysis => ({
x: analysis.date,
y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value)
}))
}));

hasHistoryData = () =>
some(this.props.measuresHistory, measure => measure.history && measure.history.length > 2);

render() {
const { loading } = this.props;

if (loading) {
return (
<div className="project-activity-graph-container">
<div className="text-center">
<i className="spinner" />
</div>
</div>
);
}

if (!this.hasHistoryData()) {
return (
<div className="project-activity-graph-container">
<div className="note text-center">
{translate('component_measures.no_history')}
</div>
</div>
);
}

const { metricsType } = this.props;
const formatValue = value => formatMeasure(value, metricsType);
const formatYTick = tick => formatMeasure(tick, getShortType(metricsType));
const series = this.getSeries();
return (
<div className="project-activity-graph-container">
<StaticGraphsLegend series={series} />
<div className="project-activity-graph">
<ResizeHelper>
<AdvancedTimeline
basisCurve={false}
series={series}
metricType={metricsType}
events={this.getEvents()}
interpolate="linear"
formatValue={formatValue}
formatYTick={formatYTick}
leakPeriodDate={this.props.leakPeriodDate}
padding={[25, 25, 30, 60]}
/>
</ResizeHelper>
</div>
</div>
);
}
}

+ 42
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js Прегледај датотеку

@@ -0,0 +1,42 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import classNames from 'classnames';
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
import { translate } from '../../../helpers/l10n';

type Props = {
series: Array<{ name: string }>
};

export default function StaticGraphsLegend({ series }: Props) {
return (
<div className="project-activity-graph-legends">
{series.map((serie, idx) => (
<span className="big-spacer-left big-spacer-right" key={serie.name}>
<ChartLegendIcon
className={classNames('spacer-right line-chart-legend', 'line-chart-legend-' + idx)}
/>
{translate('metric', serie.name, 'name')}
</span>
))}
</div>
);
}

+ 55
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css Прегледај датотеку

@@ -1,3 +1,58 @@
.project-activity-page {
min-height: 600px;
height: calc(100vh - 250px);
}

.project-activity-page-side-outer {
width: 400px;
overflow: auto;
}

.project-activity-page-side-outer .boxed-group {
margin-bottom: 0;
}

.project-activity-layout-page-main {
flex-grow: 1;
min-width: 640px;
padding-left: 20px;
display: flex;
}

.project-activity-layout-page-main-inner {
min-width: 640px;
max-width: 880px;
margin-bottom: 0px;
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
}

.project-activity-list {
max-width: 400px;
}

.project-activity-graph-container {
padding: 10px 0;
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
}

.project-activity-graph {
flex: 1;
max-height: 500px;
}

.project-activity-graph-legends {
flex-grow: 0;
padding-bottom: 16px;
text-align: center;
}

.project-activity-days-list {}

.project-activity-day {

+ 20
- 2
server/sonar-web/src/main/js/apps/projectActivity/types.js Прегледај датотеку

@@ -32,6 +32,23 @@ export type Analysis = {
events: Array<Event>
};

export type LeakPeriod = {
date: string,
index: number,
mode: string,
parameter: string
};

export type HistoryItem = { date: Date, value: string };

export type MeasureHistory = { metric: string, history: Array<HistoryItem> };

export type Metric = {
key: string,
name: string,
type: string
};

export type Paging = {
pageIndex: number,
pageSize: number,
@@ -39,6 +56,7 @@ export type Paging = {
};

export type Query = {
project: string,
category: string
category: string,
graph: string,
project: string
};

+ 16
- 9
server/sonar-web/src/main/js/apps/projectActivity/utils.js Прегледај датотеку

@@ -22,19 +22,26 @@ import { cleanQuery, parseAsString, serializeString } from '../../helpers/query'
import type { Query } from './types';
import type { RawQuery } from '../../helpers/query';

export const GRAPH_TYPES = ['overview'];
export const GRAPHS_METRICS = { overview: ['bugs', 'vulnerabilities', 'code_smells'] };

export const parseQuery = (urlQuery: RawQuery): Query => ({
project: parseAsString(urlQuery['id']),
category: parseAsString(urlQuery['category'])
category: parseAsString(urlQuery['category']),
graph: parseAsString(urlQuery['graph']) || 'overview',
project: parseAsString(urlQuery['id'])
});

export const serializeQuery = (query: Query): Query =>
export const serializeQuery = (query: Query): RawQuery =>
cleanQuery({
project: serializeString(query.project),
category: serializeString(query.category)
category: serializeString(query.category),
project: serializeString(query.project)
});

export const serializeUrlQuery = (query: Query): RawQuery =>
cleanQuery({
id: serializeString(query.project),
category: serializeString(query.category)
export const serializeUrlQuery = (query: Query): RawQuery => {
const graph = query.graph === 'overview' ? '' : query.graph;
return cleanQuery({
category: serializeString(query.category),
graph: serializeString(graph),
id: serializeString(query.project)
});
};

+ 217
- 0
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js Прегледај датотеку

@@ -0,0 +1,217 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import { flatten } from 'lodash';
import { extent, max } from 'd3-array';
import { scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import { line as d3Line, curveBasis } from 'd3-shape';

type Point = { x: Date, y: number | string };

type Serie = { name: string, data: Array<Point> };

type Event = { className?: string, name: string, date: Date };

type Scale = Function;

type Props = {
basisCurve?: boolean,
events?: Array<Event>,
eventSize?: number,
formatYTick: number => string,
formatValue: number => string,
height: number,
width: number,
leakPeriodDate: Date,
padding: Array<number>,
series: Array<Serie>
};

export default class AdvancedTimeline extends React.PureComponent {
props: Props;

static defaultProps = {
eventSize: 8,
padding: [10, 10, 10, 10]
};

getRatingScale = (availableHeight: number) =>
scalePoint().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]);

getLevelScale = (availableHeight: number) =>
scalePoint().domain(['ERROR', 'WARN', 'OK']).range([availableHeight, 0]);

getYScale = (availableHeight: number, flatData: Array<Point>) => {
if (this.props.metricType === 'RATING') {
return this.getRatingScale(availableHeight);
} else if (this.props.metricType === 'LEVEL') {
return this.getLevelScale(availableHeight);
} else {
return scaleLinear().range([availableHeight, 0]).domain([0, max(flatData, d => d.y)]).nice();
}
};

getXScale = (availableWidth: number, flatData: Array<Point>) =>
scaleTime().domain(extent(flatData, d => d.x)).range([0, availableWidth]).clamp(true);

getScales = () => {
const availableWidth = this.props.width - this.props.padding[1] - this.props.padding[3];
const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
const flatData = flatten(this.props.series.map((serie: Serie) => serie.data));
return {
xScale: this.getXScale(availableWidth, flatData),
yScale: this.getYScale(availableHeight, flatData)
};
};

getEventMarker = (size: number) => {
const half = size / 2;
return `M${half} 0 L${size} ${half} L ${half} ${size} L0 ${half} L${half} 0 L${size} ${half}`;
};

renderHorizontalGrid = (xScale: Scale, yScale: Scale) => {
const hasTicks = typeof yScale.ticks === 'function';
const ticks = hasTicks ? yScale.ticks(4) : yScale.domain();

if (!ticks.length) {
ticks.push(yScale.domain()[1]);
}

return (
<g>
{ticks.map(tick => (
<g key={tick}>
<text
className="line-chart-tick line-chart-tick-x"
dx="-1em"
dy="0.3em"
textAnchor="end"
x={xScale.range()[0]}
y={yScale(tick)}>
{this.props.formatYTick(tick)}
</text>
<line
className="line-chart-grid"
x1={xScale.range()[0]}
x2={xScale.range()[1]}
y1={yScale(tick)}
y2={yScale(tick)}
/>
</g>
))}
</g>
);
};

renderTicks = (xScale: Scale, yScale: Scale) => {
const format = xScale.tickFormat(7);
const ticks = xScale.ticks(7);
const y = yScale.range()[0];
return (
<g>
{ticks.slice(0, -1).map((tick, index) => {
const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
const x = (xScale(tick) + xScale(nextTick)) / 2;
return (
<text key={index} className="line-chart-tick" x={x} y={y} dy="2em">
{format(tick)}
</text>
);
})}
</g>
);
};

renderLeak = (xScale: Scale, yScale: Scale) => {
if (!this.props.leakPeriodDate) {
return null;
}
const yScaleRange = yScale.range();
return (
<rect
x={xScale(this.props.leakPeriodDate)}
y={yScaleRange[yScaleRange.length - 1]}
width={xScale.range()[1] - xScale(this.props.leakPeriodDate)}
height={yScaleRange[0] - yScaleRange[yScaleRange.length - 1]}
fill="#fbf3d5"
/>
);
};

renderLines = (xScale: Scale, yScale: Scale) => {
const line = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y));
if (this.props.basisCurve) {
line.curve(curveBasis);
}
return (
<g>
{this.props.series.map((serie, idx) => (
<path
key={`${idx}-${serie.name}`}
className={classNames('line-chart-path', 'line-chart-path-' + idx)}
d={line(serie.data)}
/>
))}
</g>
);
};

renderEvents = (xScale: Scale, yScale: Scale) => {
const { events, eventSize } = this.props;
if (!events || !eventSize) {
return null;
}

const offset = eventSize / 2;
return (
<g>
{events.map((event, idx) => (
<path
d={this.getEventMarker(eventSize)}
className={classNames('line-chart-event', event.className)}
key={`${idx}-${event.date.getTime()}`}
transform={`translate(${xScale(event.date) - offset}, ${yScale.range()[0] - offset})`}
/>
))}
</g>
);
};

render() {
if (!this.props.width || !this.props.height) {
return <div />;
}

const { xScale, yScale } = this.getScales();
return (
<svg className="line-chart" width={this.props.width} height={this.props.height}>
<g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
{this.renderLeak(xScale, yScale)}
{this.renderHorizontalGrid(xScale, yScale)}
{this.renderTicks(xScale, yScale)}
{this.renderLines(xScale, yScale)}
{this.renderEvents(xScale, yScale)}
</g>
</svg>
);
}
}

+ 75
- 0
server/sonar-web/src/main/js/components/common/ResizeHelper.js Прегледај датотеку

@@ -0,0 +1,75 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import ReactDOM from 'react-dom';

type Props = {
children: React.Element<*>,
height?: number,
width?: number
};

type State = {
height?: number,
width?: number
};

export default class ResizeHelper extends React.PureComponent {
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = { height: props.height, width: props.width };
}

componentDidMount() {
if (this.isResizable()) {
this.handleResize();
window.addEventListener('resize', this.handleResize);
}
}

componentWillUnmount() {
if (this.isResizable()) {
window.removeEventListener('resize', this.handleResize);
}
}

isResizable = () => {
return !this.props.width || !this.props.height;
};

handleResize = () => {
const domNode = ReactDOM.findDOMNode(this);
if (domNode && domNode.parentElement) {
const boundingClientRect = domNode.parentElement.getBoundingClientRect();
this.setState({ width: boundingClientRect.width, height: boundingClientRect.height });
}
};

render() {
return React.cloneElement(this.props.children, {
width: this.props.width || this.state.width,
height: this.props.height || this.state.height
});
}
}

+ 40
- 0
server/sonar-web/src/main/js/components/icons-components/ChartLegendIcon.js Прегледај датотеку

@@ -0,0 +1,40 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';

type Props = { className?: string, size?: number };

export default function ChartLegendIcon({ className, size = 16 }: Props) {
/* eslint-disable max-len */
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width={size}
height={size}>
<path
style={{ fill: 'currentColor' }}
d="M14.325 7.143v1.714q0 0.357-0.25 0.607t-0.607 0.25h-10.857q-0.357 0-0.607-0.25t-0.25-0.607v-1.714q0-0.357 0.25-0.607t0.607-0.25h10.857q0.357 0 0.607 0.25t0.25 0.607z"
/>
</svg>
);
}

+ 48
- 2
server/sonar-web/src/main/less/components/graphics.less Прегледај датотеку

@@ -107,21 +107,67 @@
* Line Chart
*/

@defaultSerieColor: @darkBlue;
@serieColor1: @blue;
@serieColor2: #26adff;

.line-chart {
}

.line-chart-path {
fill: none;
stroke: @blue;
stroke: @defaultSerieColor;
stroke-width: 2px;

&.line-chart-path-1 {
stroke: @serieColor1
}

&.line-chart-path-2 {
stroke: @serieColor2;
}
}

.line-chart-legend {
color: @defaultSerieColor;

&.line-chart-legend-1 {
color: @serieColor1;
}

&.line-chart-legend-2 {
color: @serieColor2;
}
}

.line-chart-point {
fill: #fff;
stroke: @darkBlue;
stroke: @defaultSerieColor;
stroke-width: 2px;
}

.line-chart-event {
fill: #fff;
stroke: @defaultSerieColor;
stroke-width: 2px;

&.VERSION {
stroke: @green;
}

&.QUALITY_GATE {
stroke: @blue;
}

&.QUALITY_PROFILE {
stroke: @orange;
}

&.OTHER {
stroke: @purple;
}
}

.line-chart-backdrop {
}


+ 4
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties Прегледај датотеку

@@ -591,7 +591,6 @@ comparison.page=Compare
view_projects.page=Projects
portfolios.page=Portfolios
project_activity.page=Activity
project_activity.page.description=The page shows the history of project analyses.


#------------------------------------------------------------------------------
@@ -1271,8 +1270,8 @@ manual_rules.add_manual_rule=Add Manual Rule
#
#------------------------------------------------------------------------------

project_activity.project_analyzed=Project Analyzed
project_activity.add_version=Create Version
project_activity.project_analyzed=Project Analyzed
project_activity.remove_version=Remove Version
project_activity.remove_version.question=Are you sure you want to delete this version?
project_activity.change_version=Change Version
@@ -1282,6 +1281,9 @@ project_activity.remove_custom_event=Delete Event
project_activity.remove_custom_event.question=Are you sure you want to delete this event?
project_activity.delete_analysis=Delete Analysis
project_activity.delete_analysis.question=Are you sure you want to delete this analysis from the project history?
project_activity.filter_events=Filter events

project_activity.graphs.overview=Overview

project_history.col.year=Year
project_history.col.month=Month

Loading…
Откажи
Сачувај