--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import { getJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
+
+type GetApplicationLeakResponse = Array<{
+ date: string,
+ project: string,
+ projectName: string
+}>;
+
+export const getApplicationLeak = (application: string): Promise<GetApplicationLeakResponse> =>
+ getJSON('/api/views/show_leak', { application }).then(r => r.leaks, throwGlobalError);
const data = { gateId, projectKey };
return post(url, data);
}
+
+export function getApplicationQualityGate(application) {
+ return getJSON('/api/qualitygates/application_status', { application });
+}
populateRecentHistory = () => {
const { breadcrumbs } = this.props.component;
const { qualifier } = breadcrumbs[breadcrumbs.length - 1];
- if (['TRK', 'VW', 'DEV'].indexOf(qualifier) !== -1) {
+ if (['TRK', 'VW', 'APP', 'DEV'].indexOf(qualifier) !== -1) {
RecentHistory.add(
this.props.component.key,
this.props.component.name,
return qualifier === 'VW' || qualifier === 'SVW';
}
+ isApplication() {
+ return this.props.component.qualifier === 'APP';
+ }
+
renderDashboardLink() {
const pathname = this.isView() ? '/portfolio' : '/dashboard';
return (
<Link
to={{ pathname: '/code', query: { id: this.props.component.key } }}
activeClassName="active">
- {this.isView() ? translate('view_projects.page') : translate('code.page')}
+ {this.isView() || this.isApplication()
+ ? translate('view_projects.page')
+ : translate('code.page')}
</Link>
</li>
);
}
renderActivityLink() {
- if (!this.isProject()) {
+ if (!this.isProject() && !this.isApplication()) {
return null;
}
}
renderSettingsLink() {
- if (!this.props.conf.showSettings) {
+ if (!this.props.conf.showSettings || this.isApplication()) {
return null;
}
return (
return null;
}
- if (qualifier !== 'TRK' && qualifier !== 'VW') {
+ if (qualifier !== 'TRK' && qualifier !== 'VW' && qualifier !== 'APP') {
return null;
}
return null;
}
- if (!['VW', 'SVW', 'TRK'].includes(component.qualifier) || component.organization == null) {
+ if (
+ !['VW', 'SVW', 'APP', 'TRK'].includes(component.qualifier) ||
+ component.organization == null
+ ) {
return null;
}
// @flow
import { sortBy } from 'lodash';
-const ORDER = ['DEV', 'VW', 'SVW', 'TRK', 'BRC', 'FIL', 'UTS'];
+const ORDER = ['DEV', 'VW', 'SVW', 'APP', 'TRK', 'BRC', 'FIL', 'UTS'];
export function sortQualifiers(qualifiers: Array<string>) {
return sortBy(qualifiers, qualifier => ORDER.indexOf(qualifier));
addComponentBreadcrumbs(component.key, component.breadcrumbs);
this.setState({ loading: true });
- const isView = component.qualifier === 'VW' || component.qualifier === 'SVW';
- retrieveComponentChildren(component.key, isView)
+ const isPortfolio = ['VW', 'SVW'].includes(component.qualifier);
+ retrieveComponentChildren(component.key, isPortfolio)
.then(r => {
addComponent(r.baseComponent);
this.handleUpdate();
loadComponent(componentKey) {
this.setState({ loading: true });
- const isView =
- this.props.component.qualifier === 'VW' || this.props.component.qualifier === 'SVW';
- retrieveComponent(componentKey, isView)
+ const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
+ retrieveComponent(componentKey, isPortfolio)
.then(r => {
if (this.mounted) {
if (['FIL', 'UTS'].includes(r.component.qualifier)) {
handleLoadMore() {
const { baseComponent, page } = this.state;
- const isView =
- this.props.component.qualifier === 'VW' || this.props.component.qualifier === 'SVW';
- loadMoreChildren(baseComponent.key, page + 1, isView)
+ const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
+ loadMoreChildren(baseComponent.key, page + 1, isPortfolio)
.then(r => {
if (this.mounted) {
this.setState({
render() {
const { component, rootComponent, selected, previous, canBrowse } = this.props;
- const isView = ['VW', 'SVW'].includes(rootComponent.qualifier);
+ const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier);
+ const isApplication = rootComponent.qualifier === 'APP';
let componentAction = null;
}
}
- const columns = isView
+ const columns = isPortfolio
? [
{ metric: 'releasability_rating', type: 'RATING' },
{ metric: 'reliability_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' },
{ metric: 'coverage', type: 'PERCENT' },
{ metric: 'duplicated_lines_density', type: 'PERCENT' }
- ];
+ ].filter(Boolean);
return (
<tr className={classNames({ selected })}>
import { translate } from '../../../helpers/l10n';
const ComponentsHeader = ({ baseComponent, rootComponent }) => {
- const isView = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW';
+ const isPortfolio = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW';
+ const isApplication = rootComponent.qualifier === 'APP';
- const columns = isView
+ const columns = isPortfolio
? [
translate('metric_domain.Releasability'),
translate('metric_domain.Reliability'),
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'),
translate('metric', 'coverage', 'name'),
translate('metric', 'duplicated_lines_density', 'short_name')
- ];
+ ].filter(Boolean);
return (
<thead>
const { component, onError } = this.props;
this.setState({ loading: true });
- const isView = component.qualifier === 'VW' || component.qualifier === 'SVW';
- const qualifiers = isView ? 'SVW,TRK' : 'BRC,UTS,FIL';
+ const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier);
+ const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL';
getTree(component.key, { q: query, s: 'qualifier,name', qualifiers })
.then(r => {
'alert_status'
];
-const VIEW_METRICS = [
+const PORTFOLIO_METRICS = [
'releasability_rating',
'alert_status',
'reliability_rating',
}
}
-function getMetrics(isView) {
- return isView ? VIEW_METRICS : METRICS;
+function getMetrics(isPortfolio) {
+ return isPortfolio ? PORTFOLIO_METRICS : METRICS;
}
/**
* @param {string} componentKey
- * @param {boolean} isView
+ * @param {boolean} isPortfolio
* @returns {Promise}
*/
-function retrieveComponentBase(componentKey, isView) {
+function retrieveComponentBase(componentKey, isPortfolio) {
const existing = getComponentFromBucket(componentKey);
if (existing) {
return Promise.resolve(existing);
}
- const metrics = getMetrics(isView);
+ const metrics = getMetrics(isPortfolio);
return getComponent(componentKey, metrics).then(component => {
addComponent(component);
/**
* @param {string} componentKey
- * @param {boolean} isView
+ * @param {boolean} isPortfolio
* @returns {Promise}
*/
-export function retrieveComponentChildren(componentKey, isView) {
+export function retrieveComponentChildren(componentKey, isPortfolio) {
const existing = getComponentChildren(componentKey);
if (existing) {
return Promise.resolve({
});
}
- const metrics = getMetrics(isView);
+ const metrics = getMetrics(isPortfolio);
return getChildren(componentKey, metrics, { ps: PAGE_SIZE, s: 'qualifier,name' })
.then(prepareChildren)
/**
* @param {string} componentKey
- * @param {boolean} isView
+ * @param {boolean} isPortfolio
* @returns {Promise}
*/
-export function retrieveComponent(componentKey, isView) {
+export function retrieveComponent(componentKey, isPortfolio) {
return Promise.all([
- retrieveComponentBase(componentKey, isView),
- retrieveComponentChildren(componentKey, isView),
+ retrieveComponentBase(componentKey, isPortfolio),
+ retrieveComponentChildren(componentKey, isPortfolio),
retrieveComponentBreadcrumbs(componentKey)
]).then(r => {
return {
});
}
-export function loadMoreChildren(componentKey, page, isView) {
- const metrics = getMetrics(isView);
+export function loadMoreChildren(componentKey, page, isPortfolio) {
+ const metrics = getMetrics(isPortfolio);
return getChildren(componentKey, metrics, { ps: PAGE_SIZE, p: page })
.then(prepareChildren)
import moment from 'moment';
import Tooltip from '../../../components/controls/Tooltip';
import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods';
-import { translateWithParameters } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export default function LeakPeriodLegend({ component, period }) {
+ if (component.qualifier === 'APP') {
+ return (
+ <div className="measures-domains-leak-header">
+ {translate('issues.leak_period')}
+ </div>
+ );
+ }
-export default function LeakPeriodLegend({ period }) {
const label = (
<div className="measures-domains-leak-header">
{translateWithParameters('overview.leak_period_x', getPeriodLabel(period))}
{isDiff &&
<div className="pull-right">
- <LeakPeriodLegend period={leakPeriod} />
+ <LeakPeriodLegend component={component} period={leakPeriod} />
</div>}
<TooltipsContainer options={{ html: false }}>
</ul>
</nav>
- {leakPeriod != null && <LeakPeriodLegend period={leakPeriod} />}
+ {leakPeriod != null && <LeakPeriodLegend component={component} period={leakPeriod} />}
</header>
<main id="component-measures-home-main">
*/
import { startFetching, stopFetching } from '../store/statusActions';
import { getMeasuresAndMeta } from '../../../api/measures';
-import { getLeakPeriod } from '../../../helpers/periods';
import { getLeakValue } from '../utils';
import { getMeasuresAppComponent, getMeasuresAppAllMetrics } from '../../../store/rootReducer';
}
function banQualityGate(component, measures) {
- if (['VW', 'SVW'].includes(component.qualifier)) {
- return measures;
+ let newMeasures = [...measures];
+
+ if (!['VW', 'SVW', 'APP'].includes(component.qualifier)) {
+ newMeasures = newMeasures.filter(measure => measure.metric !== 'alert_status');
+ }
+
+ if (component.qualifier === 'APP') {
+ newMeasures = newMeasures.filter(
+ measure =>
+ measure.metric !== 'releasability_rating' && measure.metric !== 'releasability_effort'
+ );
}
- return measures.filter(measure => measure.metric !== 'alert_status');
+
+ return newMeasures;
}
export function fetchMeasures() {
.map(metric => metric.key);
getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => {
- const leakPeriod = getLeakPeriod(r.periods);
const measures = banQualityGate(component, r.component.measures)
.map(measure => {
const metric = metrics.find(metric => metric.key === measure.metric);
})
.filter(measure => {
const hasValue = measure.value != null;
- const hasLeakValue = !!leakPeriod && measure.leak != null;
+ const hasLeakValue = measure.leak != null;
return hasValue || hasLeakValue;
});
- dispatch(receiveMeasures(measures, r.periods));
+ const newBugs = measures.find(measure => measure.metric.key === 'new_bugs');
+
+ const applicationPeriods = newBugs ? [{ index: 1 }] : [];
+ const periods = component.qualifier === 'APP' ? applicationPeriods : r.periods;
+
+ dispatch(receiveMeasures(measures, periods));
dispatch(stopFetching());
});
};
handleSearch = (query: string) => {
const { component, organization } = this.props;
- if (component != null && ['VW', 'SVW'].includes(component.qualifier)) {
+ if (component != null && ['VW', 'SVW', 'APP'].includes(component.qualifier)) {
return getTree(component.key, { ps: 50, q: query, qualifiers: 'TRK' }).then(response =>
response.components.map(component => ({
label: component.name,
*/
// @flow
import React from 'react';
-import { withRouter } from 'react-router';
import OverviewApp from './OverviewApp';
import EmptyOverview from './EmptyOverview';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
router: Object
};
-class App extends React.PureComponent {
+export default class App extends React.PureComponent {
props: Props;
state: Object;
+ static contextTypes = {
+ router: React.PropTypes.object
+ };
+
componentDidMount() {
- if (['VW', 'SVW'].includes(this.props.component.qualifier)) {
- this.props.router.replace({
+ if (this.isPortfolio()) {
+ this.context.router.replace({
pathname: '/portfolio',
query: { id: this.props.component.key }
});
}
}
+ isPortfolio() {
+ return this.props.component.qualifier === 'VW' || this.props.component.qualifier === 'SVW';
+ }
+
render() {
+ if (this.isPortfolio()) {
+ return null;
+ }
+
const { component } = this.props;
if (['FIL', 'UTS'].includes(component.qualifier)) {
return <EmptyOverview component={component} />;
}
- return <OverviewApp {...this.props} leakPeriodIndex="1" />;
+ return <OverviewApp component={component} />;
}
}
-
-export default withRouter(App);
-
-export const UnconnectedApp = App;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Tooltip from '../../../components/controls/Tooltip';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import { getApplicationLeak } from '../../../api/application';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+ component: { key: string }
+};
+
+type State = {
+ leaks: ?Array<{ date: string, project: string, projectName: string }>
+};
+
+export default class ApplicationLeakPeriodLegend extends React.Component {
+ mounted: boolean;
+ props: Props;
+ state: State = {
+ leaks: null
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.component.key !== this.props.component.key) {
+ this.setState({ leaks: null });
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchLeaks = (visible: boolean) => {
+ if (visible && this.state.leaks == null) {
+ getApplicationLeak(this.props.component.key).then(
+ leaks => {
+ if (this.mounted) {
+ this.setState({ leaks });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ leaks: [] });
+ }
+ }
+ );
+ }
+ };
+
+ renderOverlay = () =>
+ this.state.leaks != null
+ ? <ul className="text-left">
+ {this.state.leaks.map(leak =>
+ <li key={leak.project}>
+ {leak.projectName}: <FormattedDate date={leak.date} format="LL" />
+ </li>
+ )}
+ </ul>
+ : <i className="spinner spinner-margin" />;
+
+ render() {
+ return (
+ <Tooltip onVisibleChange={this.fetchLeaks} overlay={this.renderOverlay()}>
+ <div className="overview-legend overview-legend-spaced-line">
+ {translate('issues.leak_period')}
+ </div>
+ </Tooltip>
+ );
+ }
+}
import { uniq } from 'lodash';
import moment from 'moment';
import QualityGate from '../qualityGate/QualityGate';
+import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate';
import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities';
import CodeSmells from '../main/CodeSmells';
import Coverage from '../main/Coverage';
}, throwGlobalError);
}
+ getApplicationLeakPeriod = () =>
+ this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null;
+
renderLoading() {
return (
<div className="text-center">
return this.renderLoading();
}
- const leakPeriod = getLeakPeriod(periods);
+ const leakPeriod =
+ component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods);
const domainProps = { component, measures, leakPeriod, history, historyStartDate };
return (
<div className="page page-limited">
<div className="overview page-with-sidebar">
<div className="overview-main page-main">
- <QualityGate component={component} measures={measures} />
+ {component.qualifier === 'APP'
+ ? <ApplicationQualityGate component={component} />
+ : <QualityGate component={component} measures={measures} />}
<div className="overview-domains-list">
<BugsAndVulnerabilities {...domainProps} />
*/
import React from 'react';
import { shallow } from 'enzyme';
-import { UnconnectedApp } from '../App';
+import App from '../App';
import OverviewApp from '../OverviewApp';
import EmptyOverview from '../EmptyOverview';
it('should render OverviewApp', () => {
const component = { id: 'id', analysisDate: '2016-01-01' };
- const output = shallow(<UnconnectedApp component={component} />);
+ const output = shallow(<App component={component} />);
expect(output.type()).toBe(OverviewApp);
});
it('should render EmptyOverview', () => {
const component = { id: 'id' };
- const output = shallow(<UnconnectedApp component={component} />);
+ const output = shallow(<App component={component} />);
expect(output.type()).toBe(EmptyOverview);
});
-
-it('should pass leakPeriodIndex', () => {
- const component = { id: 'id', analysisDate: '2016-01-01' };
- const output = shallow(<UnconnectedApp component={component} />);
- expect(output.prop('leakPeriodIndex')).toBe('1');
-});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import ApplicationLeakPeriodLegend from '../ApplicationLeakPeriodLegend';
+
+it('renders', () => {
+ const wrapper = shallow(<ApplicationLeakPeriodLegend component={{ key: 'foo' }} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({
+ leaks: [
+ { date: '2017-01-01T11:39:03+0100', project: 'foo', projectName: 'Foo' },
+ { date: '2017-02-01T11:39:03+0100', project: 'bar', projectName: 'Bar' }
+ ]
+ });
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Tooltip
+ onVisibleChange={[Function]}
+ overlay={
+ <i
+ className="spinner spinner-margin"
+ />
+ }
+ placement="bottom"
+>
+ <div
+ className="overview-legend overview-legend-spaced-line"
+ >
+ issues.leak_period
+ </div>
+</Tooltip>
+`;
+
+exports[`renders 2`] = `
+<Tooltip
+ onVisibleChange={[Function]}
+ overlay={
+ <ul
+ className="text-left"
+ >
+ <li>
+ Foo
+ :
+ <FormattedDate
+ date="2017-01-01T11:39:03+0100"
+ format="LL"
+ />
+ </li>
+ <li>
+ Bar
+ :
+ <FormattedDate
+ date="2017-02-01T11:39:03+0100"
+ format="LL"
+ />
+ </li>
+ </ul>
+ }
+ placement="bottom"
+>
+ <div
+ className="overview-legend overview-legend-spaced-line"
+ >
+ issues.leak_period
+ </div>
+</Tooltip>
+`;
import { Link } from 'react-router';
import enhance from './enhance';
import LeakPeriodLegend from '../components/LeakPeriodLegend';
+import ApplicationLeakPeriodLegend from '../components/ApplicationLeakPeriodLegend';
import { getMetricName } from '../helpers/metrics';
import { translate } from '../../../helpers/l10n';
import BugIcon from '../../../components/icons-components/BugIcon';
}
renderLeak() {
- const { leakPeriod } = this.props;
+ const { component, leakPeriod } = this.props;
if (leakPeriod == null) {
return null;
return (
<div className="overview-domain-leak">
- <LeakPeriodLegend period={leakPeriod} />
+ {component.qualifier === 'APP'
+ ? <ApplicationLeakPeriodLegend component={component} />
+ : <LeakPeriodLegend period={leakPeriod} />}
<div className="overview-domain-measures">
<div className="overview-domain-measure">
</div>
);
};
+
renderRating = metricKey => {
const { component, measures } = this.props;
const measure = measures.find(measure => measure.metric.key === metricKey);
</Tooltip>
);
};
+
renderIssues = (metric, type) => {
const { measures, component } = this.props;
const measure = measures.find(measure => measure.metric.key === metric);
</Tooltip>
);
};
+
renderHistoryLink = metricKey => {
const linkClass =
'button button-small button-compact spacer-left overview-domain-measure-history-link';
</Link>
);
};
+
renderTimeline = (metricKey, range, children) => {
if (!this.props.history) {
return null;
</div>
);
};
+
render() {
return (
<ComposedComponent
const { qualifier, description, qualityProfiles, qualityGate } = component;
const isProject = qualifier === 'TRK';
- const isView = qualifier === 'VW' || qualifier === 'SVW';
- const isDeveloper = qualifier === 'DEV';
+ const isApplication = qualifier === 'APP';
const hasDescription = !!description;
const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
const hasQualityGate = !!qualityGate;
- const shouldShowQualityProfiles = !isView && !isDeveloper && hasQualityProfiles;
- const shouldShowQualityGate = !isView && !isDeveloper && hasQualityGate;
+ const shouldShowQualityProfiles = isProject && hasQualityProfiles;
+ const shouldShowQualityGate = isProject && hasQualityGate;
const hasOrganization = component.organization != null && areThereCustomOrganizations;
return (
{isProject && <MetaTags component={component} />}
- {isProject && <AnalysesList project={component.key} history={history} router={router} />}
+ {(isProject || isApplication) &&
+ <AnalysesList project={component.key} history={history} router={router} />}
{shouldShowQualityGate &&
<MetaQualityGate
profiles={qualityProfiles}
/>}
- <MetaLinks component={component} />
+ {isProject && <MetaLinks component={component} />}
<MetaKey component={component} />
*/
import React from 'react';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import { DrilldownLink } from '../../../components/shared/drilldown-link';
import LanguageDistribution from '../../../components/charts/LanguageDistribution';
+import SizeRating from '../../../components/ui/SizeRating';
import { formatMeasure } from '../../../helpers/measures';
import { getMetricName } from '../helpers/metrics';
-import SizeRating from '../../../components/ui/SizeRating';
+import { translate } from '../../../helpers/l10n';
export default class MetaSize extends React.PureComponent {
static propTypes = {
measures: PropTypes.array.isRequired
};
- render() {
- const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc');
+ renderLoC = ncloc =>
+ <div
+ id="overview-ncloc"
+ className={classNames('overview-meta-size-ncloc', {
+ 'is-half-width': this.props.component.qualifier === 'APP'
+ })}>
+ <span className="spacer-right">
+ <SizeRating value={ncloc.value} />
+ </span>
+ <DrilldownLink component={this.props.component.key} metric="ncloc">
+ {formatMeasure(ncloc.value, 'SHORT_INT')}
+ </DrilldownLink>
+ <div className="overview-domain-measure-label text-muted">
+ {getMetricName('ncloc')}
+ </div>
+ </div>;
+
+ renderLoCDistribution = () => {
const languageDistribution = this.props.measures.find(
measure => measure.metric.key === 'ncloc_language_distribution'
);
- if (ncloc == null || languageDistribution == null) {
- return null;
- }
+ return languageDistribution
+ ? <div id="overview-language-distribution" className="overview-meta-size-lang-dist">
+ <LanguageDistribution distribution={languageDistribution.value} />
+ </div>
+ : null;
+ };
- return (
- <div id="overview-size" className="overview-meta-card">
- <div id="overview-ncloc" className="overview-meta-size-ncloc">
- <span className="spacer-right">
- <SizeRating value={ncloc.value} />
- </span>
- <DrilldownLink component={this.props.component.key} metric="ncloc">
- {formatMeasure(ncloc.value, 'SHORT_INT')}
+ renderProjects = () => {
+ const projects = this.props.measures.find(measure => measure.metric.key === 'projects');
+
+ return projects
+ ? <div
+ id="overview-projects"
+ className="overview-meta-size-ncloc is-half-width bordered-left">
+ <DrilldownLink component={this.props.component.key} metric="projects">
+ {formatMeasure(projects.value, 'SHORT_INT')}
</DrilldownLink>
<div className="overview-domain-measure-label text-muted">
- {getMetricName('ncloc')}
+ {translate('metric.projects.name')}
</div>
</div>
- <div id="overview-language-distribution" className="overview-meta-size-lang-dist">
- <LanguageDistribution distribution={languageDistribution.value} />
- </div>
+ : null;
+ };
+
+ render() {
+ const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc');
+
+ if (ncloc == null) {
+ return null;
+ }
+
+ return (
+ <div id="overview-size" className="overview-meta-card">
+ {this.renderLoC(ncloc)}
+ {this.props.component.qualifier === 'APP'
+ ? this.renderProjects()
+ : this.renderLoCDistribution()}
</div>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { keyBy } from 'lodash';
+import ApplicationQualityGateProject from './ApplicationQualityGateProject';
+import Level from '../../../components/ui/Level';
+import { getApplicationQualityGate } from '../../../api/quality-gates';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+ component: { key: string }
+};
+
+type State = {
+ loading: boolean,
+ metrics?: { [string]: Object },
+ projects?: Array<{
+ conditions: Array<Object>,
+ key: string,
+ name: string,
+ status: string
+ }>,
+ status?: string
+};
+
+export default class ApplicationQualityGate extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State = {
+ loading: true
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchDetails();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.component.key !== this.props.component.key) {
+ this.fetchDetails();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchDetails = () => {
+ this.setState({ loading: true });
+ getApplicationQualityGate(this.props.component.key).then(({ status, projects, metrics }) => {
+ if (this.mounted) {
+ this.setState({
+ loading: false,
+ metrics: keyBy(metrics, 'key'),
+ status,
+ projects
+ });
+ }
+ });
+ };
+
+ render() {
+ const { metrics, status, projects } = this.state;
+
+ return (
+ <div className="overview-quality-gate" id="overview-quality-gate">
+ <h2 className="overview-title">
+ {translate('overview.quality_gate')}
+ {this.state.loading && <i className="spinner spacer-left" />}
+ {status != null && <Level level={status} />}
+ </h2>
+
+ {projects != null &&
+ <div
+ id="overview-quality-gate-conditions-list"
+ className="overview-quality-gate-conditions-list clearfix">
+ {projects
+ .filter(project => project.status !== 'OK')
+ .map(project =>
+ <ApplicationQualityGateProject
+ key={project.key}
+ metrics={metrics}
+ project={project}
+ />
+ )}
+ </div>}
+ </div>
+ );
+ }
+}
--- /dev/null
+.application-quality-gate-project {
+ padding: 10px;
+}
+
+.overview-quality-gate-condition:hover .application-quality-gate-project {
+ padding: 9px;
+}
+
+.application-quality-gate-project-conditions {
+ margin-top: 4px;
+}
+
+.application-quality-gate-project-conditions > li {
+ margin-top: 4px;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { Link } from 'react-router';
+import classNames from 'classnames';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { getProjectUrl } from '../../../helpers/urls';
+import './ApplicationQualityGateProject.css';
+
+type Condition = {
+ comparator: string,
+ errorThreshold?: string,
+ metricKey: string,
+ onLeak: boolean,
+ status: string,
+ value: string,
+ warningThreshold?: string
+};
+
+type Props = {
+ metrics: {
+ [string]: {
+ key: string,
+ name: string,
+ type: string
+ }
+ },
+ project: {
+ conditions: Array<Condition>,
+ key: string,
+ name: string,
+ status: string
+ }
+};
+
+export default class ApplicationQualityGateProject extends React.PureComponent {
+ props: Props;
+
+ renderCondition = (condition: Condition) => {
+ const metric = this.props.metrics[condition.metricKey];
+ const metricName = getLocalizedMetricName(metric);
+ const threshold = condition.errorThreshold || condition.warningThreshold;
+ const isDiff = isDiffMetric(condition.metricKey);
+
+ return (
+ <li key={condition.metricKey}>
+ <span className="text-limited">
+ <strong>{formatMeasure(condition.value, metric.type)}</strong> {metricName}
+ {!isDiff && condition.onLeak && ' ' + translate('quality_gates.conditions.leak')}
+ </span>
+ <span
+ className={classNames('pull-right', 'big-spacer-left', {
+ 'text-danger': condition.status === 'ERROR',
+ 'text-warning': condition.status === 'WARN'
+ })}>
+ {translate('quality_gates.operator', condition.comparator, 'short')}{' '}
+ {formatMeasure(threshold, metric.type)}
+ </span>
+ </li>
+ );
+ };
+
+ render() {
+ const { project } = this.props;
+
+ return (
+ <Link
+ className={classNames(
+ 'overview-quality-gate-condition',
+ 'overview-quality-gate-condition-' + project.status.toLowerCase()
+ )}
+ to={getProjectUrl(project.key)}>
+ <div className="application-quality-gate-project">
+ <h4>
+ {project.name}
+ </h4>
+ <ul className="application-quality-gate-project-conditions">
+ {project.conditions.filter(c => c.status !== 'OK').map(this.renderCondition)}
+ </ul>
+ </div>
+ </Link>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import ApplicationQualityGate from '../ApplicationQualityGate';
+
+it('renders', () => {
+ const wrapper = shallow(<ApplicationQualityGate component={{ key: 'foo' }} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({
+ loading: false,
+ metrics: {},
+ status: 'ERROR',
+ projects: [
+ { conditions: [], key: 'project1', name: 'project1', status: 'ERROR' },
+ { conditions: [], key: 'project2', name: 'project2', status: 'OK' },
+ { conditions: [], key: 'project3', name: 'project3', status: 'WARN' }
+ ]
+ });
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import ApplicationQualityGateProject from '../ApplicationQualityGateProject';
+
+const metrics = {
+ bugs: { key: 'bugs', name: 'Bugs', type: 'INT' },
+ new_coverage: { key: 'new_coverage', name: 'Coverage on New Code', type: 'PERCENT' },
+ skipped_tests: { key: 'skipped_tests', name: 'Skipped Tests', type: 'INT' }
+};
+
+it('renders', () => {
+ const project = {
+ key: 'foo',
+ name: 'Foo',
+ status: 'ERROR',
+ conditions: [
+ {
+ status: 'ERROR',
+ metricKey: 'new_coverage',
+ comparator: 'LT',
+ onLeak: true,
+ errorThreshold: '85',
+ value: '82.50562381034781'
+ },
+ {
+ status: 'WARN',
+ metricKey: 'bugs',
+ comparator: 'GT',
+ onLeak: false,
+ warningThreshold: '0',
+ value: '17'
+ },
+ {
+ status: 'ERROR',
+ metricKey: 'bugs',
+ comparator: 'GT',
+ onLeak: true,
+ warningThreshold: '0',
+ value: '3'
+ },
+ {
+ status: 'OK',
+ metricKey: 'skipped_tests',
+ comparator: 'GT',
+ onLeak: false,
+ warningThreshold: '0',
+ value: '0'
+ }
+ ]
+ };
+ const wrapper = shallow(<ApplicationQualityGateProject metrics={metrics} project={project} />);
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+ className="overview-quality-gate"
+ id="overview-quality-gate"
+>
+ <h2
+ className="overview-title"
+ >
+ overview.quality_gate
+ <i
+ className="spinner spacer-left"
+ />
+ </h2>
+</div>
+`;
+
+exports[`renders 2`] = `
+<div
+ className="overview-quality-gate"
+ id="overview-quality-gate"
+>
+ <h2
+ className="overview-title"
+ >
+ overview.quality_gate
+ <Level
+ level="ERROR"
+ muted={false}
+ small={false}
+ />
+ </h2>
+ <div
+ className="overview-quality-gate-conditions-list clearfix"
+ id="overview-quality-gate-conditions-list"
+ >
+ <ApplicationQualityGateProject
+ metrics={Object {}}
+ project={
+ Object {
+ "conditions": Array [],
+ "key": "project1",
+ "name": "project1",
+ "status": "ERROR",
+ }
+ }
+ />
+ <ApplicationQualityGateProject
+ metrics={Object {}}
+ project={
+ Object {
+ "conditions": Array [],
+ "key": "project3",
+ "name": "project3",
+ "status": "WARN",
+ }
+ }
+ />
+ </div>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Link
+ className="overview-quality-gate-condition overview-quality-gate-condition-error"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+>
+ <div
+ className="application-quality-gate-project"
+ >
+ <h4>
+ Foo
+ </h4>
+ <ul
+ className="application-quality-gate-project-conditions"
+ >
+ <li>
+ <span
+ className="text-limited"
+ >
+ <strong>
+ 82.5%
+ </strong>
+
+ Coverage on New Code
+ </span>
+ <span
+ className="pull-right big-spacer-left text-danger"
+ >
+ quality_gates.operator.LT.short
+
+ 85.0%
+ </span>
+ </li>
+ <li>
+ <span
+ className="text-limited"
+ >
+ <strong>
+ 17
+ </strong>
+
+ Bugs
+ </span>
+ <span
+ className="pull-right big-spacer-left text-warning"
+ >
+ quality_gates.operator.GT.short
+
+ 0
+ </span>
+ </li>
+ <li>
+ <span
+ className="text-limited"
+ >
+ <strong>
+ 3
+ </strong>
+
+ Bugs
+ quality_gates.conditions.leak
+ </span>
+ <span
+ className="pull-right big-spacer-left text-danger"
+ >
+ quality_gates.operator.GT.short
+
+ 0
+ </span>
+ </li>
+ </ul>
+ </div>
+</Link>
+`;
text-align: center;
}
+.overview-meta-size-ncloc.is-half-width {
+ width: 50%;
+ box-sizing: border-box;
+}
+
.overview-meta-size-ncloc a {
line-height: 24px;
font-size: 18px;
// size
'ncloc',
'ncloc_language_distribution',
+ 'projects',
'new_lines'
];
import { getRootQualifiers } from '../../../store/appState/duck';
const mapStateToProps = state => ({
- topQualifiers: getRootQualifiers(getAppState(state))
+ // treat applications as portfolios
+ topQualifiers: getRootQualifiers(getAppState(state)).filter(q => q !== 'APP')
});
export default connect(mapStateToProps)(App);
/>
<PageError />
{this.props.component.qualifier === 'TRK' &&
- <VisibilitySelector
- canTurnToPrivate={canTurnToPrivate}
- className="big-spacer-top big-spacer-bottom"
- onChange={this.handleVisibilityChange}
- visibility={this.props.component.visibility}
- />}
- {!canTurnToPrivate &&
- <UpgradeOrganizationBox organization={this.props.component.organization} />}
- {this.state.disclaimer &&
- <PublicProjectDisclaimer
- component={this.props.component}
- onClose={this.closeDisclaimer}
- onConfirm={this.turnProjectToPublic}
- />}
+ <div>
+ <VisibilitySelector
+ canTurnToPrivate={canTurnToPrivate}
+ className="big-spacer-top big-spacer-bottom"
+ onChange={this.handleVisibilityChange}
+ visibility={this.props.component.visibility}
+ />
+ {!canTurnToPrivate &&
+ <UpgradeOrganizationBox organization={this.props.component.organization} />}
+ {this.state.disclaimer &&
+ <PublicProjectDisclaimer
+ component={this.props.component}
+ onClose={this.closeDisclaimer}
+ onConfirm={this.turnProjectToPublic}
+ />}
+ </div>}
<AllHoldersList
component={this.props.component}
filter={this.state.filter}
const canApplyPermissionTemplate =
configuration != null && configuration.canApplyPermissionTemplate;
- const description = ['VW', 'SVW'].includes(component.qualifier)
+ const description = ['VW', 'SVW', 'APP'].includes(component.qualifier)
? translate('roles.page.description_portfolio')
: translate('roles.page.description2');
TRK: PERMISSIONS_ORDER_FOR_PROJECT,
VW: PERMISSIONS_ORDER_FOR_VIEW,
SVW: PERMISSIONS_ORDER_FOR_VIEW,
+ APP: PERMISSIONS_ORDER_FOR_VIEW,
DEV: PERMISSIONS_ORDER_FOR_DEV
};
import { translate } from '../../../helpers/l10n';
export default function Header(props: { component: { qualifier: string } }) {
- const description = ['VW', 'SVW'].includes(props.component.qualifier)
+ const { qualifier } = props.component;
+ const description = ['VW', 'SVW'].includes(qualifier)
? translate('portfolio_deletion.page.description')
- : translate('project_deletion.page.description');
+ : qualifier === 'APP'
+ ? translate('application_deletion.page.description')
+ : translate('project_deletion.page.description');
return (
<header className="page-header">
*/
export const PAGE_SIZE = 50;
-export const QUALIFIERS_ORDER = ['TRK', 'VW', 'DEV'];
+export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV'];
export const TYPE = {
ALL: 'ALL',
qualifiers.update.APP=Update Application
qualifier.description.VW=Potentially multi-level, management-oriented overview aggregation.
+qualifier.description.SVW=Potentially multi-level, management-oriented overview aggregation.
qualifier.description.APP=Single-level aggregation with a technical focus and a project-like homepage.
#------------------------------------------------------------------------------
deletion.page=Deletion
project_deletion.page.description=Delete this project from SonarQube. The operation cannot be undone.
portfolio_deletion.page.description=Delete this portfolio from SonarQube. Component projects and Local Reference Portfolios will not be deleted, but component Standard Portfolios will be deleted. This operation cannot be undone.
+application_deletion.page.description=Delete this application from SonarQube. Application projects will not be deleted. This operation cannot be undone.
provisioning.page=Provisioning
provisioning.page.description=Use this page to initialize projects if you would like to configure them before the first analysis. Once a project is provisioned, you have access to perform all project configurations on it.