Przeglądaj źródła

SONAR-11164 Add measures page for short-lived branches and PR

tags/7.5
Grégoire Aubert 5 lat temu
rodzic
commit
1b6dcc529a

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts Wyświetl plik

@@ -102,7 +102,7 @@ describe('groupByDomains', () => {
describe('parseQuery', () => {
it('should correctly parse the url query', () => {
expect(utils.parseQuery({})).toEqual({
metric: 'project_overview',
metric: utils.DEFAULT_METRIC,
selected: '',
view: utils.DEFAULT_VIEW
});

+ 96
- 50
server/sonar-web/src/main/js/apps/component-measures/components/App.tsx Wyświetl plik

@@ -22,10 +22,21 @@ import * as key from 'keymaster';
import { InjectedRouter } from 'react-router';
import Helmet from 'react-helmet';
import MeasureContentContainer from './MeasureContentContainer';
import MeasuresEmpty from './MeasuresEmpty';
import MeasureOverviewContainer from './MeasureOverviewContainer';
import Sidebar from '../sidebar/Sidebar';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery, Query } from '../utils';
import {
isProjectOverview,
hasBubbleChart,
parseQuery,
serializeQuery,
Query,
hasFullMeasures,
getMeasuresPageMetricKeys,
groupByDomains,
sortMeasures
} from '../utils';
import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
@@ -33,14 +44,13 @@ import {
translateWithParameters,
translate
} from '../../../helpers/l10n';
import { getDisplayMetrics } from '../../../helpers/measures';
import { RawQuery } from '../../../helpers/query';
import {
BranchLike,
ComponentMeasure,
CurrentUser,
MeasureEnhanced,
Metric,
CurrentUser,
Period
} from '../../../app/types';
import '../../../components/search-navigator.css';
@@ -117,8 +127,8 @@ export default class App extends React.PureComponent<Props, State> {

fetchMeasures = ({ branchLike, component, fetchMeasures, metrics }: Props) => {
this.setState({ loading: true });
const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key);

const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike);
fetchMeasures(component.key, filteredKeys, branchLike).then(
({ measures, leakPeriod }) => {
if (this.mounted) {
@@ -139,6 +149,34 @@ export default class App extends React.PureComponent<Props, State> {
);
};

getHelmetTitle = (query: Query, displayOverview: boolean, metric?: Metric) => {
if (displayOverview && query.metric) {
return isProjectOverview(query.metric)
? translate('component_measures.overview.project_overview.facet')
: translateWithParameters(
'component_measures.domain_x_overview',
getLocalizedMetricDomain(query.metric)
);
}
return metric ? metric.name : translate('layout.measures');
};

getSelectedMetric = (query: Query, displayOverview: boolean) => {
if (displayOverview) {
return undefined;
}
const metric = this.props.metrics[query.metric];
if (!metric) {
const domainMeasures = groupByDomains(this.state.measures);
const firstMeasure =
domainMeasures[0] && sortMeasures(domainMeasures[0].name, domainMeasures[0].measures)[0];
if (firstMeasure && typeof firstMeasure !== 'string') {
return firstMeasure.metric;
}
}
return metric;
};

updateQuery = (newQuery: Partial<Query>) => {
const query = serializeQuery({
...parseQuery(this.props.location.query),
@@ -154,16 +192,51 @@ export default class App extends React.PureComponent<Props, State> {
});
};

getHelmetTitle = (metric?: Metric) => {
if (metric && hasBubbleChart(metric.key)) {
return isProjectOverview(metric.key)
? translate('component_measures.overview.project_overview.facet')
: translateWithParameters(
'component_measures.domain_x_overview',
getLocalizedMetricDomain(metric.key)
);
renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => {
const { branchLike, component, fetchMeasures, metrics } = this.props;
const { leakPeriod, measures } = this.state;

if (measures.length === 0) {
return <MeasuresEmpty />;
}
return metric ? metric.name : translate('layout.measures');

if (displayOverview) {
return (
<MeasureOverviewContainer
branchLike={branchLike}
className="layout-page-main"
currentUser={this.props.currentUser}
domain={query.metric}
leakPeriod={leakPeriod}
metrics={metrics}
rootComponent={component}
router={this.props.router}
selected={query.selected}
updateQuery={this.updateQuery}
/>
);
}

if (!metric) {
return <MeasuresEmpty />;
}

return (
<MeasureContentContainer
branchLike={branchLike}
className="layout-page-main"
currentUser={this.props.currentUser}
fetchMeasures={fetchMeasures}
leakPeriod={leakPeriod}
metric={metric}
metrics={metrics}
rootComponent={component}
router={this.props.router}
selected={query.selected}
updateQuery={this.updateQuery}
view={query.view}
/>
);
};

render() {
@@ -171,14 +244,16 @@ export default class App extends React.PureComponent<Props, State> {
if (isLoading) {
return <i className="spinner spinner-margin" />;
}
const { branchLike, component, fetchMeasures, metrics } = this.props;
const { leakPeriod } = this.state;
const { branchLike } = this.props;
const { measures } = this.state;
const query = parseQuery(this.props.location.query);
const metric = metrics[query.metric];
const hasOverview = hasFullMeasures(branchLike);
const displayOverview = hasOverview && hasBubbleChart(query.metric);
const metric = this.getSelectedMetric(query, displayOverview);
return (
<div className="layout-page" id="component-measures">
<Suggestions suggestions="component_measures" />
<Helmet title={this.getHelmetTitle(metric)} />
<Helmet title={this.getHelmetTitle(query, displayOverview, metric)} />

<ScreenPositionHelper className="layout-page-side-outer">
{({ top }) => (
@@ -186,8 +261,9 @@ export default class App extends React.PureComponent<Props, State> {
<div className="layout-page-side-inner">
<div className="layout-page-filters">
<Sidebar
measures={this.state.measures}
selectedMetric={query.metric}
hasOverview={hasOverview}
measures={measures}
selectedMetric={metric ? metric.key : query.metric}
updateQuery={this.updateQuery}
/>
</div>
@@ -196,37 +272,7 @@ export default class App extends React.PureComponent<Props, State> {
)}
</ScreenPositionHelper>

{metric && (
<MeasureContentContainer
branchLike={branchLike}
className="layout-page-main"
currentUser={this.props.currentUser}
fetchMeasures={fetchMeasures}
leakPeriod={leakPeriod}
metric={metric}
metrics={metrics}
rootComponent={component}
router={this.props.router}
selected={query.selected}
updateQuery={this.updateQuery}
view={query.view}
/>
)}
{!metric &&
hasBubbleChart(query.metric) && (
<MeasureOverviewContainer
branchLike={branchLike}
className="layout-page-main"
currentUser={this.props.currentUser}
domain={query.metric}
leakPeriod={leakPeriod}
metrics={metrics}
rootComponent={component}
router={this.props.router}
selected={query.selected}
updateQuery={this.updateQuery}
/>
)}
{this.renderContent(displayOverview, query, metric)}
</div>
);
}

+ 9
- 5
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx Wyświetl plik

@@ -28,7 +28,8 @@ import Tooltip from '../../../components/controls/Tooltip';
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { getMeasureHistoryUrl } from '../../../helpers/urls';
import { isDiffMetric } from '../../../helpers/measures';
import { MeasureEnhanced, Metric, ComponentMeasure, BranchLike, Period } from '../../../app/types';
import { BranchLike, ComponentMeasure, MeasureEnhanced, Metric, Period } from '../../../app/types';
import { hasFullMeasures } from '../utils';

interface Props {
branchLike?: BranchLike;
@@ -42,7 +43,9 @@ interface Props {
export default function MeasureHeader(props: Props) {
const { branchLike, component, leakPeriod, measure, metric, secondaryMeasure } = props;
const isDiff = isDiffMetric(metric.key);
const hasHistory = component.qualifier !== 'FIL' && component.qualifier !== 'UTS';
const hasHistory =
component.qualifier !== 'FIL' && component.qualifier !== 'UTS' && hasFullMeasures(branchLike);
const displayLeak = hasFullMeasures(branchLike);
return (
<div className="measure-details-header big-spacer-bottom">
<div className="measure-details-primary">
@@ -79,9 +82,10 @@ export default function MeasureHeader(props: Props) {
)}
</div>
<div className="measure-details-primary-actions">
{leakPeriod && (
<LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
)}
{displayLeak &&
leakPeriod && (
<LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
)}
</div>
</div>
{secondaryMeasure &&

+ 29
- 0
server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx Wyświetl plik

@@ -0,0 +1,29 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { translate } from '../../../helpers/l10n';

export default function MeasuresEmpty() {
return (
<div className="layout-page-main">
<div className="note text-center">{translate('component_measures.empty')}</div>
</div>
);
}

+ 9
- 5
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx Wyświetl plik

@@ -21,6 +21,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import App from '../App';
import { waitAndUpdate } from '../../../../helpers/testUtils';

const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' };

@@ -48,21 +49,24 @@ const PROPS = {
component: COMPONENT,
currentUser: { isLoggedIn: false },
location: { pathname: '/component_measures', query: { metric: 'coverage' } },
fetchMeasures: jest.fn().mockResolvedValue({ component: COMPONENT, measures: [] }),
fetchMeasures: jest.fn().mockResolvedValue({
component: COMPONENT,
measures: [{ metric: 'coverage', value: '80.0' }]
}),
fetchMetrics: jest.fn(),
metrics: METRICS,
metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'],
router: { push: jest.fn() } as any
};

it('should render correctly', () => {
it('should render correctly', async () => {
const wrapper = shallow(<App {...PROPS} />);
expect(wrapper.find('.spinner')).toHaveLength(1);
wrapper.setState({ loading: false });
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

it('should render a measure overview', () => {
it('should render a measure overview', async () => {
const wrapper = shallow(
<App
{...PROPS}
@@ -70,6 +74,6 @@ it('should render a measure overview', () => {
/>
);
expect(wrapper.find('.spinner')).toHaveLength(1);
wrapper.setState({ loading: false });
await waitAndUpdate(wrapper);
expect(wrapper.find('MeasureOverviewContainer')).toHaveLength(1);
});

+ 17
- 3
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx Wyświetl plik

@@ -81,10 +81,24 @@ it('should render correctly for leak', () => {
).toMatchSnapshot();
});

it('should render with branch', () => {
const shortBranch = { isMain: false, name: 'feature', mergeBranch: '', type: 'SHORT' };
it('should render with long living branch', () => {
const longBranch = { isMain: false, name: 'branch-6.7', type: 'LONG' };
expect(
shallow(<MeasureHeader branchLike={shortBranch} {...PROPS} />).find('Link')
shallow(<MeasureHeader branchLike={longBranch} {...PROPS} />).find('Link')
).toMatchSnapshot();
});

it('should render with short living branch', () => {
const shortBranch = { isMain: false, name: 'feature', mergeBranch: 'master', type: 'SHORT' };
expect(
shallow(
<MeasureHeader
{...PROPS}
branchLike={shortBranch}
measure={LEAK_MEASURE}
metric={LEAK_METRIC}
/>
)
).toMatchSnapshot();
});


+ 37
- 2
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap Wyświetl plik

@@ -128,7 +128,7 @@ exports[`should render correctly for leak 1`] = `
</div>
`;

exports[`should render with branch 1`] = `
exports[`should render with long living branch 1`] = `
<Link
className="js-show-history spacer-left button button-small"
onlyActiveOnIndex={false}
@@ -137,7 +137,7 @@ exports[`should render with branch 1`] = `
Object {
"pathname": "/project/activity",
"query": Object {
"branch": "feature",
"branch": "branch-6.7",
"custom_metrics": "reliability_rating",
"graph": "custom",
"id": "foo",
@@ -149,6 +149,41 @@ exports[`should render with branch 1`] = `
</Link>
`;

exports[`should render with short living branch 1`] = `
<div
className="measure-details-header big-spacer-bottom"
>
<div
className="measure-details-primary"
>
<div
className="measure-details-metric"
>
<IssueTypeIcon
className="little-spacer-right text-text-bottom"
query="new_reliability_rating"
/>
Reliability Rating on New Code
<span
className="measure-details-value spacer-left"
>
<strong>
<Measure
className="domain-measures-leak"
metricKey="new_reliability_rating"
metricType="RATING"
value="3.0"
/>
</strong>
</span>
</div>
<div
className="measure-details-primary-actions"
/>
</div>
</div>
`;

exports[`should work with measure without value 1`] = `
<div
className="measure-details-header big-spacer-bottom"

+ 15
- 10
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx Wyświetl plik

@@ -41,6 +41,7 @@ import { MeasureEnhanced } from '../../../app/types';

interface Props {
domain: { name: string; measures: MeasureEnhanced[] };
hasOverview: boolean;
onChange: (metric: string) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -48,24 +49,28 @@ interface Props {
}

export default class DomainFacet extends React.PureComponent<Props> {
getValues = () => {
const { domain, selected } = this.props;
const measureSelected = domain.measures.find(measure => measure.metric.key === selected);
const overviewSelected = domain.name === selected && this.hasOverview(domain.name);
if (measureSelected) {
return [getLocalizedMetricName(measureSelected.metric)];
}
return overviewSelected ? [translate('component_measures.domain_overview')] : [];
};

handleHeaderClick = () => {
this.props.onToggle(this.props.domain.name);
};

hasFacetSelected = (domain: { name: string }, measures: MeasureEnhanced[], selected: string) => {
const measureSelected = measures.find(measure => measure.metric.key === selected);
const overviewSelected = domain.name === selected && hasBubbleChart(domain.name);
const overviewSelected = domain.name === selected && this.hasOverview(domain.name);
return measureSelected || overviewSelected;
};

getValues = () => {
const { domain, selected } = this.props;
const measureSelected = domain.measures.find(measure => measure.metric.key === selected);
const overviewSelected = domain.name === selected && hasBubbleChart(domain.name);
if (measureSelected) {
return [getLocalizedMetricName(measureSelected.metric)];
}
return overviewSelected ? [translate('component_measures.domain_overview')] : [];
hasOverview = (domain: string) => {
return this.props.hasOverview && hasBubbleChart(domain);
};

renderItemFacetStat = (item: MeasureEnhanced) => {
@@ -115,7 +120,7 @@ export default class DomainFacet extends React.PureComponent<Props> {

renderOverviewFacet = () => {
const { domain, selected } = this.props;
if (!hasBubbleChart(domain.name)) {
if (!this.hasOverview(domain.name)) {
return null;
}
return (

+ 11
- 6
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx Wyświetl plik

@@ -24,9 +24,10 @@ import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW, Query }
import { MeasureEnhanced } from '../../../app/types';

interface Props {
hasOverview: boolean;
measures: MeasureEnhanced[];
selectedMetric: string;
updateQuery: (query: Query) => void;
updateQuery: (query: Partial<Query>) => void;
}

interface State {
@@ -73,16 +74,20 @@ export default class Sidebar extends React.PureComponent<Props, State> {
this.props.updateQuery({ metric, ...this.resetSelection(metric) });

render() {
const { hasOverview } = this.props;
return (
<div>
<ProjectOverviewFacet
onChange={this.changeMetric}
selected={this.props.selectedMetric}
value={PROJECT_OVERVEW}
/>
{hasOverview && (
<ProjectOverviewFacet
onChange={this.changeMetric}
selected={this.props.selectedMetric}
value={PROJECT_OVERVEW}
/>
)}
{groupByDomains(this.props.measures).map(domain => (
<DomainFacet
domain={domain}
hasOverview={hasOverview}
key={domain.name}
onChange={this.changeMetric}
onToggle={this.toggleFacet}

+ 14
- 23
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.tsx Wyświetl plik

@@ -51,10 +51,11 @@ const DOMAIN = {
};

const PROPS = {
domain: DOMAIN,
hasOverview: true,
onChange: () => {},
onToggle: () => {},
open: true,
domain: DOMAIN,
selected: 'foo'
};

@@ -71,6 +72,16 @@ it('should render closed', () => {
expect(wrapper.find('FacetItemsList')).toHaveLength(0);
});

it('should render without overview', () => {
const wrapper = shallow(<DomainFacet {...PROPS} hasOverview={false} />);
expect(
wrapper
.find('FacetItem')
.filterWhere(node => node.getElement().key === 'Reliability')
.exists()
).toBe(false);
});

it('should not display subtitles of new measures if there is none', () => {
const domain = {
name: 'Reliability',
@@ -82,17 +93,7 @@ it('should not display subtitles of new measures if there is none', () => {
]
};

expect(
shallow(
<DomainFacet
domain={domain}
onChange={() => {}}
onToggle={() => {}}
open={true}
selected={'foo'}
/>
)
).toMatchSnapshot();
expect(shallow(<DomainFacet {...PROPS} domain={domain} />)).toMatchSnapshot();
});

it('should not display subtitles of new measures if there is none, even on last line', () => {
@@ -106,15 +107,5 @@ it('should not display subtitles of new measures if there is none, even on last
]
};

expect(
shallow(
<DomainFacet
domain={domain}
onChange={() => {}}
onToggle={() => {}}
open={true}
selected={'foo'}
/>
)
).toMatchSnapshot();
expect(shallow(<DomainFacet {...PROPS} domain={domain} />)).toMatchSnapshot();
});

+ 1
- 0
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx Wyświetl plik

@@ -61,6 +61,7 @@ const MEASURES = [
];

const PROPS = {
hasOverview: true,
measures: MEASURES,
selectedMetric: 'duplicated_lines_density',
updateQuery: () => {}

+ 2
- 0
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap Wyświetl plik

@@ -49,6 +49,7 @@ exports[`should display two facets 1`] = `
"name": "Coverage",
}
}
hasOverview={true}
key="Coverage"
onChange={[Function]}
onToggle={[Function]}
@@ -80,6 +81,7 @@ exports[`should display two facets 1`] = `
"name": "Duplications",
}
}
hasOverview={true}
key="Duplications"
onChange={[Function]}
onToggle={[Function]}

+ 26
- 3
server/sonar-web/src/main/js/apps/component-measures/utils.ts Wyświetl plik

@@ -25,10 +25,13 @@ import {
ComponentMeasure,
ComponentMeasureEnhanced,
Metric,
MeasureEnhanced
MeasureEnhanced,
BranchLike
} from '../../app/types';
import { enhanceMeasure } from '../../components/measure/utils';
import { cleanQuery, parseAsString, RawQuery, serializeString } from '../../helpers/query';
import { isLongLivingBranch, isMainBranch } from '../../helpers/branches';
import { getDisplayMetrics } from '../../helpers/measures';

export const PROJECT_OVERVEW = 'project_overview';
export const DEFAULT_VIEW = 'list';
@@ -146,13 +149,33 @@ export function hasTreemap(metric: string, type: string): boolean {
}

export function hasBubbleChart(domainName: string): boolean {
return bubbles[domainName] != null;
return bubbles[domainName] !== undefined;
}

export function hasFacetStat(metric: string): boolean {
return metric !== 'alert_status';
}

export function hasFullMeasures(branch?: BranchLike) {
return !branch || isLongLivingBranch(branch) || isMainBranch(branch);
}

export function getMeasuresPageMetricKeys(metrics: { [key: string]: Metric }, branch?: BranchLike) {
if (!hasFullMeasures(branch)) {
return [
'new_coverage',
'new_lines_to_cover',
'new_uncovered_lines',
'new_line_coverage',
'new_conditions_to_cover',
'new_uncovered_conditions',
'new_branch_coverage'
];
}

return getDisplayMetrics(Object.values(metrics)).map(metric => metric.key);
}

export function getBubbleMetrics(domain: string, metrics: { [key: string]: Metric }) {
const conf = bubbles[domain];
return {
@@ -182,7 +205,7 @@ const parseView = (metric: string, rawView?: string) => {
};

export interface Query {
metric?: string;
metric: string;
selected?: string;
view: string;
}

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Wyświetl plik

@@ -2521,6 +2521,7 @@ component_measures.legend.size_x=Size: {0}
component_measures.legend.worse_of_x_y=Worse of {0} and {1}
component_measures.no_history=There is no historical data.
component_measures.not_found=The requested measure was not found.
component_measures.empty=No measures.
component_measures.to_select_files=to select files
component_measures.to_navigate=to navigate
component_measures.to_navigate_files=to next/previous file

Ładowanie…
Anuluj
Zapisz