@@ -98,7 +98,9 @@ export default class AddMemberForm extends React.PureComponent { | |||
</div> | |||
<footer className="modal-foot"> | |||
<div> | |||
<button type="submit">{translate('organization.members.add_to_members')}</button> | |||
<button type="submit" disabled={!this.state.selectedMember}> | |||
{translate('organization.members.add_to_members')} | |||
</button> | |||
<button type="reset" className="button-link" onClick={this.closeForm}> | |||
{translate('cancel')} | |||
</button> |
@@ -61,6 +61,7 @@ exports[`should render and open the modal 2`] = ` | |||
> | |||
<div> | |||
<button | |||
disabled={true} | |||
type="submit" | |||
> | |||
organization.members.add_to_members |
@@ -19,13 +19,13 @@ | |||
*/ | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import { some, sortBy } from 'lodash'; | |||
import { sortBy } from 'lodash'; | |||
import { AutoSizer } from 'react-virtualized'; | |||
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline'; | |||
import GraphsTooltips from './GraphsTooltips'; | |||
import StaticGraphsLegend from './StaticGraphsLegend'; | |||
import GraphsLegendStatic from './GraphsLegendStatic'; | |||
import { formatMeasure, getShortType } from '../../../helpers/measures'; | |||
import { EVENT_TYPES, isCustomGraph } from '../utils'; | |||
import { EVENT_TYPES, hasHistoryData, isCustomGraph } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Analysis, MeasureHistory } from '../types'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
@@ -52,7 +52,7 @@ type State = { | |||
tooltipXPos: ?number | |||
}; | |||
export default class StaticGraphs extends React.PureComponent { | |||
export default class GraphsHistory extends React.PureComponent { | |||
props: Props; | |||
state: State = { | |||
tooltipIdx: null, | |||
@@ -99,13 +99,12 @@ export default class StaticGraphs extends React.PureComponent { | |||
return []; | |||
}; | |||
hasSeriesData = () => some(this.props.series, serie => serie.data && serie.data.length > 2); | |||
updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) => | |||
this.setState({ selectedDate, tooltipXPos, tooltipIdx }); | |||
render() { | |||
const { loading } = this.props; | |||
const { graph, series } = this.props; | |||
if (loading) { | |||
return ( | |||
@@ -117,7 +116,7 @@ export default class StaticGraphs extends React.PureComponent { | |||
); | |||
} | |||
if (!this.hasSeriesData()) { | |||
if (!hasHistoryData(series)) { | |||
return ( | |||
<div className="project-activity-graph-container"> | |||
<div className="note text-center"> | |||
@@ -132,10 +131,9 @@ export default class StaticGraphs extends React.PureComponent { | |||
} | |||
const { selectedDate, tooltipIdx, tooltipXPos } = this.state; | |||
const { graph, series } = this.props; | |||
return ( | |||
<div className="project-activity-graph-container"> | |||
<StaticGraphsLegend series={series} /> | |||
<GraphsLegendStatic series={series} /> | |||
<div className="project-activity-graph"> | |||
<AutoSizer> | |||
{({ height, width }) => ( |
@@ -25,7 +25,7 @@ type Props = { | |||
series: Array<{ name: string, translatedName: string, style: string }> | |||
}; | |||
export default function StaticGraphsLegend({ series }: Props) { | |||
export default function GraphsLegendStatic({ series }: Props) { | |||
return ( | |||
<div className="project-activity-graph-legends"> | |||
{series.map(serie => ( |
@@ -19,9 +19,9 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { some } from 'lodash'; | |||
import { AutoSizer } from 'react-virtualized'; | |||
import ZoomTimeLine from '../../../components/charts/ZoomTimeLine'; | |||
import { hasHistoryData } from '../utils'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
type Props = { | |||
@@ -35,37 +35,31 @@ type Props = { | |||
updateGraphZoom: (from: ?Date, to: ?Date) => void | |||
}; | |||
export default class GraphsZoom extends React.PureComponent { | |||
props: Props; | |||
hasHistoryData = () => some(this.props.series, serie => serie.data && serie.data.length > 2); | |||
render() { | |||
const { loading } = this.props; | |||
if (loading || !this.hasHistoryData()) { | |||
return null; | |||
} | |||
return ( | |||
<div className="project-activity-graph-zoom"> | |||
<AutoSizer disableHeight={true}> | |||
{({ width }) => ( | |||
<ZoomTimeLine | |||
endDate={this.props.graphEndDate} | |||
height={64} | |||
width={width} | |||
interpolate="linear" | |||
leakPeriodDate={this.props.leakPeriodDate} | |||
metricType={this.props.metricsType} | |||
padding={[0, 10, 18, 60]} | |||
series={this.props.series} | |||
showAreas={this.props.showAreas} | |||
startDate={this.props.graphStartDate} | |||
updateZoom={this.props.updateGraphZoom} | |||
/> | |||
)} | |||
</AutoSizer> | |||
</div> | |||
); | |||
export default function GraphsZoom(props: Props) { | |||
const { loading } = props; | |||
if (loading || !hasHistoryData(props.series)) { | |||
return null; | |||
} | |||
return ( | |||
<div className="project-activity-graph-zoom"> | |||
<AutoSizer disableHeight={true}> | |||
{({ width }) => ( | |||
<ZoomTimeLine | |||
endDate={props.graphEndDate} | |||
height={64} | |||
width={width} | |||
interpolate="linear" | |||
leakPeriodDate={props.leakPeriodDate} | |||
metricType={props.metricsType} | |||
padding={[0, 10, 18, 60]} | |||
series={props.series} | |||
showAreas={props.showAreas} | |||
startDate={props.graphStartDate} | |||
updateZoom={props.updateGraphZoom} | |||
/> | |||
)} | |||
</AutoSizer> | |||
</div> | |||
); | |||
} |
@@ -125,6 +125,7 @@ export default class ProjectActivityApp extends React.PureComponent { | |||
leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()} | |||
loading={this.props.graphLoading} | |||
measuresHistory={measuresHistory} | |||
metrics={this.props.metrics} | |||
metricsType={this.getMetricType()} | |||
project={this.props.project.key} | |||
query={query} |
@@ -22,7 +22,7 @@ import React from 'react'; | |||
import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash'; | |||
import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader'; | |||
import GraphsZoom from './GraphsZoom'; | |||
import StaticGraphs from './StaticGraphs'; | |||
import GraphsHistory from './GraphsHistory'; | |||
import { | |||
datesQueryChanged, | |||
generateSeries, | |||
@@ -30,7 +30,7 @@ import { | |||
historyQueryChanged | |||
} from '../utils'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
import type { Analysis, MeasureHistory, Query } from '../types'; | |||
import type { Analysis, MeasureHistory, Metric, Query } from '../types'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
type Props = { | |||
@@ -38,6 +38,7 @@ type Props = { | |||
leakPeriodDate: Date, | |||
loading: boolean, | |||
measuresHistory: Array<MeasureHistory>, | |||
metrics: Array<Metric>, | |||
metricsType: string, | |||
project: string, | |||
query: Query, | |||
@@ -136,8 +137,13 @@ export default class ProjectActivityGraphs extends React.PureComponent { | |||
const { series } = this.state; | |||
return ( | |||
<div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"> | |||
<ProjectActivityGraphsHeader graph={query.graph} updateQuery={this.props.updateQuery} /> | |||
<StaticGraphs | |||
<ProjectActivityGraphsHeader | |||
graph={query.graph} | |||
metrics={this.props.metrics} | |||
selectedMetrics={this.props.query.customMetrics} | |||
updateQuery={this.props.updateQuery} | |||
/> | |||
<GraphsHistory | |||
analyses={this.props.analyses} | |||
eventFilter={query.category} | |||
graph={query.graph} |
@@ -20,13 +20,17 @@ | |||
// @flow | |||
import React from 'react'; | |||
import Select from 'react-select'; | |||
import { GRAPH_TYPES } from '../utils'; | |||
import AddGraphMetric from './forms/AddGraphMetric'; | |||
import { isCustomGraph, GRAPH_TYPES } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Metric } from '../types'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
type Props = { | |||
updateQuery: RawQuery => void, | |||
graph: string | |||
graph: string, | |||
metrics: Array<Metric>, | |||
selectedMetrics: Array<string>, | |||
updateQuery: RawQuery => void | |||
}; | |||
export default class ProjectActivityGraphsHeader extends React.PureComponent { | |||
@@ -38,6 +42,11 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent { | |||
} | |||
}; | |||
handleAddMetric = (metric: string) => { | |||
const selectedMetrics = [...this.props.selectedMetrics, metric]; | |||
this.props.updateQuery({ customMetrics: selectedMetrics }); | |||
}; | |||
render() { | |||
const selectOptions = GRAPH_TYPES.map(graph => ({ | |||
label: translate('project_activity.graphs', graph), | |||
@@ -54,6 +63,13 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent { | |||
options={selectOptions} | |||
onChange={this.handleGraphChange} | |||
/> | |||
{isCustomGraph(this.props.graph) && | |||
<AddGraphMetric | |||
addMetric={this.handleAddMetric} | |||
className="spacer-left" | |||
metrics={this.props.metrics} | |||
selectedMetrics={this.props.selectedMetrics} | |||
/>} | |||
</header> | |||
); | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import StaticGraphs from '../StaticGraphs'; | |||
import GraphsHistory from '../GraphsHistory'; | |||
const ANALYSES = [ | |||
{ | |||
@@ -95,20 +95,20 @@ const DEFAULT_PROPS = { | |||
}; | |||
it('should show a loading view', () => { | |||
expect(shallow(<StaticGraphs {...DEFAULT_PROPS} loading={true} />)).toMatchSnapshot(); | |||
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} loading={true} />)).toMatchSnapshot(); | |||
}); | |||
it('should show that there is no data', () => { | |||
expect(shallow(<StaticGraphs {...DEFAULT_PROPS} series={EMPTY_SERIES} />)).toMatchSnapshot(); | |||
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} series={EMPTY_SERIES} />)).toMatchSnapshot(); | |||
}); | |||
it('should correctly render a graph', () => { | |||
expect(shallow(<StaticGraphs {...DEFAULT_PROPS} />)).toMatchSnapshot(); | |||
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />)).toMatchSnapshot(); | |||
}); | |||
it('should correctly filter events', () => { | |||
expect(shallow(<StaticGraphs {...DEFAULT_PROPS} />).instance().getEvents()).toMatchSnapshot(); | |||
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />).instance().getEvents()).toMatchSnapshot(); | |||
expect( | |||
shallow(<StaticGraphs {...DEFAULT_PROPS} eventFilter="OTHER" />).instance().getEvents() | |||
shallow(<GraphsHistory {...DEFAULT_PROPS} eventFilter="OTHER" />).instance().getEvents() | |||
).toMatchSnapshot(); | |||
}); |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import StaticGraphsLegend from '../StaticGraphsLegend'; | |||
import GraphsLegendStatic from '../GraphsLegendStatic'; | |||
const SERIES = [ | |||
{ name: 'bugs', translatedName: 'Bugs', style: '2', data: [] }, | |||
@@ -27,5 +27,5 @@ const SERIES = [ | |||
]; | |||
it('should render correctly the list of series', () => { | |||
expect(shallow(<StaticGraphsLegend series={SERIES} />)).toMatchSnapshot(); | |||
expect(shallow(<GraphsLegendStatic series={SERIES} />)).toMatchSnapshot(); | |||
}); |
@@ -29,7 +29,7 @@ exports[`should correctly render a graph 1`] = ` | |||
<div | |||
className="project-activity-graph-container" | |||
> | |||
<StaticGraphsLegend | |||
<GraphsLegendStatic | |||
series={ | |||
Array [ | |||
Object { |
@@ -179,6 +179,15 @@ exports[`should render correctly 1`] = ` | |||
}, | |||
] | |||
} | |||
metrics={ | |||
Array [ | |||
Object { | |||
"key": "code_smells", | |||
"name": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
metricsType="INT" | |||
project="org.sonarsource.sonarqube:sonarqube" | |||
query={ |
@@ -8,7 +8,7 @@ exports[`should render correctly the graph and legends 1`] = ` | |||
graph="overview" | |||
updateQuery={[Function]} | |||
/> | |||
<StaticGraphs | |||
<GraphsHistory | |||
analyses={ | |||
Array [ | |||
Object { |
@@ -0,0 +1,147 @@ | |||
/* | |||
* 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 Modal from 'react-modal'; | |||
import Select from 'react-select'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import type { Metric } from '../../types'; | |||
type Props = { | |||
addMetric: (metric: string) => void, | |||
className?: string, | |||
metrics: Array<Metric>, | |||
selectedMetrics: Array<string> | |||
}; | |||
type State = { | |||
open: boolean, | |||
selectedMetric?: string | |||
}; | |||
export default class AddGraphMetric extends React.PureComponent { | |||
props: Props; | |||
state: State = { | |||
open: false | |||
}; | |||
getMetricsType = () => { | |||
if (this.props.selectedMetrics.length > 0) { | |||
const metric = this.props.metrics.find( | |||
metric => metric.key === this.props.selectedMetrics[0] | |||
); | |||
return metric && metric.type; | |||
} | |||
}; | |||
getMetricsOptions = () => { | |||
const selectedType = this.getMetricsType(); | |||
return this.props.metrics | |||
.filter(metric => { | |||
if (metric.hidden) { | |||
return false; | |||
} | |||
if (selectedType) { | |||
return selectedType === metric.type && !this.props.selectedMetrics.includes(metric.key); | |||
} | |||
return true; | |||
}) | |||
.map((metric: Metric) => ({ | |||
value: metric.key, | |||
label: metric.custom ? metric.name : translate('metric', metric.key, 'name') | |||
})); | |||
}; | |||
openForm = () => { | |||
this.setState({ | |||
open: true | |||
}); | |||
}; | |||
closeForm = () => { | |||
this.setState({ | |||
open: false, | |||
selectedMetric: undefined | |||
}); | |||
}; | |||
handleChange = (option: { value: string, label: string }) => | |||
this.setState({ selectedMetric: option.value }); | |||
handleSubmit = (e: Object) => { | |||
e.preventDefault(); | |||
if (this.state.selectedMetric) { | |||
this.props.addMetric(this.state.selectedMetric); | |||
this.closeForm(); | |||
} | |||
}; | |||
renderModal() { | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
contentLabel="graph metric add" | |||
className="modal" | |||
overlayClassName="modal-overlay" | |||
onRequestClose={this.closeForm}> | |||
<header className="modal-head"> | |||
<h2>{translate('project_activity.graphs.custom.add_metric')}</h2> | |||
</header> | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-body"> | |||
<div className="modal-large-field"> | |||
<label>{translate('project_activity.graphs.custom.search')}</label> | |||
<Select | |||
autofocus={true} | |||
className="Select-big" | |||
clearable={false} | |||
noResultsText={translate('no_results')} | |||
onChange={this.handleChange} | |||
options={this.getMetricsOptions()} | |||
placeholder="" | |||
searchable={true} | |||
value={this.state.selectedMetric} | |||
/> | |||
</div> | |||
</div> | |||
<footer className="modal-foot"> | |||
<div> | |||
<button type="submit" disabled={!this.state.selectedMetric}> | |||
{translate('project_activity.graphs.custom.add')} | |||
</button> | |||
<button type="reset" className="button-link" onClick={this.closeForm}> | |||
{translate('cancel')} | |||
</button> | |||
</div> | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<button className={this.props.className} onClick={this.openForm}> | |||
{translate('project_activity.graphs.custom.add')} | |||
{this.state.open && this.renderModal()} | |||
</button> | |||
); | |||
} | |||
} |
@@ -37,6 +37,8 @@ export type HistoryItem = { date: Date, value: string }; | |||
export type MeasureHistory = { metric: string, history: Array<HistoryItem> }; | |||
export type Metric = { | |||
custom?: boolean, | |||
hidden?: boolean, | |||
key: string, | |||
name: string, | |||
type: string |
@@ -65,6 +65,11 @@ export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean = | |||
return previousFrom !== nextFrom || previousTo !== nextTo; | |||
}; | |||
export const hasHistoryData = (series: Array<Serie>) => | |||
series.some( | |||
serie => serie.data && serie.data.length > 2 && serie.data.some(p => p.y || p.y === 0) | |||
); | |||
export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => | |||
prevQuery.graph !== nextQuery.graph; | |||
@@ -1,13 +0,0 @@ | |||
.Select-big .Select-control { | |||
padding-top: 4px; | |||
padding-bottom: 4px; | |||
} | |||
.Select-big .Select-placeholder { | |||
margin-top: 4px; | |||
margin-bottom: 4px; | |||
} | |||
.Select-big .Select-value-label { | |||
margin-top: 5px; | |||
} |
@@ -24,7 +24,6 @@ import { debounce } from 'lodash'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import UsersSelectSearchOption from './UsersSelectSearchOption'; | |||
import UsersSelectSearchValue from './UsersSelectSearchValue'; | |||
import './UsersSelectSearch.css'; | |||
export type Option = { | |||
login: string, |
@@ -62,11 +62,9 @@ export default class UsersSelectSearchOption extends React.PureComponent { | |||
onMouseEnter={this.handleMouseEnter} | |||
onMouseMove={this.handleMouseMove} | |||
title={user.name}> | |||
<div className="little-spacer-bottom little-spacer-top"> | |||
<Avatar hash={user.avatar} email={user.email} name={user.name} size={AVATAR_SIZE} /> | |||
<strong className="spacer-left">{this.props.children}</strong> | |||
<span className="note little-spacer-left">{user.login}</span> | |||
</div> | |||
<Avatar hash={user.avatar} email={user.email} name={user.name} size={AVATAR_SIZE} /> | |||
<strong className="spacer-left">{this.props.children}</strong> | |||
<span className="note little-spacer-left">{user.login}</span> | |||
</div> | |||
); | |||
} |
@@ -7,25 +7,21 @@ exports[`should render correctly with email instead of hash 1`] = ` | |||
onMouseMove={[Function]} | |||
title="Administrator" | |||
> | |||
<div | |||
className="little-spacer-bottom little-spacer-top" | |||
<Connect(Avatar) | |||
email="admin@admin.ch" | |||
name="Administrator" | |||
size={20} | |||
/> | |||
<strong | |||
className="spacer-left" | |||
> | |||
<Connect(Avatar) | |||
email="admin@admin.ch" | |||
name="Administrator" | |||
size={20} | |||
/> | |||
<strong | |||
className="spacer-left" | |||
> | |||
Administrator | |||
</strong> | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
admin | |||
</span> | |||
</div> | |||
Administrator | |||
</strong> | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
admin | |||
</span> | |||
</div> | |||
`; | |||
@@ -36,24 +32,20 @@ exports[`should render correctly without all parameters 1`] = ` | |||
onMouseMove={[Function]} | |||
title="Administrator" | |||
> | |||
<div | |||
className="little-spacer-bottom little-spacer-top" | |||
<Connect(Avatar) | |||
hash="7daf6c79d4802916d83f6266e24850af" | |||
name="Administrator" | |||
size={20} | |||
/> | |||
<strong | |||
className="spacer-left" | |||
> | |||
Administrator | |||
</strong> | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
<Connect(Avatar) | |||
hash="7daf6c79d4802916d83f6266e24850af" | |||
name="Administrator" | |||
size={20} | |||
/> | |||
<strong | |||
className="spacer-left" | |||
> | |||
Administrator | |||
</strong> | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
admin | |||
</span> | |||
</div> | |||
admin | |||
</span> | |||
</div> | |||
`; |
@@ -124,7 +124,10 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
} else if (props.metricType === 'LEVEL') { | |||
return this.getLevelScale(availableHeight); | |||
} else { | |||
return scaleLinear().range([availableHeight, 0]).domain([0, max(flatData, d => d.y)]).nice(); | |||
return scaleLinear() | |||
.range([availableHeight, 0]) | |||
.domain([0, max(flatData, d => d.y) || 0]) | |||
.nice(); | |||
} | |||
}; | |||
@@ -346,6 +346,29 @@ | |||
opacity: 0.5; | |||
} | |||
.Select-big .Select-control { | |||
padding-top: 4px; | |||
padding-bottom: 4px; | |||
} | |||
.Select-big .Select-placeholder { | |||
margin-top: 4px; | |||
margin-bottom: 4px; | |||
} | |||
.Select-big .Select-value-label { | |||
display: inline-block; | |||
margin-top: 5px; | |||
} | |||
.Select-big .Select-option { | |||
padding: 4px 8px; | |||
} | |||
.Select-big img { | |||
padding-top: 0; | |||
} | |||
.Select--multi .Select-value-icon, | |||
.Select--multi .Select-value-label { | |||
display: inline-block; |
@@ -1288,7 +1288,10 @@ project_activity.graphs.overview=Overview | |||
project_activity.graphs.coverage=Coverage | |||
project_activity.graphs.duplications=Duplications | |||
project_activity.graphs.custom=Custom | |||
project_activity.graphs.custom.add=Add metric | |||
project_activity.graphs.custom.add_metric=Add a metric | |||
project_activity.graphs.custom.no_history=There is no historical data to show, please add more metrics to your graph. | |||
project_activity.graphs.custom.search=Search for a metric by name | |||
project_activity.custom_metric.covered_lines=Covered Lines | |||