diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps/overview')
19 files changed, 769 insertions, 46 deletions
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' ]; |