Display portfolio's children branch information and group issues by project and branchtags/9.2.0.49834
getBranchLikeQuery, | getBranchLikeQuery, | ||||
isBranch, | isBranch, | ||||
isMainBranch, | isMainBranch, | ||||
isPullRequest | |||||
isPullRequest, | |||||
sortBranches | |||||
} from '../../../helpers/branch-like'; | } from '../../../helpers/branch-like'; | ||||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | import { translate, translateWithParameters } from '../../../helpers/l10n'; | ||||
import * as measures from '../../../helpers/measures'; | import * as measures from '../../../helpers/measures'; | ||||
isBranch, | isBranch, | ||||
isMainBranch, | isMainBranch, | ||||
isPullRequest, | isPullRequest, | ||||
sortBranches, | |||||
getStandards, | getStandards, | ||||
renderCWECategory, | renderCWECategory, | ||||
renderOwaspTop10Category, | renderOwaspTop10Category, |
const { branchLike, component: rootComponent } = this.props; | const { branchLike, component: rootComponent } = this.props; | ||||
if (component.refKey) { | if (component.refKey) { | ||||
this.props.router.push(getProjectUrl(component.refKey)); | |||||
this.props.router.push(getProjectUrl(component.refKey, component.branch)); | |||||
} else { | } else { | ||||
this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key)); | this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key)); | ||||
} | } |
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { getProjectUrl } from '../../../helpers/urls'; | import { getProjectUrl } from '../../../helpers/urls'; | ||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { ComponentQualifier } from '../../../types/component'; | |||||
export function getTooltip(component: T.ComponentMeasure) { | export function getTooltip(component: T.ComponentMeasure) { | ||||
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; | const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; | ||||
if (isFile && component.path) { | if (isFile && component.path) { | ||||
return component.path + '\n\n' + component.key; | 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[]) { | export function mostCommonPrefix(strings: string[]) { | ||||
let inner = null; | 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 = ( | inner = ( | ||||
<Link className="link-with-icon" to={getProjectUrl(component.refKey, branch)}> | <Link className="link-with-icon" to={getProjectUrl(component.refKey, branch)}> | ||||
<QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> | <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> | ||||
); | ); | ||||
} | } | ||||
if (rootComponent.qualifier === 'APP') { | |||||
if ( | |||||
[ComponentQualifier.Application, ComponentQualifier.Portfolio].includes( | |||||
rootComponent.qualifier as ComponentQualifier | |||||
) && | |||||
[ComponentQualifier.Application, ComponentQualifier.Project].includes( | |||||
component.qualifier as ComponentQualifier | |||||
) | |||||
) { | |||||
return ( | return ( | ||||
<span className="max-width-100 display-inline-flex-center"> | <span className="max-width-100 display-inline-flex-center"> | ||||
<span className="text-ellipsis" title={getTooltip(component)}> | <span className="text-ellipsis" title={getTooltip(component)}> |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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 * as React from 'react'; | ||||
import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; | import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; | ||||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { getCodeMetrics } from '../utils'; | import { getCodeMetrics } from '../utils'; | ||||
import Component from './Component'; | import Component from './Component'; | ||||
)} | )} | ||||
{components.length ? ( | {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 | <Component | ||||
branchLike={branchLike} | branchLike={branchLike} | ||||
canBePinned={canBePinned} | canBePinned={canBePinned} | ||||
canBrowse={true} | canBrowse={true} | ||||
component={component} | component={component} | ||||
hasBaseComponent={baseComponent !== undefined} | hasBaseComponent={baseComponent !== undefined} | ||||
key={component.key} | |||||
key={getComponentMeasureUniqueKey(component)} | |||||
metrics={metrics} | metrics={metrics} | ||||
previous={index > 0 ? list[index - 1] : undefined} | previous={index > 0 ? list[index - 1] : undefined} | ||||
rootComponent={rootComponent} | rootComponent={rootComponent} | ||||
selected={selected && component.key === selected.key} | |||||
selected={ | |||||
selected && | |||||
getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected) | |||||
} | |||||
/> | /> | ||||
)) | )) | ||||
) : ( | ) : ( |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; | import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; | ||||
import { mockComponentMeasure } from '../../../../helpers/mocks/component'; | import { mockComponentMeasure } from '../../../../helpers/mocks/component'; | ||||
import { ComponentQualifier } from '../../../../types/component'; | |||||
import ComponentName, { getTooltip, mostCommonPrefix, Props } from '../ComponentName'; | import ComponentName, { getTooltip, mostCommonPrefix, Props } from '../ComponentName'; | ||||
describe('#getTooltip', () => { | describe('#getTooltip', () => { | ||||
component: mockComponentMeasure(false, { | component: mockComponentMeasure(false, { | ||||
branch: 'foo', | branch: 'foo', | ||||
refKey: 'src/main/ts/app', | refKey: 'src/main/ts/app', | ||||
qualifier: 'TRK' | |||||
qualifier: ComponentQualifier.Project | |||||
}) | }) | ||||
}) | }) | ||||
).toMatchSnapshot(); | ).toMatchSnapshot(); | ||||
component: mockComponentMeasure(false, { | component: mockComponentMeasure(false, { | ||||
branch: 'foo', | branch: 'foo', | ||||
refKey: 'src/main/ts/app', | 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(); | ).toMatchSnapshot(); | ||||
}); | }); |
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | import { mockBranch } from '../../../../helpers/mocks/branch-like'; | ||||
import { ComponentQualifier } from '../../../../types/component'; | |||||
import { Components } from '../Components'; | 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 METRICS = { coverage: { id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' } }; | ||||
const BRANCH = mockBranch({ name: 'feature' }); | const BRANCH = mockBranch({ name: 'feature' }); | ||||
exports[`#ComponentName should render correctly for files 4`] = ` | exports[`#ComponentName should render correctly for files 4`] = ` | ||||
<span | <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" | 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> | ||||
</span> | </span> | ||||
`; | `; | ||||
exports[`#ComponentName should render correctly for files 5`] = ` | exports[`#ComponentName should render correctly for files 5`] = ` | ||||
<span | <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" | 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> | ||||
</span> | </span> | ||||
`; | `; | ||||
className="max-width-100 display-inline-block text-ellipsis" | className="max-width-100 display-inline-block text-ellipsis" | ||||
title="Foo | title="Foo | ||||
foo | |||||
foo" | foo" | ||||
> | > | ||||
<Link | <Link | ||||
className="text-ellipsis" | className="text-ellipsis" | ||||
title="Foo | title="Foo | ||||
foo | |||||
foo" | foo" | ||||
> | > | ||||
<Link | <Link | ||||
</span> | </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`] = ` | exports[`#getTooltip should correctly format component information 1`] = ` | ||||
"src/index.tsx | "src/index.tsx | ||||
<ComponentsHeader | <ComponentsHeader | ||||
baseComponent={ | baseComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
} | } | ||||
rootComponent={ | rootComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
canBePinned={true} | canBePinned={true} | ||||
component={ | component={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
} | } | ||||
rootComponent={ | rootComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
canBrowse={true} | canBrowse={true} | ||||
component={ | component={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
} | } | ||||
} | } | ||||
hasBaseComponent={true} | hasBaseComponent={true} | ||||
key="foo" | |||||
key="foo/develop" | |||||
metrics={ | metrics={ | ||||
Array [ | Array [ | ||||
Object { | Object { | ||||
} | } | ||||
rootComponent={ | rootComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
canBrowse={true} | canBrowse={true} | ||||
component={ | component={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
} | } | ||||
} | } | ||||
hasBaseComponent={false} | hasBaseComponent={false} | ||||
key="foo" | |||||
key="foo/develop" | |||||
metrics={Array []} | metrics={Array []} | ||||
rootComponent={ | rootComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
<ComponentsHeader | <ComponentsHeader | ||||
baseComponent={ | baseComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
} | } | ||||
rootComponent={ | rootComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
canBePinned={true} | canBePinned={true} | ||||
component={ | component={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
} | } | ||||
rootComponent={ | rootComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
canBrowse={true} | canBrowse={true} | ||||
component={ | component={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", | ||||
} | } | ||||
} | } | ||||
hasBaseComponent={true} | hasBaseComponent={true} | ||||
key="foo" | |||||
key="foo/develop" | |||||
metrics={ | metrics={ | ||||
Array [ | Array [ | ||||
Object { | Object { | ||||
} | } | ||||
rootComponent={ | rootComponent={ | ||||
Object { | Object { | ||||
"branch": "develop", | |||||
"key": "foo", | "key": "foo", | ||||
"name": "Foo", | "name": "Foo", | ||||
"qualifier": "TRK", | "qualifier": "TRK", |
}); | }); | ||||
}); | }); | ||||
}); | }); | ||||
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); | |||||
}); | |||||
}); | |||||
}); |
canBrowse: boolean; | canBrowse: boolean; | ||||
component: T.ComponentMeasure; | component: T.ComponentMeasure; | ||||
isLast: boolean; | isLast: boolean; | ||||
handleSelect: (component: string) => void; | |||||
handleSelect: (component: T.ComponentMeasureIntern) => void; | |||||
} | } | ||||
export default class Breadcrumb extends React.PureComponent<Props> { | export default class Breadcrumb extends React.PureComponent<Props> { | ||||
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { | handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { | ||||
event.preventDefault(); | event.preventDefault(); | ||||
event.currentTarget.blur(); | event.currentTarget.blur(); | ||||
this.props.handleSelect(this.props.component.key); | |||||
this.props.handleSelect(this.props.component); | |||||
}; | }; | ||||
render() { | render() { |
branchLike?: BranchLike; | branchLike?: BranchLike; | ||||
className?: string; | className?: string; | ||||
component: T.ComponentMeasure; | component: T.ComponentMeasure; | ||||
handleSelect: (component: string) => void; | |||||
handleSelect: (component: T.ComponentMeasureIntern) => void; | |||||
rootComponent: T.ComponentMeasure; | rootComponent: T.ComponentMeasure; | ||||
} | } | ||||
const { breadcrumbs } = this.state; | const { breadcrumbs } = this.state; | ||||
if (breadcrumbs.length > 1) { | if (breadcrumbs.length > 1) { | ||||
const idx = this.props.backToFirst ? 0 : breadcrumbs.length - 2; | const idx = this.props.backToFirst ? 0 : breadcrumbs.length - 2; | ||||
this.props.handleSelect(breadcrumbs[idx].key); | |||||
this.props.handleSelect(breadcrumbs[idx]); | |||||
} | } | ||||
return false; | return false; | ||||
}); | }); |
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | ||||
import PageActions from '../../../components/ui/PageActions'; | import PageActions from '../../../components/ui/PageActions'; | ||||
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | ||||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { isDiffMetric } from '../../../helpers/measures'; | import { isDiffMetric } from '../../../helpers/measures'; | ||||
import { RequestData } from '../../../helpers/request'; | import { RequestData } from '../../../helpers/request'; | ||||
import { scrollToElement } from '../../../helpers/scrolling'; | import { scrollToElement } from '../../../helpers/scrolling'; | ||||
import { getProjectUrl } from '../../../helpers/urls'; | import { getProjectUrl } from '../../../helpers/urls'; | ||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { isFile, isView } from '../../../types/component'; | |||||
import { MeasurePageView } from '../../../types/measures'; | import { MeasurePageView } from '../../../types/measures'; | ||||
import { MetricKey } from '../../../types/metrics'; | import { MetricKey } from '../../../types/metrics'; | ||||
import { complementary } from '../config/complementary'; | import { complementary } from '../config/complementary'; | ||||
import FilesView from '../drilldown/FilesView'; | import FilesView from '../drilldown/FilesView'; | ||||
import TreeMapView from '../drilldown/TreeMapView'; | import TreeMapView from '../drilldown/TreeMapView'; | ||||
import { enhanceComponent, isFileType, isViewType, Query } from '../utils'; | |||||
import { enhanceComponent, Query } from '../utils'; | |||||
import Breadcrumbs from './Breadcrumbs'; | import Breadcrumbs from './Breadcrumbs'; | ||||
import MeasureContentHeader from './MeasureContentHeader'; | import MeasureContentHeader from './MeasureContentHeader'; | ||||
import MeasureHeader from './MeasureHeader'; | import MeasureHeader from './MeasureHeader'; | ||||
metric?: T.Metric; | metric?: T.Metric; | ||||
paging?: T.Paging; | paging?: T.Paging; | ||||
secondaryMeasure?: T.Measure; | secondaryMeasure?: T.Measure; | ||||
selected?: string; | |||||
selectedComponent?: T.ComponentMeasureIntern; | |||||
} | } | ||||
export default class MeasureContent extends React.PureComponent<Props, State> { | export default class MeasureContent extends React.PureComponent<Props, State> { | ||||
measure => measure.metric !== this.props.requestedMetric.key | measure => measure.metric !== this.props.requestedMetric.key | ||||
); | ); | ||||
this.setState(({ selected }) => ({ | |||||
this.setState(({ selectedComponent }) => ({ | |||||
baseComponent: tree.baseComponent, | baseComponent: tree.baseComponent, | ||||
components, | components, | ||||
measure, | measure, | ||||
metric, | metric, | ||||
paging: tree.paging, | paging: tree.paging, | ||||
secondaryMeasure, | 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 | |||||
})); | })); | ||||
} | } | ||||
}); | }); | ||||
this.props.updateQuery({ view }); | 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) { | if (this.container) { | ||||
this.container.focus(); | this.container.focus(); | ||||
} | } | ||||
}; | }; | ||||
onSelectComponent = (componentKey: string) => { | |||||
this.setState({ selected: componentKey }); | |||||
onSelectComponent = (component: T.ComponentMeasureIntern) => { | |||||
this.setState({ selectedComponent: component }); | |||||
}; | }; | ||||
getSelectedIndex = () => { | 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; | return index !== -1 ? index : undefined; | ||||
}; | }; | ||||
paging={this.state.paging} | paging={this.state.paging} | ||||
rootComponent={this.props.rootComponent} | rootComponent={this.props.rootComponent} | ||||
selectedIdx={selectedIdx} | selectedIdx={selectedIdx} | ||||
selectedKey={selectedIdx !== undefined ? this.state.selected : undefined} | |||||
selectedComponent={ | |||||
selectedIdx !== undefined | |||||
? (this.state.selectedComponent as T.ComponentMeasureEnhanced) | |||||
: undefined | |||||
} | |||||
view={view} | 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() { | render() { | ||||
const measureValue = | const measureValue = | ||||
measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value); | measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value); | ||||
const isFile = isFileType(baseComponent); | |||||
const isFileComponent = isFile(baseComponent.qualifier); | |||||
const selectedIdx = this.getSelectedIndex(); | const selectedIdx = this.getSelectedIndex(); | ||||
return ( | return ( | ||||
} | } | ||||
right={ | right={ | ||||
<div className="display-flex-center"> | <div className="display-flex-center"> | ||||
{!isFile && metric && ( | |||||
{!isFileComponent && metric && ( | |||||
<> | <> | ||||
<div>{translate('component_measures.view_as')}</div> | <div>{translate('component_measures.view_as')}</div> | ||||
<MeasureViewSelect | <MeasureViewSelect | ||||
metric={metric} | metric={metric} | ||||
secondaryMeasure={secondaryMeasure} | secondaryMeasure={secondaryMeasure} | ||||
/> | /> | ||||
{isFile ? ( | |||||
{isFileComponent ? ( | |||||
<div className="measure-details-viewer"> | <div className="measure-details-viewer"> | ||||
<SourceViewer | <SourceViewer | ||||
branchLike={branchLike} | branchLike={branchLike} |
import PageActions from '../../../components/ui/PageActions'; | import PageActions from '../../../components/ui/PageActions'; | ||||
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | ||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { isFile } from '../../../types/component'; | |||||
import BubbleChart from '../drilldown/BubbleChart'; | 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 Breadcrumbs from './Breadcrumbs'; | ||||
import LeakPeriodLegend from './LeakPeriodLegend'; | import LeakPeriodLegend from './LeakPeriodLegend'; | ||||
import MeasureContentHeader from './MeasureContentHeader'; | import MeasureContentHeader from './MeasureContentHeader'; | ||||
onIssueChange?: (issue: T.Issue) => void; | onIssueChange?: (issue: T.Issue) => void; | ||||
rootComponent: T.ComponentMeasure; | rootComponent: T.ComponentMeasure; | ||||
updateLoading: (param: T.Dict<boolean>) => void; | updateLoading: (param: T.Dict<boolean>) => void; | ||||
updateSelected: (component: string) => void; | |||||
updateSelected: (component: T.ComponentMeasureIntern) => void; | |||||
} | } | ||||
interface State { | interface State { | ||||
fetchComponents = () => { | fetchComponents = () => { | ||||
const { branchLike, component, domain, metrics } = this.props; | const { branchLike, component, domain, metrics } = this.props; | ||||
if (isFileType(component)) { | |||||
if (isFile(component.qualifier)) { | |||||
this.setState({ components: [], paging: undefined }); | this.setState({ components: [], paging: undefined }); | ||||
return; | return; | ||||
} | } | ||||
const { branchLike, component, domain, metrics } = this.props; | const { branchLike, component, domain, metrics } = this.props; | ||||
const { paging } = this.state; | const { paging } = this.state; | ||||
if (isFileType(component)) { | |||||
if (isFile(component.qualifier)) { | |||||
return ( | return ( | ||||
<div className="measure-details-viewer"> | <div className="measure-details-viewer"> | ||||
<SourceViewer | <SourceViewer |
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | ||||
import { getProjectUrl } from '../../../helpers/urls'; | import { getProjectUrl } from '../../../helpers/urls'; | ||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { isViewType, Query } from '../utils'; | |||||
import { isView } from '../../../types/component'; | |||||
import { Query } from '../utils'; | |||||
import MeasureOverview from './MeasureOverview'; | import MeasureOverview from './MeasureOverview'; | ||||
interface Props { | interface 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 { | } else { | ||||
this.props.updateQuery({ | this.props.updateQuery({ | ||||
selected: component !== this.props.rootComponent.key ? component : undefined | |||||
selected: component.key !== this.props.rootComponent.key ? component.key : undefined | |||||
}); | }); | ||||
} | } | ||||
}; | }; |
} from '../../../helpers/l10n'; | } from '../../../helpers/l10n'; | ||||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | ||||
import { isDefined } from '../../../helpers/types'; | import { isDefined } from '../../../helpers/types'; | ||||
import { isProject } from '../../../types/component'; | |||||
import { | import { | ||||
BUBBLES_FETCH_LIMIT, | BUBBLES_FETCH_LIMIT, | ||||
getBubbleMetrics, | getBubbleMetrics, | ||||
domain: string; | domain: string; | ||||
metrics: T.Dict<T.Metric>; | metrics: T.Dict<T.Metric>; | ||||
paging?: T.Paging; | paging?: T.Paging; | ||||
updateSelected: (component: string) => void; | |||||
updateSelected: (component: T.ComponentMeasureIntern) => void; | |||||
} | } | ||||
interface State { | interface State { | ||||
}; | }; | ||||
getTooltip( | getTooltip( | ||||
componentName: string, | |||||
component: T.ComponentMeasureEnhanced, | |||||
values: { x: number; y: number; size: number; colors?: Array<number | undefined> }, | values: { x: number; y: number; size: number; colors?: Array<number | undefined> }, | ||||
metrics: { x: T.Metric; y: T.Metric; size: T.Metric; colors?: T.Metric[] } | metrics: { x: T.Metric; y: T.Metric; size: T.Metric; colors?: T.Metric[] } | ||||
) { | ) { | ||||
const inner = [ | 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.x.name}: ${formatMeasure(values.x, metrics.x.type)}`, | ||||
`${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`, | `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`, | ||||
`${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}` | `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}` | ||||
]; | |||||
].filter(s => !!s); | |||||
const { colors: valuesColors } = values; | const { colors: valuesColors } = values; | ||||
const { colors: metricColors } = metrics; | const { colors: metricColors } = metrics; | ||||
if (valuesColors && metricColors) { | if (valuesColors && metricColors) { | ||||
}; | }; | ||||
handleBubbleClick = (component: T.ComponentMeasureEnhanced) => | handleBubbleClick = (component: T.ComponentMeasureEnhanced) => | ||||
this.props.updateSelected(component.refKey || component.key); | |||||
this.props.updateSelected(component); | |||||
getDescription(domain: string) { | getDescription(domain: string) { | ||||
const description = `component_measures.overview.${domain}.description`; | const description = `component_measures.overview.${domain}.description`; | ||||
size, | size, | ||||
color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined, | color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined, | ||||
data: component, | data: component, | ||||
tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics) | |||||
tooltip: this.getTooltip(component, { x, y, size, colors }, metrics) | |||||
}; | }; | ||||
}) | }) | ||||
.filter(isDefined); | .filter(isDefined); |
import BranchIcon from '../../../components/icons/BranchIcon'; | import BranchIcon from '../../../components/icons/BranchIcon'; | ||||
import LinkIcon from '../../../components/icons/LinkIcon'; | import LinkIcon from '../../../components/icons/LinkIcon'; | ||||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | import QualifierIcon from '../../../components/icons/QualifierIcon'; | ||||
import { fillBranchLike } from '../../../helpers/branch-like'; | |||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { splitPath } from '../../../helpers/path'; | 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 { BranchLike } from '../../../types/branch-like'; | ||||
import { | import { | ||||
ComponentQualifier, | ComponentQualifier, | ||||
} | } | ||||
let path: LocationDescriptor; | 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 ( | return ( | ||||
<QualifierIcon className="little-spacer-right" qualifier={component.qualifier} /> | <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} /> | ||||
{head.length > 0 && <span className="note">{head}/</span>} | {head.length > 0 && <span className="note">{head}/</span>} | ||||
<span>{tail}</span> | <span>{tail}</span> | ||||
{isApplication(rootComponent.qualifier) && | |||||
{(isApplication(rootComponent.qualifier) || isPortfolioLike(rootComponent.qualifier)) && | |||||
(component.branch ? ( | (component.branch ? ( | ||||
<> | <> | ||||
<BranchIcon className="spacer-left little-spacer-right" /> | <BranchIcon className="spacer-left little-spacer-right" /> |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | import { getLocalizedMetricName } from '../../../helpers/l10n'; | ||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { MeasurePageView } from '../../../types/measures'; | import { MeasurePageView } from '../../../types/measures'; | ||||
metric: T.Metric; | metric: T.Metric; | ||||
metrics: T.Dict<T.Metric>; | metrics: T.Dict<T.Metric>; | ||||
rootComponent: T.ComponentMeasure; | rootComponent: T.ComponentMeasure; | ||||
selectedComponent?: string; | |||||
selectedComponent?: T.ComponentMeasureEnhanced; | |||||
view: MeasurePageView; | view: MeasurePageView; | ||||
} | } | ||||
{components.map(component => ( | {components.map(component => ( | ||||
<ComponentsListRow | <ComponentsListRow | ||||
component={component} | component={component} | ||||
isSelected={component.key === props.selectedComponent} | |||||
key={component.key} | |||||
isSelected={ | |||||
getComponentMeasureUniqueKey(component) === | |||||
getComponentMeasureUniqueKey(props.selectedComponent) | |||||
} | |||||
key={getComponentMeasureUniqueKey(component)} | |||||
metric={metric} | metric={metric} | ||||
otherMetrics={otherMetrics} | otherMetrics={otherMetrics} | ||||
{...props} | {...props} |
components: T.ComponentMeasureEnhanced[]; | components: T.ComponentMeasureEnhanced[]; | ||||
defaultShowBestMeasures: boolean; | defaultShowBestMeasures: boolean; | ||||
fetchMore: () => void; | fetchMore: () => void; | ||||
handleSelect: (component: string) => void; | |||||
handleOpen: (component: string) => void; | |||||
handleSelect: (component: T.ComponentMeasureEnhanced) => void; | |||||
handleOpen: (component: T.ComponentMeasureEnhanced) => void; | |||||
loadingMore: boolean; | loadingMore: boolean; | ||||
metric: T.Metric; | metric: T.Metric; | ||||
metrics: T.Dict<T.Metric>; | metrics: T.Dict<T.Metric>; | ||||
paging?: T.Paging; | paging?: T.Paging; | ||||
rootComponent: T.ComponentMeasure; | rootComponent: T.ComponentMeasure; | ||||
selectedKey?: string; | |||||
selectedComponent?: T.ComponentMeasureEnhanced; | |||||
selectedIdx?: number; | selectedIdx?: number; | ||||
view: MeasurePageView; | view: MeasurePageView; | ||||
} | } | ||||
componentDidMount() { | componentDidMount() { | ||||
this.attachShortcuts(); | this.attachShortcuts(); | ||||
if (this.props.selectedKey !== undefined) { | |||||
if (this.props.selectedComponent !== undefined) { | |||||
this.scrollToElement(); | this.scrollToElement(); | ||||
} | } | ||||
} | } | ||||
componentDidUpdate(prevProps: Props) { | componentDidUpdate(prevProps: Props) { | ||||
if (this.props.selectedKey !== undefined && prevProps.selectedKey !== this.props.selectedKey) { | |||||
if ( | |||||
this.props.selectedComponent && | |||||
prevProps.selectedComponent !== this.props.selectedComponent | |||||
) { | |||||
this.scrollToElement(); | this.scrollToElement(); | ||||
} | } | ||||
if (prevProps.metric.key !== this.props.metric.key || prevProps.view !== this.props.view) { | if (prevProps.metric.key !== this.props.metric.key || prevProps.view !== this.props.view) { | ||||
}; | }; | ||||
openSelected = () => { | openSelected = () => { | ||||
if (this.props.selectedKey !== undefined) { | |||||
this.props.handleOpen(this.props.selectedKey); | |||||
if (this.props.selectedComponent !== undefined) { | |||||
this.props.handleOpen(this.props.selectedComponent); | |||||
} | } | ||||
}; | }; | ||||
const { selectedIdx } = this.props; | const { selectedIdx } = this.props; | ||||
const visibleComponents = this.getVisibleComponents(); | const visibleComponents = this.getVisibleComponents(); | ||||
if (selectedIdx !== undefined && selectedIdx > 0) { | if (selectedIdx !== undefined && selectedIdx > 0) { | ||||
this.props.handleSelect(visibleComponents[selectedIdx - 1].key); | |||||
this.props.handleSelect(visibleComponents[selectedIdx - 1]); | |||||
} else { | } else { | ||||
this.props.handleSelect(visibleComponents[visibleComponents.length - 1].key); | |||||
this.props.handleSelect(visibleComponents[visibleComponents.length - 1]); | |||||
} | } | ||||
}; | }; | ||||
const { selectedIdx } = this.props; | const { selectedIdx } = this.props; | ||||
const visibleComponents = this.getVisibleComponents(); | const visibleComponents = this.getVisibleComponents(); | ||||
if (selectedIdx !== undefined && selectedIdx < visibleComponents.length - 1) { | if (selectedIdx !== undefined && selectedIdx < visibleComponents.length - 1) { | ||||
this.props.handleSelect(visibleComponents[selectedIdx + 1].key); | |||||
this.props.handleSelect(visibleComponents[selectedIdx + 1]); | |||||
} else { | } else { | ||||
this.props.handleSelect(visibleComponents[0].key); | |||||
this.props.handleSelect(visibleComponents[0]); | |||||
} | } | ||||
}; | }; | ||||
metric={this.props.metric} | metric={this.props.metric} | ||||
metrics={this.props.metrics} | metrics={this.props.metrics} | ||||
rootComponent={this.props.rootComponent} | rootComponent={this.props.rootComponent} | ||||
selectedComponent={this.props.selectedKey} | |||||
selectedComponent={this.props.selectedComponent} | |||||
view={this.props.view} | view={this.props.view} | ||||
/> | /> | ||||
{hidingBestMeasures && this.props.paging && ( | {hidingBestMeasures && this.props.paging && ( |
import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'; | import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'; | ||||
import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap'; | import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap'; | ||||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | import QualifierIcon from '../../../components/icons/QualifierIcon'; | ||||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||||
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; | import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; | ||||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | ||||
import { isDefined } from '../../../helpers/types'; | import { isDefined } from '../../../helpers/types'; | ||||
interface Props { | interface Props { | ||||
branchLike?: BranchLike; | branchLike?: BranchLike; | ||||
components: T.ComponentMeasureEnhanced[]; | components: T.ComponentMeasureEnhanced[]; | ||||
handleSelect: (component: string) => void; | |||||
handleSelect: (component: T.ComponentMeasureIntern) => void; | |||||
metric: T.Metric; | metric: T.Metric; | ||||
} | } | ||||
color: colorValue ? (colorScale as Function)(colorValue) : undefined, | color: colorValue ? (colorScale as Function)(colorValue) : undefined, | ||||
gradient: !colorValue ? NA_GRADIENT : undefined, | gradient: !colorValue ? NA_GRADIENT : undefined, | ||||
icon: <QualifierIcon fill={colors.baseFontColor} qualifier={component.qualifier} />, | 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, | size: sizeValue, | ||||
measureValue: colorValue, | measureValue: colorValue, | ||||
metric, | metric, | ||||
tooltip: this.getTooltip({ | tooltip: this.getTooltip({ | ||||
colorMetric: metric, | colorMetric: metric, | ||||
colorValue, | colorValue, | ||||
componentName: component.name, | |||||
component, | |||||
sizeMetric: sizeMeasure.metric, | sizeMetric: sizeMeasure.metric, | ||||
sizeValue | sizeValue | ||||
}) | |||||
}), | |||||
component | |||||
}; | }; | ||||
}) | }) | ||||
.filter(isDefined); | .filter(isDefined); | ||||
getTooltip = ({ | getTooltip = ({ | ||||
colorMetric, | colorMetric, | ||||
colorValue, | colorValue, | ||||
componentName, | |||||
component, | |||||
sizeMetric, | sizeMetric, | ||||
sizeValue | sizeValue | ||||
}: { | }: { | ||||
colorMetric: T.Metric; | colorMetric: T.Metric; | ||||
colorValue?: string; | colorValue?: string; | ||||
componentName: string; | |||||
component: T.ComponentMeasureEnhanced; | |||||
sizeMetric: T.Metric; | sizeMetric: T.Metric; | ||||
sizeValue: number; | sizeValue: number; | ||||
}) => { | }) => { | ||||
colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—'; | colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—'; | ||||
return ( | return ( | ||||
<div className="text-left"> | <div className="text-left"> | ||||
{componentName} | |||||
{[component.name, component.branch].filter(s => !!s).join(' / ')} | |||||
<br /> | <br /> | ||||
{`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`} | {`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`} | ||||
<br /> | <br /> |
*/ | */ | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like'; | |||||
import { Link } from 'react-router'; | |||||
import { | import { | ||||
mockComponentMeasure, | mockComponentMeasure, | ||||
mockComponentMeasureEnhanced | mockComponentMeasureEnhanced | ||||
it('should render correctly', () => { | it('should render correctly', () => { | ||||
expect(shallowRender()).toMatchSnapshot('default'); | 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( | expect( | ||||
shallowRender({ | 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( | 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({ | 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) { | function shallowRender(overrides: Partial<ComponentCellProps> = {}, metricKey = MetricKey.bugs) { | ||||
const metric = mockMetric({ key: metricKey }); | const metric = mockMetric({ key: metricKey }); |
// Jest Snapshot v1, https://goo.gl/fbAQLP | // 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 | <td | ||||
className="measure-details-component-cell" | className="measure-details-component-cell" | ||||
> | > | ||||
> | > | ||||
<Link | <Link | ||||
className="link-no-underline" | className="link-no-underline" | ||||
id="component-measures-component-link-foo:src/index.tsx" | |||||
id="component-measures-component-link-foo" | |||||
onlyActiveOnIndex={false} | onlyActiveOnIndex={false} | ||||
style={Object {}} | style={Object {}} | ||||
to={ | to={ | ||||
Object { | Object { | ||||
"pathname": "/component_measures", | "pathname": "/component_measures", | ||||
"query": Object { | "query": Object { | ||||
"branch": "develop", | |||||
"id": "foo", | "id": "foo", | ||||
"metric": "bugs", | "metric": "bugs", | ||||
"selected": "foo:src/index.tsx", | |||||
"selected": "foo", | |||||
"view": "list", | "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 | <span | ||||
title="foo" | title="foo" | ||||
> | > | ||||
<QualifierIcon | <QualifierIcon | ||||
className="little-spacer-right" | className="little-spacer-right" | ||||
qualifier="APP" | |||||
qualifier="TRK" | |||||
/> | /> | ||||
<span> | <span> | ||||
Foo | Foo | ||||
</span> | </span> | ||||
<BranchIcon | |||||
className="spacer-left little-spacer-right" | |||||
/> | |||||
<span | |||||
className="note" | |||||
> | |||||
develop | |||||
</span> | |||||
</span> | </span> | ||||
</Link> | </Link> | ||||
</div> | </div> | ||||
</td> | </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 | <td | ||||
className="measure-details-component-cell" | className="measure-details-component-cell" | ||||
> | > | ||||
Object { | Object { | ||||
"pathname": "/component_measures", | "pathname": "/component_measures", | ||||
"query": Object { | "query": Object { | ||||
"branch": "branch-6.7", | |||||
"id": "app-key", | |||||
"metric": "projects", | |||||
"id": "foo", | |||||
"metric": "bugs", | |||||
"selected": "foo", | |||||
"view": "list", | "view": "list", | ||||
}, | }, | ||||
} | } | ||||
} | } | ||||
> | > | ||||
<span | |||||
className="big-spacer-right" | |||||
> | |||||
<LinkIcon /> | |||||
</span> | |||||
<span | <span | ||||
title="foo" | title="foo" | ||||
> | > | ||||
<QualifierIcon | <QualifierIcon | ||||
className="little-spacer-right" | className="little-spacer-right" | ||||
qualifier="APP" | |||||
qualifier="TRK" | |||||
/> | /> | ||||
<span> | <span> | ||||
Foo | Foo | ||||
</span> | </span> | ||||
<span | |||||
className="spacer-left badge" | |||||
> | |||||
branches.main_branch | |||||
</span> | |||||
</span> | </span> | ||||
</Link> | </Link> | ||||
</div> | </div> | ||||
</td> | </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 | <td | ||||
className="measure-details-component-cell" | className="measure-details-component-cell" | ||||
> | > | ||||
Object { | Object { | ||||
"pathname": "/component_measures", | "pathname": "/component_measures", | ||||
"query": Object { | "query": Object { | ||||
"id": "vw-key", | |||||
"metric": "alert_status", | |||||
"pullRequest": "1001", | |||||
"branch": "develop", | |||||
"id": "foo", | |||||
"metric": "bugs", | |||||
"selected": "foo", | |||||
"view": "list", | "view": "list", | ||||
}, | }, | ||||
} | } | ||||
} | } | ||||
> | > | ||||
<span | |||||
className="big-spacer-right" | |||||
> | |||||
<LinkIcon /> | |||||
</span> | |||||
<span | <span | ||||
title="foo" | title="foo" | ||||
> | > | ||||
<QualifierIcon | <QualifierIcon | ||||
className="little-spacer-right" | className="little-spacer-right" | ||||
qualifier="VW" | |||||
qualifier="TRK" | |||||
/> | /> | ||||
<span> | <span> | ||||
Foo | Foo | ||||
</td> | </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 | <td | ||||
className="measure-details-component-cell" | className="measure-details-component-cell" | ||||
> | > | ||||
Object { | Object { | ||||
"pathname": "/component_measures", | "pathname": "/component_measures", | ||||
"query": Object { | "query": Object { | ||||
"id": "project-key", | |||||
"id": "foo", | |||||
"metric": "bugs", | "metric": "bugs", | ||||
"selected": "foo", | |||||
"view": "list", | "view": "list", | ||||
}, | }, | ||||
} | } | ||||
} | } | ||||
> | > | ||||
<span | |||||
className="big-spacer-right" | |||||
> | |||||
<LinkIcon /> | |||||
</span> | |||||
<span | <span | ||||
title="foo" | title="foo" | ||||
> | > | ||||
</td> | </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 | <td | ||||
className="measure-details-component-cell" | className="measure-details-component-cell" | ||||
> | > | ||||
style={Object {}} | style={Object {}} | ||||
to={ | to={ | ||||
Object { | Object { | ||||
"pathname": "/dashboard", | |||||
"pathname": "/component_measures", | |||||
"query": Object { | "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 | <span | ||||
title="foo" | title="foo" | ||||
> | > | ||||
<span> | <span> | ||||
Foo | Foo | ||||
</span> | </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> | ||||
</span> | </span> | ||||
</Link> | </Link> | ||||
</td> | </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 | <td | ||||
className="measure-details-component-cell" | className="measure-details-component-cell" | ||||
> | > | ||||
<span> | <span> | ||||
Foo | Foo | ||||
</span> | </span> | ||||
<BranchIcon | |||||
className="spacer-left little-spacer-right" | |||||
/> | |||||
<span | <span | ||||
className="note" | |||||
className="spacer-left badge" | |||||
> | > | ||||
develop | |||||
branches.main_branch | |||||
</span> | </span> | ||||
</span> | </span> | ||||
</Link> | </Link> | ||||
</td> | </td> | ||||
`; | `; | ||||
exports[`should render correctly: root component is application, component is on main branch 1`] = ` | |||||
exports[`should render correctly: default 1`] = ` | |||||
<td | <td | ||||
className="measure-details-component-cell" | className="measure-details-component-cell" | ||||
> | > | ||||
<span> | <span> | ||||
index.tsx | index.tsx | ||||
</span> | </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> | </span> | ||||
</Link> | </Link> | ||||
</div> | </div> |
return { ...component, value, leak, measures: enhancedMeasures }; | 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 { | export function isSecurityReviewMetric(metricKey: MetricKey | string): boolean { | ||||
return [ | return [ | ||||
MetricKey.security_hotspots, | MetricKey.security_hotspots, |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import BranchIcon from '../../../components/icons/BranchIcon'; | |||||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | import QualifierIcon from '../../../components/icons/QualifierIcon'; | ||||
import { translateWithParameters } from '../../../helpers/l10n'; | import { translateWithParameters } from '../../../helpers/l10n'; | ||||
import { collapsePath, limitComponentName } from '../../../helpers/path'; | import { collapsePath, limitComponentName } from '../../../helpers/path'; | ||||
import { ComponentQualifier } from '../../../types/component'; | |||||
import { getSelectedLocation } from '../utils'; | import { getSelectedLocation } from '../utils'; | ||||
interface Props { | interface Props { | ||||
selectedFlowIndex, | selectedFlowIndex, | ||||
selectedLocationIndex | selectedLocationIndex | ||||
}: Props) { | }: 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 selectedLocation = getSelectedLocation(issue, selectedFlowIndex, selectedLocationIndex); | ||||
const componentName = selectedLocation ? selectedLocation.componentName : issue.componentLongName; | const componentName = selectedLocation ? selectedLocation.componentName : issue.componentLongName; | ||||
const projectName = [issue.projectName, issue.branch].filter(s => !!s).join(' - '); | |||||
return ( | return ( | ||||
<div | <div | ||||
<QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} /> | <QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} /> | ||||
{displayProject && ( | {displayProject && ( | ||||
<span title={issue.projectName}> | |||||
<span title={projectName}> | |||||
{limitComponentName(issue.projectName)} | {limitComponentName(issue.projectName)} | ||||
{issue.branch && ( | |||||
<> | |||||
{' - '} | |||||
<BranchIcon /> | |||||
<span>{issue.branch}</span> | |||||
</> | |||||
)} | |||||
<span className="slash-separator" /> | <span className="slash-separator" /> | ||||
</span> | </span> | ||||
)} | )} |
render() { | render() { | ||||
const { branchLike, component, issue, previousIssue } = this.props; | 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 ( | return ( | ||||
<li className="issues-workspace-list-item"> | <li className="issues-workspace-list-item"> |
componentLongName: 'comp-name', | componentLongName: 'comp-name', | ||||
componentQualifier: ComponentQualifier.File, | componentQualifier: ComponentQualifier.File, | ||||
project: 'proj', | project: 'proj', | ||||
projectName: 'proj-name' | |||||
projectName: 'proj-name', | |||||
branch: 'test-branch' | |||||
}); | }); | ||||
it('renders', () => { | it('renders', () => { |
/* | |||||
* 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} | |||||
/> | |||||
); | |||||
} |
qualifier="FIL" | qualifier="FIL" | ||||
/> | /> | ||||
<span | <span | ||||
title="proj-name" | |||||
title="proj-name - test-branch" | |||||
> | > | ||||
proj-name | proj-name | ||||
- | |||||
<BranchIcon /> | |||||
<span> | |||||
test-branch | |||||
</span> | |||||
<span | <span | ||||
className="slash-separator" | className="slash-separator" | ||||
/> | /> | ||||
qualifier="FIL" | qualifier="FIL" | ||||
/> | /> | ||||
<span | <span | ||||
title="proj-name" | |||||
title="proj-name - test-branch" | |||||
> | > | ||||
proj-name | proj-name | ||||
- | |||||
<BranchIcon /> | |||||
<span> | |||||
test-branch | |||||
</span> | |||||
<span | <span | ||||
className="slash-separator" | className="slash-separator" | ||||
/> | /> |
// 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> | |||||
`; |
defaultMessage={translate('portfolio.x_in_y')} | defaultMessage={translate('portfolio.x_in_y')} | ||||
id="portfolio.x_in_y" | id="portfolio.x_in_y" | ||||
values={{ | values={{ | ||||
projects: ( | |||||
project_branches: ( | |||||
<Link | <Link | ||||
to={getComponentDrilldownUrl({ | to={getComponentDrilldownUrl({ | ||||
componentKey: component, | componentKey: component, | ||||
value={String(effort.projects)} | value={String(effort.projects)} | ||||
/> | /> | ||||
{effort.projects === 1 | {effort.projects === 1 | ||||
? translate('project_singular') | |||||
: translate('project_plural')} | |||||
? translate('portfolio.project_branch') | |||||
: translate('portfolio.project_branches')} | |||||
</span> | </span> | ||||
</Link> | </Link> | ||||
), | ), |
{metricKey === 'releasability' | {metricKey === 'releasability' | ||||
? Number(effort) > 0 && ( | ? Number(effort) > 0 && ( | ||||
<> | <> | ||||
<h3>{translate('portfolio.lowest_rated_projects')}</h3> | |||||
<h3>{translate('portfolio.lowest_rated_project_branches')}</h3> | |||||
<div className="portfolio-effort"> | <div className="portfolio-effort"> | ||||
<Link | <Link | ||||
to={getComponentDrilldownUrl({ | to={getComponentDrilldownUrl({ | ||||
value={effort} | value={effort} | ||||
/> | /> | ||||
{Number(effort) === 1 | {Number(effort) === 1 | ||||
? translate('project_singular') | |||||
: translate('project_plural')} | |||||
? translate('portfolio.project_branch') | |||||
: translate('portfolio.project_branches')} | |||||
</span> | </span> | ||||
</Link> | </Link> | ||||
<Level | <Level | ||||
) | ) | ||||
: effort && ( | : effort && ( | ||||
<> | <> | ||||
<h3>{translate('portfolio.lowest_rated_projects')}</h3> | |||||
<h3>{translate('portfolio.lowest_rated_project_branches')}</h3> | |||||
<Effort component={component} effort={effort} metricKey={keys.rating} /> | <Effort component={component} effort={effort} metricKey={keys.rating} /> | ||||
</> | </> | ||||
)} | )} |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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 * as React from 'react'; | ||||
import { Link } from 'react-router'; | import { Link } from 'react-router'; | ||||
import { colors } from '../../../app/theme'; | import { colors } from '../../../app/theme'; | ||||
import BranchIcon from '../../../components/icons/BranchIcon'; | |||||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | import QualifierIcon from '../../../components/icons/QualifierIcon'; | ||||
import Measure from '../../../components/measure/Measure'; | import Measure from '../../../components/measure/Measure'; | ||||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | import { translate, translateWithParameters } from '../../../helpers/l10n'; | ||||
const projectsPageUrl = { pathname: '/code', query: { id: component } }; | const projectsPageUrl = { pathname: '/code', query: { id: component } }; | ||||
const subCompList = sortBy( | |||||
subComponents, | |||||
c => c.qualifier, | |||||
c => c.name.toLowerCase(), | |||||
c => c.branch?.toLowerCase() | |||||
); | |||||
return ( | return ( | ||||
<div className="panel panel-white portfolio-sub-components" id="portfolio-sub-components"> | <div className="panel panel-white portfolio-sub-components" id="portfolio-sub-components"> | ||||
<table className="data zebra"> | <table className="data zebra"> | ||||
</tr> | </tr> | ||||
</thead> | </thead> | ||||
<tbody> | <tbody> | ||||
{subComponents.map(component => ( | |||||
<tr key={component.key}> | |||||
{subCompList.map(comp => ( | |||||
<tr key={[comp.key, comp.branch].filter(s => !!s).join('/')}> | |||||
<td> | <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> | </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> | </tr> | ||||
))} | ))} | ||||
</tbody> | </tbody> |
*/ | */ | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { ComponentQualifier } from '../../../../types/component'; | |||||
import WorstProjects from '../WorstProjects'; | import WorstProjects from '../WorstProjects'; | ||||
it('renders', () => { | it('renders', () => { | ||||
ncloc: '200' | ncloc: '200' | ||||
}, | }, | ||||
name: 'Foo', | 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', | key: 'bar', | ||||
ncloc: '100' | ncloc: '100' | ||||
}, | }, | ||||
name: 'Bar', | name: 'Bar', | ||||
qualifier: 'TRK', | |||||
qualifier: ComponentQualifier.Project, | |||||
refKey: 'barbar' | refKey: 'barbar' | ||||
}, | }, | ||||
{ | { | ||||
ncloc: '150' | ncloc: '150' | ||||
}, | }, | ||||
name: 'Baz', | name: 'Baz', | ||||
qualifier: 'TRK', | |||||
qualifier: ComponentQualifier.Project, | |||||
refKey: 'bazbaz' | refKey: 'bazbaz' | ||||
} | } | ||||
]; | ]; |
id="portfolio.x_in_y" | id="portfolio.x_in_y" | ||||
values={ | values={ | ||||
Object { | Object { | ||||
"projects": <Link | |||||
"project_branches": <Link | |||||
onlyActiveOnIndex={false} | onlyActiveOnIndex={false} | ||||
style={Object {}} | style={Object {}} | ||||
to={ | to={ | ||||
metricType="SHORT_INT" | metricType="SHORT_INT" | ||||
value="3" | value="3" | ||||
/> | /> | ||||
project_plural | |||||
portfolio.project_branches | |||||
</span> | </span> | ||||
</Link>, | </Link>, | ||||
"rating": <Rating | "rating": <Rating |
rating="3" | rating="3" | ||||
/> | /> | ||||
<h3> | <h3> | ||||
portfolio.lowest_rated_projects | |||||
portfolio.lowest_rated_project_branches | |||||
</h3> | </h3> | ||||
<Effort | <Effort | ||||
component="foo" | component="foo" | ||||
rating="2" | rating="2" | ||||
/> | /> | ||||
<h3> | <h3> | ||||
portfolio.lowest_rated_projects | |||||
portfolio.lowest_rated_project_branches | |||||
</h3> | </h3> | ||||
<div | <div | ||||
className="portfolio-effort" | className="portfolio-effort" | ||||
metricType="SHORT_INT" | metricType="SHORT_INT" | ||||
value={5} | value={5} | ||||
/> | /> | ||||
project_plural | |||||
portfolio.project_branches | |||||
</span> | </span> | ||||
</Link> | </Link> | ||||
<Level | <Level | ||||
rating="2" | rating="2" | ||||
/> | /> | ||||
<h3> | <h3> | ||||
portfolio.lowest_rated_projects | |||||
portfolio.lowest_rated_project_branches | |||||
</h3> | </h3> | ||||
<div | <div | ||||
className="portfolio-effort" | className="portfolio-effort" | ||||
metricType="SHORT_INT" | metricType="SHORT_INT" | ||||
value={1} | value={1} | ||||
/> | /> | ||||
project_singular | |||||
portfolio.project_branch | |||||
</span> | </span> | ||||
</Link> | </Link> | ||||
<Level | <Level |
</thead> | </thead> | ||||
<tbody> | <tbody> | ||||
<tr | <tr | ||||
key="foo" | |||||
key="foo_app" | |||||
> | > | ||||
<td> | <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 | <QualifierIcon | ||||
className="spacer-right" | |||||
qualifier="SVW" | qualifier="SVW" | ||||
/> | /> | ||||
Foo | |||||
</Link> | |||||
<Link | |||||
onlyActiveOnIndex={false} | |||||
style={Object {}} | |||||
to={ | |||||
Object { | |||||
"pathname": "/portfolio", | |||||
"query": Object { | |||||
"id": "foo", | |||||
}, | |||||
} | |||||
} | |||||
> | |||||
Foo | |||||
</Link> | |||||
</span> | |||||
</td> | </td> | ||||
<td | <td | ||||
className="text-center" | className="text-center" | ||||
</td> | </td> | ||||
</tr> | </tr> | ||||
<tr | <tr | ||||
key="bar" | |||||
key="bar/branch-1" | |||||
> | > | ||||
<td> | <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 | <QualifierIcon | ||||
className="spacer-right" | |||||
qualifier="TRK" | 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> | ||||
<td | <td | ||||
className="text-center" | className="text-center" | ||||
key="baz" | key="baz" | ||||
> | > | ||||
<td> | <td> | ||||
<Link | |||||
className="link-with-icon" | |||||
onlyActiveOnIndex={false} | |||||
style={Object {}} | |||||
to={ | |||||
Object { | |||||
"pathname": "/dashboard", | |||||
"query": Object { | |||||
"id": "bazbaz", | |||||
}, | |||||
} | |||||
} | |||||
<span | |||||
className="display-flex-center" | |||||
> | > | ||||
<QualifierIcon | <QualifierIcon | ||||
className="spacer-right" | |||||
qualifier="TRK" | 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> | ||||
<td | <td | ||||
className="text-center" | className="text-center" |
name: string; | name: string; | ||||
refKey?: string; | refKey?: string; | ||||
qualifier: string; | qualifier: string; | ||||
branch?: string; | |||||
} | } |
metric?: { key: string; type: string }; | metric?: { key: string; type: string }; | ||||
size: number; | size: number; | ||||
tooltip?: React.ReactNode; | tooltip?: React.ReactNode; | ||||
component: T.ComponentMeasureEnhanced; | |||||
} | } | ||||
interface HierarchicalTreemapItem extends TreeMapItem { | interface HierarchicalTreemapItem extends TreeMapItem { | ||||
interface Props { | interface Props { | ||||
height: number; | height: number; | ||||
items: TreeMapItem[]; | items: TreeMapItem[]; | ||||
onRectangleClick?: (item: string) => void; | |||||
onRectangleClick?: (item: T.ComponentMeasureEnhanced) => void; | |||||
width: number; | width: number; | ||||
} | } | ||||
return prefix.substr(0, prefix.length - lastPrefixPart.length); | return prefix.substr(0, prefix.length - lastPrefixPart.length); | ||||
}; | }; | ||||
handleClick = (component: T.ComponentMeasureEnhanced) => { | |||||
if (this.props.onRectangleClick) { | |||||
this.props.onRectangleClick(component); | |||||
} | |||||
}; | |||||
render() { | render() { | ||||
const { items, height, width } = this.props; | const { items, height, width } = this.props; | ||||
const hierarchy = d3Hierarchy({ children: items } as HierarchicalTreemapItem) | const hierarchy = d3Hierarchy({ children: items } as HierarchicalTreemapItem) | ||||
key={node.data.key} | key={node.data.key} | ||||
label={node.data.label} | label={node.data.label} | ||||
link={node.data.link} | link={node.data.link} | ||||
onClick={this.props.onRectangleClick} | |||||
onClick={() => this.handleClick(node.data.component)} | |||||
placement={node.x0 === 0 || node.x1 < halfWidth ? 'right' : 'left'} | placement={node.x0 === 0 || node.x1 < halfWidth ? 'right' : 'left'} | ||||
prefix={prefix} | prefix={prefix} | ||||
value={ | value={ |
*/ | */ | ||||
import { mount } from 'enzyme'; | import { mount } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component'; | |||||
import TreeMap from '../TreeMap'; | import TreeMap from '../TreeMap'; | ||||
import TreeMapRect from '../TreeMapRect'; | import TreeMapRect from '../TreeMapRect'; | ||||
it('should render correctly', () => { | it('should render correctly', () => { | ||||
const items = [ | 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', | key: '3', | ||||
size: 20, | size: 20, | ||||
gradient: '#777', | gradient: '#777', | ||||
label: 'SonarQube :: Search', | label: 'SonarQube :: Search', | ||||
metric: { key: 'coverage', type: 'PERCENT' } | |||||
metric: { key: 'coverage', type: 'PERCENT' }, | |||||
component: mockComponentMeasureEnhanced() | |||||
} | } | ||||
]; | ]; | ||||
const onRectClick = jest.fn(); | const onRectClick = jest.fn(); | ||||
expect(event.stopPropagation).toHaveBeenCalled(); | expect(event.stopPropagation).toHaveBeenCalled(); | ||||
(rects.first().instance() as TreeMapRect).handleRectClick(); | (rects.first().instance() as TreeMapRect).handleRectClick(); | ||||
expect(onRectClick).toHaveBeenCalledWith('2'); | |||||
expect(onRectClick).toHaveBeenCalledWith(expect.objectContaining({ key: 'foo' })); | |||||
}); | }); |
render() { | render() { | ||||
return ( | return ( | ||||
<li className={classNames({ 'select-list-list-disabled': this.props.disabled })}> | |||||
<li | |||||
className={classNames({ | |||||
'select-list-list-disabled': this.props.disabled | |||||
})}> | |||||
<Checkbox | <Checkbox | ||||
checked={this.props.selected} | 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} | disabled={this.props.disabled} | ||||
loading={this.state.loading} | loading={this.state.loading} | ||||
onCheck={this.handleCheck}> | 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> | </Checkbox> | ||||
</li> | </li> | ||||
); | ); |
> | > | ||||
<Checkbox | <Checkbox | ||||
checked={false} | checked={false} | ||||
className="select-list-list-checkbox" | |||||
className="select-list-list-checkbox display-flex-center" | |||||
loading={false} | loading={false} | ||||
onCheck={[Function]} | onCheck={[Function]} | ||||
thirdState={false} | thirdState={false} | ||||
> | > | ||||
<span | <span | ||||
className="little-spacer-left" | |||||
className="little-spacer-left flex-1" | |||||
> | > | ||||
foo | foo | ||||
</span> | </span> | ||||
> | > | ||||
<Checkbox | <Checkbox | ||||
checked={false} | checked={false} | ||||
className="select-list-list-checkbox" | |||||
className="select-list-list-checkbox display-flex-center" | |||||
loading={true} | loading={true} | ||||
onCheck={[Function]} | onCheck={[Function]} | ||||
thirdState={false} | thirdState={false} | ||||
> | > | ||||
<span | <span | ||||
className="little-spacer-left" | |||||
className="little-spacer-left flex-1" | |||||
> | > | ||||
foo | foo | ||||
</span> | </span> |
import key from 'keymaster'; | import key from 'keymaster'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import PageActions from '../../components/ui/PageActions'; | import PageActions from '../../components/ui/PageActions'; | ||||
import { getComponentMeasureUniqueKey } from '../../helpers/component'; | |||||
import { getWrappedDisplayName } from './utils'; | import { getWrappedDisplayName } from './utils'; | ||||
export interface WithKeyboardNavigationProps { | export interface WithKeyboardNavigationProps { | ||||
getCurrentIndex = () => { | getCurrentIndex = () => { | ||||
const { selected, components = [] } = this.props; | 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) => { | skipIfFile = (handler: () => void) => { |
/* | |||||
* 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; | |||||
} |
// 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, | |||||
} | |||||
`; |
/* | |||||
* 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(); | |||||
} | |||||
); |
): componentQualifier is ComponentQualifier.Project { | ): componentQualifier is ComponentQualifier.Project { | ||||
return componentQualifier === 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); | |||||
} |
name: string; | name: string; | ||||
} | } | ||||
interface ComponentMeasureIntern { | |||||
export interface ComponentMeasureIntern { | |||||
branch?: string; | branch?: string; | ||||
description?: string; | description?: string; | ||||
isFavorite?: boolean; | isFavorite?: boolean; |
projects=Projects | projects=Projects | ||||
projects_=project(s) | projects_=project(s) | ||||
x_projects_={0} project(s) | x_projects_={0} project(s) | ||||
project_singular=project | |||||
project_plural=projects | project_plural=projects | ||||
projects_management=Projects Management | projects_management=Projects Management | ||||
quality_profile=Quality Profile | quality_profile=Quality Profile | ||||
metric.profile.name=Profile | metric.profile.name=Profile | ||||
metric.profile_version.description=Selected Quality Profile version | metric.profile_version.description=Selected Quality Profile version | ||||
metric.profile_version.name=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.description=Public API | ||||
metric.public_api.name=Public API | metric.public_api.name=Public API | ||||
metric.public_documented_api_density.description=Public documented classes and functions balanced by ncloc | metric.public_documented_api_density.description=Public documented classes and functions balanced by ncloc | ||||
#------------------------------------------------------------------------------ | #------------------------------------------------------------------------------ | ||||
portfolio.has_always_been_x=has always been {rating} | portfolio.has_always_been_x=has always been {rating} | ||||
portfolio.was_x_y=was {rating} {date} | 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.has_qg_status=Has Quality Gate Status | ||||
portfolio.have_qg_status=Have Quality Gate Status | portfolio.have_qg_status=Have Quality Gate Status | ||||
portfolio.empty=This portfolio is empty. | portfolio.empty=This portfolio is empty. | ||||
portfolio.app.empty=This application is empty. | portfolio.app.empty=This application is empty. | ||||
portfolio.app.no_lines_of_code=All projects in this application are empty | portfolio.app.no_lines_of_code=All projects in this application are empty | ||||
portfolio.metric_trend=Metric trend | 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.health_factors=Portfolio health factors | ||||
portfolio.activity_link=Activity | portfolio.activity_link=Activity | ||||
portfolio.measures_link=Measures | portfolio.measures_link=Measures | ||||
portfolio.language_breakdown_link=Language breakdown | portfolio.language_breakdown_link=Language breakdown | ||||
portfolio.breakdown=Portfolio 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.number_of_lines=Number of lines of code | ||||
portfolio.metric_domain.vulnerabilities=Security Vulnerabilities | portfolio.metric_domain.vulnerabilities=Security Vulnerabilities |
/** | /** | ||||
* @since 3.0 | * @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) | .setDirection(Metric.DIRECTION_WORST) | ||||
.setQualitative(false) | .setQualitative(false) | ||||
.setDomain(DOMAIN_SIZE) | .setDomain(DOMAIN_SIZE) |