--- /dev/null
+/*
+ * 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 { addComponent, getComponent, addComponentChildren, getComponentChildren } from '../bucket';
+import { ComponentMeasure } from '../../../app/types';
+
+const component: ComponentMeasure = { key: 'frodo', name: 'frodo', qualifier: 'frodo' };
+
+const componentKey: string = 'foo';
+const childrenA: ComponentMeasure[] = [
+ { key: 'foo', name: 'foo', qualifier: 'foo' },
+ { key: 'bar', name: 'bar', qualifier: 'bar' }
+];
+const childrenB: ComponentMeasure[] = [
+ { key: 'bart', name: 'bart', qualifier: 'bart' },
+ { key: 'simpson', name: 'simpson', qualifier: 'simpson' }
+];
+
+it('should have empty bucket at start', () => {
+ expect(getComponent(component.key)).toBeUndefined();
+});
+
+it('should be able to store components in a bucket', () => {
+ addComponent(component);
+ expect(getComponent(component.key)).toEqual(component);
+});
+
+it('should have empty children bucket at start', () => {
+ expect(getComponentChildren(componentKey)).toBeUndefined();
+});
+
+it('should be able to store children components in a bucket', () => {
+ addComponentChildren(componentKey, childrenA, childrenA.length, 1);
+ expect(getComponentChildren(componentKey).children).toEqual(childrenA);
+});
+
+it('should append new children components at the end of the bucket', () => {
+ addComponentChildren(componentKey, childrenB, 4, 2);
+ const finalBucket = getComponentChildren(componentKey);
+ expect(finalBucket.children).toEqual([...childrenA, ...childrenB]);
+ expect(finalBucket.total).toBe(4);
+ expect(finalBucket.page).toBe(2);
+});
* 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 {
export function getComponentChildren(
componentKey: string
): {
- children: Component[];
+ children: ComponentMeasure[];
page: number;
total: number;
} {
* 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<CodeComponent>;
- components?: Array<CodeComponent>;
- error?: string;
+ baseComponent?: ComponentMeasure;
+ breadcrumbs: Breadcrumb[];
+ components?: ComponentMeasure[];
loading: boolean;
page: number;
- searchResults?: Array<CodeComponent>;
- sourceViewer?: CodeComponent;
+ searchResults?: ComponentMeasure[];
+ sourceViewer?: ComponentMeasure;
total: number;
}
-export default class App extends React.PureComponent<Props, State> {
+export class App extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
loading: true,
componentDidMount() {
this.mounted = true;
+ this.props.fetchMetrics();
this.handleComponentChange();
}
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({
});
}
}
- })
- .catch(e => {
- if (this.mounted) {
- this.setState({ loading: false });
- parseError(e).then(this.handleError);
- }
- });
+ },
+ this.stopLoading
+ );
}
handleUpdate() {
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', {
<Suggestions suggestions="code" />
<Helmet title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle} />
- {error && <div className="alert alert-danger">{error}</div>}
-
- <Search
- branchLike={branchLike}
- component={component}
- location={location}
- onError={this.handleError}
- />
+ <Search branchLike={branchLike} component={component} location={location} />
<div className="code-components">
{shouldShowBreadcrumbs && (
baseComponent={baseComponent}
branchLike={branchLike}
components={components}
+ metrics={this.props.metrics}
rootComponent={component}
/>
</div>
);
}
}
+
+const mapStateToProps = (state: any): StateToProps => ({
+ metrics: getMetrics(state)
+});
+
+const mapDispatchToProps: DispatchToProps = { fetchMetrics };
+
+export default connect<StateToProps, DispatchToProps, Props>(
+ mapStateToProps,
+ mapDispatchToProps
+)(App);
*/
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) {
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;
interface Props {
branchLike?: BranchLike;
canBrowse?: boolean;
- component: IComponent;
- previous?: IComponent;
- rootComponent: IComponent;
+ component: IComponentMeasure;
+ metrics: Metric[];
+ previous?: IComponentMeasure;
+ rootComponent: IComponentMeasure;
selected?: boolean;
}
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;
}
}
- 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 (
<tr className={classNames({ selected })} ref={node => (this.node = node)}>
<td className="thin nowrap">
/>
</td>
- {columns.map(column => (
- <td className="thin nowrap text-right" key={column.metric}>
+ {metrics.map(metric => (
+ <td className="thin nowrap text-right" key={metric.key}>
<div className="code-components-cell">
- <ComponentMeasure
- component={component}
- metricKey={column.metric}
- metricType={column.type}
- />
+ <ComponentMeasure component={component} metric={metric} />
</div>
</td>
))}
*/
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) {
*/
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) &&
return <span />;
}
- return <Measure metricKey={finalMetricKey} metricType={finalMetricType} value={measure.value} />;
+ const value = isDiffMetric(metric.key) ? getLeakValue(measure) : measure.value;
+ return <Measure metricKey={finalMetricKey} metricType={finalMetricType} value={value} />;
}
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;
interface Props {
branchLike?: BranchLike;
canBrowse?: boolean;
- component: Component;
- previous?: Component;
- rootComponent: Component;
+ component: ComponentMeasure;
+ previous?: ComponentMeasure;
+ rootComponent: ComponentMeasure;
}
export default function ComponentName(props: Props) {
*/
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<Props> {
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 (
<table className="data zebra">
<ComponentsHeader
baseComponent={baseComponent}
- branchLike={branchLike}
+ metrics={metricKeys}
rootComponent={rootComponent}
/>
{baseComponent && (
branchLike={branchLike}
component={baseComponent}
key={baseComponent.key}
+ metrics={metrics}
rootComponent={rootComponent}
/>
<tr className="blank">
canBrowse={true}
component={component}
key={component.key}
+ metrics={metrics}
previous={index > 0 ? list[index - 1] : undefined}
rootComponent={rootComponent}
selected={component === selected}
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 (
<thead>
<tr className="code-components-header">
<th className="thin nowrap"> </th>
<th> </th>
- {columns.map((column, index) => (
- <th
- className={classNames('thin', 'nowrap', 'text-right', {
- 'code-components-cell': index > 0
- })}
- key={column}>
- {baseComponent && column}
- </th>
- ))}
+ {baseComponent &&
+ columns.map((column, index) => (
+ <th
+ className={classNames('thin', 'nowrap', 'text-right', {
+ 'code-components-cell': index > 0
+ })}
+ key={column}>
+ {column}
+ </th>
+ ))}
</tr>
</thead>
);
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;
}
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 });
}
}
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) {
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);
});
}
})
- .catch(e => {
+ .catch(() => {
if (this.mounted) {
this.setState({ loading: false });
- parseError(e).then(onError);
}
});
}
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);
/>
{loading && <i className="spinner spacer-left" />}
- {results != null && (
+ {results && (
<div className="boxed-group boxed-group-inner spacer-top">
<Components
branchLike={this.props.branchLike}
components={results}
+ metrics={{}}
rootComponent={component}
selected={selected}
/>
* 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';
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<any>).mockClear();
});
organization: 'foo',
qualifier: 'FOO'
}}
+ fetchMetrics={jest.fn()}
location={{ query: { branch: 'b', id: 'foo', line: '7' } }}
+ metrics={METRICS}
/>
);
};
--- /dev/null
+/*
+ * 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(<ComponentMeasure component={COMPONENT_MEASURE} metric={METRIC} />)
+ ).toMatchSnapshot();
+});
+
+it('renders correctly for leak values', () => {
+ expect(
+ shallow(
+ <ComponentMeasure
+ component={{
+ ...COMPONENT,
+ measures: [
+ { value: '3.0', periods: [{ index: 1, value: '10.0' }], metric: LEAK_METRIC.key }
+ ]
+ }}
+ metric={LEAK_METRIC}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders correctly when no measure found', () => {
+ expect(shallow(<ComponentMeasure component={COMPONENT} metric={METRIC} />)).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * 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(
+ <Components
+ baseComponent={COMPONENT}
+ components={[COMPONENT]}
+ metrics={METRICS}
+ rootComponent={COMPONENT}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders correctly for a search', () => {
+ expect(
+ shallow(<Components components={[COMPONENT]} metrics={METRICS} rootComponent={COMPONENT} />)
+ ).toMatchSnapshot();
+});
+
+it('handle no components correctly', () => {
+ expect(
+ shallow(
+ <Components
+ baseComponent={PORTFOLIO}
+ components={[]}
+ metrics={METRICS}
+ rootComponent={PORTFOLIO}
+ />
+ )
+ .find('ComponentsEmpty')
+ .exists()
+ ).toBe(true);
+});
--- /dev/null
+/*
+ * 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 ComponentsHeader from '../ComponentsHeader';
+
+const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' };
+const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: 'VW' };
+const METRICS = ['foo', 'bar'];
+
+it('renders correctly for projects', () => {
+ expect(
+ shallow(
+ <ComponentsHeader baseComponent={COMPONENT} metrics={METRICS} rootComponent={COMPONENT} />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders correctly for portfolios', () => {
+ expect(
+ shallow(
+ <ComponentsHeader baseComponent={PORTFOLIO} metrics={METRICS} rootComponent={PORTFOLIO} />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders correctly for a search', () => {
+ expect(
+ shallow(<ComponentsHeader metrics={METRICS} rootComponent={COMPONENT} />)
+ ).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<Measure
+ metricKey="coverage"
+ metricType="PERCENT"
+ value="3.0"
+/>
+`;
+
+exports[`renders correctly for leak values 1`] = `
+<Measure
+ metricKey="new_coverage"
+ metricType="PERCENT"
+ value="10.0"
+/>
+`;
+
+exports[`renders correctly when no measure found 1`] = `<span />`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<table
+ className="data zebra"
+>
+ <ComponentsHeader
+ baseComponent={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ metrics={
+ Array [
+ "ncloc",
+ "bugs",
+ "vulnerabilities",
+ "code_smells",
+ "coverage",
+ "duplicated_lines_density",
+ ]
+ }
+ rootComponent={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ />
+ <tbody>
+ <Component
+ component={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ key="foo"
+ metrics={
+ Array [
+ Object {
+ "id": "1",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ },
+ ]
+ }
+ rootComponent={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ />
+ <tr
+ className="blank"
+ >
+ <td
+ colSpan={8}
+ >
+
+ </td>
+ </tr>
+ </tbody>
+ <tbody>
+ <Component
+ canBrowse={true}
+ component={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ key="foo"
+ metrics={
+ Array [
+ Object {
+ "id": "1",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ },
+ ]
+ }
+ rootComponent={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ selected={false}
+ />
+ </tbody>
+</table>
+`;
+
+exports[`renders correctly for a search 1`] = `
+<table
+ className="data zebra"
+>
+ <ComponentsHeader
+ metrics={
+ Array [
+ "ncloc",
+ "bugs",
+ "vulnerabilities",
+ "code_smells",
+ "coverage",
+ "duplicated_lines_density",
+ ]
+ }
+ rootComponent={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ />
+ <tbody>
+ <Component
+ canBrowse={true}
+ component={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ key="foo"
+ metrics={
+ Array [
+ Object {
+ "id": "1",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ },
+ ]
+ }
+ rootComponent={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ }
+ }
+ selected={false}
+ />
+ </tbody>
+</table>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly for a search 1`] = `
+<thead>
+ <tr
+ className="code-components-header"
+ >
+ <th
+ className="thin nowrap"
+ >
+
+ </th>
+ <th>
+
+ </th>
+ </tr>
+</thead>
+`;
+
+exports[`renders correctly for portfolios 1`] = `
+<thead>
+ <tr
+ className="code-components-header"
+ >
+ <th
+ className="thin nowrap"
+ >
+
+ </th>
+ <th>
+
+ </th>
+ <th
+ className="thin nowrap text-right"
+ key="metric_domain.Releasability"
+ >
+ metric_domain.Releasability
+ </th>
+ <th
+ className="thin nowrap text-right code-components-cell"
+ key="metric_domain.Reliability"
+ >
+ metric_domain.Reliability
+ </th>
+ <th
+ className="thin nowrap text-right code-components-cell"
+ key="metric_domain.Security"
+ >
+ metric_domain.Security
+ </th>
+ <th
+ className="thin nowrap text-right code-components-cell"
+ key="metric_domain.Maintainability"
+ >
+ metric_domain.Maintainability
+ </th>
+ <th
+ className="thin nowrap text-right code-components-cell"
+ key="metric.ncloc.name"
+ >
+ metric.ncloc.name
+ </th>
+ </tr>
+</thead>
+`;
+
+exports[`renders correctly for projects 1`] = `
+<thead>
+ <tr
+ className="code-components-header"
+ >
+ <th
+ className="thin nowrap"
+ >
+
+ </th>
+ <th>
+
+ </th>
+ <th
+ className="thin nowrap text-right"
+ key="metric.foo.name"
+ >
+ metric.foo.name
+ </th>
+ <th
+ className="thin nowrap text-right code-components-cell"
+ key="metric.bar.name"
+ >
+ metric.bar.name
+ </th>
+ </tr>
+</thead>
+`;
+++ /dev/null
-/*
- * 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 { Component } from '../../types';
-import {
- addComponent,
- getComponent,
- addComponentChildren,
- getComponentChildren
-} from '../../bucket';
-
-const component: Component = { key: 'frodo', name: 'frodo', qualifier: 'frodo' };
-
-const componentKey: string = 'foo';
-const childrenA: Component[] = [
- { key: 'foo', name: 'foo', qualifier: 'foo' },
- { key: 'bar', name: 'bar', qualifier: 'bar' }
-];
-const childrenB: Component[] = [
- { key: 'bart', name: 'bart', qualifier: 'bart' },
- { key: 'simpson', name: 'simpson', qualifier: 'simpson' }
-];
-
-it('should have empty bucket at start', () => {
- expect(getComponent(component.key)).toBeUndefined();
-});
-
-it('should be able to store components in a bucket', () => {
- addComponent(component);
- expect(getComponent(component.key)).toEqual(component);
-});
-
-it('should have empty children bucket at start', () => {
- expect(getComponentChildren(componentKey)).toBeUndefined();
-});
-
-it('should be able to store children components in a bucket', () => {
- addComponentChildren(componentKey, childrenA, childrenA.length, 1);
- expect(getComponentChildren(componentKey).children).toEqual(childrenA);
-});
-
-it('should append new children components at the end of the bucket', () => {
- addComponentChildren(componentKey, childrenB, 4, 2);
- const finalBucket = getComponentChildren(componentKey);
- expect(finalBucket.children).toEqual([...childrenA, ...childrenB]);
- expect(finalBucket.total).toBe(4);
- expect(finalBucket.page).toBe(2);
-});
+++ /dev/null
-/*
- * 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 { Measure } from '../../app/types';
-
-export interface Component extends Breadcrumb {
- branch?: string;
- measures?: Measure[];
- path?: string;
- refKey?: string;
-}
-
-export interface Breadcrumb {
- key: string;
- name: string;
- qualifier: string;
-}
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(
metrics: string[],
page: number,
branchLike?: BranchLike
-): Promise<Component[]> {
+): Promise<ComponentMeasure[]> {
return getChildren(componentKey, metrics, {
p: page,
ps: PAGE_SIZE,
componentKey: string,
metrics: string[],
branchLike?: BranchLike
-): Promise<Component[]> {
+): Promise<ComponentMeasure[]> {
return requestChildren(componentKey, metrics, 1, branchLike);
}
interface Children {
- components: Component[];
+ components: ComponentMeasure[];
page: number;
total: number;
}
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 => {
};
}
+function showLeakMeasure(branchLike?: BranchLike) {
+ return isShortLivingBranch(branchLike) || isPullRequest(branchLike);
+}
+
function prepareChildren(r: any): Children {
return {
components: r.components,
};
}
-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);
}
}
}
-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,
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({
});
}
- const metrics = getMetrics(isPortfolio);
+ const metrics = getCodeMetrics(qualifier, branchLike);
return getChildren(componentKey, metrics, {
ps: PAGE_SIZE,
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
};
});
}
export function loadMoreChildren(
componentKey: string,
page: number,
- isPortfolio: boolean,
+ qualifier: string,
branchLike?: BranchLike
): Promise<Children> {
- const metrics = getMetrics(isPortfolio);
+ const metrics = getCodeMetrics(qualifier, branchLike);
return getChildren(componentKey, metrics, {
ps: PAGE_SIZE,