@@ -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); |
@@ -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 }); | |||
} |
@@ -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, |
@@ -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; | |||
} | |||
@@ -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; | |||
} | |||
@@ -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)); |
@@ -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({ |
@@ -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 })}> |
@@ -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> |
@@ -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 => { |
@@ -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) |
@@ -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))} |
@@ -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 }}> |
@@ -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"> |
@@ -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()); | |||
}); | |||
}; |
@@ -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, |
@@ -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; |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} /> |
@@ -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'); | |||
}); |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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"> |
@@ -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 |
@@ -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} /> | |||
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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; |
@@ -58,6 +58,7 @@ export const METRICS = [ | |||
// size | |||
'ncloc', | |||
'ncloc_language_distribution', | |||
'projects', | |||
'new_lines' | |||
]; | |||
@@ -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); |
@@ -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} |
@@ -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'); | |||
@@ -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 | |||
}; |
@@ -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"> |
@@ -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', |
@@ -439,6 +439,7 @@ qualifiers.update.DEV=Update Developer | |||
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. | |||
#------------------------------------------------------------------------------ | |||
@@ -571,6 +572,7 @@ update_key.page.description=Edit the keys of a project and/or its modules. Key c | |||
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. | |||