Display portfolio's children branch information and group issues by project and branchtags/9.2.0.49834
@@ -75,7 +75,8 @@ import { | |||
getBranchLikeQuery, | |||
isBranch, | |||
isMainBranch, | |||
isPullRequest | |||
isPullRequest, | |||
sortBranches | |||
} from '../../../helpers/branch-like'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import * as measures from '../../../helpers/measures'; | |||
@@ -138,6 +139,7 @@ const exposeLibraries = () => { | |||
isBranch, | |||
isMainBranch, | |||
isPullRequest, | |||
sortBranches, | |||
getStandards, | |||
renderCWECategory, | |||
renderOwaspTop10Category, |
@@ -217,7 +217,7 @@ export class CodeApp extends React.PureComponent<Props, State> { | |||
const { branchLike, component: rootComponent } = this.props; | |||
if (component.refKey) { | |||
this.props.router.push(getProjectUrl(component.refKey)); | |||
this.props.router.push(getProjectUrl(component.refKey, component.branch)); | |||
} else { | |||
this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key)); | |||
} |
@@ -26,14 +26,16 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
export function getTooltip(component: T.ComponentMeasure) { | |||
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; | |||
if (isFile && component.path) { | |||
return component.path + '\n\n' + component.key; | |||
} else { | |||
return component.name + '\n\n' + component.key; | |||
} | |||
return [component.name, component.key, component.branch].filter(s => !!s).join('\n\n'); | |||
} | |||
export function mostCommonPrefix(strings: string[]) { | |||
@@ -82,8 +84,12 @@ export default function ComponentName({ | |||
let inner = null; | |||
if (component.refKey && component.qualifier !== 'SVW') { | |||
const branch = rootComponent.qualifier === 'APP' ? component.branch : undefined; | |||
if (component.refKey && component.qualifier !== ComponentQualifier.SubPortfolio) { | |||
const branch = [ComponentQualifier.Application, ComponentQualifier.Portfolio].includes( | |||
rootComponent.qualifier as ComponentQualifier | |||
) | |||
? component.branch | |||
: undefined; | |||
inner = ( | |||
<Link className="link-with-icon" to={getProjectUrl(component.refKey, branch)}> | |||
<QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> | |||
@@ -107,7 +113,14 @@ export default function ComponentName({ | |||
); | |||
} | |||
if (rootComponent.qualifier === 'APP') { | |||
if ( | |||
[ComponentQualifier.Application, ComponentQualifier.Portfolio].includes( | |||
rootComponent.qualifier as ComponentQualifier | |||
) && | |||
[ComponentQualifier.Application, ComponentQualifier.Project].includes( | |||
component.qualifier as ComponentQualifier | |||
) | |||
) { | |||
return ( | |||
<span className="max-width-100 display-inline-flex-center"> | |||
<span className="text-ellipsis" title={getTooltip(component)}> |
@@ -17,9 +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. | |||
*/ | |||
import { intersection } from 'lodash'; | |||
import { intersection, sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; | |||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { getCodeMetrics } from '../utils'; | |||
import Component from './Component'; | |||
@@ -82,18 +83,26 @@ export class Components extends React.PureComponent<Props> { | |||
)} | |||
{components.length ? ( | |||
components.map((component, index, list) => ( | |||
sortBy( | |||
components, | |||
c => c.qualifier, | |||
c => c.name.toLowerCase(), | |||
c => c.branch?.toLowerCase() | |||
).map((component, index, list) => ( | |||
<Component | |||
branchLike={branchLike} | |||
canBePinned={canBePinned} | |||
canBrowse={true} | |||
component={component} | |||
hasBaseComponent={baseComponent !== undefined} | |||
key={component.key} | |||
key={getComponentMeasureUniqueKey(component)} | |||
metrics={metrics} | |||
previous={index > 0 ? list[index - 1] : undefined} | |||
rootComponent={rootComponent} | |||
selected={selected && component.key === selected.key} | |||
selected={ | |||
selected && | |||
getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected) | |||
} | |||
/> | |||
)) | |||
) : ( |
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponentMeasure } from '../../../../helpers/mocks/component'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import ComponentName, { getTooltip, mostCommonPrefix, Props } from '../ComponentName'; | |||
describe('#getTooltip', () => { | |||
@@ -79,7 +80,7 @@ describe('#ComponentName', () => { | |||
component: mockComponentMeasure(false, { | |||
branch: 'foo', | |||
refKey: 'src/main/ts/app', | |||
qualifier: 'TRK' | |||
qualifier: ComponentQualifier.Project | |||
}) | |||
}) | |||
).toMatchSnapshot(); | |||
@@ -88,9 +89,19 @@ describe('#ComponentName', () => { | |||
component: mockComponentMeasure(false, { | |||
branch: 'foo', | |||
refKey: 'src/main/ts/app', | |||
qualifier: 'TRK' | |||
qualifier: ComponentQualifier.Project | |||
}), | |||
rootComponent: mockComponentMeasure(false, { qualifier: 'APP' }) | |||
rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Application }) | |||
}) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallowRender({ | |||
component: mockComponentMeasure(false, { | |||
refKey: 'src/main/ts/app', | |||
qualifier: ComponentQualifier.Project | |||
}), | |||
rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Portfolio }) | |||
}) | |||
).toMatchSnapshot(); | |||
}); |
@@ -20,10 +20,16 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Components } from '../Components'; | |||
const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; | |||
const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: 'VW' }; | |||
const COMPONENT = { | |||
key: 'foo', | |||
name: 'Foo', | |||
qualifier: ComponentQualifier.Project, | |||
branch: 'develop' | |||
}; | |||
const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: ComponentQualifier.Portfolio }; | |||
const METRICS = { coverage: { id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' } }; | |||
const BRANCH = mockBranch({ name: 'feature' }); | |||
@@ -115,59 +115,34 @@ foo:src/index.tsx" | |||
exports[`#ComponentName should render correctly for files 4`] = ` | |||
<span | |||
className="max-width-100 display-inline-flex-center" | |||
> | |||
<span | |||
className="text-ellipsis" | |||
title="src/index.tsx | |||
className="max-width-100 display-inline-block text-ellipsis" | |||
title="src/index.tsx | |||
foo:src/index.tsx" | |||
> | |||
<span> | |||
<QualifierIcon | |||
qualifier="FIL" | |||
/> | |||
index.tsx | |||
</span> | |||
</span> | |||
<span | |||
className="spacer-left badge flex-1" | |||
> | |||
branches.main_branch | |||
> | |||
<span> | |||
<QualifierIcon | |||
qualifier="FIL" | |||
/> | |||
index.tsx | |||
</span> | |||
</span> | |||
`; | |||
exports[`#ComponentName should render correctly for files 5`] = ` | |||
<span | |||
className="max-width-100 display-inline-flex-center" | |||
> | |||
<span | |||
className="text-ellipsis" | |||
title="src/index.tsx | |||
className="max-width-100 display-inline-block text-ellipsis" | |||
title="src/index.tsx | |||
foo:src/index.tsx" | |||
> | |||
<span> | |||
<QualifierIcon | |||
qualifier="FIL" | |||
/> | |||
index.tsx | |||
</span> | |||
</span> | |||
<span | |||
className="text-ellipsis spacer-left" | |||
> | |||
<BranchIcon | |||
className="little-spacer-right" | |||
> | |||
<span> | |||
<QualifierIcon | |||
qualifier="FIL" | |||
/> | |||
<span | |||
className="note" | |||
> | |||
foo | |||
</span> | |||
index.tsx | |||
</span> | |||
</span> | |||
`; | |||
@@ -177,6 +152,8 @@ exports[`#ComponentName should render correctly for refs 1`] = ` | |||
className="max-width-100 display-inline-block text-ellipsis" | |||
title="Foo | |||
foo | |||
foo" | |||
> | |||
<Link | |||
@@ -212,6 +189,8 @@ exports[`#ComponentName should render correctly for refs 2`] = ` | |||
className="text-ellipsis" | |||
title="Foo | |||
foo | |||
foo" | |||
> | |||
<Link | |||
@@ -252,6 +231,47 @@ foo" | |||
</span> | |||
`; | |||
exports[`#ComponentName should render correctly for refs 3`] = ` | |||
<span | |||
className="max-width-100 display-inline-flex-center" | |||
> | |||
<span | |||
className="text-ellipsis" | |||
title="Foo | |||
foo" | |||
> | |||
<Link | |||
className="link-with-icon" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "src/main/ts/app", | |||
}, | |||
} | |||
} | |||
> | |||
<QualifierIcon | |||
qualifier="TRK" | |||
/> | |||
<span> | |||
Foo | |||
</span> | |||
</Link> | |||
</span> | |||
<span | |||
className="spacer-left badge flex-1" | |||
> | |||
branches.main_branch | |||
</span> | |||
</span> | |||
`; | |||
exports[`#getTooltip should correctly format component information 1`] = ` | |||
"src/index.tsx | |||
@@ -7,6 +7,7 @@ exports[`renders correctly 1`] = ` | |||
<ComponentsHeader | |||
baseComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -20,6 +21,7 @@ exports[`renders correctly 1`] = ` | |||
} | |||
rootComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -31,6 +33,7 @@ exports[`renders correctly 1`] = ` | |||
canBePinned={true} | |||
component={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -50,6 +53,7 @@ exports[`renders correctly 1`] = ` | |||
} | |||
rootComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -79,13 +83,14 @@ exports[`renders correctly 1`] = ` | |||
canBrowse={true} | |||
component={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
hasBaseComponent={true} | |||
key="foo" | |||
key="foo/develop" | |||
metrics={ | |||
Array [ | |||
Object { | |||
@@ -98,6 +103,7 @@ exports[`renders correctly 1`] = ` | |||
} | |||
rootComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -127,16 +133,18 @@ exports[`renders correctly for a search 1`] = ` | |||
canBrowse={true} | |||
component={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
hasBaseComponent={false} | |||
key="foo" | |||
key="foo/develop" | |||
metrics={Array []} | |||
rootComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -164,6 +172,7 @@ exports[`renders correctly for leak 1`] = ` | |||
<ComponentsHeader | |||
baseComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -177,6 +186,7 @@ exports[`renders correctly for leak 1`] = ` | |||
} | |||
rootComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -196,6 +206,7 @@ exports[`renders correctly for leak 1`] = ` | |||
canBePinned={true} | |||
component={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -215,6 +226,7 @@ exports[`renders correctly for leak 1`] = ` | |||
} | |||
rootComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
@@ -252,13 +264,14 @@ exports[`renders correctly for leak 1`] = ` | |||
canBrowse={true} | |||
component={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
hasBaseComponent={true} | |||
key="foo" | |||
key="foo/develop" | |||
metrics={ | |||
Array [ | |||
Object { | |||
@@ -271,6 +284,7 @@ exports[`renders correctly for leak 1`] = ` | |||
} | |||
rootComponent={ | |||
Object { | |||
"branch": "develop", | |||
"key": "foo", | |||
"name": "Foo", | |||
"qualifier": "TRK", |
@@ -196,31 +196,3 @@ describe('extract measure', () => { | |||
}); | |||
}); | |||
}); | |||
describe('Component classification', () => { | |||
const componentBuilder = (qual: ComponentQualifier): T.ComponentMeasure => { | |||
return { | |||
qualifier: qual, | |||
key: '1', | |||
name: 'TEST' | |||
}; | |||
}; | |||
it('should be file type', () => { | |||
[ComponentQualifier.File, ComponentQualifier.TestFile].forEach(qual => { | |||
const component = componentBuilder(qual); | |||
expect(utils.isFileType(component)).toBe(true); | |||
}); | |||
}); | |||
it('should be view type', () => { | |||
[ | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.SubPortfolio, | |||
ComponentQualifier.Application | |||
].forEach(qual => { | |||
const component = componentBuilder(qual); | |||
expect(utils.isViewType(component)).toBe(true); | |||
}); | |||
}); | |||
}); |
@@ -25,14 +25,14 @@ interface Props { | |||
canBrowse: boolean; | |||
component: T.ComponentMeasure; | |||
isLast: boolean; | |||
handleSelect: (component: string) => void; | |||
handleSelect: (component: T.ComponentMeasureIntern) => void; | |||
} | |||
export default class Breadcrumb extends React.PureComponent<Props> { | |||
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.props.handleSelect(this.props.component.key); | |||
this.props.handleSelect(this.props.component); | |||
}; | |||
render() { |
@@ -29,7 +29,7 @@ interface Props { | |||
branchLike?: BranchLike; | |||
className?: string; | |||
component: T.ComponentMeasure; | |||
handleSelect: (component: string) => void; | |||
handleSelect: (component: T.ComponentMeasureIntern) => void; | |||
rootComponent: T.ComponentMeasure; | |||
} | |||
@@ -66,7 +66,7 @@ export default class Breadcrumbs extends React.PureComponent<Props, State> { | |||
const { breadcrumbs } = this.state; | |||
if (breadcrumbs.length > 1) { | |||
const idx = this.props.backToFirst ? 0 : breadcrumbs.length - 2; | |||
this.props.handleSelect(breadcrumbs[idx].key); | |||
this.props.handleSelect(breadcrumbs[idx]); | |||
} | |||
return false; | |||
}); |
@@ -25,18 +25,20 @@ import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import PageActions from '../../../components/ui/PageActions'; | |||
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | |||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
import { RequestData } from '../../../helpers/request'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { isFile, isView } from '../../../types/component'; | |||
import { MeasurePageView } from '../../../types/measures'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { complementary } from '../config/complementary'; | |||
import FilesView from '../drilldown/FilesView'; | |||
import TreeMapView from '../drilldown/TreeMapView'; | |||
import { enhanceComponent, isFileType, isViewType, Query } from '../utils'; | |||
import { enhanceComponent, Query } from '../utils'; | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import MeasureContentHeader from './MeasureContentHeader'; | |||
import MeasureHeader from './MeasureHeader'; | |||
@@ -63,7 +65,7 @@ interface State { | |||
metric?: T.Metric; | |||
paging?: T.Paging; | |||
secondaryMeasure?: T.Measure; | |||
selected?: string; | |||
selectedComponent?: T.ComponentMeasureIntern; | |||
} | |||
export default class MeasureContent extends React.PureComponent<Props, State> { | |||
@@ -125,15 +127,21 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
measure => measure.metric !== this.props.requestedMetric.key | |||
); | |||
this.setState(({ selected }) => ({ | |||
this.setState(({ selectedComponent }) => ({ | |||
baseComponent: tree.baseComponent, | |||
components, | |||
measure, | |||
metric, | |||
paging: tree.paging, | |||
secondaryMeasure, | |||
selected: | |||
components.length > 0 && components.find(c => c.key === selected) ? selected : undefined | |||
selectedComponent: | |||
components.length > 0 && | |||
components.find( | |||
c => | |||
getComponentMeasureUniqueKey(c) === getComponentMeasureUniqueKey(selectedComponent) | |||
) | |||
? selectedComponent | |||
: undefined | |||
})); | |||
} | |||
}); | |||
@@ -223,34 +231,39 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
this.props.updateQuery({ view }); | |||
}; | |||
onOpenComponent = (componentKey: string) => { | |||
if (isViewType(this.props.rootComponent)) { | |||
const component = this.state.components.find( | |||
component => component.refKey === componentKey || component.key === componentKey | |||
onOpenComponent = (component: T.ComponentMeasureIntern) => { | |||
if (isView(this.props.rootComponent.qualifier)) { | |||
const comp = this.state.components.find( | |||
c => | |||
c.refKey === component.key || | |||
getComponentMeasureUniqueKey(c) === getComponentMeasureUniqueKey(component) | |||
); | |||
if (component && component.refKey !== undefined) { | |||
if (this.props.view === 'treemap') { | |||
this.props.router.push(getProjectUrl(componentKey)); | |||
} | |||
return; | |||
if (comp) { | |||
this.props.router.push(getProjectUrl(comp.refKey || comp.key, component.branch)); | |||
} | |||
return; | |||
} | |||
this.setState(state => ({ selected: state.baseComponent!.key })); | |||
this.updateSelected(componentKey); | |||
this.setState(state => ({ selectedComponent: state.baseComponent })); | |||
this.updateSelected(component.key); | |||
if (this.container) { | |||
this.container.focus(); | |||
} | |||
}; | |||
onSelectComponent = (componentKey: string) => { | |||
this.setState({ selected: componentKey }); | |||
onSelectComponent = (component: T.ComponentMeasureIntern) => { | |||
this.setState({ selectedComponent: component }); | |||
}; | |||
getSelectedIndex = () => { | |||
const componentKey = isFileType(this.state.baseComponent!) | |||
? this.state.baseComponent!.key | |||
: this.state.selected; | |||
const index = this.state.components.findIndex(component => component.key === componentKey); | |||
const componentKey = isFile(this.state.baseComponent?.qualifier) | |||
? getComponentMeasureUniqueKey(this.state.baseComponent) | |||
: getComponentMeasureUniqueKey(this.state.selectedComponent); | |||
const index = this.state.components.findIndex( | |||
component => getComponentMeasureUniqueKey(component) === componentKey | |||
); | |||
return index !== -1 ? index : undefined; | |||
}; | |||
@@ -281,20 +294,24 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
paging={this.state.paging} | |||
rootComponent={this.props.rootComponent} | |||
selectedIdx={selectedIdx} | |||
selectedKey={selectedIdx !== undefined ? this.state.selected : undefined} | |||
selectedComponent={ | |||
selectedIdx !== undefined | |||
? (this.state.selectedComponent as T.ComponentMeasureEnhanced) | |||
: undefined | |||
} | |||
view={view} | |||
/> | |||
); | |||
} else { | |||
return ( | |||
<TreeMapView | |||
branchLike={this.props.branchLike} | |||
components={this.state.components} | |||
handleSelect={this.onOpenComponent} | |||
metric={metric} | |||
/> | |||
); | |||
} | |||
return ( | |||
<TreeMapView | |||
branchLike={this.props.branchLike} | |||
components={this.state.components} | |||
handleSelect={this.onOpenComponent} | |||
metric={metric} | |||
/> | |||
); | |||
} | |||
render() { | |||
@@ -307,7 +324,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
const measureValue = | |||
measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value); | |||
const isFile = isFileType(baseComponent); | |||
const isFileComponent = isFile(baseComponent.qualifier); | |||
const selectedIdx = this.getSelectedIndex(); | |||
return ( | |||
@@ -330,7 +347,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
} | |||
right={ | |||
<div className="display-flex-center"> | |||
{!isFile && metric && ( | |||
{!isFileComponent && metric && ( | |||
<> | |||
<div>{translate('component_measures.view_as')}</div> | |||
<MeasureViewSelect | |||
@@ -368,7 +385,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
metric={metric} | |||
secondaryMeasure={secondaryMeasure} | |||
/> | |||
{isFile ? ( | |||
{isFileComponent ? ( | |||
<div className="measure-details-viewer"> | |||
<SourceViewer | |||
branchLike={branchLike} |
@@ -25,14 +25,9 @@ import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
import PageActions from '../../../components/ui/PageActions'; | |||
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { isFile } from '../../../types/component'; | |||
import BubbleChart from '../drilldown/BubbleChart'; | |||
import { | |||
BUBBLES_FETCH_LIMIT, | |||
enhanceComponent, | |||
getBubbleMetrics, | |||
hasFullMeasures, | |||
isFileType | |||
} from '../utils'; | |||
import { BUBBLES_FETCH_LIMIT, enhanceComponent, getBubbleMetrics, hasFullMeasures } from '../utils'; | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import LeakPeriodLegend from './LeakPeriodLegend'; | |||
import MeasureContentHeader from './MeasureContentHeader'; | |||
@@ -48,7 +43,7 @@ interface Props { | |||
onIssueChange?: (issue: T.Issue) => void; | |||
rootComponent: T.ComponentMeasure; | |||
updateLoading: (param: T.Dict<boolean>) => void; | |||
updateSelected: (component: string) => void; | |||
updateSelected: (component: T.ComponentMeasureIntern) => void; | |||
} | |||
interface State { | |||
@@ -82,7 +77,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> { | |||
fetchComponents = () => { | |||
const { branchLike, component, domain, metrics } = this.props; | |||
if (isFileType(component)) { | |||
if (isFile(component.qualifier)) { | |||
this.setState({ components: [], paging: undefined }); | |||
return; | |||
} | |||
@@ -120,7 +115,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> { | |||
const { branchLike, component, domain, metrics } = this.props; | |||
const { paging } = this.state; | |||
if (isFileType(component)) { | |||
if (isFile(component.qualifier)) { | |||
return ( | |||
<div className="measure-details-viewer"> | |||
<SourceViewer |
@@ -23,7 +23,8 @@ import { getComponentShow } from '../../../api/components'; | |||
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { isViewType, Query } from '../utils'; | |||
import { isView } from '../../../types/component'; | |||
import { Query } from '../utils'; | |||
import MeasureOverview from './MeasureOverview'; | |||
interface Props { | |||
@@ -102,12 +103,12 @@ export default class MeasureOverviewContainer extends React.PureComponent<Props, | |||
} | |||
}; | |||
updateSelected = (component: string) => { | |||
if (this.state.component && isViewType(this.state.component)) { | |||
this.props.router.push(getProjectUrl(component)); | |||
updateSelected = (component: T.ComponentMeasureIntern) => { | |||
if (this.state.component && isView(this.state.component.qualifier)) { | |||
this.props.router.push(getProjectUrl(component.refKey || component.key, component.branch)); | |||
} else { | |||
this.props.updateQuery({ | |||
selected: component !== this.props.rootComponent.key ? component : undefined | |||
selected: component.key !== this.props.rootComponent.key ? component.key : undefined | |||
}); | |||
} | |||
}; |
@@ -30,6 +30,7 @@ import { | |||
} from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | |||
import { isDefined } from '../../../helpers/types'; | |||
import { isProject } from '../../../types/component'; | |||
import { | |||
BUBBLES_FETCH_LIMIT, | |||
getBubbleMetrics, | |||
@@ -46,7 +47,7 @@ interface Props { | |||
domain: string; | |||
metrics: T.Dict<T.Metric>; | |||
paging?: T.Paging; | |||
updateSelected: (component: string) => void; | |||
updateSelected: (component: T.ComponentMeasureIntern) => void; | |||
} | |||
interface State { | |||
@@ -67,16 +68,18 @@ export default class BubbleChart extends React.PureComponent<Props, State> { | |||
}; | |||
getTooltip( | |||
componentName: string, | |||
component: T.ComponentMeasureEnhanced, | |||
values: { x: number; y: number; size: number; colors?: Array<number | undefined> }, | |||
metrics: { x: T.Metric; y: T.Metric; size: T.Metric; colors?: T.Metric[] } | |||
) { | |||
const inner = [ | |||
componentName, | |||
[component.name, isProject(component.qualifier) ? component.branch : undefined] | |||
.filter(s => !!s) | |||
.join(' / '), | |||
`${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`, | |||
`${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`, | |||
`${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}` | |||
]; | |||
].filter(s => !!s); | |||
const { colors: valuesColors } = values; | |||
const { colors: metricColors } = metrics; | |||
if (valuesColors && metricColors) { | |||
@@ -106,7 +109,7 @@ export default class BubbleChart extends React.PureComponent<Props, State> { | |||
}; | |||
handleBubbleClick = (component: T.ComponentMeasureEnhanced) => | |||
this.props.updateSelected(component.refKey || component.key); | |||
this.props.updateSelected(component); | |||
getDescription(domain: string) { | |||
const description = `component_measures.overview.${domain}.description`; | |||
@@ -144,7 +147,7 @@ export default class BubbleChart extends React.PureComponent<Props, State> { | |||
size, | |||
color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined, | |||
data: component, | |||
tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics) | |||
tooltip: this.getTooltip(component, { x, y, size, colors }, metrics) | |||
}; | |||
}) | |||
.filter(isDefined); |
@@ -23,13 +23,10 @@ import { Link } from 'react-router'; | |||
import BranchIcon from '../../../components/icons/BranchIcon'; | |||
import LinkIcon from '../../../components/icons/LinkIcon'; | |||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | |||
import { fillBranchLike } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { splitPath } from '../../../helpers/path'; | |||
import { | |||
getBranchLikeUrl, | |||
getComponentDrilldownUrlWithSelection, | |||
getProjectUrl | |||
} from '../../../helpers/urls'; | |||
import { getComponentDrilldownUrlWithSelection, getProjectUrl } from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { | |||
ComponentQualifier, | |||
@@ -67,33 +64,29 @@ export default function ComponentCell(props: ComponentCellProps) { | |||
} | |||
let path: LocationDescriptor; | |||
if (component.refKey) { | |||
if ( | |||
!isPortfolioLike(component.qualifier) && | |||
([MetricKey.releasability_rating, MetricKey.alert_status] as string[]).includes(metric.key) | |||
) { | |||
path = isApplication(component.qualifier) | |||
? getProjectUrl(component.refKey, component.branch) | |||
: getBranchLikeUrl(component.refKey, branchLike); | |||
} else if (isProject(component.qualifier) && metric.key === MetricKey.projects) { | |||
path = getBranchLikeUrl(component.refKey, branchLike); | |||
} else { | |||
path = getComponentDrilldownUrlWithSelection( | |||
component.refKey, | |||
'', | |||
metric.key, | |||
branchLike, | |||
view | |||
); | |||
} | |||
} else { | |||
path = getComponentDrilldownUrlWithSelection( | |||
rootComponent.key, | |||
component.key, | |||
metric.key, | |||
branchLike, | |||
view | |||
); | |||
const targetKey = component.refKey || rootComponent.key; | |||
const selectionKey = component.refKey ? '' : component.key; | |||
// drilldown by default | |||
path = getComponentDrilldownUrlWithSelection( | |||
targetKey, | |||
selectionKey, | |||
metric.key, | |||
component.branch ? fillBranchLike(component.branch) : branchLike, | |||
view | |||
); | |||
// This metric doesn't exist for project | |||
if (metric.key === MetricKey.projects && isProject(component.qualifier)) { | |||
path = getProjectUrl(targetKey, component.branch); | |||
} | |||
// Those metric doesn't exist for application and project | |||
if ( | |||
([MetricKey.releasability_rating, MetricKey.alert_status] as string[]).includes(metric.key) && | |||
(isApplication(component.qualifier) || isProject(component.qualifier)) | |||
) { | |||
path = getProjectUrl(targetKey, component.branch); | |||
} | |||
return ( | |||
@@ -112,7 +105,7 @@ export default function ComponentCell(props: ComponentCellProps) { | |||
<QualifierIcon className="little-spacer-right" qualifier={component.qualifier} /> | |||
{head.length > 0 && <span className="note">{head}/</span>} | |||
<span>{tail}</span> | |||
{isApplication(rootComponent.qualifier) && | |||
{(isApplication(rootComponent.qualifier) || isPortfolioLike(rootComponent.qualifier)) && | |||
(component.branch ? ( | |||
<> | |||
<BranchIcon className="spacer-left little-spacer-right" /> |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { MeasurePageView } from '../../../types/measures'; | |||
@@ -31,7 +32,7 @@ interface Props { | |||
metric: T.Metric; | |||
metrics: T.Dict<T.Metric>; | |||
rootComponent: T.ComponentMeasure; | |||
selectedComponent?: string; | |||
selectedComponent?: T.ComponentMeasureEnhanced; | |||
view: MeasurePageView; | |||
} | |||
@@ -63,8 +64,11 @@ export default function ComponentsList({ components, metric, metrics, ...props } | |||
{components.map(component => ( | |||
<ComponentsListRow | |||
component={component} | |||
isSelected={component.key === props.selectedComponent} | |||
key={component.key} | |||
isSelected={ | |||
getComponentMeasureUniqueKey(component) === | |||
getComponentMeasureUniqueKey(props.selectedComponent) | |||
} | |||
key={getComponentMeasureUniqueKey(component)} | |||
metric={metric} | |||
otherMetrics={otherMetrics} | |||
{...props} |
@@ -35,14 +35,14 @@ interface Props { | |||
components: T.ComponentMeasureEnhanced[]; | |||
defaultShowBestMeasures: boolean; | |||
fetchMore: () => void; | |||
handleSelect: (component: string) => void; | |||
handleOpen: (component: string) => void; | |||
handleSelect: (component: T.ComponentMeasureEnhanced) => void; | |||
handleOpen: (component: T.ComponentMeasureEnhanced) => void; | |||
loadingMore: boolean; | |||
metric: T.Metric; | |||
metrics: T.Dict<T.Metric>; | |||
paging?: T.Paging; | |||
rootComponent: T.ComponentMeasure; | |||
selectedKey?: string; | |||
selectedComponent?: T.ComponentMeasureEnhanced; | |||
selectedIdx?: number; | |||
view: MeasurePageView; | |||
} | |||
@@ -65,13 +65,16 @@ export default class FilesView extends React.PureComponent<Props, State> { | |||
componentDidMount() { | |||
this.attachShortcuts(); | |||
if (this.props.selectedKey !== undefined) { | |||
if (this.props.selectedComponent !== undefined) { | |||
this.scrollToElement(); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (this.props.selectedKey !== undefined && prevProps.selectedKey !== this.props.selectedKey) { | |||
if ( | |||
this.props.selectedComponent && | |||
prevProps.selectedComponent !== this.props.selectedComponent | |||
) { | |||
this.scrollToElement(); | |||
} | |||
if (prevProps.metric.key !== this.props.metric.key || prevProps.view !== this.props.view) { | |||
@@ -128,8 +131,8 @@ export default class FilesView extends React.PureComponent<Props, State> { | |||
}; | |||
openSelected = () => { | |||
if (this.props.selectedKey !== undefined) { | |||
this.props.handleOpen(this.props.selectedKey); | |||
if (this.props.selectedComponent !== undefined) { | |||
this.props.handleOpen(this.props.selectedComponent); | |||
} | |||
}; | |||
@@ -137,9 +140,9 @@ export default class FilesView extends React.PureComponent<Props, State> { | |||
const { selectedIdx } = this.props; | |||
const visibleComponents = this.getVisibleComponents(); | |||
if (selectedIdx !== undefined && selectedIdx > 0) { | |||
this.props.handleSelect(visibleComponents[selectedIdx - 1].key); | |||
this.props.handleSelect(visibleComponents[selectedIdx - 1]); | |||
} else { | |||
this.props.handleSelect(visibleComponents[visibleComponents.length - 1].key); | |||
this.props.handleSelect(visibleComponents[visibleComponents.length - 1]); | |||
} | |||
}; | |||
@@ -147,9 +150,9 @@ export default class FilesView extends React.PureComponent<Props, State> { | |||
const { selectedIdx } = this.props; | |||
const visibleComponents = this.getVisibleComponents(); | |||
if (selectedIdx !== undefined && selectedIdx < visibleComponents.length - 1) { | |||
this.props.handleSelect(visibleComponents[selectedIdx + 1].key); | |||
this.props.handleSelect(visibleComponents[selectedIdx + 1]); | |||
} else { | |||
this.props.handleSelect(visibleComponents[0].key); | |||
this.props.handleSelect(visibleComponents[0]); | |||
} | |||
}; | |||
@@ -174,7 +177,7 @@ export default class FilesView extends React.PureComponent<Props, State> { | |||
metric={this.props.metric} | |||
metrics={this.props.metrics} | |||
rootComponent={this.props.rootComponent} | |||
selectedComponent={this.props.selectedKey} | |||
selectedComponent={this.props.selectedComponent} | |||
view={this.props.view} | |||
/> | |||
{hidingBestMeasures && this.props.paging && ( |
@@ -25,6 +25,7 @@ import ColorBoxLegend from '../../../components/charts/ColorBoxLegend'; | |||
import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'; | |||
import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap'; | |||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | |||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | |||
import { isDefined } from '../../../helpers/types'; | |||
@@ -35,7 +36,7 @@ import EmptyResult from './EmptyResult'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
components: T.ComponentMeasureEnhanced[]; | |||
handleSelect: (component: string) => void; | |||
handleSelect: (component: T.ComponentMeasureIntern) => void; | |||
metric: T.Metric; | |||
} | |||
@@ -88,18 +89,19 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
color: colorValue ? (colorScale as Function)(colorValue) : undefined, | |||
gradient: !colorValue ? NA_GRADIENT : undefined, | |||
icon: <QualifierIcon fill={colors.baseFontColor} qualifier={component.qualifier} />, | |||
key: component.refKey || component.key, | |||
label: component.name, | |||
key: getComponentMeasureUniqueKey(component) ?? '', | |||
label: [component.name, component.branch].filter(s => !!s).join(' / '), | |||
size: sizeValue, | |||
measureValue: colorValue, | |||
metric, | |||
tooltip: this.getTooltip({ | |||
colorMetric: metric, | |||
colorValue, | |||
componentName: component.name, | |||
component, | |||
sizeMetric: sizeMeasure.metric, | |||
sizeValue | |||
}) | |||
}), | |||
component | |||
}; | |||
}) | |||
.filter(isDefined); | |||
@@ -134,13 +136,13 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
getTooltip = ({ | |||
colorMetric, | |||
colorValue, | |||
componentName, | |||
component, | |||
sizeMetric, | |||
sizeValue | |||
}: { | |||
colorMetric: T.Metric; | |||
colorValue?: string; | |||
componentName: string; | |||
component: T.ComponentMeasureEnhanced; | |||
sizeMetric: T.Metric; | |||
sizeValue: number; | |||
}) => { | |||
@@ -148,7 +150,7 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—'; | |||
return ( | |||
<div className="text-left"> | |||
{componentName} | |||
{[component.name, component.branch].filter(s => !!s).join(' / ')} | |||
<br /> | |||
{`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`} | |||
<br /> |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like'; | |||
import { Link } from 'react-router'; | |||
import { | |||
mockComponentMeasure, | |||
mockComponentMeasureEnhanced | |||
@@ -32,89 +32,123 @@ import ComponentCell, { ComponentCellProps } from '../ComponentCell'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
}); | |||
it.each([ | |||
[ComponentQualifier.Project, undefined], | |||
[ComponentQualifier.Project, 'develop'], | |||
[ComponentQualifier.Application, undefined], | |||
[ComponentQualifier.Application, 'develop'], | |||
[ComponentQualifier.Portfolio, undefined], | |||
[ComponentQualifier.Portfolio, 'develop'] | |||
])( | |||
'should render correctly for a "%s" root component and a component with branch "%s"', | |||
(rootComponentQualifier: ComponentQualifier, componentBranch: string | undefined) => { | |||
expect( | |||
shallowRender({ | |||
rootComponent: mockComponentMeasure(false, { qualifier: rootComponentQualifier }), | |||
component: mockComponentMeasureEnhanced({ branch: componentBranch }) | |||
}) | |||
).toMatchSnapshot(); | |||
} | |||
); | |||
it('should properly deal with key and refKey', () => { | |||
expect( | |||
shallowRender({ | |||
rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Application }) | |||
component: mockComponentMeasureEnhanced({ | |||
qualifier: ComponentQualifier.SubPortfolio, | |||
refKey: 'port-key' | |||
}) | |||
}) | |||
).toMatchSnapshot('root component is application, component is on main branch'); | |||
.find(Link) | |||
.props().to | |||
).toEqual(expect.objectContaining({ query: expect.objectContaining({ id: 'port-key' }) })); | |||
expect( | |||
shallowRender({ | |||
rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Application }), | |||
component: mockComponentMeasureEnhanced({ branch: 'develop' }) | |||
shallowRender() | |||
.find(Link) | |||
.props().to | |||
).toEqual( | |||
expect.objectContaining({ | |||
query: expect.objectContaining({ id: 'foo', selected: 'foo:src/index.tsx' }) | |||
}) | |||
).toMatchSnapshot('root component is application, component has branch'); | |||
expect( | |||
shallowRender({ component: mockComponentMeasureEnhanced({ refKey: 'project-key' }) }) | |||
).toMatchSnapshot('ref project component'); | |||
expect( | |||
shallowRender( | |||
{ | |||
component: mockComponentMeasureEnhanced({ | |||
refKey: 'project-key', | |||
qualifier: ComponentQualifier.Project | |||
}), | |||
branchLike: mockBranch() | |||
}, | |||
MetricKey.releasability_rating | |||
) | |||
).toMatchSnapshot('ref project component, releasability metric'); | |||
expect( | |||
shallowRender( | |||
{ | |||
component: mockComponentMeasureEnhanced({ | |||
refKey: 'app-key', | |||
qualifier: ComponentQualifier.Application | |||
}), | |||
branchLike: mockBranch() | |||
}, | |||
MetricKey.projects | |||
) | |||
).toMatchSnapshot('ref application component, projects'); | |||
expect( | |||
shallowRender( | |||
{ | |||
component: mockComponentMeasureEnhanced({ | |||
refKey: 'project-key', | |||
qualifier: ComponentQualifier.Project | |||
}), | |||
branchLike: mockBranch() | |||
}, | |||
MetricKey.projects | |||
) | |||
).toMatchSnapshot('ref project component, projects'); | |||
expect( | |||
shallowRender( | |||
{ | |||
component: mockComponentMeasureEnhanced({ | |||
refKey: 'app-key', | |||
qualifier: ComponentQualifier.Application | |||
}), | |||
branchLike: mockPullRequest() | |||
}, | |||
MetricKey.alert_status | |||
) | |||
).toMatchSnapshot('ref application component, alert_status metric'); | |||
expect( | |||
shallowRender( | |||
); | |||
}); | |||
it.each([ | |||
[ | |||
ComponentQualifier.File, | |||
MetricKey.bugs, | |||
expect.objectContaining({ | |||
pathname: '/component_measures', | |||
query: expect.objectContaining({ branch: 'develop' }) | |||
}) | |||
], | |||
[ | |||
ComponentQualifier.Directory, | |||
MetricKey.bugs, | |||
expect.objectContaining({ | |||
pathname: '/component_measures', | |||
query: expect.objectContaining({ branch: 'develop' }) | |||
}) | |||
], | |||
[ | |||
ComponentQualifier.Project, | |||
MetricKey.projects, | |||
expect.objectContaining({ | |||
pathname: '/dashboard', | |||
query: expect.objectContaining({ branch: 'develop' }) | |||
}) | |||
], | |||
[ | |||
ComponentQualifier.Application, | |||
MetricKey.releasability_rating, | |||
expect.objectContaining({ | |||
pathname: '/dashboard', | |||
query: expect.objectContaining({ branch: 'develop' }) | |||
}) | |||
], | |||
[ | |||
ComponentQualifier.Project, | |||
MetricKey.releasability_rating, | |||
expect.objectContaining({ | |||
pathname: '/dashboard', | |||
query: expect.objectContaining({ branch: 'develop' }) | |||
}) | |||
], | |||
[ | |||
ComponentQualifier.Application, | |||
MetricKey.alert_status, | |||
expect.objectContaining({ | |||
pathname: '/dashboard', | |||
query: expect.objectContaining({ branch: 'develop' }) | |||
}) | |||
], | |||
[ | |||
ComponentQualifier.Project, | |||
MetricKey.alert_status, | |||
expect.objectContaining({ | |||
pathname: '/dashboard', | |||
query: expect.objectContaining({ branch: 'develop' }) | |||
}) | |||
] | |||
])( | |||
'should display the proper link path for %s component qualifier and %s metric key', | |||
(componentQualifier: ComponentQualifier, metricKey: MetricKey, expectedTo: any) => { | |||
const wrapper = shallowRender( | |||
{ | |||
component: mockComponentMeasureEnhanced({ | |||
refKey: 'vw-key', | |||
qualifier: ComponentQualifier.Portfolio | |||
}), | |||
branchLike: mockPullRequest() | |||
qualifier: componentQualifier, | |||
branch: 'develop' | |||
}) | |||
}, | |||
MetricKey.alert_status | |||
) | |||
).toMatchSnapshot('ref portfolio component, alert_status metric'); | |||
expect( | |||
shallowRender({ | |||
component: mockComponentMeasureEnhanced({ | |||
key: 'svw-bar', | |||
qualifier: ComponentQualifier.SubPortfolio | |||
}) | |||
}) | |||
).toMatchSnapshot('sub-portfolio component'); | |||
}); | |||
metricKey | |||
); | |||
expect(wrapper.find(Link).props().to).toEqual(expectedTo); | |||
} | |||
); | |||
function shallowRender(overrides: Partial<ComponentCellProps> = {}, metricKey = MetricKey.bugs) { | |||
const metric = mockMetric({ key: metricKey }); |
@@ -1,6 +1,6 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
exports[`should render correctly for a "APP" root component and a component with branch "develop" 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
@@ -9,87 +9,47 @@ exports[`should render correctly: default 1`] = ` | |||
> | |||
<Link | |||
className="link-no-underline" | |||
id="component-measures-component-link-foo:src/index.tsx" | |||
id="component-measures-component-link-foo" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"branch": "develop", | |||
"id": "foo", | |||
"metric": "bugs", | |||
"selected": "foo:src/index.tsx", | |||
"selected": "foo", | |||
"view": "list", | |||
}, | |||
} | |||
} | |||
> | |||
<span | |||
title="foo:src/index.tsx" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="FIL" | |||
/> | |||
<span | |||
className="note" | |||
> | |||
src | |||
/ | |||
</span> | |||
<span> | |||
index.tsx | |||
</span> | |||
</span> | |||
</Link> | |||
</div> | |||
</td> | |||
`; | |||
exports[`should render correctly: ref application component, alert_status metric 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
> | |||
<Link | |||
className="link-no-underline" | |||
id="component-measures-component-link-foo" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "app-key", | |||
}, | |||
} | |||
} | |||
> | |||
<span | |||
className="big-spacer-right" | |||
> | |||
<LinkIcon /> | |||
</span> | |||
<span | |||
title="foo" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="APP" | |||
qualifier="TRK" | |||
/> | |||
<span> | |||
Foo | |||
</span> | |||
<BranchIcon | |||
className="spacer-left little-spacer-right" | |||
/> | |||
<span | |||
className="note" | |||
> | |||
develop | |||
</span> | |||
</span> | |||
</Link> | |||
</div> | |||
</td> | |||
`; | |||
exports[`should render correctly: ref application component, projects 1`] = ` | |||
exports[`should render correctly for a "APP" root component and a component with branch "undefined" 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
@@ -105,36 +65,36 @@ exports[`should render correctly: ref application component, projects 1`] = ` | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"branch": "branch-6.7", | |||
"id": "app-key", | |||
"metric": "projects", | |||
"id": "foo", | |||
"metric": "bugs", | |||
"selected": "foo", | |||
"view": "list", | |||
}, | |||
} | |||
} | |||
> | |||
<span | |||
className="big-spacer-right" | |||
> | |||
<LinkIcon /> | |||
</span> | |||
<span | |||
title="foo" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="APP" | |||
qualifier="TRK" | |||
/> | |||
<span> | |||
Foo | |||
</span> | |||
<span | |||
className="spacer-left badge" | |||
> | |||
branches.main_branch | |||
</span> | |||
</span> | |||
</Link> | |||
</div> | |||
</td> | |||
`; | |||
exports[`should render correctly: ref portfolio component, alert_status metric 1`] = ` | |||
exports[`should render correctly for a "TRK" root component and a component with branch "develop" 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
@@ -150,25 +110,21 @@ exports[`should render correctly: ref portfolio component, alert_status metric 1 | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"id": "vw-key", | |||
"metric": "alert_status", | |||
"pullRequest": "1001", | |||
"branch": "develop", | |||
"id": "foo", | |||
"metric": "bugs", | |||
"selected": "foo", | |||
"view": "list", | |||
}, | |||
} | |||
} | |||
> | |||
<span | |||
className="big-spacer-right" | |||
> | |||
<LinkIcon /> | |||
</span> | |||
<span | |||
title="foo" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="VW" | |||
qualifier="TRK" | |||
/> | |||
<span> | |||
Foo | |||
@@ -179,7 +135,7 @@ exports[`should render correctly: ref portfolio component, alert_status metric 1 | |||
</td> | |||
`; | |||
exports[`should render correctly: ref project component 1`] = ` | |||
exports[`should render correctly for a "TRK" root component and a component with branch "undefined" 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
@@ -195,18 +151,14 @@ exports[`should render correctly: ref project component 1`] = ` | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"id": "project-key", | |||
"id": "foo", | |||
"metric": "bugs", | |||
"selected": "foo", | |||
"view": "list", | |||
}, | |||
} | |||
} | |||
> | |||
<span | |||
className="big-spacer-right" | |||
> | |||
<LinkIcon /> | |||
</span> | |||
<span | |||
title="foo" | |||
> | |||
@@ -223,7 +175,7 @@ exports[`should render correctly: ref project component 1`] = ` | |||
</td> | |||
`; | |||
exports[`should render correctly: ref project component, projects 1`] = ` | |||
exports[`should render correctly for a "VW" root component and a component with branch "develop" 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
@@ -237,19 +189,17 @@ exports[`should render correctly: ref project component, projects 1`] = ` | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"branch": "branch-6.7", | |||
"id": "project-key", | |||
"branch": "develop", | |||
"id": "foo", | |||
"metric": "bugs", | |||
"selected": "foo", | |||
"view": "list", | |||
}, | |||
} | |||
} | |||
> | |||
<span | |||
className="big-spacer-right" | |||
> | |||
<LinkIcon /> | |||
</span> | |||
<span | |||
title="foo" | |||
> | |||
@@ -260,48 +210,13 @@ exports[`should render correctly: ref project component, projects 1`] = ` | |||
<span> | |||
Foo | |||
</span> | |||
</span> | |||
</Link> | |||
</div> | |||
</td> | |||
`; | |||
exports[`should render correctly: ref project component, releasability metric 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
> | |||
<Link | |||
className="link-no-underline" | |||
id="component-measures-component-link-foo" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": "branch-6.7", | |||
"id": "project-key", | |||
}, | |||
} | |||
} | |||
> | |||
<span | |||
className="big-spacer-right" | |||
> | |||
<LinkIcon /> | |||
</span> | |||
<span | |||
title="foo" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="TRK" | |||
<BranchIcon | |||
className="spacer-left little-spacer-right" | |||
/> | |||
<span> | |||
Foo | |||
<span | |||
className="note" | |||
> | |||
develop | |||
</span> | |||
</span> | |||
</Link> | |||
@@ -309,7 +224,7 @@ exports[`should render correctly: ref project component, releasability metric 1` | |||
</td> | |||
`; | |||
exports[`should render correctly: root component is application, component has branch 1`] = ` | |||
exports[`should render correctly for a "VW" root component and a component with branch "undefined" 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
@@ -343,13 +258,10 @@ exports[`should render correctly: root component is application, component has b | |||
<span> | |||
Foo | |||
</span> | |||
<BranchIcon | |||
className="spacer-left little-spacer-right" | |||
/> | |||
<span | |||
className="note" | |||
className="spacer-left badge" | |||
> | |||
develop | |||
branches.main_branch | |||
</span> | |||
</span> | |||
</Link> | |||
@@ -357,7 +269,7 @@ exports[`should render correctly: root component is application, component has b | |||
</td> | |||
`; | |||
exports[`should render correctly: root component is application, component is on main branch 1`] = ` | |||
exports[`should render correctly: default 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
@@ -397,51 +309,6 @@ exports[`should render correctly: root component is application, component is on | |||
<span> | |||
index.tsx | |||
</span> | |||
<span | |||
className="spacer-left badge" | |||
> | |||
branches.main_branch | |||
</span> | |||
</span> | |||
</Link> | |||
</div> | |||
</td> | |||
`; | |||
exports[`should render correctly: sub-portfolio component 1`] = ` | |||
<td | |||
className="measure-details-component-cell" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
> | |||
<Link | |||
className="link-no-underline" | |||
id="component-measures-component-link-svw-bar" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"id": "foo", | |||
"metric": "bugs", | |||
"selected": "svw-bar", | |||
"view": "list", | |||
}, | |||
} | |||
} | |||
> | |||
<span | |||
title="svw-bar" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="SVW" | |||
/> | |||
<span> | |||
Foo | |||
</span> | |||
</span> | |||
</Link> | |||
</div> |
@@ -105,20 +105,6 @@ export function enhanceComponent( | |||
return { ...component, value, leak, measures: enhancedMeasures }; | |||
} | |||
export function isFileType(component: { qualifier: string | ComponentQualifier }): boolean { | |||
return [ComponentQualifier.File, ComponentQualifier.TestFile].includes( | |||
component.qualifier as ComponentQualifier | |||
); | |||
} | |||
export function isViewType(component: T.ComponentMeasure): boolean { | |||
return [ | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.SubPortfolio, | |||
ComponentQualifier.Application | |||
].includes(component.qualifier as ComponentQualifier); | |||
} | |||
export function isSecurityReviewMetric(metricKey: MetricKey | string): boolean { | |||
return [ | |||
MetricKey.security_hotspots, |
@@ -18,9 +18,11 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import BranchIcon from '../../../components/icons/BranchIcon'; | |||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { collapsePath, limitComponentName } from '../../../helpers/path'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { getSelectedLocation } from '../utils'; | |||
interface Props { | |||
@@ -36,11 +38,22 @@ export default function ComponentBreadcrumbs({ | |||
selectedFlowIndex, | |||
selectedLocationIndex | |||
}: Props) { | |||
const displayProject = !component || !['TRK', 'BRC', 'DIR'].includes(component.qualifier); | |||
const displaySubProject = !component || !['BRC', 'DIR'].includes(component.qualifier); | |||
const displayProject = | |||
!component || | |||
![ | |||
ComponentQualifier.Project, | |||
ComponentQualifier.SubProject, | |||
ComponentQualifier.Directory | |||
].includes(component.qualifier as ComponentQualifier); | |||
const displaySubProject = | |||
!component || | |||
![ComponentQualifier.SubProject, ComponentQualifier.Directory].includes( | |||
component.qualifier as ComponentQualifier | |||
); | |||
const selectedLocation = getSelectedLocation(issue, selectedFlowIndex, selectedLocationIndex); | |||
const componentName = selectedLocation ? selectedLocation.componentName : issue.componentLongName; | |||
const projectName = [issue.projectName, issue.branch].filter(s => !!s).join(' - '); | |||
return ( | |||
<div | |||
@@ -52,8 +65,15 @@ export default function ComponentBreadcrumbs({ | |||
<QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} /> | |||
{displayProject && ( | |||
<span title={issue.projectName}> | |||
<span title={projectName}> | |||
{limitComponentName(issue.projectName)} | |||
{issue.branch && ( | |||
<> | |||
{' - '} | |||
<BranchIcon /> | |||
<span>{issue.branch}</span> | |||
</> | |||
)} | |||
<span className="slash-separator" /> | |||
</span> | |||
)} |
@@ -87,7 +87,10 @@ export default class ListItem extends React.PureComponent<Props> { | |||
render() { | |||
const { branchLike, component, issue, previousIssue } = this.props; | |||
const displayComponent = !previousIssue || previousIssue.component !== issue.component; | |||
const displayComponent = | |||
!previousIssue || | |||
previousIssue.component !== issue.component || | |||
previousIssue.branch !== issue.branch; | |||
return ( | |||
<li className="issues-workspace-list-item"> |
@@ -28,7 +28,8 @@ const baseIssue = mockIssue(false, { | |||
componentLongName: 'comp-name', | |||
componentQualifier: ComponentQualifier.File, | |||
project: 'proj', | |||
projectName: 'proj-name' | |||
projectName: 'proj-name', | |||
branch: 'test-branch' | |||
}); | |||
it('renders', () => { |
@@ -0,0 +1,51 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockIssue } from '../../../../helpers/testMocks'; | |||
import ListItem from '../ListItem'; | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<ListItem['props']> = {}) { | |||
return shallow<ListItem>( | |||
<ListItem | |||
branchLike={mockBranch()} | |||
checked={false} | |||
component={mockComponent()} | |||
issue={mockIssue()} | |||
onChange={jest.fn()} | |||
onCheck={jest.fn()} | |||
onClick={jest.fn()} | |||
onFilterChange={jest.fn()} | |||
onPopupToggle={jest.fn()} | |||
openPopup={undefined} | |||
previousIssue={mockIssue(false, { branch: 'branch-8.7' })} | |||
selected={false} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -10,9 +10,14 @@ exports[`renders 1`] = ` | |||
qualifier="FIL" | |||
/> | |||
<span | |||
title="proj-name" | |||
title="proj-name - test-branch" | |||
> | |||
proj-name | |||
- | |||
<BranchIcon /> | |||
<span> | |||
test-branch | |||
</span> | |||
<span | |||
className="slash-separator" | |||
/> | |||
@@ -35,9 +40,14 @@ exports[`renders with sub-project 1`] = ` | |||
qualifier="FIL" | |||
/> | |||
<span | |||
title="proj-name" | |||
title="proj-name - test-branch" | |||
> | |||
proj-name | |||
- | |||
<BranchIcon /> | |||
<span> | |||
test-branch | |||
</span> | |||
<span | |||
className="slash-separator" | |||
/> |
@@ -0,0 +1,115 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<li | |||
className="issues-workspace-list-item" | |||
> | |||
<div | |||
className="issues-workspace-list-component note" | |||
> | |||
<ComponentBreadcrumbs | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
} | |||
} | |||
issue={ | |||
Object { | |||
"actions": Array [], | |||
"component": "main.js", | |||
"componentLongName": "main.js", | |||
"componentQualifier": "FIL", | |||
"componentUuid": "foo1234", | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"flows": Array [], | |||
"fromHotspot": false, | |||
"key": "AVsae-CQS-9G3txfbFN2", | |||
"line": 25, | |||
"message": "Reduce the number of conditional operators (4) used in the expression", | |||
"project": "myproject", | |||
"projectKey": "foo", | |||
"projectName": "Foo", | |||
"rule": "javascript:S1067", | |||
"ruleName": "foo", | |||
"secondaryLocations": Array [], | |||
"severity": "MAJOR", | |||
"status": "OPEN", | |||
"textRange": Object { | |||
"endLine": 26, | |||
"endOffset": 15, | |||
"startLine": 25, | |||
"startOffset": 0, | |||
}, | |||
"transitions": Array [], | |||
"type": "BUG", | |||
} | |||
} | |||
/> | |||
</div> | |||
<Issue | |||
branchLike={ | |||
Object { | |||
"analysisDate": "2018-01-01", | |||
"excludedFromPurge": true, | |||
"isMain": false, | |||
"name": "branch-6.7", | |||
} | |||
} | |||
checked={false} | |||
issue={ | |||
Object { | |||
"actions": Array [], | |||
"component": "main.js", | |||
"componentLongName": "main.js", | |||
"componentQualifier": "FIL", | |||
"componentUuid": "foo1234", | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"flows": Array [], | |||
"fromHotspot": false, | |||
"key": "AVsae-CQS-9G3txfbFN2", | |||
"line": 25, | |||
"message": "Reduce the number of conditional operators (4) used in the expression", | |||
"project": "myproject", | |||
"projectKey": "foo", | |||
"projectName": "Foo", | |||
"rule": "javascript:S1067", | |||
"ruleName": "foo", | |||
"secondaryLocations": Array [], | |||
"severity": "MAJOR", | |||
"status": "OPEN", | |||
"textRange": Object { | |||
"endLine": 26, | |||
"endOffset": 15, | |||
"startLine": 25, | |||
"startOffset": 0, | |||
}, | |||
"transitions": Array [], | |||
"type": "BUG", | |||
} | |||
} | |||
onChange={[MockFunction]} | |||
onCheck={[MockFunction]} | |||
onClick={[MockFunction]} | |||
onFilter={[Function]} | |||
onPopupToggle={[MockFunction]} | |||
selected={false} | |||
/> | |||
</li> | |||
`; |
@@ -38,7 +38,7 @@ export default function Effort({ component, effort, metricKey }: Props) { | |||
defaultMessage={translate('portfolio.x_in_y')} | |||
id="portfolio.x_in_y" | |||
values={{ | |||
projects: ( | |||
project_branches: ( | |||
<Link | |||
to={getComponentDrilldownUrl({ | |||
componentKey: component, | |||
@@ -53,8 +53,8 @@ export default function Effort({ component, effort, metricKey }: Props) { | |||
value={String(effort.projects)} | |||
/> | |||
{effort.projects === 1 | |||
? translate('project_singular') | |||
: translate('project_plural')} | |||
? translate('portfolio.project_branch') | |||
: translate('portfolio.project_branches')} | |||
</span> | |||
</Link> | |||
), |
@@ -73,7 +73,7 @@ export default function MetricBox({ component, measures, metricKey }: MetricBoxP | |||
{metricKey === 'releasability' | |||
? Number(effort) > 0 && ( | |||
<> | |||
<h3>{translate('portfolio.lowest_rated_projects')}</h3> | |||
<h3>{translate('portfolio.lowest_rated_project_branches')}</h3> | |||
<div className="portfolio-effort"> | |||
<Link | |||
to={getComponentDrilldownUrl({ | |||
@@ -88,8 +88,8 @@ export default function MetricBox({ component, measures, metricKey }: MetricBoxP | |||
value={effort} | |||
/> | |||
{Number(effort) === 1 | |||
? translate('project_singular') | |||
: translate('project_plural')} | |||
? translate('portfolio.project_branch') | |||
: translate('portfolio.project_branches')} | |||
</span> | |||
</Link> | |||
<Level | |||
@@ -107,7 +107,7 @@ export default function MetricBox({ component, measures, metricKey }: MetricBoxP | |||
) | |||
: effort && ( | |||
<> | |||
<h3>{translate('portfolio.lowest_rated_projects')}</h3> | |||
<h3>{translate('portfolio.lowest_rated_project_branches')}</h3> | |||
<Effort component={component} effort={effort} metricKey={keys.rating} /> | |||
</> | |||
)} |
@@ -17,10 +17,11 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { max } from 'lodash'; | |||
import { max, sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { colors } from '../../../app/theme'; | |||
import BranchIcon from '../../../components/icons/BranchIcon'; | |||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
@@ -48,6 +49,13 @@ export default function WorstProjects({ component, subComponents, total }: Props | |||
const projectsPageUrl = { pathname: '/code', query: { id: component } }; | |||
const subCompList = sortBy( | |||
subComponents, | |||
c => c.qualifier, | |||
c => c.name.toLowerCase(), | |||
c => c.branch?.toLowerCase() | |||
); | |||
return ( | |||
<div className="panel panel-white portfolio-sub-components" id="portfolio-sub-components"> | |||
<table className="data zebra"> | |||
@@ -75,26 +83,39 @@ export default function WorstProjects({ component, subComponents, total }: Props | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{subComponents.map(component => ( | |||
<tr key={component.key}> | |||
{subCompList.map(comp => ( | |||
<tr key={[comp.key, comp.branch].filter(s => !!s).join('/')}> | |||
<td> | |||
<Link | |||
className="link-with-icon" | |||
to={getComponentOverviewUrl( | |||
component.refKey || component.key, | |||
component.qualifier | |||
)}> | |||
<QualifierIcon qualifier={component.qualifier} /> {component.name} | |||
</Link> | |||
<span className="display-flex-center"> | |||
<QualifierIcon className="spacer-right" qualifier={comp.qualifier} /> | |||
<Link | |||
to={getComponentOverviewUrl(comp.refKey || comp.key, comp.qualifier, { | |||
branch: comp.branch | |||
})}> | |||
{comp.name} | |||
</Link> | |||
{[ComponentQualifier.Application, ComponentQualifier.Project].includes( | |||
comp.qualifier as ComponentQualifier | |||
) && | |||
(comp.branch ? ( | |||
<span className="spacer-left"> | |||
<BranchIcon className="little-spacer-right" /> | |||
<span className="note">{comp.branch}</span> | |||
</span> | |||
) : ( | |||
<span className="spacer-left badge">{translate('branches.main_branch')}</span> | |||
))} | |||
</span> | |||
</td> | |||
{component.qualifier === ComponentQualifier.Project | |||
? renderCell(component.measures, 'alert_status', 'LEVEL') | |||
: renderCell(component.measures, 'releasability_rating', 'RATING')} | |||
{renderCell(component.measures, 'reliability_rating', 'RATING')} | |||
{renderCell(component.measures, 'security_rating', 'RATING')} | |||
{renderCell(component.measures, 'security_review_rating', 'RATING')} | |||
{renderCell(component.measures, 'sqale_rating', 'RATING')} | |||
{renderNcloc(component.measures, maxLoc)} | |||
{comp.qualifier === ComponentQualifier.Project | |||
? renderCell(comp.measures, 'alert_status', 'LEVEL') | |||
: renderCell(comp.measures, 'releasability_rating', 'RATING')} | |||
{renderCell(comp.measures, 'reliability_rating', 'RATING')} | |||
{renderCell(comp.measures, 'security_rating', 'RATING')} | |||
{renderCell(comp.measures, 'security_review_rating', 'RATING')} | |||
{renderCell(comp.measures, 'sqale_rating', 'RATING')} | |||
{renderNcloc(comp.measures, maxLoc)} | |||
</tr> | |||
))} | |||
</tbody> |
@@ -19,6 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import WorstProjects from '../WorstProjects'; | |||
it('renders', () => { | |||
@@ -33,7 +34,33 @@ it('renders', () => { | |||
ncloc: '200' | |||
}, | |||
name: 'Foo', | |||
qualifier: 'SVW' | |||
qualifier: ComponentQualifier.SubPortfolio | |||
}, | |||
{ | |||
key: 'foo_app', | |||
measures: { | |||
releasability_rating: '3', | |||
reliability_rating: '2', | |||
security_rating: '1', | |||
sqale_rating: '4', | |||
ncloc: '200' | |||
}, | |||
name: 'Foo', | |||
qualifier: ComponentQualifier.Application | |||
}, | |||
{ | |||
key: 'bar', | |||
measures: { | |||
alert_status: 'ERROR', | |||
reliability_rating: '2', | |||
security_rating: '1', | |||
sqale_rating: '4', | |||
ncloc: '100' | |||
}, | |||
name: 'Bar', | |||
qualifier: ComponentQualifier.Project, | |||
refKey: 'barbar', | |||
branch: 'branch-1' | |||
}, | |||
{ | |||
key: 'bar', | |||
@@ -45,7 +72,7 @@ it('renders', () => { | |||
ncloc: '100' | |||
}, | |||
name: 'Bar', | |||
qualifier: 'TRK', | |||
qualifier: ComponentQualifier.Project, | |||
refKey: 'barbar' | |||
}, | |||
{ | |||
@@ -58,7 +85,7 @@ it('renders', () => { | |||
ncloc: '150' | |||
}, | |||
name: 'Baz', | |||
qualifier: 'TRK', | |||
qualifier: ComponentQualifier.Project, | |||
refKey: 'bazbaz' | |||
} | |||
]; |
@@ -9,7 +9,7 @@ exports[`renders 1`] = ` | |||
id="portfolio.x_in_y" | |||
values={ | |||
Object { | |||
"projects": <Link | |||
"project_branches": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
@@ -30,7 +30,7 @@ exports[`renders 1`] = ` | |||
metricType="SHORT_INT" | |||
value="3" | |||
/> | |||
project_plural | |||
portfolio.project_branches | |||
</span> | |||
</Link>, | |||
"rating": <Rating |
@@ -26,7 +26,7 @@ exports[`should render correctly 1`] = ` | |||
rating="3" | |||
/> | |||
<h3> | |||
portfolio.lowest_rated_projects | |||
portfolio.lowest_rated_project_branches | |||
</h3> | |||
<Effort | |||
component="foo" | |||
@@ -84,7 +84,7 @@ exports[`should render correctly for releasability 1`] = ` | |||
rating="2" | |||
/> | |||
<h3> | |||
portfolio.lowest_rated_projects | |||
portfolio.lowest_rated_project_branches | |||
</h3> | |||
<div | |||
className="portfolio-effort" | |||
@@ -109,7 +109,7 @@ exports[`should render correctly for releasability 1`] = ` | |||
metricType="SHORT_INT" | |||
value={5} | |||
/> | |||
project_plural | |||
portfolio.project_branches | |||
</span> | |||
</Link> | |||
<Level | |||
@@ -165,7 +165,7 @@ exports[`should render correctly for releasability 2`] = ` | |||
rating="2" | |||
/> | |||
<h3> | |||
portfolio.lowest_rated_projects | |||
portfolio.lowest_rated_project_branches | |||
</h3> | |||
<div | |||
className="portfolio-effort" | |||
@@ -190,7 +190,7 @@ exports[`should render correctly for releasability 2`] = ` | |||
metricType="SHORT_INT" | |||
value={1} | |||
/> | |||
project_singular | |||
portfolio.project_branch | |||
</span> | |||
</Link> | |||
<Level |
@@ -47,28 +47,136 @@ exports[`renders 1`] = ` | |||
</thead> | |||
<tbody> | |||
<tr | |||
key="foo" | |||
key="foo_app" | |||
> | |||
<td> | |||
<Link | |||
className="link-with-icon" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/portfolio", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier="APP" | |||
/> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo_app", | |||
}, | |||
} | |||
} | |||
} | |||
> | |||
Foo | |||
</Link> | |||
<span | |||
className="spacer-left badge" | |||
> | |||
branches.main_branch | |||
</span> | |||
</span> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="releasability_rating" | |||
metricType="RATING" | |||
value="3" | |||
/> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="reliability_rating" | |||
metricType="RATING" | |||
value="2" | |||
/> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="security_rating" | |||
metricType="RATING" | |||
value="1" | |||
/> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="security_review_rating" | |||
metricType="RATING" | |||
/> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="sqale_rating" | |||
metricType="RATING" | |||
value="4" | |||
/> | |||
</td> | |||
<td | |||
className="text-right" | |||
> | |||
<span | |||
className="note" | |||
> | |||
<Measure | |||
metricKey="ncloc" | |||
metricType="SHORT_INT" | |||
value="200" | |||
/> | |||
</span> | |||
<svg | |||
className="spacer-left" | |||
height="16" | |||
width="50" | |||
> | |||
<rect | |||
className="bar-chart-bar" | |||
fill="#4b9fd5" | |||
height="10" | |||
width={50} | |||
x="0" | |||
y="3" | |||
/> | |||
</svg> | |||
</td> | |||
</tr> | |||
<tr | |||
key="foo" | |||
> | |||
<td> | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier="SVW" | |||
/> | |||
Foo | |||
</Link> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/portfolio", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
Foo | |||
</Link> | |||
</span> | |||
</td> | |||
<td | |||
className="text-center" | |||
@@ -143,28 +251,149 @@ exports[`renders 1`] = ` | |||
</td> | |||
</tr> | |||
<tr | |||
key="bar" | |||
key="bar/branch-1" | |||
> | |||
<td> | |||
<Link | |||
className="link-with-icon" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"id": "barbar", | |||
}, | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier="TRK" | |||
/> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": "branch-1", | |||
"id": "barbar", | |||
}, | |||
} | |||
} | |||
} | |||
> | |||
Bar | |||
</Link> | |||
<span | |||
className="spacer-left" | |||
> | |||
<BranchIcon | |||
className="little-spacer-right" | |||
/> | |||
<span | |||
className="note" | |||
> | |||
branch-1 | |||
</span> | |||
</span> | |||
</span> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="alert_status" | |||
metricType="LEVEL" | |||
value="ERROR" | |||
/> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="reliability_rating" | |||
metricType="RATING" | |||
value="2" | |||
/> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="security_rating" | |||
metricType="RATING" | |||
value="1" | |||
/> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="security_review_rating" | |||
metricType="RATING" | |||
/> | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Measure | |||
metricKey="sqale_rating" | |||
metricType="RATING" | |||
value="4" | |||
/> | |||
</td> | |||
<td | |||
className="text-right" | |||
> | |||
<span | |||
className="note" | |||
> | |||
<Measure | |||
metricKey="ncloc" | |||
metricType="SHORT_INT" | |||
value="100" | |||
/> | |||
</span> | |||
<svg | |||
className="spacer-left" | |||
height="16" | |||
width="50" | |||
> | |||
<rect | |||
className="bar-chart-bar" | |||
fill="#4b9fd5" | |||
height="10" | |||
width={25} | |||
x="0" | |||
y="3" | |||
/> | |||
</svg> | |||
</td> | |||
</tr> | |||
<tr | |||
key="bar" | |||
> | |||
<td> | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier="TRK" | |||
/> | |||
Bar | |||
</Link> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "barbar", | |||
}, | |||
} | |||
} | |||
> | |||
Bar | |||
</Link> | |||
<span | |||
className="spacer-left badge" | |||
> | |||
branches.main_branch | |||
</span> | |||
</span> | |||
</td> | |||
<td | |||
className="text-center" | |||
@@ -242,25 +471,34 @@ exports[`renders 1`] = ` | |||
key="baz" | |||
> | |||
<td> | |||
<Link | |||
className="link-with-icon" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"id": "bazbaz", | |||
}, | |||
} | |||
} | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier="TRK" | |||
/> | |||
Baz | |||
</Link> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "bazbaz", | |||
}, | |||
} | |||
} | |||
> | |||
Baz | |||
</Link> | |||
<span | |||
className="spacer-left badge" | |||
> | |||
branches.main_branch | |||
</span> | |||
</span> | |||
</td> | |||
<td | |||
className="text-center" |
@@ -23,4 +23,5 @@ export interface SubComponent { | |||
name: string; | |||
refKey?: string; | |||
qualifier: string; | |||
branch?: string; | |||
} |
@@ -35,6 +35,7 @@ export interface TreeMapItem { | |||
metric?: { key: string; type: string }; | |||
size: number; | |||
tooltip?: React.ReactNode; | |||
component: T.ComponentMeasureEnhanced; | |||
} | |||
interface HierarchicalTreemapItem extends TreeMapItem { | |||
@@ -44,7 +45,7 @@ interface HierarchicalTreemapItem extends TreeMapItem { | |||
interface Props { | |||
height: number; | |||
items: TreeMapItem[]; | |||
onRectangleClick?: (item: string) => void; | |||
onRectangleClick?: (item: T.ComponentMeasureEnhanced) => void; | |||
width: number; | |||
} | |||
@@ -64,6 +65,12 @@ export default class TreeMap extends React.PureComponent<Props> { | |||
return prefix.substr(0, prefix.length - lastPrefixPart.length); | |||
}; | |||
handleClick = (component: T.ComponentMeasureEnhanced) => { | |||
if (this.props.onRectangleClick) { | |||
this.props.onRectangleClick(component); | |||
} | |||
}; | |||
render() { | |||
const { items, height, width } = this.props; | |||
const hierarchy = d3Hierarchy({ children: items } as HierarchicalTreemapItem) | |||
@@ -90,7 +97,7 @@ export default class TreeMap extends React.PureComponent<Props> { | |||
key={node.data.key} | |||
label={node.data.label} | |||
link={node.data.link} | |||
onClick={this.props.onRectangleClick} | |||
onClick={() => this.handleClick(node.data.component)} | |||
placement={node.x0 === 0 || node.x1 < halfWidth ? 'right' : 'left'} | |||
prefix={prefix} | |||
value={ |
@@ -19,19 +19,33 @@ | |||
*/ | |||
import { mount } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component'; | |||
import TreeMap from '../TreeMap'; | |||
import TreeMapRect from '../TreeMapRect'; | |||
it('should render correctly', () => { | |||
const items = [ | |||
{ key: '1', size: 10, color: '#777', label: 'SonarQube :: Server' }, | |||
{ key: '2', size: 30, color: '#777', label: 'SonarQube :: Web' }, | |||
{ | |||
key: '1', | |||
size: 10, | |||
color: '#777', | |||
label: 'SonarQube :: Server', | |||
component: mockComponentMeasureEnhanced() | |||
}, | |||
{ | |||
key: '2', | |||
size: 30, | |||
color: '#777', | |||
label: 'SonarQube :: Web', | |||
component: mockComponentMeasureEnhanced() | |||
}, | |||
{ | |||
key: '3', | |||
size: 20, | |||
gradient: '#777', | |||
label: 'SonarQube :: Search', | |||
metric: { key: 'coverage', type: 'PERCENT' } | |||
metric: { key: 'coverage', type: 'PERCENT' }, | |||
component: mockComponentMeasureEnhanced() | |||
} | |||
]; | |||
const onRectClick = jest.fn(); | |||
@@ -49,5 +63,5 @@ it('should render correctly', () => { | |||
expect(event.stopPropagation).toHaveBeenCalled(); | |||
(rects.first().instance() as TreeMapRect).handleRectClick(); | |||
expect(onRectClick).toHaveBeenCalledWith('2'); | |||
expect(onRectClick).toHaveBeenCalledWith(expect.objectContaining({ key: 'foo' })); | |||
}); |
@@ -61,14 +61,21 @@ export default class SelectListListElement extends React.PureComponent<Props, St | |||
render() { | |||
return ( | |||
<li className={classNames({ 'select-list-list-disabled': this.props.disabled })}> | |||
<li | |||
className={classNames({ | |||
'select-list-list-disabled': this.props.disabled | |||
})}> | |||
<Checkbox | |||
checked={this.props.selected} | |||
className={classNames('select-list-list-checkbox', { active: this.props.active })} | |||
className={classNames('select-list-list-checkbox display-flex-center', { | |||
active: this.props.active | |||
})} | |||
disabled={this.props.disabled} | |||
loading={this.state.loading} | |||
onCheck={this.handleCheck}> | |||
<span className="little-spacer-left">{this.props.renderElement(this.props.element)}</span> | |||
<span className="little-spacer-left flex-1"> | |||
{this.props.renderElement(this.props.element)} | |||
</span> | |||
</Checkbox> | |||
</li> | |||
); |
@@ -6,13 +6,13 @@ exports[`should display a loader when checking 1`] = ` | |||
> | |||
<Checkbox | |||
checked={false} | |||
className="select-list-list-checkbox" | |||
className="select-list-list-checkbox display-flex-center" | |||
loading={false} | |||
onCheck={[Function]} | |||
thirdState={false} | |||
> | |||
<span | |||
className="little-spacer-left" | |||
className="little-spacer-left flex-1" | |||
> | |||
foo | |||
</span> | |||
@@ -26,13 +26,13 @@ exports[`should display a loader when checking 2`] = ` | |||
> | |||
<Checkbox | |||
checked={false} | |||
className="select-list-list-checkbox" | |||
className="select-list-list-checkbox display-flex-center" | |||
loading={true} | |||
onCheck={[Function]} | |||
thirdState={false} | |||
> | |||
<span | |||
className="little-spacer-left" | |||
className="little-spacer-left flex-1" | |||
> | |||
foo | |||
</span> |
@@ -20,6 +20,7 @@ | |||
import key from 'keymaster'; | |||
import * as React from 'react'; | |||
import PageActions from '../../components/ui/PageActions'; | |||
import { getComponentMeasureUniqueKey } from '../../helpers/component'; | |||
import { getWrappedDisplayName } from './utils'; | |||
export interface WithKeyboardNavigationProps { | |||
@@ -72,7 +73,12 @@ export default function withKeyboardNavigation<P>( | |||
getCurrentIndex = () => { | |||
const { selected, components = [] } = this.props; | |||
return selected ? components.findIndex(component => component.key === selected.key) : -1; | |||
return selected | |||
? components.findIndex( | |||
component => | |||
getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected) | |||
) | |||
: -1; | |||
}; | |||
skipIfFile = (handler: () => void) => { |
@@ -0,0 +1,25 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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. | |||
*/ | |||
export function getComponentMeasureUniqueKey( | |||
component?: T.ComponentMeasure | T.ComponentMeasureEnhanced | |||
) { | |||
return component ? [component.key, component.branch].filter(s => !!s).join('/') : undefined; | |||
} |
@@ -0,0 +1,71 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`[Function isApplication] should work properly 1`] = ` | |||
Object { | |||
"APP": true, | |||
"BRC": false, | |||
"DEV": false, | |||
"DIR": false, | |||
"FIL": false, | |||
"SVW": false, | |||
"TRK": false, | |||
"UTS": false, | |||
"VW": false, | |||
} | |||
`; | |||
exports[`[Function isFile] should work properly 1`] = ` | |||
Object { | |||
"APP": false, | |||
"BRC": false, | |||
"DEV": false, | |||
"DIR": false, | |||
"FIL": true, | |||
"SVW": false, | |||
"TRK": false, | |||
"UTS": true, | |||
"VW": false, | |||
} | |||
`; | |||
exports[`[Function isPortfolioLike] should work properly 1`] = ` | |||
Object { | |||
"APP": false, | |||
"BRC": false, | |||
"DEV": false, | |||
"DIR": false, | |||
"FIL": false, | |||
"SVW": true, | |||
"TRK": false, | |||
"UTS": false, | |||
"VW": true, | |||
} | |||
`; | |||
exports[`[Function isProject] should work properly 1`] = ` | |||
Object { | |||
"APP": false, | |||
"BRC": false, | |||
"DEV": false, | |||
"DIR": false, | |||
"FIL": false, | |||
"SVW": false, | |||
"TRK": true, | |||
"UTS": false, | |||
"VW": false, | |||
} | |||
`; | |||
exports[`[Function isView] should work properly 1`] = ` | |||
Object { | |||
"APP": true, | |||
"BRC": false, | |||
"DEV": false, | |||
"DIR": false, | |||
"FIL": false, | |||
"SVW": true, | |||
"TRK": false, | |||
"UTS": false, | |||
"VW": true, | |||
} | |||
`; |
@@ -0,0 +1,39 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { | |||
ComponentQualifier, | |||
isApplication, | |||
isFile, | |||
isPortfolioLike, | |||
isProject, | |||
isView | |||
} from '../component'; | |||
it.each([[isFile], [isView], [isProject], [isApplication], [isPortfolioLike]])( | |||
'%p should work properly', | |||
(utilityMethod: (componentQualifier: ComponentQualifier) => void) => { | |||
const results = Object.values(ComponentQualifier).reduce( | |||
(prev, qualifier) => ({ ...prev, [qualifier]: utilityMethod(qualifier) }), | |||
{} | |||
); | |||
expect(results).toMatchSnapshot(); | |||
} | |||
); |
@@ -79,3 +79,17 @@ export function isProject( | |||
): componentQualifier is ComponentQualifier.Project { | |||
return componentQualifier === ComponentQualifier.Project; | |||
} | |||
export function isFile(componentQualifier?: string | ComponentQualifier): boolean { | |||
return [ComponentQualifier.File, ComponentQualifier.TestFile].includes( | |||
componentQualifier as ComponentQualifier | |||
); | |||
} | |||
export function isView(componentQualifier?: string | ComponentQualifier): boolean { | |||
return [ | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.SubPortfolio, | |||
ComponentQualifier.Application | |||
].includes(componentQualifier as ComponentQualifier); | |||
} |
@@ -167,7 +167,7 @@ declare namespace T { | |||
name: string; | |||
} | |||
interface ComponentMeasureIntern { | |||
export interface ComponentMeasureIntern { | |||
branch?: string; | |||
description?: string; | |||
isFavorite?: boolean; |
@@ -150,7 +150,6 @@ project_x=Project: {0} | |||
projects=Projects | |||
projects_=project(s) | |||
x_projects_={0} project(s) | |||
project_singular=project | |||
project_plural=projects | |||
projects_management=Projects Management | |||
quality_profile=Quality Profile | |||
@@ -2441,8 +2440,8 @@ metric.profile.description=Selected Quality Profile | |||
metric.profile.name=Profile | |||
metric.profile_version.description=Selected Quality Profile version | |||
metric.profile_version.name=Profile Version | |||
metric.projects.description=Number of projects | |||
metric.projects.name=Projects | |||
metric.projects.description=Number of project branches | |||
metric.projects.name=Project branches | |||
metric.public_api.description=Public API | |||
metric.public_api.name=Public API | |||
metric.public_documented_api_density.description=Public documented classes and functions balanced by ncloc | |||
@@ -4021,7 +4020,9 @@ branch_like_navigation.tutorial_for_ci=Show me how to set up my CI | |||
#------------------------------------------------------------------------------ | |||
portfolio.has_always_been_x=has always been {rating} | |||
portfolio.was_x_y=was {rating} {date} | |||
portfolio.x_in_y={projects} in {rating} | |||
portfolio.x_in_y={project_branches} in {rating} | |||
portfolio.project_branch=project branch | |||
portfolio.project_branches=project branches | |||
portfolio.has_qg_status=Has Quality Gate Status | |||
portfolio.have_qg_status=Have Quality Gate Status | |||
portfolio.empty=This portfolio is empty. | |||
@@ -4030,13 +4031,13 @@ portfolio.not_computed=This portfolio is not yet computed. | |||
portfolio.app.empty=This application is empty. | |||
portfolio.app.no_lines_of_code=All projects in this application are empty | |||
portfolio.metric_trend=Metric trend | |||
portfolio.lowest_rated_projects=Lowest rated projects | |||
portfolio.lowest_rated_project_branches=Lowest rated project branches | |||
portfolio.health_factors=Portfolio health factors | |||
portfolio.activity_link=Activity | |||
portfolio.measures_link=Measures | |||
portfolio.language_breakdown_link=Language breakdown | |||
portfolio.breakdown=Portfolio breakdown | |||
portfolio.number_of_projects=Number of projects | |||
portfolio.number_of_projects=Number of project branches | |||
portfolio.number_of_lines=Number of lines of code | |||
portfolio.metric_domain.vulnerabilities=Security Vulnerabilities |
@@ -229,8 +229,8 @@ public final class CoreMetrics { | |||
/** | |||
* @since 3.0 | |||
*/ | |||
public static final Metric<Integer> PROJECTS = new Metric.Builder(PROJECTS_KEY, "Projects", Metric.ValueType.INT) | |||
.setDescription("Number of projects") | |||
public static final Metric<Integer> PROJECTS = new Metric.Builder(PROJECTS_KEY, "Project branches", Metric.ValueType.INT) | |||
.setDescription("Number of project branches") | |||
.setDirection(Metric.DIRECTION_WORST) | |||
.setQualitative(false) | |||
.setDomain(DOMAIN_SIZE) |