From bc4d2e6948669842ff325c789f570a4f46f0af41 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Wed, 22 Aug 2018 15:19:20 +0200 Subject: [PATCH] SONAR-11159 SONAR-11163 Update Code page for pull requests and short living branches with Coverage --- .../__tests__/buckets-test.tsx | 15 +- .../sonar-web/src/main/js/apps/code/bucket.ts | 14 +- .../src/main/js/apps/code/components/App.tsx | 136 +++++++-------- .../js/apps/code/components/Breadcrumbs.tsx | 7 +- .../js/apps/code/components/Component.tsx | 49 ++---- .../js/apps/code/components/ComponentLink.tsx | 5 +- .../apps/code/components/ComponentMeasure.tsx | 20 ++- .../js/apps/code/components/ComponentName.tsx | 11 +- .../js/apps/code/components/ComponentPin.tsx | 5 +- .../js/apps/code/components/Components.tsx | 19 ++- .../apps/code/components/ComponentsHeader.tsx | 67 ++++---- .../main/js/apps/code/components/Search.tsx | 27 ++- .../code/components/__tests__/App-test.tsx | 10 +- .../__tests__/ComponentMeasure-test.tsx | 56 ++++++ .../components/__tests__/Components-test.tsx | 60 +++++++ .../__tests__/ComponentsHeader-test.tsx} | 39 +++-- .../ComponentMeasure-test.tsx.snap | 19 +++ .../__snapshots__/Components-test.tsx.snap | 160 ++++++++++++++++++ .../ComponentsHeader-test.tsx.snap | 94 ++++++++++ .../sonar-web/src/main/js/apps/code/utils.ts | 84 +++++---- 20 files changed, 641 insertions(+), 256 deletions(-) rename server/sonar-web/src/main/js/apps/code/{components => }/__tests__/buckets-test.tsx (86%) create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx rename server/sonar-web/src/main/js/apps/code/{types.ts => components/__tests__/ComponentsHeader-test.tsx} (50%) create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/buckets-test.tsx b/server/sonar-web/src/main/js/apps/code/__tests__/buckets-test.tsx similarity index 86% rename from server/sonar-web/src/main/js/apps/code/components/__tests__/buckets-test.tsx rename to server/sonar-web/src/main/js/apps/code/__tests__/buckets-test.tsx index 9ccf00c799d..5eeb0c80896 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/buckets-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/__tests__/buckets-test.tsx @@ -17,22 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Component } from '../../types'; -import { - addComponent, - getComponent, - addComponentChildren, - getComponentChildren -} from '../../bucket'; +import { addComponent, getComponent, addComponentChildren, getComponentChildren } from '../bucket'; +import { ComponentMeasure } from '../../../app/types'; -const component: Component = { key: 'frodo', name: 'frodo', qualifier: 'frodo' }; +const component: ComponentMeasure = { key: 'frodo', name: 'frodo', qualifier: 'frodo' }; const componentKey: string = 'foo'; -const childrenA: Component[] = [ +const childrenA: ComponentMeasure[] = [ { key: 'foo', name: 'foo', qualifier: 'foo' }, { key: 'bar', name: 'bar', qualifier: 'bar' } ]; -const childrenB: Component[] = [ +const childrenB: ComponentMeasure[] = [ { key: 'bart', name: 'bart', qualifier: 'bart' }, { key: 'simpson', name: 'simpson', qualifier: 'simpson' } ]; diff --git a/server/sonar-web/src/main/js/apps/code/bucket.ts b/server/sonar-web/src/main/js/apps/code/bucket.ts index dd172ca8eef..3e1600fbde7 100644 --- a/server/sonar-web/src/main/js/apps/code/bucket.ts +++ b/server/sonar-web/src/main/js/apps/code/bucket.ts @@ -17,29 +17,29 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Breadcrumb, Component } from './types'; +import { ComponentMeasure, Breadcrumb } from '../../app/types'; -let bucket: { [key: string]: Component } = {}; +let bucket: { [key: string]: ComponentMeasure } = {}; let childrenBucket: { [key: string]: { - children: Component[]; + children: ComponentMeasure[]; page: number; total: number; }; } = {}; let breadcrumbsBucket: { [key: string]: Breadcrumb[] } = {}; -export function addComponent(component: Component): void { +export function addComponent(component: ComponentMeasure): void { bucket[component.key] = component; } -export function getComponent(componentKey: string): Component { +export function getComponent(componentKey: string): ComponentMeasure { return bucket[componentKey]; } export function addComponentChildren( componentKey: string, - children: Component[], + children: ComponentMeasure[], total: number, page: number ): void { @@ -53,7 +53,7 @@ export function addComponentChildren( export function getComponentChildren( componentKey: string ): { - children: Component[]; + children: ComponentMeasure[]; page: number; total: number; } { diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/App.tsx index f7a7354b0e6..e6579501d88 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/App.tsx @@ -17,43 +17,53 @@ * 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 classNames from 'classnames'; import * as React from 'react'; +import * as classNames from 'classnames'; +import { connect } from 'react-redux'; import Helmet from 'react-helmet'; import Components from './Components'; import Breadcrumbs from './Breadcrumbs'; import Search from './Search'; import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; -import { Component as CodeComponent } from '../types'; import { retrieveComponentChildren, retrieveComponent, loadMoreChildren } from '../utils'; -import { Component, BranchLike } from '../../../app/types'; +import { Breadcrumb, Component, ComponentMeasure, BranchLike, Metric } from '../../../app/types'; import ListFooter from '../../../components/controls/ListFooter'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; +import { fetchMetrics } from '../../../store/rootActions'; +import { getMetrics } from '../../../store/rootReducer'; import { isSameBranchLike } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; -import { parseError } from '../../../helpers/request'; import '../code.css'; -interface Props { +interface StateToProps { + metrics: { [metric: string]: Metric }; +} + +interface DispatchToProps { + fetchMetrics: () => void; +} + +interface OwnProps { branchLike?: BranchLike; component: Component; location: { query: { [x: string]: string } }; } +type Props = StateToProps & DispatchToProps & OwnProps; + interface State { - baseComponent?: CodeComponent; - breadcrumbs: Array; - components?: Array; - error?: string; + baseComponent?: ComponentMeasure; + breadcrumbs: Breadcrumb[]; + components?: ComponentMeasure[]; loading: boolean; page: number; - searchResults?: Array; - sourceViewer?: CodeComponent; + searchResults?: ComponentMeasure[]; + sourceViewer?: ComponentMeasure; total: number; } -export default class App extends React.PureComponent { +export class App extends React.PureComponent { mounted = false; state: State = { loading: true, @@ -64,6 +74,7 @@ export default class App extends React.PureComponent { componentDidMount() { this.mounted = true; + this.props.fetchMetrics(); this.handleComponentChange(); } @@ -90,28 +101,19 @@ export default class App extends React.PureComponent { addComponentBreadcrumbs(component.key, component.breadcrumbs); this.setState({ loading: true }); - const isPortfolio = ['VW', 'SVW'].includes(component.qualifier); - retrieveComponentChildren(component.key, isPortfolio, branchLike) - .then(() => { - addComponent(component); - if (this.mounted) { - this.handleUpdate(); - } - }) - .catch(e => { - if (this.mounted) { - this.setState({ loading: false }); - parseError(e).then(this.handleError); - } - }); + retrieveComponentChildren(component.key, component.qualifier, branchLike).then(() => { + addComponent(component); + if (this.mounted) { + this.handleUpdate(); + } + }, this.stopLoading); } loadComponent(componentKey: string) { this.setState({ loading: true }); - const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); - retrieveComponent(componentKey, isPortfolio, this.props.branchLike) - .then(r => { + retrieveComponent(componentKey, this.props.component.qualifier, this.props.branchLike).then( + r => { if (this.mounted) { if (['FIL', 'UTS'].includes(r.component.qualifier)) { this.setState({ @@ -133,13 +135,9 @@ export default class App extends React.PureComponent { }); } } - }) - .catch(e => { - if (this.mounted) { - this.setState({ loading: false }); - parseError(e).then(this.handleError); - } - }); + }, + this.stopLoading + ); } handleUpdate() { @@ -155,42 +153,31 @@ export default class App extends React.PureComponent { if (!baseComponent || !components) { return; } - const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); - loadMoreChildren(baseComponent.key, page + 1, isPortfolio, this.props.branchLike) - .then(r => { - if (this.mounted) { - this.setState({ - components: [...components, ...r.components], - page: r.page, - total: r.total - }); - } - }) - .catch(e => { - if (this.mounted) { - this.setState({ loading: false }); - parseError(e).then(this.handleError); - } - }); + loadMoreChildren( + baseComponent.key, + page + 1, + this.props.component.qualifier, + this.props.branchLike + ).then(r => { + if (this.mounted) { + this.setState({ + components: [...components, ...r.components], + page: r.page, + total: r.total + }); + } + }, this.stopLoading); }; - handleError = (error: string) => { + stopLoading = () => { if (this.mounted) { - this.setState({ error }); + this.setState({ loading: false }); } }; render() { const { branchLike, component, location } = this.props; - const { - loading, - error, - baseComponent, - components, - breadcrumbs, - total, - sourceViewer - } = this.state; + const { loading, baseComponent, components, breadcrumbs, total, sourceViewer } = this.state; const shouldShowBreadcrumbs = breadcrumbs.length > 1; const componentsClassName = classNames('boxed-group', 'boxed-group-inner', 'spacer-top', { @@ -207,14 +194,7 @@ export default class App extends React.PureComponent { - {error &&
{error}
} - - +
{shouldShowBreadcrumbs && ( @@ -232,6 +212,7 @@ export default class App extends React.PureComponent { baseComponent={baseComponent} branchLike={branchLike} components={components} + metrics={this.props.metrics} rootComponent={component} />
@@ -252,3 +233,14 @@ export default class App extends React.PureComponent { ); } } + +const mapStateToProps = (state: any): StateToProps => ({ + metrics: getMetrics(state) +}); + +const mapDispatchToProps: DispatchToProps = { fetchMetrics }; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(App); diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx index 7ea212cd7ee..c1fca8d200b 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx @@ -19,13 +19,12 @@ */ import * as React from 'react'; import ComponentName from './ComponentName'; -import { Component } from '../types'; -import { BranchLike } from '../../../app/types'; +import { BranchLike, Breadcrumb, ComponentMeasure } from '../../../app/types'; interface Props { branchLike?: BranchLike; - breadcrumbs: Component[]; - rootComponent: Component; + breadcrumbs: Breadcrumb[]; + rootComponent: ComponentMeasure; } export default function Breadcrumbs({ branchLike, breadcrumbs, rootComponent }: Props) { diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.tsx b/server/sonar-web/src/main/js/apps/code/components/Component.tsx index c12248a826a..e62b377faa7 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Component.tsx @@ -23,9 +23,7 @@ import ComponentName from './ComponentName'; import ComponentMeasure from './ComponentMeasure'; import ComponentLink from './ComponentLink'; import ComponentPin from './ComponentPin'; -import { Component as IComponent } from '../types'; -import { BranchLike } from '../../../app/types'; -import { isShortLivingBranch, isPullRequest } from '../../../helpers/branches'; +import { BranchLike, Metric, ComponentMeasure as IComponentMeasure } from '../../../app/types'; const TOP_OFFSET = 200; const BOTTOM_OFFSET = 10; @@ -33,9 +31,10 @@ const BOTTOM_OFFSET = 10; interface Props { branchLike?: BranchLike; canBrowse?: boolean; - component: IComponent; - previous?: IComponent; - rootComponent: IComponent; + component: IComponentMeasure; + metrics: Metric[]; + previous?: IComponentMeasure; + rootComponent: IComponentMeasure; selected?: boolean; } @@ -76,15 +75,13 @@ export default class Component extends React.PureComponent { render() { const { branchLike, + canBrowse = false, component, - rootComponent, - selected = false, + metrics, previous, - canBrowse = false + rootComponent, + selected = false } = this.props; - const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier); - const isApplication = rootComponent.qualifier === 'APP'; - const hideCoverageAndDuplicates = isShortLivingBranch(branchLike) || isPullRequest(branchLike); let componentAction = null; @@ -99,24 +96,6 @@ export default class Component extends React.PureComponent { } } - const columns = isPortfolio - ? [ - { metric: 'releasability_rating', type: 'RATING' }, - { metric: 'reliability_rating', type: 'RATING' }, - { metric: 'security_rating', type: 'RATING' }, - { metric: 'sqale_rating', type: 'RATING' }, - { metric: 'ncloc', type: 'SHORT_INT' } - ] - : ([ - isApplication && { metric: 'alert_status', type: 'LEVEL' }, - { metric: 'ncloc', type: 'SHORT_INT' }, - { metric: 'bugs', type: 'SHORT_INT' }, - { metric: 'vulnerabilities', type: 'SHORT_INT' }, - { metric: 'code_smells', type: 'SHORT_INT' }, - !hideCoverageAndDuplicates && { metric: 'coverage', type: 'PERCENT' }, - !hideCoverageAndDuplicates && { metric: 'duplicated_lines_density', type: 'PERCENT' } - ].filter(Boolean) as Array<{ metric: string; type: string }>); - return ( (this.node = node)}> @@ -132,14 +111,10 @@ export default class Component extends React.PureComponent { /> - {columns.map(column => ( - + {metrics.map(metric => ( +
- +
))} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx index 60927947472..eb6e931c945 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx @@ -19,15 +19,14 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import { Component } from '../types'; -import { BranchLike } from '../../../app/types'; +import { BranchLike, ComponentMeasure } from '../../../app/types'; import LinkIcon from '../../../components/icons-components/LinkIcon'; import { translate } from '../../../helpers/l10n'; import { getBranchLikeUrl } from '../../../helpers/urls'; interface Props { branchLike?: BranchLike; - component: Component; + component: ComponentMeasure; } export default function ComponentLink({ component, branchLike }: Props) { diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx index bb3cbe506a2..e33ff7e5d68 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx @@ -19,20 +19,21 @@ */ import * as React from 'react'; import Measure from '../../../components/measure/Measure'; -import { Component } from '../types'; +import { Metric, ComponentMeasure as IComponentMeasure } from '../../../app/types'; +import { isDiffMetric } from '../../../helpers/measures'; +import { getLeakValue } from '../../../components/measure/utils'; interface Props { - component: Component; - metricKey: string; - metricType: string; + component: IComponentMeasure; + metric: Metric; } -export default function ComponentMeasure({ component, metricKey, metricType }: Props) { +export default function ComponentMeasure({ component, metric }: Props) { const isProject = component.qualifier === 'TRK'; - const isReleasability = metricKey === 'releasability_rating'; + const isReleasability = metric.key === 'releasability_rating'; - const finalMetricKey = isProject && isReleasability ? 'alert_status' : metricKey; - const finalMetricType = isProject && isReleasability ? 'LEVEL' : metricType; + const finalMetricKey = isProject && isReleasability ? 'alert_status' : metric.key; + const finalMetricType = isProject && isReleasability ? 'LEVEL' : metric.type; const measure = Array.isArray(component.measures) && @@ -42,5 +43,6 @@ export default function ComponentMeasure({ component, metricKey, metricType }: P return ; } - return ; + const value = isDiffMetric(metric.key) ? getLeakValue(measure) : measure.value; + return ; } diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx index 391c1862c04..e303cffef93 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -20,15 +20,14 @@ import * as React from 'react'; import { Link } from 'react-router'; import Truncated from './Truncated'; -import { Component } from '../types'; import * as theme from '../../../app/theme'; -import { BranchLike } from '../../../app/types'; +import { BranchLike, ComponentMeasure } from '../../../app/types'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { getBranchLikeQuery } from '../../../helpers/branches'; import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon'; import { translate } from '../../../helpers/l10n'; -function getTooltip(component: Component) { +function getTooltip(component: ComponentMeasure) { const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; if (isFile && component.path) { return component.path + '\n\n' + component.key; @@ -55,9 +54,9 @@ function mostCommitPrefix(strings: string[]) { interface Props { branchLike?: BranchLike; canBrowse?: boolean; - component: Component; - previous?: Component; - rootComponent: Component; + component: ComponentMeasure; + previous?: ComponentMeasure; + rootComponent: ComponentMeasure; } export default function ComponentName(props: Props) { diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx index 18d77d03f67..24ef4d65621 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx @@ -19,15 +19,14 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { Component } from '../types'; -import { BranchLike } from '../../../app/types'; +import { BranchLike, ComponentMeasure } from '../../../app/types'; import PinIcon from '../../../components/icons-components/PinIcon'; import { WorkspaceContext } from '../../../components/workspace/context'; import { translate } from '../../../helpers/l10n'; interface Props { branchLike?: BranchLike; - component: Component; + component: ComponentMeasure; } export default class ComponentPin extends React.PureComponent { diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.tsx b/server/sonar-web/src/main/js/apps/code/components/Components.tsx index 59b017c40b6..030bdfd188b 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Components.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Components.tsx @@ -21,24 +21,27 @@ import * as React from 'react'; import Component from './Component'; import ComponentsEmpty from './ComponentsEmpty'; import ComponentsHeader from './ComponentsHeader'; -import { Component as IComponent } from '../types'; -import { BranchLike } from '../../../app/types'; +import { BranchLike, ComponentMeasure, Metric } from '../../../app/types'; +import { getCodeMetrics } from '../utils'; interface Props { - baseComponent?: IComponent; + baseComponent?: ComponentMeasure; branchLike?: BranchLike; - components: IComponent[]; - rootComponent: IComponent; - selected?: IComponent; + components: ComponentMeasure[]; + metrics: { [metric: string]: Metric }; + rootComponent: ComponentMeasure; + selected?: ComponentMeasure; } export default function Components(props: Props) { const { baseComponent, branchLike, components, rootComponent, selected } = props; + const metricKeys = getCodeMetrics(rootComponent.qualifier, branchLike); + const metrics = metricKeys.map(metric => props.metrics[metric]).filter(Boolean); return ( {baseComponent && ( @@ -47,6 +50,7 @@ export default function Components(props: Props) { branchLike={branchLike} component={baseComponent} key={baseComponent.key} + metrics={metrics} rootComponent={rootComponent} /> @@ -62,6 +66,7 @@ export default function Components(props: Props) { canBrowse={true} component={component} key={component.key} + metrics={metrics} previous={index > 0 ? list[index - 1] : undefined} rootComponent={rootComponent} selected={component === selected} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx index 3455f7972fc..2aeb89071a9 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx @@ -20,53 +20,48 @@ import * as React from 'react'; import * as classNames from 'classnames'; import { translate } from '../../../helpers/l10n'; -import { Component } from '../types'; -import { isShortLivingBranch, isPullRequest } from '../../../helpers/branches'; -import { BranchLike } from '../../../app/types'; +import { ComponentMeasure } from '../../../app/types'; interface Props { - branchLike?: BranchLike; - baseComponent?: Component; - rootComponent: Component; + baseComponent?: ComponentMeasure; + metrics: string[]; + rootComponent: ComponentMeasure; } -export default function ComponentsHeader({ baseComponent, branchLike, rootComponent }: Props) { - const isPortfolio = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW'; - const isApplication = rootComponent.qualifier === 'APP'; - const hideCoverageAndDuplicates = isShortLivingBranch(branchLike) || isPullRequest(branchLike); +const SHORT_NAME_METRICS = ['duplicated_lines_density']; - const columns = isPortfolio - ? [ - translate('metric_domain.Releasability'), - translate('metric_domain.Reliability'), - translate('metric_domain.Security'), - translate('metric_domain.Maintainability'), - translate('metric', 'ncloc', 'name') - ] - : ([ - isApplication && translate('metric.alert_status.name'), - translate('metric', 'ncloc', 'name'), - translate('metric', 'bugs', 'name'), - translate('metric', 'vulnerabilities', 'name'), - translate('metric', 'code_smells', 'name'), - !hideCoverageAndDuplicates && translate('metric', 'coverage', 'name'), - !hideCoverageAndDuplicates && translate('metric', 'duplicated_lines_density', 'short_name') - ].filter(Boolean) as string[]); +export default function ComponentsHeader({ baseComponent, metrics, rootComponent }: Props) { + const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier); + let columns: string[] = []; + if (isPortfolio) { + columns = [ + translate('metric_domain.Releasability'), + translate('metric_domain.Reliability'), + translate('metric_domain.Security'), + translate('metric_domain.Maintainability'), + translate('metric', 'ncloc', 'name') + ]; + } else { + columns = metrics.map(metric => + translate('metric', metric, SHORT_NAME_METRICS.includes(metric) ? 'short_name' : 'name') + ); + } return ( - {columns.map((column, index) => ( - - ))} + {baseComponent && + columns.map((column, index) => ( + + ))} ); diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index 7cdd3af90ac..8129b592c16 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -21,26 +21,23 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import * as classNames from 'classnames'; import Components from './Components'; -import { Component } from '../types'; import { getTree } from '../../../api/components'; -import { BranchLike } from '../../../app/types'; +import { BranchLike, ComponentMeasure } from '../../../app/types'; import SearchBox from '../../../components/controls/SearchBox'; import { getBranchLikeQuery } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; -import { parseError } from '../../../helpers/request'; import { getProjectUrl } from '../../../helpers/urls'; interface Props { branchLike?: BranchLike; - component: Component; + component: ComponentMeasure; location: {}; - onError: (error: string) => void; } interface State { query: string; loading: boolean; - results?: Component[]; + results?: ComponentMeasure[]; selectedIndex?: number; } @@ -78,14 +75,14 @@ export default class Search extends React.PureComponent { handleSelectNext() { const { selectedIndex, results } = this.state; - if (results != null && selectedIndex != null && selectedIndex < results.length - 1) { + if (results && selectedIndex !== undefined && selectedIndex < results.length - 1) { this.setState({ selectedIndex: selectedIndex + 1 }); } } handleSelectPrevious() { const { selectedIndex, results } = this.state; - if (results != null && selectedIndex != null && selectedIndex > 0) { + if (results && selectedIndex !== undefined && selectedIndex > 0) { this.setState({ selectedIndex: selectedIndex - 1 }); } } @@ -93,7 +90,7 @@ export default class Search extends React.PureComponent { handleSelectCurrent() { const { branchLike, component } = this.props; const { results, selectedIndex } = this.state; - if (results != null && selectedIndex != null) { + if (results && selectedIndex !== undefined) { const selected = results[selectedIndex]; if (selected.refKey) { @@ -127,7 +124,7 @@ export default class Search extends React.PureComponent { handleSearch = (query: string) => { if (this.mounted) { - const { branchLike, component, onError } = this.props; + const { branchLike, component } = this.props; this.setState({ loading: true }); const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); @@ -149,10 +146,9 @@ export default class Search extends React.PureComponent { }); } }) - .catch(e => { + .catch(() => { if (this.mounted) { this.setState({ loading: false }); - parseError(e).then(onError); } }); } @@ -170,9 +166,9 @@ export default class Search extends React.PureComponent { render() { const { component } = this.props; const { loading, selectedIndex, results } = this.state; - const selected = selectedIndex != null && results != null ? results[selectedIndex] : undefined; + const selected = selectedIndex !== undefined && results ? results[selectedIndex] : undefined; const containerClassName = classNames('code-search', { - 'code-search-with-results': results != null + 'code-search-with-results': Boolean(results) }); const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); @@ -189,11 +185,12 @@ export default class Search extends React.PureComponent { /> {loading && } - {results != null && ( + {results && (
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx index 0171b0bca0f..42b5af7bb2f 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx @@ -17,9 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* eslint-disable camelcase */ import * as React from 'react'; import { shallow } from 'enzyme'; -import App from '../App'; +import { App } from '../App'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { retrieveComponent } from '../../utils'; @@ -34,6 +35,11 @@ jest.mock('../../utils', () => ({ retrieveComponentChildren: () => Promise.resolve() })); +const METRICS = { + coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, + new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } +}; + beforeEach(() => { (retrieveComponent as jest.Mock).mockClear(); }); @@ -80,7 +86,9 @@ const getWrapper = () => { organization: 'foo', qualifier: 'FOO' }} + fetchMetrics={jest.fn()} location={{ query: { branch: 'b', id: 'foo', line: '7' } }} + metrics={METRICS} /> ); }; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx new file mode 100644 index 00000000000..c317d15d70c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx @@ -0,0 +1,56 @@ +/* + * 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 { shallow } from 'enzyme'; +import ComponentMeasure from '../ComponentMeasure'; + +const METRIC = { id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' }; +const LEAK_METRIC = { id: '2', key: 'new_coverage', type: 'PERCENT', name: 'Coverage on New Code' }; +const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; +const COMPONENT_MEASURE = { + ...COMPONENT, + measures: [{ value: '3.0', periods: [{ index: 1, value: '10.0' }], metric: METRIC.key }] +}; + +it('renders correctly', () => { + expect( + shallow() + ).toMatchSnapshot(); +}); + +it('renders correctly for leak values', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('renders correctly when no measure found', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx new file mode 100644 index 00000000000..8d0dfbe21c5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx @@ -0,0 +1,60 @@ +/* + * 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 { shallow } from 'enzyme'; +import Components from '../Components'; + +const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; +const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: 'VW' }; +const METRICS = { coverage: { id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' } }; + +it('renders correctly', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('renders correctly for a search', () => { + expect( + shallow() + ).toMatchSnapshot(); +}); + +it('handle no components correctly', () => { + expect( + shallow( + + ) + .find('ComponentsEmpty') + .exists() + ).toBe(true); +}); diff --git a/server/sonar-web/src/main/js/apps/code/types.ts b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentsHeader-test.tsx similarity index 50% rename from server/sonar-web/src/main/js/apps/code/types.ts rename to server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentsHeader-test.tsx index f8fbc5af202..4c7ab9f2414 100644 --- a/server/sonar-web/src/main/js/apps/code/types.ts +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentsHeader-test.tsx @@ -17,17 +17,32 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Measure } from '../../app/types'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import ComponentsHeader from '../ComponentsHeader'; -export interface Component extends Breadcrumb { - branch?: string; - measures?: Measure[]; - path?: string; - refKey?: string; -} +const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; +const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: 'VW' }; +const METRICS = ['foo', 'bar']; -export interface Breadcrumb { - key: string; - name: string; - qualifier: string; -} +it('renders correctly for projects', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('renders correctly for portfolios', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('renders correctly for a search', () => { + expect( + shallow() + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap new file mode 100644 index 00000000000..824a3eb07a0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + +`; + +exports[`renders correctly for leak values 1`] = ` + +`; + +exports[`renders correctly when no measure found 1`] = ``; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap new file mode 100644 index 00000000000..5c20c57810a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
    0 - })} - key={column}> - {baseComponent && column} - 0 + })} + key={column}> + {column} +
+ + + + + + + + + + +
+   +
+`; + +exports[`renders correctly for a search 1`] = ` + + + + + +
+`; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap new file mode 100644 index 00000000000..0d0a7b3c83d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly for a search 1`] = ` + + + +   + + +   + + + +`; + +exports[`renders correctly for portfolios 1`] = ` + + + +   + + +   + + + metric_domain.Releasability + + + metric_domain.Reliability + + + metric_domain.Security + + + metric_domain.Maintainability + + + metric.ncloc.name + + + +`; + +exports[`renders correctly for projects 1`] = ` + + + +   + + +   + + + metric.foo.name + + + metric.bar.name + + + +`; diff --git a/server/sonar-web/src/main/js/apps/code/utils.ts b/server/sonar-web/src/main/js/apps/code/utils.ts index 75ef812c977..cd429f3d6fa 100644 --- a/server/sonar-web/src/main/js/apps/code/utils.ts +++ b/server/sonar-web/src/main/js/apps/code/utils.ts @@ -26,30 +26,37 @@ import { addComponentBreadcrumbs, getComponentBreadcrumbs } from './bucket'; -import { Breadcrumb, Component } from './types'; import { getChildren, getComponent, getBreadcrumbs } from '../../api/components'; -import { BranchLike } from '../../app/types'; -import { getBranchLikeQuery } from '../../helpers/branches'; +import { BranchLike, ComponentMeasure, Breadcrumb } from '../../app/types'; +import { getBranchLikeQuery, isShortLivingBranch, isPullRequest } from '../../helpers/branches'; const METRICS = [ 'ncloc', - 'code_smells', 'bugs', 'vulnerabilities', + 'code_smells', 'coverage', - 'duplicated_lines_density', - 'alert_status' + 'duplicated_lines_density' ]; +const APPLICATION_METRICS = ['alert_status', ...METRICS]; + const PORTFOLIO_METRICS = [ 'releasability_rating', - 'alert_status', 'reliability_rating', 'security_rating', 'sqale_rating', 'ncloc' ]; +const LEAK_METRICS = [ + 'new_lines', + 'new_bugs', + 'new_vulnerabilities', + 'new_code_smells', + 'new_coverage' +]; + const PAGE_SIZE = 100; function requestChildren( @@ -57,7 +64,7 @@ function requestChildren( metrics: string[], page: number, branchLike?: BranchLike -): Promise { +): Promise { return getChildren(componentKey, metrics, { p: page, ps: PAGE_SIZE, @@ -76,12 +83,12 @@ function requestAllChildren( componentKey: string, metrics: string[], branchLike?: BranchLike -): Promise { +): Promise { return requestChildren(componentKey, metrics, 1, branchLike); } interface Children { - components: Component[]; + components: ComponentMeasure[]; page: number; total: number; } @@ -93,7 +100,7 @@ interface ExpandRootDirFunc { function expandRootDir(metrics: string[], branchLike?: BranchLike): ExpandRootDirFunc { return function({ components, total, ...other }) { const rootDir = components.find( - (component: Component) => component.qualifier === 'DIR' && component.name === '/' + (component: ComponentMeasure) => component.qualifier === 'DIR' && component.name === '/' ); if (rootDir) { return requestAllChildren(rootDir.key, metrics, branchLike).then(rootDirComponents => { @@ -107,6 +114,10 @@ function expandRootDir(metrics: string[], branchLike?: BranchLike): ExpandRootDi }; } +function showLeakMeasure(branchLike?: BranchLike) { + return isShortLivingBranch(branchLike) || isPullRequest(branchLike); +} + function prepareChildren(r: any): Children { return { components: r.components, @@ -115,13 +126,13 @@ function prepareChildren(r: any): Children { }; } -function skipRootDir(breadcrumbs: Component[]) { +function skipRootDir(breadcrumbs: ComponentMeasure[]) { return breadcrumbs.filter(component => { return !(component.qualifier === 'DIR' && component.name === '/'); }); } -function storeChildrenBase(children: Component[]) { +function storeChildrenBase(children: ComponentMeasure[]) { children.forEach(addComponent); } @@ -135,21 +146,26 @@ function storeChildrenBreadcrumbs(parentComponentKey: string, children: Breadcru } } -function getMetrics(isPortfolio: boolean) { - return isPortfolio ? PORTFOLIO_METRICS : METRICS; +export function getCodeMetrics(qualifier: string, branchLike?: BranchLike) { + if (['VW', 'SVW'].includes(qualifier)) { + return PORTFOLIO_METRICS; + } + if (qualifier === 'APP') { + return APPLICATION_METRICS; + } + if (showLeakMeasure(branchLike)) { + return LEAK_METRICS; + } + return METRICS; } -function retrieveComponentBase( - componentKey: string, - isPortfolio: boolean, - branchLike?: BranchLike -) { +function retrieveComponentBase(componentKey: string, qualifier: string, branchLike?: BranchLike) { const existing = getComponentFromBucket(componentKey); if (existing) { return Promise.resolve(existing); } - const metrics = getMetrics(isPortfolio); + const metrics = getCodeMetrics(qualifier, branchLike); return getComponent({ componentKey, @@ -163,9 +179,9 @@ function retrieveComponentBase( export function retrieveComponentChildren( componentKey: string, - isPortfolio: boolean, + qualifier: string, branchLike?: BranchLike -): Promise<{ components: Component[]; page: number; total: number }> { +): Promise<{ components: ComponentMeasure[]; page: number; total: number }> { const existing = getComponentChildren(componentKey); if (existing) { return Promise.resolve({ @@ -175,7 +191,7 @@ export function retrieveComponentChildren( }); } - const metrics = getMetrics(isPortfolio); + const metrics = getCodeMetrics(qualifier, branchLike); return getChildren(componentKey, metrics, { ps: PAGE_SIZE, @@ -211,26 +227,26 @@ function retrieveComponentBreadcrumbs( export function retrieveComponent( componentKey: string, - isPortfolio: boolean, + qualifier: string, branchLike?: BranchLike ): Promise<{ - breadcrumbs: Component[]; - component: Component; - components: Component[]; + breadcrumbs: Breadcrumb[]; + component: ComponentMeasure; + components: ComponentMeasure[]; page: number; total: number; }> { return Promise.all([ - retrieveComponentBase(componentKey, isPortfolio, branchLike), - retrieveComponentChildren(componentKey, isPortfolio, branchLike), + retrieveComponentBase(componentKey, qualifier, branchLike), + retrieveComponentChildren(componentKey, qualifier, branchLike), retrieveComponentBreadcrumbs(componentKey, branchLike) ]).then(r => { return { + breadcrumbs: r[2], component: r[0], components: r[1].components, - total: r[1].total, page: r[1].page, - breadcrumbs: r[2] + total: r[1].total }; }); } @@ -238,10 +254,10 @@ export function retrieveComponent( export function loadMoreChildren( componentKey: string, page: number, - isPortfolio: boolean, + qualifier: string, branchLike?: BranchLike ): Promise { - const metrics = getMetrics(isPortfolio); + const metrics = getCodeMetrics(qualifier, branchLike); return getChildren(componentKey, metrics, { ps: PAGE_SIZE, -- 2.39.5