diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-01-18 09:10:50 +0100 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-01-25 15:16:50 +0100 |
commit | 57e81b11a8e94d3d21a24ff8b14aea368582b7f0 (patch) | |
tree | e6a00045d4251bdf1e7f32299475d232654c3aec /server/sonar-web | |
parent | df96a05cc325b2946c25ee8277f64638ed72288c (diff) | |
download | sonarqube-57e81b11a8e94d3d21a24ff8b14aea368582b7f0.tar.gz sonarqube-57e81b11a8e94d3d21a24ff8b14aea368582b7f0.zip |
Fetch metrics with redux on project dashboard pages
Diffstat (limited to 'server/sonar-web')
20 files changed, 202 insertions, 185 deletions
diff --git a/server/sonar-web/src/main/js/app/components/App.tsx b/server/sonar-web/src/main/js/app/components/App.tsx index 2b83e2a6468..0b7375c9c93 100644 --- a/server/sonar-web/src/main/js/app/components/App.tsx +++ b/server/sonar-web/src/main/js/app/components/App.tsx @@ -38,6 +38,7 @@ interface State { canAdmin: boolean; loading: boolean; onSonarCloud: boolean; + organizationsEnabled: boolean; } class App extends React.PureComponent<Props, State> { @@ -46,19 +47,27 @@ class App extends React.PureComponent<Props, State> { static childContextTypes = { branchesEnabled: PropTypes.bool.isRequired, canAdmin: PropTypes.bool.isRequired, - onSonarCloud: PropTypes.bool + onSonarCloud: PropTypes.bool, + organizationsEnabled: PropTypes.bool }; constructor(props: Props) { super(props); - this.state = { branchesEnabled: false, canAdmin: false, loading: true, onSonarCloud: false }; + this.state = { + branchesEnabled: false, + canAdmin: false, + loading: true, + onSonarCloud: false, + organizationsEnabled: false + }; } getChildContext() { return { branchesEnabled: this.state.branchesEnabled, canAdmin: this.state.canAdmin, - onSonarCloud: this.state.onSonarCloud + onSonarCloud: this.state.onSonarCloud, + organizationsEnabled: this.state.organizationsEnabled }; } @@ -93,7 +102,8 @@ class App extends React.PureComponent<Props, State> { canAdmin: appState.canAdmin, onSonarCloud: Boolean( appState.settings && appState.settings['sonar.sonarcloud.enabled'] === 'true' - ) + ), + organizationsEnabled: appState.organizationsEnabled }); } return appState; diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index de3102a3957..75d48a09dff 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; import ComponentContainerNotFound from './ComponentContainerNotFound'; import ComponentNav from './nav/component/ComponentNav'; @@ -28,7 +29,6 @@ import { Task, getTasksForComponent } from '../../api/ce'; import { getComponentData } from '../../api/components'; import { getComponentNavigation } from '../../api/nav'; import { fetchOrganizations } from '../../store/rootActions'; -import { areThereCustomOrganizations } from '../../store/rootReducer'; import { STATUSES } from '../../apps/background-tasks/constants'; interface Props { @@ -37,7 +37,6 @@ interface Props { location: { query: { branch?: string; id: string }; }; - organizationsEnabled?: boolean; } interface State { @@ -52,6 +51,10 @@ interface State { export class ComponentContainer extends React.PureComponent<Props, State> { mounted: boolean; + static contextTypes = { + organizationsEnabled: PropTypes.bool + }; + constructor(props: Props) { super(props); this.state = { branches: [], loading: true }; @@ -98,7 +101,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { ([nav, data]) => { const component = this.addQualifier({ ...nav, ...data }); - if (this.props.organizationsEnabled) { + if (this.context.organizationsEnabled) { this.props.fetchOrganizations([component.organization]); } @@ -197,10 +200,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> { } } -const mapStateToProps = (state: any) => ({ - organizationsEnabled: areThereCustomOrganizations(state) -}); - const mapDispatchToProps = { fetchOrganizations }; -export default connect<any, any, any>(mapStateToProps, mapDispatchToProps)(ComponentContainer); +export default connect<any, any, any>(null, mapDispatchToProps)(ComponentContainer); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index bb6c69b6098..b66a2aff8da 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -132,12 +132,10 @@ it('loads organization', async () => { const fetchOrganizations = jest.fn(); mount( - <ComponentContainer - fetchOrganizations={fetchOrganizations} - location={{ query: { id: 'foo' } }} - organizationsEnabled={true}> + <ComponentContainer fetchOrganizations={fetchOrganizations} location={{ query: { id: 'foo' } }}> <Inner /> - </ComponentContainer> + </ComponentContainer>, + { context: { organizationsEnabled: true } } ); await new Promise(setImmediate); @@ -150,12 +148,10 @@ it('fetches status', async () => { ); mount( - <ComponentContainer - fetchOrganizations={jest.fn()} - location={{ query: { id: 'foo' } }} - organizationsEnabled={true}> + <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}> <Inner /> - </ComponentContainer> + </ComponentContainer>, + { context: { organizationsEnabled: true } } ); await new Promise(setImmediate); diff --git a/server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx b/server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx index e0b1ca1d754..fc9a9db4554 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx +++ b/server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx @@ -18,43 +18,30 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; import Select from '../../../components/controls/Select'; import { fetchWebApi } from '../../../api/web-api'; -import { Metric } from '../../../app/types'; import { BadgeColors, BadgeType, BadgeOptions } from './utils'; import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; -import { fetchMetrics } from '../../../store/rootActions'; -import { getMetrics } from '../../../store/rootReducer'; - -interface StateToProps { - metrics: { [key: string]: Metric }; -} - -interface DispatchToProps { - fetchMetrics: () => void; -} +import { Metric } from '../../../app/types'; -interface OwnProps { +interface Props { className?: string; + metrics: { [key: string]: Metric }; options: BadgeOptions; type: BadgeType; updateOptions: (options: Partial<BadgeOptions>) => void; } -type Props = StateToProps & DispatchToProps & OwnProps; - interface State { badgeMetrics: string[]; } -export class BadgeParams extends React.PureComponent<Props> { +export default class BadgeParams extends React.PureComponent<Props> { mounted: boolean; state: State = { badgeMetrics: [] }; componentDidMount() { this.mounted = true; - this.props.fetchMetrics(); this.fetchBadgeMetrics(); } @@ -84,16 +71,14 @@ export class BadgeParams extends React.PureComponent<Props> { value: color })); - getMetricOptions = () => { - const { metrics } = this.props; - return this.state.badgeMetrics.map(key => { - const metric = metrics[key]; + getMetricOptions = () => + this.state.badgeMetrics.map(key => { + const metric = this.props.metrics[key]; return { value: key, - label: metric && getLocalizedMetricName(metric) + label: metric ? getLocalizedMetricName(metric) : key }; }); - }; handleColorChange = ({ value }: { value: BadgeColors }) => this.props.updateOptions({ color: value }); @@ -143,14 +128,3 @@ export class BadgeParams extends React.PureComponent<Props> { } } } - -const mapDispatchToProps: DispatchToProps = { fetchMetrics }; - -const mapStateToProps = (state: any): StateToProps => ({ - metrics: getMetrics(state) -}); - -export default connect<StateToProps, DispatchToProps, OwnProps>( - mapStateToProps, - mapDispatchToProps -)(BadgeParams); diff --git a/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx b/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx index 7e4e0daf249..ae664043b7c 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx +++ b/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx @@ -22,12 +22,14 @@ import Modal from '../../../components/controls/Modal'; import BadgeButton from './BadgeButton'; import BadgeSnippet from './BadgeSnippet'; import BadgeParams from './BadgeParams'; -import { getBadgeUrl, BadgeType, BadgeOptions } from './utils'; +import { BadgeType, BadgeOptions, getBadgeUrl } from './utils'; +import { Metric } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; import './styles.css'; interface Props { branch?: string; + metrics: { [key: string]: Metric }; project: string; } @@ -90,6 +92,7 @@ export default class BadgesModal extends React.PureComponent<Props, State> { </p> <BadgeParams className="big-spacer-bottom" + metrics={this.props.metrics} options={badgeOptions} type={selectedType} updateOptions={this.handleUpdateOptions} diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx index 5108e572be4..5e8936739b3 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import { BadgeParams } from '../BadgeParams'; +import BadgeParams from '../BadgeParams'; import { BadgeType } from '../utils'; import { Metric } from '../../../../app/types'; @@ -62,7 +62,6 @@ it('should display measure badge params', () => { function getWrapper(props = {}) { return shallow( <BadgeParams - fetchMetrics={jest.fn()} metrics={METRICS} options={{ color: 'white', metric: 'alert_status' }} type={BadgeType.marketing} diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx index 3eee7bf7074..fc403fc3f17 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx @@ -27,7 +27,7 @@ jest.mock('../../../../helpers/urls', () => ({ })); it('should display the modal after click', () => { - const wrapper = shallow(<BadgesModal branch="branch-6.6" project="foo" />); + const wrapper = shallow(<BadgesModal branch="branch-6.6" metrics={{}} project="foo" />); expect(wrapper).toMatchSnapshot(); click(wrapper.find('button')); expect(wrapper.find('Modal')).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap index 00e92664a30..f43e19a5715 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap @@ -61,8 +61,9 @@ exports[`should display the modal after click 2`] = ` > overview.badges.measure.description </p> - <Connect(BadgeParams) + <BadgeParams className="big-spacer-bottom" + metrics={Object {}} options={ Object { "color": "white", diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx index 07d22017edf..44285fe03da 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { uniq } from 'lodash'; +import { connect } from 'react-redux'; import QualityGate from '../qualityGate/QualityGate'; import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate'; import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities'; @@ -36,15 +37,27 @@ import { getCustomGraph, getGraph } from '../../../helpers/storage'; import { METRICS, HISTORY_METRICS_LIST } from '../utils'; import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils'; import { getBranchName } from '../../../helpers/branches'; -import { Branch, Component } from '../../../app/types'; +import { fetchMetrics } from '../../../store/rootActions'; +import { getMetrics } from '../../../store/rootReducer'; +import { Branch, Component, Metric } from '../../../app/types'; import '../styles.css'; -interface Props { +interface OwnProps { branch?: Branch; component: Component; onComponentChange: (changes: {}) => void; } +interface StateToProps { + metrics: { [key: string]: Metric }; +} + +interface DispatchToProps { + fetchMetrics: () => void; +} + +type Props = StateToProps & DispatchToProps & OwnProps; + interface State { history?: History; historyStartDate?: Date; @@ -53,12 +66,13 @@ interface State { periods?: Period[]; } -export default class OverviewApp extends React.PureComponent<Props, State> { +export class OverviewApp extends React.PureComponent<Props, State> { mounted: boolean; state: State = { loading: true, measures: [] }; componentDidMount() { this.mounted = true; + this.props.fetchMetrics(); this.loadMeasures().then(this.loadHistory, () => {}); } @@ -183,6 +197,7 @@ export default class OverviewApp extends React.PureComponent<Props, State> { component={component} history={history} measures={measures} + metrics={this.props.metrics} onComponentChange={this.props.onComponentChange} /> </div> @@ -191,3 +206,14 @@ export default class OverviewApp extends React.PureComponent<Props, State> { ); } } + +const mapDispatchToProps: DispatchToProps = { fetchMetrics }; + +const mapStateToProps = (state: any): StateToProps => ({ + metrics: getMetrics(state) +}); + +export default connect<StateToProps, DispatchToProps, OwnProps>( + mapStateToProps, + mapDispatchToProps +)(OverviewApp); diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx index 30a3d98e7b1..ac3f80bb2a8 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx +++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx @@ -20,7 +20,6 @@ import * as React from 'react'; import { Link } from 'react-router'; import Analysis from './Analysis'; -import { getAllMetrics } from '../../../api/metrics'; import { getProjectActivity, Analysis as IAnalysis } from '../../../api/projectActivity'; import PreviewGraph from '../../../components/preview-graph/PreviewGraph'; import { translate } from '../../../helpers/l10n'; @@ -31,20 +30,20 @@ interface Props { branch?: string; component: Component; history?: History; + metrics: { [key: string]: Metric }; qualifier: string; } interface State { analyses: IAnalysis[]; loading: boolean; - metrics: Metric[]; } const PAGE_SIZE = 3; export default class AnalysesList extends React.PureComponent<Props, State> { mounted: boolean; - state: State = { analyses: [], loading: true, metrics: [] }; + state: State = { analyses: [], loading: true }; componentDidMount() { this.mounted = true; @@ -75,17 +74,15 @@ export default class AnalysesList extends React.PureComponent<Props, State> { fetchData = () => { this.setState({ loading: true }); - Promise.all([ - getProjectActivity({ - branch: this.props.branch, - project: this.getTopLevelComponent(), - ps: PAGE_SIZE - }), - getAllMetrics() - ]).then( - ([{ analyses }, metrics]) => { + + getProjectActivity({ + branch: this.props.branch, + project: this.getTopLevelComponent(), + ps: PAGE_SIZE + }).then( + ({ analyses }) => { if (this.mounted) { - this.setState({ analyses, metrics, loading: false }); + this.setState({ analyses, loading: false }); } }, () => { @@ -125,7 +122,7 @@ export default class AnalysesList extends React.PureComponent<Props, State> { branch={this.props.branch} history={this.props.history} project={this.props.component.key} - metrics={this.state.metrics} + metrics={this.props.metrics} /> {this.renderList(analyses)} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx index bfffecd1f7c..b84da6cb1f8 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; +import * as PropTypes from 'prop-types'; import MetaKey from './MetaKey'; import MetaOrganizationKey from './MetaOrganizationKey'; import MetaLinks from './MetaLinks'; @@ -28,90 +28,86 @@ import AnalysesList from '../events/AnalysesList'; import MetaSize from './MetaSize'; import MetaTags from './MetaTags'; import BadgesModal from '../badges/BadgesModal'; -import { areThereCustomOrganizations, getGlobalSettingValue } from '../../../store/rootReducer'; -import { Visibility, Component } from '../../../app/types'; +import { Visibility, Component, Metric } from '../../../app/types'; import { History } from '../../../api/time-machine'; import { MeasureEnhanced } from '../../../helpers/measures'; -interface OwnProps { +interface Props { branch?: string; component: Component; history?: History; measures: MeasureEnhanced[]; + metrics: { [key: string]: Metric }; onComponentChange: (changes: {}) => void; } -interface StateToProps { - areThereCustomOrganizations: boolean; - onSonarCloud: boolean; -} - -export function Meta(props: OwnProps & StateToProps) { - const { branch, component, areThereCustomOrganizations } = props; - const { qualifier, description, qualityProfiles, qualityGate, visibility } = component; - - const isProject = qualifier === 'TRK'; - const isPrivate = visibility === Visibility.Private; +export default class Meta extends React.PureComponent<Props> { + static contextTypes = { + onSonarCloud: PropTypes.bool, + organizationsEnabled: PropTypes.bool + }; - const hasDescription = !!description; - const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0; - const hasQualityGate = !!qualityGate; + render() { + const { onSonarCloud, organizationsEnabled } = this.context; + const { branch, component, metrics } = this.props; + const { qualifier, description, qualityProfiles, qualityGate, visibility } = component; - const shouldShowQualityProfiles = isProject && hasQualityProfiles; - const shouldShowQualityGate = isProject && hasQualityGate; - const hasOrganization = component.organization != null && areThereCustomOrganizations; + const isProject = qualifier === 'TRK'; + const isPrivate = visibility === Visibility.Private; - return ( - <div className="overview-meta"> - {hasDescription && ( - <div className="overview-meta-card overview-meta-description">{description}</div> - )} + const hasDescription = !!description; + const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0; + const hasQualityGate = !!qualityGate; - <MetaSize branch={branch} component={component} measures={props.measures} /> + const shouldShowQualityProfiles = isProject && hasQualityProfiles; + const shouldShowQualityGate = isProject && hasQualityGate; + const hasOrganization = component.organization != null && organizationsEnabled; - {isProject && <MetaTags component={component} onComponentChange={props.onComponentChange} />} + return ( + <div className="overview-meta"> + {hasDescription && ( + <div className="overview-meta-card overview-meta-description">{description}</div> + )} - <AnalysesList - branch={branch} - component={component} - qualifier={component.qualifier} - history={props.history} - /> + <MetaSize branch={branch} component={component} measures={this.props.measures} /> - {shouldShowQualityGate && ( - <MetaQualityGate - gate={qualityGate} - organization={hasOrganization && component.organization} - /> - )} + {isProject && ( + <MetaTags component={component} onComponentChange={this.props.onComponentChange} /> + )} - {shouldShowQualityProfiles && ( - <MetaQualityProfiles + <AnalysesList + branch={branch} component={component} - customOrganizations={areThereCustomOrganizations} - profiles={qualityProfiles} + history={this.props.history} + metrics={metrics} + qualifier={component.qualifier} /> - )} - - {isProject && <MetaLinks component={component} />} - <MetaKey component={component} /> - - {hasOrganization && <MetaOrganizationKey component={component} />} - - {props.onSonarCloud && - isProject && - !isPrivate && <BadgesModal branch={branch} project={component.key} />} - </div> - ); + {shouldShowQualityGate && ( + <MetaQualityGate + gate={qualityGate} + organization={hasOrganization && component.organization} + /> + )} + + {shouldShowQualityProfiles && ( + <MetaQualityProfiles + component={component} + customOrganizations={organizationsEnabled} + profiles={qualityProfiles} + /> + )} + + {isProject && <MetaLinks component={component} />} + + <MetaKey component={component} /> + + {hasOrganization && <MetaOrganizationKey component={component} />} + + {onSonarCloud && + isProject && + !isPrivate && <BadgesModal branch={branch} metrics={metrics} project={component.key} />} + </div> + ); + } } - -const mapStateToProps = (state: any): StateToProps => { - const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled'); - return { - areThereCustomOrganizations: areThereCustomOrganizations(state), - onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true') - }; -}; - -export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(Meta); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx index e433633238e..b90026b8524 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx @@ -20,8 +20,7 @@ import * as React from 'react'; import { getDisplayedHistoryMetrics, DEFAULT_GRAPH } from '../../projectActivity/utils'; import PreviewGraph from '../../../components/preview-graph/PreviewGraph'; -import { getAllMetrics } from '../../../api/metrics'; -import { getAllTimeMachineData } from '../../../api/time-machine'; +import { getAllTimeMachineData, History } from '../../../api/time-machine'; import { Metric } from '../../../app/types'; import { parseDate } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; @@ -29,18 +28,14 @@ import { getCustomGraph, getGraph } from '../../../helpers/storage'; const AnyPreviewGraph = PreviewGraph as any; -interface History { - [metric: string]: Array<{ date: Date; value: string }>; -} - interface Props { component: string; + metrics: { [key: string]: Metric }; } interface State { history?: History; loading: boolean; - metrics?: Metric[]; } export default class Activity extends React.PureComponent<Props> { @@ -71,8 +66,8 @@ export default class Activity extends React.PureComponent<Props> { } this.setState({ loading: true }); - return Promise.all([getAllTimeMachineData(component, graphMetrics), getAllMetrics()]).then( - ([timeMachine, metrics]) => { + return getAllTimeMachineData(component, graphMetrics).then( + timeMachine => { if (this.mounted) { const history: History = {}; timeMachine.measures.forEach(measure => { @@ -82,7 +77,7 @@ export default class Activity extends React.PureComponent<Props> { })); history[measure.metric] = measureHistory; }); - this.setState({ history, loading: false, metrics }); + this.setState({ history, loading: false }); } }, () => { @@ -103,11 +98,10 @@ export default class Activity extends React.PureComponent<Props> { {this.state.loading ? ( <i className="spinner" /> ) : ( - this.state.metrics !== undefined && this.state.history !== undefined && ( <AnyPreviewGraph history={this.state.history} - metrics={this.state.metrics} + metrics={this.props.metrics} project={this.props.component} renderWhenEmpty={this.renderWhenEmpty} /> diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx index f5450166b5d..84ba6bbbf0a 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { connect } from 'react-redux'; import Summary from './Summary'; import Report from './Report'; import WorstProjects from './WorstProjects'; @@ -31,12 +32,25 @@ import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../u import { getMeasures } from '../../../api/measures'; import { getChildren } from '../../../api/components'; import { translate } from '../../../helpers/l10n'; +import { fetchMetrics } from '../../../store/rootActions'; +import { getMetrics } from '../../../store/rootReducer'; +import { Metric } from '../../../app/types'; import '../styles.css'; -interface Props { +interface OwnProps { component: { key: string; name: string }; } +interface StateToProps { + metrics: { [key: string]: Metric }; +} + +interface DispatchToProps { + fetchMetrics: () => void; +} + +type Props = StateToProps & DispatchToProps & OwnProps; + interface State { loading: boolean; measures?: { [key: string]: string | undefined }; @@ -44,12 +58,13 @@ interface State { totalSubComponents?: number; } -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted: boolean; state: State = { loading: true }; componentDidMount() { this.mounted = true; + this.props.fetchMetrics(); this.fetchData(); } @@ -171,7 +186,7 @@ export default class App extends React.PureComponent<Props, State> { <aside className="page-sidebar-fixed"> {!this.isEmpty() && !this.isNotComputed() && <Summary component={component} measures={measures!} />} - <Activity component={component.key} /> + <Activity component={component.key} metrics={this.props.metrics} /> <Report component={component} /> </aside> </div> @@ -179,3 +194,13 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +const mapDispatchToProps: DispatchToProps = { fetchMetrics }; + +const mapStateToProps = (state: any): StateToProps => ({ + metrics: getMetrics(state) +}); + +export default connect<StateToProps, DispatchToProps, Props>(mapStateToProps, mapDispatchToProps)( + App +); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx index 8a9462dd697..c8c2e81f295 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx @@ -23,10 +23,6 @@ jest.mock('../../../../helpers/storage', () => ({ getGraph: () => 'custom' })); -jest.mock('../../../../api/metrics', () => ({ - getAllMetrics: jest.fn(() => Promise.resolve([])) -})); - jest.mock('../../../../api/time-machine', () => ({ getAllTimeMachineData: jest.fn(() => Promise.resolve({ @@ -47,17 +43,15 @@ import * as React from 'react'; import { mount, shallow } from 'enzyme'; import Activity from '../Activity'; -const getAllMetrics = require('../../../../api/metrics').getAllMetrics as jest.Mock<any>; const getAllTimeMachineData = require('../../../../api/time-machine') .getAllTimeMachineData as jest.Mock<any>; beforeEach(() => { - getAllMetrics.mockClear(); getAllTimeMachineData.mockClear(); }); it('renders', () => { - const wrapper = shallow(<Activity component="foo" />); + const wrapper = shallow(<Activity component="foo" metrics={{}} />); wrapper.setState({ history: { coverage: [ @@ -72,7 +66,6 @@ it('renders', () => { }); it('fetches history', () => { - mount(<Activity component="foo" />); - expect(getAllMetrics).toBeCalled(); + mount(<Activity component="foo" metrics={{}} />); expect(getAllTimeMachineData).toBeCalledWith('foo', ['coverage']); }); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx index dc474149e9f..6cfe8d6890b 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx @@ -43,7 +43,7 @@ jest.mock('../Report', () => ({ import * as React from 'react'; import { shallow, mount } from 'enzyme'; -import App from '../App'; +import { App } from '../App'; const getMeasures = require('../../../../api/measures').getMeasures as jest.Mock<any>; const getChildren = require('../../../../api/components').getChildren as jest.Mock<any>; @@ -51,7 +51,7 @@ const getChildren = require('../../../../api/components').getChildren as jest.Mo const component = { key: 'foo', name: 'Foo' }; it('renders', () => { - const wrapper = shallow(<App component={component} />); + const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />); wrapper.setState({ loading: false, measures: { ncloc: '173', reliability_rating: '1' }, @@ -62,13 +62,13 @@ it('renders', () => { }); it('renders when portfolio is empty', () => { - const wrapper = shallow(<App component={component} />); + const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />); wrapper.setState({ loading: false, measures: { reliability_rating: '1' } }); expect(wrapper).toMatchSnapshot(); }); it('renders when portfolio is not computed', () => { - const wrapper = shallow(<App component={component} />); + const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />); wrapper.setState({ loading: false, measures: { ncloc: '173' } }); expect(wrapper).toMatchSnapshot(); }); @@ -76,7 +76,7 @@ it('renders when portfolio is not computed', () => { it('fetches measures and children components', () => { getMeasures.mockClear(); getChildren.mockClear(); - mount(<App component={component} />); + mount(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />); expect(getMeasures).toBeCalledWith('foo', [ 'projects', 'ncloc', diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap index 2a0038daecb..d48d3089a5d 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap @@ -22,13 +22,7 @@ exports[`renders 1`] = ` ], } } - metrics={ - Array [ - Object { - "key": "coverage", - }, - ] - } + metrics={Object {}} project="foo" renderWhenEmpty={[Function]} /> diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap index 2e26c3cc408..76ccc5b4cb8 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap @@ -77,6 +77,7 @@ exports[`renders 1`] = ` /> <Activity component="foo" + metrics={Object {}} /> <Report component={ @@ -117,6 +118,7 @@ exports[`renders when portfolio is empty 1`] = ` > <Activity component="foo" + metrics={Object {}} /> <Report component={ @@ -154,6 +156,7 @@ exports[`renders when portfolio is not computed 1`] = ` > <Activity component="foo" + metrics={Object {}} /> <Report component={ diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js index 0718ef5d03d..5a8ae9aa5a1 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -98,10 +98,17 @@ export const generateCoveredLinesMetric = ( }; }; +function findMetric(key /*: string */, metrics /*: Array<Metric> | { [string]: Metric } */) { + if (Array.isArray(metrics)) { + return metrics.find(metric => metric.key === key); + } + return metrics[key]; +} + export function generateSeries( measuresHistory /*: Array<MeasureHistory> */, graph /*: string */, - metrics /*: Array<Metric> */, + metrics /*: Array<Metric> | { [string]: Metric } */, displayedMetrics /*: Array<string> */ ) /*: Array<Serie> */ { if (displayedMetrics.length <= 0) { @@ -114,7 +121,7 @@ export function generateSeries( if (measure.metric === 'uncovered_lines' && !isCustomGraph(graph)) { return generateCoveredLinesMetric(measure, measuresHistory); } - const metric = metrics.find(metric => metric.key === measure.metric); + const metric = findMetric(measure.metric, metrics); return { data: measure.history.map(analysis => ({ x: analysis.date, diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts index 744eed0493d..61de9d4f695 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts +++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts @@ -24,7 +24,7 @@ import { Metric } from '../../app/types'; interface Props { branch?: string; history?: History; - metrics: Metric[]; + metrics: { [key: string]: Metric }; project: string; renderWhenEmpty?: () => void; } diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js index 9483414c1a8..8a8ac8443c5 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js +++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js @@ -41,7 +41,7 @@ import { formatMeasure, getShortType } from '../../helpers/measures'; type Props = { branch?: string, history: ?History, - metrics: Array<Metric>, + metrics: { [string]: Metric }, project: string, renderWhenEmpty?: () => void }; @@ -121,7 +121,7 @@ export default class PreviewGraph extends React.PureComponent { history /*: ?History */, graph /*: string */, customMetrics /*: Array<string> */, - metrics /*: Array<Metric> */ + metrics /*: { [string]: Metric } */ ) => { const myHistory = history; if (!myHistory) { |