@@ -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; |
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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} |
@@ -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} |
@@ -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(); |
@@ -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", |
@@ -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); |
@@ -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)} |
@@ -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); |
@@ -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} | |||
/> |
@@ -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 | |||
); |
@@ -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']); | |||
}); |
@@ -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', |
@@ -22,13 +22,7 @@ exports[`renders 1`] = ` | |||
], | |||
} | |||
} | |||
metrics={ | |||
Array [ | |||
Object { | |||
"key": "coverage", | |||
}, | |||
] | |||
} | |||
metrics={Object {}} | |||
project="foo" | |||
renderWhenEmpty={[Function]} | |||
/> |
@@ -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={ |
@@ -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, |
@@ -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; | |||
} |
@@ -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) { |