@@ -114,14 +114,14 @@ export function getComponentTree( | |||
metricKeys: metrics.join(','), | |||
strategy | |||
}); | |||
return getJSON(url, data); | |||
return getJSON(url, data).catch(throwGlobalError); | |||
} | |||
export function getChildren( | |||
componentKey: string, | |||
metrics: string[] = [], | |||
additional: RequestData = {} | |||
): Promise<any> { | |||
) { | |||
return getComponentTree('children', componentKey, metrics, additional); | |||
} | |||
@@ -129,14 +129,14 @@ export function getComponentLeaves( | |||
componentKey: string, | |||
metrics: string[] = [], | |||
additional: RequestData = {} | |||
): Promise<any> { | |||
) { | |||
return getComponentTree('leaves', componentKey, metrics, additional); | |||
} | |||
export function getComponent( | |||
data: { componentKey: string; metricKeys: string } & BranchParameters | |||
): Promise<any> { | |||
return getJSON('/api/measures/component', data).then(r => r.component); | |||
return getJSON('/api/measures/component', data).then(r => r.component, throwGlobalError); | |||
} | |||
export interface TreeComponent extends LightComponent { | |||
@@ -165,7 +165,7 @@ export function getTree(data: { | |||
} | |||
export function getComponentShow(data: { component: string } & BranchParameters): Promise<any> { | |||
return getJSON('/api/components/show', data); | |||
return getJSON('/api/components/show', data).catch(throwGlobalError); | |||
} | |||
export function getParents(component: string): Promise<any> { |
@@ -20,14 +20,14 @@ | |||
import { getJSON, RequestData, postJSON, post } from '../helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { | |||
Metric, | |||
CustomMeasure, | |||
Paging, | |||
BranchParameters, | |||
Measure, | |||
MeasurePeriod | |||
Metric, | |||
Paging, | |||
Period, | |||
PeriodMeasure | |||
} from '../app/types'; | |||
import { Period } from '../helpers/periods'; | |||
export function getMeasures( | |||
data: { componentKey: string; metricKeys: string } & BranchParameters | |||
@@ -55,7 +55,7 @@ export function getMeasuresAndMeta( | |||
interface MeasuresForProjects { | |||
component: string; | |||
metric: string; | |||
periods?: MeasurePeriod[]; | |||
periods?: PeriodMeasure[]; | |||
value?: string; | |||
} | |||
@@ -152,12 +152,6 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderComponentMeasuresLink() { | |||
const { branchLike } = this.props; | |||
if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) { | |||
return null; | |||
} | |||
return ( | |||
<li> | |||
<Link |
@@ -1097,6 +1097,24 @@ exports[`should work for short-living branches 1`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"branch": "feature", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.measures | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" |
@@ -275,7 +275,6 @@ | |||
} | |||
.modal-foot { | |||
line-height: var(--controlHeight); | |||
padding: 10px; | |||
border-top: 1px solid #ccc; | |||
background-color: var(--gray94); |
@@ -107,12 +107,14 @@ export interface ComponentQualityProfile { | |||
} | |||
interface ComponentMeasureIntern { | |||
branch?: string; | |||
isFavorite?: boolean; | |||
isRecentlyBrowsed?: boolean; | |||
key: string; | |||
match?: string; | |||
name: string; | |||
organization?: string; | |||
path?: string; | |||
project?: string; | |||
qualifier: string; | |||
refKey?: string; | |||
@@ -376,18 +378,6 @@ export interface MainBranch extends Branch { | |||
status?: { qualityGateStatus: string }; | |||
} | |||
export interface MeasurePeriod { | |||
bestValue?: boolean; | |||
index: number; | |||
value: string; | |||
} | |||
interface MeasureIntern { | |||
bestValue?: boolean; | |||
periods?: MeasurePeriod[]; | |||
value?: string; | |||
} | |||
export interface Measure extends MeasureIntern { | |||
metric: string; | |||
} | |||
@@ -397,6 +387,12 @@ export interface MeasureEnhanced extends MeasureIntern { | |||
leak?: string; | |||
} | |||
interface MeasureIntern { | |||
bestValue?: boolean; | |||
periods?: PeriodMeasure[]; | |||
value?: string; | |||
} | |||
export interface Metric { | |||
bestValue?: string; | |||
custom?: boolean; | |||
@@ -478,6 +474,28 @@ export interface Paging { | |||
total: number; | |||
} | |||
export interface Period { | |||
date: string; | |||
index: number; | |||
mode: PeriodMode; | |||
modeParam?: string; | |||
parameter?: string; | |||
} | |||
export interface PeriodMeasure { | |||
bestValue?: boolean; | |||
index: number; | |||
value: string; | |||
} | |||
export enum PeriodMode { | |||
Days = 'days', | |||
Date = 'date', | |||
Version = 'version', | |||
PreviousAnalysis = 'previous_analysis', | |||
PreviousVersion = 'previous_version' | |||
} | |||
export interface PermissionTemplate { | |||
defaultFor: string[]; | |||
id: string; |
@@ -17,15 +17,15 @@ | |||
* 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 * as React from 'react'; | |||
import * as key from 'keymaster'; | |||
import { InjectedRouter } from 'react-router'; | |||
import Helmet from 'react-helmet'; | |||
import key from 'keymaster'; | |||
import MeasureContentContainer from './MeasureContentContainer'; | |||
import MeasureOverviewContainer from './MeasureOverviewContainer'; | |||
import Sidebar from '../sidebar/Sidebar'; | |||
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; | |||
import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery } from '../utils'; | |||
import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery, Query } from '../utils'; | |||
import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; | |||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import { | |||
@@ -34,67 +34,64 @@ import { | |||
translate | |||
} from '../../../helpers/l10n'; | |||
import { getDisplayMetrics } from '../../../helpers/measures'; | |||
/*:: import type { Component, Query, Period } from '../types'; */ | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
/*:: import type { Metric } from '../../../app/flow-types'; */ | |||
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ | |||
import { RawQuery } from '../../../helpers/query'; | |||
import { | |||
BranchLike, | |||
ComponentMeasure, | |||
MeasureEnhanced, | |||
Metric, | |||
CurrentUser, | |||
Period | |||
} from '../../../app/types'; | |||
import '../../../components/search-navigator.css'; | |||
import '../style.css'; | |||
/*:: type Props = {| | |||
branchLike?: { id?: string; name: string }, | |||
component: Component, | |||
currentUser: { isLoggedIn: boolean }, | |||
location: { pathname: string, query: RawQuery }, | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: ComponentMeasure; | |||
currentUser: CurrentUser; | |||
location: { pathname: string; query: RawQuery }; | |||
fetchMeasures: ( | |||
component: string, | |||
metricsKey: Array<string>, | |||
branchLike?: { id?: string; name: string } | |||
) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }>, | |||
fetchMetrics: () => void, | |||
metrics: { [string]: Metric }, | |||
metricsKey: Array<string>, | |||
router: { | |||
push: ({ pathname: string, query?: RawQuery }) => void | |||
} | |||
|}; */ | |||
metricsKey: string[], | |||
branchLike?: BranchLike | |||
) => Promise<{ component: ComponentMeasure; measures: MeasureEnhanced[]; leakPeriod?: Period }>; | |||
fetchMetrics: () => void; | |||
metrics: { [metric: string]: Metric }; | |||
metricsKey: string[]; | |||
router: InjectedRouter; | |||
} | |||
/*:: type State = {| | |||
loading: boolean, | |||
measures: Array<MeasureEnhanced>, | |||
leakPeriod: ?Period | |||
|}; */ | |||
interface State { | |||
loading: boolean; | |||
measures: MeasureEnhanced[]; | |||
leakPeriod?: Period; | |||
} | |||
export default class App extends React.PureComponent { | |||
/*:: mounted: boolean; */ | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
export default class App extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
constructor(props /*: Props */) { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
loading: true, | |||
measures: [], | |||
leakPeriod: null | |||
}; | |||
this.state = { loading: true, measures: [] }; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
// $FlowFixMe | |||
document.body.classList.add('white-page'); | |||
// $FlowFixMe | |||
document.documentElement.classList.add('white-page'); | |||
this.props.fetchMetrics(); | |||
this.fetchMeasures(this.props); | |||
key.setScope('measures-files'); | |||
const footer = document.getElementById('footer'); | |||
if (footer) { | |||
footer.classList.add('page-footer-with-sidebar'); | |||
} | |||
key.setScope('measures-files'); | |||
this.props.fetchMetrics(); | |||
this.fetchMeasures(this.props); | |||
} | |||
componentWillReceiveProps(nextProps /*: Props */) { | |||
componentWillReceiveProps(nextProps: Props) { | |||
if ( | |||
!isSameBranchLike(nextProps.branchLike, this.props.branchLike) || | |||
nextProps.component.key !== this.props.component.key || | |||
@@ -106,27 +103,31 @@ export default class App extends React.PureComponent { | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
// $FlowFixMe | |||
document.body.classList.remove('white-page'); | |||
// $FlowFixMe | |||
document.documentElement.classList.remove('white-page'); | |||
key.deleteScope('measures-files'); | |||
const footer = document.getElementById('footer'); | |||
if (footer) { | |||
footer.classList.remove('page-footer-with-sidebar'); | |||
} | |||
key.deleteScope('measures-files'); | |||
} | |||
fetchMeasures = ({ branchLike, component, fetchMeasures, metrics } /*: Props */) => { | |||
fetchMeasures = ({ branchLike, component, fetchMeasures, metrics }: Props) => { | |||
this.setState({ loading: true }); | |||
const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key); | |||
fetchMeasures(component.key, filteredKeys, branchLike).then( | |||
({ measures, leakPeriod }) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
leakPeriod, | |||
measures: measures.filter(measure => measure.value != null || measure.leak != null) | |||
measures: measures.filter( | |||
measure => measure.value !== undefined || measure.leak !== undefined | |||
) | |||
}); | |||
} | |||
}, | |||
@@ -138,7 +139,7 @@ export default class App extends React.PureComponent { | |||
); | |||
}; | |||
updateQuery = (newQuery /*: Query */) => { | |||
updateQuery = (newQuery: Partial<Query>) => { | |||
const query = serializeQuery({ | |||
...parseQuery(this.props.location.query), | |||
...newQuery | |||
@@ -153,19 +154,16 @@ export default class App extends React.PureComponent { | |||
}); | |||
}; | |||
getHelmetTitle = ( | |||
metric /*: Metric */, | |||
query /*: {metric: string, selected: string, view: string }*/ | |||
) => { | |||
if (metric == null && hasBubbleChart(query.metric)) { | |||
return isProjectOverview(query.metric) | |||
getHelmetTitle = (metric?: Metric) => { | |||
if (metric && hasBubbleChart(metric.key)) { | |||
return isProjectOverview(metric.key) | |||
? translate('component_measures.overview.project_overview.facet') | |||
: translateWithParameters( | |||
'component_measures.domain_x_overview', | |||
getLocalizedMetricDomain(query.metric) | |||
getLocalizedMetricDomain(metric.key) | |||
); | |||
} | |||
return metric != null ? metric.name : translate('layout.measures'); | |||
return metric ? metric.name : translate('layout.measures'); | |||
}; | |||
render() { | |||
@@ -180,7 +178,7 @@ export default class App extends React.PureComponent { | |||
return ( | |||
<div className="layout-page" id="component-measures"> | |||
<Suggestions suggestions="component_measures" /> | |||
<Helmet title={this.getHelmetTitle(metric, query)} /> | |||
<Helmet title={this.getHelmetTitle(metric)} /> | |||
<ScreenPositionHelper className="layout-page-side-outer"> | |||
{({ top }) => ( | |||
@@ -198,7 +196,7 @@ export default class App extends React.PureComponent { | |||
)} | |||
</ScreenPositionHelper> | |||
{metric != null && ( | |||
{metric && ( | |||
<MeasureContentContainer | |||
branchLike={branchLike} | |||
className="layout-page-main" | |||
@@ -214,7 +212,7 @@ export default class App extends React.PureComponent { | |||
view={query.view} | |||
/> | |||
)} | |||
{metric == null && | |||
{!metric && | |||
hasBubbleChart(query.metric) && ( | |||
<MeasureOverviewContainer | |||
branchLike={branchLike} |
@@ -17,9 +17,9 @@ | |||
* 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 { Dispatch } from 'redux'; | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import { withRouter, WithRouterProps } from 'react-router'; | |||
import App from './App'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import { getCurrentUser, getMetrics, getMetricsKey } from '../../../store/rootReducer'; | |||
@@ -28,31 +28,57 @@ import { getMeasuresAndMeta } from '../../../api/measures'; | |||
import { getLeakPeriod } from '../../../helpers/periods'; | |||
import { enhanceMeasure } from '../../../components/measure/utils'; | |||
import { getBranchLikeQuery } from '../../../helpers/branches'; | |||
/*:: import type { Component, Period } from '../types'; */ | |||
/*:: import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; */ | |||
import { | |||
BranchLike, | |||
ComponentMeasure, | |||
CurrentUser, | |||
Measure, | |||
MeasureEnhanced, | |||
Metric, | |||
Period | |||
} from '../../../app/types'; | |||
const mapStateToProps = state => ({ | |||
interface StateToProps { | |||
currentUser: CurrentUser; | |||
metrics: { [metric: string]: Metric }; | |||
metricsKey: string[]; | |||
} | |||
interface DispatchToProps { | |||
fetchMeasures: ( | |||
component: string, | |||
metricsKey: string[], | |||
branchLike?: BranchLike | |||
) => Promise<{ component: ComponentMeasure; measures: MeasureEnhanced[]; leakPeriod?: Period }>; | |||
fetchMetrics: () => void; | |||
} | |||
interface OwnProps { | |||
branchLike?: BranchLike; | |||
component: ComponentMeasure; | |||
} | |||
const mapStateToProps = (state: any): StateToProps => ({ | |||
currentUser: getCurrentUser(state), | |||
metrics: getMetrics(state), | |||
metricsKey: getMetricsKey(state) | |||
}); | |||
function banQualityGate(component /*: Component */) /*: Array<Measure> */ { | |||
const bannedMetrics = []; | |||
if (!['VW', 'SVW'].includes(component.qualifier)) { | |||
function banQualityGate({ measures = [], qualifier }: ComponentMeasure): Measure[] { | |||
const bannedMetrics: string[] = []; | |||
if (!['VW', 'SVW'].includes(qualifier)) { | |||
bannedMetrics.push('alert_status'); | |||
} | |||
if (component.qualifier === 'APP') { | |||
if (qualifier === 'APP') { | |||
bannedMetrics.push('releasability_rating', 'releasability_effort'); | |||
} | |||
return component.measures.filter(measure => !bannedMetrics.includes(measure.metric)); | |||
return measures.filter(measure => !bannedMetrics.includes(measure.metric)); | |||
} | |||
const fetchMeasures = ( | |||
component /*: string */, | |||
metricsKey /*: Array<string> */, | |||
branchLike /*: { id?: string; name: string } | void */ | |||
) => (dispatch, getState) => { | |||
const fetchMeasures = (component: string, metricsKey: string[], branchLike?: BranchLike) => ( | |||
_dispatch: Dispatch<any>, | |||
getState: () => any | |||
) => { | |||
if (metricsKey.length <= 0) { | |||
return Promise.resolve({ component: {}, measures: [], leakPeriod: null }); | |||
} | |||
@@ -60,21 +86,23 @@ const fetchMeasures = ( | |||
return getMeasuresAndMeta(component, metricsKey, { | |||
additionalFields: 'periods', | |||
...getBranchLikeQuery(branchLike) | |||
}).then(r => { | |||
const measures = banQualityGate(r.component).map(measure => | |||
}).then(({ component, periods }) => { | |||
const measures = banQualityGate(component).map(measure => | |||
enhanceMeasure(measure, getMetrics(getState())) | |||
); | |||
const newBugs = measures.find(measure => measure.metric.key === 'new_bugs'); | |||
const applicationPeriods = newBugs ? [{ index: 1 }] : []; | |||
const periods = r.component.qualifier === 'APP' ? applicationPeriods : r.periods; | |||
return { component: r.component, measures, leakPeriod: getLeakPeriod(periods) }; | |||
const applicationPeriods = newBugs ? [{ index: 1 } as Period] : []; | |||
const leakPeriod = getLeakPeriod(component.qualifier === 'APP' ? applicationPeriods : periods); | |||
return { component, measures, leakPeriod }; | |||
}, throwGlobalError); | |||
}; | |||
const mapDispatchToProps = { fetchMeasures, fetchMetrics }; | |||
const mapDispatchToProps: DispatchToProps = { fetchMeasures: fetchMeasures as any, fetchMetrics }; | |||
export default connect( | |||
mapStateToProps, | |||
mapDispatchToProps | |||
)(withRouter(App)); | |||
export default withRouter<OwnProps>( | |||
connect<StateToProps, DispatchToProps, OwnProps & WithRouterProps>( | |||
mapStateToProps, | |||
mapDispatchToProps | |||
)(App) | |||
); |
@@ -17,22 +17,21 @@ | |||
* 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 * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
/*:: type Props = { | |||
className?: string, | |||
current: ?number, | |||
total: number | |||
}; */ | |||
interface Props { | |||
className?: string; | |||
current?: number; | |||
total: number; | |||
} | |||
export default function FilesCounter({ className, current, total } /*: Props */) { | |||
export default function FilesCounter({ className, current, total }: Props) { | |||
return ( | |||
<span className={className}> | |||
<strong> | |||
{current != null && ( | |||
{current !== undefined && ( | |||
<span> | |||
{formatMeasure(current, 'INT')} | |||
{' / '} |
@@ -24,10 +24,10 @@ import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { getPeriodLabel, getPeriodDate, Period, PeriodMode } from '../../../helpers/periods'; | |||
import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { differenceInDays } from '../../../helpers/dates'; | |||
import { ComponentMeasure } from '../../../app/types'; | |||
import { ComponentMeasure, Period, PeriodMode } from '../../../app/types'; | |||
interface Props { | |||
className?: string; |
@@ -44,10 +44,10 @@ import { | |||
isLoggedIn, | |||
Metric, | |||
Paging, | |||
MeasureEnhanced | |||
MeasureEnhanced, | |||
Period | |||
} from '../../../app/types'; | |||
import { RequestData } from '../../../helpers/request'; | |||
import { Period } from '../../../helpers/periods'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
@@ -328,10 +328,8 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
<MeasureHeader | |||
branchLike={branchLike} | |||
component={component} | |||
components={this.state.components} | |||
leakPeriod={this.props.leakPeriod} | |||
// fall back to `undefined` to be compatible with typescript files where we compare with `=== undefined` | |||
measure={measure || undefined} | |||
measure={measure} | |||
metric={metric} | |||
secondaryMeasure={this.props.secondaryMeasure} | |||
/> |
@@ -26,9 +26,9 @@ import { | |||
Metric, | |||
BranchLike, | |||
CurrentUser, | |||
MeasureEnhanced | |||
MeasureEnhanced, | |||
Period | |||
} from '../../../app/types'; | |||
import { Period } from '../../../helpers/periods'; | |||
interface Props { | |||
branchLike?: BranchLike; |
@@ -17,8 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import LeakPeriodLegend from './LeakPeriodLegend'; | |||
import HistoryIcon from '../../../components/icons-components/HistoryIcon'; | |||
@@ -29,21 +28,18 @@ import Tooltip from '../../../components/controls/Tooltip'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import { getMeasureHistoryUrl } from '../../../helpers/urls'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
/*:: import type { Component, Period } from '../types'; */ | |||
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ | |||
/*:: import type { Metric } from '../../../app/flow-types'; */ | |||
import { MeasureEnhanced, Metric, ComponentMeasure, BranchLike, Period } from '../../../app/types'; | |||
/*:: type Props = {| | |||
branchLike?: { id?: string; name: string }, | |||
component: Component, | |||
components: Array<Component>, | |||
leakPeriod?: Period, | |||
measure?: MeasureEnhanced, | |||
metric: Metric, | |||
secondaryMeasure: ?MeasureEnhanced | |||
|}; */ | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: ComponentMeasure; | |||
leakPeriod?: Period; | |||
measure?: MeasureEnhanced; | |||
metric: Metric; | |||
secondaryMeasure?: MeasureEnhanced; | |||
} | |||
export default function MeasureHeader(props /*: Props*/) { | |||
export default function MeasureHeader(props: Props) { | |||
const { branchLike, component, leakPeriod, measure, metric, secondaryMeasure } = props; | |||
const isDiff = isDiffMetric(metric.key); | |||
const hasHistory = component.qualifier !== 'FIL' && component.qualifier !== 'UTS'; | |||
@@ -83,13 +79,14 @@ export default function MeasureHeader(props /*: Props*/) { | |||
)} | |||
</div> | |||
<div className="measure-details-primary-actions"> | |||
{leakPeriod != null && ( | |||
{leakPeriod && ( | |||
<LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} /> | |||
)} | |||
</div> | |||
</div> | |||
{secondaryMeasure && | |||
secondaryMeasure.metric.key === 'ncloc_language_distribution' && ( | |||
secondaryMeasure.metric.key === 'ncloc_language_distribution' && | |||
secondaryMeasure.value !== undefined && ( | |||
<div className="measure-details-secondary"> | |||
<LanguageDistributionContainer | |||
alignTicks={true} |
@@ -17,8 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import LeakPeriodLegend from './LeakPeriodLegend'; | |||
import MeasureFavoriteContainer from './MeasureFavoriteContainer'; | |||
@@ -29,44 +28,47 @@ import { getComponentLeaves } from '../../../api/components'; | |||
import { enhanceComponent, getBubbleMetrics, isFileType } from '../utils'; | |||
import { getBranchLikeQuery } from '../../../helpers/branches'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
/*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */ | |||
/*:: import type { Metric } from '../../../app/flow-types'; */ | |||
import { | |||
BranchLike, | |||
ComponentMeasure, | |||
ComponentMeasureEnhanced, | |||
CurrentUser, | |||
Metric, | |||
Paging, | |||
Period | |||
} from '../../../app/types'; | |||
/*:: type Props = {| | |||
branchLike?: { id?: string; name: string }, | |||
className?: string, | |||
component: Component, | |||
currentUser: { isLoggedIn: boolean }, | |||
domain: string, | |||
leakPeriod: Period, | |||
loading: boolean, | |||
metrics: { [string]: Metric }, | |||
rootComponent: Component, | |||
updateLoading: ({ [string]: boolean }) => void, | |||
updateSelected: string => void | |||
|}; */ | |||
interface Props { | |||
branchLike?: BranchLike; | |||
className?: string; | |||
component: ComponentMeasure; | |||
currentUser: CurrentUser; | |||
domain: string; | |||
leakPeriod?: Period; | |||
loading: boolean; | |||
metrics: { [metric: string]: Metric }; | |||
rootComponent: ComponentMeasure; | |||
updateLoading: (param: { [key: string]: boolean }) => void; | |||
updateSelected: (component: string) => void; | |||
} | |||
/*:: type State = { | |||
components: Array<ComponentEnhanced>, | |||
paging?: Paging | |||
}; */ | |||
interface State { | |||
components: ComponentMeasureEnhanced[]; | |||
paging?: Paging; | |||
} | |||
const BUBBLES_LIMIT = 500; | |||
export default class MeasureOverview extends React.PureComponent { | |||
/*:: mounted: boolean; */ | |||
/*:: props: Props; */ | |||
state /*: State */ = { | |||
components: [], | |||
paging: null | |||
}; | |||
export default class MeasureOverview extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { components: [] }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchComponents(this.props); | |||
} | |||
componentWillReceiveProps(nextProps /*: Props */) { | |||
componentWillReceiveProps(nextProps: Props) { | |||
if ( | |||
nextProps.component !== this.props.component || | |||
nextProps.metrics !== this.props.metrics || | |||
@@ -80,16 +82,16 @@ export default class MeasureOverview extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
fetchComponents = (props /*: Props */) => { | |||
fetchComponents = (props: Props) => { | |||
const { branchLike, component, domain, metrics } = props; | |||
if (isFileType(component)) { | |||
this.setState({ components: [], paging: null }); | |||
this.setState({ components: [], paging: undefined }); | |||
return; | |||
} | |||
const { x, y, size, colors } = getBubbleMetrics(domain, metrics); | |||
const metricsKey = [x.key, y.key, size.key]; | |||
if (colors) { | |||
metricsKey.push(colors.map(metric => metric.key)); | |||
metricsKey.push(...colors.map(metric => metric.key)); | |||
} | |||
const options = { | |||
...getBranchLikeQuery(branchLike), | |||
@@ -105,7 +107,9 @@ export default class MeasureOverview extends React.PureComponent { | |||
if (domain === this.props.domain) { | |||
if (this.mounted) { | |||
this.setState({ | |||
components: r.components.map(component => enhanceComponent(component, null, metrics)), | |||
components: r.components.map(component => | |||
enhanceComponent(component, undefined, metrics) | |||
), | |||
paging: r.paging | |||
}); | |||
} | |||
@@ -171,7 +175,7 @@ export default class MeasureOverview extends React.PureComponent { | |||
</div> | |||
<div className="layout-page-main-inner measure-details-content"> | |||
<div className="clearfix big-spacer-bottom"> | |||
{leakPeriod != null && ( | |||
{leakPeriod && ( | |||
<LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} /> | |||
)} | |||
</div> |
@@ -17,49 +17,43 @@ | |||
* 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 * as React from 'react'; | |||
import { InjectedRouter } from 'react-router'; | |||
import MeasureOverview from './MeasureOverview'; | |||
import { getComponentShow } from '../../../api/components'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { isViewType } from '../utils'; | |||
import { isViewType, Query } from '../utils'; | |||
import { getBranchLikeQuery } from '../../../helpers/branches'; | |||
/*:: import type { Component, Period, Query } from '../types'; */ | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
/*:: import type { Metric } from '../../../app/flow-types'; */ | |||
import { BranchLike, ComponentMeasure, CurrentUser, Metric, Period } from '../../../app/types'; | |||
/*:: type Props = {| | |||
branchLike?: { id?: string; name: string }, | |||
className?: string, | |||
rootComponent: Component, | |||
currentUser: { isLoggedIn: boolean }, | |||
domain: string, | |||
leakPeriod: Period, | |||
metrics: { [string]: Metric }, | |||
router: { | |||
push: ({ pathname: string, query?: RawQuery }) => void | |||
}, | |||
selected: ?string, | |||
updateQuery: Query => void | |||
|}; */ | |||
interface Props { | |||
branchLike?: BranchLike; | |||
className?: string; | |||
currentUser: CurrentUser; | |||
domain: string; | |||
leakPeriod?: Period; | |||
metrics: { [metric: string]: Metric }; | |||
rootComponent: ComponentMeasure; | |||
router: InjectedRouter; | |||
selected?: string; | |||
updateQuery: (query: Partial<Query>) => void; | |||
} | |||
/*:: type State = { | |||
component: ?Component, | |||
loading: { | |||
component: boolean, | |||
bubbles: boolean | |||
} | |||
}; */ | |||
interface LoadingState { | |||
bubbles: boolean; | |||
component: boolean; | |||
} | |||
export default class MeasureOverviewContainer extends React.PureComponent { | |||
/*:: mounted: boolean; */ | |||
/*:: props: Props; */ | |||
state /*: State */ = { | |||
component: null, | |||
loading: { | |||
component: false, | |||
bubbles: false | |||
} | |||
interface State { | |||
component?: ComponentMeasure; | |||
loading: LoadingState; | |||
} | |||
export default class MeasureOverviewContainer extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
loading: { bubbles: false, component: false } | |||
}; | |||
componentDidMount() { | |||
@@ -67,7 +61,7 @@ export default class MeasureOverviewContainer extends React.PureComponent { | |||
this.fetchComponent(this.props); | |||
} | |||
componentWillReceiveProps(nextProps /*: Props */) { | |||
componentWillReceiveProps(nextProps: Props) { | |||
const { component } = this.state; | |||
const componentChanged = | |||
!component || | |||
@@ -82,7 +76,7 @@ export default class MeasureOverviewContainer extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
fetchComponent = ({ branchLike, rootComponent, selected } /*: Props */) => { | |||
fetchComponent = ({ branchLike, rootComponent, selected }: Props) => { | |||
if (!selected || rootComponent.key === selected) { | |||
this.setState({ component: rootComponent }); | |||
this.updateLoading({ component: false }); | |||
@@ -100,18 +94,18 @@ export default class MeasureOverviewContainer extends React.PureComponent { | |||
); | |||
}; | |||
updateLoading = (loading /*: { [string]: boolean } */) => { | |||
updateLoading = (loading: Partial<LoadingState>) => { | |||
if (this.mounted) { | |||
this.setState(state => ({ loading: { ...state.loading, ...loading } })); | |||
} | |||
}; | |||
updateSelected = (component /*: string */) => { | |||
updateSelected = (component: string) => { | |||
if (this.state.component && isViewType(this.state.component)) { | |||
this.props.router.push(getProjectUrl(component)); | |||
} else { | |||
this.props.updateQuery({ | |||
selected: component !== this.props.rootComponent.key ? component : null | |||
selected: component !== this.props.rootComponent.key ? component : undefined | |||
}); | |||
} | |||
}; |
@@ -17,11 +17,10 @@ | |||
* 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 * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function MetricNotFound({ className } /*: { className?: string } */) { | |||
export default function MetricNotFound({ className }: { className?: string }) { | |||
return ( | |||
<div className={className}> | |||
<div className="alert alert-danger">{translate('component_measures.not_found')}</div> |
@@ -17,23 +17,22 @@ | |||
* 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 * as React from 'react'; | |||
import FilesCounter from './FilesCounter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: import type { Paging } from '../types'; */ | |||
import { Paging } from '../../../app/types'; | |||
/*:: type Props = {| | |||
current: ?number, | |||
isFile: ?boolean, | |||
paging: ?Paging, | |||
totalLoadedComponents?: number, | |||
view?: string | |||
|}; */ | |||
interface Props { | |||
current?: number; | |||
isFile?: boolean; | |||
paging?: Paging; | |||
totalLoadedComponents?: number; | |||
view?: string; | |||
} | |||
export default function PageActions(props /*: Props */) { | |||
export default function PageActions(props: Props) { | |||
const { isFile, paging, totalLoadedComponents } = props; | |||
const showShortcuts = ['list', 'tree'].includes(props.view); | |||
const showShortcuts = props.view && ['list', 'tree'].includes(props.view); | |||
return ( | |||
<div className="pull-right"> | |||
{!isFile && showShortcuts && renderShortcuts()} |
@@ -17,36 +17,42 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
/* eslint-disable camelcase */ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import App from '../App'; | |||
const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; | |||
const METRICS = { | |||
lines_to_cover: { | |||
id: '1', | |||
key: 'lines_to_cover', | |||
type: 'INT', | |||
name: 'Lines to Cover', | |||
domain: 'Coverage' | |||
}, | |||
coverage: { key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, | |||
coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, | |||
duplicated_lines_density: { | |||
id: '3', | |||
key: 'duplicated_lines_density', | |||
type: 'PERCENT', | |||
name: 'Duplicated Lines (%)', | |||
domain: 'Duplications' | |||
}, | |||
new_bugs: { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } | |||
new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } | |||
}; | |||
const PROPS = { | |||
branch: { isMain: true, name: 'master' }, | |||
component: { key: 'foo' }, | |||
component: COMPONENT, | |||
currentUser: { isLoggedIn: false }, | |||
location: { pathname: '/component_measures', query: { metric: 'coverage' } }, | |||
fetchMeasures: () => Promise.resolve({ measures: [] }), | |||
fetchMetrics: () => {}, | |||
fetchMeasures: jest.fn().mockResolvedValue({ component: COMPONENT, measures: [] }), | |||
fetchMetrics: jest.fn(), | |||
metrics: METRICS, | |||
metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'], | |||
router: { push: () => {} } | |||
router: { push: jest.fn() } as any | |||
}; | |||
it('should render correctly', () => { |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import FilesCounter from '../FilesCounter'; | |||
@@ -26,5 +26,5 @@ it('should display x files on y total', () => { | |||
}); | |||
it('should display only total of files', () => { | |||
expect(shallow(<FilesCounter current={null} total={123455} />)).toMatchSnapshot(); | |||
expect(shallow(<FilesCounter current={undefined} total={123455} />)).toMatchSnapshot(); | |||
}); |
@@ -20,9 +20,8 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import LeakPeriodLegend from '../LeakPeriodLegend'; | |||
import { PeriodMode, Period } from '../../../../helpers/periods'; | |||
import { differenceInDays } from '../../../../helpers/dates'; | |||
import { ComponentMeasure } from '../../../../app/types'; | |||
import { ComponentMeasure, Period, PeriodMode } from '../../../../app/types'; | |||
jest.mock('../../../../helpers/dates', () => { | |||
const dates = require.requireActual('../../../../helpers/dates'); |
@@ -17,11 +17,13 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import MeasureHeader from '../MeasureHeader'; | |||
import { PeriodMode } from '../../../../app/types'; | |||
const METRIC = { | |||
id: '1', | |||
key: 'reliability_rating', | |||
type: 'RATING', | |||
name: 'Reliability Rating' | |||
@@ -35,6 +37,7 @@ const MEASURE = { | |||
}; | |||
const LEAK_METRIC = { | |||
id: '2', | |||
key: 'new_reliability_rating', | |||
type: 'RATING', | |||
name: 'Reliability Rating on New Code' | |||
@@ -49,28 +52,23 @@ const LEAK_MEASURE = { | |||
const SECONDARY = { | |||
value: 'java=175123;js=26382', | |||
metric: { | |||
id: '3', | |||
key: 'ncloc_language_distribution', | |||
type: 'DATA', | |||
name: 'Lines of Code Per Language' | |||
}, | |||
leak: null | |||
} | |||
}; | |||
const PROPS = { | |||
component: { key: 'foo', qualifier: 'TRK' }, | |||
components: [], | |||
handleSelect: () => {}, | |||
component: { key: 'foo', name: 'Foo', qualifier: 'TRK' }, | |||
leakPeriod: { | |||
date: '2017-05-16T13:50:02+0200', | |||
index: 1, | |||
mode: 'previous_version', | |||
mode: PeriodMode.PreviousVersion, | |||
parameter: '6,4' | |||
}, | |||
measure: MEASURE, | |||
metric: METRIC, | |||
paging: null, | |||
secondaryMeasure: null, | |||
selectedIdx: null | |||
metric: METRIC | |||
}; | |||
it('should render correctly', () => { | |||
@@ -109,20 +107,6 @@ it('should display secondary measure too', () => { | |||
expect(wrapper.find('Connect(LanguageDistribution)')).toHaveLength(1); | |||
}); | |||
it('should display correctly for open file', () => { | |||
const wrapper = shallow( | |||
<MeasureHeader | |||
{...PROPS} | |||
component={{ key: 'bar', qualifier: 'FIL' }} | |||
components={[{ key: 'foo' }, { key: 'bar' }, { key: 'baz' }]} | |||
selectedIdx={1} | |||
/> | |||
); | |||
expect(wrapper.find('.measure-details-primary-actions')).toMatchSnapshot(); | |||
wrapper.setProps({ components: [{ key: 'foo' }, { key: 'bar' }] }); | |||
expect(wrapper.find('.measure-details-primary-actions')).toMatchSnapshot(); | |||
}); | |||
it('should work with measure without value', () => { | |||
expect(shallow(<MeasureHeader {...PROPS} measure={undefined} />)).toMatchSnapshot(); | |||
}); |
@@ -17,10 +17,16 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PageActions from '../PageActions'; | |||
const PAGING = { | |||
pageIndex: 1, | |||
pageSize: 100, | |||
total: 120 | |||
}; | |||
it('should display correctly for a project', () => { | |||
expect( | |||
shallow(<PageActions isFile={false} totalLoadedComponents={20} view="list" />) | |||
@@ -46,7 +52,7 @@ it('should display the total of files', () => { | |||
<PageActions | |||
current={12} | |||
isFile={false} | |||
paging={{ total: 120 }} | |||
paging={PAGING} | |||
totalLoadedComponents={20} | |||
view="treemap" | |||
/> | |||
@@ -57,7 +63,7 @@ it('should display the total of files', () => { | |||
<PageActions | |||
current={12} | |||
isFile={true} | |||
paging={{ total: 120 }} | |||
paging={PAGING} | |||
totalLoadedComponents={20} | |||
view="list" | |||
/> |
@@ -18,11 +18,37 @@ exports[`should render correctly 1`] = ` | |||
/> | |||
<MeasureContentContainer | |||
className="layout-page-main" | |||
fetchMeasures={[Function]} | |||
leakPeriod={null} | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": false, | |||
} | |||
} | |||
fetchMeasures={ | |||
[MockFunction] { | |||
"calls": Array [ | |||
Array [ | |||
"foo", | |||
Array [ | |||
"lines_to_cover", | |||
"coverage", | |||
"duplicated_lines_density", | |||
"new_bugs", | |||
], | |||
undefined, | |||
], | |||
], | |||
"results": Array [ | |||
Object { | |||
"isThrow": false, | |||
"value": Promise {}, | |||
}, | |||
], | |||
} | |||
} | |||
metric={ | |||
Object { | |||
"domain": "Coverage", | |||
"id": "2", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
@@ -32,24 +58,28 @@ exports[`should render correctly 1`] = ` | |||
Object { | |||
"coverage": Object { | |||
"domain": "Coverage", | |||
"id": "2", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
"duplicated_lines_density": Object { | |||
"domain": "Duplications", | |||
"id": "3", | |||
"key": "duplicated_lines_density", | |||
"name": "Duplicated Lines (%)", | |||
"type": "PERCENT", | |||
}, | |||
"lines_to_cover": Object { | |||
"domain": "Coverage", | |||
"id": "1", | |||
"key": "lines_to_cover", | |||
"name": "Lines to Cover", | |||
"type": "INT", | |||
}, | |||
"new_bugs": Object { | |||
"domain": "Reliability", | |||
"id": "4", | |||
"key": "new_bugs", | |||
"name": "New Bugs", | |||
"type": "INT", | |||
@@ -59,11 +89,13 @@ exports[`should render correctly 1`] = ` | |||
rootComponent={ | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
router={ | |||
Object { | |||
"push": [Function], | |||
"push": [MockFunction], | |||
} | |||
} | |||
selected="" |
@@ -1,53 +1,5 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display correctly for open file 1`] = ` | |||
<div | |||
className="measure-details-primary-actions" | |||
> | |||
<LeakPeriodLegend | |||
className="spacer-left" | |||
component={ | |||
Object { | |||
"key": "bar", | |||
"qualifier": "FIL", | |||
} | |||
} | |||
period={ | |||
Object { | |||
"date": "2017-05-16T13:50:02+0200", | |||
"index": 1, | |||
"mode": "previous_version", | |||
"parameter": "6,4", | |||
} | |||
} | |||
/> | |||
</div> | |||
`; | |||
exports[`should display correctly for open file 2`] = ` | |||
<div | |||
className="measure-details-primary-actions" | |||
> | |||
<LeakPeriodLegend | |||
className="spacer-left" | |||
component={ | |||
Object { | |||
"key": "bar", | |||
"qualifier": "FIL", | |||
} | |||
} | |||
period={ | |||
Object { | |||
"date": "2017-05-16T13:50:02+0200", | |||
"index": 1, | |||
"mode": "previous_version", | |||
"parameter": "6,4", | |||
} | |||
} | |||
/> | |||
</div> | |||
`; | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="measure-details-header big-spacer-bottom" | |||
@@ -104,6 +56,7 @@ exports[`should render correctly 1`] = ` | |||
component={ | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
@@ -157,6 +110,7 @@ exports[`should render correctly for leak 1`] = ` | |||
component={ | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
@@ -250,6 +204,7 @@ exports[`should work with measure without value 1`] = ` | |||
component={ | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
} | |||
} |
@@ -23,7 +23,7 @@ export const bubbles: { | |||
y: string; | |||
size: string; | |||
colors?: string[]; | |||
yDomain?: number[]; | |||
yDomain?: [number, number]; | |||
}; | |||
} = { | |||
Reliability: { |
@@ -17,7 +17,12 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
export const domains: { [domain: string]: { categories?: string[]; order: string[] } } = { | |||
interface Domains { | |||
[domain: string]: { categories?: string[]; order: string[] }; | |||
} | |||
export const domains: Domains = { | |||
Reliability: { | |||
categories: ['new_code_category', 'overall_category'], | |||
order: [ |
@@ -17,10 +17,9 @@ | |||
* 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 * as React from 'react'; | |||
import EmptyResult from './EmptyResult'; | |||
import OriginalBubbleChart from '../../../components/charts/BubbleChart'; | |||
import OriginalBubbleChart, { BubbleItem } from '../../../components/charts/BubbleChart'; | |||
import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | |||
@@ -32,43 +31,31 @@ import { | |||
} from '../../../helpers/l10n'; | |||
import { getBubbleMetrics, getBubbleYDomain, isProjectOverview } from '../utils'; | |||
import { RATING_COLORS } from '../../../helpers/constants'; | |||
/*:: import type { Component, ComponentEnhanced } from '../types'; */ | |||
/*:: import type { Metric } from '../../../app/flow-types'; */ | |||
import { ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types'; | |||
const HEIGHT = 500; | |||
/*:: type Props = {| | |||
component: Component, | |||
components: Array<ComponentEnhanced>, | |||
domain: string, | |||
metrics: { [string]: Metric }, | |||
updateSelected: string => void | |||
|}; */ | |||
export default class BubbleChart extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
component: ComponentMeasure; | |||
components: ComponentMeasureEnhanced[]; | |||
domain: string; | |||
metrics: { [metric: string]: Metric }; | |||
updateSelected: (component: string) => void; | |||
} | |||
getMeasureVal = (component /*: ComponentEnhanced */, metric /*: Metric */) => { | |||
export default class BubbleChart extends React.PureComponent<Props> { | |||
getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => { | |||
const measure = component.measures.find(measure => measure.metric.key === metric.key); | |||
if (measure) { | |||
return Number(isDiffMetric(metric.key) ? measure.leak : measure.value); | |||
if (!measure) { | |||
return undefined; | |||
} | |||
return Number(isDiffMetric(metric.key) ? measure.leak : measure.value); | |||
}; | |||
getTooltip( | |||
componentName /*: string */, | |||
values /*: { | |||
x: number, | |||
y: number, | |||
size: number, | |||
colors: ?Array<?number> | |||
}*/, | |||
metrics /*: { | |||
x: Metric , | |||
y: Metric , | |||
size: Metric , | |||
colors: ?Array<Metric> | |||
}*/ | |||
componentName: string, | |||
values: { x: number; y: number; size: number; colors?: Array<number | undefined> }, | |||
metrics: { x: Metric; y: Metric; size: Metric; colors?: Array<Metric> } | |||
) { | |||
const inner = [ | |||
componentName, | |||
@@ -76,10 +63,11 @@ export default class BubbleChart extends React.PureComponent { | |||
`${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`, | |||
`${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}` | |||
]; | |||
if (values.colors && metrics.colors) { | |||
metrics.colors.forEach((metric, idx) => { | |||
// $FlowFixMe colors is always defined at this point | |||
const colorValue = values.colors[idx]; | |||
const { colors: valuesColors } = values; | |||
const { colors: metricColors } = metrics; | |||
if (valuesColors && metricColors) { | |||
metricColors.forEach((metric, idx) => { | |||
const colorValue = valuesColors[idx]; | |||
if (colorValue || colorValue === 0) { | |||
inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`); | |||
} | |||
@@ -97,10 +85,10 @@ export default class BubbleChart extends React.PureComponent { | |||
); | |||
} | |||
handleBubbleClick = (component /*: ComponentEnhanced */) => | |||
handleBubbleClick = (component: ComponentMeasureEnhanced) => | |||
this.props.updateSelected(component.refKey || component.key); | |||
getDescription(domain /*: string */) { | |||
getDescription(domain: string) { | |||
const description = `component_measures.overview.${domain}.description`; | |||
const translatedDescription = translate(description); | |||
if (description === translatedDescription) { | |||
@@ -109,14 +97,7 @@ export default class BubbleChart extends React.PureComponent { | |||
return translatedDescription; | |||
} | |||
renderBubbleChart( | |||
metrics /*: { | |||
x: Metric , | |||
y: Metric , | |||
size: Metric , | |||
colors: ?Array<Metric> | |||
}*/ | |||
) { | |||
renderBubbleChart(metrics: { x: Metric; y: Metric; size: Metric; colors?: Metric[] }) { | |||
const items = this.props.components | |||
.map(component => { | |||
const x = this.getMeasureVal(component, metrics.x); | |||
@@ -125,25 +106,27 @@ export default class BubbleChart extends React.PureComponent { | |||
const colors = | |||
metrics.colors && metrics.colors.map(metric => this.getMeasureVal(component, metric)); | |||
if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) { | |||
return null; | |||
return undefined; | |||
} | |||
return { | |||
x, | |||
y, | |||
size, | |||
color: | |||
colors != null ? RATING_COLORS[Math.max(...colors.filter(Boolean)) - 1] : undefined, | |||
link: component, | |||
colors !== undefined | |||
? RATING_COLORS[Math.max(...colors.filter(Boolean) as number[]) - 1] | |||
: undefined, | |||
data: component, | |||
tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics) | |||
}; | |||
}) | |||
.filter(Boolean); | |||
.filter(Boolean) as BubbleItem<ComponentMeasureEnhanced>[]; | |||
const formatXTick = tick => formatMeasure(tick, metrics.x.type); | |||
const formatYTick = tick => formatMeasure(tick, metrics.y.type); | |||
const formatXTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.x.type); | |||
const formatYTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.y.type); | |||
return ( | |||
<OriginalBubbleChart | |||
<OriginalBubbleChart<ComponentMeasureEnhanced> | |||
formatXTick={formatXTick} | |||
formatYTick={formatYTick} | |||
height={HEIGHT} | |||
@@ -155,11 +138,7 @@ export default class BubbleChart extends React.PureComponent { | |||
); | |||
} | |||
renderChartHeader( | |||
domain /*: string */, | |||
sizeMetric /*: Metric */, | |||
colorsMetric /*: ?Array<Metric> */ | |||
) { | |||
renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric?: Metric[]) { | |||
const title = isProjectOverview(domain) | |||
? translate('component_measures.overview', domain, 'title') | |||
: translateWithParameters( |
@@ -20,8 +20,13 @@ | |||
import * as React from 'react'; | |||
import * as key from 'keymaster'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { BranchLike, ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types'; | |||
import { Period } from '../../../helpers/periods'; | |||
import { | |||
BranchLike, | |||
ComponentMeasure, | |||
ComponentMeasureEnhanced, | |||
Metric, | |||
Period | |||
} from '../../../app/types'; | |||
interface Props { | |||
branchLike?: BranchLike; |
@@ -17,8 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import LinkIcon from '../../../components/icons-components/LinkIcon'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
@@ -27,80 +26,72 @@ import { splitPath } from '../../../helpers/path'; | |||
import { | |||
getPathUrlAsString, | |||
getBranchLikeUrl, | |||
getLongLivingBranchUrl, | |||
getComponentDrilldownUrlWithSelection | |||
getComponentDrilldownUrlWithSelection, | |||
getProjectUrl | |||
} from '../../../helpers/urls'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: import type { Component, ComponentEnhanced } from '../types'; */ | |||
/*:: import type { Metric } from '../../../app/flow-types'; */ | |||
import { BranchLike, ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types'; | |||
/*:: type Props = { | |||
branchLike?: { id?: string; name: string }, | |||
component: ComponentEnhanced, | |||
onClick: string => void, | |||
metric: Metric, | |||
rootComponent: Component | |||
}; */ | |||
export default class ComponentCell extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: ComponentMeasureEnhanced; | |||
onClick: (component: string) => void; | |||
metric: Metric; | |||
rootComponent: ComponentMeasure; | |||
} | |||
handleClick = (e /*: MouseEvent */) => { | |||
const isLeftClickEvent = e.button === 0; | |||
const isModifiedEvent = !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); | |||
export default class ComponentCell extends React.PureComponent<Props> { | |||
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { | |||
const isLeftClickEvent = event.button === 0; | |||
const isModifiedEvent = !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); | |||
if (isLeftClickEvent && !isModifiedEvent) { | |||
e.preventDefault(); | |||
event.preventDefault(); | |||
this.props.onClick(this.props.component.key); | |||
} | |||
}; | |||
renderInner() { | |||
renderInner(componentKey: string) { | |||
const { component } = this.props; | |||
let head = ''; | |||
let tail = component.name; | |||
let branch = null; | |||
let branchComponent = null; | |||
if (['DIR', 'FIL', 'UTS'].includes(component.qualifier)) { | |||
const parts = splitPath(component.path); | |||
({ head, tail } = parts); | |||
if (['DIR', 'FIL', 'UTS'].includes(component.qualifier) && component.path) { | |||
({ head, tail } = splitPath(component.path)); | |||
} | |||
if (this.props.rootComponent.qualifier === 'APP') { | |||
branch = ( | |||
<React.Fragment> | |||
branchComponent = ( | |||
<> | |||
{component.branch ? ( | |||
<React.Fragment> | |||
<> | |||
<LongLivingBranchIcon className="spacer-left little-spacer-right" /> | |||
<span className="note">{component.branch}</span> | |||
</React.Fragment> | |||
</> | |||
) : ( | |||
<span className="spacer-left outline-badge">{translate('branches.main_branch')}</span> | |||
)} | |||
</React.Fragment> | |||
</> | |||
); | |||
} | |||
return ( | |||
<span title={component.refKey || component.key}> | |||
<span title={componentKey}> | |||
<QualifierIcon qualifier={component.qualifier} /> | |||
| |||
{head.length > 0 && <span className="note">{head}/</span>} | |||
<span>{tail}</span> | |||
{branch} | |||
{branchComponent} | |||
</span> | |||
); | |||
} | |||
render() { | |||
const { branchLike, component, metric, rootComponent } = this.props; | |||
const to = | |||
this.props.rootComponent.qualifier === 'APP' | |||
? getLongLivingBranchUrl(component.refKey, component.branch) | |||
: getBranchLikeUrl(component.refKey, branchLike); | |||
return ( | |||
<td className="measure-details-component-cell"> | |||
<div className="text-ellipsis"> | |||
{component.refKey == null ? ( | |||
{!component.refKey ? ( | |||
<a | |||
className="link-no-underline" | |||
href={getPathUrlAsString( | |||
@@ -113,17 +104,21 @@ export default class ComponentCell extends React.PureComponent { | |||
)} | |||
id={'component-measures-component-link-' + component.key} | |||
onClick={this.handleClick}> | |||
{this.renderInner()} | |||
{this.renderInner(component.key)} | |||
</a> | |||
) : ( | |||
<Link | |||
className="link-no-underline" | |||
id={'component-measures-component-link-' + component.key} | |||
to={to}> | |||
id={'component-measures-component-link-' + component.refKey} | |||
to={ | |||
this.props.rootComponent.qualifier === 'APP' | |||
? getProjectUrl(component.refKey, component.branch) | |||
: getBranchLikeUrl(component.refKey, branchLike) | |||
}> | |||
<span className="big-spacer-right"> | |||
<LinkIcon /> | |||
</span> | |||
{this.renderInner()} | |||
{this.renderInner(component.refKey)} | |||
</Link> | |||
)} | |||
</div> |
@@ -17,25 +17,23 @@ | |||
* 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 classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import ComponentCell from './ComponentCell'; | |||
import MeasureCell from './MeasureCell'; | |||
/*:: import type { Component, ComponentEnhanced } from '../types'; */ | |||
/*:: import type { Metric } from '../../../app/flow-types'; */ | |||
import { ComponentMeasure, Metric, ComponentMeasureEnhanced, BranchLike } from '../../../app/types'; | |||
/*:: type Props = {| | |||
branchLike?: { id?: string; name: string }, | |||
component: ComponentEnhanced, | |||
isSelected: boolean, | |||
onClick: string => void, | |||
otherMetrics: Array<Metric>, | |||
metric: Metric, | |||
rootComponent: Component | |||
|}; */ | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: ComponentMeasureEnhanced; | |||
isSelected: boolean; | |||
onClick: (component: string) => void; | |||
otherMetrics: Metric[]; | |||
metric: Metric; | |||
rootComponent: ComponentMeasure; | |||
} | |||
export default function ComponentsListRow(props /*: Props */) { | |||
export default function ComponentsListRow(props: Props) { | |||
const { branchLike, component, rootComponent } = props; | |||
const otherMeasures = props.otherMetrics.map(metric => { | |||
const measure = component.measures.find(measure => measure.metric.key === metric.key); |
@@ -17,22 +17,19 @@ | |||
* 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 * as React from 'react'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
/*:: import type { ComponentEnhanced } from '../types'; */ | |||
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ | |||
/*:: import type { Metric } from '../../../app/flow-types'; */ | |||
import { Metric, MeasureEnhanced, ComponentMeasureEnhanced } from '../../../app/types'; | |||
/*:: type Props = { | |||
component: ComponentEnhanced, | |||
measure?: MeasureEnhanced, | |||
metric: Metric | |||
}; */ | |||
interface Props { | |||
component: ComponentMeasureEnhanced; | |||
measure?: MeasureEnhanced; | |||
metric: Metric; | |||
} | |||
export default function MeasureCell({ component, measure, metric } /*: Props */) { | |||
const getValue = (item /*: { leak?: ?string; value?: string } */) => | |||
export default function MeasureCell({ component, measure, metric }: Props) { | |||
const getValue = (item: { leak?: string; value?: string }) => | |||
isDiffMetric(metric.key) ? item.leak : item.value; | |||
const value = getValue(measure || component); |
@@ -17,19 +17,19 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import MeasureCell from '../MeasureCell'; | |||
describe('should correctly take the value', () => { | |||
const renderAndTakeValue = props => | |||
const renderAndTakeValue = (props: any) => | |||
shallow(<MeasureCell {...props} />) | |||
.find('Measure') | |||
.prop('value'); | |||
it('absolute value', () => { | |||
const component = { value: '123' }; | |||
const metric = { key: 'coverage' }; | |||
const metric = { id: '1', key: 'coverage' }; | |||
const measure = { value: '567' }; | |||
expect(renderAndTakeValue({ component, metric })).toEqual('123'); | |||
@@ -38,7 +38,7 @@ describe('should correctly take the value', () => { | |||
it('leak value', () => { | |||
const component = { leak: '234' }; | |||
const metric = { key: 'new_coverage' }; | |||
const metric = { id: '1', key: 'new_coverage' }; | |||
const measure = { leak: '678' }; | |||
expect(renderAndTakeValue({ component, metric })).toEqual('234'); |
@@ -17,8 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import FacetMeasureValue from './FacetMeasureValue'; | |||
import BubblesIcon from '../../../components/icons-components/BubblesIcon'; | |||
import FacetBox from '../../../components/facet/FacetBox'; | |||
@@ -38,26 +37,22 @@ import { | |||
getLocalizedMetricName, | |||
translate | |||
} from '../../../helpers/l10n'; | |||
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ | |||
import { MeasureEnhanced } from '../../../app/types'; | |||
/*:: type Props = {| | |||
onChange: (metric: string) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
domain: { name: string, measures: Array<MeasureEnhanced> }, | |||
selected: string | |||
|}; */ | |||
export default class DomainFacet extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
domain: { name: string; measures: MeasureEnhanced[] }; | |||
onChange: (metric: string) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
selected: string; | |||
} | |||
handleHeaderClick = () => this.props.onToggle(this.props.domain.name); | |||
export default class DomainFacet extends React.PureComponent<Props> { | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.props.domain.name); | |||
}; | |||
hasFacetSelected = ( | |||
domain /*: { name: string } */, | |||
measures /*: Array<MeasureEnhanced> */, | |||
selected /*: string */ | |||
) => { | |||
hasFacetSelected = (domain: { name: string }, measures: MeasureEnhanced[], selected: string) => { | |||
const measureSelected = measures.find(measure => measure.metric.key === selected); | |||
const overviewSelected = domain.name === selected && hasBubbleChart(domain.name); | |||
return measureSelected || overviewSelected; | |||
@@ -73,8 +68,9 @@ export default class DomainFacet extends React.PureComponent { | |||
return overviewSelected ? [translate('component_measures.domain_overview')] : []; | |||
}; | |||
renderItemFacetStat = (item /*: MeasureEnhanced */) => | |||
hasFacetStat(item.metric.key) ? <FacetMeasureValue measure={item} /> : null; | |||
renderItemFacetStat = (item: MeasureEnhanced) => { | |||
return hasFacetStat(item.metric.key) ? <FacetMeasureValue measure={item} /> : null; | |||
}; | |||
renderItemsFacet = () => { | |||
const { domain, selected } = this.props; | |||
@@ -110,6 +106,7 @@ export default class DomainFacet extends React.PureComponent { | |||
} | |||
onClick={this.props.onChange} | |||
stat={this.renderItemFacetStat(item)} | |||
tooltip={translateMetric(item.metric)} | |||
value={item.metric.key} | |||
/> | |||
) | |||
@@ -133,6 +130,7 @@ export default class DomainFacet extends React.PureComponent { | |||
} | |||
onClick={this.props.onChange} | |||
stat={<BubblesIcon size={14} />} | |||
tooltip={translate('component_measures.domain_overview')} | |||
value={domain.name} | |||
/> | |||
); |
@@ -17,13 +17,16 @@ | |||
* 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 * as React from 'react'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ | |||
import { MeasureEnhanced } from '../../../app/types'; | |||
export default function FacetMeasureValue({ measure } /*: { measure: MeasureEnhanced } */) { | |||
interface Props { | |||
measure: MeasureEnhanced; | |||
} | |||
export default function FacetMeasureValue({ measure }: Props) { | |||
if (isDiffMetric(measure.metric.key)) { | |||
return ( | |||
<div |
@@ -17,20 +17,19 @@ | |||
* 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 * as React from 'react'; | |||
import FacetBox from '../../../components/facet/FacetBox'; | |||
import FacetItem from '../../../components/facet/FacetItem'; | |||
import FacetItemsList from '../../../components/facet/FacetItemsList'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: type Props = {| | |||
onChange: (metric: string) => void, | |||
selected: string, | |||
value: string | |||
|}; */ | |||
interface Props { | |||
onChange: (metric: string) => void; | |||
selected: string; | |||
value: string; | |||
} | |||
export default function ProjectOverviewFacet({ value, selected, onChange } /*: Props */) { | |||
export default function ProjectOverviewFacet({ value, selected, onChange }: Props) { | |||
const facetName = translate('component_measures.overview', value, 'facet'); | |||
return ( | |||
<FacetBox property={value}> | |||
@@ -39,12 +38,9 @@ export default function ProjectOverviewFacet({ value, selected, onChange } /*: P | |||
active={value === selected} | |||
disabled={false} | |||
key={value} | |||
name={ | |||
<strong id={`measure-overview-${value}-name`} title={facetName}> | |||
{facetName} | |||
</strong> | |||
} | |||
name={<strong id={`measure-overview-${value}-name`}>{facetName}</strong>} | |||
onClick={onChange} | |||
tooltip={facetName} | |||
value={value} | |||
/> | |||
</FacetItemsList> |
@@ -17,42 +17,39 @@ | |||
* 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 * as React from 'react'; | |||
import ProjectOverviewFacet from './ProjectOverviewFacet'; | |||
import DomainFacet from './DomainFacet'; | |||
import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW } from '../utils'; | |||
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ | |||
/*:: import type { Query } from '../types'; */ | |||
import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW, Query } from '../utils'; | |||
import { MeasureEnhanced } from '../../../app/types'; | |||
/*:: type Props = {| | |||
measures: Array<MeasureEnhanced>, | |||
selectedMetric: string, | |||
updateQuery: Query => void | |||
|}; */ | |||
/*:: type State = {| | |||
openFacets: { [string]: boolean } | |||
|}; */ | |||
interface Props { | |||
measures: MeasureEnhanced[]; | |||
selectedMetric: string; | |||
updateQuery: (query: Query) => void; | |||
} | |||
export default class Sidebar extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
interface State { | |||
openFacets: { [metric: string]: boolean }; | |||
} | |||
constructor(props /*: Props */) { | |||
export default class Sidebar extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { openFacets: this.getOpenFacets({}, props) }; | |||
} | |||
componentWillReceiveProps(nextProps /*: Props */) { | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (nextProps.selectedMetric !== this.props.selectedMetric) { | |||
this.setState(state => this.getOpenFacets(state.openFacets, nextProps)); | |||
this.setState(({ openFacets }) => ({ | |||
openFacets: this.getOpenFacets(openFacets, nextProps) | |||
})); | |||
} | |||
} | |||
getOpenFacets = ( | |||
openFacets /*: { [string]: boolean } */, | |||
{ measures, selectedMetric } /*: Props */ | |||
openFacets: { [metric: string]: boolean }, | |||
{ measures, selectedMetric }: Props | |||
) => { | |||
const newOpenFacets = { ...openFacets }; | |||
const measure = measures.find(measure => measure.metric.key === selectedMetric); | |||
@@ -64,15 +61,15 @@ export default class Sidebar extends React.PureComponent { | |||
return newOpenFacets; | |||
}; | |||
toggleFacet = (name /*: string */) => { | |||
this.setState(({ openFacets } /*: State */) => ({ | |||
toggleFacet = (name: string) => { | |||
this.setState(({ openFacets }) => ({ | |||
openFacets: { ...openFacets, [name]: !openFacets[name] } | |||
})); | |||
}; | |||
resetSelection = (metric /*: string */) => ({ selected: null, view: getDefaultView(metric) }); | |||
resetSelection = (metric: string) => ({ selected: undefined, view: getDefaultView(metric) }); | |||
changeMetric = (metric /*: string */) => | |||
changeMetric = (metric: string) => | |||
this.props.updateQuery({ metric, ...this.resetSelection(metric) }); | |||
render() { |
@@ -17,8 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import DomainFacet from '../DomainFacet'; | |||
@@ -27,6 +26,7 @@ const DOMAIN = { | |||
measures: [ | |||
{ | |||
metric: { | |||
id: '1', | |||
key: 'bugs', | |||
type: 'INT', | |||
name: 'Bugs', | |||
@@ -38,6 +38,7 @@ const DOMAIN = { | |||
}, | |||
{ | |||
metric: { | |||
id: '2', | |||
key: 'new_bugs', | |||
type: 'INT', | |||
name: 'New Bugs', | |||
@@ -75,7 +76,7 @@ it('should not display subtitles of new measures if there is none', () => { | |||
name: 'Reliability', | |||
measures: [ | |||
{ | |||
metric: { key: 'bugs', type: 'INT', name: 'Bugs', domain: 'Reliability' }, | |||
metric: { id: '1', key: 'bugs', type: 'INT', name: 'Bugs', domain: 'Reliability' }, | |||
value: '5' | |||
} | |||
] | |||
@@ -99,7 +100,7 @@ it('should not display subtitles of new measures if there is none, even on last | |||
name: 'Reliability', | |||
measures: [ | |||
{ | |||
metric: { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }, | |||
metric: { id: '2', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }, | |||
value: '5' | |||
} | |||
] |
@@ -17,13 +17,13 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import FacetMeasureValue from '../FacetMeasureValue'; | |||
const MEASURE = { | |||
metric: { | |||
id: '1', | |||
key: 'bugs', | |||
type: 'INT', | |||
name: 'Bugs', | |||
@@ -35,6 +35,7 @@ const MEASURE = { | |||
}; | |||
const LEAK_MEASURE = { | |||
metric: { | |||
id: '2', | |||
key: 'new_bugs', | |||
type: 'INT', | |||
name: 'New Bugs', |
@@ -17,13 +17,14 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import Sidebar from '../Sidebar'; | |||
const MEASURES = [ | |||
{ | |||
metric: { | |||
id: '1', | |||
key: 'lines_to_cover', | |||
type: 'INT', | |||
name: 'Lines to Cover', | |||
@@ -35,6 +36,7 @@ const MEASURES = [ | |||
}, | |||
{ | |||
metric: { | |||
id: '2', | |||
key: 'coverage', | |||
type: 'PERCENT', | |||
name: 'Coverage', | |||
@@ -46,6 +48,7 @@ const MEASURES = [ | |||
}, | |||
{ | |||
metric: { | |||
id: '3', | |||
key: 'duplicated_lines_density', | |||
type: 'PERCENT', | |||
name: 'Duplicated Lines (%)', | |||
@@ -70,8 +73,8 @@ it('should display two facets', () => { | |||
it('should correctly toggle facets', () => { | |||
const wrapper = shallow(<Sidebar {...PROPS} />); | |||
expect(wrapper.state('openFacets').bugs).toBeUndefined(); | |||
wrapper.instance().toggleFacet('bugs'); | |||
(wrapper.instance() as Sidebar).toggleFacet('bugs'); | |||
expect(wrapper.state('openFacets').bugs).toBeTruthy(); | |||
wrapper.instance().toggleFacet('bugs'); | |||
(wrapper.instance() as Sidebar).toggleFacet('bugs'); | |||
expect(wrapper.state('openFacets').bugs).toBeFalsy(); | |||
}); |
@@ -30,6 +30,7 @@ exports[`should display facet item list 1`] = ` | |||
size={14} | |||
/> | |||
} | |||
tooltip="component_measures.domain_overview" | |||
value="Reliability" | |||
/> | |||
<span | |||
@@ -64,6 +65,7 @@ exports[`should display facet item list 1`] = ` | |||
"leak": "5", | |||
"metric": Object { | |||
"domain": "Reliability", | |||
"id": "2", | |||
"key": "new_bugs", | |||
"name": "New Bugs", | |||
"type": "INT", | |||
@@ -78,6 +80,7 @@ exports[`should display facet item list 1`] = ` | |||
} | |||
/> | |||
} | |||
tooltip="New Bugs" | |||
value="new_bugs" | |||
/> | |||
<span | |||
@@ -112,6 +115,7 @@ exports[`should display facet item list 1`] = ` | |||
"leak": "5", | |||
"metric": Object { | |||
"domain": "Reliability", | |||
"id": "1", | |||
"key": "bugs", | |||
"name": "Bugs", | |||
"type": "INT", | |||
@@ -127,6 +131,7 @@ exports[`should display facet item list 1`] = ` | |||
} | |||
/> | |||
} | |||
tooltip="Bugs" | |||
value="bugs" | |||
/> | |||
</FacetItemsList> | |||
@@ -167,6 +172,7 @@ exports[`should display facet item list with bugs selected 1`] = ` | |||
size={14} | |||
/> | |||
} | |||
tooltip="component_measures.domain_overview" | |||
value="Reliability" | |||
/> | |||
<span | |||
@@ -201,6 +207,7 @@ exports[`should display facet item list with bugs selected 1`] = ` | |||
"leak": "5", | |||
"metric": Object { | |||
"domain": "Reliability", | |||
"id": "2", | |||
"key": "new_bugs", | |||
"name": "New Bugs", | |||
"type": "INT", | |||
@@ -215,6 +222,7 @@ exports[`should display facet item list with bugs selected 1`] = ` | |||
} | |||
/> | |||
} | |||
tooltip="New Bugs" | |||
value="new_bugs" | |||
/> | |||
<span | |||
@@ -249,6 +257,7 @@ exports[`should display facet item list with bugs selected 1`] = ` | |||
"leak": "5", | |||
"metric": Object { | |||
"domain": "Reliability", | |||
"id": "1", | |||
"key": "bugs", | |||
"name": "Bugs", | |||
"type": "INT", | |||
@@ -264,6 +273,7 @@ exports[`should display facet item list with bugs selected 1`] = ` | |||
} | |||
/> | |||
} | |||
tooltip="Bugs" | |||
value="bugs" | |||
/> | |||
</FacetItemsList> | |||
@@ -300,6 +310,7 @@ exports[`should not display subtitles of new measures if there is none 1`] = ` | |||
size={14} | |||
/> | |||
} | |||
tooltip="component_measures.domain_overview" | |||
value="Reliability" | |||
/> | |||
<span | |||
@@ -333,6 +344,7 @@ exports[`should not display subtitles of new measures if there is none 1`] = ` | |||
Object { | |||
"metric": Object { | |||
"domain": "Reliability", | |||
"id": "1", | |||
"key": "bugs", | |||
"name": "Bugs", | |||
"type": "INT", | |||
@@ -342,6 +354,7 @@ exports[`should not display subtitles of new measures if there is none 1`] = ` | |||
} | |||
/> | |||
} | |||
tooltip="Bugs" | |||
value="bugs" | |||
/> | |||
</FacetItemsList> | |||
@@ -378,6 +391,7 @@ exports[`should not display subtitles of new measures if there is none, even on | |||
size={14} | |||
/> | |||
} | |||
tooltip="component_measures.domain_overview" | |||
value="Reliability" | |||
/> | |||
<span | |||
@@ -411,6 +425,7 @@ exports[`should not display subtitles of new measures if there is none, even on | |||
Object { | |||
"metric": Object { | |||
"domain": "Reliability", | |||
"id": "2", | |||
"key": "new_bugs", | |||
"name": "New Bugs", | |||
"type": "INT", | |||
@@ -420,6 +435,7 @@ exports[`should not display subtitles of new measures if there is none, even on | |||
} | |||
/> | |||
} | |||
tooltip="New Bugs" | |||
value="new_bugs" | |||
/> | |||
</FacetItemsList> |
@@ -15,6 +15,7 @@ exports[`should display two facets 1`] = ` | |||
"leak": "70", | |||
"metric": Object { | |||
"domain": "Coverage", | |||
"id": "1", | |||
"key": "lines_to_cover", | |||
"name": "Lines to Cover", | |||
"type": "INT", | |||
@@ -31,6 +32,7 @@ exports[`should display two facets 1`] = ` | |||
"leak": "0.0999999999999943", | |||
"metric": Object { | |||
"domain": "Coverage", | |||
"id": "2", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
@@ -61,6 +63,7 @@ exports[`should display two facets 1`] = ` | |||
"leak": "0.0", | |||
"metric": Object { | |||
"domain": "Duplications", | |||
"id": "3", | |||
"key": "duplicated_lines_density", | |||
"name": "Duplicated Lines (%)", | |||
"type": "PERCENT", |
@@ -1,58 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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. | |||
*/ | |||
/*:: import type { Measure, MeasureEnhanced } from '../../components/measure/types'; */ | |||
/*:: type ComponentIntern = { | |||
isFavorite?: boolean, | |||
isRecentlyBrowsed?: boolean, | |||
key: string, | |||
match?: string, | |||
name: string, | |||
organization?: string, | |||
project?: string, | |||
qualifier: string | |||
}; */ | |||
/*:: export type Component = ComponentIntern & { measures?: Array<Measure> }; */ | |||
/*:: export type ComponentEnhanced = ComponentIntern & { | |||
value?: ?string, | |||
leak?: ?string, | |||
measures: Array<MeasureEnhanced> | |||
}; */ | |||
/*:: export type Paging = { | |||
pageIndex: number, | |||
pageSize: number, | |||
total: number | |||
}; */ | |||
/*:: export type Period = { | |||
index: number, | |||
date: string, | |||
mode: string, | |||
parameter?: string | |||
}; */ | |||
/*:: export type Query = { | |||
metric: ?string, | |||
selected: ?string, | |||
view: string | |||
}; */ |
@@ -79,10 +79,7 @@ export function sortMeasures( | |||
]); | |||
} | |||
export function addMeasureCategories( | |||
domainName: string, | |||
measures: MeasureEnhanced[] | |||
) /*: Array<any> */ { | |||
export function addMeasureCategories(domainName: string, measures: MeasureEnhanced[]) { | |||
const categories = domains[domainName] && domains[domainName].categories; | |||
if (categories && categories.length > 0) { | |||
return [...categories, ...measures]; | |||
@@ -121,7 +118,7 @@ export const groupByDomains = memoize((measures: MeasureEnhanced[]) => { | |||
})); | |||
return sortBy(domains, [ | |||
(domain: { name: string; measure: MeasureEnhanced[] }) => { | |||
(domain: { name: string; measures: MeasureEnhanced[] }) => { | |||
const idx = KNOWN_DOMAINS.indexOf(domain.name); | |||
return idx >= 0 ? idx : KNOWN_DOMAINS.length; | |||
}, | |||
@@ -162,7 +159,7 @@ export function getBubbleMetrics(domain: string, metrics: { [key: string]: Metri | |||
x: metrics[conf.x], | |||
y: metrics[conf.y], | |||
size: metrics[conf.size], | |||
colors: conf.colors ? conf.colors.map(color => metrics[color]) : null | |||
colors: conf.colors && conf.colors.map(color => metrics[color]) | |||
}; | |||
} | |||
@@ -23,9 +23,10 @@ import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { getPeriodDate, getPeriodLabel, Period, PeriodMode } from '../../../helpers/periods'; | |||
import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { differenceInDays } from '../../../helpers/dates'; | |||
import { Period, PeriodMode } from '../../../app/types'; | |||
interface Props { | |||
period: Period; |
@@ -32,7 +32,7 @@ import { getMeasuresAndMeta } from '../../../api/measures'; | |||
import { getAllTimeMachineData, History } from '../../../api/time-machine'; | |||
import { parseDate } from '../../../helpers/dates'; | |||
import { enhanceMeasuresWithMetrics } from '../../../helpers/measures'; | |||
import { getLeakPeriod, Period } from '../../../helpers/periods'; | |||
import { getLeakPeriod } from '../../../helpers/periods'; | |||
import { get } from '../../../helpers/storage'; | |||
import { METRICS, HISTORY_METRICS_LIST } from '../utils'; | |||
import { | |||
@@ -48,7 +48,7 @@ import { | |||
} from '../../../helpers/branches'; | |||
import { fetchMetrics } from '../../../store/rootActions'; | |||
import { getMetrics, Store } from '../../../store/rootReducer'; | |||
import { BranchLike, Component, Metric, MeasureEnhanced } from '../../../app/types'; | |||
import { BranchLike, Component, Metric, MeasureEnhanced, Period } from '../../../app/types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import '../styles.css'; | |||
@@ -20,8 +20,8 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import LeakPeriodLegend from '../LeakPeriodLegend'; | |||
import { PeriodMode, Period } from '../../../../helpers/periods'; | |||
import { differenceInDays } from '../../../../helpers/dates'; | |||
import { Period, PeriodMode } from '../../../../app/types'; | |||
jest.mock('../../../../helpers/dates', () => { | |||
const dates = require.requireActual('../../../../helpers/dates'); |
@@ -33,13 +33,13 @@ import { | |||
getRatingTooltip | |||
} from '../../../helpers/measures'; | |||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import { getPeriodDate, Period } from '../../../helpers/periods'; | |||
import { getPeriodDate } from '../../../helpers/periods'; | |||
import { | |||
getComponentDrilldownUrl, | |||
getComponentIssuesUrl, | |||
getMeasureHistoryUrl | |||
} from '../../../helpers/urls'; | |||
import { Component, BranchLike, MeasureEnhanced } from '../../../app/types'; | |||
import { Component, BranchLike, MeasureEnhanced, Period } from '../../../app/types'; | |||
import { History } from '../../../api/time-machine'; | |||
import { getBranchLikeQuery } from '../../../helpers/branches'; | |||
@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
/* eslint-disable camelcase */ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectCardLeak from '../ProjectCardLeak'; |
@@ -48,7 +48,6 @@ import { | |||
SourceViewerFile | |||
} from '../../app/types'; | |||
import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches'; | |||
import { parseDate } from '../../helpers/dates'; | |||
import { translate } from '../../helpers/l10n'; | |||
import './styles.css'; | |||
@@ -28,14 +28,16 @@ import { event, select } from 'd3-selection'; | |||
import { sortBy, uniq } from 'lodash'; | |||
import Tooltip from '../controls/Tooltip'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Location } from '../../helpers/urls'; | |||
import './BubbleChart.css'; | |||
const TICKS_COUNT = 5; | |||
interface BubbleProps { | |||
interface BubbleProps<T> { | |||
color?: string; | |||
link?: string; | |||
onClick?: (link?: string) => void; | |||
link?: string | Location; | |||
onClick?: (ref?: T) => void; | |||
data?: T; | |||
r: number; | |||
scale: number; | |||
tooltip?: string | React.ReactNode; | |||
@@ -43,12 +45,12 @@ interface BubbleProps { | |||
y: number; | |||
} | |||
export class Bubble extends React.PureComponent<BubbleProps> { | |||
export class Bubble<T> extends React.PureComponent<BubbleProps<T>> { | |||
handleClick = (event: React.MouseEvent<SVGCircleElement>) => { | |||
if (this.props.onClick) { | |||
event.stopPropagation(); | |||
event.preventDefault(); | |||
this.props.onClick(this.props.link); | |||
this.props.onClick(this.props.data); | |||
} | |||
}; | |||
@@ -75,17 +77,18 @@ export class Bubble extends React.PureComponent<BubbleProps> { | |||
} | |||
} | |||
interface Item { | |||
export interface BubbleItem<T> { | |||
color?: string; | |||
key?: string; | |||
link?: any; | |||
link?: string | Location; | |||
data?: T; | |||
size: number; | |||
tooltip?: React.ReactNode; | |||
x: number; | |||
y: number; | |||
} | |||
interface Props { | |||
interface Props<T> { | |||
displayXGrid?: boolean; | |||
displayXTicks?: boolean; | |||
displayYGrid?: boolean; | |||
@@ -93,8 +96,8 @@ interface Props { | |||
formatXTick?: (tick: number) => string; | |||
formatYTick?: (tick: number) => string; | |||
height: number; | |||
items: Item[]; | |||
onBubbleClick?: (link?: string) => void; | |||
items: BubbleItem<T>[]; | |||
onBubbleClick?: (ref?: T) => void; | |||
padding?: [number, number, number, number]; | |||
sizeDomain?: [number, number]; | |||
sizeRange?: [number, number]; | |||
@@ -108,7 +111,7 @@ interface State { | |||
type Scale = ScaleLinear<number, number>; | |||
export default class BubbleChart extends React.Component<Props, State> { | |||
export default class BubbleChart<T> extends React.Component<Props<T>, State> { | |||
node: SVGSVGElement | null = null; | |||
selection: any = null; | |||
transform: any = null; | |||
@@ -122,7 +125,7 @@ export default class BubbleChart extends React.Component<Props, State> { | |||
sizeRange: [5, 45] | |||
}; | |||
constructor(props: Props) { | |||
constructor(props: Props<T>) { | |||
super(props); | |||
this.state = { transform: { x: 0, y: 0, k: 1 } }; | |||
} | |||
@@ -317,6 +320,7 @@ export default class BubbleChart extends React.Component<Props, State> { | |||
key={item.key || index} | |||
link={item.link} | |||
onClick={this.props.onBubbleClick} | |||
data={item.data} | |||
r={sizeScale(item.size)} | |||
scale={1 / transform.k} | |||
tooltip={item.tooltip} |
@@ -35,7 +35,7 @@ it('should render bubble links', () => { | |||
it('should render bubbles with click handlers', () => { | |||
const onClick = jest.fn(); | |||
const items = [{ x: 1, y: 10, size: 7, link: 'foo' }, { x: 2, y: 30, size: 5, link: 'bar' }]; | |||
const items = [{ x: 1, y: 10, size: 7, data: 'foo' }, { x: 2, y: 30, size: 5, data: 'bar' }]; | |||
const chart = mount(<BubbleChart height={100} items={items} onBubbleClick={onClick} />); | |||
chart.find(Bubble).forEach(bubble => expect(bubble).toMatchSnapshot()); | |||
}); |
@@ -130,8 +130,8 @@ exports[`should render bubble links 2`] = ` | |||
exports[`should render bubbles with click handlers 1`] = ` | |||
<Bubble | |||
data="foo" | |||
key="0" | |||
link="foo" | |||
onClick={[MockFunction]} | |||
r={45} | |||
scale={1} | |||
@@ -159,8 +159,8 @@ exports[`should render bubbles with click handlers 1`] = ` | |||
exports[`should render bubbles with click handlers 2`] = ` | |||
<Bubble | |||
data="bar" | |||
key="1" | |||
link="bar" | |||
onClick={[MockFunction]} | |||
r={33.57142857142857} | |||
scale={1} |
@@ -19,6 +19,7 @@ | |||
*/ | |||
import { getRatingTooltip as nextGetRatingTooltip, isDiffMetric } from '../../helpers/measures'; | |||
import { Metric, Measure, MeasureEnhanced } from '../../app/types'; | |||
import { getLeakPeriod } from '../../helpers/periods'; | |||
const KNOWN_RATINGS = ['sqale_rating', 'reliability_rating', 'security_rating']; | |||
@@ -37,7 +38,7 @@ export function getLeakValue(measure: Measure | undefined): string | undefined { | |||
if (!measure || !measure.periods) { | |||
return undefined; | |||
} | |||
const period = measure.periods.find(period => period.index === 1); | |||
const period = getLeakPeriod(measure.periods); | |||
return period && period.value; | |||
} | |||
@@ -87,16 +87,12 @@ export function fileFromPath(path: string | null): string | null { | |||
} | |||
} | |||
export function splitPath(path: string): { head: string; tail: string } | null { | |||
if (typeof path === 'string') { | |||
const tokens = path.split('/'); | |||
return { | |||
head: tokens.slice(0, -1).join('/'), | |||
tail: tokens[tokens.length - 1] | |||
}; | |||
} else { | |||
return null; | |||
} | |||
export function splitPath(path: string) { | |||
const tokens = path.split('/'); | |||
return { | |||
head: tokens.slice(0, -1).join('/'), | |||
tail: tokens[tokens.length - 1] | |||
}; | |||
} | |||
export function limitComponentName(str: string, limit = 30): string { |
@@ -19,32 +19,16 @@ | |||
*/ | |||
import { translate, translateWithParameters } from './l10n'; | |||
import { parseDate } from './dates'; | |||
import { Period, PeriodMode, PeriodMeasure } from '../app/types'; | |||
export enum PeriodMode { | |||
Days = 'days', | |||
Date = 'date', | |||
Version = 'version', | |||
PreviousAnalysis = 'previous_analysis', | |||
PreviousVersion = 'previous_version' | |||
} | |||
export interface Period { | |||
date: string; | |||
index: number; | |||
mode: PeriodMode; | |||
modeParam?: string; | |||
parameter?: string; | |||
} | |||
function getPeriod(periods: Period[] | undefined, index: number) { | |||
function getPeriod<T extends Period | PeriodMeasure>(periods: T[] | undefined, index: number) { | |||
if (!Array.isArray(periods)) { | |||
return undefined; | |||
} | |||
return periods.find(period => period.index === index); | |||
} | |||
export function getLeakPeriod(periods: Period[] | undefined) { | |||
export function getLeakPeriod<T extends Period | PeriodMeasure>(periods: T[] | undefined) { | |||
return getPeriod(periods, 1); | |||
} | |||