diff options
Diffstat (limited to 'server/sonar-web/src')
41 files changed, 912 insertions, 115 deletions
diff --git a/server/sonar-web/src/main/js/api/application.js b/server/sonar-web/src/main/js/api/application.js new file mode 100644 index 00000000000..bbc9016c1cb --- /dev/null +++ b/server/sonar-web/src/main/js/api/application.js @@ -0,0 +1,31 @@ +/* + * 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); diff --git a/server/sonar-web/src/main/js/api/quality-gates.js b/server/sonar-web/src/main/js/api/quality-gates.js index 7c60f3a2a24..878c2e32dca 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.js +++ b/server/sonar-web/src/main/js/api/quality-gates.js @@ -105,3 +105,7 @@ export function dissociateGateWithProject(gateId, projectKey) { const data = { gateId, projectKey }; return post(url, data); } + +export function getApplicationQualityGate(application) { + return getJSON('/api/qualitygates/application_status', { application }); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js index d9963b913ac..61738902ee9 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js @@ -56,7 +56,7 @@ export default class ComponentNav extends React.PureComponent { 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, diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js index e7f2ca6d463..5e3038d65f5 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js @@ -57,6 +57,10 @@ export default class ComponentNavMenu extends React.PureComponent { return qualifier === 'VW' || qualifier === 'SVW'; } + isApplication() { + return this.props.component.qualifier === 'APP'; + } + renderDashboardLink() { const pathname = this.isView() ? '/portfolio' : '/dashboard'; return ( @@ -78,14 +82,16 @@ export default class ComponentNavMenu extends React.PureComponent { <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; } @@ -167,7 +173,7 @@ export default class ComponentNavMenu extends React.PureComponent { } renderSettingsLink() { - if (!this.props.conf.showSettings) { + if (!this.props.conf.showSettings || this.isApplication()) { return null; } return ( @@ -293,7 +299,7 @@ export default class ComponentNavMenu extends React.PureComponent { return null; } - if (qualifier !== 'TRK' && qualifier !== 'VW') { + if (qualifier !== 'TRK' && qualifier !== 'VW' && qualifier !== 'APP') { return null; } diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResult.js b/server/sonar-web/src/main/js/app/components/search/SearchResult.js index 7f6337dcda0..0488535cba6 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchResult.js +++ b/server/sonar-web/src/main/js/app/components/search/SearchResult.js @@ -87,7 +87,10 @@ export default class SearchResult extends React.PureComponent { 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; } diff --git a/server/sonar-web/src/main/js/app/components/search/utils.js b/server/sonar-web/src/main/js/app/components/search/utils.js index 5ed66863da2..ebeea69e645 100644 --- a/server/sonar-web/src/main/js/app/components/search/utils.js +++ b/server/sonar-web/src/main/js/app/components/search/utils.js @@ -20,7 +20,7 @@ // @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)); diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js index a85387a0e5e..3a0f994aa1c 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.js +++ b/server/sonar-web/src/main/js/apps/code/components/App.js @@ -74,8 +74,8 @@ class App extends React.PureComponent { 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(); @@ -91,9 +91,8 @@ class App extends React.PureComponent { 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)) { @@ -135,9 +134,8 @@ class App extends React.PureComponent { 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({ diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.js index 6770e4c28a6..e36ad13bcf6 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.js +++ b/server/sonar-web/src/main/js/apps/code/components/Component.js @@ -61,7 +61,8 @@ export default class Component extends React.PureComponent { 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; @@ -76,7 +77,7 @@ export default class Component extends React.PureComponent { } } - const columns = isView + const columns = isPortfolio ? [ { metric: 'releasability_rating', type: 'RATING' }, { metric: 'reliability_rating', type: 'RATING' }, @@ -85,13 +86,14 @@ export default class Component extends React.PureComponent { { 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 })}> diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js index 1552dfad89d..a2977ae960b 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js @@ -21,9 +21,10 @@ import React from 'react'; 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'), @@ -32,13 +33,14 @@ const ComponentsHeader = ({ baseComponent, rootComponent }) => { 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> diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.js index 12b644d943c..a34d87e3c9e 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.js +++ b/server/sonar-web/src/main/js/apps/code/components/Search.js @@ -132,8 +132,8 @@ export default class Search extends React.PureComponent { 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 => { diff --git a/server/sonar-web/src/main/js/apps/code/utils.js b/server/sonar-web/src/main/js/apps/code/utils.js index 44d539a2122..2a1f5552cc0 100644 --- a/server/sonar-web/src/main/js/apps/code/utils.js +++ b/server/sonar-web/src/main/js/apps/code/utils.js @@ -39,7 +39,7 @@ const METRICS = [ 'alert_status' ]; -const VIEW_METRICS = [ +const PORTFOLIO_METRICS = [ 'releasability_rating', 'alert_status', 'reliability_rating', @@ -111,22 +111,22 @@ function storeChildrenBreadcrumbs(parentComponentKey, children) { } } -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); @@ -136,10 +136,10 @@ function retrieveComponentBase(componentKey, isView) { /** * @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({ @@ -149,7 +149,7 @@ export function retrieveComponentChildren(componentKey, isView) { }); } - const metrics = getMetrics(isView); + const metrics = getMetrics(isPortfolio); return getChildren(componentKey, metrics, { ps: PAGE_SIZE, s: 'qualifier,name' }) .then(prepareChildren) @@ -176,13 +176,13 @@ function retrieveComponentBreadcrumbs(componentKey) { /** * @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 { @@ -195,8 +195,8 @@ export function retrieveComponent(componentKey, isView) { }); } -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) diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js index 302c02959bf..22483f15b54 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js @@ -21,9 +21,17 @@ import React from 'react'; 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))} diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js index 392b7e19587..f2281c24eba 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js @@ -56,7 +56,7 @@ export default function MeasureDetailsHeader({ {isDiff && <div className="pull-right"> - <LeakPeriodLegend period={leakPeriod} /> + <LeakPeriodLegend component={component} period={leakPeriod} /> </div>} <TooltipsContainer options={{ html: false }}> diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/Home.js b/server/sonar-web/src/main/js/apps/component-measures/home/Home.js index fa3ddd96662..b81be299a02 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/home/Home.js +++ b/server/sonar-web/src/main/js/apps/component-measures/home/Home.js @@ -70,7 +70,7 @@ export default class Home extends React.PureComponent { </ul> </nav> - {leakPeriod != null && <LeakPeriodLegend period={leakPeriod} />} + {leakPeriod != null && <LeakPeriodLegend component={component} period={leakPeriod} />} </header> <main id="component-measures-home-main"> diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/actions.js b/server/sonar-web/src/main/js/apps/component-measures/home/actions.js index ed8e64d79a6..6ae4585b20b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/home/actions.js +++ b/server/sonar-web/src/main/js/apps/component-measures/home/actions.js @@ -19,7 +19,6 @@ */ 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'; @@ -30,10 +29,20 @@ export function receiveMeasures(measures, periods) { } 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() { @@ -50,7 +59,6 @@ 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); @@ -59,11 +67,16 @@ export function fetchMeasures() { }) .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()); }); }; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js index 8d4c9269e1e..62a1af46084 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js @@ -70,7 +70,7 @@ export default class ProjectFacet extends React.PureComponent { 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, diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js index 247a4d81704..e704e35cd8d 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.js +++ b/server/sonar-web/src/main/js/apps/overview/components/App.js @@ -19,7 +19,6 @@ */ // @flow import React from 'react'; -import { withRouter } from 'react-router'; import OverviewApp from './OverviewApp'; import EmptyOverview from './EmptyOverview'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; @@ -35,20 +34,32 @@ type Props = { 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)) { @@ -63,10 +74,6 @@ class App extends React.PureComponent { return <EmptyOverview component={component} />; } - return <OverviewApp {...this.props} leakPeriodIndex="1" />; + return <OverviewApp component={component} />; } } - -export default withRouter(App); - -export const UnconnectedApp = App; diff --git a/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js new file mode 100644 index 00000000000..3b43a02e39f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js @@ -0,0 +1,93 @@ +/* + * 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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js index 79ab68e793c..aea20cf3d40 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -22,6 +22,7 @@ import React from 'react'; 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'; @@ -122,6 +123,9 @@ export default class OverviewApp extends React.PureComponent { }, throwGlobalError); } + getApplicationLeakPeriod = () => + this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null; + renderLoading() { return ( <div className="text-center"> @@ -138,14 +142,17 @@ export default class OverviewApp extends React.PureComponent { 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} /> diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js index 70dd35ee95d..a39a89c9c99 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js @@ -19,24 +19,18 @@ */ 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'); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.js b/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.js new file mode 100644 index 00000000000..ce900ba1fb7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.js @@ -0,0 +1,35 @@ +/* + * 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(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap new file mode 100644 index 00000000000..217405a6565 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap @@ -0,0 +1,54 @@ +// 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> +`; diff --git a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js index f427b1a9a44..ec90cafe30f 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js +++ b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js @@ -21,6 +21,7 @@ import React from 'react'; 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'; @@ -54,7 +55,7 @@ class BugsAndVulnerabilities extends React.PureComponent { } renderLeak() { - const { leakPeriod } = this.props; + const { component, leakPeriod } = this.props; if (leakPeriod == null) { return null; @@ -62,7 +63,9 @@ class BugsAndVulnerabilities extends React.PureComponent { 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"> diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js index 7c465b46c27..b5599d43c00 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js @@ -118,6 +118,7 @@ export default function enhance(ComposedComponent) { </div> ); }; + renderRating = metricKey => { const { component, measures } = this.props; const measure = measures.find(measure => measure.metric.key === metricKey); @@ -139,6 +140,7 @@ export default function enhance(ComposedComponent) { </Tooltip> ); }; + renderIssues = (metric, type) => { const { measures, component } = this.props; const measure = measures.find(measure => measure.metric.key === metric); @@ -160,6 +162,7 @@ export default function enhance(ComposedComponent) { </Tooltip> ); }; + renderHistoryLink = metricKey => { const linkClass = 'button button-small button-compact spacer-left overview-domain-measure-history-link'; @@ -171,6 +174,7 @@ export default function enhance(ComposedComponent) { </Link> ); }; + renderTimeline = (metricKey, range, children) => { if (!this.props.history) { return null; @@ -190,6 +194,7 @@ export default function enhance(ComposedComponent) { </div> ); }; + render() { return ( <ComposedComponent diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index 4d84a3f2e23..689db65fbed 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -34,15 +34,14 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route 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 ( @@ -56,7 +55,8 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route {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 @@ -71,7 +71,7 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route profiles={qualityProfiles} />} - <MetaLinks component={component} /> + {isProject && <MetaLinks component={component} />} <MetaKey component={component} /> diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js index 6c632128757..fe153ca52fd 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js @@ -19,11 +19,13 @@ */ 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 = { @@ -31,32 +33,65 @@ export default class MetaSize extends React.PureComponent { 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> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js new file mode 100644 index 00000000000..80229f4cc22 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js @@ -0,0 +1,108 @@ +/* + * 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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.css b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.css new file mode 100644 index 00000000000..b42aa641aff --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.css @@ -0,0 +1,15 @@ +.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; +} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js new file mode 100644 index 00000000000..1c96978d4ae --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js @@ -0,0 +1,103 @@ +/* + * 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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js new file mode 100644 index 00000000000..ca434aebb90 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js @@ -0,0 +1,39 @@ +/* + * 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(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js new file mode 100644 index 00000000000..9aad7367621 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js @@ -0,0 +1,73 @@ +/* + * 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(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap new file mode 100644 index 00000000000..247c1251986 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap @@ -0,0 +1,62 @@ +// 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> +`; diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap new file mode 100644 index 00000000000..c887cc5062e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap @@ -0,0 +1,84 @@ +// 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> +`; diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index 8744bb255b3..43437c10623 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -339,6 +339,11 @@ 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; diff --git a/server/sonar-web/src/main/js/apps/overview/utils.js b/server/sonar-web/src/main/js/apps/overview/utils.js index 2ea31331be9..3c3f1c5a439 100644 --- a/server/sonar-web/src/main/js/apps/overview/utils.js +++ b/server/sonar-web/src/main/js/apps/overview/utils.js @@ -58,6 +58,7 @@ export const METRICS = [ // size 'ncloc', 'ncloc_language_distribution', + 'projects', 'new_lines' ]; diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js b/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js index d324dd36255..6f8f2e18aa9 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js @@ -23,7 +23,8 @@ import { getAppState } from '../../../store/rootReducer'; 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); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js index d06a080b3e9..13d1290f721 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js @@ -349,20 +349,22 @@ export default class App extends React.PureComponent { /> <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} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js index 8b968108a19..2cbe207fabb 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js @@ -55,7 +55,7 @@ export default class PageHeader extends React.PureComponent { 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'); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/constants.js b/server/sonar-web/src/main/js/apps/permissions/project/constants.js index 7a92d73237d..cc5ed8a4d30 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/constants.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/constants.js @@ -27,5 +27,6 @@ export const PERMISSIONS_ORDER_BY_QUALIFIER = { 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 }; diff --git a/server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js b/server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js index d236654ed98..8269e2ac6af 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js +++ b/server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js @@ -22,9 +22,12 @@ import React from 'react'; 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"> diff --git a/server/sonar-web/src/main/js/apps/projects-admin/constants.js b/server/sonar-web/src/main/js/apps/projects-admin/constants.js index 6f0f0323b4f..057d08d9109 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/constants.js +++ b/server/sonar-web/src/main/js/apps/projects-admin/constants.js @@ -19,7 +19,7 @@ */ 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', |